#!/usr/bin/env php
<?php
/**
 * Generated by Box.
 *
 * @link https://github.com/herrera-io/php-box/
 */
if (class_exists('Phar')) {
Phar::mapPhar('default.phar');
require 'phar://' . __FILE__ . '/bin/cscart-sdk';
}
__HALT_COMPILER(); ?>
f               <   vendor/psr/event-dispatcher/src/EventDispatcherInterface.php        'a      ;   vendor/psr/event-dispatcher/src/StoppableEventInterface.php              =   vendor/psr/event-dispatcher/src/ListenerProviderInterface.php        e      /   vendor/psr/container/src/ContainerInterface.php              7   vendor/psr/container/src/NotFoundExceptionInterface.php          >悴      8   vendor/psr/container/src/ContainerExceptionInterface.php          ^33      1   vendor/cscart/upgrade-builder/tests/bootstrap.php8       8         7   vendor/cscart/upgrade-builder/tests/TarArchiverTest.php        A      [   vendor/cscart/upgrade-builder/tests/data/upgrade/migrations/62312332434_test_migration1.php                [   vendor/cscart/upgrade-builder/tests/data/upgrade/migrations/62312332434_test_migration2.php                Z   vendor/cscart/upgrade-builder/tests/data/upgrade/migrations/62312332434_mve_migration3.php                J   vendor/cscart/upgrade-builder/tests/data/upgrade/validators/validator1.php                J   vendor/cscart/upgrade-builder/tests/data/upgrade/validators/validator2.php                G   vendor/cscart/upgrade-builder/tests/data/upgrade/scripts/pre_script.php$       $   mϴ      H   vendor/cscart/upgrade-builder/tests/data/upgrade/scripts/post_script.php%       %   ؛˴      @   vendor/cscart/upgrade-builder/tests/data/upgrade/extra/extra.php'       '   rKhC      E   vendor/cscart/upgrade-builder/tests/data/upgrade/extra_files/test.php                      7   vendor/cscart/upgrade-builder/tests/ZipArchiverTest.php        Z[      8   vendor/cscart/upgrade-builder/tests/AddonBuilderTest.php        (˕      >   vendor/cscart/upgrade-builder/tests/CoreReleaseArchiveTest.php        u      3   vendor/cscart/upgrade-builder/tests/BuilderTest.php        l      :   vendor/cscart/upgrade-builder/tests/UpgradePackageTest.php              ?   vendor/cscart/upgrade-builder/tests/AddonReleaseArchiveTest.php         j/      7   vendor/cscart/upgrade-builder/tests/CoreBuilderTest.php4      4  K       ,   vendor/cscart/upgrade-builder/autoloader.php        I      =   vendor/cscart/upgrade-builder/src/UpgradePackageInterface.php        ߴ      1   vendor/cscart/upgrade-builder/src/TarArchiver.php        2ߚ      1   vendor/cscart/upgrade-builder/src/ZipArchiver.php        <      8   vendor/cscart/upgrade-builder/src/CoreReleaseArchive.php        N^      9   vendor/cscart/upgrade-builder/src/AddonReleaseArchive.php3      3  ᘴ      1   vendor/cscart/upgrade-builder/src/CoreBuilder.php        lg      7   vendor/cscart/upgrade-builder/src/ArchiverInterface.php        rq<      2   vendor/cscart/upgrade-builder/src/AddonBuilder.phpu      u  ;      =   vendor/cscart/upgrade-builder/src/ReleaseArchiveInterface.php        sj      -   vendor/cscart/upgrade-builder/src/Builder.phpO      O  ^/R      4   vendor/cscart/upgrade-builder/src/ReleaseArchive.php&      &  E       4   vendor/cscart/upgrade-builder/src/UpgradePackage.php        wN>      "   vendor/composer/autoload_files.php        l      %   vendor/composer/InstalledVersions.phpPB      PB  )|         vendor/composer/installed.phpH(      H(  Ŵ      "   vendor/composer/platform_check.php        4      #   vendor/composer/autoload_static.php        j      !   vendor/composer/autoload_real.php        K摴      !   vendor/composer/autoload_psr4.php        &         vendor/composer/ClassLoader.php?      ?  2@u      %   vendor/composer/autoload_classmap.phpC      C  r      '   vendor/composer/autoload_namespaces.php          /t         vendor/autoload.php        ࢬ      *   vendor/doctrine/dbal/bin/doctrine-dbal.php        \      >   vendor/doctrine/dbal/lib/Doctrine/DBAL/Logging/LoggerChain.php        Ԡ      <   vendor/doctrine/dbal/lib/Doctrine/DBAL/Logging/SQLLogger.php*      *        =   vendor/doctrine/dbal/lib/Doctrine/DBAL/Logging/DebugStack.php        7      @   vendor/doctrine/dbal/lib/Doctrine/DBAL/Logging/EchoSQLLogger.php        J      L   vendor/doctrine/dbal/lib/Doctrine/DBAL/Connections/MasterSlaveConnection.php
      
  
      S   vendor/doctrine/dbal/lib/Doctrine/DBAL/Connections/PrimaryReadReplicaConnection.php1      1        I   vendor/doctrine/dbal/lib/Doctrine/DBAL/Event/Listeners/SQLSessionInit.php        0|Bx      L   vendor/doctrine/dbal/lib/Doctrine/DBAL/Event/Listeners/OracleSessionInit.php        :      K   vendor/doctrine/dbal/lib/Doctrine/DBAL/Event/Listeners/MysqlSessionInit.phpv      v  ^      V   vendor/doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaAlterTableChangeColumnEventArgs.php        9N      Q   vendor/doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaCreateTableColumnEventArgs.php        p      D   vendor/doctrine/dbal/lib/Doctrine/DBAL/Event/ConnectionEventArgs.php	      	        S   vendor/doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaAlterTableAddColumnEventArgs.php\      \  v%      @   vendor/doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaEventArgs.php         Cϸ      I   vendor/doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaDropTableEventArgs.php        )      J   vendor/doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaAlterTableEventArgs.php        ݰǴ      O   vendor/doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaIndexDefinitionEventArgs.php              V   vendor/doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaAlterTableRenameColumnEventArgs.php        ޲V      P   vendor/doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaColumnDefinitionEventArgs.php
      
  nn      V   vendor/doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaAlterTableRemoveColumnEventArgs.php        TM      K   vendor/doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaCreateTableEventArgs.php        ="      <   vendor/doctrine/dbal/lib/Doctrine/DBAL/Schema/SchemaDiff.php        K@      7   vendor/doctrine/dbal/lib/Doctrine/DBAL/Schema/Index.php"      "         <   vendor/doctrine/dbal/lib/Doctrine/DBAL/Schema/ColumnDiff.php        `z      >   vendor/doctrine/dbal/lib/Doctrine/DBAL/Schema/SchemaConfig.phpp      p        F   vendor/doctrine/dbal/lib/Doctrine/DBAL/Schema/DrizzleSchemaManager.php2      2  N      E   vendor/doctrine/dbal/lib/Doctrine/DBAL/Schema/OracleSchemaManager.php;1      ;1  nܴ      H   vendor/doctrine/dbal/lib/Doctrine/DBAL/Schema/SQLServerSchemaManager.php)      )  E      6   vendor/doctrine/dbal/lib/Doctrine/DBAL/Schema/View.php        ~.      B   vendor/doctrine/dbal/lib/Doctrine/DBAL/Schema/DB2SchemaManager.php        R]      E   vendor/doctrine/dbal/lib/Doctrine/DBAL/Schema/SqliteSchemaManager.phpA      A  >vg      8   vendor/doctrine/dbal/lib/Doctrine/DBAL/Schema/Schema.phpD.      D.  d~      Y   vendor/doctrine/dbal/lib/Doctrine/DBAL/Schema/Synchronizer/AbstractSchemaSynchronizer.php        ]yf      Q   vendor/doctrine/dbal/lib/Doctrine/DBAL/Schema/Synchronizer/SchemaSynchronizer.phpD      D        Y   vendor/doctrine/dbal/lib/Doctrine/DBAL/Schema/Synchronizer/SingleDatabaseSynchronizer.php        Yb      ?   vendor/doctrine/dbal/lib/Doctrine/DBAL/Schema/AbstractAsset.phpF      F  	ޒ      <   vendor/doctrine/dbal/lib/Doctrine/DBAL/Schema/Comparator.phpM      M  ő      ;   vendor/doctrine/dbal/lib/Doctrine/DBAL/Schema/TableDiff.phpq      q  )݈      F   vendor/doctrine/dbal/lib/Doctrine/DBAL/Schema/ForeignKeyConstraint.phpN*      N*  isȴ      D   vendor/doctrine/dbal/lib/Doctrine/DBAL/Schema/MySqlSchemaManager.php,      ,  i      A   vendor/doctrine/dbal/lib/Doctrine/DBAL/Schema/SchemaException.php        U      I   vendor/doctrine/dbal/lib/Doctrine/DBAL/Schema/PostgreSqlSchemaManager.php@      @        :   vendor/doctrine/dbal/lib/Doctrine/DBAL/Schema/Sequence.php        0      <   vendor/doctrine/dbal/lib/Doctrine/DBAL/Schema/Constraint.php        z;c      7   vendor/doctrine/dbal/lib/Doctrine/DBAL/Schema/Table.php]      ]  ;$      G   vendor/doctrine/dbal/lib/Doctrine/DBAL/Schema/AbstractSchemaManager.phpu      u  v'      <   vendor/doctrine/dbal/lib/Doctrine/DBAL/Schema/Identifier.php        J1      J   vendor/doctrine/dbal/lib/Doctrine/DBAL/Schema/SQLAnywhereSchemaManager.php        9      8   vendor/doctrine/dbal/lib/Doctrine/DBAL/Schema/Column.php%!      %!  w      R   vendor/doctrine/dbal/lib/Doctrine/DBAL/Schema/Visitor/CreateSchemaSqlCollector.phpW	      W	  ;8      I   vendor/doctrine/dbal/lib/Doctrine/DBAL/Schema/Visitor/AbstractVisitor.php        a<R      K   vendor/doctrine/dbal/lib/Doctrine/DBAL/Schema/Visitor/SchemaDiffVisitor.php        մ      B   vendor/doctrine/dbal/lib/Doctrine/DBAL/Schema/Visitor/Graphviz.php        tr8      J   vendor/doctrine/dbal/lib/Doctrine/DBAL/Schema/Visitor/NamespaceVisitor.php\      \  1r      P   vendor/doctrine/dbal/lib/Doctrine/DBAL/Schema/Visitor/DropSchemaSqlCollector.php
      
  ^      P   vendor/doctrine/dbal/lib/Doctrine/DBAL/Schema/Visitor/RemoveNamespacedAssets.php=	      =	  2      A   vendor/doctrine/dbal/lib/Doctrine/DBAL/Schema/Visitor/Visitor.php        󲺴      I   vendor/doctrine/dbal/lib/Doctrine/DBAL/Exception/TableExistsException.php          d2      N   vendor/doctrine/dbal/lib/Doctrine/DBAL/Exception/InvalidFieldNameException.php          fRT4      R   vendor/doctrine/dbal/lib/Doctrine/DBAL/Exception/DatabaseObjectExistsException.php        %D      F   vendor/doctrine/dbal/lib/Doctrine/DBAL/Exception/ReadOnlyException.php          acD      [   vendor/doctrine/dbal/lib/Doctrine/DBAL/Exception/ForeignKeyConstraintViolationException.php          rC      F   vendor/doctrine/dbal/lib/Doctrine/DBAL/Exception/DeadlockException.php          {D      T   vendor/doctrine/dbal/lib/Doctrine/DBAL/Exception/DatabaseObjectNotFoundException.php        "q      H   vendor/doctrine/dbal/lib/Doctrine/DBAL/Exception/ConnectionException.php          B=      W   vendor/doctrine/dbal/lib/Doctrine/DBAL/Exception/UniqueConstraintViolationException.php                X   vendor/doctrine/dbal/lib/Doctrine/DBAL/Exception/NotNullConstraintViolationException.php          Chu      Q   vendor/doctrine/dbal/lib/Doctrine/DBAL/Exception/ConstraintViolationException.php          A<      C   vendor/doctrine/dbal/lib/Doctrine/DBAL/Exception/ConnectionLost.php          DI      D   vendor/doctrine/dbal/lib/Doctrine/DBAL/Exception/DriverException.phpr      r  ;      K   vendor/doctrine/dbal/lib/Doctrine/DBAL/Exception/TableNotFoundException.php          cuZc      P   vendor/doctrine/dbal/lib/Doctrine/DBAL/Exception/NonUniqueFieldNameException.php          E;I      M   vendor/doctrine/dbal/lib/Doctrine/DBAL/Exception/LockWaitTimeoutException.php          @~\´      G   vendor/doctrine/dbal/lib/Doctrine/DBAL/Exception/RetryableException.php          dZP      ?   vendor/doctrine/dbal/lib/Doctrine/DBAL/Exception/NoKeyValue.php        u4      I   vendor/doctrine/dbal/lib/Doctrine/DBAL/Exception/SyntaxErrorException.php                D   vendor/doctrine/dbal/lib/Doctrine/DBAL/Exception/ServerException.php          s      M   vendor/doctrine/dbal/lib/Doctrine/DBAL/Exception/InvalidArgumentException.php        S      4   vendor/doctrine/dbal/lib/Doctrine/DBAL/Statement.phpEZ      EZ  貴      1   vendor/doctrine/dbal/lib/Doctrine/DBAL/Events.phpj      j  x	      4   vendor/doctrine/dbal/lib/Doctrine/DBAL/FetchMode.php        =˼״      1   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver.phpL      L  0M      I   vendor/doctrine/dbal/lib/Doctrine/DBAL/Id/TableGeneratorSchemaVisitor.php        P      <   vendor/doctrine/dbal/lib/Doctrine/DBAL/Id/TableGenerator.php~      ~        U   vendor/doctrine/dbal/lib/Doctrine/DBAL/Tools/Console/Command/ReservedWordsCommand.php`      `  㖫      N   vendor/doctrine/dbal/lib/Doctrine/DBAL/Tools/Console/Command/RunSqlCommand.php        6s      N   vendor/doctrine/dbal/lib/Doctrine/DBAL/Tools/Console/Command/ImportCommand.php^      ^  ˑ      P   vendor/doctrine/dbal/lib/Doctrine/DBAL/Tools/Console/Helper/ConnectionHelper.phpi      i  1Y/y      K   vendor/doctrine/dbal/lib/Doctrine/DBAL/Tools/Console/ConnectionProvider.phpP      P  0i      F   vendor/doctrine/dbal/lib/Doctrine/DBAL/Tools/Console/ConsoleRunner.php        >=      d   vendor/doctrine/dbal/lib/Doctrine/DBAL/Tools/Console/ConnectionProvider/SingleConnectionProvider.php        ;Kb      K   vendor/doctrine/dbal/lib/Doctrine/DBAL/Tools/Console/ConnectionNotFound.php          
z      7   vendor/doctrine/dbal/lib/Doctrine/DBAL/Tools/Dumper.php        a[      V   vendor/doctrine/dbal/lib/Doctrine/DBAL/Sharding/SQLAzure/Schema/MultiTenantVisitor.php        Y      Q   vendor/doctrine/dbal/lib/Doctrine/DBAL/Sharding/SQLAzure/SQLAzureShardManager.php        xn      \   vendor/doctrine/dbal/lib/Doctrine/DBAL/Sharding/SQLAzure/SQLAzureFederationsSynchronizer.php"      "         J   vendor/doctrine/dbal/lib/Doctrine/DBAL/Sharding/PoolingShardConnection.php        YQ{      @   vendor/doctrine/dbal/lib/Doctrine/DBAL/Sharding/ShardManager.php        3      V   vendor/doctrine/dbal/lib/Doctrine/DBAL/Sharding/ShardChoser/MultiTenantShardChoser.php        'm      K   vendor/doctrine/dbal/lib/Doctrine/DBAL/Sharding/ShardChoser/ShardChoser.php        s       G   vendor/doctrine/dbal/lib/Doctrine/DBAL/Sharding/PoolingShardManager.php	      	  x      E   vendor/doctrine/dbal/lib/Doctrine/DBAL/Sharding/ShardingException.php        m      @   vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/SimpleArrayType.phpE      E  '&մ      ;   vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/BinaryType.phpC      C  or!      <   vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/IntegerType.phpT      T  /      D   vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/ConversionException.php        B      >   vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/JsonArrayType.php        3Q      H   vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/DateTimeTzImmutableType.php/      /  ;ô      F   vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/DateTimeImmutableType.php        |״      6   vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/Types.php        Ǵ      :   vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/ArrayType.phpL      L  (d      I   vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/VarDateTimeImmutableType.php        >      9   vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/JsonType.php        r      A   vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/DateIntervalType.php        9~      9   vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/TimeType.php        p      =   vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/TypeRegistry.php        nQ\      F   vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/PhpIntegerMappingType.php          ô      5   vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/Type.php@/      @/  1܅      9   vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/BlobType.php5      5  pZp      <   vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/DecimalType.php        lu=)      ?   vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/DateTimeTzType.phpy	      y	  "-x      G   vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/PhpDateTimeMappingType.php          ַX      @   vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/VarDateTimeType.php}      }  f[#      ;   vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/StringType.php        <>'      ;   vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/ObjectType.php        J      B   vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/TimeImmutableType.php         Ĵ      <   vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/BooleanType.php        Ξ      9   vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/TextType.php        Yʃ      :   vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/FloatType.phpU      U  Թ      ;   vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/BigIntType.phpY      Y  YO!      @   vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/AsciiStringType.php^      ^  j      9   vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/GuidType.php        aّ      =   vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/SmallIntType.php`      `  ;      9   vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/DateType.php        G      B   vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/DateImmutableType.php        1#P      =   vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/DateTimeType.php               D   vendor/doctrine/dbal/lib/Doctrine/DBAL/TransactionIsolationLevel.phpe      e  ֤      5   vendor/doctrine/dbal/lib/Doctrine/DBAL/Connection.php      >rX      3   vendor/doctrine/dbal/lib/Doctrine/DBAL/LockMode.php              8   vendor/doctrine/dbal/lib/Doctrine/DBAL/DriverManager.phpvC      vC  	      >   vendor/doctrine/dbal/lib/Doctrine/DBAL/ConnectionException.php              E   vendor/doctrine/dbal/lib/Doctrine/DBAL/VersionAwarePlatformDriver.php        n>      8   vendor/doctrine/dbal/lib/Doctrine/DBAL/DBALException.php$      $  xl      H   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLSrv/Exception/Error.php"      "  D      B   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLSrv/Statement.phpa       a   C      ?   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLSrv/Driver.php        M)x      E   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLSrv/LastInsertId.php        )      H   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLSrv/SQLSrvException.php        '      C   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLSrv/Connection.phpc       c   D۴      H   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLSrv/SQLSrvStatement.php4      4  ,      I   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLSrv/SQLSrvConnection.php5      5  d      I   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/AbstractSQLServerDriver.php        G-e/      ;   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/Statement.php        f-W      K   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/AbstractSQLAnywhereDriver.php        ".      <   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/FetchUtils.php)      )  M      A   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOPgSql/Driver.php        ݵ      F   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/AbstractOracleDriver.php        !      C   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/AbstractDB2Driver.php        _H      F   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/AbstractSQLiteDriver.php              I   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/AbstractDriverException.php          δ      K   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/ServerInfoAwareConnection.php~      ~  I²N      H   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOQueryImplementation.php        )'      E   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOSqlsrv/Statement.php        Y+      B   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOSqlsrv/Driver.php
      
  L       F   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOSqlsrv/Connection.php        /W      P   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/IBMDB2/Exception/PrepareFailed.php        _e      R   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/IBMDB2/Exception/ConnectionError.php        ԹZ      \   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/IBMDB2/Exception/CannotCreateTemporaryFile.php*      *  JB?      S   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/IBMDB2/Exception/ConnectionFailed.php        9      [   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/IBMDB2/Exception/CannotCopyStreamToStream.php8      8  Yc&      ]   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/IBMDB2/Exception/CannotWriteToTemporaryFile.php4      4  i6Ǵ      Q   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/IBMDB2/Exception/StatementError.php        WT      B   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/IBMDB2/Statement.php^       ^   zBG      ?   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/IBMDB2/Driver.phpr       r   1      B   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/IBMDB2/DB2Driver.php        ~      C   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/IBMDB2/Connection.php`       `   y      E   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/IBMDB2/DB2Exception.php          o      G   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/IBMDB2/DataSourceName.php        s4.      F   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/IBMDB2/DB2Connection.php        fb?      E   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/IBMDB2/DB2Statement.php9      9  dd      H   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/DrizzlePDOMySql/Driver.php        0      L   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/DrizzlePDOMySql/Connection.php        j      c   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/AbstractSQLServerDriver/Exception/PortWithoutHost.php}      }        ?   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOIbm/Driver.php        ⬊      C   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/AbstractException.php_      _  ݴ      E   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/AbstractMySQLDriver.php]      ]  3      <   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/Connection.php        4ň6      J   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/ExceptionConverterDriver.php        ޺='      D   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PingableConnection.php        L_      B   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOOracle/Driver.php        1      B   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOSqlite/Driver.phpx	      x	  E=ɴ      A   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/DriverException.php          잓      F   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDO/SQLSrv/Statement.php          4G3      C   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDO/SQLSrv/Driver.php          w\f      G   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDO/SQLSrv/Connection.php          @      ?   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDO/Statement.php}       }   g2^      C   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDO/SQLite/Driver.php          s      @   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDO/Connection.php          A      @   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDO/OCI/Driver.php          2      B   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDO/PgSQL/Driver.php          %.      B   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDO/MySQL/Driver.php          p      ?   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDO/Exception.php9      9   R      D   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLAnywhere/Driver.php        jܴ      R   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLAnywhere/SQLAnywhereException.php	      	  ?      R   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLAnywhere/SQLAnywhereStatement.php1      1  ng      S   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLAnywhere/SQLAnywhereConnection.php        D5      A   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/ResultStatement.php?      ?  U!      R   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/Mysqli/Exception/ConnectionError.php        (3      P   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/Mysqli/Exception/InvalidOption.php        U       S   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/Mysqli/Exception/ConnectionFailed.phpN      N  D~X      \   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/Mysqli/Exception/FailedReadingStreamOffset.php        q^[{      N   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/Mysqli/Exception/UnknownType.php        zɴ      Q   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/Mysqli/Exception/StatementError.php        l      B   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/Mysqli/Statement.phpa       a   p:=      I   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/Mysqli/MysqliConnection.php!      !  m      ?   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/Mysqli/Driver.php`      `  Pi/k      C   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/Mysqli/Connection.phpc       c   wYla      H   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/Mysqli/MysqliStatement.php:      :  Ŵ      H   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/Mysqli/MysqliException.php          4e      8   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/Result.php	      	  # DĴ      C   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/StatementIterator.phph      h  RU      J   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/AbstractPostgreSQLDriver.php        Fm      ;   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/Exception.php        t2      ?   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOConnection.php        .m;      X   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/AbstractOracleDriver/EasyConnectString.php
      
  ?      >   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOStatement.php+      +        M   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOStatementImplementations.php        s      A   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOMySql/Driver.php        !      U   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/OCI8/Exception/SequenceDoesNotExist.phpy      y  =v      [   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/OCI8/Exception/NonTerminatedStringLiteral.php        u      V   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/OCI8/Exception/UnknownParameterIndex.php        {      @   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/OCI8/Statement.php]       ]         D   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/OCI8/OCI8Exception.phpk      k  #      =   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/OCI8/Driver.php        bW      D   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/OCI8/OCI8Statement.phpE      E  W      E   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/OCI8/OCI8Connection.php        	      A   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/OCI8/Connection.php_       _   j      >   vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOException.php              8   vendor/doctrine/dbal/lib/Doctrine/DBAL/ParameterType.php        pk      5   vendor/doctrine/dbal/lib/Doctrine/DBAL/ColumnCase.php        (ô      M   vendor/doctrine/dbal/lib/Doctrine/DBAL/Query/Expression/ExpressionBuilder.php0%      0%  E@      O   vendor/doctrine/dbal/lib/Doctrine/DBAL/Query/Expression/CompositeExpression.php2      2  L      ?   vendor/doctrine/dbal/lib/Doctrine/DBAL/Query/QueryException.php        |q>      =   vendor/doctrine/dbal/lib/Doctrine/DBAL/Query/QueryBuilder.php5      5        9   vendor/doctrine/dbal/lib/Doctrine/DBAL/SQLParserUtils.php(      (  r      @   vendor/doctrine/dbal/lib/Doctrine/DBAL/Portability/Statement.phpD(      D(  +_´      A   vendor/doctrine/dbal/lib/Doctrine/DBAL/Portability/Connection.phpc      c  F[N@      D   vendor/doctrine/dbal/lib/Doctrine/DBAL/Portability/OptimizeFlags.php        C´      E   vendor/doctrine/dbal/lib/Doctrine/DBAL/Cache/ResultCacheStatement.php"      "  J      ?   vendor/doctrine/dbal/lib/Doctrine/DBAL/Cache/ArrayStatement.phpe      e  _-      B   vendor/doctrine/dbal/lib/Doctrine/DBAL/Cache/QueryCacheProfile.php7      7  NѴ      ?   vendor/doctrine/dbal/lib/Doctrine/DBAL/Cache/CacheException.php        ՠ{      1   vendor/doctrine/dbal/lib/Doctrine/DBAL/Result.php        Ov      F   vendor/doctrine/dbal/lib/Doctrine/DBAL/ForwardCompatibility/Result.php&      &  'U      U   vendor/doctrine/dbal/lib/Doctrine/DBAL/ForwardCompatibility/DriverResultStatement.php          H}      O   vendor/doctrine/dbal/lib/Doctrine/DBAL/ForwardCompatibility/DriverStatement.php          Рд      8   vendor/doctrine/dbal/lib/Doctrine/DBAL/Configuration.php        r8      4   vendor/doctrine/dbal/lib/Doctrine/DBAL/Exception.phpg       g   *      I   vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/PostgreSQL92Platform.php        pw      J   vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/PostgreSQL100Platform.php        \      E   vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/AbstractPlatform.php(     (  Ҵ      J   vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/SQLAnywhere11Platform.phpD      D  9a      F   vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/SQLServerPlatform.php        )      D   vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/DrizzlePlatform.phpB      B  p&      J   vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/SQLServer2005Platform.php        @U      D   vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/MySQL80Platform.phpX      X  >0      =   vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/TrimMode.php6      6  rQ      J   vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/SQLServer2012Platform.php        ec      J   vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/SQLServer2008Platform.php	      	  L2      E   vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/SQLAzurePlatform.php        ?n      J   vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/SQLAnywhere12Platform.php
      
  HR      C   vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/SqlitePlatform.phpՍ      Ս  t       P   vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/MariaDb102Keywords.php        6      S   vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/SQLAnywhere11Keywords.php        C|      L   vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/SQLiteKeywords.php        oy      Q   vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/SQLAnywhereKeywords.phpN      N  \w0      R   vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/PostgreSQL91Keywords.php^
      ^
  q      K   vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/MsSQLKeywords.phpL      L  @N      R   vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/PostgreSQL92Keywords.php        T      I   vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/DB2Keywords.php$      $  =l      W   vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/ReservedKeywordsValidator.phpS
      S
  ~x	      P   vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/PostgreSQLKeywords.php        M	      S   vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/SQLAnywhere16Keywords.php        M      O   vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/SQLServerKeywords.php        i      S   vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/SQLServer2008Keywords.phpT      T  O낕      M   vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/DrizzleKeywords.php(      (  Q>ڴ      M   vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/MySQL57Keywords.php        +7      L   vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/OracleKeywords.php
      
  P\      S   vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/SQLAnywhere12Keywords.php        s+Ĵ      S   vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/PostgreSQL100Keywords.php        !ߴ      K   vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/MySQLKeywords.php        \      R   vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/PostgreSQL94Keywords.phpv      v  ^1~      I   vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/KeywordList.phpY      Y  3      M   vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/MySQL80Keywords.php              S   vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/SQLServer2012Keywords.php        ٴ      S   vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/SQLServer2005Keywords.php        la      D   vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/MySQL57Platform.php        S      G   vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/PostgreSqlPlatform.php        n      H   vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/MariaDb1027Platform.php4      4  n      @   vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/DB2Platform.phpb      b  _.      I   vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/PostgreSQL94Platform.php1      1  Yʴ      C   vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/OraclePlatform.php        Z̴      I   vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/PostgreSQL91Platform.php        E)Ŵ      B   vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/MySqlPlatform.php<      <  ɹ      E   vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/DateIntervalUnit.php        N      J   vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/SQLAnywhere16Platform.php        o}\H      H   vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/SQLAnywherePlatform.php              B   vendor/doctrine/dbal/lib/Doctrine/DBAL/SQLParserUtilsException.php        P      =   vendor/doctrine/dbal/lib/Doctrine/DBAL/Abstraction/Result.php2      2  K0      2   vendor/doctrine/dbal/lib/Doctrine/DBAL/Version.php        Y:      /   vendor/doctrine/event-manager/src/EventArgs.php*      *  Cfϴ      2   vendor/doctrine/event-manager/src/EventManager.php]      ]  Bȴ      5   vendor/doctrine/event-manager/src/EventSubscriber.php        aS      0   vendor/doctrine/deprecations/src/Deprecation.phpa%      a%  ] :      ?   vendor/doctrine/deprecations/src/PHPUnit/VerifyDeprecations.php        =Q      B   vendor/doctrine/cache/lib/Doctrine/Common/Cache/ClearableCache.php        G5      A   vendor/doctrine/cache/lib/Doctrine/Common/Cache/MultiGetCache.phpQ      Q  vzg      9   vendor/doctrine/cache/lib/Doctrine/Common/Cache/Cache.php
      
        A   vendor/doctrine/cache/lib/Doctrine/Common/Cache/CacheProvider.php!      !  Q      A   vendor/doctrine/cache/lib/Doctrine/Common/Cache/MultiPutCache.php        %      B   vendor/doctrine/cache/lib/Doctrine/Common/Cache/FlushableCache.php^      ^  S<      D   vendor/doctrine/cache/lib/Doctrine/Common/Cache/MultiDeleteCache.php        ۴      H   vendor/doctrine/cache/lib/Doctrine/Common/Cache/Psr6/InvalidArgument.php         >      E   vendor/doctrine/cache/lib/Doctrine/Common/Cache/Psr6/CacheAdapter.php          ;D"      I   vendor/doctrine/cache/lib/Doctrine/Common/Cache/Psr6/DoctrineProvider.php              B   vendor/doctrine/cache/lib/Doctrine/Common/Cache/Psr6/CacheItem.php
      
  `4      G   vendor/doctrine/cache/lib/Doctrine/Common/Cache/Psr6/TypedCacheItem.php{      {  P
"      G   vendor/doctrine/cache/lib/Doctrine/Common/Cache/MultiOperationCache.php          /      6   vendor/symfony/dependency-injection/TypedReference.php'      '  |      1   vendor/symfony/dependency-injection/Container.phph6      h6  Y      @   vendor/symfony/dependency-injection/Exception/LogicException.php        l.      L   vendor/symfony/dependency-injection/Exception/ParameterNotFoundException.php        J@P      O   vendor/symfony/dependency-injection/Exception/InvalidParameterTypeException.php<      <   ݴ      G   vendor/symfony/dependency-injection/Exception/EnvParameterException.php
      
  0s      H   vendor/symfony/dependency-injection/Exception/BadMethodCallException.php        
O,      S   vendor/symfony/dependency-injection/Exception/ServiceCircularReferenceException.phpT      T  Ś      B   vendor/symfony/dependency-injection/Exception/RuntimeException.php        
+      K   vendor/symfony/dependency-injection/Exception/AutowiringFailedException.php
      
  8r      D   vendor/symfony/dependency-injection/Exception/ExceptionInterface.phpb      b  >      J   vendor/symfony/dependency-injection/Exception/ServiceNotFoundException.php        >      F   vendor/symfony/dependency-injection/Exception/EnvNotFoundException.php        >      F   vendor/symfony/dependency-injection/Exception/OutOfBoundsException.php        {      J   vendor/symfony/dependency-injection/Exception/InvalidArgumentException.php               U   vendor/symfony/dependency-injection/Exception/ParameterCircularReferenceException.php        l.i      @   vendor/symfony/dependency-injection/TaggedContainerInterface.php        _S      8   vendor/symfony/dependency-injection/ReverseContainer.php	      	  $BJ      5   vendor/symfony/dependency-injection/Dumper/Dumper.php        2gU      8   vendor/symfony/dependency-injection/Dumper/PhpDumper.php      }Q      >   vendor/symfony/dependency-injection/Dumper/DumperInterface.phpG      G  P	'      8   vendor/symfony/dependency-injection/Dumper/XmlDumper.php(H      (H  },      9   vendor/symfony/dependency-injection/Dumper/YamlDumper.php7      7  ,ȴ      =   vendor/symfony/dependency-injection/Dumper/GraphvizDumper.php>$      >$  Xe      8   vendor/symfony/dependency-injection/Dumper/Preloader.php        %A0      9   vendor/symfony/dependency-injection/Loader/FileLoader.php{;      {;  ?      <   vendor/symfony/dependency-injection/Loader/ClosureLoader.phpZ      Z  f{	      =   vendor/symfony/dependency-injection/Loader/GlobFileLoader.phpR      R  W}      >   vendor/symfony/dependency-injection/Loader/DirectoryLoader.php	      	  *x      <   vendor/symfony/dependency-injection/Loader/PhpFileLoader.phpG!      G!  H#      <   vendor/symfony/dependency-injection/Loader/IniFileLoader.php        N      =   vendor/symfony/dependency-injection/Loader/YamlFileLoader.php        wƺS      <   vendor/symfony/dependency-injection/Loader/XmlFileLoader.phpf      f  c      L   vendor/symfony/dependency-injection/Loader/Configurator/Traits/BindTrait.php         w      M   vendor/symfony/dependency-injection/Loader/Configurator/Traits/ShareTrait.php=      =  O[      P   vendor/symfony/dependency-injection/Loader/Configurator/Traits/DecorateTrait.phpP      P        M   vendor/symfony/dependency-injection/Loader/Configurator/Traits/ClassTrait.php#      #  Վ      N   vendor/symfony/dependency-injection/Loader/Configurator/Traits/ParentTrait.php        0      L   vendor/symfony/dependency-injection/Loader/Configurator/Traits/CallTrait.php0      0  zpn      P   vendor/symfony/dependency-injection/Loader/Configurator/Traits/ArgumentTrait.php        Vj      P   vendor/symfony/dependency-injection/Loader/Configurator/Traits/AbstractTrait.php        Mɔc      K   vendor/symfony/dependency-injection/Loader/Configurator/Traits/TagTrait.php        jg*      P   vendor/symfony/dependency-injection/Loader/Configurator/Traits/PropertyTrait.php]      ]  I      Q   vendor/symfony/dependency-injection/Loader/Configurator/Traits/SyntheticTrait.php              T   vendor/symfony/dependency-injection/Loader/Configurator/Traits/FromCallableTrait.php	      	  7      T   vendor/symfony/dependency-injection/Loader/Configurator/Traits/ConfiguratorTrait.php        W      L   vendor/symfony/dependency-injection/Loader/Configurator/Traits/LazyTrait.php5      5  F
      Q   vendor/symfony/dependency-injection/Loader/Configurator/Traits/DeprecateTrait.phpF      F  dLӴ      O   vendor/symfony/dependency-injection/Loader/Configurator/Traits/FactoryTrait.php(      (  }j      N   vendor/symfony/dependency-injection/Loader/Configurator/Traits/PublicTrait.php        ɕԴ      U   vendor/symfony/dependency-injection/Loader/Configurator/Traits/AutoconfigureTrait.php(      (  f$      P   vendor/symfony/dependency-injection/Loader/Configurator/Traits/AutowireTrait.php>      >  1I*      L   vendor/symfony/dependency-injection/Loader/Configurator/Traits/FileTrait.php9      9  }      S   vendor/symfony/dependency-injection/Loader/Configurator/Traits/ConstructorTrait.phpG      G  xE      R   vendor/symfony/dependency-injection/Loader/Configurator/ParametersConfigurator.phpZ      Z  z      Q   vendor/symfony/dependency-injection/Loader/Configurator/PrototypeConfigurator.php
      
        P   vendor/symfony/dependency-injection/Loader/Configurator/AbstractConfigurator.phpP      P  Jg      R   vendor/symfony/dependency-injection/Loader/Configurator/InstanceofConfigurator.php        @O      U   vendor/symfony/dependency-injection/Loader/Configurator/InlineServiceConfigurator.php8      8  lz      P   vendor/symfony/dependency-injection/Loader/Configurator/ServicesConfigurator.php        gX-      T   vendor/symfony/dependency-injection/Loader/Configurator/FromCallableConfigurator.php        1PѴ      Q   vendor/symfony/dependency-injection/Loader/Configurator/ReferenceConfigurator.php        >|      Q   vendor/symfony/dependency-injection/Loader/Configurator/ContainerConfigurator.php        ;J      P   vendor/symfony/dependency-injection/Loader/Configurator/DefaultsConfigurator.php	      	  Jش      K   vendor/symfony/dependency-injection/Loader/Configurator/EnvConfigurator.php8      8  56]      X   vendor/symfony/dependency-injection/Loader/Configurator/ClosureReferenceConfigurator.phpy      y  ]      W   vendor/symfony/dependency-injection/Loader/Configurator/AbstractServiceConfigurator.php        9E?L      M   vendor/symfony/dependency-injection/Loader/Configurator/AliasConfigurator.php        XVմ      O   vendor/symfony/dependency-injection/Loader/Configurator/ServiceConfigurator.phpi      i  g޻      B   vendor/symfony/dependency-injection/ExpressionLanguageProvider.php        24X      D   vendor/symfony/dependency-injection/Argument/RewindableGenerator.php        D      G   vendor/symfony/dependency-injection/Argument/ServiceLocatorArgument.php        ۴      A   vendor/symfony/dependency-injection/Argument/IteratorArgument.php*      *  uϣ      J   vendor/symfony/dependency-injection/Argument/ReferenceSetArgumentTrait.php        ߭      ?   vendor/symfony/dependency-injection/Argument/ServiceLocator.php        |5$      >   vendor/symfony/dependency-injection/Argument/BoundArgument.php        $4      <   vendor/symfony/dependency-injection/Argument/LazyClosure.php        	E      A   vendor/symfony/dependency-injection/Argument/AbstractArgument.php        Դ      G   vendor/symfony/dependency-injection/Argument/ServiceClosureArgument.php        M޴      G   vendor/symfony/dependency-injection/Argument/TaggedIteratorArgument.phpU      U  C      B   vendor/symfony/dependency-injection/Argument/ArgumentInterface.php@      @  *O      7   vendor/symfony/dependency-injection/EnvVarProcessor.php%4      %4  4      D   vendor/symfony/dependency-injection/Extension/ExtensionInterface.phpG      G  `|16      @   vendor/symfony/dependency-injection/Extension/ExtensionTrait.phpy      y  A$      K   vendor/symfony/dependency-injection/Extension/PrependExtensionInterface.php7      7  PK      ;   vendor/symfony/dependency-injection/Extension/Extension.php        y      Q   vendor/symfony/dependency-injection/Extension/ConfigurationExtensionInterface.php(      (  4Zy      P   vendor/symfony/dependency-injection/Extension/ConfigurableExtensionInterface.php        Ĵ      C   vendor/symfony/dependency-injection/Extension/AbstractExtension.php        #`L      ?   vendor/symfony/dependency-injection/ContainerAwareInterface.php        ֹ      C   vendor/symfony/dependency-injection/Attribute/AutowireDecorated.php        6
L      =   vendor/symfony/dependency-injection/Attribute/AsDecorator.phpn      n   %랴      >   vendor/symfony/dependency-injection/Attribute/AsTaggedItem.php        e״      8   vendor/symfony/dependency-injection/Attribute/Target.php              9   vendor/symfony/dependency-injection/Attribute/AsAlias.php        fz      B   vendor/symfony/dependency-injection/Attribute/AutoconfigureTag.php        ό      @   vendor/symfony/dependency-injection/Attribute/TaggedIterator.php8      8  |%      9   vendor/symfony/dependency-injection/Attribute/Exclude.php        M      B   vendor/symfony/dependency-injection/Attribute/AutowireIterator.php#      #  ScV      H   vendor/symfony/dependency-injection/Attribute/AutowireServiceClosure.php              >   vendor/symfony/dependency-injection/Attribute/MapDecorated.phpT      T  k      ?   vendor/symfony/dependency-injection/Attribute/Autoconfigure.php              A   vendor/symfony/dependency-injection/Attribute/AutowireLocator.php        ϥ      6   vendor/symfony/dependency-injection/Attribute/When.php        _      :   vendor/symfony/dependency-injection/Attribute/Autowire.php_      _  g|      B   vendor/symfony/dependency-injection/Attribute/AutowireCallable.phpc      c  "      ?   vendor/symfony/dependency-injection/Attribute/TaggedLocator.php6      6  h      :   vendor/symfony/dependency-injection/ExpressionLanguage.php7      7  /*      0   vendor/symfony/dependency-injection/Variable.php        !#      M   vendor/symfony/dependency-injection/Compiler/RegisterReverseContainerPass.phpC      C  !	5      =   vendor/symfony/dependency-injection/Compiler/AutowirePass.php>|      >|  Ԫp       Y   vendor/symfony/dependency-injection/Compiler/CheckFactoryBuilderCircularReferencePass.phpl      l  N      P   vendor/symfony/dependency-injection/Compiler/MergeExtensionConfigurationPass.php          %ͣ      M   vendor/symfony/dependency-injection/Compiler/InlineServiceDefinitionsPass.php        d~      L   vendor/symfony/dependency-injection/Compiler/AutowireRequiredMethodsPass.php.      .  /      K   vendor/symfony/dependency-injection/Compiler/PriorityTaggedServiceTrait.php        zj      J   vendor/symfony/dependency-injection/Compiler/CheckTypeDeclarationsPass.phpQ1      Q1  O      D   vendor/symfony/dependency-injection/Compiler/ResolveBindingsPass.php	*      	*        R   vendor/symfony/dependency-injection/Compiler/ResolveInstanceofConditionalsPass.php        q,      S   vendor/symfony/dependency-injection/Compiler/ReplaceAliasByActualDefinitionPass.php&      &  $      K   vendor/symfony/dependency-injection/Compiler/CheckReferenceValidityPass.php        ܉      9   vendor/symfony/dependency-injection/Compiler/Compiler.php
      
  
      I   vendor/symfony/dependency-injection/Compiler/RemovePrivateAliasesPass.phpv      v        O   vendor/symfony/dependency-injection/Compiler/RegisterServiceSubscribersPass.php        Ѿ      R   vendor/symfony/dependency-injection/Compiler/AliasDeprecatedPublicServicesPass.php        %{j      M   vendor/symfony/dependency-injection/Compiler/AnalyzeServiceReferencesPass.php	      	  ߌmk      E   vendor/symfony/dependency-injection/Compiler/ResolveNoPreloadPass.php        H|s      A   vendor/symfony/dependency-injection/Compiler/ResolveClassPass.phpE      E  k      E   vendor/symfony/dependency-injection/Compiler/DecoratorServicePass.phph      h  #      L   vendor/symfony/dependency-injection/Compiler/CheckCircularReferencesPass.php        6˴      F   vendor/symfony/dependency-injection/Compiler/ServiceReferenceGraph.php
      
  WE      O   vendor/symfony/dependency-injection/Compiler/ResolveReferencesToAliasesPass.php        u      J   vendor/symfony/dependency-injection/Compiler/ResolveNamedArgumentsPass.php        g      K   vendor/symfony/dependency-injection/Compiler/ResolveEnvPlaceholdersPass.php        G      J   vendor/symfony/dependency-injection/Compiler/ServiceReferenceGraphNode.php        t1면      H   vendor/symfony/dependency-injection/Compiler/ResolveFactoryClassPass.php        WZF      M   vendor/symfony/dependency-injection/Compiler/DefinitionErrorExceptionPass.phpy      y  -      O   vendor/symfony/dependency-injection/Compiler/AttributeAutoconfigurationPass.php        t      ]   vendor/symfony/dependency-injection/Compiler/CheckExceptionOnInvalidReferenceBehaviorPass.php        X\d      J   vendor/symfony/dependency-injection/Compiler/ServiceReferenceGraphEdge.phpD	      D	  Iʙ      J   vendor/symfony/dependency-injection/Compiler/ResolveDecoratorStackPass.php-      -  I)      J   vendor/symfony/dependency-injection/Compiler/RemoveBuildParametersPass.php        bnX      ;   vendor/symfony/dependency-injection/Compiler/PassConfig.php        EQĴ      H   vendor/symfony/dependency-injection/Compiler/AutowireAsDecoratorPass.phpa      a  1d      C   vendor/symfony/dependency-injection/Compiler/ResolveHotPathPass.php 	       	  l%      F   vendor/symfony/dependency-injection/Compiler/ExtensionCompilerPass.php{      {  1$(      M   vendor/symfony/dependency-injection/Compiler/ResolveInvalidReferencesPass.php{      {  
>[      R   vendor/symfony/dependency-injection/Compiler/ResolveTaggedIteratorArgumentPass.phpW      W  -z      N   vendor/symfony/dependency-injection/Compiler/RemoveAbstractDefinitionsPass.php        ƙ      N   vendor/symfony/dependency-injection/Compiler/ResolveServiceSubscribersPass.php        W(      E   vendor/symfony/dependency-injection/Compiler/AutoAliasServicePass.php        4'M      K   vendor/symfony/dependency-injection/Compiler/CheckArgumentsValidityPass.php        VD      L   vendor/symfony/dependency-injection/Compiler/CheckDefinitionValidityPass.php        wr      Q   vendor/symfony/dependency-injection/Compiler/ResolveParameterPlaceHoldersPass.php}      }  qN      F   vendor/symfony/dependency-injection/Compiler/CompilerPassInterface.php        S{      L   vendor/symfony/dependency-injection/Compiler/ResolveChildDefinitionsPass.php        GV      O   vendor/symfony/dependency-injection/Compiler/AutowireRequiredPropertiesPass.php        Ұh      L   vendor/symfony/dependency-injection/Compiler/RemoveUnusedDefinitionsPass.php_      _  P      F   vendor/symfony/dependency-injection/Compiler/AbstractRecursivePass.php_)      _)  <ȴ      F   vendor/symfony/dependency-injection/Compiler/ServiceLocatorTagPass.php        : *      T   vendor/symfony/dependency-injection/Compiler/RegisterAutoconfigureAttributesPass.php        -X      M   vendor/symfony/dependency-injection/Compiler/RegisterEnvVarProcessorsPass.php        [:      L   vendor/symfony/dependency-injection/Compiler/ValidateEnvPlaceholdersPass.php        A#)      -   vendor/symfony/dependency-injection/Alias.php              ;   vendor/symfony/dependency-injection/ContainerAwareTrait.phpB      B  R      Q   vendor/symfony/dependency-injection/Config/ContainerParametersResourceChecker.php        D      J   vendor/symfony/dependency-injection/Config/ContainerParametersResource.php              :   vendor/symfony/dependency-injection/ContainerInterface.php	      	  v[      6   vendor/symfony/dependency-injection/ServiceLocator.phpA      A  [RR      M   vendor/symfony/dependency-injection/LazyProxy/PhpDumper/LazyServiceDumper.php        V      K   vendor/symfony/dependency-injection/LazyProxy/PhpDumper/DumperInterface.php<      <  Ŵ      F   vendor/symfony/dependency-injection/LazyProxy/PhpDumper/NullDumper.php        n/Om      V   vendor/symfony/dependency-injection/LazyProxy/Instantiator/RealServiceInstantiator.php8      8  *      T   vendor/symfony/dependency-injection/LazyProxy/Instantiator/InstantiatorInterface.phpS      S  r&G      V   vendor/symfony/dependency-injection/LazyProxy/Instantiator/LazyServiceInstantiator.php        sE      =   vendor/symfony/dependency-injection/LazyProxy/ProxyHelper.php        b<M      @   vendor/symfony/dependency-injection/EnvVarProcessorInterface.php        .Q)@      1   vendor/symfony/dependency-injection/Parameter.phpY      Y  N=      8   vendor/symfony/dependency-injection/ContainerBuilder.phpz      z  =-$      A   vendor/symfony/dependency-injection/ParameterBag/ParameterBag.php$      $  q	ڴ      J   vendor/symfony/dependency-injection/ParameterBag/ContainerBagInterface.php        s      G   vendor/symfony/dependency-injection/ParameterBag/FrozenParameterBag.php              A   vendor/symfony/dependency-injection/ParameterBag/ContainerBag.php        auA      O   vendor/symfony/dependency-injection/ParameterBag/EnvPlaceholderParameterBag.phpu      u  Y 	      J   vendor/symfony/dependency-injection/ParameterBag/ParameterBagInterface.php	      	  <      =   vendor/symfony/dependency-injection/EnvVarLoaderInterface.php        >_<      7   vendor/symfony/dependency-injection/ChildDefinition.phpB
      B
  0      1   vendor/symfony/dependency-injection/Reference.php        bѴ      2   vendor/symfony/dependency-injection/Definition.phpP      P        K   vendor/symfony/event-dispatcher/DependencyInjection/AddEventAliasesPass.php{      {  ״      M   vendor/symfony/event-dispatcher/DependencyInjection/RegisterListenersPass.php!      !  OͰ      <   vendor/symfony/event-dispatcher/EventDispatcherInterface.php        a(\      9   vendor/symfony/event-dispatcher/Debug/WrappedListener.php        yG      B   vendor/symfony/event-dispatcher/Debug/TraceableEventDispatcher.php8      8  6+p      =   vendor/symfony/event-dispatcher/Attribute/AsEventListener.php        U      3   vendor/symfony/event-dispatcher/EventDispatcher.php'#      '#  IJ      <   vendor/symfony/event-dispatcher/ImmutableEventDispatcher.php}      }   Ǵ      0   vendor/symfony/event-dispatcher/GenericEvent.php        m      <   vendor/symfony/event-dispatcher/EventSubscriberInterface.php        s      '   vendor/symfony/config/Util/XmlUtils.php$      $  ô      <   vendor/symfony/config/Util/Exception/InvalidXmlException.php&      &  Ch      <   vendor/symfony/config/Util/Exception/XmlParsingException.php        _      7   vendor/symfony/config/Exception/LoaderLoadException.php        X      N   vendor/symfony/config/Exception/FileLoaderImportCircularReferenceException.phpM      M  m      D   vendor/symfony/config/Exception/FileLocatorFileNotFoundException.php@      @  J      +   vendor/symfony/config/Loader/FileLoader.phpU      U  ͼm      0   vendor/symfony/config/Loader/LoaderInterface.phpn      n  )5      /   vendor/symfony/config/Loader/GlobFileLoader.php        84:      /   vendor/symfony/config/Loader/LoaderResolver.php:      :  h      1   vendor/symfony/config/Loader/DelegatingLoader.php        }>      2   vendor/symfony/config/Loader/ParamConfigurator.php`      `        8   vendor/symfony/config/Loader/LoaderResolverInterface.php        [      >   vendor/symfony/config/Loader/DirectoryAwareLoaderInterface.php        f      '   vendor/symfony/config/Loader/Loader.php        Cʚ      4   vendor/symfony/config/ResourceCheckerConfigCache.php        }ܴ      .   vendor/symfony/config/Definition/Processor.php	      	  X       D   vendor/symfony/config/Definition/Exception/DuplicateKeyException.phpE      E  |      J   vendor/symfony/config/Definition/Exception/ForbiddenOverwriteException.phpQ      Q  :2&      I   vendor/symfony/config/Definition/Exception/InvalidDefinitionException.php        ]F4      L   vendor/symfony/config/Definition/Exception/InvalidConfigurationException.php        [	      8   vendor/symfony/config/Definition/Exception/Exception.php        po:      @   vendor/symfony/config/Definition/Exception/UnsetKeyException.php              C   vendor/symfony/config/Definition/Exception/InvalidTypeException.php        @      >   vendor/symfony/config/Definition/Dumper/XmlReferenceDumper.php''      ''  yo      ?   vendor/symfony/config/Definition/Dumper/YamlReferenceDumper.php        B"O      @   vendor/symfony/config/Definition/Loader/DefinitionFileLoader.php        re=      ;   vendor/symfony/config/Definition/PrototypeNodeInterface.phpb      b        :   vendor/symfony/config/Definition/ConfigurableInterface.phpR      R  RQ      @   vendor/symfony/config/Definition/Builder/FloatNodeDefinition.php        ^      B   vendor/symfony/config/Definition/Builder/NumericNodeDefinition.phpS      S  J      A   vendor/symfony/config/Definition/Builder/ScalarNodeDefinition.php        #U      @   vendor/symfony/config/Definition/Builder/ArrayNodeDefinition.php>      >  >      8   vendor/symfony/config/Definition/Builder/TreeBuilder.php,      ,  *|      >   vendor/symfony/config/Definition/Builder/ValidationBuilder.php        9F      8   vendor/symfony/config/Definition/Builder/NodeBuilder.php,      ,  Z      C   vendor/symfony/config/Definition/Builder/VariableNodeDefinition.php?      ?  Z      8   vendor/symfony/config/Definition/Builder/ExprBuilder.phpI      I  K	      9   vendor/symfony/config/Definition/Builder/MergeBuilder.php        _YF      @   vendor/symfony/config/Definition/Builder/NodeParentInterface.php        YӴ      ;   vendor/symfony/config/Definition/Builder/NodeDefinition.php_      _  [̴      B   vendor/symfony/config/Definition/Builder/BuilderAwareInterface.php^      ^  ]	      A   vendor/symfony/config/Definition/Builder/NormalizationBuilder.php        vɴ      ?   vendor/symfony/config/Definition/Builder/EnumNodeDefinition.php        !l      B   vendor/symfony/config/Definition/Builder/IntegerNodeDefinition.php        sr      J   vendor/symfony/config/Definition/Builder/ParentNodeDefinitionInterface.php              B   vendor/symfony/config/Definition/Builder/BooleanNodeDefinition.php              -   vendor/symfony/config/Definition/EnumNode.php
      
  G       /   vendor/symfony/config/Definition/ScalarNode.phpa      a  Vܩ      8   vendor/symfony/config/Definition/PrototypedArrayNode.phpS,      S,        .   vendor/symfony/config/Definition/ArrayNode.php.      .  蕴      -   vendor/symfony/config/Definition/BaseNode.phpH;      H;  9@2      ;   vendor/symfony/config/Definition/ConfigurationInterface.phpe      e  u      .   vendor/symfony/config/Definition/FloatNode.php        EQ      2   vendor/symfony/config/Definition/Configuration.phpZ      Z  A      1   vendor/symfony/config/Definition/VariableNode.phpv      v  x      0   vendor/symfony/config/Definition/IntegerNode.php:      :   /      0   vendor/symfony/config/Definition/NumericNode.phpc      c  ˖      H   vendor/symfony/config/Definition/Configurator/DefinitionConfigurator.phpR      R  dcӴ      2   vendor/symfony/config/Definition/NodeInterface.php        o      0   vendor/symfony/config/Definition/BooleanNode.php        S	      ,   vendor/symfony/config/ConfigCacheFactory.phpG      G  
@|       8   vendor/symfony/config/Builder/ConfigBuilderInterface.php        $c      *   vendor/symfony/config/Builder/Property.php        o;"      8   vendor/symfony/config/Builder/ConfigBuilderGenerator.phpgU      gU  Icֲ      A   vendor/symfony/config/Builder/ConfigBuilderGeneratorInterface.php        _&8      (   vendor/symfony/config/Builder/Method.php{      {  ^13      .   vendor/symfony/config/Builder/ClassBuilder.phpf      f  VG      %   vendor/symfony/config/FileLocator.php
      
        .   vendor/symfony/config/FileLocatorInterface.phpf      f  YIW      ;   vendor/symfony/config/ResourceCheckerConfigCacheFactory.php)      )  1qe      5   vendor/symfony/config/ConfigCacheFactoryInterface.php        >k      .   vendor/symfony/config/ConfigCacheInterface.php        x;      %   vendor/symfony/config/ConfigCache.php        0      8   vendor/symfony/config/Resource/FileExistenceResource.php        ò      4   vendor/symfony/config/Resource/DirectoryResource.php
      
  3      4   vendor/symfony/config/Resource/ResourceInterface.php        Mմ      3   vendor/symfony/config/Resource/ComposerResource.php        `      >   vendor/symfony/config/Resource/SelfCheckingResourceChecker.php^      ^  2      @   vendor/symfony/config/Resource/SelfCheckingResourceInterface.php        B      /   vendor/symfony/config/Resource/FileResource.php        o
      9   vendor/symfony/config/Resource/ClassExistenceResource.php~      ~  V      :   vendor/symfony/config/Resource/ReflectionClassResource.php        rk      /   vendor/symfony/config/Resource/GlobResource.php$      $  o+      2   vendor/symfony/config/ResourceCheckerInterface.php        ι7      7   vendor/symfony/polyfill-intl-normalizer/bootstrap80.php        dt      5   vendor/symfony/polyfill-intl-normalizer/bootstrap.php        k)      6   vendor/symfony/polyfill-intl-normalizer/Normalizer.php,      ,  9      [   vendor/symfony/polyfill-intl-normalizer/Resources/unidata/rawCompatibilityDecomposition.php      c*❴      W   vendor/symfony/polyfill-intl-normalizer/Resources/unidata/rawCanonicalDecomposition.php        1[s      X   vendor/symfony/polyfill-intl-normalizer/Resources/unidata/compatibilityDecomposition.phpo     o c,      L   vendor/symfony/polyfill-intl-normalizer/Resources/unidata/combiningClass.phpD5      D5        R   vendor/symfony/polyfill-intl-normalizer/Resources/unidata/canonicalComposition.phpD      D  'CԴ      T   vendor/symfony/polyfill-intl-normalizer/Resources/unidata/canonicalDecomposition.php{      {  je      F   vendor/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php        %      8   vendor/symfony/var-exporter/Exception/LogicException.phpq      q  Ż      @   vendor/symfony/var-exporter/Exception/ClassNotFoundException.php#      #        <   vendor/symfony/var-exporter/Exception/ExceptionInterface.phpV      V  '/I      F   vendor/symfony/var-exporter/Exception/NotInstantiableTypeException.php0      0  Eݴ      8   vendor/symfony/var-exporter/Internal/LazyObjectTrait.php        qH      /   vendor/symfony/var-exporter/Internal/Values.php              1   vendor/symfony/var-exporter/Internal/Exporter.phpG      G  bC      1   vendor/symfony/var-exporter/Internal/Hydrator.php-      -  _˴      ;   vendor/symfony/var-exporter/Internal/LazyDecoratorTrait.phpl      l  Ѵ      ;   vendor/symfony/var-exporter/Internal/LazyObjectRegistry.php6      6  U8      8   vendor/symfony/var-exporter/Internal/LazyObjectState.php
      
  x      5   vendor/symfony/var-exporter/Internal/NamedClosure.php        $5      2   vendor/symfony/var-exporter/Internal/Reference.php        `C      1   vendor/symfony/var-exporter/Internal/Registry.phpD      D  wp      (   vendor/symfony/var-exporter/Hydrator.php        x+8ش      +   vendor/symfony/var-exporter/VarExporter.php8      8  V%6      .   vendor/symfony/var-exporter/LazyProxyTrait.php4      4  j      3   vendor/symfony/var-exporter/LazyObjectInterface.phpV      V  b'g      +   vendor/symfony/var-exporter/ProxyHelper.phpf~      f~  {b      ,   vendor/symfony/var-exporter/Instantiator.php	      	  &ыM      .   vendor/symfony/var-exporter/LazyGhostTrait.php5      5  DӴ      $   vendor/symfony/string/ByteString.php:      :  du      3   vendor/symfony/string/Inflector/FrenchInflector.phpK      K  n@      4   vendor/symfony/string/Inflector/SpanishInflector.php        ,      6   vendor/symfony/string/Inflector/InflectorInterface.phpC      C  Qcc      4   vendor/symfony/string/Inflector/EnglishInflector.phpF      F  δ      4   vendor/symfony/string/Exception/RuntimeException.phpp      p  0ʴ      6   vendor/symfony/string/Exception/ExceptionInterface.phpQ      Q  $մ      <   vendor/symfony/string/Exception/InvalidArgumentException.php        e      /   vendor/symfony/string/AbstractUnicodeString.phpq      q  =ڴ      '   vendor/symfony/string/UnicodeString.php=      =  V8n      (   vendor/symfony/string/AbstractString.phpP      P        &   vendor/symfony/string/TruncateMode.php        :      )   vendor/symfony/string/CodePointString.php        S4      $   vendor/symfony/string/LazyString.php[      [  @d      2   vendor/symfony/string/Slugger/SluggerInterface.php        ^      .   vendor/symfony/string/Slugger/AsciiSlugger.php        d      <   vendor/symfony/string/Resources/data/wcswidth_table_zero.php]<      ]<  ?p      <   vendor/symfony/string/Resources/data/wcswidth_table_wide.php2      2  ?Y      -   vendor/symfony/string/Resources/functions.phpT      T  0      <   vendor/symfony/filesystem/Exception/IOExceptionInterface.php        '~i      3   vendor/symfony/filesystem/Exception/IOException.php              8   vendor/symfony/filesystem/Exception/RuntimeException.php        iҴ      =   vendor/symfony/filesystem/Exception/FileNotFoundException.php        <\P      :   vendor/symfony/filesystem/Exception/ExceptionInterface.php        nj      @   vendor/symfony/filesystem/Exception/InvalidArgumentException.php        *      (   vendor/symfony/filesystem/Filesystem.php[v      [v  ~˧H      "   vendor/symfony/filesystem/Path.phpf      f  _+      1   vendor/symfony/deprecation-contracts/function.php        Oݴ      /   vendor/symfony/yaml/Exception/DumpException.php              0   vendor/symfony/yaml/Exception/ParseException.php        Ŵ      2   vendor/symfony/yaml/Exception/RuntimeException.php        _q      4   vendor/symfony/yaml/Exception/ExceptionInterface.php        B9         vendor/symfony/yaml/Dumper.php>      >  ~r         vendor/symfony/yaml/Yaml.php        i±	      +   vendor/symfony/yaml/Command/LintCommand.php(      (  ״      #   vendor/symfony/yaml/ParserState.phpt      t  VQ{         vendor/symfony/yaml/Parser.php        op\      !   vendor/symfony/yaml/Unescaper.php5      5  `      '   vendor/symfony/yaml/Tag/TaggedValue.php        eu         vendor/symfony/yaml/Inline.php5      5  %         vendor/symfony/yaml/Escaper.phph      h  Ó      8   vendor/symfony/service-contracts/ServiceLocatorTrait.php        v|      =   vendor/symfony/service-contracts/ServiceProviderInterface.phpf      f  δ      <   vendor/symfony/service-contracts/ContainerAwareInterface.php              7   vendor/symfony/service-contracts/Attribute/Required.php        e;Z      @   vendor/symfony/service-contracts/Attribute/SubscribedService.php        Xѓ      ?   vendor/symfony/service-contracts/ContainerProviderInterface.php        ش      B   vendor/symfony/service-contracts/ServiceMethodsSubscriberTrait.phpM      M  jĴ      ?   vendor/symfony/service-contracts/ServiceSubscriberInterface.php
      
  Ŵ      ?   vendor/symfony/service-contracts/ServiceCollectionInterface.phpr      r  K0      @   vendor/symfony/service-contracts/Test/ServiceLocatorTestCase.php        p>      <   vendor/symfony/service-contracts/Test/ServiceLocatorTest.php~      ~  Âs      ;   vendor/symfony/service-contracts/ServiceSubscriberTrait.php        r      3   vendor/symfony/service-contracts/ResetInterface.php        񁒳      F   vendor/symfony/event-dispatcher-contracts/EventDispatcherInterface.php        A      3   vendor/symfony/event-dispatcher-contracts/Event.php        с@      0   vendor/symfony/polyfill-mbstring/bootstrap80.php9'      9'  w*      .   vendor/symfony/polyfill-mbstring/bootstrap.phpk      k  АH@      0   vendor/symfony/polyfill-mbstring/bootstrap72.phpQ!      Q!  3^      -   vendor/symfony/polyfill-mbstring/Mbstring.phpʛ      ʛ  t1      @   vendor/symfony/polyfill-mbstring/Resources/unidata/upperCase.phpf      f  P      F   vendor/symfony/polyfill-mbstring/Resources/unidata/titleCaseRegexp.php9      9  >|zK      B   vendor/symfony/polyfill-mbstring/Resources/unidata/caseFolding.phpa	      a	  |⳴      @   vendor/symfony/polyfill-mbstring/Resources/unidata/lowerCase.php_      _  d      9   vendor/symfony/finder/Exception/AccessDeniedException.php        cW޴      >   vendor/symfony/finder/Exception/DirectoryNotFoundException.php        RI      %   vendor/symfony/finder/SplFileInfo.php        *      #   vendor/symfony/finder/Gitignore.php        hn      9   vendor/symfony/finder/Iterator/FileTypeFilterIterator.phpo      o  "f      5   vendor/symfony/finder/Iterator/PathFilterIterator.php        dސ{      =   vendor/symfony/finder/Iterator/MultiplePcreFilterIterator.php        D      ;   vendor/symfony/finder/Iterator/VcsIgnoredFilterIterator.phpn      n  كt      <   vendor/symfony/finder/Iterator/FilecontentFilterIterator.php        ~       A   vendor/symfony/finder/Iterator/ExcludeDirectoryFilterIterator.php        f1      ;   vendor/symfony/finder/Iterator/DepthRangeFilterIterator.phpb      b  ,      =   vendor/symfony/finder/Iterator/RecursiveDirectoryIterator.php(      (   襴      3   vendor/symfony/finder/Iterator/SortableIterator.php        澺      9   vendor/symfony/finder/Iterator/FilenameFilterIterator.phpQ      Q        :   vendor/symfony/finder/Iterator/SizeRangeFilterIterator.phpP      P  gɴ      /   vendor/symfony/finder/Iterator/LazyIterator.php        -9      :   vendor/symfony/finder/Iterator/DateRangeFilterIterator.phpy      y        7   vendor/symfony/finder/Iterator/CustomFilterIterator.php        #      /   vendor/symfony/finder/Comparator/Comparator.php        2TG      3   vendor/symfony/finder/Comparator/DateComparator.php        Y      5   vendor/symfony/finder/Comparator/NumberComparator.php
      
  hML۴          vendor/symfony/finder/Finder.php_      _  a_         vendor/symfony/finder/Glob.php        m+G      5   vendor/symfony/polyfill-intl-grapheme/bootstrap80.php        ԃR      3   vendor/symfony/polyfill-intl-grapheme/bootstrap.php        ]W      2   vendor/symfony/polyfill-intl-grapheme/Grapheme.php5      5  ?h>      5   vendor/symfony/polyfill-intl-grapheme/bootstrap85.php	      	  ɴ      -   vendor/symfony/console/Event/ConsoleEvent.phpP      P  ^O      2   vendor/symfony/console/Event/ConsoleErrorEvent.php        aM      6   vendor/symfony/console/Event/ConsoleTerminateEvent.php        99      4   vendor/symfony/console/Event/ConsoleCommandEvent.php.      .        3   vendor/symfony/console/Event/ConsoleSignalEvent.php        _W      D   vendor/symfony/console/DependencyInjection/AddConsoleCommandPass.phpw      w  #LҴ      :   vendor/symfony/console/Exception/MissingInputException.php        Qg;      3   vendor/symfony/console/Exception/LogicException.php        SML      =   vendor/symfony/console/Exception/CommandNotFoundException.php        fZj      5   vendor/symfony/console/Exception/RuntimeException.php        *b      7   vendor/symfony/console/Exception/ExceptionInterface.php        l      >   vendor/symfony/console/Exception/RunCommandFailedException.phps      s  ٴ      ;   vendor/symfony/console/Exception/InvalidOptionException.php        /el      ?   vendor/symfony/console/Exception/NamespaceNotFoundException.php        BLH      =   vendor/symfony/console/Exception/InvalidArgumentException.php        u i      +   vendor/symfony/console/Debug/CliRequest.phpU      U  vr          vendor/symfony/console/Color.php        .{j      5   vendor/symfony/console/Completion/CompletionInput.php        p,9      0   vendor/symfony/console/Completion/Suggestion.php<      <  <Mؖ      A   vendor/symfony/console/Completion/Output/BashCompletionOutput.php        S      A   vendor/symfony/console/Completion/Output/FishCompletionOutput.php        +Ǵ      @   vendor/symfony/console/Completion/Output/ZshCompletionOutput.php        yZ1      F   vendor/symfony/console/Completion/Output/CompletionOutputInterface.php        3O      ;   vendor/symfony/console/Completion/CompletionSuggestions.phpF      F  $*      &   vendor/symfony/console/Application.php        AG      .   vendor/symfony/console/Attribute/AsCommand.php]      ]  ֚      8   vendor/symfony/console/Formatter/NullOutputFormatter.php        %      =   vendor/symfony/console/Formatter/NullOutputFormatterStyle.php[      [  }8a      B   vendor/symfony/console/Formatter/OutputFormatterStyleInterface.php        &      F   vendor/symfony/console/Formatter/WrappableOutputFormatterInterface.php        #ó      =   vendor/symfony/console/Formatter/OutputFormatterInterface.php        `)      4   vendor/symfony/console/Formatter/OutputFormatter.php$      $  c      >   vendor/symfony/console/Formatter/OutputFormatterStyleStack.php
      
  ´      9   vendor/symfony/console/Formatter/OutputFormatterStyle.php              /   vendor/symfony/console/Logger/ConsoleLogger.php        @      /   vendor/symfony/console/Tester/CommandTester.phpG	      G	  Sb      3   vendor/symfony/console/Tester/ApplicationTester.php        &l6      @   vendor/symfony/console/Tester/Constraint/CommandIsSuccessful.php!      !  к*      9   vendor/symfony/console/Tester/CommandCompletionTester.php        R      -   vendor/symfony/console/Tester/TesterTrait.php        ^      8   vendor/symfony/console/Output/ConsoleOutputInterface.php/      /  Rд      .   vendor/symfony/console/Output/StreamOutput.php         v      5   vendor/symfony/console/Output/TrimmedBufferOutput.php2      2  T      1   vendor/symfony/console/Output/OutputInterface.php        '      (   vendor/symfony/console/Output/Output.php        }Y0      /   vendor/symfony/console/Output/AnsiColorMode.php        K      /   vendor/symfony/console/Output/ConsoleOutput.php)      )        0   vendor/symfony/console/Output/BufferedOutput.phpF      F  +n      ,   vendor/symfony/console/Output/NullOutput.php#      #  S      6   vendor/symfony/console/Output/ConsoleSectionOutput.php~$      ~$        2   vendor/symfony/console/Command/CompleteCommand.php$      $  4      *   vendor/symfony/console/Command/Command.phpY      Y  +:C      8   vendor/symfony/console/Command/DumpCompletionCommand.phpY      Y  (8v      0   vendor/symfony/console/Command/LockableTrait.php        1      .   vendor/symfony/console/Command/HelpCommand.phpU
      U
  	ƴ      3   vendor/symfony/console/Command/TraceableCommand.php%(      %(  ;\(      .   vendor/symfony/console/Command/ListCommand.php
      
  '      =   vendor/symfony/console/Command/SignalableCommandInterface.php^      ^  9P      .   vendor/symfony/console/Command/LazyCommand.php        x      (   vendor/symfony/console/ConsoleEvents.php~      ~  ~6      3   vendor/symfony/console/SingleCommandApplication.php        SUʴ      @   vendor/symfony/console/Descriptor/ReStructuredTextDescriptor.php)      )  p      4   vendor/symfony/console/Descriptor/TextDescriptor.php0      0  C;      9   vendor/symfony/console/Descriptor/DescriptorInterface.phpQ      Q  m'      4   vendor/symfony/console/Descriptor/JsonDescriptor.phpq      q  x꽴      3   vendor/symfony/console/Descriptor/XmlDescriptor.php0&      0&  ww      0   vendor/symfony/console/Descriptor/Descriptor.php`
      `
  %|      <   vendor/symfony/console/Descriptor/ApplicationDescription.php        xۜԴ      8   vendor/symfony/console/Descriptor/MarkdownDescriptor.php        :ش      2   vendor/symfony/console/Helper/InputAwareHelper.php              0   vendor/symfony/console/Helper/QuestionHelper.phpN      N  ]kִ      ,   vendor/symfony/console/Helper/TableStyle.php1      1  (      1   vendor/symfony/console/Helper/HelperInterface.phpI      I  @Ɔ      0   vendor/symfony/console/Helper/TableSeparator.php        &      /   vendor/symfony/console/Helper/ProcessHelper.php        6      +   vendor/symfony/console/Helper/HelperSet.phpK      K  U>ߴ      (   vendor/symfony/console/Helper/Dumper.php-      -  ʹ      7   vendor/symfony/console/Helper/SymfonyQuestionHelper.php        ʴVδ      1   vendor/symfony/console/Helper/FormatterHelper.php        FN      +   vendor/symfony/console/Helper/TableCell.php        c'      3   vendor/symfony/console/Helper/ProgressIndicator.phph      h        2   vendor/symfony/console/Helper/DescriptorHelper.phpN
      N
  D'TB      6   vendor/symfony/console/Helper/DebugFormatterHelper.php          3      -   vendor/symfony/console/Helper/ProgressBar.phpNO      NO  "~ߴ      (   vendor/symfony/console/Helper/Helper.phps      s  !q      /   vendor/symfony/console/Helper/OutputWrapper.php        Ro      0   vendor/symfony/console/Helper/TableCellStyle.php        c      '   vendor/symfony/console/Helper/Table.phpC      C  fY[      5   vendor/symfony/console/Helper/TerminalInputHelper.php        ]F      +   vendor/symfony/console/Helper/TableRows.phpN      N  $      6   vendor/symfony/console/Messenger/RunCommandContext.php&      &  b7      =   vendor/symfony/console/Messenger/RunCommandMessageHandler.php+      +  &VCǴ      6   vendor/symfony/console/Messenger/RunCommandMessage.php        mr      ,   vendor/symfony/console/Input/StringInput.php
      
  Ɨg      0   vendor/symfony/console/Input/InputDefinition.php.      .        4   vendor/symfony/console/Input/InputAwareInterface.phpU      U  }U      /   vendor/symfony/console/Input/InputInterface.phpU      U  @-x      9   vendor/symfony/console/Input/StreamableInputInterface.php         &      +   vendor/symfony/console/Input/ArrayInput.php        GA      .   vendor/symfony/console/Input/InputArgument.php        \      ,   vendor/symfony/console/Input/InputOption.phpu!      u!  RSX;      *   vendor/symfony/console/Input/ArgvInput.php0      0  ͲҴ      &   vendor/symfony/console/Input/Input.phpQ      Q        #   vendor/symfony/console/Terminal.phpq      q  ,      =   vendor/symfony/console/DataCollector/CommandDataCollector.php        O      =   vendor/symfony/console/CommandLoader/FactoryCommandLoader.php"      "  J6\      ?   vendor/symfony/console/CommandLoader/CommandLoaderInterface.php*      *  8      ?   vendor/symfony/console/CommandLoader/ContainerCommandLoader.php        hH      2   vendor/symfony/console/CI/GithubActionReporter.php]      ]  총      6   vendor/symfony/console/EventListener/ErrorListener.phpA      A  %sӴ      8   vendor/symfony/console/SignalRegistry/SignalRegistry.php        '|O      3   vendor/symfony/console/SignalRegistry/SignalMap.php        V      ,   vendor/symfony/console/Style/OutputStyle.phps      s  K8L      -   vendor/symfony/console/Style/SymfonyStyle.php9=      9=  Q      /   vendor/symfony/console/Style/StyleInterface.php
      
  $      8   vendor/symfony/console/Question/ConfirmationQuestion.php(      (  F*      ,   vendor/symfony/console/Question/Question.php              2   vendor/symfony/console/Question/ChoiceQuestion.php]      ]  /Z      !   vendor/symfony/console/Cursor.php        u      '   vendor/symfony/polyfill-ctype/Ctype.php        8      -   vendor/symfony/polyfill-ctype/bootstrap80.phpr      r  F)      +   vendor/symfony/polyfill-ctype/bootstrap.php@      @  jQ9         src/Sdk/Entities/Addon.phpU      U  BSc      1   src/Sdk/Commands/Traits/ValidateCartPathTrait.phpp      p  .e{      -   src/Sdk/Commands/AddonBuildUpgradeCommand.php	      	  Re      (   src/Sdk/Commands/AddonSymlinkCommand.php@      @  )O      %   src/Sdk/Commands/AddonSyncCommand.php
      
  8*      '   src/Sdk/Commands/AddonExportCommand.php%      %  m          src/Slx/Util/NameConversions.php        a<      '   src/Slx/Tools/TemplateFileCollector.phpD      D  ]      :   src/Slx/Tools/FnHookExplorer/AddonRegistrationResolver.php4      4  8      3   src/Slx/Tools/FnHookExplorer/FindsPhpFilesTrait.php        uX      /   src/Slx/Tools/FnHookExplorer/FnHookExplorer.php(      (        2   src/Slx/Tools/FnHookExplorer/HookBodyExtractor.phpt      t  ی      ;   src/Slx/Tools/FnHookExplorer/CoreEditionHandlerResolver.php        ~_      2   src/Slx/Tools/FnHookExplorer/DefinitionLocator.php+      +  .h      7   src/Slx/Tools/FnHookExplorer/OopHookHandlerResolver.phpWp      Wp        &   src/Slx/Tools/ControllerModeFinder.php&      &  j#M      '   src/Slx/Tools/HtmlTagBalanceChecker.php          xF      &   src/Slx/Tools/HtmlTagBalanceFinder.php0       0    S      1   src/Slx/Commands/Traits/CartDbConnectionTrait.php        Zn      +   src/Slx/Commands/Traits/GetRootDirTrait.php        S(|      -   src/Slx/Commands/Traits/CartDbParamsTrait.php	      	  6ˏ      '   src/Slx/Commands/FindFnHooksCommand.php"%      "%  {      2   src/Slx/Commands/FindUnbalancedHtmlTagsCommand.php%      %  g4Ŵ      -   src/Slx/Commands/FindTemplateFilesCommand.php        hB      .   src/Slx/Commands/FindControllerModeCommand.php        ,Z      #   vendor/psr/event-dispatcher/LICENSE(      (  }]      )   vendor/psr/event-dispatcher/composer.json0      0  h]      %   vendor/psr/event-dispatcher/README.mdE      E  S         vendor/psr/container/LICENSEy      y  Op      "   vendor/psr/container/composer.json        m         vendor/psr/container/README.mdB      B  g?      +   vendor/cscart/upgrade-builder/composer.json        o_      )   vendor/cscart/upgrade-builder/phpunit.xml        Hʹ      D   vendor/cscart/upgrade-builder/tests/data/releases/ultimate_4.3.1.zip4      4  8B2      B   vendor/cscart/upgrade-builder/tests/data/releases/addon_v1.0.0.tgz        uωz      B   vendor/cscart/upgrade-builder/tests/data/releases/addon_v2.0.0.zip        @>      B   vendor/cscart/upgrade-builder/tests/data/releases/addon_v2.0.0.tgz              D   vendor/cscart/upgrade-builder/tests/data/releases/ultimate_4.3.2.zipp     p Hޅ      B   vendor/cscart/upgrade-builder/tests/data/releases/addon_v1.0.0.zip        $:ɴ      D   vendor/cscart/upgrade-builder/tests/data/releases/ultimate_4.3.2.tgz      j      D   vendor/cscart/upgrade-builder/tests/data/releases/ultimate_4.3.5.tgz      8ꩴ      D   vendor/cscart/upgrade-builder/tests/data/releases/ultimate_4.3.1.tgzS      S        ?   vendor/cscart/upgrade-builder/tests/data/releases/mve_4.3.4.tgz        &      D   vendor/cscart/upgrade-builder/tests/data/releases/ultimate_4.3.4.tgz        t      '   vendor/cscart/upgrade-builder/README.md          ]ݴ         vendor/composer/LICENSE.      .            vendor/composer/installed.json      F         vendor/bin/doctrine-dbal        [Y         vendor/bin/yaml-lint&      &  /%`         vendor/doctrine/dbal/LICENSE)      )  `XQ      "   vendor/doctrine/dbal/composer.jsonU      U  >,մ         vendor/doctrine/dbal/README.md        Ӿ,޴      &   vendor/doctrine/dbal/bin/doctrine-dbalB       B   ?      $   vendor/doctrine/dbal/CONTRIBUTING.md          c9ȴ      %   vendor/doctrine/event-manager/LICENSE)      )  9޴      +   vendor/doctrine/event-manager/composer.json        &Y      /   vendor/doctrine/event-manager/phpstan.neon.dist8       8          '   vendor/doctrine/event-manager/README.mdH      H  g      (   vendor/doctrine/event-manager/UPGRADE.md=      =        '   vendor/doctrine/event-manager/psalm.xml        &      $   vendor/doctrine/deprecations/LICENSE)      )  "0      *   vendor/doctrine/deprecations/composer.json        5m      &   vendor/doctrine/deprecations/README.mda      a  X         vendor/doctrine/cache/LICENSE)      )  9޴      #   vendor/doctrine/cache/composer.jsonj      j           vendor/doctrine/cache/README.md        BH      $   vendor/doctrine/cache/UPGRADE-1.4.md        iش      %   vendor/doctrine/cache/UPGRADE-1.11.md        i	      +   vendor/symfony/dependency-injection/LICENSE,      ,  U      1   vendor/symfony/dependency-injection/composer.json        B      O   vendor/symfony/dependency-injection/Loader/schema/dic/services/services-1.0.xsdB      B  o      -   vendor/symfony/dependency-injection/README.mdC      C  <      0   vendor/symfony/dependency-injection/CHANGELOG.mdhI      hI  Z[      '   vendor/symfony/event-dispatcher/LICENSE,      ,  U      -   vendor/symfony/event-dispatcher/composer.json        t      )   vendor/symfony/event-dispatcher/README.mdW      W  ,fB      ,   vendor/symfony/event-dispatcher/CHANGELOG.md]      ]  uE~         vendor/symfony/config/LICENSE,      ,  U      #   vendor/symfony/config/composer.json        P9"         vendor/symfony/config/README.mdT      T        "   vendor/symfony/config/CHANGELOG.mde      e  (,      /   vendor/symfony/polyfill-intl-normalizer/LICENSE,      ,  H      5   vendor/symfony/polyfill-intl-normalizer/composer.json        	rT      1   vendor/symfony/polyfill-intl-normalizer/README.md        +tK      #   vendor/symfony/var-exporter/LICENSE,      ,        )   vendor/symfony/var-exporter/composer.json        .      %   vendor/symfony/var-exporter/README.md              (   vendor/symfony/var-exporter/CHANGELOG.md        Eٴ         vendor/symfony/string/LICENSE,      ,  զ_ϴ      #   vendor/symfony/string/composer.json        Ou         vendor/symfony/string/README.md+      +  L      "   vendor/symfony/string/CHANGELOG.md        4B      !   vendor/symfony/filesystem/LICENSE,      ,  U      '   vendor/symfony/filesystem/composer.jsonM      M  W      #   vendor/symfony/filesystem/README.md              &   vendor/symfony/filesystem/CHANGELOG.md        I      ,   vendor/symfony/deprecation-contracts/LICENSE,      ,  K      2   vendor/symfony/deprecation-contracts/composer.jsonI      I        .   vendor/symfony/deprecation-contracts/README.md        X      1   vendor/symfony/deprecation-contracts/CHANGELOG.md          h{#         vendor/symfony/yaml/LICENSE,      ,  U      !   vendor/symfony/yaml/composer.json        8Zʹ         vendor/symfony/yaml/README.md        ـT          vendor/symfony/yaml/CHANGELOG.mdk      k  lc      +   vendor/symfony/yaml/Resources/bin/yaml-lint              (   vendor/symfony/service-contracts/LICENSE,      ,        .   vendor/symfony/service-contracts/composer.jsonv      v  6ʴ      *   vendor/symfony/service-contracts/README.mdJ      J  Г      -   vendor/symfony/service-contracts/CHANGELOG.md          h{#      1   vendor/symfony/event-dispatcher-contracts/LICENSE,      ,        7   vendor/symfony/event-dispatcher-contracts/composer.json        a      3   vendor/symfony/event-dispatcher-contracts/README.mdZ      Z  9{Am      6   vendor/symfony/event-dispatcher-contracts/CHANGELOG.md          h{#      (   vendor/symfony/polyfill-mbstring/LICENSE,      ,  H      .   vendor/symfony/polyfill-mbstring/composer.json        vE      *   vendor/symfony/polyfill-mbstring/README.mdr      r  A`         vendor/symfony/finder/LICENSE,      ,  U      #   vendor/symfony/finder/composer.json                 vendor/symfony/finder/README.md        C      "   vendor/symfony/finder/CHANGELOG.md
      
  3g      -   vendor/symfony/polyfill-intl-grapheme/LICENSE,      ,  H      3   vendor/symfony/polyfill-intl-grapheme/composer.json        vs?      /   vendor/symfony/polyfill-intl-grapheme/README.md        6         vendor/symfony/console/LICENSE,      ,  U      $   vendor/symfony/console/composer.json        k          vendor/symfony/console/README.md        Æf      #   vendor/symfony/console/CHANGELOG.md)      )   b_e      0   vendor/symfony/console/Resources/completion.bash        zߴ      0   vendor/symfony/console/Resources/completion.fish        ܾ      4   vendor/symfony/console/Resources/bin/hiddeninput.exe $       $  v      /   vendor/symfony/console/Resources/completion.zsh              %   vendor/symfony/polyfill-ctype/LICENSE,      ,        +   vendor/symfony/polyfill-ctype/composer.json        e      '   vendor/symfony/polyfill-ctype/README.md^      ^  lHk         bin/cscart-sdk              <?php
declare(strict_types=1);

namespace Psr\EventDispatcher;

/**
 * Defines a dispatcher for events.
 */
interface EventDispatcherInterface
{
    /**
     * Provide all relevant listeners with an event to process.
     *
     * @param object $event
     *   The object to process.
     *
     * @return object
     *   The Event that was passed, now modified by listeners.
     */
    public function dispatch(object $event);
}
<?php
declare(strict_types=1);

namespace Psr\EventDispatcher;

/**
 * An Event whose processing may be interrupted when the event has been handled.
 *
 * A Dispatcher implementation MUST check to determine if an Event
 * is marked as stopped after each listener is called.  If it is then it should
 * return immediately without calling any further Listeners.
 */
interface StoppableEventInterface
{
    /**
     * Is propagation stopped?
     *
     * This will typically only be used by the Dispatcher to determine if the
     * previous listener halted propagation.
     *
     * @return bool
     *   True if the Event is complete and no further listeners should be called.
     *   False to continue calling listeners.
     */
    public function isPropagationStopped() : bool;
}
<?php
declare(strict_types=1);

namespace Psr\EventDispatcher;

/**
 * Mapper from an event to the listeners that are applicable to that event.
 */
interface ListenerProviderInterface
{
    /**
     * @param object $event
     *   An event for which to return the relevant listeners.
     * @return iterable[callable]
     *   An iterable (array, iterator, or generator) of callables.  Each
     *   callable MUST be type-compatible with $event.
     */
    public function getListenersForEvent(object $event) : iterable;
}
<?php

declare(strict_types=1);

namespace Psr\Container;

/**
 * Describes the interface of a container that exposes methods to read its entries.
 */
interface ContainerInterface
{
    /**
     * Finds an entry of the container by its identifier and returns it.
     *
     * @param string $id Identifier of the entry to look for.
     *
     * @throws NotFoundExceptionInterface  No entry was found for **this** identifier.
     * @throws ContainerExceptionInterface Error while retrieving the entry.
     *
     * @return mixed Entry.
     */
    public function get(string $id);

    /**
     * Returns true if the container can return an entry for the given identifier.
     * Returns false otherwise.
     *
     * `has($id)` returning true does not mean that `get($id)` will not throw an exception.
     * It does however mean that `get($id)` will not throw a `NotFoundExceptionInterface`.
     *
     * @param string $id Identifier of the entry to look for.
     *
     * @return bool
     */
    public function has(string $id): bool;
}
<?php

namespace Psr\Container;

/**
 * No entry was found in the container.
 */
interface NotFoundExceptionInterface extends ContainerExceptionInterface
{
}
<?php

namespace Psr\Container;

use Throwable;

/**
 * Base interface representing a generic exception in a container.
 */
interface ContainerExceptionInterface extends Throwable
{
}
<?php
require_once dirname(__DIR__) . '/autoloader.php';<?php

/**
 * Class TarArchiverTest
 */
class TarArchiverTest extends PHPUnit_Framework_TestCase
{
    public function tearDown()
    {
        parent::tearDown();

        exec("rm -rf " . __DIR__ . '/data/runtime/tmp');
    }

    public function setUp()
    {
        parent::setUp();

        exec("mkdir " . __DIR__ . '/data/runtime/tmp');
    }

    /**
     * @dataProvider findProvider
     */
    public function testFind($archive, $regex, $expected_files)
    {
        $archiver = new \upgrade\builder\TarArchiver();
        $found_files = $archiver->find($archive, $regex);

        $this->assertEquals($expected_files, $found_files);
    }

    /**
     * @dataProvider extractProvider
     */
    public function testExtract($archive, $files)
    {
        $tmp_dir = __DIR__ . '/data/runtime/tmp';

        $archiver = new \upgrade\builder\TarArchiver();
        $archiver->extract($archive, $tmp_dir, $files);

        $files_exist = true;
        foreach ($files as $file) {
            $files_exist = $files_exist && is_file($tmp_dir . '/' . $file);
        }

        $this->assertTrue($files_exist);
    }

    public function findProvider()
    {
        return array(
            array(
                __DIR__ . '/data/releases/addon_v1.0.0.tgz',
                'app/addons/.*/addon.xml',
                array('app/addons/sample/addon.xml')
            ),
            array(
                __DIR__ . '/data/releases/addon_v1.0.0.tgz',
                'app/addons/.*/.*.php',
                array('app/addons/sample/func.php', 'app/addons/sample/init.php')
            ),
            array(
                __DIR__ . '/data/releases/addon_v1.0.0.tgz',
                'app/addons/.*/missing.file',
                array()
            ),
        );
    }

    public function extractProvider()
    {
        return array(
            array(
                __DIR__ . '/data/releases/addon_v1.0.0.tgz',
                array('app/addons/sample/addon.xml')
            ),
            array(
                __DIR__ . '/data/releases/addon_v1.0.0.tgz',
                array('app/addons/sample/func.php', 'app/addons/sample/init.php')
            ),
        );
    }
}<?php
<?php
<?php
<?php
<?php
<?php
/**** THIS IS PRE SCRIPT ****/<?php
/**** THIS IS POST SCRIPT ****/<?php
return array(
    'extra' => 1
);<?php

/**
 * Class ZipArchiverTest
 */
class ZipArchiverTest extends PHPUnit_Framework_TestCase
{
    public function tearDown()
    {
        parent::tearDown();

        exec("rm -rf " . __DIR__ . '/data/runtime/tmp');
    }

    public function setUp()
    {
        parent::setUp();

        exec("mkdir " . __DIR__ . '/data/runtime/tmp');
    }

    /**
     * @dataProvider findProvider
     */
    public function testFind($archive, $regex, $expected_files)
    {
        $archiver = new \upgrade\builder\ZipArchiver();
        $found_files = $archiver->find($archive, $regex);

        $this->assertEquals($expected_files, $found_files);
    }

    /**
     * @dataProvider extractProvider
     */
    public function testExtract($archive, $files)
    {
        $tmp_dir = __DIR__ . '/data/runtime/tmp';

        $archiver = new \upgrade\builder\ZipArchiver();
        $archiver->extract($archive, $tmp_dir, $files);

        $files_exist = true;
        foreach ($files as $file) {
            $files_exist = $files_exist && is_file($tmp_dir . '/' . $file);
        }

        $this->assertTrue($files_exist);
    }

    public function findProvider()
    {
        return array(
            array(
                __DIR__ . '/data/releases/addon_v1.0.0.zip',
                'app/addons/.*/addon.xml',
                array('app/addons/sample/addon.xml')
            ),
            array(
                __DIR__ . '/data/releases/addon_v1.0.0.zip',
                'app/addons/.*/.*.php',
                array('app/addons/sample/func.php', 'app/addons/sample/init.php')
            ),
            array(
                __DIR__ . '/data/releases/addon_v1.0.0.zip',
                'app/addons/.*/missing.file',
                array()
            ),
        );
    }

    public function extractProvider()
    {
        return array(
            array(
                __DIR__ . '/data/releases/addon_v1.0.0.zip',
                array('app/addons/sample/addon.xml')
            ),
            array(
                __DIR__ . '/data/releases/addon_v1.0.0.zip',
                array('app/addons/sample/func.php', 'app/addons/sample/init.php')
            ),
        );
    }
}<?php

/**
 * Class AddonBuilderTest
 */
class AddonBuilderTest extends PHPUnit_Framework_TestCase
{
    public function tearDown()
    {
        parent::tearDown();

        exec("rm -rf " . __DIR__ . '/data/runtime/tmp');
    }

    public function setUp()
    {
        parent::setUp();

        exec("mkdir " . __DIR__ . '/data/runtime/tmp');
    }

    /**
     * @dataProvider packageProvider
     */
    public function testUpgradeFiles($package1, $package2)
    {
        $release1 = new \upgrade\builder\ReleaseArchive($package1['path'], $package1['name'], $package1['version']);
        $release2 = new \upgrade\builder\ReleaseArchive($package2['path'], $package2['name'], $package2['version']);

        $builder = new \upgrade\builder\AddonBuilder($release1, $release2, __DIR__ . '/data/runtime/tmp');
        $builder->initPaths();

        $builder->unpackArchives();

        $this->assertEquals(
            array(
                __DIR__ . '/data/runtime/tmp/to/app/addons/sample/upgrades/2.0.0/migrations/62312332434_test_migration1.php',
                __DIR__ . '/data/runtime/tmp/to/app/addons/sample/upgrades/2.0.0/migrations/62312332434_test_migration2.php'
            ),
            $builder->getMigrations()
        );

        $this->assertEquals(
            array(
                __DIR__ . '/data/runtime/tmp/to/app/addons/sample/upgrades/2.0.0/validators/validator1.php',
                __DIR__ . '/data/runtime/tmp/to/app/addons/sample/upgrades/2.0.0/validators/validator2.php'
            ),
            $builder->getValidators()
        );

        $this->assertEquals(
            array(
                __DIR__ . '/data/runtime/tmp/to/app/addons/sample/upgrades/2.0.0/extra_files/test.php',
            ),
            $builder->getExtraFiles()
        );

        $this->assertEquals(
            array(
                'extra' => 1
            ),
            $builder->getExtraPackageData()
        );

        $this->assertEquals(
            __DIR__ . '/data/runtime/tmp/to/app/addons/sample/upgrades/2.0.0/scripts/pre_script.php',
            $builder->getPreScriptFile()
        );

        $this->assertEquals(
            __DIR__ . '/data/runtime/tmp/to/app/addons/sample/upgrades/2.0.0/scripts/post_script.php',
            $builder->getPostScriptFile()
        );
    }

    public function packageProvider()
    {
        return array(
            array(
                array(
                    'path' => __DIR__ . '/data/releases/addon_v1.0.0.tgz',
                    'name' => 'sample',
                    'version' => '1.0.0'
                ),
                array(
                    'path' => __DIR__ . '/data/releases/addon_v2.0.0.tgz',
                    'name' => 'sample',
                    'version' => '2.0.0'
                ),
            ),
            array(
                array(
                    'path' => __DIR__ . '/data/releases/addon_v1.0.0.zip',
                    'name' => 'sample',
                    'version' => '1.0.0'
                ),
                array(
                    'path' => __DIR__ . '/data/releases/addon_v2.0.0.zip',
                    'name' => 'sample',
                    'version' => '2.0.0'
                ),
            ),
            array(
                array(
                    'path' => __DIR__ . '/data/releases/addon_v1.0.0.tgz',
                    'name' => 'sample',
                    'version' => '1.0.0'
                ),
                array(
                    'path' => __DIR__ . '/data/releases/addon_v2.0.0.zip',
                    'name' => 'sample',
                    'version' => '2.0.0'
                ),
            ),
            array(
                array(
                    'path' => __DIR__ . '/data/releases/addon_v1.0.0.zip',
                    'name' => 'sample',
                    'version' => '1.0.0'
                ),
                array(
                    'path' => __DIR__ . '/data/releases/addon_v2.0.0.tgz',
                    'name' => 'sample',
                    'version' => '2.0.0'
                ),
            ),
        );
    }
}<?php

/**
 * Class CoreReleaseArchiveTest
 */
class CoreReleaseArchiveTest extends PHPUnit_Framework_TestCase
{
    /**
     * @dataProvider releaseDataProvider
     */
    public function testCoreRelease($file, $version, $name)
    {
        $release = new \upgrade\builder\CoreReleaseArchive($file);

        $this->assertEquals($version, $release->getVersion());
        $this->assertEquals($name, $release->getName());
    }

    public function releaseDataProvider()
    {
        return array(
            array(__DIR__ . '/data/releases/ultimate_4.3.4.tgz', '4.3.4', 'ultimate'),
            array(__DIR__ . '/data/releases/ultimate_4.3.5.tgz', '4.3.5', 'ultimate'),
            array(__DIR__ . '/data/releases/mve_4.3.4.tgz', '4.3.4', 'mve'),
        );
    }
}<?php

/**
 * Class BuilderTest
 */
class BuilderTest extends PHPUnit_Framework_TestCase
{
    public function tearDown()
    {
        parent::tearDown();

        exec("rm -rf " . __DIR__ . '/data/runtime/tmp');
    }

    public function setUp()
    {
        parent::setUp();

        exec("mkdir " . __DIR__ . '/data/runtime/tmp');
    }

    public function testBuilder()
    {
        $release1 = new \upgrade\builder\ReleaseArchive(__DIR__ . '/data/releases/ultimate_4.3.4.tgz', 'ultimate', '4.3.4');
        $release2 = new \upgrade\builder\ReleaseArchive(__DIR__ . '/data/releases/ultimate_4.3.5.tgz', 'ultimate', '4.3.5');

        $builder = new \upgrade\builder\Builder($release1, $release2, __DIR__ . '/data/runtime/tmp');

        $builder->setMigrationsPath(__DIR__ . '/data/upgrade/migrations');
        $builder->setValidatorsPath(__DIR__ . '/data/upgrade/validators');
        $builder->setScriptsPath(__DIR__ . '/data/upgrade/scripts');
        $builder->setExtraFilesPath(__DIR__ . '/data/upgrade/extra_files');
        $builder->setExtraPath(__DIR__ . '/data/upgrade/extra/extra.php');

        $this->assertEquals(
            array(
                __DIR__ . '/data/upgrade/migrations/62312332434_mve_migration3.php',
                __DIR__ . '/data/upgrade/migrations/62312332434_test_migration1.php',
                __DIR__ . '/data/upgrade/migrations/62312332434_test_migration2.php'
            ),
            $builder->getMigrations()
        );

        $this->assertEquals(
            array(
                __DIR__ . '/data/upgrade/validators/validator1.php',
                __DIR__ . '/data/upgrade/validators/validator2.php',
            ),
            $builder->getValidators()
        );

        $this->assertEquals(
            array(
                __DIR__ . '/data/upgrade/extra_files/test.php',
            ),
            $builder->getExtraFiles()
        );

        $this->assertEquals(
            array(
                'extra' => 1
            ),
            $builder->getExtraPackageData()
        );

        $this->assertEquals(__DIR__ . '/data/upgrade/scripts/pre_script.php', $builder->getPreScriptFile());
        $this->assertEquals(__DIR__ . '/data/upgrade/scripts/post_script.php', $builder->getPostScriptFile());

        $builder->unpackArchives();
        $files = $builder->getFiles();

        $this->assertEquals('new', $files['api.php']['status']);
        $this->assertEquals('deleted', $files['app/test.php']['status']);
        $this->assertEquals('changed', $files['app/test1.php']['status']);
        $this->assertEquals('new', $files['changelog.txt']['status']);
        $this->assertEquals('changed', $files['config.php']['status']);
        $this->assertEquals('new', $files['index.php']['status']);
        $this->assertEquals('new', $files['test3.sql']['status']);
        $this->assertEquals('deleted', $files['test2.sql']['status']);
        $this->assertEquals('deleted', $files['robots.txt']['status']);
        $this->assertEquals('deleted', $files['admin.php']['status']);

        $builder->setExcludeFiles(array('config.php'));
        $files = $builder->getFiles();

        $this->assertCount(9, $files);
        $this->assertArrayNotHasKey('config.php', $files);

        $builder->setExcludeFiles(array(), array('app/test.php'));
        $files = $builder->getFiles();

        $this->assertCount(9, $files);
        $this->assertArrayNotHasKey('app/test.php', $files);

        $builder->setExcludeFiles(array());
        $builder->setExcludeExtensions(array('sql'));
        $files = $builder->getFiles();

        $this->assertCount(8, $files);
        $this->assertArrayNotHasKey('test3.sql', $files);
        $this->assertArrayNotHasKey('test2.sql', $files);
    }
}<?php

/**
 * Class UpgradePackageTest
 */
class UpgradePackageTest extends PHPUnit_Framework_TestCase
{

    public function tearDown()
    {
        parent::tearDown();

        exec("rm -rf " . __DIR__ . '/data/runtime/upgrade');
    }

    public function setUp()
    {
        parent::setUp();

        exec("mkdir " . __DIR__ . '/data/runtime/upgrade');
    }

    public function testSchema()
    {
        $package = new \upgrade\builder\UpgradePackage(__DIR__ . '/data/runtime/upgrade');

        $package->setMigrations(array(
            __DIR__ . '/data/upgrade/migrations/62312332434_mve_migration3.php',
            __DIR__ . '/data/upgrade/migrations/62312332434_test_migration1.php',
            __DIR__ . '/data/upgrade/migrations/62312332434_test_migration2.php'
        ));

        $package->setValidators(array(
            __DIR__ . '/data/upgrade/validators/validator1.php',
            __DIR__ . '/data/upgrade/validators/validator2.php',
        ));

        $package->setPreScript(__DIR__ . '/data/upgrade/scripts/pre_script.php');
        $package->setPostScript(__DIR__ . '/data/upgrade/scripts/post_script.php');
        $package->setFiles(array(
            'test.php' => array(
                'status' => 'new',
                'hash' => 'hash1',
                'src' => __DIR__ . '/data/upgrade/migrations/62312332434_mve_migration3.php',
            ),
            'test2.php' => array(
                'status' => 'changed',
                'hash' => 'hash2',
                'src' => __DIR__ . '/data/upgrade/migrations/62312332434_mve_migration3.php',
            ),
            'test3.php' => array(
                'status' => 'deleted',
                'hash' => 'hash3',
                'src' => __DIR__ . '/data/upgrade/migrations/62312332434_mve_migration3.php',
            )
        ));

        $package->extendScheme(array(
            'files' => array(
                'test4.php' => array(
                    'status' => 'deleted',
                    'hash' => 'hash4'
                )
            )
        ));

        $this->assertEquals(
            array(
                'migrations' => array(
                    '62312332434_mve_migration3.php',
                    '62312332434_test_migration1.php',
                    '62312332434_test_migration2.php',
                ),
                'validators' => array(
                    'validator1',
                    'validator2',
                ),
                'files' => array(
                    'test.php' => array(
                        'status' => 'new',
                        'hash' => 'hash1'
                    ),
                    'test2.php' => array(
                        'status' => 'changed',
                        'hash' => 'hash2'
                    ),
                    'test3.php' => array(
                        'status' => 'deleted',
                        'hash' => 'hash3'
                    ),
                    'test4.php' => array(
                        'status' => 'deleted',
                        'hash' => 'hash4'
                    )
                ),
                'scripts' => array(
                    'pre' => 'pre_script.php',
                    'post' => 'post_script.php',
                )
            ),
            $package->getSchema()
        );

        $this->assertFileExists(__DIR__ . '/data/runtime/upgrade/package/test.php');
        $this->assertFileExists(__DIR__ . '/data/runtime/upgrade/package/test2.php');
        $this->assertFileNotExists(__DIR__ . '/data/runtime/upgrade/package/test3.php');

        $package->setPreScriptExtraCode("/**** EXTRA_CODE ****/");
        $package->create(__DIR__ . '/data/runtime/upgrade/package.zip');

        $this->assertFileExists(__DIR__ . '/data/runtime/upgrade/package.json');
        $this->assertFileExists(__DIR__ . '/data/runtime/upgrade/package.zip');

        $code = <<<DATA
<?php
/**** EXTRA_CODE ****/


/**** THIS IS PRE SCRIPT ****/
DATA;

        $this->assertStringEqualsFile(__DIR__ . '/data/runtime/upgrade/scripts/pre_script.php', $code);
    }
}<?php

/**
 * Class CoreReleaseArchiveTest
 */
class AddonReleaseArchiveTest extends PHPUnit_Framework_TestCase
{
    /**
     * @dataProvider releaseDataProvider
     */
    public function testCoreRelease($file, $version, $name)
    {
        $release = new \upgrade\builder\AddonReleaseArchive($file);

        $this->assertEquals($version, $release->getVersion());
        $this->assertEquals($name, $release->getName());
    }

    public function releaseDataProvider()
    {
        return array(
            array(__DIR__ . '/data/releases/addon_v1.0.0.tgz', '1.0.0', 'sample'),
            array(__DIR__ . '/data/releases/addon_v2.0.0.tgz', '2.0.0', 'sample')
        );
    }
}<?php

/**
 * Class CoreBuilderTest
 */
class CoreBuilderTest extends PHPUnit_Framework_TestCase
{
    public function tearDown()
    {
        parent::tearDown();

        exec("rm -rf " . __DIR__ . '/data/runtime/tmp');
    }

    public function setUp()
    {
        parent::setUp();

        exec("mkdir " . __DIR__ . '/data/runtime/tmp');
    }

    /**
     * @dataProvider packageProvider
     */
    public function testUpgradeFiles($package1, $package2)
    {
        $release1 = new \upgrade\builder\ReleaseArchive($package1['path'], $package1['name'], $package1['version']);
        $release2 = new \upgrade\builder\ReleaseArchive($package2['path'], $package2['name'], $package2['version']);

        $builder = new \upgrade\builder\CoreBuilder($release1, $release2, __DIR__ . '/data/runtime/tmp');

        $builder->setMigrationsPath(__DIR__ . '/data/upgrade/migrations');
        $builder->setValidatorsPath(__DIR__ . '/data/upgrade/validators');
        $builder->setScriptsPath(__DIR__ . '/data/upgrade/scripts');
        $builder->setExtraFilesPath(__DIR__ . '/data/upgrade/extra_files');
        $builder->setExtraPath(__DIR__ . '/data/upgrade/extra/extra.php');

        $this->assertEquals(
            array(
                __DIR__ . '/data/upgrade/migrations/62312332434_test_migration1.php',
                __DIR__ . '/data/upgrade/migrations/62312332434_test_migration2.php'
            ),
            $builder->getMigrations()
        );

        $builder->unpackArchives();
        $files = $builder->getFiles();

        $this->assertEquals('deleted', $files['app/addons/ebay/addon.xml']['status']);
        $this->assertEquals('new', $files['app/addons/sample/addon.xml']['status']);
        $this->assertEquals('new', $files['app/addons/twigmo/addon.xml']['status']);
        $this->assertEquals('new', $files['app/addons/twigmo/file1.php']['status']);
        $this->assertEquals('new', $files['app/addons/twigmo_best/addon.xml']['status']);
        $this->assertEquals('new', $files['app/addons/twigmo_best/file1.php']['status']);

        $builder->setExcludeAddons(array(
            'ebay',
            'sample',
            'twigmo' => array(
                'file1.php'
            )
        ));

        $files = $builder->getFiles();

        $this->assertArrayNotHasKey('app/addons/ebay/addon.xml', $files);
        $this->assertArrayNotHasKey('app/addons/ebay/addon.xml', $files);
        $this->assertArrayNotHasKey('app/addons/twigmo/addon.xml', $files);
        $this->assertArrayNotHasKey('js/addons/twigmo/test.txt', $files);
        $this->assertArrayNotHasKey('var/langs/en/addons/twigmo.po', $files);
        $this->assertArrayHasKey('app/addons/twigmo_best/addon.xml', $files);
        $this->assertArrayHasKey('app/addons/twigmo_best/file1.php', $files);
        $this->assertArrayHasKey('js/addons/twigmo_best/test.txt', $files);
        $this->assertArrayHasKey('var/langs/en/addons/twigmo_best.po', $files);
        $this->assertEquals('new', $files['app/addons/twigmo/file1.php']['status']);
        $this->assertEquals('new', $files['app/addons/twigmo_best/addon.xml']['status']);
        $this->assertEquals('new', $files['app/addons/twigmo_best/file1.php']['status']);
        $this->assertEquals('new', $files['sample.php']['status']);

        $builder->setExcludeFiles(array('app/addons'));
        $files = $builder->getFiles();

        $this->assertArrayNotHasKey('app/addons/ebay/addon.xml', $files);
        $this->assertArrayNotHasKey('app/addons/ebay/addon.xml', $files);
        $this->assertArrayNotHasKey('app/addons/twigmo/addon.xml', $files);
        $this->assertArrayNotHasKey('app/addons/twigmo/file1.php', $files);
        $this->assertArrayNotHasKey('app/addons/sample/addon.xml', $files);
        $this->assertArrayHasKey('js/addons/twigmo_best/test.txt', $files);
    }

    public function packageProvider()
    {
        return array(
            array(
                array(
                    'path' => __DIR__ . '/data/releases/ultimate_4.3.1.tgz',
                    'name' => 'ultimate',
                    'version' => '4.3.1'
                ),
                array(
                    'path' => __DIR__ . '/data/releases/ultimate_4.3.2.tgz',
                    'name' => 'ultimate',
                    'version' => '4.3.2'
                )
            ),
            array(
                array(
                    'path' => __DIR__ . '/data/releases/ultimate_4.3.1.zip',
                    'name' => 'ultimate',
                    'version' => '4.3.1'
                ),
                array(
                    'path' => __DIR__ . '/data/releases/ultimate_4.3.2.zip',
                    'name' => 'ultimate',
                    'version' => '4.3.2'
                )
            ),
            array(
                array(
                    'path' => __DIR__ . '/data/releases/ultimate_4.3.1.tgz',
                    'name' => 'ultimate',
                    'version' => '4.3.1'
                ),
                array(
                    'path' => __DIR__ . '/data/releases/ultimate_4.3.2.zip',
                    'name' => 'ultimate',
                    'version' => '4.3.2'
                )
            ),
            array(
                array(
                    'path' => __DIR__ . '/data/releases/ultimate_4.3.1.zip',
                    'name' => 'ultimate',
                    'version' => '4.3.1'
                ),
                array(
                    'path' => __DIR__ . '/data/releases/ultimate_4.3.2.tgz',
                    'name' => 'ultimate',
                    'version' => '4.3.2'
                )
            )
        );
    }
}<?php
spl_autoload_register(function ($class) {
    if (strpos($class, 'upgrade\builder') === 0) {
        $path = str_replace(array('upgrade\builder', '\\'), array('', DIRECTORY_SEPARATOR), $class) . '.php';

        include __DIR__ . '/src/' . $path;
    }
});
<?php


namespace upgrade\builder;


interface UpgradePackageInterface
{
    /**
     * Set files
     * @param array $files
     */
    public function setFiles(array $files);

    /**
     * Set migrations
     * @param array $migrations
     */
    public function setMigrations(array $migrations);

    /**
     * Set validators
     * @param array $validators
     */
    public function setValidators(array $validators);

    /**
     * Set post script
     * @param string $file
     */
    public function setPostScript($file);

    /**
     * Set pre script
     * @param string $file
     */
    public function setPreScript($file);

    /**
     * Set pre script extra code
     * @param string $code
     */
    public function setPreScriptExtraCode($code);

    /**
     * Set extra files
     * @param array $files
     * @param string $dir
     * @return mixed
     */
    public function setExtraFiles(array $files, $dir = "extra_files");

    /**
     * @param array $data
     * @return mixed
     */
    public function extendScheme(array $data);

    /**
     * Create archive package
     * @param string $file
     * @return bool
     */
    public function create($file);

    /**
     * Get package schema
     * @return array
     */
    public function getSchema();
}<?php


namespace upgrade\builder;

/**
 * Class TarArchiver
 * @package upgrade\builder
 */
class TarArchiver implements ArchiverInterface
{
    public function __construct()
    {
        exec("tar --version 2> /dev/null", $out, $result);
        if ($result !== 0) {
            throw new \Exception('tar is not installed');
        }
    }

    /**
     * @inheritdoc
     */
    public function find($archive = '', $regex = '')
    {
        exec("tar -tf {$archive} 2> /dev/null | grep \"{$regex}\\$\"", $output, $result);

        if ($result !== 0) {
            $output = array();
        }

        return $output;
    }

    /**
     * @inheritdoc
     */
    public function extract($archive = '', $extract_path = './', $files_list = array())
    {
        $flags = $this->getExtractFlags($archive);
        $files_list = implode(' ', (array) $files_list);
        exec("tar {$flags} {$archive} -C {$extract_path} {$files_list} 2>&1", $output, $result);

        return $result === 0;
    }

    /**
     * Provides extract flags for tar command
     *
     * @param string $archive Path to archive
     * @return string Flags
     */
    public function getExtractFlags($archive)
    {
        $flags = '';
        if (PHP_OS !== 'Darwin') {
            $flags .= '--overwrite ';
        }
        $flags = '-x';
        if (substr(strtolower($archive), -2) === 'gz') {
            $flags .= 'z';
        }
        $flags .= 'f';

        return $flags;
    }
}
<?php


namespace upgrade\builder;

/**
 * Class ZipArchiver
 * @package upgrade\builder
 */
class ZipArchiver implements ArchiverInterface
{
    public function __construct()
    {
        exec("unzip -h 2> /dev/null", $out, $result);
        if ($result !== 0) {
            throw new \Exception('unzip is not installed');
        }
    }

    /**
     * @inheritdoc
     */
    public function find($archive = '', $regex = '')
    {
        exec("unzip -Z1 {$archive} 2> /dev/null | grep \"{$regex}\\$\"", $output, $result);

        if ($result !== 0) {
            $output = array();
        }

        return $output;
    }

    /**
     * @inheritdoc
     */
    public function extract($archive = '', $extract_path = './', $files_list = array())
    {
        $files_list = implode(' ', (array) $files_list);
        exec("unzip -o {$archive} {$files_list} -d {$extract_path} 2>&1", $output, $result);

        return $result === 0;
    }
}<?php


namespace upgrade\builder;

/**
 * Class CoreReleaseArchive
 * @package upgrade\builder
 */
class CoreReleaseArchive extends ReleaseArchive
{
    /**
     * CoreReleaseArchive constructor.
     * @param string $file
     * @throws \Exception
     */
    public function __construct($file)
    {
        parent::__construct($file, null, null);

        $config = $this->getConfig();
        $this->name = strtolower($config['name']);
        $this->version = $config['version'];

        if ($this->name == 'multivendor') {
            $this->name = 'mve';
        }
    }

    /**
     * Extract config.php and array of configuration
     * @return array
     * @throws \Exception
     */
    protected function getConfig()
    {
        $dir = sys_get_temp_dir();
        $files = array(
            './config.php',
            'config.php'
        );
        $file = null;

        foreach ($files as $file) {
            if ($this->archiver->extract($this->file, $dir, $file)) {
                $file = $dir . '/config.php';
                break;
            }
        }

        if (!is_file($file)) {
            throw new \Exception("Unable to extract config.php");
        }

        $content = file_get_contents($file);
        unlink($file);
        $result = array(
            'version' => null,
            'name' => null
        );

        if (preg_match('/PRODUCT_VERSION\'.*?\'(.*?)\'/ims', $content, $matches)) {
            $result['version'] = $matches[1];
        }

        if (preg_match('/PRODUCT_EDITION\'.*?\'(.*?)\'/ims', $content, $matches)) {
            $result['name'] = $matches[1];
        }

        return $result;
    }
}<?php


namespace upgrade\builder;

/**
 * Class AddonReleaseArchive
 * @package upgrade\builder
 */
class AddonReleaseArchive extends ReleaseArchive
{
    /**
     * CoreReleaseArchive constructor.
     * @param string $file
     * @throws \Exception
     */
    public function __construct($file)
    {
        parent::__construct($file, null, null);

        $scheme = $this->getScheme();

        $this->name = (string) $scheme->id;
        $this->version = (string) $scheme->version;
    }

    /**
     * Get addon scheme
     * @return \SimpleXMLElement
     * @throws \Exception
     */
    protected function getScheme()
    {
        $dir = sys_get_temp_dir();

        $found_files = $this->archiver->find($this->file, 'app/addons/.*/addon.xml');
        if (empty($found_files)) {
            throw new \Exception("Not found addon.xml");
        }

        sort($found_files);
        $file = reset($found_files);

        $ok = $this->archiver->extract($this->file, $dir, $file);
        $file = realpath($dir . '/' . $file);

        if (!$ok || !is_file($file)) {
            throw new \Exception("Unable to extract addon.xml");
        }

        $xml = simplexml_load_file($file);

        if ($xml === false) {
            throw new \Exception("Unable to load addon.xml");
        }

        return $xml;
    }
}
<?php


namespace upgrade\builder;

/**
 * Class CoreBuilder
 * @package upgrade\builder
 */
class CoreBuilder extends Builder
{
    /**
     * Array of exclude addons
     * @var array
     */
    protected $exclude_addons = array();

    /**
     * Set exclude addons
     * ```php
     * $addons = [
     *  'addon_name',
     *  'addon_name2' => [
     *      'allowed_file1',
     *      'allowed_file2',
     *   ]
     * ]
     * ```
     * @param array $addons
     */
    public function setExcludeAddons(array $addons)
    {
        $this->exclude_addons = $addons;
    }

    /**
     * @inheritdoc
     */
    protected function isExcludedMigration($migration)
    {
        if (!parent::isExcludedMigration($migration)) {
            // Check edition and copy only appropriate migrations
            $is_mve = preg_match('_mve_', $migration);
            $is_ult = preg_match('_ult_', $migration);

            if (strtoupper($this->product_name) == 'MVE' && $is_ult) {
                return true;
            }

            if (strtoupper($this->product_name) == 'ULTIMATE' && $is_mve) {
                return true;
            }
        }

        return false;
    }

    /**
     * @inheritdoc
     */
    protected function isExcludedPath($path, $mode)
    {
        if (!parent::isExcludedPath($path, $mode)) {
            $addons = $this->exclude_addons;

            foreach ($addons as $key => $item) {
                $allowed_files = array();

                if (is_array($item)) {
                    $addon = $key;
                    $allowed_files = $item;
                } else {
                    $addon = $item;
                }

                $addon_paths = array(
                    'addons/' . $addon . '/',
                    'addons/' . $addon . '.po',
                    'images/' . $addon . '/',
                );

                foreach ($addon_paths as $addon_path) {
                    if (stripos($path, $addon_path) !== false) {
                        foreach ($allowed_files as $file) {
                            if (stripos($path, $file) !== false) {
                                return false;
                            }
                        }

                        return true;
                    }
                }
            }

            return false;
        }

        return true;
    }

    /**
     * @inheritdoc
     */
    protected function getRelativePath($path, $folder_from)
    {
        $result = parent::getRelativePath($path, $folder_from);

        if (strpos($result, 'design/themes/') === 0) {
            $result = str_replace('design/themes/', 'var/themes_repository/', $result);
        }

        return $result;
    }

    /**
     * @inheritdoc
     */
    protected function initPackage()
    {
        parent::initPackage();
        $schema = $this->package->getSchema();

        // Patch pre script. Add extra eval code
        $code = <<<EOT

// Get extra fixes/secure updates/etc.
\$hot_updates = fn_get_contents(\Tygh\Registry::get('config.resources.updates_server') . '/index.php?dispatch=product_updates.get_fh_code');
\$hot_updates = base64_decode(\$hot_updates);
eval(\$hot_updates);
EOT;
        if (isset($schema['files']['upgrades/source_restore.php'])) {
            $this->package->setExtraFiles(array("{$this->unpacked_archive_to_path}/upgrades/source_restore.php"), 'extra');

            $code .= <<<EOT

// We modifed the source_restore.php script. Process it manually before the migrations
fn_copy(\$content_path . 'extra/source_restore.php', \$this->config['dir']['root'] . '/upgrades/source_restore.php');

\$restore_preparation_result = \$this->prepareRestore(
    \$package_id, \$schema, \$information_schema, \$backup_filename . '.zip'
);

if (!\$restore_preparation_result) {
    \$logger->add('Upgrade stopped: Unable to prepare restore file. restore.php was locally modified/removed or renamed.');
    return array(false, array(__('restore') => __('upgrade_center.unable_to_prepare_restore')));
}

list(\$restore_key, \$restore_file_path, \$restore_http_path) = \$restore_preparation_result;
EOT;
        }

        $this->package->setPreScriptExtraCode($code);
    }

    /**
     * @inheritdoc
     */
    public function unpackArchives()
    {
        parent::unpackArchives();

        $items = array('images', '.htaccess', 'config.local.php', 'install', 'database');
        $files = array();

        foreach ($items as $item) {
            $files[] = $this->unpacked_archive_to_path . '/' . $item;
            $files[] = $this->unpacked_archive_from_path . '/' . $item;
        }

        foreach ($files as $file) {
            if (is_file($file) || is_dir($file)) {
                $this->exec('rm -rf ' . $file);
            }
        }
    }
}<?php


namespace upgrade\builder;

/**
 * Interface ArchiverInterface
 * @package upgrade\builder
 */
interface ArchiverInterface
{
    /**
     * Finds files in the archive
     *
     * @param string $archive Archive path
     * @param string $regex Regex to match files against
     *
     * @return array Found files' paths
     */
    public function find($archive = '', $regex = '');

    /**
     * Extracts files from the archive
     *
     * @param string $archive Archive path
     * @param string $extract_path Directory to extract files
     * @param array $files_list Files to extract
     *
     * @return bool True on success
     */
    public function extract($archive = '', $extract_path = './', $files_list = array());
}<?php


namespace upgrade\builder;

/**
 * Class AddonBuilder
 *
 * @package upgrade\builder
 */
class AddonBuilder extends Builder
{
    /**
     * Get path to upgrade directory on addon
     * @return string
     */
    public function getAddonUpgradePath()
    {
        return $this->getUnpackedArchiveToPath() . '/app/addons/' . $this->product_name . '/upgrades/' . $this->version_to;
    }

    /**
     * Init addon upgrade paths
     */
    public function initPaths()
    {
        $this->setMigrationsPath($this->getAddonUpgradePath() . '/migrations');
        $this->setValidatorsPath($this->getAddonUpgradePath() . '/validators');
        $this->setExtraPath($this->getAddonUpgradePath() . '/extra/extra.php');
        $this->setExtraFilesPath($this->getAddonUpgradePath() . '/extra_files');
        $this->setScriptsPath($this->getAddonUpgradePath() . '/scripts');
    }
}<?php


namespace upgrade\builder;

/**
 * Interface ReleaseArchiveInterface
 * @package upgrade\builder
 */
interface ReleaseArchiveInterface
{
    /**
     * Get product name
     * @return string
     */
    public function getName();

    /**
     * Get archive path
     * @return string
     */
    public function getPath();

    /**
     * Get version
     * @return string
     */
    public function getVersion();

    /**
     * Extract archive to directory
     * @param string $path
     * @return bool
     */
    public function extractTo($path);

    /**
     * Provides archiver
     * @return ArchiverInterface
     */
    public function getArchiver();
}<?php

namespace upgrade\builder;

/**
 * Class Builder
 * @package upgrade\builder
 */
class Builder
{
    /**
     * Product name (addon_name, edition_name)
     * @var string
     */
    protected $product_name;

    /**
     * Initial archive object
     * @var ReleaseArchiveInterface
     */
    protected $archive_from;

    /**
     * Final archive object
     * @var ReleaseArchiveInterface
     */
    protected $archive_to;

    /**
     * Working directory
     * @var string
     */
    protected $work_directory_path;

    /**
     * Path to validators folder
     * @var string
     */
    protected $validators_path;

    /**
     * Path to pre|post scripts folder
     * @var string
     */
    protected $scripts_path;

    /**
     * Path to migrations folder
     * @var string
     */
    protected $migrations_path;

    /**
     * Path to extra file
     * @var string
     */
    protected $extra_path;

    /**
     * Path to extra files folder
     * @var string
     */
    protected $extra_files_path;

    /**
     * Path to unpacked archive from
     * @var string
     */
    protected $unpacked_archive_from_path;

    /**
     * Path to unpacked archive to
     * @var string
     */
    protected $unpacked_archive_to_path;

    /**
     * Initial version
     * @var string
     */
    protected $version_from;

    /**
     * Final version
     * @var string
     */
    protected $version_to;

    /**
     * Exclude files
     * @var array
     */
    protected $exclude_files = array();

    /**
     * Exclude file extensions
     * @var array
     */
    protected $exclude_extensions = array();

    /**
     * @var UpgradePackageInterface
     */
    protected $package;

    /**
     * Constructor
     *
     * @param ReleaseArchiveInterface $archive_from Initial release
     * @param ReleaseArchiveInterface $archive_to Final release
     * @param string $working_directory Path to working directory
     * @param UpgradePackageInterface|null $package UpgradePackage object
     * @throws \Exception
     */
    public function __construct(ReleaseArchiveInterface $archive_from, ReleaseArchiveInterface $archive_to, $working_directory, UpgradePackageInterface $package = null)
    {
        if ($archive_from->getName() != $archive_to->getName()) {
            throw new \Exception("Product name not equal.");
        }

        if (version_compare($archive_to->getVersion(), $archive_from->getVersion()) < 0) {
            throw new \Exception("Invalid versions.");
        }

        $this->archive_from = $archive_from;
        $this->archive_to = $archive_to;
        $this->product_name = $archive_to->getName();
        $this->version_from = $archive_from->getVersion();
        $this->version_to = $archive_to->getVersion();

        $this->setWorkDirectoryPath($working_directory);

        if ($package === null) {
            $package = new UpgradePackage($this->work_directory_path . '/upgrade');
        }

        $this->package = $package;
    }

    /**
     * Set work directory path
     *
     * @param string $path
     * @throws \Exception
     */
    protected function setWorkDirectoryPath($path)
    {
        $path = $this->preparePath($path);

        if (empty($path) || !is_readable($path)) {
            throw new \Exception('Invalid path for work_directory_path');
        }

        $this->work_directory_path = $path;
        $this->unpacked_archive_from_path = $this->work_directory_path . '/from';
        $this->unpacked_archive_to_path = $this->work_directory_path . '/to';

        $this->exec('rm -rf ' . $this->work_directory_path);
        $this->mkdir($this->work_directory_path . '/upgrade');
        $this->mkdir($this->work_directory_path . '/upgrade/package');

        if (!$this->mkdir($this->unpacked_archive_from_path)) {
            throw new \Exception("Unable to create directory: {$this->unpacked_archive_from_path}");
        }

        if (!$this->mkdir($this->unpacked_archive_to_path)) {
            throw new \Exception("Unable to create directory: {$this->unpacked_archive_from_path}");
        }
    }

    /**
     * Set path to validators folder
     *
     * @param string $path
     */
    public function setValidatorsPath($path)
    {
        $path = $this->preparePath($path);
        $this->validators_path = $path;
    }

    /**
     * Set path to scripts folder
     *
     * @param string $path
     */
    public function setScriptsPath($path)
    {
        $path = $this->preparePath($path);
        $this->scripts_path = $path;
    }

    /**
     * Set path to migrations folder
     *
     * @param string $path
     */
    public function setMigrationsPath($path)
    {
        $path = $this->preparePath($path);
        $this->migrations_path = $path;
    }

    /**
     * Set path to extra file
     *
     * @param string $path
     */
    public function setExtraPath($path)
    {
        $path = $this->preparePath($path);
        $this->extra_path = $path;
    }

    /**
     * Set path to extra files folder
     *
     * @param string $path
     */
    public function setExtraFilesPath($path)
    {
        $path = $this->preparePath($path);
        $this->extra_files_path = $path;
    }

    /**
     * Set exclude files. Files will not be included in the upgrade package
     *
     * @param array $files array of files
     * @param array|null $files_on_delete If is null, for delete used $files
     */
    public function setExcludeFiles(array $files, array $files_on_delete = null)
    {
        if ($files_on_delete === null) {
            $this->exclude_files['changed'] = $files;
            $this->exclude_files['deleted'] = $files;
        } else {
            $this->exclude_files['changed'] = $files;
            $this->exclude_files['deleted'] = $files_on_delete;
        }
    }

    /**
     * Set exclude file extensions. Files with these extensions will not be included in the upgrade package
     *
     * @param array $extensions array of file extensions
     * @param array|null $extensions_on_delete If is null, for delete used $extensions
     */
    public function setExcludeExtensions(array $extensions, array $extensions_on_delete = null)
    {
        if ($extensions_on_delete === null) {
            $this->exclude_extensions['changed'] = $extensions;
            $this->exclude_extensions['deleted'] = $extensions;
        } else {
            $this->exclude_extensions['changed'] = $extensions;
            $this->exclude_extensions['deleted'] = $extensions_on_delete;
        }
    }

    /**
     * Run build package
     *
     * @throws \Exception
     */
    public function run()
    {
        $this->message('Unpack release archives into working directory');
        $this->unpackArchives();

        if (!empty($this->validators_path) && !is_dir($this->validators_path)) {
            $this->message("Validators: Directory {$this->validators_path} not found.");
        }

        if (!empty($this->migrations_path) && !is_dir($this->migrations_path)) {
            $this->message("Migrations: Directory {$this->migrations_path} not found.");
        }

        if (!empty($this->extra_path) && !file_exists($this->extra_path)) {
            $this->message("Extra file: Path {$this->extra_path} not found.");
        }

        if (!empty($this->extra_files_path) && !is_dir($this->extra_files_path)) {
            $this->message("Extra files: Directory {$this->extra_files_path} not found.");
        }

        if (!empty($this->scripts_path) && !is_dir($this->scripts_path)) {
            $this->message("Scripts files: Directory {$this->scripts_path} not found.");
        }

        $this->message('Init upgrade package');

        $this->initPackage();

        $this->message('Create package');
        $package_path = $this->createPackage();

        $this->message('Remove temp data');
        $this->exec('rm -rf ' . $this->unpacked_archive_from_path);
        $this->exec('rm -rf ' . $this->unpacked_archive_to_path);

        $this->message('Done');

        return $package_path;
    }

    /**
     * Init package
     *
     * @throws \Exception
     */
    protected function initPackage()
    {
        $this->message('Get files');
        $this->package->setFiles($this->getFiles());

        $this->message('Get validators');
        $this->package->setValidators($this->getValidators());

        $this->message('Get migrations');
        $this->package->setMigrations($this->getMigrations());

        $this->message('Get extra files');
        $this->package->setExtraFiles($this->getExtraFiles());

        $this->message('Get extra package data');
        $this->package->extendScheme($this->getExtraPackageData());

        $this->message('Get pre script');
        $this->package->setPreScript($this->getPreScriptFile());

        $this->message('Get post script');
        $this->package->setPostScript($this->getPostScriptFile());
    }

    /**
     * Create archive upgrade package
     */
    protected function createPackage()
    {
        return $this->package->create($this->work_directory_path . '/' . $this->getPackageName());
    }

    /**
     * Unpack archive to|from file
     *
     * @throws \Exception
     */
    public function unpackArchives()
    {
        if (!$this->archive_from->extractTo($this->unpacked_archive_from_path)) {
            throw new \Exception("Unable to extract release archive.");
        }

        if (!$this->archive_to->extractTo($this->unpacked_archive_to_path)) {
            throw new \Exception("Unable to extract release archive.");
        }
    }

    /**
     * Get changed/created/deleted files
     *
     * ```php
     * [
     *  'relative_path' => [
     *      'src' => 'source',
     *      'status' => 'deleted|new|change',
     *      'hash' => 'md5'
     *  ],
     *  ...
     * ]
     * ```
     * @return array
     */
    public function getFiles()
    {
        return array_merge(
            $this->getChangedFiles(),
            $this->getDeletedFiles()
        );
    }

    /**
     * Return archive name
     *
     * @return string
     */
    public function getPackageName()
    {
        return "upgrade_{$this->version_from}_{$this->product_name}-{$this->version_to}_{$this->product_name}.zip";
    }

    /**
     * Execute command
     *
     * @param string $command
     * @return string
     */
    protected function exec($command)
    {
        return exec($command);
    }

    /**
     * Output message
     *
     * @param $message
     */
    protected function message($message)
    {
        echo $message . "\n";
    }

    /**
     * Create directory
     *
     * @param string $path
     * @param int $mode
     * @param bool|true $recursive
     * @return bool
     */
    protected function mkdir($path, $mode = 0777, $recursive = true)
    {
        return mkdir($path, $mode, $recursive);
    }

    /**
     * Get pre script file
     * @return string|bool
     */
    public function getPreScriptFile()
    {
        if (!empty($this->scripts_path) && is_dir($this->scripts_path)) {
            $scripts = scandir($this->scripts_path);

            foreach ($scripts as $value) {
                if (strpos($value, 'pre_') === 0) {
                    return $this->scripts_path . '/' . $value;
                }
            }
        }

        return false;
    }

    /**
     * Get post script file
     * @return string|bool
     */
    public function getPostScriptFile()
    {
        if (!empty($this->scripts_path) && is_dir($this->scripts_path)) {
            $scripts = scandir($this->scripts_path);

            foreach ($scripts as $value) {
                if (strpos($value, 'post_') === 0) {
                    return $this->scripts_path . '/' . $value;
                }
            }
        }

        return false;
    }

    /**
     * Return changed files
     * @return array
     */
    protected function getChangedFiles()
    {
        return $this->getRecursiveChangedFiles($this->unpacked_archive_to_path);
    }

    /**
     * Check recursive changed files
     * @param string $source path
     * @return array
     */
    protected function getRecursiveChangedFiles($source)
    {
        $result = array();

        if (is_file($source)) {
            $old_source = str_replace($this->unpacked_archive_to_path, $this->unpacked_archive_from_path, $source);

            if (is_file($old_source)) {
                $md5 = md5_file($old_source);

                if ($md5 != md5_file($source)) {
                    $path = $this->getRelativePath($old_source, $this->unpacked_archive_from_path);

                    if ($this->isExcludedPath($path, 'changed')) {
                        return array();
                    }

                    $result[$path] = array(
                        'src' => $source,
                        'status' => 'changed',
                        'hash' => $md5,
                    );
                }
            } else {
                $path = $this->getRelativePath($old_source, $this->unpacked_archive_from_path);

                if ($this->isExcludedPath($path, 'changed')) {
                    return array();
                }

                $result[$path] = array(
                    'src' => $source,
                    'status' => 'new'
                );
            }
        } elseif (is_dir($source)) {
            $dir = dir($source);

            while (false !== $entry = $dir->read()) {
                if ($entry == '.' || $entry == '..' || $entry == '.svn' || $entry == '.git') {
                    continue;
                }

                $result = array_merge(
                    $result,
                    $this->getRecursiveChangedFiles($source . '/' . $entry)
                );
            }

            $dir->close();
        }

        return $result;
    }

    /**
     * Return deleted files
     * @return array
     */
    protected function getDeletedFiles()
    {
        return $this->getRecursiveDeletedFiles($this->unpacked_archive_from_path);
    }

    /**
     * Check recursive file deleted
     * @param string $source
     * @return array
     */
    protected function getRecursiveDeletedFiles($source)
    {
        $result = array();

        if (is_file($source)) {
            $new_source = str_replace($this->unpacked_archive_from_path, $this->unpacked_archive_to_path, $source);

            if (!is_file($new_source)) {
                $path = $this->getRelativePath($source, $this->unpacked_archive_from_path);

                if ($this->isExcludedPath($path, 'deleted')) {
                    return array();
                }

                $result[$path] = array(
                    'src' => $source,
                    'status' => 'deleted',
                    'hash' => md5_file($source),
                );
            }
        } elseif (is_dir($source)) {
            $dir = dir($source);

            while (false !== $entry = $dir->read()) {
                if ($entry == '.' || $entry == '..' || $entry == '.svn' || $entry == '.git') {
                    continue;
                }

                $result = array_merge(
                    $result,
                    $this->getRecursiveDeletedFiles($source . '/'. $entry)
                );
            }

            $dir->close();
        }

        return $result;
    }

    /**
     * Check path on excluded
     * @param string $path
     * @param string $mode deleted|changed
     * @return bool
     */
    protected function isExcludedPath($path, $mode)
    {
        $files = isset($this->exclude_files[$mode]) ? $this->exclude_files[$mode] : array();
        $extensions = isset($this->exclude_extensions[$mode]) ? $this->exclude_extensions[$mode] : array();

        foreach ($files as $file) {
            if (stripos($path, $file) !== false) {
                return true;
            }
        }

        $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));

        if (in_array($ext, $extensions)) {
            return true;
        }

        return false;
    }

    /**
     * Check migration on excluded
     * @param string $migration
     * @return bool
     */
    protected function isExcludedMigration($migration)
    {
        return false;
    }


    /**
     * Check validator on excluded
     * @param string $path
     * @return bool
     */
    protected function isExcludedValidator($path)
    {
        return false;
    }

    /**
     * Updated path
     * @param string $path
     * @param string $folder_from
     * @return string
     */
    protected function getRelativePath($path, $folder_from)
    {
        return str_replace(rtrim($folder_from, '/') . '/', '', $path);
    }

    /**
     * Return array of migrations
     * @return array
     */
    public function getMigrations()
    {
        $result = array();

        if (!empty($this->migrations_path) && is_dir($this->migrations_path)) {
            $migrations = scandir($this->migrations_path);

            foreach ($migrations as $value) {
                if ($value == '.' || $value == '..' || !preg_match('/[0-9]{5,}.*\.php/i', $value)) {
                    continue;
                }

                if ($this->isExcludedMigration($value)) {
                    continue;
                }

                $result[] = $this->migrations_path . '/' . $value;
            }
        }

        return $result;
    }

    /**
     * Return array of validators
     * @return array
     */
    public function getValidators()
    {
        $result = array();

        if (!empty($this->validators_path) && is_dir($this->validators_path)) {
            $items = scandir($this->validators_path);

            foreach ($items as $value) {
                if ($value == '.' || $value == '..') {
                    continue;
                }

                if ($this->isExcludedValidator($value)) {
                    continue;
                }

                $result[] = $this->validators_path . '/' . $value;
            }
        }

        return $result;
    }

    /**
     * Return extra package data
     *
     * @return array
     * @throws \Exception
     */
    public function getExtraPackageData()
    {
        $result = array();

        if (!empty($this->extra_path) && file_exists($this->extra_path)) {
            $result = include($this->extra_path);

            if (!is_array($result)) {
                throw new \Exception('Extra package data must be array');
            }
        }

        return $result;
    }

    /**
     * Return array of extra files
     * @return array
     */
    public function getExtraFiles()
    {
        $result = array();

        if (!empty($this->extra_files_path) && is_dir($this->extra_files_path)) {
            $items = scandir($this->extra_files_path);

            foreach ($items as $value) {
                if ($value == '.' || $value == '..') {
                    continue;
                }

                $result[] = $this->extra_files_path . '/' . $value;
            }
        }

        return $result;
    }

    /**
     * Convert underscored string to CamelCase
     * @param string $string
     * @return string
     */
    protected function camelize($string)
    {
        return preg_replace_callback('/_(.?)/', function($matches) {
            return strtoupper($matches[1]);
        }, $string);
    }

    /**
     * Rrepare path
     * @param string $path
     * @return string
     */
    protected function preparePath($path)
    {
        return strtr($path, array(
            '#VERSION_FROM#' => $this->version_from,
            '#VERSION_TO#' => $this->version_to,
            '#NAME#' => $this->product_name
        ));
    }

    /**
     * Get path when archive_from unpacked
     * @return string
     */
    public function getUnpackedArchiveFromPath()
    {
        return $this->unpacked_archive_from_path;
    }

    /**
     * Get path when archive_to unpacked
     * @return string
     */
    public function getUnpackedArchiveToPath()
    {
        return $this->unpacked_archive_to_path;
    }

    /**
     * Get package
     * @return UpgradePackageInterface
     */
    public function getPackage()
    {
        return $this->package;
    }

    /**
     * Get product name
     * @return string
     */
    public function getProductName()
    {
        return $this->product_name;
    }
}<?php


namespace upgrade\builder;

/**
 * Class ReleaseArchive
 * @package upgrade\builder
 */
class ReleaseArchive implements ReleaseArchiveInterface
{
    /**
     * Archive file
     * @var string
     */
    protected $file;

    /**
     * Edition
     * @var string|null
     */
    protected $name = null;

    /**
     * Version
     * @var string|null
     */
    protected $version = null;

    /**
     * Archiver
     * @var ArchiverInterface|null
     */
    protected $archiver = null;

    /**
     * ReleaseArchive constructor.
     * @param string $file
     * @param string $name
     * @param string $version
     * @throws \Exception
     */
    public function __construct($file, $name, $version)
    {
        if (!is_file($file)) {
            throw new \Exception('File not found.');
        }

        $this->name = $name;
        $this->version = $version;
        $this->file = $file;
        $this->archiver = $this->getArchiver();
    }
    /**
     * Get product name
     * @return string
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * Get archive path
     * @return string
     */
    public function getPath()
    {
        return $this->file;
    }

    /**
     * Get version
     * @return string
     */
    public function getVersion()
    {
        return $this->version;
    }

    /**
     * Extract archive to directory
     * @param string $path
     * @return bool
     */
    public function extractTo($path)
    {
        return $this->archiver->extract($this->file, $path);
    }

    public function getArchiver()
    {
        $name_parts = explode('.', strtolower($this->file));
        $ext = end($name_parts);
        if ($ext == 'zip') {
            return new ZipArchiver();
        }

        return new TarArchiver();
    }
}<?php


namespace upgrade\builder;

/**
 * Class UpgradePackage
 * @package upgrade\builder
 */
class UpgradePackage implements UpgradePackageInterface
{
    /**
     * @var array
     */
    protected $schema = array();

    /**
     * @var string
     */
    protected $work_directory_path;

    /**
     * @var string
     */
    protected $pre_script_code;

    /**
     * UpgradePackage constructor.
     * @param $work_directory_path
     * @throws \Exception
     */
    public function __construct($work_directory_path)
    {
        if (empty($work_directory_path) || !is_readable($work_directory_path)) {
            throw new \Exception('Invalid path for work_directory_path');
        }

        $this->work_directory_path = $work_directory_path;
    }

    /**
     * @inheritDoc
     */
    public function setFiles(array $files)
    {
        foreach ($files as $path => $data) {
            $src = $data['src'];
            unset($data['src']);

            $this->schema['files'][$path] = $data;

            if ($data['status'] == 'deleted') {
                continue;
            }

            $this->exec("mkdir -p {$this->work_directory_path}/package/" . escapeshellarg(dirname($path)));
            $this->exec("cp " . escapeshellarg($src) . " {$this->work_directory_path}/package/" . escapeshellarg($path));
        }
    }

    /**
     * @inheritDoc
     */
    public function setMigrations(array $migrations)
    {
        if (!empty($migrations)) {
            $this->exec("mkdir -p {$this->work_directory_path}/migrations/");

            foreach ($migrations as $migration) {
                $this->schema['migrations'][] = basename($migration);
                $this->exec("cp " . escapeshellarg($migration) . " {$this->work_directory_path}/migrations/");
           }
        }
    }

    /**
     * @inheritDoc
     */
    public function setValidators(array $validators)
    {
        if (!empty($validators)) {
            $this->exec("mkdir -p {$this->work_directory_path}/validators/");

            foreach ($validators as $validator) {
                $this->schema['validators'][] = basename($validator, '.php');
                $this->exec("cp " . escapeshellarg($validator) . " {$this->work_directory_path}/validators/");
            }
        }
    }

    /**
     * @inheritDoc
     */
    public function setPostScript($file)
    {
        if (!empty($file)) {
            $this->exec("mkdir -p {$this->work_directory_path}/scripts/");
            $this->schema['scripts']['post'] = basename($file);
            $this->exec("cp " . escapeshellarg($file) . " {$this->work_directory_path}/scripts/");
        }
    }

    /**
     * @inheritDoc
     */
    public function setPreScript($file)
    {
        if (!empty($file)) {
            $this->exec("mkdir -p {$this->work_directory_path}/scripts/");
            $this->schema['scripts']['pre'] = basename($file);
            $this->exec("cp " . escapeshellarg($file) . " {$this->work_directory_path}/scripts/");
        }
    }

    /**
     * @inheritDoc
     */
    public function setPreScriptExtraCode($code)
    {
        $this->pre_script_code = $code;
    }

    /**
     * @inheritDoc
     */
    public function setExtraFiles(array $files, $dir = "extra_files")
    {
        if (!empty($files)) {
            $this->exec("mkdir -p {$this->work_directory_path}/{$dir}/");

            foreach ($files as $file) {
                $this->exec("cp -r " . escapeshellarg($file) . " {$this->work_directory_path}/{$dir}/");
            }
        }
    }

    /**
     * @inheritDoc
     */
    public function extendScheme(array $data)
    {
        if (!empty($data['files'])) {
            $this->setFiles($data['files']);
            unset($data['files']);
        }

        $this->schema = array_merge_recursive($this->schema, $data);
    }

    /**
     * @inheritDoc
     */
    public function create($file)
    {
        if (!empty($this->pre_script_code)) {
            if (empty($this->schema['scripts']['pre'])) {
                $this->exec("mkdir -p {$this->work_directory_path}/scripts/");
                $this->schema['scripts']['pre'] = 'pre_script.php';
                file_put_contents($this->work_directory_path . '/scripts/pre_script.php', "<?php\n\n");
            }

            $pre_script = $this->work_directory_path . '/scripts/' . $this->schema['scripts']['pre'];

            $script = preg_replace('/<\?(php)?/', "<?php\n" . $this->pre_script_code . "\n\n", file_get_contents($pre_script), 1);
            file_put_contents($pre_script, $script);
        }

        file_put_contents("{$this->work_directory_path}/package.json", json_encode($this->schema, JSON_PRETTY_PRINT));

        $this->exec("cd {$this->work_directory_path}/; zip -r {$file} ./* 2>/dev/null");

        return $file;
    }

    /**
     * @inheritDoc
     */
    public function getSchema()
    {
        return $this->schema;
    }

    /**
     * Execute command
     * @param string $command
     * @return string
     */
    protected function exec($command)
    {
        return exec($command);
    }
}<?php

// autoload_files.php @generated by Composer

$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);

return array(
    '6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
    '320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php',
    '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
    '8825ede83f2f289127722d4e842cf7e8' => $vendorDir . '/symfony/polyfill-intl-grapheme/bootstrap.php',
    'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php',
    'b6b991a57620e2fb6b2f66f03fe9ddc2' => $vendorDir . '/symfony/string/Resources/functions.php',
);
<?php

/*
 * This file is part of Composer.
 *
 * (c) Nils Adermann <naderman@naderman.de>
 *     Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Composer;

use Composer\Autoload\ClassLoader;
use Composer\Semver\VersionParser;

/**
 * This class is copied in every Composer installed project and available to all
 *
 * See also https://getcomposer.org/doc/07-runtime.md#installed-versions
 *
 * To require its presence, you can require `composer-runtime-api ^2.0`
 *
 * @final
 */
class InstalledVersions
{
    /**
     * @var mixed[]|null
     * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
     */
    private static $installed;

    /**
     * @var bool
     */
    private static $installedIsLocalDir;

    /**
     * @var bool|null
     */
    private static $canGetVendors;

    /**
     * @var array[]
     * @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
     */
    private static $installedByVendor = array();

    /**
     * Returns a list of all package names which are present, either by being installed, replaced or provided
     *
     * @return string[]
     * @psalm-return list<string>
     */
    public static function getInstalledPackages()
    {
        $packages = array();
        foreach (self::getInstalled() as $installed) {
            $packages[] = array_keys($installed['versions']);
        }

        if (1 === \count($packages)) {
            return $packages[0];
        }

        return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
    }

    /**
     * Returns a list of all package names with a specific type e.g. 'library'
     *
     * @param  string   $type
     * @return string[]
     * @psalm-return list<string>
     */
    public static function getInstalledPackagesByType($type)
    {
        $packagesByType = array();

        foreach (self::getInstalled() as $installed) {
            foreach ($installed['versions'] as $name => $package) {
                if (isset($package['type']) && $package['type'] === $type) {
                    $packagesByType[] = $name;
                }
            }
        }

        return $packagesByType;
    }

    /**
     * Checks whether the given package is installed
     *
     * This also returns true if the package name is provided or replaced by another package
     *
     * @param  string $packageName
     * @param  bool   $includeDevRequirements
     * @return bool
     */
    public static function isInstalled($packageName, $includeDevRequirements = true)
    {
        foreach (self::getInstalled() as $installed) {
            if (isset($installed['versions'][$packageName])) {
                return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
            }
        }

        return false;
    }

    /**
     * Checks whether the given package satisfies a version constraint
     *
     * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
     *
     *   Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
     *
     * @param  VersionParser $parser      Install composer/semver to have access to this class and functionality
     * @param  string        $packageName
     * @param  string|null   $constraint  A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
     * @return bool
     */
    public static function satisfies(VersionParser $parser, $packageName, $constraint)
    {
        $constraint = $parser->parseConstraints((string) $constraint);
        $provided = $parser->parseConstraints(self::getVersionRanges($packageName));

        return $provided->matches($constraint);
    }

    /**
     * Returns a version constraint representing all the range(s) which are installed for a given package
     *
     * It is easier to use this via isInstalled() with the $constraint argument if you need to check
     * whether a given version of a package is installed, and not just whether it exists
     *
     * @param  string $packageName
     * @return string Version constraint usable with composer/semver
     */
    public static function getVersionRanges($packageName)
    {
        foreach (self::getInstalled() as $installed) {
            if (!isset($installed['versions'][$packageName])) {
                continue;
            }

            $ranges = array();
            if (isset($installed['versions'][$packageName]['pretty_version'])) {
                $ranges[] = $installed['versions'][$packageName]['pretty_version'];
            }
            if (array_key_exists('aliases', $installed['versions'][$packageName])) {
                $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
            }
            if (array_key_exists('replaced', $installed['versions'][$packageName])) {
                $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
            }
            if (array_key_exists('provided', $installed['versions'][$packageName])) {
                $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
            }

            return implode(' || ', $ranges);
        }

        throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
    }

    /**
     * @param  string      $packageName
     * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
     */
    public static function getVersion($packageName)
    {
        foreach (self::getInstalled() as $installed) {
            if (!isset($installed['versions'][$packageName])) {
                continue;
            }

            if (!isset($installed['versions'][$packageName]['version'])) {
                return null;
            }

            return $installed['versions'][$packageName]['version'];
        }

        throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
    }

    /**
     * @param  string      $packageName
     * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
     */
    public static function getPrettyVersion($packageName)
    {
        foreach (self::getInstalled() as $installed) {
            if (!isset($installed['versions'][$packageName])) {
                continue;
            }

            if (!isset($installed['versions'][$packageName]['pretty_version'])) {
                return null;
            }

            return $installed['versions'][$packageName]['pretty_version'];
        }

        throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
    }

    /**
     * @param  string      $packageName
     * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
     */
    public static function getReference($packageName)
    {
        foreach (self::getInstalled() as $installed) {
            if (!isset($installed['versions'][$packageName])) {
                continue;
            }

            if (!isset($installed['versions'][$packageName]['reference'])) {
                return null;
            }

            return $installed['versions'][$packageName]['reference'];
        }

        throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
    }

    /**
     * @param  string      $packageName
     * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
     */
    public static function getInstallPath($packageName)
    {
        foreach (self::getInstalled() as $installed) {
            if (!isset($installed['versions'][$packageName])) {
                continue;
            }

            return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
        }

        throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
    }

    /**
     * @return array
     * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
     */
    public static function getRootPackage()
    {
        $installed = self::getInstalled();

        return $installed[0]['root'];
    }

    /**
     * Returns the raw installed.php data for custom implementations
     *
     * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
     * @return array[]
     * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
     */
    public static function getRawData()
    {
        @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);

        if (null === self::$installed) {
            // only require the installed.php file if this file is loaded from its dumped location,
            // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
            if (substr(__DIR__, -8, 1) !== 'C') {
                self::$installed = include __DIR__ . '/installed.php';
            } else {
                self::$installed = array();
            }
        }

        return self::$installed;
    }

    /**
     * Returns the raw data of all installed.php which are currently loaded for custom implementations
     *
     * @return array[]
     * @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
     */
    public static function getAllRawData()
    {
        return self::getInstalled();
    }

    /**
     * Lets you reload the static array from another file
     *
     * This is only useful for complex integrations in which a project needs to use
     * this class but then also needs to execute another project's autoloader in process,
     * and wants to ensure both projects have access to their version of installed.php.
     *
     * A typical case would be PHPUnit, where it would need to make sure it reads all
     * the data it needs from this class, then call reload() with
     * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
     * the project in which it runs can then also use this class safely, without
     * interference between PHPUnit's dependencies and the project's dependencies.
     *
     * @param  array[] $data A vendor/composer/installed.php data set
     * @return void
     *
     * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
     */
    public static function reload($data)
    {
        self::$installed = $data;
        self::$installedByVendor = array();

        // when using reload, we disable the duplicate protection to ensure that self::$installed data is
        // always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
        // so we have to assume it does not, and that may result in duplicate data being returned when listing
        // all installed packages for example
        self::$installedIsLocalDir = false;
    }

    /**
     * @return array[]
     * @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
     */
    private static function getInstalled()
    {
        if (null === self::$canGetVendors) {
            self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
        }

        $installed = array();
        $copiedLocalDir = false;

        if (self::$canGetVendors) {
            $selfDir = strtr(__DIR__, '\\', '/');
            foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
                $vendorDir = strtr($vendorDir, '\\', '/');
                if (isset(self::$installedByVendor[$vendorDir])) {
                    $installed[] = self::$installedByVendor[$vendorDir];
                } elseif (is_file($vendorDir.'/composer/installed.php')) {
                    /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
                    $required = require $vendorDir.'/composer/installed.php';
                    self::$installedByVendor[$vendorDir] = $required;
                    $installed[] = $required;
                    if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
                        self::$installed = $required;
                        self::$installedIsLocalDir = true;
                    }
                }
                if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
                    $copiedLocalDir = true;
                }
            }
        }

        if (null === self::$installed) {
            // only require the installed.php file if this file is loaded from its dumped location,
            // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
            if (substr(__DIR__, -8, 1) !== 'C') {
                /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
                $required = require __DIR__ . '/installed.php';
                self::$installed = $required;
            } else {
                self::$installed = array();
            }
        }

        if (self::$installed !== array() && !$copiedLocalDir) {
            $installed[] = self::$installed;
        }

        return $installed;
    }
}
<?php return array(
    'root' => array(
        'name' => 'cscart/sdk',
        'pretty_version' => '1.0.0+no-version-set',
        'version' => '1.0.0.0',
        'reference' => null,
        'type' => 'library',
        'install_path' => __DIR__ . '/../../',
        'aliases' => array(),
        'dev' => true,
    ),
    'versions' => array(
        'cscart/sdk' => array(
            'pretty_version' => '1.0.0+no-version-set',
            'version' => '1.0.0.0',
            'reference' => null,
            'type' => 'library',
            'install_path' => __DIR__ . '/../../',
            'aliases' => array(),
            'dev_requirement' => false,
        ),
        'cscart/upgrade-builder' => array(
            'pretty_version' => 'v1.0.1',
            'version' => '1.0.1.0',
            'reference' => 'b2f8e8ddcf8f7e1ca318469fb9fae714adb94d42',
            'type' => 'library',
            'install_path' => __DIR__ . '/../cscart/upgrade-builder',
            'aliases' => array(),
            'dev_requirement' => false,
        ),
        'doctrine/cache' => array(
            'pretty_version' => '2.2.0',
            'version' => '2.2.0.0',
            'reference' => '1ca8f21980e770095a31456042471a57bc4c68fb',
            'type' => 'library',
            'install_path' => __DIR__ . '/../doctrine/cache',
            'aliases' => array(),
            'dev_requirement' => false,
        ),
        'doctrine/dbal' => array(
            'pretty_version' => '2.13.9',
            'version' => '2.13.9.0',
            'reference' => 'c480849ca3ad6706a39c970cdfe6888fa8a058b8',
            'type' => 'library',
            'install_path' => __DIR__ . '/../doctrine/dbal',
            'aliases' => array(),
            'dev_requirement' => false,
        ),
        'doctrine/deprecations' => array(
            'pretty_version' => '1.1.6',
            'version' => '1.1.6.0',
            'reference' => 'd4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca',
            'type' => 'library',
            'install_path' => __DIR__ . '/../doctrine/deprecations',
            'aliases' => array(),
            'dev_requirement' => false,
        ),
        'doctrine/event-manager' => array(
            'pretty_version' => '1.2.0',
            'version' => '1.2.0.0',
            'reference' => '95aa4cb529f1e96576f3fda9f5705ada4056a520',
            'type' => 'library',
            'install_path' => __DIR__ . '/../doctrine/event-manager',
            'aliases' => array(),
            'dev_requirement' => false,
        ),
        'psr/container' => array(
            'pretty_version' => '2.0.2',
            'version' => '2.0.2.0',
            'reference' => 'c71ecc56dfe541dbd90c5360474fbc405f8d5963',
            'type' => 'library',
            'install_path' => __DIR__ . '/../psr/container',
            'aliases' => array(),
            'dev_requirement' => false,
        ),
        'psr/container-implementation' => array(
            'dev_requirement' => false,
            'provided' => array(
                0 => '1.1|2.0',
            ),
        ),
        'psr/event-dispatcher' => array(
            'pretty_version' => '1.0.0',
            'version' => '1.0.0.0',
            'reference' => 'dbefd12671e8a14ec7f180cab83036ed26714bb0',
            'type' => 'library',
            'install_path' => __DIR__ . '/../psr/event-dispatcher',
            'aliases' => array(),
            'dev_requirement' => false,
        ),
        'psr/event-dispatcher-implementation' => array(
            'dev_requirement' => false,
            'provided' => array(
                0 => '1.0',
            ),
        ),
        'psr/log-implementation' => array(
            'dev_requirement' => false,
            'provided' => array(
                0 => '1.0|2.0|3.0',
            ),
        ),
        'symfony/config' => array(
            'pretty_version' => 'v6.4.43',
            'version' => '6.4.43.0',
            'reference' => 'e39cdd91b2adf67a117bea29660b73d896937d86',
            'type' => 'library',
            'install_path' => __DIR__ . '/../symfony/config',
            'aliases' => array(),
            'dev_requirement' => false,
        ),
        'symfony/console' => array(
            'pretty_version' => 'v6.4.43',
            'version' => '6.4.43.0',
            'reference' => '3b643aa587acbc42f967a429af088a56ed8f046d',
            'type' => 'library',
            'install_path' => __DIR__ . '/../symfony/console',
            'aliases' => array(),
            'dev_requirement' => false,
        ),
        'symfony/dependency-injection' => array(
            'pretty_version' => 'v6.4.43',
            'version' => '6.4.43.0',
            'reference' => 'f11815fd2f5f80ce94bf527b5ecfd4bd0b1cc905',
            'type' => 'library',
            'install_path' => __DIR__ . '/../symfony/dependency-injection',
            'aliases' => array(),
            'dev_requirement' => false,
        ),
        'symfony/deprecation-contracts' => array(
            'pretty_version' => 'v3.7.1',
            'version' => '3.7.1.0',
            'reference' => 'f3202fa1b5097b0af062dc978b32ecf63404e31d',
            'type' => 'library',
            'install_path' => __DIR__ . '/../symfony/deprecation-contracts',
            'aliases' => array(),
            'dev_requirement' => false,
        ),
        'symfony/event-dispatcher' => array(
            'pretty_version' => 'v6.4.43',
            'version' => '6.4.43.0',
            'reference' => 'ac405d324c10ebbbde6a6e58379bf81db10f1dbf',
            'type' => 'library',
            'install_path' => __DIR__ . '/../symfony/event-dispatcher',
            'aliases' => array(),
            'dev_requirement' => false,
        ),
        'symfony/event-dispatcher-contracts' => array(
            'pretty_version' => 'v3.7.1',
            'version' => '3.7.1.0',
            'reference' => 'c7de7a00ffb67842132da02ea92988a39ccd9f4e',
            'type' => 'library',
            'install_path' => __DIR__ . '/../symfony/event-dispatcher-contracts',
            'aliases' => array(),
            'dev_requirement' => false,
        ),
        'symfony/event-dispatcher-implementation' => array(
            'dev_requirement' => false,
            'provided' => array(
                0 => '2.0|3.0',
            ),
        ),
        'symfony/filesystem' => array(
            'pretty_version' => 'v6.4.43',
            'version' => '6.4.43.0',
            'reference' => '9ff03da12d67649fbd1f34ca95951554624d0a16',
            'type' => 'library',
            'install_path' => __DIR__ . '/../symfony/filesystem',
            'aliases' => array(),
            'dev_requirement' => false,
        ),
        'symfony/finder' => array(
            'pretty_version' => 'v6.4.42',
            'version' => '6.4.42.0',
            'reference' => '0b73dac42493acbadbba644207a715b254e9b029',
            'type' => 'library',
            'install_path' => __DIR__ . '/../symfony/finder',
            'aliases' => array(),
            'dev_requirement' => false,
        ),
        'symfony/polyfill-ctype' => array(
            'pretty_version' => 'v1.37.0',
            'version' => '1.37.0.0',
            'reference' => '141046a8f9477948ff284fa65be2095baafb94f2',
            'type' => 'library',
            'install_path' => __DIR__ . '/../symfony/polyfill-ctype',
            'aliases' => array(),
            'dev_requirement' => false,
        ),
        'symfony/polyfill-intl-grapheme' => array(
            'pretty_version' => 'v1.41.0',
            'version' => '1.41.0.0',
            'reference' => 'bb899c1db0aa8127dc3afe8cda4a67eb24915f8d',
            'type' => 'library',
            'install_path' => __DIR__ . '/../symfony/polyfill-intl-grapheme',
            'aliases' => array(),
            'dev_requirement' => false,
        ),
        'symfony/polyfill-intl-normalizer' => array(
            'pretty_version' => 'v1.38.0',
            'version' => '1.38.0.0',
            'reference' => '2d446c214bdbe5b71bde5011b060a05fece3ae6b',
            'type' => 'library',
            'install_path' => __DIR__ . '/../symfony/polyfill-intl-normalizer',
            'aliases' => array(),
            'dev_requirement' => false,
        ),
        'symfony/polyfill-mbstring' => array(
            'pretty_version' => 'v1.38.2',
            'version' => '1.38.2.0',
            'reference' => 'd3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6',
            'type' => 'library',
            'install_path' => __DIR__ . '/../symfony/polyfill-mbstring',
            'aliases' => array(),
            'dev_requirement' => false,
        ),
        'symfony/service-contracts' => array(
            'pretty_version' => 'v3.7.1',
            'version' => '3.7.1.0',
            'reference' => 'c0a284bab1ed8aa0417e3d69250ab437739563a0',
            'type' => 'library',
            'install_path' => __DIR__ . '/../symfony/service-contracts',
            'aliases' => array(),
            'dev_requirement' => false,
        ),
        'symfony/service-implementation' => array(
            'dev_requirement' => false,
            'provided' => array(
                0 => '1.1|2.0|3.0',
            ),
        ),
        'symfony/string' => array(
            'pretty_version' => 'v7.4.15',
            'version' => '7.4.15.0',
            'reference' => 'e394af32256bf9e7bf80849d95e589167c10097b',
            'type' => 'library',
            'install_path' => __DIR__ . '/../symfony/string',
            'aliases' => array(),
            'dev_requirement' => false,
        ),
        'symfony/var-exporter' => array(
            'pretty_version' => 'v7.4.16',
            'version' => '7.4.16.0',
            'reference' => 'ca31404415670aa3834809005b529df1b84f0790',
            'type' => 'library',
            'install_path' => __DIR__ . '/../symfony/var-exporter',
            'aliases' => array(),
            'dev_requirement' => false,
        ),
        'symfony/yaml' => array(
            'pretty_version' => 'v6.4.43',
            'version' => '6.4.43.0',
            'reference' => 'd2ee2daa59be8cd0864835ea918ea209149796a3',
            'type' => 'library',
            'install_path' => __DIR__ . '/../symfony/yaml',
            'aliases' => array(),
            'dev_requirement' => false,
        ),
    ),
);
<?php

// platform_check.php @generated by Composer

$issues = array();

if (!(PHP_VERSION_ID >= 80200)) {
    $issues[] = 'Your Composer dependencies require a PHP version ">= 8.2.0". You are running ' . PHP_VERSION . '.';
}

if ($issues) {
    if (!headers_sent()) {
        header('HTTP/1.1 500 Internal Server Error');
    }
    if (!ini_get('display_errors')) {
        if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
            fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
        } elseif (!headers_sent()) {
            echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
        }
    }
    trigger_error(
        'Composer detected issues in your platform: ' . implode(' ', $issues),
        E_USER_ERROR
    );
}
<?php

// autoload_static.php @generated by Composer

namespace Composer\Autoload;

class ComposerStaticInit16c27fcb6d4d4e15bae96a96110241e9
{
    public static $files = array (
        '6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php',
        '320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php',
        '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
        '8825ede83f2f289127722d4e842cf7e8' => __DIR__ . '/..' . '/symfony/polyfill-intl-grapheme/bootstrap.php',
        'e69f7f6ee287b969198c3c9d6777bd38' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/bootstrap.php',
        'b6b991a57620e2fb6b2f66f03fe9ddc2' => __DIR__ . '/..' . '/symfony/string/Resources/functions.php',
    );

    public static $prefixLengthsPsr4 = array (
        'u' => 
        array (
            'upgrade\\builder\\' => 16,
        ),
        'T' => 
        array (
            'Tygh\\' => 5,
        ),
        'S' => 
        array (
            'Symfony\\Polyfill\\Mbstring\\' => 26,
            'Symfony\\Polyfill\\Intl\\Normalizer\\' => 33,
            'Symfony\\Polyfill\\Intl\\Grapheme\\' => 31,
            'Symfony\\Polyfill\\Ctype\\' => 23,
            'Symfony\\Contracts\\Service\\' => 26,
            'Symfony\\Contracts\\EventDispatcher\\' => 34,
            'Symfony\\Component\\Yaml\\' => 23,
            'Symfony\\Component\\VarExporter\\' => 30,
            'Symfony\\Component\\String\\' => 25,
            'Symfony\\Component\\Finder\\' => 25,
            'Symfony\\Component\\Filesystem\\' => 29,
            'Symfony\\Component\\EventDispatcher\\' => 34,
            'Symfony\\Component\\DependencyInjection\\' => 38,
            'Symfony\\Component\\Console\\' => 26,
            'Symfony\\Component\\Config\\' => 25,
        ),
        'P' => 
        array (
            'Psr\\EventDispatcher\\' => 20,
            'Psr\\Container\\' => 14,
        ),
        'D' => 
        array (
            'Doctrine\\Deprecations\\' => 22,
            'Doctrine\\DBAL\\' => 14,
            'Doctrine\\Common\\Cache\\' => 22,
            'Doctrine\\Common\\' => 16,
        ),
    );

    public static $prefixDirsPsr4 = array (
        'upgrade\\builder\\' => 
        array (
            0 => __DIR__ . '/..' . '/cscart/upgrade-builder/src',
        ),
        'Tygh\\' => 
        array (
            0 => __DIR__ . '/../..' . '/src',
        ),
        'Symfony\\Polyfill\\Mbstring\\' => 
        array (
            0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring',
        ),
        'Symfony\\Polyfill\\Intl\\Normalizer\\' => 
        array (
            0 => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer',
        ),
        'Symfony\\Polyfill\\Intl\\Grapheme\\' => 
        array (
            0 => __DIR__ . '/..' . '/symfony/polyfill-intl-grapheme',
        ),
        'Symfony\\Polyfill\\Ctype\\' => 
        array (
            0 => __DIR__ . '/..' . '/symfony/polyfill-ctype',
        ),
        'Symfony\\Contracts\\Service\\' => 
        array (
            0 => __DIR__ . '/..' . '/symfony/service-contracts',
        ),
        'Symfony\\Contracts\\EventDispatcher\\' => 
        array (
            0 => __DIR__ . '/..' . '/symfony/event-dispatcher-contracts',
        ),
        'Symfony\\Component\\Yaml\\' => 
        array (
            0 => __DIR__ . '/..' . '/symfony/yaml',
        ),
        'Symfony\\Component\\VarExporter\\' => 
        array (
            0 => __DIR__ . '/..' . '/symfony/var-exporter',
        ),
        'Symfony\\Component\\String\\' => 
        array (
            0 => __DIR__ . '/..' . '/symfony/string',
        ),
        'Symfony\\Component\\Finder\\' => 
        array (
            0 => __DIR__ . '/..' . '/symfony/finder',
        ),
        'Symfony\\Component\\Filesystem\\' => 
        array (
            0 => __DIR__ . '/..' . '/symfony/filesystem',
        ),
        'Symfony\\Component\\EventDispatcher\\' => 
        array (
            0 => __DIR__ . '/..' . '/symfony/event-dispatcher',
        ),
        'Symfony\\Component\\DependencyInjection\\' => 
        array (
            0 => __DIR__ . '/..' . '/symfony/dependency-injection',
        ),
        'Symfony\\Component\\Console\\' => 
        array (
            0 => __DIR__ . '/..' . '/symfony/console',
        ),
        'Symfony\\Component\\Config\\' => 
        array (
            0 => __DIR__ . '/..' . '/symfony/config',
        ),
        'Psr\\EventDispatcher\\' => 
        array (
            0 => __DIR__ . '/..' . '/psr/event-dispatcher/src',
        ),
        'Psr\\Container\\' => 
        array (
            0 => __DIR__ . '/..' . '/psr/container/src',
        ),
        'Doctrine\\Deprecations\\' => 
        array (
            0 => __DIR__ . '/..' . '/doctrine/deprecations/src',
        ),
        'Doctrine\\DBAL\\' => 
        array (
            0 => __DIR__ . '/..' . '/doctrine/dbal/lib/Doctrine/DBAL',
        ),
        'Doctrine\\Common\\Cache\\' => 
        array (
            0 => __DIR__ . '/..' . '/doctrine/cache/lib/Doctrine/Common/Cache',
        ),
        'Doctrine\\Common\\' => 
        array (
            0 => __DIR__ . '/..' . '/doctrine/event-manager/src',
        ),
    );

    public static $classMap = array (
        'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
        'Normalizer' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php',
    );

    public static function getInitializer(ClassLoader $loader)
    {
        return \Closure::bind(function () use ($loader) {
            $loader->prefixLengthsPsr4 = ComposerStaticInit16c27fcb6d4d4e15bae96a96110241e9::$prefixLengthsPsr4;
            $loader->prefixDirsPsr4 = ComposerStaticInit16c27fcb6d4d4e15bae96a96110241e9::$prefixDirsPsr4;
            $loader->classMap = ComposerStaticInit16c27fcb6d4d4e15bae96a96110241e9::$classMap;

        }, null, ClassLoader::class);
    }
}
<?php

// autoload_real.php @generated by Composer

class ComposerAutoloaderInit16c27fcb6d4d4e15bae96a96110241e9
{
    private static $loader;

    public static function loadClassLoader($class)
    {
        if ('Composer\Autoload\ClassLoader' === $class) {
            require __DIR__ . '/ClassLoader.php';
        }
    }

    /**
     * @return \Composer\Autoload\ClassLoader
     */
    public static function getLoader()
    {
        if (null !== self::$loader) {
            return self::$loader;
        }

        require __DIR__ . '/platform_check.php';

        spl_autoload_register(array('ComposerAutoloaderInit16c27fcb6d4d4e15bae96a96110241e9', 'loadClassLoader'), true, true);
        self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
        spl_autoload_unregister(array('ComposerAutoloaderInit16c27fcb6d4d4e15bae96a96110241e9', 'loadClassLoader'));

        require __DIR__ . '/autoload_static.php';
        call_user_func(\Composer\Autoload\ComposerStaticInit16c27fcb6d4d4e15bae96a96110241e9::getInitializer($loader));

        $loader->register(true);

        $filesToLoad = \Composer\Autoload\ComposerStaticInit16c27fcb6d4d4e15bae96a96110241e9::$files;
        $requireFile = \Closure::bind(static function ($fileIdentifier, $file) {
            if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
                $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;

                require $file;
            }
        }, null, null);
        foreach ($filesToLoad as $fileIdentifier => $file) {
            $requireFile($fileIdentifier, $file);
        }

        return $loader;
    }
}
<?php

// autoload_psr4.php @generated by Composer

$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);

return array(
    'upgrade\\builder\\' => array($vendorDir . '/cscart/upgrade-builder/src'),
    'Tygh\\' => array($baseDir . '/src'),
    'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'),
    'Symfony\\Polyfill\\Intl\\Normalizer\\' => array($vendorDir . '/symfony/polyfill-intl-normalizer'),
    'Symfony\\Polyfill\\Intl\\Grapheme\\' => array($vendorDir . '/symfony/polyfill-intl-grapheme'),
    'Symfony\\Polyfill\\Ctype\\' => array($vendorDir . '/symfony/polyfill-ctype'),
    'Symfony\\Contracts\\Service\\' => array($vendorDir . '/symfony/service-contracts'),
    'Symfony\\Contracts\\EventDispatcher\\' => array($vendorDir . '/symfony/event-dispatcher-contracts'),
    'Symfony\\Component\\Yaml\\' => array($vendorDir . '/symfony/yaml'),
    'Symfony\\Component\\VarExporter\\' => array($vendorDir . '/symfony/var-exporter'),
    'Symfony\\Component\\String\\' => array($vendorDir . '/symfony/string'),
    'Symfony\\Component\\Finder\\' => array($vendorDir . '/symfony/finder'),
    'Symfony\\Component\\Filesystem\\' => array($vendorDir . '/symfony/filesystem'),
    'Symfony\\Component\\EventDispatcher\\' => array($vendorDir . '/symfony/event-dispatcher'),
    'Symfony\\Component\\DependencyInjection\\' => array($vendorDir . '/symfony/dependency-injection'),
    'Symfony\\Component\\Console\\' => array($vendorDir . '/symfony/console'),
    'Symfony\\Component\\Config\\' => array($vendorDir . '/symfony/config'),
    'Psr\\EventDispatcher\\' => array($vendorDir . '/psr/event-dispatcher/src'),
    'Psr\\Container\\' => array($vendorDir . '/psr/container/src'),
    'Doctrine\\Deprecations\\' => array($vendorDir . '/doctrine/deprecations/src'),
    'Doctrine\\DBAL\\' => array($vendorDir . '/doctrine/dbal/lib/Doctrine/DBAL'),
    'Doctrine\\Common\\Cache\\' => array($vendorDir . '/doctrine/cache/lib/Doctrine/Common/Cache'),
    'Doctrine\\Common\\' => array($vendorDir . '/doctrine/event-manager/src'),
);
<?php

/*
 * This file is part of Composer.
 *
 * (c) Nils Adermann <naderman@naderman.de>
 *     Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Composer\Autoload;

/**
 * ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
 *
 *     $loader = new \Composer\Autoload\ClassLoader();
 *
 *     // register classes with namespaces
 *     $loader->add('Symfony\Component', __DIR__.'/component');
 *     $loader->add('Symfony',           __DIR__.'/framework');
 *
 *     // activate the autoloader
 *     $loader->register();
 *
 *     // to enable searching the include path (eg. for PEAR packages)
 *     $loader->setUseIncludePath(true);
 *
 * In this example, if you try to use a class in the Symfony\Component
 * namespace or one of its children (Symfony\Component\Console for instance),
 * the autoloader will first look for the class under the component/
 * directory, and it will then fallback to the framework/ directory if not
 * found before giving up.
 *
 * This class is loosely based on the Symfony UniversalClassLoader.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 * @author Jordi Boggiano <j.boggiano@seld.be>
 * @see    https://www.php-fig.org/psr/psr-0/
 * @see    https://www.php-fig.org/psr/psr-4/
 */
class ClassLoader
{
    /** @var \Closure(string):void */
    private static $includeFile;

    /** @var string|null */
    private $vendorDir;

    // PSR-4
    /**
     * @var array<string, array<string, int>>
     */
    private $prefixLengthsPsr4 = array();
    /**
     * @var array<string, list<string>>
     */
    private $prefixDirsPsr4 = array();
    /**
     * @var list<string>
     */
    private $fallbackDirsPsr4 = array();

    // PSR-0
    /**
     * List of PSR-0 prefixes
     *
     * Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
     *
     * @var array<string, array<string, list<string>>>
     */
    private $prefixesPsr0 = array();
    /**
     * @var list<string>
     */
    private $fallbackDirsPsr0 = array();

    /** @var bool */
    private $useIncludePath = false;

    /**
     * @var array<string, string>
     */
    private $classMap = array();

    /** @var bool */
    private $classMapAuthoritative = false;

    /**
     * @var array<string, bool>
     */
    private $missingClasses = array();

    /** @var string|null */
    private $apcuPrefix;

    /**
     * @var array<string, self>
     */
    private static $registeredLoaders = array();

    /**
     * @param string|null $vendorDir
     */
    public function __construct($vendorDir = null)
    {
        $this->vendorDir = $vendorDir;
        self::initializeIncludeClosure();
    }

    /**
     * @return array<string, list<string>>
     */
    public function getPrefixes()
    {
        if (!empty($this->prefixesPsr0)) {
            return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
        }

        return array();
    }

    /**
     * @return array<string, list<string>>
     */
    public function getPrefixesPsr4()
    {
        return $this->prefixDirsPsr4;
    }

    /**
     * @return list<string>
     */
    public function getFallbackDirs()
    {
        return $this->fallbackDirsPsr0;
    }

    /**
     * @return list<string>
     */
    public function getFallbackDirsPsr4()
    {
        return $this->fallbackDirsPsr4;
    }

    /**
     * @return array<string, string> Array of classname => path
     */
    public function getClassMap()
    {
        return $this->classMap;
    }

    /**
     * @param array<string, string> $classMap Class to filename map
     *
     * @return void
     */
    public function addClassMap(array $classMap)
    {
        if ($this->classMap) {
            $this->classMap = array_merge($this->classMap, $classMap);
        } else {
            $this->classMap = $classMap;
        }
    }

    /**
     * Registers a set of PSR-0 directories for a given prefix, either
     * appending or prepending to the ones previously set for this prefix.
     *
     * @param string              $prefix  The prefix
     * @param list<string>|string $paths   The PSR-0 root directories
     * @param bool                $prepend Whether to prepend the directories
     *
     * @return void
     */
    public function add($prefix, $paths, $prepend = false)
    {
        $paths = (array) $paths;
        if (!$prefix) {
            if ($prepend) {
                $this->fallbackDirsPsr0 = array_merge(
                    $paths,
                    $this->fallbackDirsPsr0
                );
            } else {
                $this->fallbackDirsPsr0 = array_merge(
                    $this->fallbackDirsPsr0,
                    $paths
                );
            }

            return;
        }

        $first = $prefix[0];
        if (!isset($this->prefixesPsr0[$first][$prefix])) {
            $this->prefixesPsr0[$first][$prefix] = $paths;

            return;
        }
        if ($prepend) {
            $this->prefixesPsr0[$first][$prefix] = array_merge(
                $paths,
                $this->prefixesPsr0[$first][$prefix]
            );
        } else {
            $this->prefixesPsr0[$first][$prefix] = array_merge(
                $this->prefixesPsr0[$first][$prefix],
                $paths
            );
        }
    }

    /**
     * Registers a set of PSR-4 directories for a given namespace, either
     * appending or prepending to the ones previously set for this namespace.
     *
     * @param string              $prefix  The prefix/namespace, with trailing '\\'
     * @param list<string>|string $paths   The PSR-4 base directories
     * @param bool                $prepend Whether to prepend the directories
     *
     * @throws \InvalidArgumentException
     *
     * @return void
     */
    public function addPsr4($prefix, $paths, $prepend = false)
    {
        $paths = (array) $paths;
        if (!$prefix) {
            // Register directories for the root namespace.
            if ($prepend) {
                $this->fallbackDirsPsr4 = array_merge(
                    $paths,
                    $this->fallbackDirsPsr4
                );
            } else {
                $this->fallbackDirsPsr4 = array_merge(
                    $this->fallbackDirsPsr4,
                    $paths
                );
            }
        } elseif (!isset($this->prefixDirsPsr4[$prefix])) {
            // Register directories for a new namespace.
            $length = strlen($prefix);
            if ('\\' !== $prefix[$length - 1]) {
                throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
            }
            $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
            $this->prefixDirsPsr4[$prefix] = $paths;
        } elseif ($prepend) {
            // Prepend directories for an already registered namespace.
            $this->prefixDirsPsr4[$prefix] = array_merge(
                $paths,
                $this->prefixDirsPsr4[$prefix]
            );
        } else {
            // Append directories for an already registered namespace.
            $this->prefixDirsPsr4[$prefix] = array_merge(
                $this->prefixDirsPsr4[$prefix],
                $paths
            );
        }
    }

    /**
     * Registers a set of PSR-0 directories for a given prefix,
     * replacing any others previously set for this prefix.
     *
     * @param string              $prefix The prefix
     * @param list<string>|string $paths  The PSR-0 base directories
     *
     * @return void
     */
    public function set($prefix, $paths)
    {
        if (!$prefix) {
            $this->fallbackDirsPsr0 = (array) $paths;
        } else {
            $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
        }
    }

    /**
     * Registers a set of PSR-4 directories for a given namespace,
     * replacing any others previously set for this namespace.
     *
     * @param string              $prefix The prefix/namespace, with trailing '\\'
     * @param list<string>|string $paths  The PSR-4 base directories
     *
     * @throws \InvalidArgumentException
     *
     * @return void
     */
    public function setPsr4($prefix, $paths)
    {
        if (!$prefix) {
            $this->fallbackDirsPsr4 = (array) $paths;
        } else {
            $length = strlen($prefix);
            if ('\\' !== $prefix[$length - 1]) {
                throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
            }
            $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
            $this->prefixDirsPsr4[$prefix] = (array) $paths;
        }
    }

    /**
     * Turns on searching the include path for class files.
     *
     * @param bool $useIncludePath
     *
     * @return void
     */
    public function setUseIncludePath($useIncludePath)
    {
        $this->useIncludePath = $useIncludePath;
    }

    /**
     * Can be used to check if the autoloader uses the include path to check
     * for classes.
     *
     * @return bool
     */
    public function getUseIncludePath()
    {
        return $this->useIncludePath;
    }

    /**
     * Turns off searching the prefix and fallback directories for classes
     * that have not been registered with the class map.
     *
     * @param bool $classMapAuthoritative
     *
     * @return void
     */
    public function setClassMapAuthoritative($classMapAuthoritative)
    {
        $this->classMapAuthoritative = $classMapAuthoritative;
    }

    /**
     * Should class lookup fail if not found in the current class map?
     *
     * @return bool
     */
    public function isClassMapAuthoritative()
    {
        return $this->classMapAuthoritative;
    }

    /**
     * APCu prefix to use to cache found/not-found classes, if the extension is enabled.
     *
     * @param string|null $apcuPrefix
     *
     * @return void
     */
    public function setApcuPrefix($apcuPrefix)
    {
        $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
    }

    /**
     * The APCu prefix in use, or null if APCu caching is not enabled.
     *
     * @return string|null
     */
    public function getApcuPrefix()
    {
        return $this->apcuPrefix;
    }

    /**
     * Registers this instance as an autoloader.
     *
     * @param bool $prepend Whether to prepend the autoloader or not
     *
     * @return void
     */
    public function register($prepend = false)
    {
        spl_autoload_register(array($this, 'loadClass'), true, $prepend);

        if (null === $this->vendorDir) {
            return;
        }

        if ($prepend) {
            self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
        } else {
            unset(self::$registeredLoaders[$this->vendorDir]);
            self::$registeredLoaders[$this->vendorDir] = $this;
        }
    }

    /**
     * Unregisters this instance as an autoloader.
     *
     * @return void
     */
    public function unregister()
    {
        spl_autoload_unregister(array($this, 'loadClass'));

        if (null !== $this->vendorDir) {
            unset(self::$registeredLoaders[$this->vendorDir]);
        }
    }

    /**
     * Loads the given class or interface.
     *
     * @param  string    $class The name of the class
     * @return true|null True if loaded, null otherwise
     */
    public function loadClass($class)
    {
        if ($file = $this->findFile($class)) {
            $includeFile = self::$includeFile;
            $includeFile($file);

            return true;
        }

        return null;
    }

    /**
     * Finds the path to the file where the class is defined.
     *
     * @param string $class The name of the class
     *
     * @return string|false The path if found, false otherwise
     */
    public function findFile($class)
    {
        // class map lookup
        if (isset($this->classMap[$class])) {
            return $this->classMap[$class];
        }
        if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
            return false;
        }
        if (null !== $this->apcuPrefix) {
            $file = apcu_fetch($this->apcuPrefix.$class, $hit);
            if ($hit) {
                return $file;
            }
        }

        $file = $this->findFileWithExtension($class, '.php');

        // Search for Hack files if we are running on HHVM
        if (false === $file && defined('HHVM_VERSION')) {
            $file = $this->findFileWithExtension($class, '.hh');
        }

        if (null !== $this->apcuPrefix) {
            apcu_add($this->apcuPrefix.$class, $file);
        }

        if (false === $file) {
            // Remember that this class does not exist.
            $this->missingClasses[$class] = true;
        }

        return $file;
    }

    /**
     * Returns the currently registered loaders keyed by their corresponding vendor directories.
     *
     * @return array<string, self>
     */
    public static function getRegisteredLoaders()
    {
        return self::$registeredLoaders;
    }

    /**
     * @param  string       $class
     * @param  string       $ext
     * @return string|false
     */
    private function findFileWithExtension($class, $ext)
    {
        // PSR-4 lookup
        $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;

        $first = $class[0];
        if (isset($this->prefixLengthsPsr4[$first])) {
            $subPath = $class;
            while (false !== $lastPos = strrpos($subPath, '\\')) {
                $subPath = substr($subPath, 0, $lastPos);
                $search = $subPath . '\\';
                if (isset($this->prefixDirsPsr4[$search])) {
                    $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
                    foreach ($this->prefixDirsPsr4[$search] as $dir) {
                        if (file_exists($file = $dir . $pathEnd)) {
                            return $file;
                        }
                    }
                }
            }
        }

        // PSR-4 fallback dirs
        foreach ($this->fallbackDirsPsr4 as $dir) {
            if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
                return $file;
            }
        }

        // PSR-0 lookup
        if (false !== $pos = strrpos($class, '\\')) {
            // namespaced class name
            $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
                . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
        } else {
            // PEAR-like class name
            $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
        }

        if (isset($this->prefixesPsr0[$first])) {
            foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
                if (0 === strpos($class, $prefix)) {
                    foreach ($dirs as $dir) {
                        if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
                            return $file;
                        }
                    }
                }
            }
        }

        // PSR-0 fallback dirs
        foreach ($this->fallbackDirsPsr0 as $dir) {
            if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
                return $file;
            }
        }

        // PSR-0 include paths.
        if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
            return $file;
        }

        return false;
    }

    /**
     * @return void
     */
    private static function initializeIncludeClosure()
    {
        if (self::$includeFile !== null) {
            return;
        }

        /**
         * Scope isolated include.
         *
         * Prevents access to $this/self from included files.
         *
         * @param  string $file
         * @return void
         */
        self::$includeFile = \Closure::bind(static function($file) {
            include $file;
        }, null, null);
    }
}
<?php

// autoload_classmap.php @generated by Composer

$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);

return array(
    'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
    'Normalizer' => $vendorDir . '/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php',
);
<?php

// autoload_namespaces.php @generated by Composer

$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);

return array(
);
<?php

// autoload.php @generated by Composer

if (PHP_VERSION_ID < 50600) {
    if (!headers_sent()) {
        header('HTTP/1.1 500 Internal Server Error');
    }
    $err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
    if (!ini_get('display_errors')) {
        if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
            fwrite(STDERR, $err);
        } elseif (!headers_sent()) {
            echo $err;
        }
    }
    trigger_error(
        $err,
        E_USER_ERROR
    );
}

require_once __DIR__ . '/composer/autoload_real.php';

return ComposerAutoloaderInit16c27fcb6d4d4e15bae96a96110241e9::getLoader();
<?php

use Doctrine\DBAL\Tools\Console\ConnectionProvider;
use Doctrine\DBAL\Tools\Console\ConsoleRunner;
use Symfony\Component\Console\Helper\HelperSet;

$files       = [__DIR__ . '/../vendor/autoload.php', __DIR__ . '/../../../autoload.php'];
$loader      = null;
$cwd         = getcwd();
$directories = [$cwd, $cwd . DIRECTORY_SEPARATOR . 'config'];
$configFile  = null;

foreach ($files as $file) {
    if (file_exists($file)) {
        $loader = require $file;

        break;
    }
}

if (! $loader) {
    throw new RuntimeException('vendor/autoload.php could not be found. Did you run `php composer.phar install`?');
}

foreach ($directories as $directory) {
    $configFile = $directory . DIRECTORY_SEPARATOR . 'cli-config.php';

    if (file_exists($configFile)) {
        break;
    }
}

if (! file_exists($configFile)) {
    ConsoleRunner::printCliConfigTemplate();

    exit(1);
}

if (! is_readable($configFile)) {
    echo 'Configuration file [' . $configFile . '] does not have read permission.' . PHP_EOL;

    exit(1);
}

$commands                      = [];
$helperSetOrConnectionProvider = require $configFile;

if (
    ! $helperSetOrConnectionProvider instanceof HelperSet
    && ! $helperSetOrConnectionProvider instanceof ConnectionProvider
) {
    foreach ($GLOBALS as $candidate) {
        if ($candidate instanceof HelperSet) {
            $helperSetOrConnectionProvider = $candidate;

            break;
        }
    }
}

ConsoleRunner::run($helperSetOrConnectionProvider, $commands);
<?php

namespace Doctrine\DBAL\Logging;

use Doctrine\Deprecations\Deprecation;

/**
 * Chains multiple SQLLogger.
 */
class LoggerChain implements SQLLogger
{
    /** @var SQLLogger[] */
    private $loggers = [];

    /**
     * @param SQLLogger[] $loggers
     */
    public function __construct(array $loggers = [])
    {
        $this->loggers = $loggers;
    }

    /**
     * Adds a logger in the chain.
     *
     * @deprecated Inject list of loggers via constructor instead
     *
     * @return void
     */
    public function addLogger(SQLLogger $logger)
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/3572',
            'LoggerChain::addLogger() is deprecated, use LoggerChain constructor instead.'
        );

        $this->loggers[] = $logger;
    }

    /**
     * {@inheritdoc}
     */
    public function startQuery($sql, ?array $params = null, ?array $types = null)
    {
        foreach ($this->loggers as $logger) {
            $logger->startQuery($sql, $params, $types);
        }
    }

    /**
     * {@inheritdoc}
     */
    public function stopQuery()
    {
        foreach ($this->loggers as $logger) {
            $logger->stopQuery();
        }
    }
}
<?php

namespace Doctrine\DBAL\Logging;

use Doctrine\DBAL\Types\Type;

/**
 * Interface for SQL loggers.
 */
interface SQLLogger
{
    /**
     * Logs a SQL statement somewhere.
     *
     * @param string                                                                    $sql    SQL statement
     * @param array<int, mixed>|array<string, mixed>|null                               $params Statement parameters
     * @param array<int, Type|int|string|null>|array<string, Type|int|string|null>|null $types  Parameter types
     *
     * @return void
     */
    public function startQuery($sql, ?array $params = null, ?array $types = null);

    /**
     * Marks the last started query as stopped. This can be used for timing of queries.
     *
     * @return void
     */
    public function stopQuery();
}
<?php

namespace Doctrine\DBAL\Logging;

use function microtime;

/**
 * Includes executed SQLs in a Debug Stack.
 */
class DebugStack implements SQLLogger
{
    /**
     * Executed SQL queries.
     *
     * @var array<int, array<string, mixed>>
     */
    public $queries = [];

    /**
     * If Debug Stack is enabled (log queries) or not.
     *
     * @var bool
     */
    public $enabled = true;

    /** @var float|null */
    public $start = null;

    /** @var int */
    public $currentQuery = 0;

    /**
     * {@inheritdoc}
     */
    public function startQuery($sql, ?array $params = null, ?array $types = null)
    {
        if (! $this->enabled) {
            return;
        }

        $this->start = microtime(true);

        $this->queries[++$this->currentQuery] = [
            'sql' => $sql,
            'params' => $params,
            'types' => $types,
            'executionMS' => 0,
        ];
    }

    /**
     * {@inheritdoc}
     */
    public function stopQuery()
    {
        if (! $this->enabled) {
            return;
        }

        $this->queries[$this->currentQuery]['executionMS'] = microtime(true) - $this->start;
    }
}
<?php

namespace Doctrine\DBAL\Logging;

use Doctrine\Deprecations\Deprecation;

use function var_dump;

use const PHP_EOL;

/**
 * A SQL logger that logs to the standard output using echo/var_dump.
 *
 * @deprecated
 */
class EchoSQLLogger implements SQLLogger
{
    public function __construct()
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/3935',
            'EchoSQLLogger is deprecated without replacement, move the code into your project if you rely on it.'
        );
    }

    /**
     * {@inheritdoc}
     */
    public function startQuery($sql, ?array $params = null, ?array $types = null)
    {
        echo $sql . PHP_EOL;

        if ($params) {
            var_dump($params);
        }

        if (! $types) {
            return;
        }

        var_dump($types);
    }

    /**
     * {@inheritdoc}
     */
    public function stopQuery()
    {
    }
}
<?php

namespace Doctrine\DBAL\Connections;

use Doctrine\Common\EventManager;
use Doctrine\DBAL\Configuration;
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\DriverManager;
use Doctrine\Deprecations\Deprecation;
use InvalidArgumentException;

/**
 * @deprecated Use PrimaryReadReplicaConnection instead
 *
 * @psalm-import-type Params from DriverManager
 */
class MasterSlaveConnection extends PrimaryReadReplicaConnection
{
    /**
     * Creates Primary Replica Connection.
     *
     * @internal The connection can be only instantiated by the driver manager.
     *
     * @param array<string,mixed> $params
     * @psalm-param Params $params
     * @phpstan-param array<string,mixed> $params
     *
     * @throws InvalidArgumentException
     */
    public function __construct(
        array $params,
        Driver $driver,
        ?Configuration $config = null,
        ?EventManager $eventManager = null
    ) {
        $this->deprecated(self::class, PrimaryReadReplicaConnection::class);

        if (isset($params['master'])) {
            $this->deprecated('Params key "master"', '"primary"');

            $params['primary'] = $params['master'];
        }

        if (isset($params['slaves'])) {
            $this->deprecated('Params key "slaves"', '"replica"');

            $params['replica'] = $params['slaves'];
        }

        if (isset($params['keepSlave'])) {
            $this->deprecated('Params key "keepSlave"', '"keepReplica"');

            $params['keepReplica'] = $params['keepSlave'];
        }

        parent::__construct($params, $driver, $config, $eventManager);
    }

    /**
     * Checks if the connection is currently towards the primary or not.
     */
    public function isConnectedToMaster(): bool
    {
        $this->deprecated('isConnectedtoMaster()', 'isConnectedToPrimary()');

        return $this->isConnectedToPrimary();
    }

    /**
     * @param string|null $connectionName
     *
     * @return bool
     */
    public function connect($connectionName = null)
    {
        if ($connectionName === 'master') {
            $connectionName = 'primary';

            $this->deprecated('connect("master")', 'ensureConnectedToPrimary()');
        }

        if ($connectionName === 'slave') {
            $connectionName = 'replica';

            $this->deprecated('connect("slave")', 'ensureConnectedToReplica()');
        }

        return $this->performConnect($connectionName);
    }

    private function deprecated(string $thing, string $instead): void
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/4054',
            '%s is deprecated since doctrine/dbal 2.11 and will be removed in 3.0, use %s instead.',
            $thing,
            $instead
        );
    }
}
<?php

namespace Doctrine\DBAL\Connections;

use Doctrine\Common\EventManager;
use Doctrine\DBAL\Configuration;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver\Connection as DriverConnection;
use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Event\ConnectionEventArgs;
use Doctrine\DBAL\Events;
use InvalidArgumentException;

use function array_rand;
use function assert;
use function count;
use function func_get_args;

/**
 * Primary-Replica Connection
 *
 * Connection can be used with primary-replica setups.
 *
 * Important for the understanding of this connection should be how and when
 * it picks the replica or primary.
 *
 * 1. Replica if primary was never picked before and ONLY if 'getWrappedConnection'
 *    or 'executeQuery' is used.
 * 2. Primary picked when 'exec', 'executeUpdate', 'executeStatement', 'insert', 'delete', 'update', 'createSavepoint',
 *    'releaseSavepoint', 'beginTransaction', 'rollback', 'commit', 'query' or
 *    'prepare' is called.
 * 3. If Primary was picked once during the lifetime of the connection it will always get picked afterwards.
 * 4. One replica connection is randomly picked ONCE during a request.
 *
 * ATTENTION: You can write to the replica with this connection if you execute a write query without
 * opening up a transaction. For example:
 *
 *      $conn = DriverManager::getConnection(...);
 *      $conn->executeQuery("DELETE FROM table");
 *
 * Be aware that Connection#executeQuery is a method specifically for READ
 * operations only.
 *
 * Use Connection#executeStatement for any SQL statement that changes/updates
 * state in the database (UPDATE, INSERT, DELETE or DDL statements).
 *
 * This connection is limited to replica operations using the
 * Connection#executeQuery operation only, because it wouldn't be compatible
 * with the ORM or SchemaManager code otherwise. Both use all the other
 * operations in a context where writes could happen to a replica, which makes
 * this restricted approach necessary.
 *
 * You can manually connect to the primary at any time by calling:
 *
 *      $conn->ensureConnectedToPrimary();
 *
 * Instantiation through the DriverManager looks like:
 *
 * @psalm-import-type Params from DriverManager
 * @example
 *
 * $conn = DriverManager::getConnection(array(
 *    'wrapperClass' => 'Doctrine\DBAL\Connections\PrimaryReadReplicaConnection',
 *    'driver' => 'pdo_mysql',
 *    'primary' => array('user' => '', 'password' => '', 'host' => '', 'dbname' => ''),
 *    'replica' => array(
 *        array('user' => 'replica1', 'password', 'host' => '', 'dbname' => ''),
 *        array('user' => 'replica2', 'password', 'host' => '', 'dbname' => ''),
 *    )
 * ));
 *
 * You can also pass 'driverOptions' and any other documented option to each of this drivers
 * to pass additional information.
 */
class PrimaryReadReplicaConnection extends Connection
{
    /**
     * Primary and Replica connection (one of the randomly picked replicas).
     *
     * @var DriverConnection[]|null[]
     */
    protected $connections = ['primary' => null, 'replica' => null];

    /**
     * You can keep the replica connection and then switch back to it
     * during the request if you know what you are doing.
     *
     * @var bool
     */
    protected $keepReplica = false;

    /**
     * Creates Primary Replica Connection.
     *
     * @internal The connection can be only instantiated by the driver manager.
     *
     * @param array<string,mixed> $params
     * @psalm-param Params $params
     * @phpstan-param array<string,mixed> $params
     *
     * @throws InvalidArgumentException
     */
    public function __construct(
        array $params,
        Driver $driver,
        ?Configuration $config = null,
        ?EventManager $eventManager = null
    ) {
        if (! isset($params['replica'], $params['primary'])) {
            throw new InvalidArgumentException('primary or replica configuration missing');
        }

        if (count($params['replica']) === 0) {
            throw new InvalidArgumentException('You have to configure at least one replica.');
        }

        if (isset($params['driver'])) {
            $params['primary']['driver'] = $params['driver'];

            foreach ($params['replica'] as $replicaKey => $replica) {
                $params['replica'][$replicaKey]['driver'] = $params['driver'];
            }
        }

        $this->keepReplica = (bool) ($params['keepReplica'] ?? false);

        parent::__construct($params, $driver, $config, $eventManager);
    }

    /**
     * Checks if the connection is currently towards the primary or not.
     */
    public function isConnectedToPrimary(): bool
    {
        return $this->_conn !== null && $this->_conn === $this->connections['primary'];
    }

    /**
     * @param string|null $connectionName
     *
     * @return bool
     */
    public function connect($connectionName = null)
    {
        if ($connectionName !== null) {
            throw new InvalidArgumentException(
                'Passing a connection name as first argument is not supported anymore.'
                    . ' Use ensureConnectedToPrimary()/ensureConnectedToReplica() instead.'
            );
        }

        return $this->performConnect();
    }

    protected function performConnect(?string $connectionName = null): bool
    {
        $requestedConnectionChange = ($connectionName !== null);
        $connectionName            = $connectionName ?: 'replica';

        if ($connectionName !== 'replica' && $connectionName !== 'primary') {
            throw new InvalidArgumentException('Invalid option to connect(), only primary or replica allowed.');
        }

        // If we have a connection open, and this is not an explicit connection
        // change request, then abort right here, because we are already done.
        // This prevents writes to the replica in case of "keepReplica" option enabled.
        if ($this->_conn !== null && ! $requestedConnectionChange) {
            return false;
        }

        $forcePrimaryAsReplica = false;

        if ($this->getTransactionNestingLevel() > 0) {
            $connectionName        = 'primary';
            $forcePrimaryAsReplica = true;
        }

        if (isset($this->connections[$connectionName])) {
            $this->_conn = $this->connections[$connectionName];

            if ($forcePrimaryAsReplica && ! $this->keepReplica) {
                $this->connections['replica'] = $this->_conn;
            }

            return false;
        }

        if ($connectionName === 'primary') {
            $this->connections['primary'] = $this->_conn = $this->connectTo($connectionName);

            // Set replica connection to primary to avoid invalid reads
            if (! $this->keepReplica) {
                $this->connections['replica'] = $this->connections['primary'];
            }
        } else {
            $this->connections['replica'] = $this->_conn = $this->connectTo($connectionName);
        }

        if ($this->_eventManager->hasListeners(Events::postConnect)) {
            $eventArgs = new ConnectionEventArgs($this);
            $this->_eventManager->dispatchEvent(Events::postConnect, $eventArgs);
        }

        return true;
    }

    /**
     * Connects to the primary node of the database cluster.
     *
     * All following statements after this will be executed against the primary node.
     */
    public function ensureConnectedToPrimary(): bool
    {
        return $this->performConnect('primary');
    }

    /**
     * Connects to a replica node of the database cluster.
     *
     * All following statements after this will be executed against the replica node,
     * unless the keepReplica option is set to false and a primary connection
     * was already opened.
     */
    public function ensureConnectedToReplica(): bool
    {
        return $this->performConnect('replica');
    }

    /**
     * Connects to a specific connection.
     *
     * @param string $connectionName
     *
     * @return DriverConnection
     */
    protected function connectTo($connectionName)
    {
        $params = $this->getParams();

        $driverOptions = $params['driverOptions'] ?? [];

        $connectionParams = $this->chooseConnectionConfiguration($connectionName, $params);

        $user     = $connectionParams['user'] ?? null;
        $password = $connectionParams['password'] ?? null;

        return $this->_driver->connect($connectionParams, $user, $password, $driverOptions);
    }

    /**
     * @param string  $connectionName
     * @param mixed[] $params
     *
     * @return mixed
     */
    protected function chooseConnectionConfiguration($connectionName, $params)
    {
        if ($connectionName === 'primary') {
            return $params['primary'];
        }

        $config = $params['replica'][array_rand($params['replica'])];

        if (! isset($config['charset']) && isset($params['primary']['charset'])) {
            $config['charset'] = $params['primary']['charset'];
        }

        return $config;
    }

    /**
     * {@inheritDoc}
     *
     * @deprecated Use {@link executeStatement()} instead.
     */
    public function executeUpdate($sql, array $params = [], array $types = [])
    {
        $this->ensureConnectedToPrimary();

        return parent::executeUpdate($sql, $params, $types);
    }

    /**
     * {@inheritDoc}
     */
    public function executeStatement($sql, array $params = [], array $types = [])
    {
        $this->ensureConnectedToPrimary();

        return parent::executeStatement($sql, $params, $types);
    }

    /**
     * {@inheritDoc}
     */
    public function beginTransaction()
    {
        $this->ensureConnectedToPrimary();

        return parent::beginTransaction();
    }

    /**
     * {@inheritDoc}
     */
    public function commit()
    {
        $this->ensureConnectedToPrimary();

        return parent::commit();
    }

    /**
     * {@inheritDoc}
     */
    public function rollBack()
    {
        $this->ensureConnectedToPrimary();

        return parent::rollBack();
    }

    /**
     * {@inheritDoc}
     */
    public function delete($table, array $criteria, array $types = [])
    {
        $this->ensureConnectedToPrimary();

        return parent::delete($table, $criteria, $types);
    }

    /**
     * {@inheritDoc}
     */
    public function close()
    {
        unset($this->connections['primary'], $this->connections['replica']);

        parent::close();

        $this->_conn       = null;
        $this->connections = ['primary' => null, 'replica' => null];
    }

    /**
     * {@inheritDoc}
     */
    public function update($table, array $data, array $criteria, array $types = [])
    {
        $this->ensureConnectedToPrimary();

        return parent::update($table, $data, $criteria, $types);
    }

    /**
     * {@inheritDoc}
     */
    public function insert($table, array $data, array $types = [])
    {
        $this->ensureConnectedToPrimary();

        return parent::insert($table, $data, $types);
    }

    /**
     * {@inheritDoc}
     */
    public function exec($sql)
    {
        $this->ensureConnectedToPrimary();

        return parent::exec($sql);
    }

    /**
     * {@inheritDoc}
     */
    public function createSavepoint($savepoint)
    {
        $this->ensureConnectedToPrimary();

        parent::createSavepoint($savepoint);
    }

    /**
     * {@inheritDoc}
     */
    public function releaseSavepoint($savepoint)
    {
        $this->ensureConnectedToPrimary();

        parent::releaseSavepoint($savepoint);
    }

    /**
     * {@inheritDoc}
     */
    public function rollbackSavepoint($savepoint)
    {
        $this->ensureConnectedToPrimary();

        parent::rollbackSavepoint($savepoint);
    }

    /**
     * {@inheritDoc}
     */
    public function query()
    {
        $this->ensureConnectedToPrimary();
        assert($this->_conn instanceof DriverConnection);

        $args = func_get_args();

        $logger = $this->getConfiguration()->getSQLLogger();
        if ($logger) {
            $logger->startQuery($args[0]);
        }

        $statement = $this->_conn->query(...$args);

        $statement->setFetchMode($this->defaultFetchMode);

        if ($logger) {
            $logger->stopQuery();
        }

        return $statement;
    }

    /**
     * {@inheritDoc}
     */
    public function prepare($sql)
    {
        $this->ensureConnectedToPrimary();

        return parent::prepare($sql);
    }
}
<?php

namespace Doctrine\DBAL\Event\Listeners;

use Doctrine\Common\EventSubscriber;
use Doctrine\DBAL\Event\ConnectionEventArgs;
use Doctrine\DBAL\Events;

/**
 * Session init listener for executing a single SQL statement right after a connection is opened.
 */
class SQLSessionInit implements EventSubscriber
{
    /** @var string */
    protected $sql;

    /**
     * @param string $sql
     */
    public function __construct($sql)
    {
        $this->sql = $sql;
    }

    /**
     * @return void
     */
    public function postConnect(ConnectionEventArgs $args)
    {
        $conn = $args->getConnection();
        $conn->exec($this->sql);
    }

    /**
     * {@inheritdoc}
     */
    public function getSubscribedEvents()
    {
        return [Events::postConnect];
    }
}
<?php

namespace Doctrine\DBAL\Event\Listeners;

use Doctrine\Common\EventSubscriber;
use Doctrine\DBAL\Event\ConnectionEventArgs;
use Doctrine\DBAL\Events;

use function array_change_key_case;
use function array_merge;
use function count;
use function implode;

use const CASE_UPPER;

/**
 * Should be used when Oracle Server default environment does not match the Doctrine requirements.
 *
 * The following environment variables are required for the Doctrine default date format:
 *
 * 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"
 */
class OracleSessionInit implements EventSubscriber
{
    /** @var string[] */
    protected $_defaultSessionVars = [
        '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',
        'NLS_NUMERIC_CHARACTERS' => '.,',
    ];

    /**
     * @param string[] $oracleSessionVars
     */
    public function __construct(array $oracleSessionVars = [])
    {
        $this->_defaultSessionVars = array_merge($this->_defaultSessionVars, $oracleSessionVars);
    }

    /**
     * @return void
     */
    public function postConnect(ConnectionEventArgs $args)
    {
        if (! count($this->_defaultSessionVars)) {
            return;
        }

        $vars = [];
        foreach (array_change_key_case($this->_defaultSessionVars, CASE_UPPER) as $option => $value) {
            if ($option === 'CURRENT_SCHEMA') {
                $vars[] = $option . ' = ' . $value;
            } else {
                $vars[] = $option . " = '" . $value . "'";
            }
        }

        $sql = 'ALTER SESSION SET ' . implode(' ', $vars);
        $args->getConnection()->executeStatement($sql);
    }

    /**
     * {@inheritdoc}
     */
    public function getSubscribedEvents()
    {
        return [Events::postConnect];
    }
}
<?php

namespace Doctrine\DBAL\Event\Listeners;

use Doctrine\Common\EventSubscriber;
use Doctrine\DBAL\Event\ConnectionEventArgs;
use Doctrine\DBAL\Events;

/**
 * MySQL Session Init Event Subscriber which allows to set the Client Encoding of the Connection.
 *
 * @deprecated Use "charset" option to PDO MySQL Connection instead.
 */
class MysqlSessionInit implements EventSubscriber
{
    /**
     * The charset.
     *
     * @var string
     */
    private $charset;

    /**
     * The collation, or FALSE if no collation.
     *
     * @var string|bool
     */
    private $collation;

    /**
     * Configure Charset and Collation options of MySQL Client for each Connection.
     *
     * @param string      $charset   The charset.
     * @param string|bool $collation The collation, or FALSE if no collation.
     */
    public function __construct($charset = 'utf8', $collation = false)
    {
        $this->charset   = $charset;
        $this->collation = $collation;
    }

    /**
     * @return void
     */
    public function postConnect(ConnectionEventArgs $args)
    {
        $collation = $this->collation ? ' COLLATE ' . $this->collation : '';
        $args->getConnection()->executeStatement('SET NAMES ' . $this->charset . $collation);
    }

    /**
     * {@inheritdoc}
     */
    public function getSubscribedEvents()
    {
        return [Events::postConnect];
    }
}
<?php

namespace Doctrine\DBAL\Event;

use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Schema\ColumnDiff;
use Doctrine\DBAL\Schema\TableDiff;

use function array_merge;
use function func_get_args;
use function is_array;

/**
 * Event Arguments used when SQL queries for changing table columns are generated inside {@link AbstractPlatform}.
 */
class SchemaAlterTableChangeColumnEventArgs extends SchemaEventArgs
{
    /** @var ColumnDiff */
    private $columnDiff;

    /** @var TableDiff */
    private $tableDiff;

    /** @var AbstractPlatform */
    private $platform;

    /** @var string[] */
    private $sql = [];

    public function __construct(ColumnDiff $columnDiff, TableDiff $tableDiff, AbstractPlatform $platform)
    {
        $this->columnDiff = $columnDiff;
        $this->tableDiff  = $tableDiff;
        $this->platform   = $platform;
    }

    /**
     * @return ColumnDiff
     */
    public function getColumnDiff()
    {
        return $this->columnDiff;
    }

    /**
     * @return TableDiff
     */
    public function getTableDiff()
    {
        return $this->tableDiff;
    }

    /**
     * @return AbstractPlatform
     */
    public function getPlatform()
    {
        return $this->platform;
    }

    /**
     * Passing multiple SQL statements as an array is deprecated. Pass each statement as an individual argument instead.
     *
     * @param string|string[] $sql
     *
     * @return SchemaAlterTableChangeColumnEventArgs
     */
    public function addSql($sql)
    {
        $this->sql = array_merge($this->sql, is_array($sql) ? $sql : func_get_args());

        return $this;
    }

    /**
     * @return string[]
     */
    public function getSql()
    {
        return $this->sql;
    }
}
<?php

namespace Doctrine\DBAL\Event;

use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Schema\Table;

use function array_merge;
use function func_get_args;
use function is_array;

/**
 * Event Arguments used when SQL queries for creating table columns are generated inside {@link AbstractPlatform}.
 */
class SchemaCreateTableColumnEventArgs extends SchemaEventArgs
{
    /** @var Column */
    private $column;

    /** @var Table */
    private $table;

    /** @var AbstractPlatform */
    private $platform;

    /** @var string[] */
    private $sql = [];

    public function __construct(Column $column, Table $table, AbstractPlatform $platform)
    {
        $this->column   = $column;
        $this->table    = $table;
        $this->platform = $platform;
    }

    /**
     * @return Column
     */
    public function getColumn()
    {
        return $this->column;
    }

    /**
     * @return Table
     */
    public function getTable()
    {
        return $this->table;
    }

    /**
     * @return AbstractPlatform
     */
    public function getPlatform()
    {
        return $this->platform;
    }

    /**
     * Passing multiple SQL statements as an array is deprecated. Pass each statement as an individual argument instead.
     *
     * @param string|string[] $sql
     *
     * @return SchemaCreateTableColumnEventArgs
     */
    public function addSql($sql)
    {
        $this->sql = array_merge($this->sql, is_array($sql) ? $sql : func_get_args());

        return $this;
    }

    /**
     * @return string[]
     */
    public function getSql()
    {
        return $this->sql;
    }
}
<?php

namespace Doctrine\DBAL\Event;

use Doctrine\Common\EventArgs;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Schema\AbstractSchemaManager;
use Doctrine\Deprecations\Deprecation;

/**
 * Event Arguments used when a Driver connection is established inside {@link Connection}.
 */
class ConnectionEventArgs extends EventArgs
{
    /** @var Connection */
    private $connection;

    public function __construct(Connection $connection)
    {
        $this->connection = $connection;
    }

    /**
     * @return Connection
     */
    public function getConnection()
    {
        return $this->connection;
    }

    /**
     * @deprecated Use ConnectionEventArgs::getConnection() and Connection::getDriver() instead.
     *
     * @return Driver
     */
    public function getDriver()
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/issues/3580',
            'ConnectionEventArgs::getDriver() is deprecated, ' .
            'use ConnectionEventArgs::getConnection()->getDriver() instead.'
        );

        return $this->connection->getDriver();
    }

    /**
     * @deprecated Use ConnectionEventArgs::getConnection() and Connection::getDatabasePlatform() instead.
     *
     * @return AbstractPlatform
     */
    public function getDatabasePlatform()
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/issues/3580',
            'ConnectionEventArgs::getDatabasePlatform() is deprecated, ' .
            'use ConnectionEventArgs::getConnection()->getDatabasePlatform() instead.'
        );

        return $this->connection->getDatabasePlatform();
    }

    /**
     * @deprecated Use ConnectionEventArgs::getConnection() and Connection::getSchemaManager() instead.
     *
     * @return AbstractSchemaManager
     */
    public function getSchemaManager()
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/issues/3580',
            'ConnectionEventArgs::getSchemaManager() is deprecated, ' .
            'use ConnectionEventArgs::getConnection()->getSchemaManager() instead.'
        );

        return $this->connection->getSchemaManager();
    }
}
<?php

namespace Doctrine\DBAL\Event;

use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Schema\TableDiff;
use Doctrine\Deprecations\Deprecation;

use function array_merge;
use function func_get_args;
use function is_array;

/**
 * Event Arguments used when SQL queries for adding table columns are generated inside {@link AbstractPlatform}.
 */
class SchemaAlterTableAddColumnEventArgs extends SchemaEventArgs
{
    /** @var Column */
    private $column;

    /** @var TableDiff */
    private $tableDiff;

    /** @var AbstractPlatform */
    private $platform;

    /** @var string[] */
    private $sql = [];

    public function __construct(Column $column, TableDiff $tableDiff, AbstractPlatform $platform)
    {
        $this->column    = $column;
        $this->tableDiff = $tableDiff;
        $this->platform  = $platform;
    }

    /**
     * @return Column
     */
    public function getColumn()
    {
        return $this->column;
    }

    /**
     * @return TableDiff
     */
    public function getTableDiff()
    {
        return $this->tableDiff;
    }

    /**
     * @return AbstractPlatform
     */
    public function getPlatform()
    {
        return $this->platform;
    }

    /**
     * Passing multiple SQL statements as an array is deprecated. Pass each statement as an individual argument instead.
     *
     * @param string|string[] $sql
     *
     * @return SchemaAlterTableAddColumnEventArgs
     */
    public function addSql($sql)
    {
        if (is_array($sql)) {
            Deprecation::trigger(
                'doctrine/dbal',
                'https://github.com/doctrine/dbal/issues/3580',
                'Passing multiple SQL statements as an array to SchemaAlterTableAddColumnEventaArrgs::addSql() ' .
                'is deprecated. Pass each statement as an individual argument instead.'
            );
        }

        $this->sql = array_merge($this->sql, is_array($sql) ? $sql : func_get_args());

        return $this;
    }

    /**
     * @return string[]
     */
    public function getSql()
    {
        return $this->sql;
    }
}
<?php

namespace Doctrine\DBAL\Event;

use Doctrine\Common\EventArgs;

/**
 * Base class for schema related events.
 */
class SchemaEventArgs extends EventArgs
{
    /** @var bool */
    private $preventDefault = false;

    /**
     * @return SchemaEventArgs
     */
    public function preventDefault()
    {
        $this->preventDefault = true;

        return $this;
    }

    /**
     * @return bool
     */
    public function isDefaultPrevented()
    {
        return $this->preventDefault;
    }
}
<?php

namespace Doctrine\DBAL\Event;

use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Schema\Table;
use InvalidArgumentException;

/**
 * Event Arguments used when the SQL query for dropping tables are generated inside {@link AbstractPlatform}.
 */
class SchemaDropTableEventArgs extends SchemaEventArgs
{
    /** @var string|Table */
    private $table;

    /** @var AbstractPlatform */
    private $platform;

    /** @var string|null */
    private $sql;

    /**
     * @param string|Table $table
     *
     * @throws InvalidArgumentException
     */
    public function __construct($table, AbstractPlatform $platform)
    {
        $this->table    = $table;
        $this->platform = $platform;
    }

    /**
     * @return string|Table
     */
    public function getTable()
    {
        return $this->table;
    }

    /**
     * @return AbstractPlatform
     */
    public function getPlatform()
    {
        return $this->platform;
    }

    /**
     * @param string $sql
     *
     * @return SchemaDropTableEventArgs
     */
    public function setSql($sql)
    {
        $this->sql = $sql;

        return $this;
    }

    /**
     * @return string|null
     */
    public function getSql()
    {
        return $this->sql;
    }
}
<?php

namespace Doctrine\DBAL\Event;

use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Schema\TableDiff;

use function array_merge;
use function func_get_args;
use function is_array;

/**
 * Event Arguments used when SQL queries for creating tables are generated inside {@link AbstractPlatform}.
 */
class SchemaAlterTableEventArgs extends SchemaEventArgs
{
    /** @var TableDiff */
    private $tableDiff;

    /** @var AbstractPlatform */
    private $platform;

    /** @var string[] */
    private $sql = [];

    public function __construct(TableDiff $tableDiff, AbstractPlatform $platform)
    {
        $this->tableDiff = $tableDiff;
        $this->platform  = $platform;
    }

    /**
     * @return TableDiff
     */
    public function getTableDiff()
    {
        return $this->tableDiff;
    }

    /**
     * @return AbstractPlatform
     */
    public function getPlatform()
    {
        return $this->platform;
    }

    /**
     * Passing multiple SQL statements as an array is deprecated. Pass each statement as an individual argument instead.
     *
     * @param string|string[] $sql
     *
     * @return SchemaAlterTableEventArgs
     */
    public function addSql($sql)
    {
        $this->sql = array_merge($this->sql, is_array($sql) ? $sql : func_get_args());

        return $this;
    }

    /**
     * @return string[]
     */
    public function getSql()
    {
        return $this->sql;
    }
}
<?php

namespace Doctrine\DBAL\Event;

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Schema\Index;
use Doctrine\Deprecations\Deprecation;

/**
 * Event Arguments used when the portable index definition is generated inside {@link AbstractSchemaManager}.
 */
class SchemaIndexDefinitionEventArgs extends SchemaEventArgs
{
    /** @var Index|null */
    private $index;

    /**
     * Raw index data as fetched from the database.
     *
     * @var mixed[]
     */
    private $tableIndex;

    /** @var string */
    private $table;

    /** @var Connection */
    private $connection;

    /**
     * @param mixed[] $tableIndex
     * @param string  $table
     */
    public function __construct(array $tableIndex, $table, Connection $connection)
    {
        $this->tableIndex = $tableIndex;
        $this->table      = $table;
        $this->connection = $connection;
    }

    /**
     * Allows to clear the index which means the index will be excluded from tables index list.
     *
     * @return SchemaIndexDefinitionEventArgs
     */
    public function setIndex(?Index $index = null)
    {
        $this->index = $index;

        return $this;
    }

    /**
     * @return Index|null
     */
    public function getIndex()
    {
        return $this->index;
    }

    /**
     * @return mixed[]
     */
    public function getTableIndex()
    {
        return $this->tableIndex;
    }

    /**
     * @return string
     */
    public function getTable()
    {
        return $this->table;
    }

    /**
     * @return Connection
     */
    public function getConnection()
    {
        return $this->connection;
    }

    /**
     * @deprecated Use SchemaIndexDefinitionEventArgs::getConnection() and Connection::getDatabasePlatform() instead.
     *
     * @return AbstractPlatform
     */
    public function getDatabasePlatform()
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/issues/3580',
            'SchemaIndexDefinitionEventArgs::getDatabasePlatform() is deprecated, ' .
            'use SchemaIndexDefinitionEventArgs::getConnection()->getDatabasePlatform() instead.'
        );

        return $this->connection->getDatabasePlatform();
    }
}
<?php

namespace Doctrine\DBAL\Event;

use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Schema\TableDiff;

use function array_merge;
use function func_get_args;
use function is_array;

/**
 * Event Arguments used when SQL queries for renaming table columns are generated inside {@link AbstractPlatform}.
 */
class SchemaAlterTableRenameColumnEventArgs extends SchemaEventArgs
{
    /** @var string */
    private $oldColumnName;

    /** @var Column */
    private $column;

    /** @var TableDiff */
    private $tableDiff;

    /** @var AbstractPlatform */
    private $platform;

    /** @var string[] */
    private $sql = [];

    /**
     * @param string $oldColumnName
     */
    public function __construct($oldColumnName, Column $column, TableDiff $tableDiff, AbstractPlatform $platform)
    {
        $this->oldColumnName = $oldColumnName;
        $this->column        = $column;
        $this->tableDiff     = $tableDiff;
        $this->platform      = $platform;
    }

    /**
     * @return string
     */
    public function getOldColumnName()
    {
        return $this->oldColumnName;
    }

    /**
     * @return Column
     */
    public function getColumn()
    {
        return $this->column;
    }

    /**
     * @return TableDiff
     */
    public function getTableDiff()
    {
        return $this->tableDiff;
    }

    /**
     * @return AbstractPlatform
     */
    public function getPlatform()
    {
        return $this->platform;
    }

    /**
     * Passing multiple SQL statements as an array is deprecated. Pass each statement as an individual argument instead.
     *
     * @param string|string[] $sql
     *
     * @return SchemaAlterTableRenameColumnEventArgs
     */
    public function addSql($sql)
    {
        $this->sql = array_merge($this->sql, is_array($sql) ? $sql : func_get_args());

        return $this;
    }

    /**
     * @return string[]
     */
    public function getSql()
    {
        return $this->sql;
    }
}
<?php

namespace Doctrine\DBAL\Event;

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Schema\Column;
use Doctrine\Deprecations\Deprecation;

/**
 * Event Arguments used when the portable column definition is generated inside {@link AbstractPlatform}.
 */
class SchemaColumnDefinitionEventArgs extends SchemaEventArgs
{
    /** @var Column|null */
    private $column;

    /**
     * Raw column data as fetched from the database.
     *
     * @var mixed[]
     */
    private $tableColumn;

    /** @var string */
    private $table;

    /** @var string */
    private $database;

    /** @var Connection */
    private $connection;

    /**
     * @param mixed[] $tableColumn
     * @param string  $table
     * @param string  $database
     */
    public function __construct(array $tableColumn, $table, $database, Connection $connection)
    {
        $this->tableColumn = $tableColumn;
        $this->table       = $table;
        $this->database    = $database;
        $this->connection  = $connection;
    }

    /**
     * Allows to clear the column which means the column will be excluded from
     * tables column list.
     *
     * @return SchemaColumnDefinitionEventArgs
     */
    public function setColumn(?Column $column = null)
    {
        $this->column = $column;

        return $this;
    }

    /**
     * @return Column|null
     */
    public function getColumn()
    {
        return $this->column;
    }

    /**
     * @return mixed[]
     */
    public function getTableColumn()
    {
        return $this->tableColumn;
    }

    /**
     * @return string
     */
    public function getTable()
    {
        return $this->table;
    }

    /**
     * @return string
     */
    public function getDatabase()
    {
        return $this->database;
    }

    /**
     * @return Connection
     */
    public function getConnection()
    {
        return $this->connection;
    }

    /**
     * @deprecated Use SchemaColumnDefinitionEventArgs::getConnection() and Connection::getDatabasePlatform() instead.
     *
     * @return AbstractPlatform
     */
    public function getDatabasePlatform()
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/issues/3580',
            'SchemaColumnDefinitionEventArgs::getDatabasePlatform() is deprecated, ' .
            'use SchemaColumnDefinitionEventArgs::getConnection()->getDatabasePlatform() instead.'
        );

        return $this->connection->getDatabasePlatform();
    }
}
<?php

namespace Doctrine\DBAL\Event;

use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Schema\TableDiff;

use function array_merge;
use function func_get_args;
use function is_array;

/**
 * Event Arguments used when SQL queries for removing table columns are generated inside {@link AbstractPlatform}.
 */
class SchemaAlterTableRemoveColumnEventArgs extends SchemaEventArgs
{
    /** @var Column */
    private $column;

    /** @var TableDiff */
    private $tableDiff;

    /** @var AbstractPlatform */
    private $platform;

    /** @var string[] */
    private $sql = [];

    public function __construct(Column $column, TableDiff $tableDiff, AbstractPlatform $platform)
    {
        $this->column    = $column;
        $this->tableDiff = $tableDiff;
        $this->platform  = $platform;
    }

    /**
     * @return Column
     */
    public function getColumn()
    {
        return $this->column;
    }

    /**
     * @return TableDiff
     */
    public function getTableDiff()
    {
        return $this->tableDiff;
    }

    /**
     * @return AbstractPlatform
     */
    public function getPlatform()
    {
        return $this->platform;
    }

    /**
     * Passing multiple SQL statements as an array is deprecated. Pass each statement as an individual argument instead.
     *
     * @param string|string[] $sql
     *
     * @return SchemaAlterTableRemoveColumnEventArgs
     */
    public function addSql($sql)
    {
        $this->sql = array_merge($this->sql, is_array($sql) ? $sql : func_get_args());

        return $this;
    }

    /**
     * @return string[]
     */
    public function getSql()
    {
        return $this->sql;
    }
}
<?php

namespace Doctrine\DBAL\Event;

use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Schema\Table;

use function array_merge;
use function func_get_args;
use function is_array;

/**
 * Event Arguments used when SQL queries for creating tables are generated inside {@link AbstractPlatform}.
 */
class SchemaCreateTableEventArgs extends SchemaEventArgs
{
    /** @var Table */
    private $table;

    /** @var mixed[][] */
    private $columns;

    /** @var mixed[] */
    private $options;

    /** @var AbstractPlatform */
    private $platform;

    /** @var string[] */
    private $sql = [];

    /**
     * @param mixed[][] $columns
     * @param mixed[]   $options
     */
    public function __construct(Table $table, array $columns, array $options, AbstractPlatform $platform)
    {
        $this->table    = $table;
        $this->columns  = $columns;
        $this->options  = $options;
        $this->platform = $platform;
    }

    /**
     * @return Table
     */
    public function getTable()
    {
        return $this->table;
    }

    /**
     * @return mixed[][]
     */
    public function getColumns()
    {
        return $this->columns;
    }

    /**
     * @return mixed[]
     */
    public function getOptions()
    {
        return $this->options;
    }

    /**
     * @return AbstractPlatform
     */
    public function getPlatform()
    {
        return $this->platform;
    }

    /**
     * Passing multiple SQL statements as an array is deprecated. Pass each statement as an individual argument instead.
     *
     * @param string|string[] $sql
     *
     * @return SchemaCreateTableEventArgs
     */
    public function addSql($sql)
    {
        $this->sql = array_merge($this->sql, is_array($sql) ? $sql : func_get_args());

        return $this;
    }

    /**
     * @return string[]
     */
    public function getSql()
    {
        return $this->sql;
    }
}
<?php

namespace Doctrine\DBAL\Schema;

use Doctrine\DBAL\Platforms\AbstractPlatform;

use function array_merge;

/**
 * Schema Diff.
 */
class SchemaDiff
{
    /** @var Schema|null */
    public $fromSchema;

    /**
     * All added namespaces.
     *
     * @var string[]
     */
    public $newNamespaces = [];

    /**
     * All removed namespaces.
     *
     * @var string[]
     */
    public $removedNamespaces = [];

    /**
     * All added tables.
     *
     * @var Table[]
     */
    public $newTables = [];

    /**
     * All changed tables.
     *
     * @var TableDiff[]
     */
    public $changedTables = [];

    /**
     * All removed tables.
     *
     * @var Table[]
     */
    public $removedTables = [];

    /** @var Sequence[] */
    public $newSequences = [];

    /** @var Sequence[] */
    public $changedSequences = [];

    /** @var Sequence[] */
    public $removedSequences = [];

    /** @var ForeignKeyConstraint[] */
    public $orphanedForeignKeys = [];

    /**
     * Constructs an SchemaDiff object.
     *
     * @param Table[]     $newTables
     * @param TableDiff[] $changedTables
     * @param Table[]     $removedTables
     */
    public function __construct($newTables = [], $changedTables = [], $removedTables = [], ?Schema $fromSchema = null)
    {
        $this->newTables     = $newTables;
        $this->changedTables = $changedTables;
        $this->removedTables = $removedTables;
        $this->fromSchema    = $fromSchema;
    }

    /**
     * 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.
     *
     * @return string[]
     */
    public function toSaveSql(AbstractPlatform $platform)
    {
        return $this->_toSql($platform, true);
    }

    /**
     * @return string[]
     */
    public function toSql(AbstractPlatform $platform)
    {
        return $this->_toSql($platform, false);
    }

    /**
     * @param bool $saveMode
     *
     * @return string[]
     */
    protected function _toSql(AbstractPlatform $platform, $saveMode = false)
    {
        $sql = [];

        if ($platform->supportsSchemas()) {
            foreach ($this->newNamespaces as $newNamespace) {
                $sql[] = $platform->getCreateSchemaSQL($newNamespace);
            }
        }

        if ($platform->supportsForeignKeyConstraints() && $saveMode === false) {
            foreach ($this->orphanedForeignKeys as $orphanedForeignKey) {
                $sql[] = $platform->getDropForeignKeySQL($orphanedForeignKey, $orphanedForeignKey->getLocalTable());
            }
        }

        if ($platform->supportsSequences() === true) {
            foreach ($this->changedSequences as $sequence) {
                $sql[] = $platform->getAlterSequenceSQL($sequence);
            }

            if ($saveMode === false) {
                foreach ($this->removedSequences as $sequence) {
                    $sql[] = $platform->getDropSequenceSQL($sequence);
                }
            }

            foreach ($this->newSequences as $sequence) {
                $sql[] = $platform->getCreateSequenceSQL($sequence);
            }
        }

        $foreignKeySql = [];
        foreach ($this->newTables as $table) {
            $sql = array_merge(
                $sql,
                $platform->getCreateTableSQL($table, AbstractPlatform::CREATE_INDEXES)
            );

            if (! $platform->supportsForeignKeyConstraints()) {
                continue;
            }

            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;
    }
}
<?php

namespace Doctrine\DBAL\Schema;

use Doctrine\DBAL\Platforms\AbstractPlatform;
use InvalidArgumentException;

use function array_filter;
use function array_keys;
use function array_map;
use function array_search;
use function array_shift;
use function count;
use function is_string;
use function strtolower;

class Index extends AbstractAsset implements Constraint
{
    /**
     * Asset identifier instances of the column names the index is associated with.
     * array($columnName => Identifier)
     *
     * @var Identifier[]
     */
    protected $_columns = [];

    /** @var bool */
    protected $_isUnique = false;

    /** @var bool */
    protected $_isPrimary = false;

    /**
     * Platform specific flags for indexes.
     * array($flagName => true)
     *
     * @var true[]
     */
    protected $_flags = [];

    /**
     * Platform specific options
     *
     * @todo $_flags should eventually be refactored into options
     * @var mixed[]
     */
    private $options = [];

    /**
     * @param string   $name
     * @param string[] $columns
     * @param bool     $isUnique
     * @param bool     $isPrimary
     * @param string[] $flags
     * @param mixed[]  $options
     */
    public function __construct(
        $name,
        array $columns,
        $isUnique = false,
        $isPrimary = false,
        array $flags = [],
        array $options = []
    ) {
        $isUnique = $isUnique || $isPrimary;

        $this->_setName($name);
        $this->_isUnique  = $isUnique;
        $this->_isPrimary = $isPrimary;
        $this->options    = $options;

        foreach ($columns as $column) {
            $this->_addColumn($column);
        }

        foreach ($flags as $flag) {
            $this->addFlag($flag);
        }
    }

    /**
     * @param string $column
     *
     * @return void
     *
     * @throws InvalidArgumentException
     */
    protected function _addColumn($column)
    {
        if (! is_string($column)) {
            throw new InvalidArgumentException('Expecting a string as Index Column');
        }

        $this->_columns[$column] = new Identifier($column);
    }

    /**
     * {@inheritdoc}
     */
    public function getColumns()
    {
        return array_keys($this->_columns);
    }

    /**
     * {@inheritdoc}
     */
    public function getQuotedColumns(AbstractPlatform $platform)
    {
        $subParts = $platform->supportsColumnLengthIndexes() && $this->hasOption('lengths')
            ? $this->getOption('lengths') : [];

        $columns = [];

        foreach ($this->_columns as $column) {
            $length = array_shift($subParts);

            $quotedColumn = $column->getQuotedName($platform);

            if ($length !== null) {
                $quotedColumn .= '(' . $length . ')';
            }

            $columns[] = $quotedColumn;
        }

        return $columns;
    }

    /**
     * @return string[]
     */
    public function getUnquotedColumns()
    {
        return array_map([$this, 'trimQuotes'], $this->getColumns());
    }

    /**
     * Is the index neither unique nor primary key?
     *
     * @return bool
     */
    public function isSimpleIndex()
    {
        return ! $this->_isPrimary && ! $this->_isUnique;
    }

    /**
     * @return bool
     */
    public function isUnique()
    {
        return $this->_isUnique;
    }

    /**
     * @return bool
     */
    public function isPrimary()
    {
        return $this->_isPrimary;
    }

    /**
     * @param string $name
     * @param int    $pos
     *
     * @return bool
     */
    public function hasColumnAtPosition($name, $pos = 0)
    {
        $name         = $this->trimQuotes(strtolower($name));
        $indexColumns = array_map('strtolower', $this->getUnquotedColumns());

        return array_search($name, $indexColumns) === $pos;
    }

    /**
     * Checks if this index exactly spans the given column names in the correct order.
     *
     * @param string[] $columnNames
     *
     * @return bool
     */
    public function spansColumns(array $columnNames)
    {
        $columns         = $this->getColumns();
        $numberOfColumns = count($columns);
        $sameColumns     = true;

        for ($i = 0; $i < $numberOfColumns; $i++) {
            if (
                isset($columnNames[$i])
                && $this->trimQuotes(strtolower($columns[$i])) === $this->trimQuotes(strtolower($columnNames[$i]))
            ) {
                continue;
            }

            $sameColumns = false;
        }

        return $sameColumns;
    }

    /**
     * Checks if the other index already fulfills all the indexing and constraint needs of the current one.
     *
     * @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->samePartialIndex($other)) {
                return false;
            }

            if (! $this->hasSameColumnLengths($other)) {
                return false;
            }

            if (! $this->isUnique() && ! $this->isPrimary()) {
                // this is a special case: If the current key is neither primary or unique, any unique 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 fulfill the requirements of just an
                // index that has no constraints.
                return true;
            }

            if ($other->isPrimary() !== $this->isPrimary()) {
                return false;
            }

            return $other->isUnique() === $this->isUnique();
        }

        return false;
    }

    /**
     * Detects if the other index is a non-unique, non primary index that can be overwritten by this one.
     *
     * @return bool
     */
    public function overrules(Index $other)
    {
        if ($other->isPrimary()) {
            return false;
        }

        if ($this->isSimpleIndex() && $other->isUnique()) {
            return false;
        }

        return $this->spansColumns($other->getColumns())
            && ($this->isPrimary() || $this->isUnique())
            && $this->samePartialIndex($other);
    }

    /**
     * Returns platform specific flags for indexes.
     *
     * @return string[]
     */
    public function getFlags()
    {
        return array_keys($this->_flags);
    }

    /**
     * Adds Flag for an index that translates to platform specific handling.
     *
     * @param string $flag
     *
     * @return Index
     *
     * @example $index->addFlag('CLUSTERED')
     */
    public function addFlag($flag)
    {
        $this->_flags[strtolower($flag)] = true;

        return $this;
    }

    /**
     * Does this index have a specific flag?
     *
     * @param string $flag
     *
     * @return bool
     */
    public function hasFlag($flag)
    {
        return isset($this->_flags[strtolower($flag)]);
    }

    /**
     * Removes a flag.
     *
     * @param string $flag
     *
     * @return void
     */
    public function removeFlag($flag)
    {
        unset($this->_flags[strtolower($flag)]);
    }

    /**
     * @param string $name
     *
     * @return bool
     */
    public function hasOption($name)
    {
        return isset($this->options[strtolower($name)]);
    }

    /**
     * @param string $name
     *
     * @return mixed
     */
    public function getOption($name)
    {
        return $this->options[strtolower($name)];
    }

    /**
     * @return mixed[]
     */
    public function getOptions()
    {
        return $this->options;
    }

    /**
     * Return whether the two indexes have the same partial index
     *
     * @return bool
     */
    private function samePartialIndex(Index $other)
    {
        if (
            $this->hasOption('where')
            && $other->hasOption('where')
            && $this->getOption('where') === $other->getOption('where')
        ) {
            return true;
        }

        return ! $this->hasOption('where') && ! $other->hasOption('where');
    }

    /**
     * Returns whether the index has the same column lengths as the other
     */
    private function hasSameColumnLengths(self $other): bool
    {
        $filter = static function (?int $length): bool {
            return $length !== null;
        };

        return array_filter($this->options['lengths'] ?? [], $filter)
            === array_filter($other->options['lengths'] ?? [], $filter);
    }
}
<?php

namespace Doctrine\DBAL\Schema;

use function in_array;

/**
 * Represents the change of a column.
 */
class ColumnDiff
{
    /** @var string */
    public $oldColumnName;

    /** @var Column */
    public $column;

    /** @var string[] */
    public $changedProperties = [];

    /** @var Column|null */
    public $fromColumn;

    /**
     * @param string   $oldColumnName
     * @param string[] $changedProperties
     */
    public function __construct(
        $oldColumnName,
        Column $column,
        array $changedProperties = [],
        ?Column $fromColumn = null
    ) {
        $this->oldColumnName     = $oldColumnName;
        $this->column            = $column;
        $this->changedProperties = $changedProperties;
        $this->fromColumn        = $fromColumn;
    }

    /**
     * @param string $propertyName
     *
     * @return bool
     */
    public function hasChanged($propertyName)
    {
        return in_array($propertyName, $this->changedProperties);
    }

    /**
     * @return Identifier
     */
    public function getOldColumnName()
    {
        $quote = $this->fromColumn && $this->fromColumn->isQuoted();

        return new Identifier($this->oldColumnName, $quote);
    }
}
<?php

namespace Doctrine\DBAL\Schema;

/**
 * Configuration for a Schema.
 */
class SchemaConfig
{
    /** @var bool */
    protected $hasExplicitForeignKeyIndexes = false;

    /** @var int */
    protected $maxIdentifierLength = 63;

    /** @var string */
    protected $name;

    /** @var mixed[] */
    protected $defaultTableOptions = [];

    /**
     * @return bool
     */
    public function hasExplicitForeignKeyIndexes()
    {
        return $this->hasExplicitForeignKeyIndexes;
    }

    /**
     * @param bool $flag
     *
     * @return void
     */
    public function setExplicitForeignKeyIndexes($flag)
    {
        $this->hasExplicitForeignKeyIndexes = (bool) $flag;
    }

    /**
     * @param int $length
     *
     * @return void
     */
    public function setMaxIdentifierLength($length)
    {
        $this->maxIdentifierLength = (int) $length;
    }

    /**
     * @return int
     */
    public function getMaxIdentifierLength()
    {
        return $this->maxIdentifierLength;
    }

    /**
     * Gets the default namespace of schema objects.
     *
     * @return string
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * Sets the default namespace name of schema objects.
     *
     * @param string $name The value to set.
     *
     * @return void
     */
    public function setName($name)
    {
        $this->name = $name;
    }

    /**
     * Gets the default options that are passed to Table instances created with
     * Schema#createTable().
     *
     * @return mixed[]
     */
    public function getDefaultTableOptions()
    {
        return $this->defaultTableOptions;
    }

    /**
     * @param mixed[] $defaultTableOptions
     *
     * @return void
     */
    public function setDefaultTableOptions(array $defaultTableOptions)
    {
        $this->defaultTableOptions = $defaultTableOptions;
    }
}
<?php

namespace Doctrine\DBAL\Schema;

use Doctrine\DBAL\Types\Type;

use function explode;
use function strtolower;
use function trim;

/**
 * Schema manager for the Drizzle RDBMS.
 */
class DrizzleSchemaManager extends AbstractSchemaManager
{
    /**
     * {@inheritdoc}
     */
    protected function _getPortableTableColumnDefinition($tableColumn)
    {
        $dbType = strtolower($tableColumn['DATA_TYPE']);

        $type                          = $this->_platform->getDoctrineTypeMapping($dbType);
        $type                          = $this->extractDoctrineTypeFromComment($tableColumn['COLUMN_COMMENT'], $type);
        $tableColumn['COLUMN_COMMENT'] = $this->removeDoctrineTypeFromComment($tableColumn['COLUMN_COMMENT'], $type);

        $options = [
            'notnull' => ! (bool) $tableColumn['IS_NULLABLE'],
            'length' => (int) $tableColumn['CHARACTER_MAXIMUM_LENGTH'],
            'default' => $tableColumn['COLUMN_DEFAULT'] ?? null,
            'autoincrement' => (bool) $tableColumn['IS_AUTO_INCREMENT'],
            'scale' => (int) $tableColumn['NUMERIC_SCALE'],
            'precision' => (int) $tableColumn['NUMERIC_PRECISION'],
            'comment' => isset($tableColumn['COLUMN_COMMENT']) && $tableColumn['COLUMN_COMMENT'] !== ''
                ? $tableColumn['COLUMN_COMMENT']
                : null,
        ];

        $column = new Column($tableColumn['COLUMN_NAME'], Type::getType($type), $options);

        if (! empty($tableColumn['COLLATION_NAME'])) {
            $column->setPlatformOption('collation', $tableColumn['COLLATION_NAME']);
        }

        return $column;
    }

    /**
     * {@inheritdoc}
     */
    protected function _getPortableDatabaseDefinition($database)
    {
        return $database['SCHEMA_NAME'];
    }

    /**
     * {@inheritdoc}
     */
    protected function _getPortableTableDefinition($table)
    {
        return $table['TABLE_NAME'];
    }

    /**
     * {@inheritdoc}
     */
    public function _getPortableTableForeignKeyDefinition($tableForeignKey)
    {
        $columns = [];
        foreach (explode(',', $tableForeignKey['CONSTRAINT_COLUMNS']) as $value) {
            $columns[] = trim($value, ' `');
        }

        $refColumns = [];
        foreach (explode(',', $tableForeignKey['REFERENCED_TABLE_COLUMNS']) as $value) {
            $refColumns[] = trim($value, ' `');
        }

        return new ForeignKeyConstraint(
            $columns,
            $tableForeignKey['REFERENCED_TABLE_NAME'],
            $refColumns,
            $tableForeignKey['CONSTRAINT_NAME'],
            [
                'onUpdate' => $tableForeignKey['UPDATE_RULE'],
                'onDelete' => $tableForeignKey['DELETE_RULE'],
            ]
        );
    }

    /**
     * {@inheritdoc}
     */
    protected function _getPortableTableIndexesList($tableIndexes, $tableName = null)
    {
        $indexes = [];
        foreach ($tableIndexes as $k) {
            $k['primary'] = (bool) $k['primary'];
            $indexes[]    = $k;
        }

        return parent::_getPortableTableIndexesList($indexes, $tableName);
    }
}
<?php

namespace Doctrine\DBAL\Schema;

use Doctrine\DBAL\DBALException;
use Doctrine\DBAL\Driver\Exception;
use Doctrine\DBAL\Platforms\OraclePlatform;
use Doctrine\DBAL\Types\Type;
use Throwable;

use function array_change_key_case;
use function array_values;
use function assert;
use function is_string;
use function preg_match;
use function sprintf;
use function str_replace;
use function strpos;
use function strtolower;
use function strtoupper;
use function trim;

use const CASE_LOWER;

/**
 * Oracle Schema Manager.
 */
class OracleSchemaManager extends AbstractSchemaManager
{
    /**
     * {@inheritdoc}
     */
    public function dropDatabase($database)
    {
        try {
            parent::dropDatabase($database);
        } catch (DBALException $exception) {
            $exception = $exception->getPrevious();
            assert($exception instanceof Throwable);

            if (! $exception instanceof Exception) {
                throw $exception;
            }

            // If we have a error code 1940 (ORA-01940), the drop database operation failed
            // because of active connections on the database.
            // To force dropping the database, we first have to close all active connections
            // on that database and issue the drop database operation again.
            if ($exception->getErrorCode() !== 1940) {
                throw $exception;
            }

            $this->killUserSessions($database);

            parent::dropDatabase($database);
        }
    }

    /**
     * {@inheritdoc}
     */
    protected function _getPortableViewDefinition($view)
    {
        $view = array_change_key_case($view, CASE_LOWER);

        return new View($this->getQuotedIdentifierName($view['view_name']), $view['text']);
    }

    /**
     * {@inheritdoc}
     */
    protected function _getPortableUserDefinition($user)
    {
        $user = array_change_key_case($user, CASE_LOWER);

        return [
            'user' => $user['username'],
        ];
    }

    /**
     * {@inheritdoc}
     */
    protected function _getPortableTableDefinition($table)
    {
        $table = array_change_key_case($table, CASE_LOWER);

        return $this->getQuotedIdentifierName($table['table_name']);
    }

    /**
     * {@inheritdoc}
     *
     * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html
     */
    protected function _getPortableTableIndexesList($tableIndexes, $tableName = null)
    {
        $indexBuffer = [];
        foreach ($tableIndexes as $tableIndex) {
            $tableIndex = array_change_key_case($tableIndex, CASE_LOWER);

            $keyName = strtolower($tableIndex['name']);
            $buffer  = [];

            if ($tableIndex['is_primary'] === 'P') {
                $keyName              = 'primary';
                $buffer['primary']    = true;
                $buffer['non_unique'] = false;
            } else {
                $buffer['primary']    = false;
                $buffer['non_unique'] = ! $tableIndex['is_unique'];
            }

            $buffer['key_name']    = $keyName;
            $buffer['column_name'] = $this->getQuotedIdentifierName($tableIndex['column_name']);
            $indexBuffer[]         = $buffer;
        }

        return parent::_getPortableTableIndexesList($indexBuffer, $tableName);
    }

    /**
     * {@inheritdoc}
     */
    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';
            }
        }

        $unsigned = $fixed = $precision = $scale = $length = null;

        if (! isset($tableColumn['column_name'])) {
            $tableColumn['column_name'] = '';
        }

        // Default values returned from database sometimes have trailing spaces.
        if (is_string($tableColumn['data_default'])) {
            $tableColumn['data_default'] = trim($tableColumn['data_default']);
        }

        if ($tableColumn['data_default'] === '' || $tableColumn['data_default'] === 'NULL') {
            $tableColumn['data_default'] = null;
        }

        if ($tableColumn['data_default'] !== null) {
            // Default values returned from database are represented as literal expressions
            if (preg_match('/^\'(.*)\'$/s', $tableColumn['data_default'], $matches)) {
                $tableColumn['data_default'] = str_replace("''", "'", $matches[1]);
            }
        }

        if ($tableColumn['data_precision'] !== null) {
            $precision = (int) $tableColumn['data_precision'];
        }

        if ($tableColumn['data_scale'] !== null) {
            $scale = (int) $tableColumn['data_scale'];
        }

        $type                    = $this->_platform->getDoctrineTypeMapping($dbType);
        $type                    = $this->extractDoctrineTypeFromComment($tableColumn['comments'], $type);
        $tableColumn['comments'] = $this->removeDoctrineTypeFromComment($tableColumn['comments'], $type);

        switch ($dbType) {
            case 'number':
                if ($precision === 20 && $scale === 0) {
                    $type = 'bigint';
                } elseif ($precision === 5 && $scale === 0) {
                    $type = 'smallint';
                } elseif ($precision === 1 && $scale === 0) {
                    $type = 'boolean';
                } elseif ($scale > 0) {
                    $type = 'decimal';
                }

                break;

            case 'varchar':
            case 'varchar2':
            case 'nvarchar2':
                $length = $tableColumn['char_length'];
                $fixed  = false;
                break;

            case 'char':
            case 'nchar':
                $length = $tableColumn['char_length'];
                $fixed  = true;
                break;
        }

        $options = [
            'notnull'    => $tableColumn['nullable'] === 'N',
            'fixed'      => (bool) $fixed,
            'unsigned'   => (bool) $unsigned,
            'default'    => $tableColumn['data_default'],
            'length'     => $length,
            'precision'  => $precision,
            'scale'      => $scale,
            'comment'    => isset($tableColumn['comments']) && $tableColumn['comments'] !== ''
                ? $tableColumn['comments']
                : null,
        ];

        return new Column($this->getQuotedIdentifierName($tableColumn['column_name']), Type::getType($type), $options);
    }

    /**
     * {@inheritdoc}
     */
    protected function _getPortableTableForeignKeysList($tableForeignKeys)
    {
        $list = [];
        foreach ($tableForeignKeys as $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']] = [
                    'name' => $this->getQuotedIdentifierName($value['constraint_name']),
                    'local' => [],
                    'foreign' => [],
                    'foreignTable' => $value['references_table'],
                    'onDelete' => $value['delete_rule'],
                ];
            }

            $localColumn   = $this->getQuotedIdentifierName($value['local_column']);
            $foreignColumn = $this->getQuotedIdentifierName($value['foreign_column']);

            $list[$value['constraint_name']]['local'][$value['position']]   = $localColumn;
            $list[$value['constraint_name']]['foreign'][$value['position']] = $foreignColumn;
        }

        $result = [];
        foreach ($list as $constraint) {
            $result[] = new ForeignKeyConstraint(
                array_values($constraint['local']),
                $this->getQuotedIdentifierName($constraint['foreignTable']),
                array_values($constraint['foreign']),
                $this->getQuotedIdentifierName($constraint['name']),
                ['onDelete' => $constraint['onDelete']]
            );
        }

        return $result;
    }

    /**
     * {@inheritdoc}
     */
    protected function _getPortableSequenceDefinition($sequence)
    {
        $sequence = array_change_key_case($sequence, CASE_LOWER);

        return new Sequence(
            $this->getQuotedIdentifierName($sequence['sequence_name']),
            (int) $sequence['increment_by'],
            (int) $sequence['min_value']
        );
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated
     */
    protected function _getPortableFunctionDefinition($function)
    {
        $function = array_change_key_case($function, CASE_LOWER);

        return $function['name'];
    }

    /**
     * {@inheritdoc}
     */
    protected function _getPortableDatabaseDefinition($database)
    {
        $database = array_change_key_case($database, CASE_LOWER);

        return $database['username'];
    }

    /**
     * {@inheritdoc}
     *
     * @param string|null $database
     *
     * Calling this method without an argument or by passing NULL is deprecated.
     */
    public function createDatabase($database = null)
    {
        if ($database === null) {
            $database = $this->_conn->getDatabase();
        }

        $statement = 'CREATE USER ' . $database;

        $params = $this->_conn->getParams();

        if (isset($params['password'])) {
            $statement .= ' IDENTIFIED BY ' . $params['password'];
        }

        $this->_conn->executeStatement($statement);

        $statement = 'GRANT DBA TO ' . $database;
        $this->_conn->executeStatement($statement);
    }

    /**
     * @param string $table
     *
     * @return bool
     */
    public function dropAutoincrement($table)
    {
        assert($this->_platform instanceof OraclePlatform);

        $sql = $this->_platform->getDropAutoincrementSql($table);
        foreach ($sql as $query) {
            $this->_conn->executeStatement($query);
        }

        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function dropTable($name)
    {
        $this->tryMethod('dropAutoincrement', $name);

        parent::dropTable($name);
    }

    /**
     * Returns the quoted representation of the given identifier name.
     *
     * Quotes non-uppercase identifiers explicitly to preserve case
     * and thus make references to the particular identifier work.
     *
     * @param string $identifier The identifier to quote.
     *
     * @return string The quoted identifier.
     */
    private function getQuotedIdentifierName($identifier)
    {
        if (preg_match('/[a-z]/', $identifier)) {
            return $this->_platform->quoteIdentifier($identifier);
        }

        return $identifier;
    }

    /**
     * Kills sessions connected with the given user.
     *
     * This is useful to force DROP USER operations which could fail because of active user sessions.
     *
     * @param string $user The name of the user to kill sessions for.
     *
     * @return void
     */
    private function killUserSessions($user)
    {
        $sql = <<<SQL
SELECT
    s.sid,
    s.serial#
FROM
    gv\$session s,
    gv\$process p
WHERE
    s.username = ?
    AND p.addr(+) = s.paddr
SQL;

        $activeUserSessions = $this->_conn->fetchAllAssociative($sql, [strtoupper($user)]);

        foreach ($activeUserSessions as $activeUserSession) {
            $activeUserSession = array_change_key_case($activeUserSession, CASE_LOWER);

            $this->_execSql(
                sprintf(
                    "ALTER SYSTEM KILL SESSION '%s, %s' IMMEDIATE",
                    $activeUserSession['sid'],
                    $activeUserSession['serial#']
                )
            );
        }
    }

    /**
     * {@inheritdoc}
     */
    public function listTableDetails($name): Table
    {
        $table = parent::listTableDetails($name);

        $platform = $this->_platform;
        assert($platform instanceof OraclePlatform);
        $sql = $platform->getListTableCommentsSQL($name);

        $tableOptions = $this->_conn->fetchAssociative($sql);

        if ($tableOptions !== false) {
            $table->addOption('comment', $tableOptions['COMMENTS']);
        }

        return $table;
    }
}
<?php

namespace Doctrine\DBAL\Schema;

use Doctrine\DBAL\DBALException;
use Doctrine\DBAL\Driver\Exception;
use Doctrine\DBAL\Platforms\SQLServerPlatform;
use Doctrine\DBAL\Types\Type;
use Throwable;

use function assert;
use function count;
use function in_array;
use function is_string;
use function preg_match;
use function sprintf;
use function str_replace;
use function strpos;
use function strtok;

/**
 * SQL Server Schema Manager.
 */
class SQLServerSchemaManager extends AbstractSchemaManager
{
    /**
     * {@inheritdoc}
     */
    public function dropDatabase($database)
    {
        try {
            parent::dropDatabase($database);
        } catch (DBALException $exception) {
            $exception = $exception->getPrevious();
            assert($exception instanceof Throwable);

            if (! $exception instanceof Exception) {
                throw $exception;
            }

            // If we have a error code 3702, the drop database operation failed
            // because of active connections on the database.
            // To force dropping the database, we first have to close all active connections
            // on that database and issue the drop database operation again.
            if ($exception->getErrorCode() !== 3702) {
                throw $exception;
            }

            $this->closeActiveDatabaseConnections($database);

            parent::dropDatabase($database);
        }
    }

    /**
     * {@inheritdoc}
     */
    protected function _getPortableSequenceDefinition($sequence)
    {
        return new Sequence($sequence['name'], (int) $sequence['increment'], (int) $sequence['start_value']);
    }

    /**
     * {@inheritdoc}
     */
    protected function _getPortableTableColumnDefinition($tableColumn)
    {
        $dbType = strtok($tableColumn['type'], '(), ');
        assert(is_string($dbType));

        $fixed   = null;
        $length  = (int) $tableColumn['length'];
        $default = $tableColumn['default'];

        if (! isset($tableColumn['name'])) {
            $tableColumn['name'] = '';
        }

        if ($default !== null) {
            $default = $this->parseDefaultExpression($default);
        }

        switch ($dbType) {
            case 'nchar':
            case 'nvarchar':
            case 'ntext':
                // Unicode data requires 2 bytes per character
                $length /= 2;
                break;

            case 'varchar':
                // TEXT type is returned as VARCHAR(MAX) with a length of -1
                if ($length === -1) {
                    $dbType = 'text';
                }

                break;
        }

        if ($dbType === 'char' || $dbType === 'nchar' || $dbType === 'binary') {
            $fixed = true;
        }

        $type                   = $this->_platform->getDoctrineTypeMapping($dbType);
        $type                   = $this->extractDoctrineTypeFromComment($tableColumn['comment'], $type);
        $tableColumn['comment'] = $this->removeDoctrineTypeFromComment($tableColumn['comment'], $type);

        $options = [
            'length'        => $length === 0 || ! in_array($type, ['text', 'string']) ? null : $length,
            'unsigned'      => false,
            'fixed'         => (bool) $fixed,
            'default'       => $default,
            'notnull'       => (bool) $tableColumn['notnull'],
            'scale'         => $tableColumn['scale'],
            'precision'     => $tableColumn['precision'],
            'autoincrement' => (bool) $tableColumn['autoincrement'],
            'comment'       => $tableColumn['comment'] !== '' ? $tableColumn['comment'] : null,
        ];

        $column = new Column($tableColumn['name'], Type::getType($type), $options);

        if (isset($tableColumn['collation']) && $tableColumn['collation'] !== 'NULL') {
            $column->setPlatformOption('collation', $tableColumn['collation']);
        }

        return $column;
    }

    private function parseDefaultExpression(string $value): ?string
    {
        while (preg_match('/^\((.*)\)$/s', $value, $matches)) {
            $value = $matches[1];
        }

        if ($value === 'NULL') {
            return null;
        }

        if (preg_match('/^\'(.*)\'$/s', $value, $matches)) {
            $value = str_replace("''", "'", $matches[1]);
        }

        if ($value === 'getdate()') {
            return $this->_platform->getCurrentTimestampSQL();
        }

        return $value;
    }

    /**
     * {@inheritdoc}
     */
    protected function _getPortableTableForeignKeysList($tableForeignKeys)
    {
        $foreignKeys = [];

        foreach ($tableForeignKeys as $tableForeignKey) {
            $name = $tableForeignKey['ForeignKey'];

            if (! isset($foreignKeys[$name])) {
                $foreignKeys[$name] = [
                    'local_columns' => [$tableForeignKey['ColumnName']],
                    'foreign_table' => $tableForeignKey['ReferenceTableName'],
                    'foreign_columns' => [$tableForeignKey['ReferenceColumnName']],
                    'name' => $name,
                    'options' => [
                        'onUpdate' => str_replace('_', ' ', $tableForeignKey['update_referential_action_desc']),
                        'onDelete' => str_replace('_', ' ', $tableForeignKey['delete_referential_action_desc']),
                    ],
                ];
            } else {
                $foreignKeys[$name]['local_columns'][]   = $tableForeignKey['ColumnName'];
                $foreignKeys[$name]['foreign_columns'][] = $tableForeignKey['ReferenceColumnName'];
            }
        }

        return parent::_getPortableTableForeignKeysList($foreignKeys);
    }

    /**
     * {@inheritdoc}
     */
    protected function _getPortableTableIndexesList($tableIndexes, $tableName = null)
    {
        foreach ($tableIndexes as &$tableIndex) {
            $tableIndex['non_unique'] = (bool) $tableIndex['non_unique'];
            $tableIndex['primary']    = (bool) $tableIndex['primary'];
            $tableIndex['flags']      = $tableIndex['flags'] ? [$tableIndex['flags']] : null;
        }

        return parent::_getPortableTableIndexesList($tableIndexes, $tableName);
    }

    /**
     * {@inheritdoc}
     */
    protected function _getPortableTableForeignKeyDefinition($tableForeignKey)
    {
        return new ForeignKeyConstraint(
            $tableForeignKey['local_columns'],
            $tableForeignKey['foreign_table'],
            $tableForeignKey['foreign_columns'],
            $tableForeignKey['name'],
            $tableForeignKey['options']
        );
    }

    /**
     * {@inheritdoc}
     */
    protected function _getPortableTableDefinition($table)
    {
        if (isset($table['schema_name']) && $table['schema_name'] !== 'dbo') {
            return $table['schema_name'] . '.' . $table['name'];
        }

        return $table['name'];
    }

    /**
     * {@inheritdoc}
     */
    protected function _getPortableDatabaseDefinition($database)
    {
        return $database['name'];
    }

    /**
     * {@inheritdoc}
     */
    protected function getPortableNamespaceDefinition(array $namespace)
    {
        return $namespace['name'];
    }

    /**
     * {@inheritdoc}
     */
    protected function _getPortableViewDefinition($view)
    {
        // @todo
        return new View($view['name'], '');
    }

    /**
     * {@inheritdoc}
     */
    public function listTableIndexes($table)
    {
        $sql = $this->_platform->getListTableIndexesSQL($table, $this->_conn->getDatabase());

        try {
            $tableIndexes = $this->_conn->fetchAllAssociative($sql);
        } catch (DBALException $e) {
            if (strpos($e->getMessage(), 'SQLSTATE [01000, 15472]') === 0) {
                return [];
            }

            throw $e;
        }

        return $this->_getPortableTableIndexesList($tableIndexes, $table);
    }

    /**
     * {@inheritdoc}
     */
    public function alterTable(TableDiff $tableDiff)
    {
        if (count($tableDiff->removedColumns) > 0) {
            foreach ($tableDiff->removedColumns as $col) {
                $columnConstraintSql = $this->getColumnConstraintSQL($tableDiff->name, $col->getName());
                foreach ($this->_conn->fetchAllAssociative($columnConstraintSql) as $constraint) {
                    $this->_conn->exec(
                        sprintf(
                            'ALTER TABLE %s DROP CONSTRAINT %s',
                            $tableDiff->name,
                            $constraint['Name']
                        )
                    );
                }
            }
        }

        parent::alterTable($tableDiff);
    }

    /**
     * Returns the SQL to retrieve the constraints for a given column.
     *
     * @param string $table
     * @param string $column
     *
     * @return string
     */
    private function getColumnConstraintSQL($table, $column)
    {
        return "SELECT sysobjects.[Name]
            FROM sysobjects INNER JOIN (SELECT [Name],[ID] FROM sysobjects WHERE XType = 'U') AS Tab
            ON Tab.[ID] = sysobjects.[Parent_Obj]
            INNER JOIN sys.default_constraints DefCons ON DefCons.[object_id] = sysobjects.[ID]
            INNER JOIN syscolumns Col ON Col.[ColID] = DefCons.[parent_column_id] AND Col.[ID] = Tab.[ID]
            WHERE Col.[Name] = " . $this->_conn->quote($column) . ' AND Tab.[Name] = ' . $this->_conn->quote($table) . '
            ORDER BY Col.[Name]';
    }

    /**
     * Closes currently active connections on the given database.
     *
     * This is useful to force DROP DATABASE operations which could fail because of active connections.
     *
     * @param string $database The name of the database to close currently active connections for.
     *
     * @return void
     */
    private function closeActiveDatabaseConnections($database)
    {
        $database = new Identifier($database);

        $this->_execSql(
            sprintf(
                'ALTER DATABASE %s SET SINGLE_USER WITH ROLLBACK IMMEDIATE',
                $database->getQuotedName($this->_platform)
            )
        );
    }

    /**
     * @param string $name
     */
    public function listTableDetails($name): Table
    {
        $table = parent::listTableDetails($name);

        $platform = $this->_platform;
        assert($platform instanceof SQLServerPlatform);
        $sql = $platform->getListTableMetadataSQL($name);

        $tableOptions = $this->_conn->fetchAssociative($sql);

        if ($tableOptions !== false) {
            $table->addOption('comment', $tableOptions['table_comment']);
        }

        return $table;
    }
}
<?php

namespace Doctrine\DBAL\Schema;

/**
 * Representation of a Database View.
 */
class View extends AbstractAsset
{
    /** @var string */
    private $sql;

    /**
     * @param string $name
     * @param string $sql
     */
    public function __construct($name, $sql)
    {
        $this->_setName($name);
        $this->sql = $sql;
    }

    /**
     * @return string
     */
    public function getSql()
    {
        return $this->sql;
    }
}
<?php

namespace Doctrine\DBAL\Schema;

use Doctrine\DBAL\Platforms\DB2Platform;
use Doctrine\DBAL\Types\Type;

use function array_change_key_case;
use function assert;
use function preg_match;
use function str_replace;
use function strpos;
use function strtolower;
use function substr;

use const CASE_LOWER;

/**
 * IBM Db2 Schema Manager.
 */
class DB2SchemaManager extends AbstractSchemaManager
{
    /**
     * {@inheritdoc}
     *
     * 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}
     */
    public function listTableNames()
    {
        $sql  = $this->_platform->getListTablesSQL();
        $sql .= ' AND CREATOR = UPPER(' . $this->_conn->quote($this->_conn->getUsername()) . ')';

        $tables = $this->_conn->fetchAllAssociative($sql);

        return $this->filterAssetNames($this->_getPortableTablesList($tables));
    }

    /**
     * {@inheritdoc}
     */
    protected function _getPortableTableColumnDefinition($tableColumn)
    {
        $tableColumn = array_change_key_case($tableColumn, CASE_LOWER);

        $length    = null;
        $fixed     = null;
        $scale     = false;
        $precision = false;

        $default = null;

        if ($tableColumn['default'] !== null && $tableColumn['default'] !== 'NULL') {
            $default = $tableColumn['default'];

            if (preg_match('/^\'(.*)\'$/s', $default, $matches)) {
                $default = str_replace("''", "'", $matches[1]);
            }
        }

        $type = $this->_platform->getDoctrineTypeMapping($tableColumn['typename']);

        if (isset($tableColumn['comment'])) {
            $type                   = $this->extractDoctrineTypeFromComment($tableColumn['comment'], $type);
            $tableColumn['comment'] = $this->removeDoctrineTypeFromComment($tableColumn['comment'], $type);
        }

        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 = [
            'length'        => $length,
            'unsigned'      => false,
            'fixed'         => (bool) $fixed,
            'default'       => $default,
            'autoincrement' => (bool) $tableColumn['autoincrement'],
            'notnull'       => $tableColumn['nulls'] === 'N',
            'scale'         => null,
            'precision'     => null,
            'comment'       => isset($tableColumn['comment']) && $tableColumn['comment'] !== ''
                ? $tableColumn['comment']
                : null,
            'platformOptions' => [],
        ];

        if ($scale !== null && $precision !== null) {
            $options['scale']     = $scale;
            $options['precision'] = $precision;
        }

        return new Column($tableColumn['colname'], Type::getType($type), $options);
    }

    /**
     * {@inheritdoc}
     */
    protected function _getPortableTablesList($tables)
    {
        $tableNames = [];
        foreach ($tables as $tableRow) {
            $tableRow     = array_change_key_case($tableRow, CASE_LOWER);
            $tableNames[] = $tableRow['name'];
        }

        return $tableNames;
    }

    /**
     * {@inheritdoc}
     */
    protected function _getPortableTableIndexesList($tableIndexes, $tableName = null)
    {
        foreach ($tableIndexes as &$tableIndexRow) {
            $tableIndexRow            = array_change_key_case($tableIndexRow, CASE_LOWER);
            $tableIndexRow['primary'] = (bool) $tableIndexRow['primary'];
        }

        return parent::_getPortableTableIndexesList($tableIndexes, $tableName);
    }

    /**
     * {@inheritdoc}
     */
    protected function _getPortableTableForeignKeyDefinition($tableForeignKey)
    {
        return new ForeignKeyConstraint(
            $tableForeignKey['local_columns'],
            $tableForeignKey['foreign_table'],
            $tableForeignKey['foreign_columns'],
            $tableForeignKey['name'],
            $tableForeignKey['options']
        );
    }

    /**
     * {@inheritdoc}
     */
    protected function _getPortableTableForeignKeysList($tableForeignKeys)
    {
        $foreignKeys = [];

        foreach ($tableForeignKeys as $tableForeignKey) {
            $tableForeignKey = array_change_key_case($tableForeignKey, CASE_LOWER);

            if (! isset($foreignKeys[$tableForeignKey['index_name']])) {
                $foreignKeys[$tableForeignKey['index_name']] = [
                    'local_columns'   => [$tableForeignKey['local_column']],
                    'foreign_table'   => $tableForeignKey['foreign_table'],
                    'foreign_columns' => [$tableForeignKey['foreign_column']],
                    'name'            => $tableForeignKey['index_name'],
                    'options'         => [
                        'onUpdate' => $tableForeignKey['on_update'],
                        'onDelete' => $tableForeignKey['on_delete'],
                    ],
                ];
            } else {
                $foreignKeys[$tableForeignKey['index_name']]['local_columns'][]   = $tableForeignKey['local_column'];
                $foreignKeys[$tableForeignKey['index_name']]['foreign_columns'][] = $tableForeignKey['foreign_column'];
            }
        }

        return parent::_getPortableTableForeignKeysList($foreignKeys);
    }

    /**
     * @param string $def
     *
     * @return string|null
     */
    protected function _getPortableForeignKeyRuleDef($def)
    {
        if ($def === 'C') {
            return 'CASCADE';
        }

        if ($def === 'N') {
            return 'SET NULL';
        }

        return null;
    }

    /**
     * {@inheritdoc}
     */
    protected function _getPortableViewDefinition($view)
    {
        $view = array_change_key_case($view, CASE_LOWER);

        $sql = '';
        $pos = strpos($view['text'], ' AS ');

        if ($pos !== false) {
            $sql = substr($view['text'], $pos + 4);
        }

        return new View($view['name'], $sql);
    }

    /**
     * {@inheritdoc}
     */
    public function listTableDetails($name): Table
    {
        $table = parent::listTableDetails($name);

        $platform = $this->_platform;
        assert($platform instanceof DB2Platform);
        $sql = $platform->getListTableCommentsSQL($name);

        $tableOptions = $this->_conn->fetchAssociative($sql);

        if ($tableOptions !== false) {
            $table->addOption('comment', $tableOptions['REMARKS']);
        }

        return $table;
    }
}
<?php

namespace Doctrine\DBAL\Schema;

use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Types\StringType;
use Doctrine\DBAL\Types\TextType;
use Doctrine\DBAL\Types\Type;

use function array_change_key_case;
use function array_map;
use function array_merge;
use function array_reverse;
use function explode;
use function file_exists;
use function preg_match;
use function preg_match_all;
use function preg_quote;
use function preg_replace;
use function rtrim;
use function sprintf;
use function str_replace;
use function strpos;
use function strtolower;
use function trim;
use function unlink;
use function usort;

use const CASE_LOWER;

/**
 * Sqlite SchemaManager.
 */
class SqliteSchemaManager extends AbstractSchemaManager
{
    /**
     * {@inheritdoc}
     */
    public function dropDatabase($database)
    {
        if (! file_exists($database)) {
            return;
        }

        unlink($database);
    }

    /**
     * {@inheritdoc}
     */
    public function createDatabase($database)
    {
        $params = $this->_conn->getParams();

        $params['path'] = $database;
        unset($params['memory']);

        $conn = DriverManager::getConnection($params);
        $conn->connect();
        $conn->close();
    }

    /**
     * {@inheritdoc}
     */
    public function renameTable($name, $newName)
    {
        $tableDiff            = new TableDiff($name);
        $tableDiff->fromTable = $this->listTableDetails($name);
        $tableDiff->newName   = $newName;
        $this->alterTable($tableDiff);
    }

    /**
     * {@inheritdoc}
     */
    public function createForeignKey(ForeignKeyConstraint $foreignKey, $table)
    {
        $tableDiff                     = $this->getTableDiffForAlterForeignKey($table);
        $tableDiff->addedForeignKeys[] = $foreignKey;

        $this->alterTable($tableDiff);
    }

    /**
     * {@inheritdoc}
     */
    public function dropAndCreateForeignKey(ForeignKeyConstraint $foreignKey, $table)
    {
        $tableDiff                       = $this->getTableDiffForAlterForeignKey($table);
        $tableDiff->changedForeignKeys[] = $foreignKey;

        $this->alterTable($tableDiff);
    }

    /**
     * {@inheritdoc}
     */
    public function dropForeignKey($foreignKey, $table)
    {
        $tableDiff                       = $this->getTableDiffForAlterForeignKey($table);
        $tableDiff->removedForeignKeys[] = $foreignKey;

        $this->alterTable($tableDiff);
    }

    /**
     * {@inheritdoc}
     */
    public function listTableForeignKeys($table, $database = null)
    {
        if ($database === null) {
            $database = $this->_conn->getDatabase();
        }

        $sql              = $this->_platform->getListTableForeignKeysSQL($table, $database);
        $tableForeignKeys = $this->_conn->fetchAllAssociative($sql);

        if (! empty($tableForeignKeys)) {
            $createSql = $this->getCreateTableSQL($table);

            if (
                $createSql !== null && preg_match_all(
                    '#
                    (?:CONSTRAINT\s+([^\s]+)\s+)?
                    (?:FOREIGN\s+KEY[^\)]+\)\s*)?
                    REFERENCES\s+[^\s]+\s+(?:\([^\)]+\))?
                    (?:
                        [^,]*?
                        (NOT\s+DEFERRABLE|DEFERRABLE)
                        (?:\s+INITIALLY\s+(DEFERRED|IMMEDIATE))?
                    )?#isx',
                    $createSql,
                    $match
                )
            ) {
                $names      = array_reverse($match[1]);
                $deferrable = array_reverse($match[2]);
                $deferred   = array_reverse($match[3]);
            } else {
                $names = $deferrable = $deferred = [];
            }

            foreach ($tableForeignKeys as $key => $value) {
                $id = $value['id'];

                $tableForeignKeys[$key] = array_merge($tableForeignKeys[$key], [
                    'constraint_name' => isset($names[$id]) && $names[$id] !== '' ? $names[$id] : $id,
                    'deferrable'      => isset($deferrable[$id]) && strtolower($deferrable[$id]) === 'deferrable',
                    'deferred'        => isset($deferred[$id]) && strtolower($deferred[$id]) === 'deferred',
                ]);
            }
        }

        return $this->_getPortableTableForeignKeysList($tableForeignKeys);
    }

    /**
     * {@inheritdoc}
     */
    protected function _getPortableTableDefinition($table)
    {
        return $table['name'];
    }

    /**
     * {@inheritdoc}
     *
     * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html
     */
    protected function _getPortableTableIndexesList($tableIndexes, $tableName = null)
    {
        $indexBuffer = [];

        // fetch primary
        $indexArray = $this->_conn->fetchAllAssociative(sprintf(
            'PRAGMA TABLE_INFO (%s)',
            $this->_conn->quote($tableName)
        ));

        usort(
            $indexArray,
            /**
             * @param array<string,mixed> $a
             * @param array<string,mixed> $b
             */
            static function (array $a, array $b): int {
                if ($a['pk'] === $b['pk']) {
                    return $a['cid'] - $b['cid'];
                }

                return $a['pk'] - $b['pk'];
            }
        );

        foreach ($indexArray as $indexColumnRow) {
            if ($indexColumnRow['pk'] === 0 || $indexColumnRow['pk'] === '0') {
                continue;
            }

            $indexBuffer[] = [
                'key_name' => 'primary',
                'primary' => true,
                'non_unique' => false,
                'column_name' => $indexColumnRow['name'],
            ];
        }

        // fetch regular indexes
        foreach ($tableIndexes as $tableIndex) {
            // Ignore indexes with reserved names, e.g. autoindexes
            if (strpos($tableIndex['name'], 'sqlite_') === 0) {
                continue;
            }

            $keyName           = $tableIndex['name'];
            $idx               = [];
            $idx['key_name']   = $keyName;
            $idx['primary']    = false;
            $idx['non_unique'] = ! $tableIndex['unique'];

            $indexArray = $this->_conn->fetchAllAssociative(sprintf(
                'PRAGMA INDEX_INFO (%s)',
                $this->_conn->quote($keyName)
            ));

            foreach ($indexArray as $indexColumnRow) {
                $idx['column_name'] = $indexColumnRow['name'];
                $indexBuffer[]      = $idx;
            }
        }

        return parent::_getPortableTableIndexesList($indexBuffer, $tableName);
    }

    /**
     * @deprecated
     *
     * @param array<string, mixed> $tableIndex
     *
     * @return array<string, bool|string>
     */
    protected function _getPortableTableIndexDefinition($tableIndex)
    {
        return [
            'name' => $tableIndex['name'],
            'unique' => (bool) $tableIndex['unique'],
        ];
    }

    /**
     * {@inheritdoc}
     */
    protected function _getPortableTableColumnList($table, $database, $tableColumns)
    {
        $list = parent::_getPortableTableColumnList($table, $database, $tableColumns);

        // find column with autoincrement
        $autoincrementColumn = null;
        $autoincrementCount  = 0;

        foreach ($tableColumns as $tableColumn) {
            if ($tableColumn['pk'] === 0 || $tableColumn['pk'] === '0') {
                continue;
            }

            $autoincrementCount++;
            if ($autoincrementColumn !== null || strtolower($tableColumn['type']) !== 'integer') {
                continue;
            }

            $autoincrementColumn = $tableColumn['name'];
        }

        if ($autoincrementCount === 1 && $autoincrementColumn !== null) {
            foreach ($list as $column) {
                if ($autoincrementColumn !== $column->getName()) {
                    continue;
                }

                $column->setAutoincrement(true);
            }
        }

        // inspect column collation and comments
        $createSql = $this->getCreateTableSQL($table) ?? '';

        foreach ($list as $columnName => $column) {
            $type = $column->getType();

            if ($type instanceof StringType || $type instanceof TextType) {
                $column->setPlatformOption(
                    'collation',
                    $this->parseColumnCollationFromSQL($columnName, $createSql) ?: 'BINARY'
                );
            }

            $comment = $this->parseColumnCommentFromSQL($columnName, $createSql);

            if ($comment === null) {
                continue;
            }

            $type = $this->extractDoctrineTypeFromComment($comment, '');

            if ($type !== '') {
                $column->setType(Type::getType($type));

                $comment = $this->removeDoctrineTypeFromComment($comment, $type);
            }

            $column->setComment($comment);
        }

        return $list;
    }

    /**
     * {@inheritdoc}
     */
    protected function _getPortableTableColumnDefinition($tableColumn)
    {
        $parts               = explode('(', $tableColumn['type']);
        $tableColumn['type'] = trim($parts[0]);
        if (isset($parts[1])) {
            $length                = trim($parts[1], ')');
            $tableColumn['length'] = $length;
        }

        $dbType   = strtolower($tableColumn['type']);
        $length   = $tableColumn['length'] ?? null;
        $unsigned = false;

        if (strpos($dbType, ' unsigned') !== false) {
            $dbType   = str_replace(' unsigned', '', $dbType);
            $unsigned = true;
        }

        $fixed   = false;
        $type    = $this->_platform->getDoctrineTypeMapping($dbType);
        $default = $tableColumn['dflt_value'];
        if ($default === 'NULL') {
            $default = null;
        }

        if ($default !== null) {
            // SQLite returns the default value as a literal expression, so we need to parse it
            if (preg_match('/^\'(.*)\'$/s', $default, $matches)) {
                $default = str_replace("''", "'", $matches[1]);
            }
        }

        $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':
                if (isset($tableColumn['length'])) {
                    if (strpos($tableColumn['length'], ',') === false) {
                        $tableColumn['length'] .= ',0';
                    }

                    [$precision, $scale] = array_map('trim', explode(',', $tableColumn['length']));
                }

                $length = null;
                break;
        }

        $options = [
            'length'   => $length,
            'unsigned' => $unsigned,
            'fixed'    => $fixed,
            'notnull'  => $notnull,
            'default'  => $default,
            'precision' => $precision,
            'scale'     => $scale,
            'autoincrement' => false,
        ];

        return new Column($tableColumn['name'], Type::getType($type), $options);
    }

    /**
     * {@inheritdoc}
     */
    protected function _getPortableViewDefinition($view)
    {
        return new View($view['name'], $view['sql']);
    }

    /**
     * {@inheritdoc}
     */
    protected function _getPortableTableForeignKeysList($tableForeignKeys)
    {
        $list = [];
        foreach ($tableForeignKeys as $value) {
            $value = array_change_key_case($value, CASE_LOWER);
            $name  = $value['constraint_name'];
            if (! isset($list[$name])) {
                if (! isset($value['on_delete']) || $value['on_delete'] === 'RESTRICT') {
                    $value['on_delete'] = null;
                }

                if (! isset($value['on_update']) || $value['on_update'] === 'RESTRICT') {
                    $value['on_update'] = null;
                }

                $list[$name] = [
                    'name' => $name,
                    'local' => [],
                    'foreign' => [],
                    'foreignTable' => $value['table'],
                    'onDelete' => $value['on_delete'],
                    'onUpdate' => $value['on_update'],
                    'deferrable' => $value['deferrable'],
                    'deferred' => $value['deferred'],
                ];
            }

            $list[$name]['local'][] = $value['from'];

            if ($value['to'] === null) {
                continue;
            }

            $list[$name]['foreign'][] = $value['to'];
        }

        $result = [];
        foreach ($list as $constraint) {
            $result[] = new ForeignKeyConstraint(
                $constraint['local'],
                $constraint['foreignTable'],
                $constraint['foreign'],
                $constraint['name'],
                [
                    'onDelete' => $constraint['onDelete'],
                    'onUpdate' => $constraint['onUpdate'],
                    'deferrable' => $constraint['deferrable'],
                    'deferred' => $constraint['deferred'],
                ]
            );
        }

        return $result;
    }

    /**
     * @param Table|string $table
     *
     * @return TableDiff
     *
     * @throws Exception
     */
    private function getTableDiffForAlterForeignKey($table)
    {
        if (! $table instanceof Table) {
            $tableDetails = $this->tryMethod('listTableDetails', $table);

            if ($tableDetails === false) {
                throw new Exception(
                    sprintf('Sqlite schema manager requires to modify foreign keys table definition "%s".', $table)
                );
            }

            $table = $tableDetails;
        }

        $tableDiff            = new TableDiff($table->getName());
        $tableDiff->fromTable = $table;

        return $tableDiff;
    }

    private function parseColumnCollationFromSQL(string $column, string $sql): ?string
    {
        $pattern = '{(?:\W' . preg_quote($column) . '\W|\W'
            . preg_quote($this->_platform->quoteSingleIdentifier($column))
            . '\W)[^,(]+(?:\([^()]+\)[^,]*)?(?:(?:DEFAULT|CHECK)\s*(?:\(.*?\))?[^,]*)*COLLATE\s+["\']?([^\s,"\')]+)}is';

        if (preg_match($pattern, $sql, $match) !== 1) {
            return null;
        }

        return $match[1];
    }

    private function parseTableCommentFromSQL(string $table, string $sql): ?string
    {
        $pattern = '/\s* # Allow whitespace characters at start of line
CREATE\sTABLE # Match "CREATE TABLE"
(?:\W"' . preg_quote($this->_platform->quoteSingleIdentifier($table), '/') . '"\W|\W' . preg_quote($table, '/')
            . '\W) # Match table name (quoted and unquoted)
( # Start capture
   (?:\s*--[^\n]*\n?)+ # Capture anything that starts with whitespaces followed by -- until the end of the line(s)
)/ix';

        if (preg_match($pattern, $sql, $match) !== 1) {
            return null;
        }

        $comment = preg_replace('{^\s*--}m', '', rtrim($match[1], "\n"));

        return $comment === '' ? null : $comment;
    }

    private function parseColumnCommentFromSQL(string $column, string $sql): ?string
    {
        $pattern = '{[\s(,](?:\W' . preg_quote($this->_platform->quoteSingleIdentifier($column))
            . '\W|\W' . preg_quote($column) . '\W)(?:\([^)]*?\)|[^,(])*?,?((?:(?!\n))(?:\s*--[^\n]*\n?)+)}i';

        if (preg_match($pattern, $sql, $match) !== 1) {
            return null;
        }

        $comment = preg_replace('{^\s*--}m', '', rtrim($match[1], "\n"));

        return $comment === '' ? null : $comment;
    }

    private function getCreateTableSQL(string $table): ?string
    {
        return $this->_conn->fetchColumn(
            <<<'SQL'
SELECT sql
  FROM (
      SELECT *
        FROM sqlite_master
   UNION ALL
      SELECT *
        FROM sqlite_temp_master
  )
WHERE type = 'table'
AND name = ?
SQL
            ,
            [$table]
        ) ?: null;
    }

    /**
     * @param string $name
     */
    public function listTableDetails($name): Table
    {
        $table = parent::listTableDetails($name);

        $tableCreateSql = $this->getCreateTableSQL($name) ?? '';

        $comment = $this->parseTableCommentFromSQL($name, $tableCreateSql);

        if ($comment !== null) {
            $table->addOption('comment', $comment);
        }

        return $table;
    }
}
<?php

namespace Doctrine\DBAL\Schema;

use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Schema\Visitor\CreateSchemaSqlCollector;
use Doctrine\DBAL\Schema\Visitor\DropSchemaSqlCollector;
use Doctrine\DBAL\Schema\Visitor\NamespaceVisitor;
use Doctrine\DBAL\Schema\Visitor\Visitor;

use function array_keys;
use function strpos;
use function strtolower;

/**
 * Object representation of a database schema.
 *
 * Different vendors have very inconsistent naming with regard to the concept
 * of a "schema". Doctrine understands a schema as the entity that conceptually
 * wraps a set of database objects such as tables, sequences, indexes and
 * foreign keys that belong to each other into a namespace. A Doctrine Schema
 * has nothing to do with the "SCHEMA" defined as in PostgreSQL, it is more
 * related to the concept of "DATABASE" that exists in MySQL and PostgreSQL.
 *
 * Every asset in the doctrine schema has a name. A name consists of either a
 * namespace.local name pair or just a local unqualified name.
 *
 * The abstraction layer that covers a PostgreSQL schema is the namespace of an
 * database object (asset). A schema can have a name, which will be used as
 * default namespace for the unqualified database objects that are created in
 * the schema.
 *
 * In the case of MySQL where cross-database queries are allowed this leads to
 * databases being "misinterpreted" as namespaces. This is intentional, however
 * the CREATE/DROP SQL visitors will just filter this queries and do not
 * execute them. Only the queries for the currently connected database are
 * executed.
 */
class Schema extends AbstractAsset
{
    /**
     * The namespaces in this schema.
     *
     * @var string[]
     */
    private $namespaces = [];

    /** @var Table[] */
    protected $_tables = [];

    /** @var Sequence[] */
    protected $_sequences = [];

    /** @var SchemaConfig */
    protected $_schemaConfig;

    /**
     * @param Table[]    $tables
     * @param Sequence[] $sequences
     * @param string[]   $namespaces
     */
    public function __construct(
        array $tables = [],
        array $sequences = [],
        ?SchemaConfig $schemaConfig = null,
        array $namespaces = []
    ) {
        if ($schemaConfig === null) {
            $schemaConfig = new SchemaConfig();
        }

        $this->_schemaConfig = $schemaConfig;
        $this->_setName($schemaConfig->getName() ?: 'public');

        foreach ($namespaces as $namespace) {
            $this->createNamespace($namespace);
        }

        foreach ($tables as $table) {
            $this->_addTable($table);
        }

        foreach ($sequences as $sequence) {
            $this->_addSequence($sequence);
        }
    }

    /**
     * @return bool
     */
    public function hasExplicitForeignKeyIndexes()
    {
        return $this->_schemaConfig->hasExplicitForeignKeyIndexes();
    }

    /**
     * @return void
     *
     * @throws SchemaException
     */
    protected function _addTable(Table $table)
    {
        $namespaceName = $table->getNamespaceName();
        $tableName     = $table->getFullQualifiedName($this->getName());

        if (isset($this->_tables[$tableName])) {
            throw SchemaException::tableAlreadyExists($tableName);
        }

        if (
            $namespaceName !== null
            && ! $table->isInDefaultNamespace($this->getName())
            && ! $this->hasNamespace($namespaceName)
        ) {
            $this->createNamespace($namespaceName);
        }

        $this->_tables[$tableName] = $table;
        $table->setSchemaConfig($this->_schemaConfig);
    }

    /**
     * @return void
     *
     * @throws SchemaException
     */
    protected function _addSequence(Sequence $sequence)
    {
        $namespaceName = $sequence->getNamespaceName();
        $seqName       = $sequence->getFullQualifiedName($this->getName());

        if (isset($this->_sequences[$seqName])) {
            throw SchemaException::sequenceAlreadyExists($seqName);
        }

        if (
            $namespaceName !== null
            && ! $sequence->isInDefaultNamespace($this->getName())
            && ! $this->hasNamespace($namespaceName)
        ) {
            $this->createNamespace($namespaceName);
        }

        $this->_sequences[$seqName] = $sequence;
    }

    /**
     * Returns the namespaces of this schema.
     *
     * @return string[] A list of namespace names.
     */
    public function getNamespaces()
    {
        return $this->namespaces;
    }

    /**
     * Gets all tables of this schema.
     *
     * @return Table[]
     */
    public function getTables()
    {
        return $this->_tables;
    }

    /**
     * @param string $name
     *
     * @return Table
     *
     * @throws SchemaException
     */
    public function getTable($name)
    {
        $name = $this->getFullQualifiedAssetName($name);
        if (! isset($this->_tables[$name])) {
            throw SchemaException::tableDoesNotExist($name);
        }

        return $this->_tables[$name];
    }

    /**
     * @param string $name
     *
     * @return string
     */
    private function getFullQualifiedAssetName($name)
    {
        $name = $this->getUnquotedAssetName($name);

        if (strpos($name, '.') === false) {
            $name = $this->getName() . '.' . $name;
        }

        return strtolower($name);
    }

    /**
     * Returns the unquoted representation of a given asset name.
     *
     * @param string $assetName Quoted or unquoted representation of an asset name.
     *
     * @return string
     */
    private function getUnquotedAssetName($assetName)
    {
        if ($this->isIdentifierQuoted($assetName)) {
            return $this->trimQuotes($assetName);
        }

        return $assetName;
    }

    /**
     * Does this schema have a namespace with the given name?
     *
     * @param string $name
     *
     * @return bool
     */
    public function hasNamespace($name)
    {
        $name = strtolower($this->getUnquotedAssetName($name));

        return isset($this->namespaces[$name]);
    }

    /**
     * Does this schema have a table with the given name?
     *
     * @param string $name
     *
     * @return bool
     */
    public function hasTable($name)
    {
        $name = $this->getFullQualifiedAssetName($name);

        return isset($this->_tables[$name]);
    }

    /**
     * Gets all table names, prefixed with a schema name, even the default one if present.
     *
     * @return string[]
     */
    public function getTableNames()
    {
        return array_keys($this->_tables);
    }

    /**
     * @param string $name
     *
     * @return bool
     */
    public function hasSequence($name)
    {
        $name = $this->getFullQualifiedAssetName($name);

        return isset($this->_sequences[$name]);
    }

    /**
     * @param string $name
     *
     * @return Sequence
     *
     * @throws SchemaException
     */
    public function getSequence($name)
    {
        $name = $this->getFullQualifiedAssetName($name);
        if (! $this->hasSequence($name)) {
            throw SchemaException::sequenceDoesNotExist($name);
        }

        return $this->_sequences[$name];
    }

    /**
     * @return Sequence[]
     */
    public function getSequences()
    {
        return $this->_sequences;
    }

    /**
     * Creates a new namespace.
     *
     * @param string $name The name of the namespace to create.
     *
     * @return Schema This schema instance.
     *
     * @throws SchemaException
     */
    public function createNamespace($name)
    {
        $unquotedName = strtolower($this->getUnquotedAssetName($name));

        if (isset($this->namespaces[$unquotedName])) {
            throw SchemaException::namespaceAlreadyExists($unquotedName);
        }

        $this->namespaces[$unquotedName] = $name;

        return $this;
    }

    /**
     * Creates a new table.
     *
     * @param string $name
     *
     * @return Table
     */
    public function createTable($name)
    {
        $table = new Table($name);
        $this->_addTable($table);

        foreach ($this->_schemaConfig->getDefaultTableOptions() as $option => $value) {
            $table->addOption($option, $value);
        }

        return $table;
    }

    /**
     * Renames a table.
     *
     * @param string $oldName
     * @param string $newName
     *
     * @return Schema
     */
    public function renameTable($oldName, $newName)
    {
        $table = $this->getTable($oldName);
        $table->_setName($newName);

        $this->dropTable($oldName);
        $this->_addTable($table);

        return $this;
    }

    /**
     * Drops a table from the schema.
     *
     * @param string $name
     *
     * @return Schema
     */
    public function dropTable($name)
    {
        $name = $this->getFullQualifiedAssetName($name);
        $this->getTable($name);
        unset($this->_tables[$name]);

        return $this;
    }

    /**
     * Creates a new sequence.
     *
     * @param string $name
     * @param int    $allocationSize
     * @param int    $initialValue
     *
     * @return Sequence
     */
    public function createSequence($name, $allocationSize = 1, $initialValue = 1)
    {
        $seq = new Sequence($name, $allocationSize, $initialValue);
        $this->_addSequence($seq);

        return $seq;
    }

    /**
     * @param string $name
     *
     * @return Schema
     */
    public function dropSequence($name)
    {
        $name = $this->getFullQualifiedAssetName($name);
        unset($this->_sequences[$name]);

        return $this;
    }

    /**
     * Returns an array of necessary SQL queries to create the schema on the given platform.
     *
     * @return string[]
     */
    public function toSql(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.
     *
     * @return string[]
     */
    public function toDropSql(AbstractPlatform $platform)
    {
        $dropSqlCollector = new DropSchemaSqlCollector($platform);
        $this->visit($dropSqlCollector);

        return $dropSqlCollector->getQueries();
    }

    /**
     * @return string[]
     */
    public function getMigrateToSql(Schema $toSchema, AbstractPlatform $platform)
    {
        $comparator = new Comparator();
        $schemaDiff = $comparator->compare($this, $toSchema);

        return $schemaDiff->toSql($platform);
    }

    /**
     * @return string[]
     */
    public function getMigrateFromSql(Schema $fromSchema, AbstractPlatform $platform)
    {
        $comparator = new Comparator();
        $schemaDiff = $comparator->compare($fromSchema, $this);

        return $schemaDiff->toSql($platform);
    }

    /**
     * @return void
     */
    public function visit(Visitor $visitor)
    {
        $visitor->acceptSchema($this);

        if ($visitor instanceof NamespaceVisitor) {
            foreach ($this->namespaces as $namespace) {
                $visitor->acceptNamespace($namespace);
            }
        }

        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;
        }
    }
}
<?php

namespace Doctrine\DBAL\Schema\Synchronizer;

use Doctrine\DBAL\Connection;
use Doctrine\Deprecations\Deprecation;
use Throwable;

/**
 * Abstract schema synchronizer with methods for executing batches of SQL.
 *
 * @deprecated
 */
abstract class AbstractSchemaSynchronizer implements SchemaSynchronizer
{
    /** @var Connection */
    protected $conn;

    public function __construct(Connection $conn)
    {
        $this->conn = $conn;

        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/4213',
            'SchemaSynchronizer API is deprecated without a replacement and will be removed in DBAL 3.0'
        );
    }

    /**
     * @param string[] $sql
     *
     * @return void
     */
    protected function processSqlSafely(array $sql)
    {
        foreach ($sql as $s) {
            try {
                $this->conn->exec($s);
            } catch (Throwable $e) {
            }
        }
    }

    /**
     * @param string[] $sql
     *
     * @return void
     */
    protected function processSql(array $sql)
    {
        foreach ($sql as $s) {
            $this->conn->exec($s);
        }
    }
}
<?php

namespace Doctrine\DBAL\Schema\Synchronizer;

use Doctrine\DBAL\Schema\Schema;

/**
 * The synchronizer knows how to synchronize a schema with the configured
 * database.
 *
 * @deprecated
 */
interface SchemaSynchronizer
{
    /**
     * Gets the SQL statements that can be executed to create the schema.
     *
     * @return string[]
     */
    public function getCreateSchema(Schema $createSchema);

    /**
     * Gets the SQL Statements to update given schema with the underlying db.
     *
     * @param bool $noDrops
     *
     * @return string[]
     */
    public function getUpdateSchema(Schema $toSchema, $noDrops = false);

    /**
     * Gets the SQL Statements to drop the given schema from underlying db.
     *
     * @return string[]
     */
    public function getDropSchema(Schema $dropSchema);

    /**
     * Gets the SQL statements to drop all schema assets from underlying db.
     *
     * @return string[]
     */
    public function getDropAllSchema();

    /**
     * Creates the Schema.
     *
     * @return void
     */
    public function createSchema(Schema $createSchema);

    /**
     * Updates the Schema to new schema version.
     *
     * @param bool $noDrops
     *
     * @return void
     */
    public function updateSchema(Schema $toSchema, $noDrops = false);

    /**
     * Drops the given database schema from the underlying db.
     *
     * @return void
     */
    public function dropSchema(Schema $dropSchema);

    /**
     * Drops all assets from the underlying db.
     *
     * @return void
     */
    public function dropAllSchema();
}
<?php

namespace Doctrine\DBAL\Schema\Synchronizer;

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Schema\Comparator;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\Visitor\DropSchemaSqlCollector;

use function count;

/**
 * Schema Synchronizer for Default DBAL Connection.
 *
 * @deprecated
 */
class SingleDatabaseSynchronizer extends AbstractSchemaSynchronizer
{
    /** @var AbstractPlatform */
    private $platform;

    public function __construct(Connection $conn)
    {
        parent::__construct($conn);
        $this->platform = $conn->getDatabasePlatform();
    }

    /**
     * {@inheritdoc}
     */
    public function getCreateSchema(Schema $createSchema)
    {
        return $createSchema->toSql($this->platform);
    }

    /**
     * {@inheritdoc}
     */
    public function getUpdateSchema(Schema $toSchema, $noDrops = false)
    {
        $comparator = new Comparator();
        $sm         = $this->conn->getSchemaManager();

        $fromSchema = $sm->createSchema();
        $schemaDiff = $comparator->compare($fromSchema, $toSchema);

        if ($noDrops) {
            return $schemaDiff->toSaveSql($this->platform);
        }

        return $schemaDiff->toSql($this->platform);
    }

    /**
     * {@inheritdoc}
     */
    public function getDropSchema(Schema $dropSchema)
    {
        $visitor = new DropSchemaSqlCollector($this->platform);
        $sm      = $this->conn->getSchemaManager();

        $fullSchema = $sm->createSchema();

        foreach ($fullSchema->getTables() as $table) {
            if ($dropSchema->hasTable($table->getName())) {
                $visitor->acceptTable($table);
            }

            foreach ($table->getForeignKeys() as $foreignKey) {
                if (! $dropSchema->hasTable($table->getName())) {
                    continue;
                }

                if (! $dropSchema->hasTable($foreignKey->getForeignTableName())) {
                    continue;
                }

                $visitor->acceptForeignKey($table, $foreignKey);
            }
        }

        if (! $this->platform->supportsSequences()) {
            return $visitor->getQueries();
        }

        foreach ($dropSchema->getSequences() as $sequence) {
            $visitor->acceptSequence($sequence);
        }

        foreach ($dropSchema->getTables() as $table) {
            $primaryKey = $table->getPrimaryKey();

            if ($primaryKey === null) {
                continue;
            }

            $columns = $primaryKey->getColumns();

            if (count($columns) > 1) {
                continue;
            }

            $checkSequence = $table->getName() . '_' . $columns[0] . '_seq';
            if (! $fullSchema->hasSequence($checkSequence)) {
                continue;
            }

            $visitor->acceptSequence($fullSchema->getSequence($checkSequence));
        }

        return $visitor->getQueries();
    }

    /**
     * {@inheritdoc}
     */
    public function getDropAllSchema()
    {
        $sm      = $this->conn->getSchemaManager();
        $visitor = new DropSchemaSqlCollector($this->platform);

        $schema = $sm->createSchema();
        $schema->visit($visitor);

        return $visitor->getQueries();
    }

    /**
     * {@inheritdoc}
     */
    public function createSchema(Schema $createSchema)
    {
        $this->processSql($this->getCreateSchema($createSchema));
    }

    /**
     * {@inheritdoc}
     */
    public function updateSchema(Schema $toSchema, $noDrops = false)
    {
        $this->processSql($this->getUpdateSchema($toSchema, $noDrops));
    }

    /**
     * {@inheritdoc}
     */
    public function dropSchema(Schema $dropSchema)
    {
        $this->processSqlSafely($this->getDropSchema($dropSchema));
    }

    /**
     * {@inheritdoc}
     */
    public function dropAllSchema()
    {
        $this->processSql($this->getDropAllSchema());
    }
}
<?php

namespace Doctrine\DBAL\Schema;

use Doctrine\DBAL\Platforms\AbstractPlatform;

use function array_map;
use function crc32;
use function dechex;
use function explode;
use function implode;
use function str_replace;
use function strpos;
use function strtolower;
use function strtoupper;
use function substr;

/**
 * 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
 */
abstract class AbstractAsset
{
    /** @var string */
    protected $_name = '';

    /**
     * Namespace of the asset. If none isset the default namespace is assumed.
     *
     * @var string|null
     */
    protected $_namespace;

    /** @var bool */
    protected $_quoted = false;

    /**
     * Sets the name of this asset.
     *
     * @param string $name
     *
     * @return void
     */
    protected function _setName($name)
    {
        if ($this->isIdentifierQuoted($name)) {
            $this->_quoted = true;
            $name          = $this->trimQuotes($name);
        }

        if (strpos($name, '.') !== false) {
            $parts            = explode('.', $name);
            $this->_namespace = $parts[0];
            $name             = $parts[1];
        }

        $this->_name = $name;
    }

    /**
     * Is this asset in the default namespace?
     *
     * @param string $defaultNamespaceName
     *
     * @return bool
     */
    public function isInDefaultNamespace($defaultNamespaceName)
    {
        return $this->_namespace === $defaultNamespaceName || $this->_namespace === null;
    }

    /**
     * Gets the namespace name of this asset.
     *
     * If NULL is returned this means the default namespace is used.
     *
     * @return string|null
     */
    public function getNamespaceName()
    {
        return $this->_namespace;
    }

    /**
     * The shortest name is stripped of the default namespace. All other
     * namespaced elements are returned as full-qualified names.
     *
     * @param string|null $defaultNamespaceName
     *
     * @return string
     */
    public function getShortestName($defaultNamespaceName)
    {
        $shortestName = $this->getName();
        if ($this->_namespace === $defaultNamespaceName) {
            $shortestName = $this->_name;
        }

        return strtolower($shortestName);
    }

    /**
     * The normalized name is full-qualified and lower-cased. Lower-casing is
     * actually wrong, but we have to do it to keep our sanity. If you are
     * using database objects that only differentiate in the casing (FOO vs
     * Foo) then you will NOT be able to use Doctrine Schema abstraction.
     *
     * Every non-namespaced element is prefixed with the default namespace
     * name which is passed as argument to this method.
     *
     * @param string $defaultNamespaceName
     *
     * @return string
     */
    public function getFullQualifiedName($defaultNamespaceName)
    {
        $name = $this->getName();
        if (! $this->_namespace) {
            $name = $defaultNamespaceName . '.' . $name;
        }

        return strtolower($name);
    }

    /**
     * Checks if this asset's name is quoted.
     *
     * @return bool
     */
    public function isQuoted()
    {
        return $this->_quoted;
    }

    /**
     * Checks if this identifier is quoted.
     *
     * @param string $identifier
     *
     * @return bool
     */
    protected function isIdentifierQuoted($identifier)
    {
        return isset($identifier[0]) && ($identifier[0] === '`' || $identifier[0] === '"' || $identifier[0] === '[');
    }

    /**
     * Trim quotes from the identifier.
     *
     * @param string $identifier
     *
     * @return string
     */
    protected function trimQuotes($identifier)
    {
        return str_replace(['`', '"', '[', ']'], '', $identifier);
    }

    /**
     * Returns the name of this schema asset.
     *
     * @return string
     */
    public function getName()
    {
        if ($this->_namespace) {
            return $this->_namespace . '.' . $this->_name;
        }

        return $this->_name;
    }

    /**
     * Gets the quoted representation of this asset but only if it was defined with one. Otherwise
     * return the plain unquoted value as inserted.
     *
     * @return string
     */
    public function getQuotedName(AbstractPlatform $platform)
    {
        $keywords = $platform->getReservedKeywordsList();
        $parts    = explode('.', $this->getName());
        foreach ($parts as $k => $v) {
            $parts[$k] = $this->_quoted || $keywords->isKeyword($v) ? $platform->quoteIdentifier($v) : $v;
        }

        return implode('.', $parts);
    }

    /**
     * Generates 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 string[] $columnNames
     * @param string   $prefix
     * @param int      $maxSize
     *
     * @return string
     */
    protected function _generateIdentifierName($columnNames, $prefix = '', $maxSize = 30)
    {
        $hash = implode('', array_map(static function ($column) {
            return dechex(crc32($column));
        }, $columnNames));

        return strtoupper(substr($prefix . '_' . $hash, 0, $maxSize));
    }
}
<?php

namespace Doctrine\DBAL\Schema;

use Doctrine\DBAL\Types;

use function array_intersect_key;
use function array_key_exists;
use function array_keys;
use function array_map;
use function array_merge;
use function array_shift;
use function array_unique;
use function assert;
use function count;
use function get_class;
use function strtolower;

/**
 * Compares two Schemas and return an instance of SchemaDiff.
 */
class Comparator
{
    /**
     * @return SchemaDiff
     */
    public static 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 differences 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.
     *
     * @return SchemaDiff
     */
    public function compare(Schema $fromSchema, Schema $toSchema)
    {
        $diff             = new SchemaDiff();
        $diff->fromSchema = $fromSchema;

        $foreignKeysToTable = [];

        foreach ($toSchema->getNamespaces() as $namespace) {
            if ($fromSchema->hasNamespace($namespace)) {
                continue;
            }

            $diff->newNamespaces[$namespace] = $namespace;
        }

        foreach ($fromSchema->getNamespaces() as $namespace) {
            if ($toSchema->hasNamespace($namespace)) {
                continue;
            }

            $diff->removedNamespaces[$namespace] = $namespace;
        }

        foreach ($toSchema->getTables() as $table) {
            $tableName = $table->getShortestName($toSchema->getName());
            if (! $fromSchema->hasTable($tableName)) {
                $diff->newTables[$tableName] = $toSchema->getTable($tableName);
            } else {
                $tableDifferences = $this->diffTable(
                    $fromSchema->getTable($tableName),
                    $toSchema->getTable($tableName)
                );

                if ($tableDifferences !== false) {
                    $diff->changedTables[$tableName] = $tableDifferences;
                }
            }
        }

        /* Check if there are tables removed */
        foreach ($fromSchema->getTables() as $table) {
            $tableName = $table->getShortestName($fromSchema->getName());

            $table = $fromSchema->getTable($tableName);
            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] = [];
                }

                $foreignKeysToTable[$foreignTable][] = $foreignKey;
            }
        }

        foreach ($diff->removedTables as $tableName => $table) {
            if (! isset($foreignKeysToTable[$tableName])) {
                continue;
            }

            $diff->orphanedForeignKeys = array_merge($diff->orphanedForeignKeys, $foreignKeysToTable[$tableName]);

            // deleting duplicated foreign keys present on both on the orphanedForeignKey
            // and the removedForeignKeys from changedTables
            foreach ($foreignKeysToTable[$tableName] as $foreignKey) {
                // strtolower the table name to make if compatible with getShortestName
                $localTableName = strtolower($foreignKey->getLocalTableName());
                if (! isset($diff->changedTables[$localTableName])) {
                    continue;
                }

                foreach ($diff->changedTables[$localTableName]->removedForeignKeys as $key => $removedForeignKey) {
                    assert($removedForeignKey instanceof ForeignKeyConstraint);

                    // We check if the key is from the removed table if not we skip.
                    if ($tableName !== strtolower($removedForeignKey->getForeignTableName())) {
                        continue;
                    }

                    unset($diff->changedTables[$localTableName]->removedForeignKeys[$key]);
                }
            }
        }

        foreach ($toSchema->getSequences() as $sequence) {
            $sequenceName = $sequence->getShortestName($toSchema->getName());
            if (! $fromSchema->hasSequence($sequenceName)) {
                if (! $this->isAutoIncrementSequenceInSchema($fromSchema, $sequence)) {
                    $diff->newSequences[] = $sequence;
                }
            } else {
                if ($this->diffSequence($sequence, $fromSchema->getSequence($sequenceName))) {
                    $diff->changedSequences[] = $toSchema->getSequence($sequenceName);
                }
            }
        }

        foreach ($fromSchema->getSequences() as $sequence) {
            if ($this->isAutoIncrementSequenceInSchema($toSchema, $sequence)) {
                continue;
            }

            $sequenceName = $sequence->getShortestName($fromSchema->getName());

            if ($toSchema->hasSequence($sequenceName)) {
                continue;
            }

            $diff->removedSequences[] = $sequence;
        }

        return $diff;
    }

    /**
     * @param Schema   $schema
     * @param Sequence $sequence
     *
     * @return bool
     */
    private function isAutoIncrementSequenceInSchema($schema, $sequence)
    {
        foreach ($schema->getTables() as $table) {
            if ($sequence->isAutoIncrementsFor($table)) {
                return true;
            }
        }

        return false;
    }

    /**
     * @return bool
     */
    public function diffSequence(Sequence $sequence1, Sequence $sequence2)
    {
        if ($sequence1->getAllocationSize() !== $sequence2->getAllocationSize()) {
            return true;
        }

        return $sequence1->getInitialValue() !== $sequence2->getInitialValue();
    }

    /**
     * Returns the difference between the tables $fromTable and $toTable.
     *
     * If there are no differences this method returns the boolean false.
     *
     * @return TableDiff|false
     */
    public function diffTable(Table $fromTable, Table $toTable)
    {
        $changes                     = 0;
        $tableDifferences            = new TableDiff($fromTable->getName());
        $tableDifferences->fromTable = $fromTable;

        $fromTableColumns = $fromTable->getColumns();
        $toTableColumns   = $toTable->getColumns();

        /* See if all the columns in "from" table exist in "to" table */
        foreach ($toTableColumns as $columnName => $column) {
            if ($fromTable->hasColumn($columnName)) {
                continue;
            }

            $tableDifferences->addedColumns[$columnName] = $column;
            $changes++;
        }

        /* See if there are any removed columns in "to" table */
        foreach ($fromTableColumns as $columnName => $column) {
            // See if column is removed in "to" table.
            if (! $toTable->hasColumn($columnName)) {
                $tableDifferences->removedColumns[$columnName] = $column;
                $changes++;
                continue;
            }

            // See if column has changed properties in "to" table.
            $changedProperties = $this->diffColumn($column, $toTable->getColumn($columnName));

            if (empty($changedProperties)) {
                continue;
            }

            $columnDiff = new ColumnDiff($column->getName(), $toTable->getColumn($columnName), $changedProperties);

            $columnDiff->fromColumn                               = $column;
            $tableDifferences->changedColumns[$column->getName()] = $columnDiff;
            $changes++;
        }

        $this->detectColumnRenamings($tableDifferences);

        $fromTableIndexes = $fromTable->getIndexes();
        $toTableIndexes   = $toTable->getIndexes();

        /* See if all the indexes in "from" table exist in "to" table */
        foreach ($toTableIndexes as $indexName => $index) {
            if (($index->isPrimary() && $fromTable->hasPrimaryKey()) || $fromTable->hasIndex($indexName)) {
                continue;
            }

            $tableDifferences->addedIndexes[$indexName] = $index;
            $changes++;
        }

        /* See if there are any removed indexes in "to" table */
        foreach ($fromTableIndexes as $indexName => $index) {
            // See if index is removed in "to" table.
            if (
                ($index->isPrimary() && ! $toTable->hasPrimaryKey()) ||
                ! $index->isPrimary() && ! $toTable->hasIndex($indexName)
            ) {
                $tableDifferences->removedIndexes[$indexName] = $index;
                $changes++;
                continue;
            }

            // See if index has changed in "to" table.
            $toTableIndex = $index->isPrimary() ? $toTable->getPrimaryKey() : $toTable->getIndex($indexName);
            assert($toTableIndex instanceof Index);

            if (! $this->diffIndex($index, $toTableIndex)) {
                continue;
            }

            $tableDifferences->changedIndexes[$indexName] = $toTableIndex;
            $changes++;
        }

        $this->detectIndexRenamings($tableDifferences);

        $fromForeignKeys = $fromTable->getForeignKeys();
        $toForeignKeys   = $toTable->getForeignKeys();

        foreach ($fromForeignKeys as $fromKey => $fromConstraint) {
            foreach ($toForeignKeys as $toKey => $toConstraint) {
                if ($this->diffForeignKey($fromConstraint, $toConstraint) === false) {
                    unset($fromForeignKeys[$fromKey], $toForeignKeys[$toKey]);
                } else {
                    if (strtolower($fromConstraint->getName()) === strtolower($toConstraint->getName())) {
                        $tableDifferences->changedForeignKeys[] = $toConstraint;
                        $changes++;
                        unset($fromForeignKeys[$fromKey], $toForeignKeys[$toKey]);
                    }
                }
            }
        }

        foreach ($fromForeignKeys as $fromConstraint) {
            $tableDifferences->removedForeignKeys[] = $fromConstraint;
            $changes++;
        }

        foreach ($toForeignKeys as $toConstraint) {
            $tableDifferences->addedForeignKeys[] = $toConstraint;
            $changes++;
        }

        return $changes ? $tableDifferences : false;
    }

    /**
     * Try to find columns that only changed their name, rename operations maybe cheaper than add/drop
     * however ambiguities between different possibilities should not lead to renaming at all.
     *
     * @return void
     */
    private function detectColumnRenamings(TableDiff $tableDifferences)
    {
        $renameCandidates = [];
        foreach ($tableDifferences->addedColumns as $addedColumnName => $addedColumn) {
            foreach ($tableDifferences->removedColumns as $removedColumn) {
                if (count($this->diffColumn($addedColumn, $removedColumn)) !== 0) {
                    continue;
                }

                $renameCandidates[$addedColumn->getName()][] = [$removedColumn, $addedColumn, $addedColumnName];
            }
        }

        foreach ($renameCandidates as $candidateColumns) {
            if (count($candidateColumns) !== 1) {
                continue;
            }

            [$removedColumn, $addedColumn] = $candidateColumns[0];
            $removedColumnName             = strtolower($removedColumn->getName());
            $addedColumnName               = strtolower($addedColumn->getName());

            if (isset($tableDifferences->renamedColumns[$removedColumnName])) {
                continue;
            }

            $tableDifferences->renamedColumns[$removedColumnName] = $addedColumn;
            unset(
                $tableDifferences->addedColumns[$addedColumnName],
                $tableDifferences->removedColumns[$removedColumnName]
            );
        }
    }

    /**
     * Try to find indexes that only changed their name, rename operations maybe cheaper than add/drop
     * however ambiguities between different possibilities should not lead to renaming at all.
     *
     * @return void
     */
    private function detectIndexRenamings(TableDiff $tableDifferences)
    {
        $renameCandidates = [];

        // Gather possible rename candidates by comparing each added and removed index based on semantics.
        foreach ($tableDifferences->addedIndexes as $addedIndexName => $addedIndex) {
            foreach ($tableDifferences->removedIndexes as $removedIndex) {
                if ($this->diffIndex($addedIndex, $removedIndex)) {
                    continue;
                }

                $renameCandidates[$addedIndex->getName()][] = [$removedIndex, $addedIndex, $addedIndexName];
            }
        }

        foreach ($renameCandidates as $candidateIndexes) {
            // If the current rename candidate contains exactly one semantically equal index,
            // we can safely rename it.
            // Otherwise it is unclear if a rename action is really intended,
            // therefore we let those ambiguous indexes be added/dropped.
            if (count($candidateIndexes) !== 1) {
                continue;
            }

            [$removedIndex, $addedIndex] = $candidateIndexes[0];

            $removedIndexName = strtolower($removedIndex->getName());
            $addedIndexName   = strtolower($addedIndex->getName());

            if (isset($tableDifferences->renamedIndexes[$removedIndexName])) {
                continue;
            }

            $tableDifferences->renamedIndexes[$removedIndexName] = $addedIndex;
            unset(
                $tableDifferences->addedIndexes[$addedIndexName],
                $tableDifferences->removedIndexes[$removedIndexName]
            );
        }
    }

    /**
     * @return bool
     */
    public function diffForeignKey(ForeignKeyConstraint $key1, ForeignKeyConstraint $key2)
    {
        if (
            array_map('strtolower', $key1->getUnquotedLocalColumns())
            !== array_map('strtolower', $key2->getUnquotedLocalColumns())
        ) {
            return true;
        }

        if (
            array_map('strtolower', $key1->getUnquotedForeignColumns())
            !== array_map('strtolower', $key2->getUnquotedForeignColumns())
        ) {
            return true;
        }

        if ($key1->getUnqualifiedForeignTableName() !== $key2->getUnqualifiedForeignTableName()) {
            return true;
        }

        if ($key1->onUpdate() !== $key2->onUpdate()) {
            return true;
        }

        return $key1->onDelete() !== $key2->onDelete();
    }

    /**
     * Returns the difference between the columns
     *
     * If there are differences this method returns $field2, otherwise the
     * boolean false.
     *
     * @return string[]
     */
    public function diffColumn(Column $column1, Column $column2)
    {
        $properties1 = $column1->toArray();
        $properties2 = $column2->toArray();

        $changedProperties = [];

        if (get_class($properties1['type']) !== get_class($properties2['type'])) {
            $changedProperties[] = 'type';
        }

        foreach (['notnull', 'unsigned', 'autoincrement'] as $property) {
            if ($properties1[$property] === $properties2[$property]) {
                continue;
            }

            $changedProperties[] = $property;
        }

        // This is a very nasty hack to make comparator work with the legacy json_array type,
        // which should be killed in v3
        if ($this->isALegacyJsonComparison($properties1['type'], $properties2['type'])) {
            array_shift($changedProperties);

            $changedProperties[] = 'comment';
        }

        // Null values need to be checked additionally as they tell whether to create or drop a default value.
        // null != 0, null != false, null != '' etc. This affects platform's table alteration SQL generation.
        if (
            ($properties1['default'] === null) !== ($properties2['default'] === null)
            || $properties1['default'] != $properties2['default']
        ) {
            $changedProperties[] = 'default';
        }

        if (
            ($properties1['type'] instanceof Types\StringType && ! $properties1['type'] instanceof Types\GuidType) ||
            $properties1['type'] instanceof Types\BinaryType
        ) {
            // check if value of length is set at all, default value assumed otherwise.
            $length1 = $properties1['length'] ?: 255;
            $length2 = $properties2['length'] ?: 255;
            if ($length1 !== $length2) {
                $changedProperties[] = 'length';
            }

            if ($properties1['fixed'] !== $properties2['fixed']) {
                $changedProperties[] = 'fixed';
            }
        } elseif ($properties1['type'] instanceof Types\DecimalType) {
            if (($properties1['precision'] ?: 10) !== ($properties2['precision'] ?: 10)) {
                $changedProperties[] = 'precision';
            }

            if ($properties1['scale'] !== $properties2['scale']) {
                $changedProperties[] = 'scale';
            }
        }

        // A null value and an empty string are actually equal for a comment so they should not trigger a change.
        if (
            $properties1['comment'] !== $properties2['comment'] &&
            ! ($properties1['comment'] === null && $properties2['comment'] === '') &&
            ! ($properties2['comment'] === null && $properties1['comment'] === '')
        ) {
            $changedProperties[] = 'comment';
        }

        $customOptions1 = $column1->getCustomSchemaOptions();
        $customOptions2 = $column2->getCustomSchemaOptions();

        foreach (array_merge(array_keys($customOptions1), array_keys($customOptions2)) as $key) {
            if (! array_key_exists($key, $properties1) || ! array_key_exists($key, $properties2)) {
                $changedProperties[] = $key;
            } elseif ($properties1[$key] !== $properties2[$key]) {
                $changedProperties[] = $key;
            }
        }

        $platformOptions1 = $column1->getPlatformOptions();
        $platformOptions2 = $column2->getPlatformOptions();

        foreach (array_keys(array_intersect_key($platformOptions1, $platformOptions2)) as $key) {
            if ($properties1[$key] === $properties2[$key]) {
                continue;
            }

            $changedProperties[] = $key;
        }

        return array_unique($changedProperties);
    }

    /**
     * TODO: kill with fire on v3.0
     *
     * @deprecated
     */
    private function isALegacyJsonComparison(Types\Type $one, Types\Type $other): bool
    {
        if (! $one instanceof Types\JsonType || ! $other instanceof Types\JsonType) {
            return false;
        }

        return (! $one instanceof Types\JsonArrayType && $other instanceof Types\JsonArrayType)
            || (! $other instanceof Types\JsonArrayType && $one instanceof Types\JsonArrayType);
    }

    /**
     * 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.
     *
     * @return bool
     */
    public function diffIndex(Index $index1, Index $index2)
    {
        return ! ($index1->isFullfilledBy($index2) && $index2->isFullfilledBy($index1));
    }
}
<?php

namespace Doctrine\DBAL\Schema;

use Doctrine\DBAL\Platforms\AbstractPlatform;

/**
 * Table Diff.
 */
class TableDiff
{
    /** @var string */
    public $name;

    /** @var string|false */
    public $newName = false;

    /**
     * All added columns
     *
     * @var Column[]
     */
    public $addedColumns;

    /**
     * All changed columns
     *
     * @var ColumnDiff[]
     */
    public $changedColumns = [];

    /**
     * All removed columns
     *
     * @var Column[]
     */
    public $removedColumns = [];

    /**
     * Columns that are only renamed from key to column instance name.
     *
     * @var Column[]
     */
    public $renamedColumns = [];

    /**
     * All added indexes.
     *
     * @var Index[]
     */
    public $addedIndexes = [];

    /**
     * All changed indexes.
     *
     * @var Index[]
     */
    public $changedIndexes = [];

    /**
     * All removed indexes
     *
     * @var Index[]
     */
    public $removedIndexes = [];

    /**
     * Indexes that are only renamed but are identical otherwise.
     *
     * @var Index[]
     */
    public $renamedIndexes = [];

    /**
     * All added foreign key definitions
     *
     * @var ForeignKeyConstraint[]
     */
    public $addedForeignKeys = [];

    /**
     * All changed foreign keys
     *
     * @var ForeignKeyConstraint[]
     */
    public $changedForeignKeys = [];

    /**
     * All removed foreign keys
     *
     * @var ForeignKeyConstraint[]|string[]
     */
    public $removedForeignKeys = [];

    /** @var Table|null */
    public $fromTable;

    /**
     * Constructs an TableDiff object.
     *
     * @param string       $tableName
     * @param Column[]     $addedColumns
     * @param ColumnDiff[] $changedColumns
     * @param Column[]     $removedColumns
     * @param Index[]      $addedIndexes
     * @param Index[]      $changedIndexes
     * @param Index[]      $removedIndexes
     */
    public function __construct(
        $tableName,
        $addedColumns = [],
        $changedColumns = [],
        $removedColumns = [],
        $addedIndexes = [],
        $changedIndexes = [],
        $removedIndexes = [],
        ?Table $fromTable = null
    ) {
        $this->name           = $tableName;
        $this->addedColumns   = $addedColumns;
        $this->changedColumns = $changedColumns;
        $this->removedColumns = $removedColumns;
        $this->addedIndexes   = $addedIndexes;
        $this->changedIndexes = $changedIndexes;
        $this->removedIndexes = $removedIndexes;
        $this->fromTable      = $fromTable;
    }

    /**
     * @param AbstractPlatform $platform The platform to use for retrieving this table diff's name.
     *
     * @return Identifier
     */
    public function getName(AbstractPlatform $platform)
    {
        return new Identifier(
            $this->fromTable instanceof Table ? $this->fromTable->getQuotedName($platform) : $this->name
        );
    }

    /**
     * @return Identifier|false
     */
    public function getNewName()
    {
        if ($this->newName === false) {
            return false;
        }

        return new Identifier($this->newName);
    }
}
<?php

namespace Doctrine\DBAL\Schema;

use Doctrine\DBAL\Platforms\AbstractPlatform;

use function array_keys;
use function array_map;
use function in_array;
use function strrpos;
use function strtolower;
use function strtoupper;
use function substr;

/**
 * An abstraction class for a foreign key constraint.
 */
class ForeignKeyConstraint extends AbstractAsset implements Constraint
{
    /**
     * Instance of the referencing table the foreign key constraint is associated with.
     *
     * @var Table
     */
    protected $_localTable;

    /**
     * Asset identifier instances of the referencing table column names the foreign key constraint is associated with.
     * array($columnName => Identifier)
     *
     * @var Identifier[]
     */
    protected $_localColumnNames;

    /**
     * Table or asset identifier instance of the referenced table name the foreign key constraint is associated with.
     *
     * @var Table|Identifier
     */
    protected $_foreignTableName;

    /**
     * Asset identifier instances of the referenced table column names the foreign key constraint is associated with.
     * array($columnName => Identifier)
     *
     * @var Identifier[]
     */
    protected $_foreignColumnNames;

    /**
     * Options associated with the foreign key constraint.
     *
     * @var mixed[]
     */
    protected $_options;

    /**
     * Initializes the foreign key constraint.
     *
     * @param string[]     $localColumnNames   Names of the referencing table columns.
     * @param Table|string $foreignTableName   Referenced table.
     * @param string[]     $foreignColumnNames Names of the referenced table columns.
     * @param string|null  $name               Name of the foreign key constraint.
     * @param mixed[]      $options            Options associated with the foreign key constraint.
     */
    public function __construct(
        array $localColumnNames,
        $foreignTableName,
        array $foreignColumnNames,
        $name = null,
        array $options = []
    ) {
        if ($name !== null) {
            $this->_setName($name);
        }

        $this->_localColumnNames = $this->createIdentifierMap($localColumnNames);

        if ($foreignTableName instanceof Table) {
            $this->_foreignTableName = $foreignTableName;
        } else {
            $this->_foreignTableName = new Identifier($foreignTableName);
        }

        $this->_foreignColumnNames = $this->createIdentifierMap($foreignColumnNames);
        $this->_options            = $options;
    }

    /**
     * @param string[] $names
     *
     * @return Identifier[]
     */
    private function createIdentifierMap(array $names): array
    {
        $identifiers = [];

        foreach ($names as $name) {
            $identifiers[$name] = new Identifier($name);
        }

        return $identifiers;
    }

    /**
     * Returns the name of the referencing table
     * the foreign key constraint is associated with.
     *
     * @return string
     */
    public function getLocalTableName()
    {
        return $this->_localTable->getName();
    }

    /**
     * Sets the Table instance of the referencing table
     * the foreign key constraint is associated with.
     *
     * @param Table $table Instance of the referencing table.
     *
     * @return void
     */
    public function setLocalTable(Table $table)
    {
        $this->_localTable = $table;
    }

    /**
     * @return Table
     */
    public function getLocalTable()
    {
        return $this->_localTable;
    }

    /**
     * Returns the names of the referencing table columns
     * the foreign key constraint is associated with.
     *
     * @return string[]
     */
    public function getLocalColumns()
    {
        return array_keys($this->_localColumnNames);
    }

    /**
     * Returns the quoted representation of the referencing table column names
     * the foreign key constraint is associated with.
     *
     * But only if they were defined with one or the referencing table column name
     * is a keyword reserved by the platform.
     * Otherwise the plain unquoted value as inserted is returned.
     *
     * @param AbstractPlatform $platform The platform to use for quotation.
     *
     * @return string[]
     */
    public function getQuotedLocalColumns(AbstractPlatform $platform)
    {
        $columns = [];

        foreach ($this->_localColumnNames as $column) {
            $columns[] = $column->getQuotedName($platform);
        }

        return $columns;
    }

    /**
     * Returns unquoted representation of local table column names for comparison with other FK
     *
     * @return string[]
     */
    public function getUnquotedLocalColumns()
    {
        return array_map([$this, 'trimQuotes'], $this->getLocalColumns());
    }

    /**
     * Returns unquoted representation of foreign table column names for comparison with other FK
     *
     * @return string[]
     */
    public function getUnquotedForeignColumns()
    {
        return array_map([$this, 'trimQuotes'], $this->getForeignColumns());
    }

    /**
     * {@inheritdoc}
     *
     * @see getLocalColumns
     */
    public function getColumns()
    {
        return $this->getLocalColumns();
    }

    /**
     * Returns the quoted representation of the referencing table column names
     * the foreign key constraint is associated with.
     *
     * But only if they were defined with one or the referencing table column name
     * is a keyword reserved by the platform.
     * Otherwise the plain unquoted value as inserted is returned.
     *
     * @see getQuotedLocalColumns
     *
     * @param AbstractPlatform $platform The platform to use for quotation.
     *
     * @return string[]
     */
    public function getQuotedColumns(AbstractPlatform $platform)
    {
        return $this->getQuotedLocalColumns($platform);
    }

    /**
     * Returns the name of the referenced table
     * the foreign key constraint is associated with.
     *
     * @return string
     */
    public function getForeignTableName()
    {
        return $this->_foreignTableName->getName();
    }

    /**
     * Returns the non-schema qualified foreign table name.
     *
     * @return string
     */
    public function getUnqualifiedForeignTableName()
    {
        $name     = $this->_foreignTableName->getName();
        $position = strrpos($name, '.');

        if ($position !== false) {
            $name = substr($name, $position + 1);
        }

        return strtolower($name);
    }

    /**
     * Returns the quoted representation of the referenced table name
     * the foreign key constraint is associated with.
     *
     * But only if it was defined with one or the referenced table name
     * is a keyword reserved by the platform.
     * Otherwise the plain unquoted value as inserted is returned.
     *
     * @param AbstractPlatform $platform The platform to use for quotation.
     *
     * @return string
     */
    public function getQuotedForeignTableName(AbstractPlatform $platform)
    {
        return $this->_foreignTableName->getQuotedName($platform);
    }

    /**
     * Returns the names of the referenced table columns
     * the foreign key constraint is associated with.
     *
     * @return string[]
     */
    public function getForeignColumns()
    {
        return array_keys($this->_foreignColumnNames);
    }

    /**
     * Returns the quoted representation of the referenced table column names
     * the foreign key constraint is associated with.
     *
     * But only if they were defined with one or the referenced table column name
     * is a keyword reserved by the platform.
     * Otherwise the plain unquoted value as inserted is returned.
     *
     * @param AbstractPlatform $platform The platform to use for quotation.
     *
     * @return string[]
     */
    public function getQuotedForeignColumns(AbstractPlatform $platform)
    {
        $columns = [];

        foreach ($this->_foreignColumnNames as $column) {
            $columns[] = $column->getQuotedName($platform);
        }

        return $columns;
    }

    /**
     * Returns whether or not a given option
     * is associated with the foreign key constraint.
     *
     * @param string $name Name of the option to check.
     *
     * @return bool
     */
    public function hasOption($name)
    {
        return isset($this->_options[$name]);
    }

    /**
     * Returns an option associated with the foreign key constraint.
     *
     * @param string $name Name of the option the foreign key constraint is associated with.
     *
     * @return mixed
     */
    public function getOption($name)
    {
        return $this->_options[$name];
    }

    /**
     * Returns the options associated with the foreign key constraint.
     *
     * @return mixed[]
     */
    public function getOptions()
    {
        return $this->_options;
    }

    /**
     * Returns the referential action for UPDATE operations
     * on the referenced table the foreign key constraint is associated with.
     *
     * @return string|null
     */
    public function onUpdate()
    {
        return $this->onEvent('onUpdate');
    }

    /**
     * Returns the referential action for DELETE operations
     * on the referenced table the foreign key constraint is associated with.
     *
     * @return string|null
     */
    public function onDelete()
    {
        return $this->onEvent('onDelete');
    }

    /**
     * Returns the referential action for a given database operation
     * on the referenced table the foreign key constraint is associated with.
     *
     * @param string $event Name of the database operation/event to return the referential action for.
     *
     * @return string|null
     */
    private function onEvent($event)
    {
        if (isset($this->_options[$event])) {
            $onEvent = strtoupper($this->_options[$event]);

            if (! in_array($onEvent, ['NO ACTION', 'RESTRICT'])) {
                return $onEvent;
            }
        }

        return null;
    }

    /**
     * Checks whether this foreign key constraint intersects the given index columns.
     *
     * Returns `true` if at least one of this foreign key's local columns
     * matches one of the given index's columns, `false` otherwise.
     *
     * @param Index $index The index to be checked against.
     *
     * @return bool
     */
    public function intersectsIndexColumns(Index $index)
    {
        foreach ($index->getColumns() as $indexColumn) {
            foreach ($this->_localColumnNames as $localColumn) {
                if (strtolower($indexColumn) === strtolower($localColumn->getName())) {
                    return true;
                }
            }
        }

        return false;
    }
}
<?php

namespace Doctrine\DBAL\Schema;

use Doctrine\DBAL\Platforms\MariaDb1027Platform;
use Doctrine\DBAL\Platforms\MySqlPlatform;
use Doctrine\DBAL\Types\Type;

use function array_change_key_case;
use function array_shift;
use function assert;
use function explode;
use function is_string;
use function preg_match;
use function strpos;
use function strtok;
use function strtolower;
use function strtr;

use const CASE_LOWER;

/**
 * Schema manager for the MySql RDBMS.
 */
class MySqlSchemaManager extends AbstractSchemaManager
{
    /**
     * @see https://mariadb.com/kb/en/library/string-literals/#escape-sequences
     */
    private const MARIADB_ESCAPE_SEQUENCES = [
        '\\0' => "\0",
        "\\'" => "'",
        '\\"' => '"',
        '\\b' => "\b",
        '\\n' => "\n",
        '\\r' => "\r",
        '\\t' => "\t",
        '\\Z' => "\x1a",
        '\\\\' => '\\',
        '\\%' => '%',
        '\\_' => '_',

        // Internally, MariaDB escapes single quotes using the standard syntax
        "''" => "'",
    ];

    /**
     * {@inheritdoc}
     */
    protected function _getPortableViewDefinition($view)
    {
        return new View($view['TABLE_NAME'], $view['VIEW_DEFINITION']);
    }

    /**
     * {@inheritdoc}
     */
    protected function _getPortableTableDefinition($table)
    {
        return array_shift($table);
    }

    /**
     * {@inheritdoc}
     */
    protected function _getPortableUserDefinition($user)
    {
        return [
            'user' => $user['User'],
            'password' => $user['Password'],
        ];
    }

    /**
     * {@inheritdoc}
     */
    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;
            }

            if (strpos($v['index_type'], 'FULLTEXT') !== false) {
                $v['flags'] = ['FULLTEXT'];
            } elseif (strpos($v['index_type'], 'SPATIAL') !== false) {
                $v['flags'] = ['SPATIAL'];
            }

            // Ignore prohibited prefix `length` for spatial index
            if (strpos($v['index_type'], 'SPATIAL') === false) {
                $v['length'] = isset($v['sub_part']) ? (int) $v['sub_part'] : null;
            }

            $tableIndexes[$k] = $v;
        }

        return parent::_getPortableTableIndexesList($tableIndexes, $tableName);
    }

    /**
     * {@inheritdoc}
     */
    protected function _getPortableDatabaseDefinition($database)
    {
        return $database['Database'];
    }

    /**
     * {@inheritdoc}
     */
    protected function _getPortableTableColumnDefinition($tableColumn)
    {
        $tableColumn = array_change_key_case($tableColumn, CASE_LOWER);

        $dbType = strtolower($tableColumn['type']);
        $dbType = strtok($dbType, '(), ');
        assert(is_string($dbType));

        $length = $tableColumn['length'] ?? strtok('(), ');

        $fixed = null;

        if (! isset($tableColumn['name'])) {
            $tableColumn['name'] = '';
        }

        $scale     = null;
        $precision = null;

        $type = $this->_platform->getDoctrineTypeMapping($dbType);

        // In cases where not connected to a database DESCRIBE $table does not return 'Comment'
        if (isset($tableColumn['comment'])) {
            $type                   = $this->extractDoctrineTypeFromComment($tableColumn['comment'], $type);
            $tableColumn['comment'] = $this->removeDoctrineTypeFromComment($tableColumn['comment'], $type);
        }

        switch ($dbType) {
            case 'char':
            case 'binary':
                $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 'tinytext':
                $length = MySqlPlatform::LENGTH_LIMIT_TINYTEXT;
                break;

            case 'text':
                $length = MySqlPlatform::LENGTH_LIMIT_TEXT;
                break;

            case 'mediumtext':
                $length = MySqlPlatform::LENGTH_LIMIT_MEDIUMTEXT;
                break;

            case 'tinyblob':
                $length = MySqlPlatform::LENGTH_LIMIT_TINYBLOB;
                break;

            case 'blob':
                $length = MySqlPlatform::LENGTH_LIMIT_BLOB;
                break;

            case 'mediumblob':
                $length = MySqlPlatform::LENGTH_LIMIT_MEDIUMBLOB;
                break;

            case 'tinyint':
            case 'smallint':
            case 'mediumint':
            case 'int':
            case 'integer':
            case 'bigint':
            case 'year':
                $length = null;
                break;
        }

        if ($this->_platform instanceof MariaDb1027Platform) {
            $columnDefault = $this->getMariaDb1027ColumnDefault($this->_platform, $tableColumn['default']);
        } else {
            $columnDefault = $tableColumn['default'];
        }

        $options = [
            'length'        => $length !== null ? (int) $length : null,
            'unsigned'      => strpos($tableColumn['type'], 'unsigned') !== false,
            'fixed'         => (bool) $fixed,
            'default'       => $columnDefault,
            'notnull'       => $tableColumn['null'] !== 'YES',
            'scale'         => null,
            'precision'     => null,
            'autoincrement' => strpos($tableColumn['extra'], 'auto_increment') !== false,
            'comment'       => isset($tableColumn['comment']) && $tableColumn['comment'] !== ''
                ? $tableColumn['comment']
                : null,
        ];

        if ($scale !== null && $precision !== null) {
            $options['scale']     = (int) $scale;
            $options['precision'] = (int) $precision;
        }

        $column = new Column($tableColumn['field'], Type::getType($type), $options);

        if (isset($tableColumn['characterset'])) {
            $column->setPlatformOption('charset', $tableColumn['characterset']);
        }

        if (isset($tableColumn['collation'])) {
            $column->setPlatformOption('collation', $tableColumn['collation']);
        }

        return $column;
    }

    /**
     * Return Doctrine/Mysql-compatible column default values for MariaDB 10.2.7+ servers.
     *
     * - Since MariaDb 10.2.7 column defaults stored in information_schema are now quoted
     *   to distinguish them from expressions (see MDEV-10134).
     * - CURRENT_TIMESTAMP, CURRENT_TIME, CURRENT_DATE are stored in information_schema
     *   as current_timestamp(), currdate(), currtime()
     * - Quoted 'NULL' is not enforced by Maria, it is technically possible to have
     *   null in some circumstances (see https://jira.mariadb.org/browse/MDEV-14053)
     * - \' is always stored as '' in information_schema (normalized)
     *
     * @link https://mariadb.com/kb/en/library/information-schema-columns-table/
     * @link https://jira.mariadb.org/browse/MDEV-13132
     *
     * @param string|null $columnDefault default value as stored in information_schema for MariaDB >= 10.2.7
     */
    private function getMariaDb1027ColumnDefault(MariaDb1027Platform $platform, ?string $columnDefault): ?string
    {
        if ($columnDefault === 'NULL' || $columnDefault === null) {
            return null;
        }

        if (preg_match('/^\'(.*)\'$/', $columnDefault, $matches)) {
            return strtr($matches[1], self::MARIADB_ESCAPE_SEQUENCES);
        }

        switch ($columnDefault) {
            case 'current_timestamp()':
                return $platform->getCurrentTimestampSQL();

            case 'curdate()':
                return $platform->getCurrentDateSQL();

            case 'curtime()':
                return $platform->getCurrentTimeSQL();
        }

        return $columnDefault;
    }

    /**
     * {@inheritdoc}
     */
    protected function _getPortableTableForeignKeysList($tableForeignKeys)
    {
        $list = [];
        foreach ($tableForeignKeys as $value) {
            $value = array_change_key_case($value, CASE_LOWER);
            if (! isset($list[$value['constraint_name']])) {
                if (! isset($value['delete_rule']) || $value['delete_rule'] === 'RESTRICT') {
                    $value['delete_rule'] = null;
                }

                if (! isset($value['update_rule']) || $value['update_rule'] === 'RESTRICT') {
                    $value['update_rule'] = null;
                }

                $list[$value['constraint_name']] = [
                    'name' => $value['constraint_name'],
                    'local' => [],
                    'foreign' => [],
                    'foreignTable' => $value['referenced_table_name'],
                    'onDelete' => $value['delete_rule'],
                    'onUpdate' => $value['update_rule'],
                ];
            }

            $list[$value['constraint_name']]['local'][]   = $value['column_name'];
            $list[$value['constraint_name']]['foreign'][] = $value['referenced_column_name'];
        }

        $result = [];
        foreach ($list as $constraint) {
            $result[] = new ForeignKeyConstraint(
                $constraint['local'],
                $constraint['foreignTable'],
                $constraint['foreign'],
                $constraint['name'],
                [
                    'onDelete' => $constraint['onDelete'],
                    'onUpdate' => $constraint['onUpdate'],
                ]
            );
        }

        return $result;
    }

    /**
     * {@inheritdoc}
     */
    public function listTableDetails($name)
    {
        $table = parent::listTableDetails($name);

        $platform = $this->_platform;
        assert($platform instanceof MySqlPlatform);
        $sql = $platform->getListTableMetadataSQL($name);

        $tableOptions = $this->_conn->fetchAssociative($sql);

        if ($tableOptions === false) {
            return $table;
        }

        $table->addOption('engine', $tableOptions['ENGINE']);

        if ($tableOptions['TABLE_COLLATION'] !== null) {
            $table->addOption('collation', $tableOptions['TABLE_COLLATION']);
        }

        if ($tableOptions['AUTO_INCREMENT'] !== null) {
            $table->addOption('autoincrement', $tableOptions['AUTO_INCREMENT']);
        }

        $table->addOption('comment', $tableOptions['TABLE_COMMENT']);
        $table->addOption('create_options', $this->parseCreateOptions($tableOptions['CREATE_OPTIONS']));

        return $table;
    }

    /**
     * @return string[]|true[]
     */
    private function parseCreateOptions(?string $string): array
    {
        $options = [];

        if ($string === null || $string === '') {
            return $options;
        }

        foreach (explode(' ', $string) as $pair) {
            $parts = explode('=', $pair, 2);

            $options[$parts[0]] = $parts[1] ?? true;
        }

        return $options;
    }
}
<?php

namespace Doctrine\DBAL\Schema;

use Doctrine\DBAL\Exception;

use function implode;
use function sprintf;

/**
 * @psalm-immutable
 */
class SchemaException extends Exception
{
    public const TABLE_DOESNT_EXIST       = 10;
    public const TABLE_ALREADY_EXISTS     = 20;
    public const COLUMN_DOESNT_EXIST      = 30;
    public const COLUMN_ALREADY_EXISTS    = 40;
    public const INDEX_DOESNT_EXIST       = 50;
    public const INDEX_ALREADY_EXISTS     = 60;
    public const SEQUENCE_DOENST_EXIST    = 70;
    public const SEQUENCE_ALREADY_EXISTS  = 80;
    public const INDEX_INVALID_NAME       = 90;
    public const FOREIGNKEY_DOESNT_EXIST  = 100;
    public const NAMESPACE_ALREADY_EXISTS = 110;

    /**
     * @param string $tableName
     *
     * @return SchemaException
     */
    public static function tableDoesNotExist($tableName)
    {
        return new self("There is no table with name '" . $tableName . "' in the schema.", self::TABLE_DOESNT_EXIST);
    }

    /**
     * @param string $indexName
     *
     * @return SchemaException
     */
    public static function indexNameInvalid($indexName)
    {
        return new self(
            sprintf('Invalid index-name %s given, has to be [a-zA-Z0-9_]', $indexName),
            self::INDEX_INVALID_NAME
        );
    }

    /**
     * @param string $indexName
     * @param string $table
     *
     * @return SchemaException
     */
    public static function indexDoesNotExist($indexName, $table)
    {
        return new self(
            sprintf("Index '%s' does not exist on table '%s'.", $indexName, $table),
            self::INDEX_DOESNT_EXIST
        );
    }

    /**
     * @param string $indexName
     * @param string $table
     *
     * @return SchemaException
     */
    public static function indexAlreadyExists($indexName, $table)
    {
        return new self(
            sprintf("An index with name '%s' was already defined on table '%s'.", $indexName, $table),
            self::INDEX_ALREADY_EXISTS
        );
    }

    /**
     * @param string $columnName
     * @param string $table
     *
     * @return SchemaException
     */
    public static function columnDoesNotExist($columnName, $table)
    {
        return new self(
            sprintf("There is no column with name '%s' on table '%s'.", $columnName, $table),
            self::COLUMN_DOESNT_EXIST
        );
    }

    /**
     * @param string $namespaceName
     *
     * @return SchemaException
     */
    public static function namespaceAlreadyExists($namespaceName)
    {
        return new self(
            sprintf("The namespace with name '%s' already exists.", $namespaceName),
            self::NAMESPACE_ALREADY_EXISTS
        );
    }

    /**
     * @param string $tableName
     *
     * @return SchemaException
     */
    public static function tableAlreadyExists($tableName)
    {
        return new self("The table with name '" . $tableName . "' already exists.", self::TABLE_ALREADY_EXISTS);
    }

    /**
     * @param string $tableName
     * @param string $columnName
     *
     * @return SchemaException
     */
    public static function columnAlreadyExists($tableName, $columnName)
    {
        return new self(
            "The column '" . $columnName . "' on table '" . $tableName . "' already exists.",
            self::COLUMN_ALREADY_EXISTS
        );
    }

    /**
     * @param string $name
     *
     * @return SchemaException
     */
    public static function sequenceAlreadyExists($name)
    {
        return new self("The sequence '" . $name . "' already exists.", self::SEQUENCE_ALREADY_EXISTS);
    }

    /**
     * @param string $name
     *
     * @return SchemaException
     */
    public static function sequenceDoesNotExist($name)
    {
        return new self("There exists no sequence with the name '" . $name . "'.", self::SEQUENCE_DOENST_EXIST);
    }

    /**
     * @param string $fkName
     * @param string $table
     *
     * @return SchemaException
     */
    public static function foreignKeyDoesNotExist($fkName, $table)
    {
        return new self(
            sprintf("There exists no foreign key with the name '%s' on table '%s'.", $fkName, $table),
            self::FOREIGNKEY_DOESNT_EXIST
        );
    }

    /**
     * @return SchemaException
     */
    public static function namedForeignKeyRequired(Table $localTable, ForeignKeyConstraint $foreignKey)
    {
        return new self(
            'The performed schema operation on ' . $localTable->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.'
        );
    }

    /**
     * @param string $changeName
     *
     * @return SchemaException
     */
    public static function alterTableChangeNotSupported($changeName)
    {
        return new self(
            sprintf("Alter table change not supported, given '%s'", $changeName)
        );
    }
}
<?php

namespace Doctrine\DBAL\Schema;

use Doctrine\DBAL\Exception\DriverException;
use Doctrine\DBAL\FetchMode;
use Doctrine\DBAL\Platforms\PostgreSqlPlatform;
use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Types\Types;

use function array_change_key_case;
use function array_filter;
use function array_keys;
use function array_map;
use function array_shift;
use function assert;
use function explode;
use function implode;
use function in_array;
use function preg_match;
use function preg_replace;
use function sprintf;
use function str_replace;
use function strpos;
use function strtolower;
use function trim;

use const CASE_LOWER;

/**
 * PostgreSQL Schema Manager.
 */
class PostgreSqlSchemaManager extends AbstractSchemaManager
{
    /** @var string[]|null */
    private $existingSchemaPaths;

    /**
     * Gets all the existing schema names.
     *
     * @return string[]
     */
    public function getSchemaNames()
    {
        $statement = $this->_conn->executeQuery(
            "SELECT nspname FROM pg_namespace WHERE nspname !~ '^pg_.*' AND nspname != 'information_schema'"
        );

        return $statement->fetchAll(FetchMode::COLUMN);
    }

    /**
     * Returns an array of schema search paths.
     *
     * This is a PostgreSQL only function.
     *
     * @return string[]
     */
    public function getSchemaSearchPaths()
    {
        $params = $this->_conn->getParams();

        $searchPaths = $this->_conn->fetchColumn('SHOW search_path');
        assert($searchPaths !== false);

        $schema = explode(',', $searchPaths);

        if (isset($params['user'])) {
            $schema = str_replace('"$user"', $params['user'], $schema);
        }

        return array_map('trim', $schema);
    }

    /**
     * Gets names of all existing schemas in the current users search path.
     *
     * This is a PostgreSQL only function.
     *
     * @return string[]
     */
    public function getExistingSchemaSearchPaths()
    {
        if ($this->existingSchemaPaths === null) {
            $this->determineExistingSchemaSearchPaths();
        }

        assert($this->existingSchemaPaths !== null);

        return $this->existingSchemaPaths;
    }

    /**
     * Sets or resets the order of the existing schemas in the current search path of the user.
     *
     * This is a PostgreSQL only function.
     *
     * @return void
     */
    public function determineExistingSchemaSearchPaths()
    {
        $names = $this->getSchemaNames();
        $paths = $this->getSchemaSearchPaths();

        $this->existingSchemaPaths = array_filter($paths, static function ($v) use ($names) {
            return in_array($v, $names);
        });
    }

    /**
     * {@inheritdoc}
     */
    public function dropDatabase($database)
    {
        try {
            parent::dropDatabase($database);
        } catch (DriverException $exception) {
            // If we have a SQLSTATE 55006, the drop database operation failed
            // because of active connections on the database.
            // To force dropping the database, we first have to close all active connections
            // on that database and issue the drop database operation again.
            if ($exception->getSQLState() !== '55006') {
                throw $exception;
            }

            assert($this->_platform instanceof PostgreSqlPlatform);

            $this->_execSql(
                [
                    $this->_platform->getDisallowDatabaseConnectionsSQL($database),
                    $this->_platform->getCloseActiveDatabaseConnectionsSQL($database),
                ]
            );

            parent::dropDatabase($database);
        }
    }

    /**
     * {@inheritdoc}
     */
    protected function _getPortableTableForeignKeyDefinition($tableForeignKey)
    {
        $onUpdate       = null;
        $onDelete       = null;
        $localColumns   = [];
        $foreignColumns = [];
        $foreignTable   = null;

        if (preg_match('(ON UPDATE ([a-zA-Z0-9]+( (NULL|ACTION|DEFAULT))?))', $tableForeignKey['condef'], $match)) {
            $onUpdate = $match[1];
        }

        if (preg_match('(ON DELETE ([a-zA-Z0-9]+( (NULL|ACTION|DEFAULT))?))', $tableForeignKey['condef'], $match)) {
            $onDelete = $match[1];
        }

        $result = preg_match('/FOREIGN KEY \((.+)\) REFERENCES (.+)\((.+)\)/', $tableForeignKey['condef'], $values);
        assert($result === 1);

        // PostgreSQL returns identifiers that are keywords with quotes, we need them later, don't get
        // the idea to trim them here.
        $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'],
            ['onUpdate' => $onUpdate, 'onDelete' => $onDelete]
        );
    }

    /**
     * {@inheritdoc}
     */
    protected function _getPortableTriggerDefinition($trigger)
    {
        return $trigger['trigger_name'];
    }

    /**
     * {@inheritdoc}
     */
    protected function _getPortableViewDefinition($view)
    {
        return new View($view['schemaname'] . '.' . $view['viewname'], $view['definition']);
    }

    /**
     * {@inheritdoc}
     */
    protected function _getPortableUserDefinition($user)
    {
        return [
            'user' => $user['usename'],
            'password' => $user['passwd'],
        ];
    }

    /**
     * {@inheritdoc}
     */
    protected function _getPortableTableDefinition($table)
    {
        $schemas     = $this->getExistingSchemaSearchPaths();
        $firstSchema = array_shift($schemas);

        if ($table['schema_name'] === $firstSchema) {
            return $table['table_name'];
        }

        return $table['schema_name'] . '.' . $table['table_name'];
    }

    /**
     * {@inheritdoc}
     *
     * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html
     */
    protected function _getPortableTableIndexesList($tableIndexes, $tableName = null)
    {
        $buffer = [];
        foreach ($tableIndexes as $row) {
            $colNumbers    = array_map('intval', explode(' ', $row['indkey']));
            $columnNameSql = sprintf(
                'SELECT attnum, attname FROM pg_attribute WHERE attrelid=%d AND attnum IN (%s) ORDER BY attnum ASC',
                $row['indrelid'],
                implode(' ,', $colNumbers)
            );

            $indexColumns = $this->_conn->fetchAllAssociative($columnNameSql);

            // required for getting the order of the columns right.
            foreach ($colNumbers as $colNum) {
                foreach ($indexColumns as $colRow) {
                    if ($colNum !== $colRow['attnum']) {
                        continue;
                    }

                    $buffer[] = [
                        'key_name' => $row['relname'],
                        'column_name' => trim($colRow['attname']),
                        'non_unique' => ! $row['indisunique'],
                        'primary' => $row['indisprimary'],
                        'where' => $row['where'],
                    ];
                }
            }
        }

        return parent::_getPortableTableIndexesList($buffer, $tableName);
    }

    /**
     * {@inheritdoc}
     */
    protected function _getPortableDatabaseDefinition($database)
    {
        return $database['datname'];
    }

    /**
     * {@inheritdoc}
     */
    protected function _getPortableSequencesList($sequences)
    {
        $sequenceDefinitions = [];

        foreach ($sequences as $sequence) {
            if ($sequence['schemaname'] !== 'public') {
                $sequenceName = $sequence['schemaname'] . '.' . $sequence['relname'];
            } else {
                $sequenceName = $sequence['relname'];
            }

            $sequenceDefinitions[$sequenceName] = $sequence;
        }

        $list = [];

        foreach ($this->filterAssetNames(array_keys($sequenceDefinitions)) as $sequenceName) {
            $list[] = $this->_getPortableSequenceDefinition($sequenceDefinitions[$sequenceName]);
        }

        return $list;
    }

    /**
     * {@inheritdoc}
     */
    protected function getPortableNamespaceDefinition(array $namespace)
    {
        return $namespace['nspname'];
    }

    /**
     * {@inheritdoc}
     */
    protected function _getPortableSequenceDefinition($sequence)
    {
        if ($sequence['schemaname'] !== 'public') {
            $sequenceName = $sequence['schemaname'] . '.' . $sequence['relname'];
        } else {
            $sequenceName = $sequence['relname'];
        }

        if (! isset($sequence['increment_by'], $sequence['min_value'])) {
            /** @var string[] $data */
            $data = $this->_conn->fetchAssoc(
                'SELECT min_value, increment_by FROM ' . $this->_platform->quoteIdentifier($sequenceName)
            );

            $sequence += $data;
        }

        return new Sequence($sequenceName, (int) $sequence['increment_by'], (int) $sequence['min_value']);
    }

    /**
     * {@inheritdoc}
     */
    protected function _getPortableTableColumnDefinition($tableColumn)
    {
        $tableColumn = array_change_key_case($tableColumn, CASE_LOWER);

        if (strtolower($tableColumn['type']) === 'varchar' || strtolower($tableColumn['type']) === 'bpchar') {
            // get length from varchar definition
            $length                = preg_replace('~.*\(([0-9]*)\).*~', '$1', $tableColumn['complete_type']);
            $tableColumn['length'] = $length;
        }

        $matches = [];

        $autoincrement = false;

        if (
            $tableColumn['default'] !== null
            && preg_match("/^nextval\('(.*)'(::.*)?\)$/", $tableColumn['default'], $matches) === 1
        ) {
            $tableColumn['sequence'] = $matches[1];
            $tableColumn['default']  = null;
            $autoincrement           = true;
        }

        if ($tableColumn['default'] !== null) {
            if (preg_match("/^['(](.*)[')]::/", $tableColumn['default'], $matches) === 1) {
                $tableColumn['default'] = $matches[1];
            } elseif (preg_match('/^NULL::/', $tableColumn['default']) === 1) {
                $tableColumn['default'] = null;
            }
        }

        $length = $tableColumn['length'] ?? null;
        if ($length === '-1' && isset($tableColumn['atttypmod'])) {
            $length = $tableColumn['atttypmod'] - 4;
        }

        if ((int) $length <= 0) {
            $length = null;
        }

        $fixed = null;

        if (! isset($tableColumn['name'])) {
            $tableColumn['name'] = '';
        }

        $precision = null;
        $scale     = null;
        $jsonb     = null;

        $dbType = strtolower($tableColumn['type']);
        if (
            $tableColumn['domain_type'] !== null
            && $tableColumn['domain_type'] !== ''
            && ! $this->_platform->hasDoctrineTypeMappingFor($tableColumn['type'])
        ) {
            $dbType                       = strtolower($tableColumn['domain_type']);
            $tableColumn['complete_type'] = $tableColumn['domain_complete_type'];
        }

        $type                   = $this->_platform->getDoctrineTypeMapping($dbType);
        $type                   = $this->extractDoctrineTypeFromComment($tableColumn['comment'], $type);
        $tableColumn['comment'] = $this->removeDoctrineTypeFromComment($tableColumn['comment'], $type);

        switch ($dbType) {
            case 'smallint':
            case 'int2':
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
                $length                 = null;
                break;

            case 'int':
            case 'int4':
            case 'integer':
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
                $length                 = null;
                break;

            case 'bigint':
            case 'int8':
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
                $length                 = null;
                break;

            case 'bool':
            case 'boolean':
                if ($tableColumn['default'] === 'true') {
                    $tableColumn['default'] = true;
                }

                if ($tableColumn['default'] === 'false') {
                    $tableColumn['default'] = false;
                }

                $length = null;
                break;

            case 'text':
            case '_varchar':
            case 'varchar':
                $tableColumn['default'] = $this->parseDefaultExpression($tableColumn['default']);
                $fixed                  = false;
                break;
            case 'interval':
                $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':
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);

                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;

            // PostgreSQL 9.4+ only
            case 'jsonb':
                $jsonb = true;
                break;
        }

        if ($tableColumn['default'] && preg_match("('([^']+)'::)", $tableColumn['default'], $match)) {
            $tableColumn['default'] = $match[1];
        }

        $options = [
            'length'        => $length,
            'notnull'       => (bool) $tableColumn['isnotnull'],
            'default'       => $tableColumn['default'],
            'precision'     => $precision,
            'scale'         => $scale,
            'fixed'         => $fixed,
            'unsigned'      => false,
            'autoincrement' => $autoincrement,
            'comment'       => isset($tableColumn['comment']) && $tableColumn['comment'] !== ''
                ? $tableColumn['comment']
                : null,
        ];

        $column = new Column($tableColumn['field'], Type::getType($type), $options);

        if (isset($tableColumn['collation']) && ! empty($tableColumn['collation'])) {
            $column->setPlatformOption('collation', $tableColumn['collation']);
        }

        if (in_array($column->getType()->getName(), [Types::JSON_ARRAY, Types::JSON], true)) {
            $column->setPlatformOption('jsonb', $jsonb);
        }

        return $column;
    }

    /**
     * PostgreSQL 9.4 puts parentheses around negative numeric default values that need to be stripped eventually.
     *
     * @param mixed $defaultValue
     *
     * @return mixed
     */
    private function fixVersion94NegativeNumericDefaultValue($defaultValue)
    {
        if ($defaultValue !== null && strpos($defaultValue, '(') === 0) {
            return trim($defaultValue, '()');
        }

        return $defaultValue;
    }

    /**
     * Parses a default value expression as given by PostgreSQL
     */
    private function parseDefaultExpression(?string $default): ?string
    {
        if ($default === null) {
            return $default;
        }

        return str_replace("''", "'", $default);
    }

    /**
     * {@inheritdoc}
     */
    public function listTableDetails($name): Table
    {
        $table = parent::listTableDetails($name);

        $platform = $this->_platform;
        assert($platform instanceof PostgreSqlPlatform);
        $sql = $platform->getListTableMetadataSQL($name);

        $tableOptions = $this->_conn->fetchAssoc($sql);

        if ($tableOptions !== false) {
            $table->addOption('comment', $tableOptions['table_comment']);
        }

        return $table;
    }
}
<?php

namespace Doctrine\DBAL\Schema;

use Doctrine\DBAL\Schema\Visitor\Visitor;

use function count;
use function sprintf;

/**
 * Sequence structure.
 */
class Sequence extends AbstractAsset
{
    /** @var int */
    protected $allocationSize = 1;

    /** @var int */
    protected $initialValue = 1;

    /** @var int|null */
    protected $cache;

    /**
     * @param string   $name
     * @param int      $allocationSize
     * @param int      $initialValue
     * @param int|null $cache
     */
    public function __construct($name, $allocationSize = 1, $initialValue = 1, $cache = null)
    {
        $this->_setName($name);
        $this->setAllocationSize($allocationSize);
        $this->setInitialValue($initialValue);
        $this->cache = $cache;
    }

    /**
     * @return int
     */
    public function getAllocationSize()
    {
        return $this->allocationSize;
    }

    /**
     * @return int
     */
    public function getInitialValue()
    {
        return $this->initialValue;
    }

    /**
     * @return int|null
     */
    public function getCache()
    {
        return $this->cache;
    }

    /**
     * @param int $allocationSize
     *
     * @return Sequence
     */
    public function setAllocationSize($allocationSize)
    {
        $this->allocationSize = (int) $allocationSize ?: 1;

        return $this;
    }

    /**
     * @param int $initialValue
     *
     * @return Sequence
     */
    public function setInitialValue($initialValue)
    {
        $this->initialValue = (int) $initialValue ?: 1;

        return $this;
    }

    /**
     * @param int $cache
     *
     * @return Sequence
     */
    public function setCache($cache)
    {
        $this->cache = $cache;

        return $this;
    }

    /**
     * Checks if this sequence is an autoincrement sequence for a given table.
     *
     * This is used inside the comparator to not report sequences as missing,
     * when the "from" schema implicitly creates the sequences.
     *
     * @return bool
     */
    public function isAutoIncrementsFor(Table $table)
    {
        $primaryKey = $table->getPrimaryKey();

        if ($primaryKey === null) {
            return false;
        }

        $pkColumns = $primaryKey->getColumns();

        if (count($pkColumns) !== 1) {
            return false;
        }

        $column = $table->getColumn($pkColumns[0]);

        if (! $column->getAutoincrement()) {
            return false;
        }

        $sequenceName      = $this->getShortestName($table->getNamespaceName());
        $tableName         = $table->getShortestName($table->getNamespaceName());
        $tableSequenceName = sprintf('%s_%s_seq', $tableName, $column->getShortestName($table->getNamespaceName()));

        return $tableSequenceName === $sequenceName;
    }

    /**
     * @return void
     */
    public function visit(Visitor $visitor)
    {
        $visitor->acceptSequence($this);
    }
}
<?php

namespace Doctrine\DBAL\Schema;

use Doctrine\DBAL\Platforms\AbstractPlatform;

/**
 * Marker interface for constraints.
 */
interface Constraint
{
    /**
     * @return string
     */
    public function getName();

    /**
     * @return string
     */
    public function getQuotedName(AbstractPlatform $platform);

    /**
     * Returns the names of the referencing table columns
     * the constraint is associated with.
     *
     * @return string[]
     */
    public function getColumns();

    /**
     * Returns the quoted representation of the column names
     * the constraint is associated with.
     *
     * But only if they were defined with one or a column name
     * is a keyword reserved by the platform.
     * Otherwise the plain unquoted value as inserted is returned.
     *
     * @param AbstractPlatform $platform The platform to use for quotation.
     *
     * @return string[]
     */
    public function getQuotedColumns(AbstractPlatform $platform);
}
<?php

namespace Doctrine\DBAL\Schema;

use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Schema\Visitor\Visitor;
use Doctrine\DBAL\Types\Type;

use function array_filter;
use function array_merge;
use function in_array;
use function preg_match;
use function strlen;
use function strtolower;

use const ARRAY_FILTER_USE_KEY;

/**
 * Object Representation of a table.
 */
class Table extends AbstractAsset
{
    /** @var Column[] */
    protected $_columns = [];

    /** @var Index[] */
    private $implicitIndexes = [];

    /** @var Index[] */
    protected $_indexes = [];

    /** @var string|false */
    protected $_primaryKeyName = false;

    /** @var ForeignKeyConstraint[] */
    protected $_fkConstraints = [];

    /** @var mixed[] */
    protected $_options = [
        'create_options' => [],
    ];

    /** @var SchemaConfig|null */
    protected $_schemaConfig;

    /**
     * @param string                 $name
     * @param Column[]               $columns
     * @param Index[]                $indexes
     * @param ForeignKeyConstraint[] $fkConstraints
     * @param int                    $idGeneratorType
     * @param mixed[]                $options
     *
     * @throws Exception
     */
    public function __construct(
        $name,
        array $columns = [],
        array $indexes = [],
        array $fkConstraints = [],
        $idGeneratorType = 0,
        array $options = []
    ) {
        if (strlen($name) === 0) {
            throw Exception::invalidTableName($name);
        }

        $this->_setName($name);

        foreach ($columns as $column) {
            $this->_addColumn($column);
        }

        foreach ($indexes as $idx) {
            $this->_addIndex($idx);
        }

        foreach ($fkConstraints as $constraint) {
            $this->_addForeignKeyConstraint($constraint);
        }

        $this->_options = array_merge($this->_options, $options);
    }

    /**
     * @return void
     */
    public function setSchemaConfig(SchemaConfig $schemaConfig)
    {
        $this->_schemaConfig = $schemaConfig;
    }

    /**
     * @return int
     */
    protected function _getMaxIdentifierLength()
    {
        if ($this->_schemaConfig instanceof SchemaConfig) {
            return $this->_schemaConfig->getMaxIdentifierLength();
        }

        return 63;
    }

    /**
     * Sets the Primary Key.
     *
     * @param string[]     $columnNames
     * @param string|false $indexName
     *
     * @return self
     */
    public function setPrimaryKey(array $columnNames, $indexName = false)
    {
        $this->_addIndex($this->_createIndex($columnNames, $indexName ?: 'primary', true, true));

        foreach ($columnNames as $columnName) {
            $column = $this->getColumn($columnName);
            $column->setNotnull(true);
        }

        return $this;
    }

    /**
     * @param string[]    $columnNames
     * @param string|null $indexName
     * @param string[]    $flags
     * @param mixed[]     $options
     *
     * @return self
     */
    public function addIndex(array $columnNames, $indexName = null, array $flags = [], array $options = [])
    {
        if ($indexName === null) {
            $indexName = $this->_generateIdentifierName(
                array_merge([$this->getName()], $columnNames),
                'idx',
                $this->_getMaxIdentifierLength()
            );
        }

        return $this->_addIndex($this->_createIndex($columnNames, $indexName, false, false, $flags, $options));
    }

    /**
     * Drops the primary key from this table.
     *
     * @return void
     */
    public function dropPrimaryKey()
    {
        if ($this->_primaryKeyName === false) {
            return;
        }

        $this->dropIndex($this->_primaryKeyName);
        $this->_primaryKeyName = false;
    }

    /**
     * Drops an index from this table.
     *
     * @param string $name The index name.
     *
     * @return void
     *
     * @throws SchemaException If the index does not exist.
     */
    public function dropIndex($name)
    {
        $name = $this->normalizeIdentifier($name);
        if (! $this->hasIndex($name)) {
            throw SchemaException::indexDoesNotExist($name, $this->_name);
        }

        unset($this->_indexes[$name]);
    }

    /**
     * @param string[]    $columnNames
     * @param string|null $indexName
     * @param mixed[]     $options
     *
     * @return self
     */
    public function addUniqueIndex(array $columnNames, $indexName = null, array $options = [])
    {
        if ($indexName === null) {
            $indexName = $this->_generateIdentifierName(
                array_merge([$this->getName()], $columnNames),
                'uniq',
                $this->_getMaxIdentifierLength()
            );
        }

        return $this->_addIndex($this->_createIndex($columnNames, $indexName, true, false, [], $options));
    }

    /**
     * Renames an index.
     *
     * @param string      $oldName The name of the index to rename from.
     * @param string|null $newName The name of the index to rename to.
     *                                  If null is given, the index name will be auto-generated.
     *
     * @return self This table instance.
     *
     * @throws SchemaException If no index exists for the given current name
     *                         or if an index with the given new name already exists on this table.
     */
    public function renameIndex($oldName, $newName = null)
    {
        $oldName           = $this->normalizeIdentifier($oldName);
        $normalizedNewName = $this->normalizeIdentifier($newName);

        if ($oldName === $normalizedNewName) {
            return $this;
        }

        if (! $this->hasIndex($oldName)) {
            throw SchemaException::indexDoesNotExist($oldName, $this->_name);
        }

        if ($this->hasIndex($normalizedNewName)) {
            throw SchemaException::indexAlreadyExists($normalizedNewName, $this->_name);
        }

        $oldIndex = $this->_indexes[$oldName];

        if ($oldIndex->isPrimary()) {
            $this->dropPrimaryKey();

            return $this->setPrimaryKey($oldIndex->getColumns(), $newName ?? false);
        }

        unset($this->_indexes[$oldName]);

        if ($oldIndex->isUnique()) {
            return $this->addUniqueIndex($oldIndex->getColumns(), $newName, $oldIndex->getOptions());
        }

        return $this->addIndex($oldIndex->getColumns(), $newName, $oldIndex->getFlags(), $oldIndex->getOptions());
    }

    /**
     * Checks if an index begins in the order of the given columns.
     *
     * @param string[] $columnNames
     *
     * @return bool
     */
    public function columnsAreIndexed(array $columnNames)
    {
        foreach ($this->getIndexes() as $index) {
            if ($index->spansColumns($columnNames)) {
                return true;
            }
        }

        return false;
    }

    /**
     * @param string[] $columnNames
     * @param string   $indexName
     * @param bool     $isUnique
     * @param bool     $isPrimary
     * @param string[] $flags
     * @param mixed[]  $options
     *
     * @return Index
     *
     * @throws SchemaException
     */
    private function _createIndex(
        array $columnNames,
        $indexName,
        $isUnique,
        $isPrimary,
        array $flags = [],
        array $options = []
    ) {
        if (preg_match('(([^a-zA-Z0-9_]+))', $this->normalizeIdentifier($indexName))) {
            throw SchemaException::indexNameInvalid($indexName);
        }

        foreach ($columnNames as $columnName) {
            if (! $this->hasColumn($columnName)) {
                throw SchemaException::columnDoesNotExist($columnName, $this->_name);
            }
        }

        return new Index($indexName, $columnNames, $isUnique, $isPrimary, $flags, $options);
    }

    /**
     * @param string  $name
     * @param string  $typeName
     * @param mixed[] $options
     *
     * @return Column
     */
    public function addColumn($name, $typeName, array $options = [])
    {
        $column = new Column($name, Type::getType($typeName), $options);

        $this->_addColumn($column);

        return $column;
    }

    /**
     * Renames a Column.
     *
     * @deprecated
     *
     * @param string $oldName
     * @param string $name
     *
     * @return void
     *
     * @throws Exception
     */
    public function renameColumn($oldName, $name)
    {
        throw new Exception('Table#renameColumn() was removed, because it drops and recreates ' .
            'the column instead. There is no fix available, because a schema diff cannot reliably detect if a ' .
            'column was renamed or one column was created and another one dropped.');
    }

    /**
     * Change Column Details.
     *
     * @param string  $name
     * @param mixed[] $options
     *
     * @return self
     */
    public function changeColumn($name, array $options)
    {
        $column = $this->getColumn($name);
        $column->setOptions($options);

        return $this;
    }

    /**
     * Drops a Column from the Table.
     *
     * @param string $name
     *
     * @return self
     */
    public function dropColumn($name)
    {
        $name = $this->normalizeIdentifier($name);
        unset($this->_columns[$name]);

        return $this;
    }

    /**
     * Adds a foreign key constraint.
     *
     * Name is inferred from the local columns.
     *
     * @param Table|string $foreignTable       Table schema instance or table name
     * @param string[]     $localColumnNames
     * @param string[]     $foreignColumnNames
     * @param mixed[]      $options
     * @param string|null  $constraintName
     *
     * @return self
     */
    public function addForeignKeyConstraint(
        $foreignTable,
        array $localColumnNames,
        array $foreignColumnNames,
        array $options = [],
        $constraintName = null
    ) {
        $constraintName = $constraintName ?: $this->_generateIdentifierName(
            array_merge((array) $this->getName(), $localColumnNames),
            'fk',
            $this->_getMaxIdentifierLength()
        );

        return $this->addNamedForeignKeyConstraint(
            $constraintName,
            $foreignTable,
            $localColumnNames,
            $foreignColumnNames,
            $options
        );
    }

    /**
     * Adds a foreign key constraint.
     *
     * Name is to be generated by the database itself.
     *
     * @deprecated Use {@link addForeignKeyConstraint}
     *
     * @param Table|string $foreignTable       Table schema instance or table name
     * @param string[]     $localColumnNames
     * @param string[]     $foreignColumnNames
     * @param mixed[]      $options
     *
     * @return self
     */
    public function addUnnamedForeignKeyConstraint(
        $foreignTable,
        array $localColumnNames,
        array $foreignColumnNames,
        array $options = []
    ) {
        return $this->addForeignKeyConstraint($foreignTable, $localColumnNames, $foreignColumnNames, $options);
    }

    /**
     * Adds a foreign key constraint with a given name.
     *
     * @deprecated Use {@link addForeignKeyConstraint}
     *
     * @param string       $name
     * @param Table|string $foreignTable       Table schema instance or table name
     * @param string[]     $localColumnNames
     * @param string[]     $foreignColumnNames
     * @param mixed[]      $options
     *
     * @return self
     *
     * @throws SchemaException
     */
    public function addNamedForeignKeyConstraint(
        $name,
        $foreignTable,
        array $localColumnNames,
        array $foreignColumnNames,
        array $options = []
    ) {
        if ($foreignTable instanceof Table) {
            foreach ($foreignColumnNames as $columnName) {
                if (! $foreignTable->hasColumn($columnName)) {
                    throw SchemaException::columnDoesNotExist($columnName, $foreignTable->getName());
                }
            }
        }

        foreach ($localColumnNames as $columnName) {
            if (! $this->hasColumn($columnName)) {
                throw SchemaException::columnDoesNotExist($columnName, $this->_name);
            }
        }

        $constraint = new ForeignKeyConstraint(
            $localColumnNames,
            $foreignTable,
            $foreignColumnNames,
            $name,
            $options
        );
        $this->_addForeignKeyConstraint($constraint);

        return $this;
    }

    /**
     * @param string $name
     * @param mixed  $value
     *
     * @return self
     */
    public function addOption($name, $value)
    {
        $this->_options[$name] = $value;

        return $this;
    }

    /**
     * @return void
     *
     * @throws SchemaException
     */
    protected function _addColumn(Column $column)
    {
        $columnName = $column->getName();
        $columnName = $this->normalizeIdentifier($columnName);

        if (isset($this->_columns[$columnName])) {
            throw SchemaException::columnAlreadyExists($this->getName(), $columnName);
        }

        $this->_columns[$columnName] = $column;
    }

    /**
     * Adds an index to the table.
     *
     * @return self
     *
     * @throws SchemaException
     */
    protected function _addIndex(Index $indexCandidate)
    {
        $indexName               = $indexCandidate->getName();
        $indexName               = $this->normalizeIdentifier($indexName);
        $replacedImplicitIndexes = [];

        foreach ($this->implicitIndexes as $name => $implicitIndex) {
            if (! $implicitIndex->isFullfilledBy($indexCandidate) || ! isset($this->_indexes[$name])) {
                continue;
            }

            $replacedImplicitIndexes[] = $name;
        }

        if (
            (isset($this->_indexes[$indexName]) && ! in_array($indexName, $replacedImplicitIndexes, true)) ||
            ($this->_primaryKeyName !== false && $indexCandidate->isPrimary())
        ) {
            throw SchemaException::indexAlreadyExists($indexName, $this->_name);
        }

        foreach ($replacedImplicitIndexes as $name) {
            unset($this->_indexes[$name], $this->implicitIndexes[$name]);
        }

        if ($indexCandidate->isPrimary()) {
            $this->_primaryKeyName = $indexName;
        }

        $this->_indexes[$indexName] = $indexCandidate;

        return $this;
    }

    /**
     * @return void
     */
    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 = $this->normalizeIdentifier($name);

        $this->_fkConstraints[$name] = $constraint;

        /* Add an implicit index (defined by the DBAL) on the foreign key
           columns. If there is already a user-defined index that fulfills these
           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 computation overhead in
           this case, however no duplicate indexes are ever added (based on
           columns). */
        $indexName = $this->_generateIdentifierName(
            array_merge([$this->getName()], $constraint->getColumns()),
            'idx',
            $this->_getMaxIdentifierLength()
        );

        $indexCandidate = $this->_createIndex($constraint->getColumns(), $indexName, false, false);

        foreach ($this->_indexes as $existingIndex) {
            if ($indexCandidate->isFullfilledBy($existingIndex)) {
                return;
            }
        }

        $this->_addIndex($indexCandidate);
        $this->implicitIndexes[$this->normalizeIdentifier($indexName)] = $indexCandidate;
    }

    /**
     * Returns whether this table has a foreign key constraint with the given name.
     *
     * @param string $name
     *
     * @return bool
     */
    public function hasForeignKey($name)
    {
        $name = $this->normalizeIdentifier($name);

        return isset($this->_fkConstraints[$name]);
    }

    /**
     * Returns the foreign key constraint with the given name.
     *
     * @param string $name The constraint name.
     *
     * @return ForeignKeyConstraint
     *
     * @throws SchemaException If the foreign key does not exist.
     */
    public function getForeignKey($name)
    {
        $name = $this->normalizeIdentifier($name);
        if (! $this->hasForeignKey($name)) {
            throw SchemaException::foreignKeyDoesNotExist($name, $this->_name);
        }

        return $this->_fkConstraints[$name];
    }

    /**
     * Removes the foreign key constraint with the given name.
     *
     * @param string $name The constraint name.
     *
     * @return void
     *
     * @throws SchemaException
     */
    public function removeForeignKey($name)
    {
        $name = $this->normalizeIdentifier($name);
        if (! $this->hasForeignKey($name)) {
            throw SchemaException::foreignKeyDoesNotExist($name, $this->_name);
        }

        unset($this->_fkConstraints[$name]);
    }

    /**
     * Returns ordered list of columns (primary keys are first, then foreign keys, then the rest)
     *
     * @return Column[]
     */
    public function getColumns()
    {
        $primaryKey        = $this->getPrimaryKey();
        $primaryKeyColumns = [];

        if ($primaryKey !== null) {
            $primaryKeyColumns = $this->filterColumns($primaryKey->getColumns());
        }

        return array_merge($primaryKeyColumns, $this->getForeignKeyColumns(), $this->_columns);
    }

    /**
     * Returns foreign key columns
     *
     * @return Column[]
     */
    private function getForeignKeyColumns()
    {
        $foreignKeyColumns = [];
        foreach ($this->getForeignKeys() as $foreignKey) {
            $foreignKeyColumns = array_merge($foreignKeyColumns, $foreignKey->getColumns());
        }

        return $this->filterColumns($foreignKeyColumns);
    }

    /**
     * Returns only columns that have specified names
     *
     * @param string[] $columnNames
     *
     * @return Column[]
     */
    private function filterColumns(array $columnNames)
    {
        return array_filter($this->_columns, static function (string $columnName) use ($columnNames) {
            return in_array($columnName, $columnNames, true);
        }, ARRAY_FILTER_USE_KEY);
    }

    /**
     * Returns whether this table has a Column with the given name.
     *
     * @param string $name The column name.
     *
     * @return bool
     */
    public function hasColumn($name)
    {
        $name = $this->normalizeIdentifier($name);

        return isset($this->_columns[$name]);
    }

    /**
     * Returns the Column with the given name.
     *
     * @param string $name The column name.
     *
     * @return Column
     *
     * @throws SchemaException If the column does not exist.
     */
    public function getColumn($name)
    {
        $name = $this->normalizeIdentifier($name);
        if (! $this->hasColumn($name)) {
            throw SchemaException::columnDoesNotExist($name, $this->_name);
        }

        return $this->_columns[$name];
    }

    /**
     * Returns the primary key.
     *
     * @return Index|null The primary key, or null if this Table has no primary key.
     */
    public function getPrimaryKey()
    {
        if ($this->_primaryKeyName !== false) {
            return $this->getIndex($this->_primaryKeyName);
        }

        return null;
    }

    /**
     * Returns the primary key columns.
     *
     * @return string[]
     *
     * @throws Exception
     */
    public function getPrimaryKeyColumns()
    {
        $primaryKey = $this->getPrimaryKey();

        if ($primaryKey === null) {
            throw new Exception('Table ' . $this->getName() . ' has no primary key.');
        }

        return $primaryKey->getColumns();
    }

    /**
     * Returns whether this table has a primary key.
     *
     * @return bool
     */
    public function hasPrimaryKey()
    {
        return $this->_primaryKeyName && $this->hasIndex($this->_primaryKeyName);
    }

    /**
     * Returns whether this table has an Index with the given name.
     *
     * @param string $name The index name.
     *
     * @return bool
     */
    public function hasIndex($name)
    {
        $name = $this->normalizeIdentifier($name);

        return isset($this->_indexes[$name]);
    }

    /**
     * Returns the Index with the given name.
     *
     * @param string $name The index name.
     *
     * @return Index
     *
     * @throws SchemaException If the index does not exist.
     */
    public function getIndex($name)
    {
        $name = $this->normalizeIdentifier($name);
        if (! $this->hasIndex($name)) {
            throw SchemaException::indexDoesNotExist($name, $this->_name);
        }

        return $this->_indexes[$name];
    }

    /**
     * @return Index[]
     */
    public function getIndexes()
    {
        return $this->_indexes;
    }

    /**
     * Returns the foreign key constraints.
     *
     * @return ForeignKeyConstraint[]
     */
    public function getForeignKeys()
    {
        return $this->_fkConstraints;
    }

    /**
     * @param string $name
     *
     * @return bool
     */
    public function hasOption($name)
    {
        return isset($this->_options[$name]);
    }

    /**
     * @param string $name
     *
     * @return mixed
     */
    public function getOption($name)
    {
        return $this->_options[$name];
    }

    /**
     * @return mixed[]
     */
    public function getOptions()
    {
        return $this->_options;
    }

    /**
     * @return void
     */
    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.
     *
     * @return void
     */
    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);
        }
    }

    /**
     * Normalizes a given identifier.
     *
     * Trims quotes and lowercases the given identifier.
     *
     * @param string|null $identifier The identifier to normalize.
     *
     * @return string The normalized identifier.
     */
    private function normalizeIdentifier($identifier)
    {
        if ($identifier === null) {
            return '';
        }

        return $this->trimQuotes(strtolower($identifier));
    }

    public function setComment(?string $comment): self
    {
        // For keeping backward compatibility with MySQL in previous releases, table comments are stored as options.
        $this->addOption('comment', $comment);

        return $this;
    }

    public function getComment(): ?string
    {
        return $this->_options['comment'] ?? null;
    }
}
<?php

namespace Doctrine\DBAL\Schema;

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\ConnectionException;
use Doctrine\DBAL\Event\SchemaColumnDefinitionEventArgs;
use Doctrine\DBAL\Event\SchemaIndexDefinitionEventArgs;
use Doctrine\DBAL\Events;
use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\Deprecations\Deprecation;
use Throwable;

use function array_filter;
use function array_intersect;
use function array_map;
use function array_values;
use function assert;
use function call_user_func_array;
use function count;
use function func_get_args;
use function is_callable;
use function is_string;
use function preg_match;
use function str_replace;
use function strtolower;

/**
 * Base class for schema managers. Schema managers are used to inspect and/or
 * modify the database schema/structure.
 */
abstract class AbstractSchemaManager
{
    /**
     * Holds instance of the Doctrine connection for this schema manager.
     *
     * @var Connection
     */
    protected $_conn;

    /**
     * Holds instance of the database platform used for this schema manager.
     *
     * @var AbstractPlatform
     */
    protected $_platform;

    /**
     * Constructor. Accepts the Connection instance to manage the schema for.
     */
    public function __construct(Connection $conn, ?AbstractPlatform $platform = null)
    {
        $this->_conn     = $conn;
        $this->_platform = $platform ?: $this->_conn->getDatabasePlatform();
    }

    /**
     * Returns the associated platform.
     *
     * @return AbstractPlatform
     */
    public function getDatabasePlatform()
    {
        return $this->_platform;
    }

    /**
     * Tries 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.
     *
     * <code>
     * $result = $sm->tryMethod('dropView', 'view_name');
     * </code>
     *
     * @return mixed
     */
    public function tryMethod()
    {
        $args   = func_get_args();
        $method = $args[0];
        unset($args[0]);
        $args = array_values($args);

        $callback = [$this, $method];
        assert(is_callable($callback));

        try {
            return call_user_func_array($callback, $args);
        } catch (Throwable $e) {
            return false;
        }
    }

    /**
     * Lists the available databases for this connection.
     *
     * @return string[]
     */
    public function listDatabases()
    {
        $sql = $this->_platform->getListDatabasesSQL();

        $databases = $this->_conn->fetchAllAssociative($sql);

        return $this->_getPortableDatabasesList($databases);
    }

    /**
     * Returns a list of all namespaces in the current database.
     *
     * @return string[]
     */
    public function listNamespaceNames()
    {
        $sql = $this->_platform->getListNamespacesSQL();

        $namespaces = $this->_conn->fetchAllAssociative($sql);

        return $this->getPortableNamespacesList($namespaces);
    }

    /**
     * Lists the available sequences for this connection.
     *
     * @param string|null $database
     *
     * @return Sequence[]
     */
    public function listSequences($database = null)
    {
        if ($database === null) {
            $database = $this->_conn->getDatabase();
        }

        $sql = $this->_platform->getListSequencesSQL($database);

        $sequences = $this->_conn->fetchAllAssociative($sql);

        return $this->filterAssetNames($this->_getPortableSequencesList($sequences));
    }

    /**
     * Lists 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' column for
     * the reason that it is not portable across different RDBMS. Use
     * {@see listTableIndexes($tableName)} to retrieve the primary key
     * of a table. Where a RDBMS specifies more details, these are held
     * in the platformDetails array.
     *
     * @param string      $table    The name of the table.
     * @param string|null $database
     *
     * @return Column[]
     */
    public function listTableColumns($table, $database = null)
    {
        if (! $database) {
            $database = $this->_conn->getDatabase();
        }

        $sql = $this->_platform->getListTableColumnsSQL($table, $database);

        $tableColumns = $this->_conn->fetchAllAssociative($sql);

        return $this->_getPortableTableColumnList($table, $database, $tableColumns);
    }

    /**
     * Lists 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[]
     */
    public function listTableIndexes($table)
    {
        $sql = $this->_platform->getListTableIndexesSQL($table, $this->_conn->getDatabase());

        $tableIndexes = $this->_conn->fetchAllAssociative($sql);

        return $this->_getPortableTableIndexesList($tableIndexes, $table);
    }

    /**
     * Returns true if all the given tables exist.
     *
     * The usage of a string $tableNames is deprecated. Pass a one-element array instead.
     *
     * @param string|string[] $names
     *
     * @return bool
     */
    public function tablesExist($names)
    {
        if (is_string($names)) {
            Deprecation::trigger(
                'doctrine/dbal',
                'https://github.com/doctrine/dbal/issues/3580',
                'The usage of a string $tableNames in AbstractSchemaManager::tablesExist is deprecated. ' .
                'Pass a one-element array instead.'
            );
        }

        $names = array_map('strtolower', (array) $names);

        return count($names) === count(array_intersect($names, array_map('strtolower', $this->listTableNames())));
    }

    /**
     * Returns a list of all tables in the current database.
     *
     * @return string[]
     */
    public function listTableNames()
    {
        $sql = $this->_platform->getListTablesSQL();

        $tables     = $this->_conn->fetchAllAssociative($sql);
        $tableNames = $this->_getPortableTablesList($tables);

        return $this->filterAssetNames($tableNames);
    }

    /**
     * Filters asset names if they are configured to return only a subset of all
     * the found elements.
     *
     * @param mixed[] $assetNames
     *
     * @return mixed[]
     */
    protected function filterAssetNames($assetNames)
    {
        $filter = $this->_conn->getConfiguration()->getSchemaAssetsFilter();
        if (! $filter) {
            return $assetNames;
        }

        return array_values(array_filter($assetNames, $filter));
    }

    /**
     * @deprecated Use Configuration::getSchemaAssetsFilter() instead
     *
     * @return string|null
     */
    protected function getFilterSchemaAssetsExpression()
    {
        return $this->_conn->getConfiguration()->getFilterSchemaAssetsExpression();
    }

    /**
     * Lists the tables for this connection.
     *
     * @return Table[]
     */
    public function listTables()
    {
        $tableNames = $this->listTableNames();

        $tables = [];
        foreach ($tableNames as $tableName) {
            $tables[] = $this->listTableDetails($tableName);
        }

        return $tables;
    }

    /**
     * @param string $name
     *
     * @return Table
     */
    public function listTableDetails($name)
    {
        $columns     = $this->listTableColumns($name);
        $foreignKeys = [];
        if ($this->_platform->supportsForeignKeyConstraints()) {
            $foreignKeys = $this->listTableForeignKeys($name);
        }

        $indexes = $this->listTableIndexes($name);

        return new Table($name, $columns, $indexes, $foreignKeys);
    }

    /**
     * Lists the views this connection has.
     *
     * @return View[]
     */
    public function listViews()
    {
        $database = $this->_conn->getDatabase();
        $sql      = $this->_platform->getListViewsSQL($database);
        $views    = $this->_conn->fetchAllAssociative($sql);

        return $this->_getPortableViewsList($views);
    }

    /**
     * Lists the foreign keys for the given table.
     *
     * @param string      $table    The name of the table.
     * @param string|null $database
     *
     * @return ForeignKeyConstraint[]
     */
    public function listTableForeignKeys($table, $database = null)
    {
        if ($database === null) {
            $database = $this->_conn->getDatabase();
        }

        $sql              = $this->_platform->getListTableForeignKeysSQL($table, $database);
        $tableForeignKeys = $this->_conn->fetchAllAssociative($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.
     *
     * @return void
     */
    public function dropDatabase($database)
    {
        $this->_execSql($this->_platform->getDropDatabaseSQL($database));
    }

    /**
     * Drops the given table.
     *
     * @param string $name The name of the table to drop.
     *
     * @return void
     */
    public function dropTable($name)
    {
        $this->_execSql($this->_platform->getDropTableSQL($name));
    }

    /**
     * Drops the index from the given table.
     *
     * @param Index|string $index The name of the index.
     * @param Table|string $table The name of the table.
     *
     * @return void
     */
    public function dropIndex($index, $table)
    {
        if ($index instanceof Index) {
            $index = $index->getQuotedName($this->_platform);
        }

        $this->_execSql($this->_platform->getDropIndexSQL($index, $table));
    }

    /**
     * Drops the constraint from the given table.
     *
     * @param Table|string $table The name of the table.
     *
     * @return void
     */
    public function dropConstraint(Constraint $constraint, $table)
    {
        $this->_execSql($this->_platform->getDropConstraintSQL($constraint, $table));
    }

    /**
     * Drops a foreign key from a table.
     *
     * @param ForeignKeyConstraint|string $foreignKey The name of the foreign key.
     * @param Table|string                $table      The name of the table with the foreign key.
     *
     * @return void
     */
    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.
     *
     * @return void
     */
    public function dropSequence($name)
    {
        $this->_execSql($this->_platform->getDropSequenceSQL($name));
    }

    /**
     * Drops a view.
     *
     * @param string $name The name of the view.
     *
     * @return void
     */
    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.
     *
     * @return void
     */
    public function createDatabase($database)
    {
        $this->_execSql($this->_platform->getCreateDatabaseSQL($database));
    }

    /**
     * Creates a new table.
     *
     * @return void
     */
    public function createTable(Table $table)
    {
        $createFlags = AbstractPlatform::CREATE_INDEXES | AbstractPlatform::CREATE_FOREIGNKEYS;
        $this->_execSql($this->_platform->getCreateTableSQL($table, $createFlags));
    }

    /**
     * Creates a new sequence.
     *
     * @param Sequence $sequence
     *
     * @return void
     *
     * @throws ConnectionException If something fails at database level.
     */
    public function createSequence($sequence)
    {
        $this->_execSql($this->_platform->getCreateSequenceSQL($sequence));
    }

    /**
     * Creates a constraint on a table.
     *
     * @param Table|string $table
     *
     * @return void
     */
    public function createConstraint(Constraint $constraint, $table)
    {
        $this->_execSql($this->_platform->getCreateConstraintSQL($constraint, $table));
    }

    /**
     * Creates a new index on a table.
     *
     * @param Table|string $table The name of the table on which the index is to be created.
     *
     * @return void
     */
    public function createIndex(Index $index, $table)
    {
        $this->_execSql($this->_platform->getCreateIndexSQL($index, $table));
    }

    /**
     * Creates a new foreign key.
     *
     * @param ForeignKeyConstraint $foreignKey The ForeignKey instance.
     * @param Table|string         $table      The name of the table on which the foreign key is to be created.
     *
     * @return void
     */
    public function createForeignKey(ForeignKeyConstraint $foreignKey, $table)
    {
        $this->_execSql($this->_platform->getCreateForeignKeySQL($foreignKey, $table));
    }

    /**
     * Creates a new view.
     *
     * @return void
     */
    public function createView(View $view)
    {
        $this->_execSql($this->_platform->getCreateViewSQL($view->getQuotedName($this->_platform), $view->getSql()));
    }

    /* dropAndCreate*() Methods */

    /**
     * Drops and creates a constraint.
     *
     * @see dropConstraint()
     * @see createConstraint()
     *
     * @param Table|string $table
     *
     * @return void
     */
    public function dropAndCreateConstraint(Constraint $constraint, $table)
    {
        $this->tryMethod('dropConstraint', $constraint, $table);
        $this->createConstraint($constraint, $table);
    }

    /**
     * Drops and creates a new index on a table.
     *
     * @param Table|string $table The name of the table on which the index is to be created.
     *
     * @return void
     */
    public function dropAndCreateIndex(Index $index, $table)
    {
        $this->tryMethod('dropIndex', $index->getQuotedName($this->_platform), $table);
        $this->createIndex($index, $table);
    }

    /**
     * Drops and creates a new foreign key.
     *
     * @param ForeignKeyConstraint $foreignKey An associative array that defines properties
     *                                         of the foreign key to be created.
     * @param Table|string         $table      The name of the table on which the foreign key is to be created.
     *
     * @return void
     */
    public function dropAndCreateForeignKey(ForeignKeyConstraint $foreignKey, $table)
    {
        $this->tryMethod('dropForeignKey', $foreignKey, $table);
        $this->createForeignKey($foreignKey, $table);
    }

    /**
     * Drops and create a new sequence.
     *
     * @return void
     *
     * @throws ConnectionException If something fails at database level.
     */
    public function dropAndCreateSequence(Sequence $sequence)
    {
        $this->tryMethod('dropSequence', $sequence->getQuotedName($this->_platform));
        $this->createSequence($sequence);
    }

    /**
     * Drops and creates a new table.
     *
     * @return void
     */
    public function dropAndCreateTable(Table $table)
    {
        $this->tryMethod('dropTable', $table->getQuotedName($this->_platform));
        $this->createTable($table);
    }

    /**
     * Drops and creates a new database.
     *
     * @param string $database The name of the database to create.
     *
     * @return void
     */
    public function dropAndCreateDatabase($database)
    {
        $this->tryMethod('dropDatabase', $database);
        $this->createDatabase($database);
    }

    /**
     * Drops and creates a new view.
     *
     * @return void
     */
    public function dropAndCreateView(View $view)
    {
        $this->tryMethod('dropView', $view->getQuotedName($this->_platform));
        $this->createView($view);
    }

    /* alterTable() Methods */

    /**
     * Alters an existing tables schema.
     *
     * @return void
     */
    public function alterTable(TableDiff $tableDiff)
    {
        foreach ($this->_platform->getAlterTableSQL($tableDiff) as $ddlQuery) {
            $this->_execSql($ddlQuery);
        }
    }

    /**
     * Renames a given table to another name.
     *
     * @param string $name    The current name of the table.
     * @param string $newName The new name of the table.
     *
     * @return void
     */
    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
     */

    /**
     * @param mixed[] $databases
     *
     * @return string[]
     */
    protected function _getPortableDatabasesList($databases)
    {
        $list = [];
        foreach ($databases as $value) {
            $value = $this->_getPortableDatabaseDefinition($value);

            if (! $value) {
                continue;
            }

            $list[] = $value;
        }

        return $list;
    }

    /**
     * Converts a list of namespace names from the native DBMS data definition to a portable Doctrine definition.
     *
     * @param mixed[][] $namespaces The list of namespace names in the native DBMS data definition.
     *
     * @return string[]
     */
    protected function getPortableNamespacesList(array $namespaces)
    {
        $namespacesList = [];

        foreach ($namespaces as $namespace) {
            $namespacesList[] = $this->getPortableNamespaceDefinition($namespace);
        }

        return $namespacesList;
    }

    /**
     * @param mixed $database
     *
     * @return mixed
     */
    protected function _getPortableDatabaseDefinition($database)
    {
        return $database;
    }

    /**
     * Converts a namespace definition from the native DBMS data definition to a portable Doctrine definition.
     *
     * @param mixed[] $namespace The native DBMS namespace definition.
     *
     * @return mixed
     */
    protected function getPortableNamespaceDefinition(array $namespace)
    {
        return $namespace;
    }

    /**
     * @deprecated
     *
     * @param mixed[][] $functions
     *
     * @return mixed[][]
     */
    protected function _getPortableFunctionsList($functions)
    {
        $list = [];
        foreach ($functions as $value) {
            $value = $this->_getPortableFunctionDefinition($value);

            if (! $value) {
                continue;
            }

            $list[] = $value;
        }

        return $list;
    }

    /**
     * @deprecated
     *
     * @param mixed[] $function
     *
     * @return mixed
     */
    protected function _getPortableFunctionDefinition($function)
    {
        return $function;
    }

    /**
     * @param mixed[][] $triggers
     *
     * @return mixed[][]
     */
    protected function _getPortableTriggersList($triggers)
    {
        $list = [];
        foreach ($triggers as $value) {
            $value = $this->_getPortableTriggerDefinition($value);

            if (! $value) {
                continue;
            }

            $list[] = $value;
        }

        return $list;
    }

    /**
     * @param mixed[] $trigger
     *
     * @return mixed
     */
    protected function _getPortableTriggerDefinition($trigger)
    {
        return $trigger;
    }

    /**
     * @param mixed[][] $sequences
     *
     * @return Sequence[]
     */
    protected function _getPortableSequencesList($sequences)
    {
        $list = [];

        foreach ($sequences as $value) {
            $list[] = $this->_getPortableSequenceDefinition($value);
        }

        return $list;
    }

    /**
     * @param mixed[] $sequence
     *
     * @return Sequence
     *
     * @throws Exception
     */
    protected function _getPortableSequenceDefinition($sequence)
    {
        throw Exception::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 string    $table        The name of the table.
     * @param string    $database
     * @param mixed[][] $tableColumns
     *
     * @return Column[]
     */
    protected function _getPortableTableColumnList($table, $database, $tableColumns)
    {
        $eventManager = $this->_platform->getEventManager();

        $list = [];
        foreach ($tableColumns as $tableColumn) {
            $column           = null;
            $defaultPrevented = false;

            if ($eventManager !== null && $eventManager->hasListeners(Events::onSchemaColumnDefinition)) {
                $eventArgs = new SchemaColumnDefinitionEventArgs($tableColumn, $table, $database, $this->_conn);
                $eventManager->dispatchEvent(Events::onSchemaColumnDefinition, $eventArgs);

                $defaultPrevented = $eventArgs->isDefaultPrevented();
                $column           = $eventArgs->getColumn();
            }

            if (! $defaultPrevented) {
                $column = $this->_getPortableTableColumnDefinition($tableColumn);
            }

            if (! $column) {
                continue;
            }

            $name        = strtolower($column->getQuotedName($this->_platform));
            $list[$name] = $column;
        }

        return $list;
    }

    /**
     * Gets Table Column Definition.
     *
     * @param mixed[] $tableColumn
     *
     * @return Column
     */
    abstract protected function _getPortableTableColumnDefinition($tableColumn);

    /**
     * Aggregates and groups the index results according to the required data result.
     *
     * @param mixed[][]   $tableIndexes
     * @param string|null $tableName
     *
     * @return Index[]
     */
    protected function _getPortableTableIndexesList($tableIndexes, $tableName = null)
    {
        $result = [];
        foreach ($tableIndexes as $tableIndex) {
            $indexName = $keyName = $tableIndex['key_name'];
            if ($tableIndex['primary']) {
                $keyName = 'primary';
            }

            $keyName = strtolower($keyName);

            if (! isset($result[$keyName])) {
                $options = [
                    'lengths' => [],
                ];

                if (isset($tableIndex['where'])) {
                    $options['where'] = $tableIndex['where'];
                }

                $result[$keyName] = [
                    'name' => $indexName,
                    'columns' => [],
                    'unique' => ! $tableIndex['non_unique'],
                    'primary' => $tableIndex['primary'],
                    'flags' => $tableIndex['flags'] ?? [],
                    'options' => $options,
                ];
            }

            $result[$keyName]['columns'][]            = $tableIndex['column_name'];
            $result[$keyName]['options']['lengths'][] = $tableIndex['length'] ?? null;
        }

        $eventManager = $this->_platform->getEventManager();

        $indexes = [];
        foreach ($result as $indexKey => $data) {
            $index            = null;
            $defaultPrevented = false;

            if ($eventManager !== null && $eventManager->hasListeners(Events::onSchemaIndexDefinition)) {
                $eventArgs = new SchemaIndexDefinitionEventArgs($data, $tableName, $this->_conn);
                $eventManager->dispatchEvent(Events::onSchemaIndexDefinition, $eventArgs);

                $defaultPrevented = $eventArgs->isDefaultPrevented();
                $index            = $eventArgs->getIndex();
            }

            if (! $defaultPrevented) {
                $index = new Index(
                    $data['name'],
                    $data['columns'],
                    $data['unique'],
                    $data['primary'],
                    $data['flags'],
                    $data['options']
                );
            }

            if (! $index) {
                continue;
            }

            $indexes[$indexKey] = $index;
        }

        return $indexes;
    }

    /**
     * @param mixed[][] $tables
     *
     * @return string[]
     */
    protected function _getPortableTablesList($tables)
    {
        $list = [];
        foreach ($tables as $value) {
            $value = $this->_getPortableTableDefinition($value);

            if (! $value) {
                continue;
            }

            $list[] = $value;
        }

        return $list;
    }

    /**
     * @param mixed $table
     *
     * @return string
     */
    protected function _getPortableTableDefinition($table)
    {
        return $table;
    }

    /**
     * @param mixed[][] $users
     *
     * @return string[][]
     */
    protected function _getPortableUsersList($users)
    {
        $list = [];
        foreach ($users as $value) {
            $value = $this->_getPortableUserDefinition($value);

            if (! $value) {
                continue;
            }

            $list[] = $value;
        }

        return $list;
    }

    /**
     * @param string[] $user
     *
     * @return string[]
     */
    protected function _getPortableUserDefinition($user)
    {
        return $user;
    }

    /**
     * @param mixed[][] $views
     *
     * @return View[]
     */
    protected function _getPortableViewsList($views)
    {
        $list = [];
        foreach ($views as $value) {
            $view = $this->_getPortableViewDefinition($value);

            if (! $view) {
                continue;
            }

            $viewName        = strtolower($view->getQuotedName($this->_platform));
            $list[$viewName] = $view;
        }

        return $list;
    }

    /**
     * @param mixed[] $view
     *
     * @return View|false
     */
    protected function _getPortableViewDefinition($view)
    {
        return false;
    }

    /**
     * @param mixed[][] $tableForeignKeys
     *
     * @return ForeignKeyConstraint[]
     */
    protected function _getPortableTableForeignKeysList($tableForeignKeys)
    {
        $list = [];

        foreach ($tableForeignKeys as $value) {
            $list[] = $this->_getPortableTableForeignKeyDefinition($value);
        }

        return $list;
    }

    /**
     * @param mixed $tableForeignKey
     *
     * @return ForeignKeyConstraint
     */
    protected function _getPortableTableForeignKeyDefinition($tableForeignKey)
    {
        return $tableForeignKey;
    }

    /**
     * @param string[]|string $sql
     *
     * @return void
     */
    protected function _execSql($sql)
    {
        foreach ((array) $sql as $query) {
            $this->_conn->executeStatement($query);
        }
    }

    /**
     * Creates a schema instance for the current database.
     *
     * @return Schema
     */
    public function createSchema()
    {
        $namespaces = [];

        if ($this->_platform->supportsSchemas()) {
            $namespaces = $this->listNamespaceNames();
        }

        $sequences = [];

        if ($this->_platform->supportsSequences()) {
            $sequences = $this->listSequences();
        }

        $tables = $this->listTables();

        return new Schema($tables, $sequences, $this->createSchemaConfig(), $namespaces);
    }

    /**
     * Creates the configuration for this schema.
     *
     * @return SchemaConfig
     */
    public function createSchemaConfig()
    {
        $schemaConfig = new SchemaConfig();
        $schemaConfig->setMaxIdentifierLength($this->_platform->getMaxIdentifierLength());

        $searchPaths = $this->getSchemaSearchPaths();
        if (isset($searchPaths[0])) {
            $schemaConfig->setName($searchPaths[0]);
        }

        $params = $this->_conn->getParams();
        if (! isset($params['defaultTableOptions'])) {
            $params['defaultTableOptions'] = [];
        }

        if (! isset($params['defaultTableOptions']['charset']) && isset($params['charset'])) {
            $params['defaultTableOptions']['charset'] = $params['charset'];
        }

        $schemaConfig->setDefaultTableOptions($params['defaultTableOptions']);

        return $schemaConfig;
    }

    /**
     * The search path for namespaces in the currently connected database.
     *
     * The first entry is usually the default namespace in the Schema. All
     * further namespaces contain tables/sequences which can also be addressed
     * with a short, not full-qualified name.
     *
     * For databases that don't support subschema/namespaces this method
     * returns the name of the currently connected database.
     *
     * @return string[]
     */
    public function getSchemaSearchPaths()
    {
        return [$this->_conn->getDatabase()];
    }

    /**
     * Given a table comment this method tries to extract a typehint for Doctrine Type, or returns
     * the type given as default.
     *
     * @param string|null $comment
     * @param string      $currentType
     *
     * @return string
     */
    public function extractDoctrineTypeFromComment($comment, $currentType)
    {
        if ($comment !== null && preg_match('(\(DC2Type:(((?!\)).)+)\))', $comment, $match)) {
            return $match[1];
        }

        return $currentType;
    }

    /**
     * @param string|null $comment
     * @param string|null $type
     *
     * @return string|null
     */
    public function removeDoctrineTypeFromComment($comment, $type)
    {
        if ($comment === null) {
            return null;
        }

        return str_replace('(DC2Type:' . $type . ')', '', $comment);
    }
}
<?php

namespace Doctrine\DBAL\Schema;

/**
 * An abstraction class for an asset identifier.
 *
 * Wraps identifier names like column names in indexes / foreign keys
 * in an abstract class for proper quotation capabilities.
 */
class Identifier extends AbstractAsset
{
    /**
     * @param string $identifier Identifier name to wrap.
     * @param bool   $quote      Whether to force quoting the given identifier.
     */
    public function __construct($identifier, $quote = false)
    {
        $this->_setName($identifier);

        if (! $quote || $this->_quoted) {
            return;
        }

        $this->_setName('"' . $this->getName() . '"');
    }
}
<?php

namespace Doctrine\DBAL\Schema;

use Doctrine\DBAL\Platforms\SQLAnywherePlatform;
use Doctrine\DBAL\Types\Type;

use function assert;
use function preg_replace;

/**
 * SAP Sybase SQL Anywhere schema manager.
 */
class SQLAnywhereSchemaManager extends AbstractSchemaManager
{
    /**
     * {@inheritdoc}
     *
     * Starts a database after creation
     * as SQL Anywhere needs a database to be started
     * before it can be used.
     *
     * @see startDatabase
     */
    public function createDatabase($database)
    {
        parent::createDatabase($database);
        $this->startDatabase($database);
    }

    /**
     * {@inheritdoc}
     *
     * Tries stopping a database before dropping
     * as SQL Anywhere needs a database to be stopped
     * before it can be dropped.
     *
     * @see stopDatabase
     */
    public function dropDatabase($database)
    {
        $this->tryMethod('stopDatabase', $database);
        parent::dropDatabase($database);
    }

    /**
     * Starts a database.
     *
     * @param string $database The name of the database to start.
     *
     * @return void
     */
    public function startDatabase($database)
    {
        assert($this->_platform instanceof SQLAnywherePlatform);
        $this->_execSql($this->_platform->getStartDatabaseSQL($database));
    }

    /**
     * Stops a database.
     *
     * @param string $database The name of the database to stop.
     *
     * @return void
     */
    public function stopDatabase($database)
    {
        assert($this->_platform instanceof SQLAnywherePlatform);
        $this->_execSql($this->_platform->getStopDatabaseSQL($database));
    }

    /**
     * {@inheritdoc}
     */
    protected function _getPortableDatabaseDefinition($database)
    {
        return $database['name'];
    }

    /**
     * {@inheritdoc}
     */
    protected function _getPortableSequenceDefinition($sequence)
    {
        return new Sequence($sequence['sequence_name'], $sequence['increment_by'], $sequence['start_with']);
    }

    /**
     * {@inheritdoc}
     */
    protected function _getPortableTableColumnDefinition($tableColumn)
    {
        $type                   = $this->_platform->getDoctrineTypeMapping($tableColumn['type']);
        $type                   = $this->extractDoctrineTypeFromComment($tableColumn['comment'], $type);
        $tableColumn['comment'] = $this->removeDoctrineTypeFromComment($tableColumn['comment'], $type);
        $precision              = null;
        $scale                  = null;
        $fixed                  = false;
        $default                = null;

        if ($tableColumn['default'] !== null) {
            // Strip quotes from default value.
            $default = preg_replace(["/^'(.*)'$/", "/''/"], ['$1', "'"], $tableColumn['default']);

            if ($default === 'autoincrement') {
                $default = null;
            }
        }

        switch ($tableColumn['type']) {
            case 'binary':
            case 'char':
            case 'nchar':
                $fixed = true;
                break;
        }

        switch ($type) {
            case 'decimal':
            case 'float':
                $precision = $tableColumn['length'];
                $scale     = $tableColumn['scale'];
                break;
        }

        return new Column(
            $tableColumn['column_name'],
            Type::getType($type),
            [
                'length'        => $type === 'string' ? $tableColumn['length'] : null,
                'precision'     => $precision,
                'scale'         => $scale,
                'unsigned'      => (bool) $tableColumn['unsigned'],
                'fixed'         => $fixed,
                'notnull'       => (bool) $tableColumn['notnull'],
                'default'       => $default,
                'autoincrement' => (bool) $tableColumn['autoincrement'],
                'comment'       => isset($tableColumn['comment']) && $tableColumn['comment'] !== ''
                    ? $tableColumn['comment']
                    : null,
            ]
        );
    }

    /**
     * {@inheritdoc}
     */
    protected function _getPortableTableDefinition($table)
    {
        return $table['table_name'];
    }

    /**
     * {@inheritdoc}
     */
    protected function _getPortableTableForeignKeyDefinition($tableForeignKey)
    {
        return new ForeignKeyConstraint(
            $tableForeignKey['local_columns'],
            $tableForeignKey['foreign_table'],
            $tableForeignKey['foreign_columns'],
            $tableForeignKey['name'],
            $tableForeignKey['options']
        );
    }

    /**
     * {@inheritdoc}
     */
    protected function _getPortableTableForeignKeysList($tableForeignKeys)
    {
        $foreignKeys = [];

        foreach ($tableForeignKeys as $tableForeignKey) {
            if (! isset($foreignKeys[$tableForeignKey['index_name']])) {
                $foreignKeys[$tableForeignKey['index_name']] = [
                    'local_columns'   => [$tableForeignKey['local_column']],
                    'foreign_table'   => $tableForeignKey['foreign_table'],
                    'foreign_columns' => [$tableForeignKey['foreign_column']],
                    'name'            => $tableForeignKey['index_name'],
                    'options'         => [
                        'notnull'           => $tableForeignKey['notnull'],
                        'match'             => $tableForeignKey['match'],
                        'onUpdate'          => $tableForeignKey['on_update'],
                        'onDelete'          => $tableForeignKey['on_delete'],
                        'check_on_commit'   => $tableForeignKey['check_on_commit'],
                        'clustered'         => $tableForeignKey['clustered'],
                        'for_olap_workload' => $tableForeignKey['for_olap_workload'],
                    ],
                ];
            } else {
                $foreignKeys[$tableForeignKey['index_name']]['local_columns'][]   = $tableForeignKey['local_column'];
                $foreignKeys[$tableForeignKey['index_name']]['foreign_columns'][] = $tableForeignKey['foreign_column'];
            }
        }

        return parent::_getPortableTableForeignKeysList($foreignKeys);
    }

    /**
     * {@inheritdoc}
     */
    protected function _getPortableTableIndexesList($tableIndexes, $tableName = null)
    {
        foreach ($tableIndexes as &$tableIndex) {
            $tableIndex['primary'] = (bool) $tableIndex['primary'];
            $tableIndex['flags']   = [];

            if ($tableIndex['clustered']) {
                $tableIndex['flags'][] = 'clustered';
            }

            if ($tableIndex['with_nulls_not_distinct']) {
                $tableIndex['flags'][] = 'with_nulls_not_distinct';
            }

            if (! $tableIndex['for_olap_workload']) {
                continue;
            }

            $tableIndex['flags'][] = 'for_olap_workload';
        }

        return parent::_getPortableTableIndexesList($tableIndexes, $tableName);
    }

    /**
     * {@inheritdoc}
     */
    protected function _getPortableViewDefinition($view)
    {
        $definition = preg_replace('/^.*\s+as\s+SELECT(.*)/i', 'SELECT$1', $view['view_def']);

        return new View($view['table_name'], $definition);
    }
}
<?php

namespace Doctrine\DBAL\Schema;

use Doctrine\DBAL\Types\Type;
use Doctrine\Deprecations\Deprecation;

use function array_merge;
use function is_numeric;
use function method_exists;

/**
 * Object representation of a database column.
 */
class Column extends AbstractAsset
{
    /** @var Type */
    protected $_type;

    /** @var int|null */
    protected $_length;

    /** @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|null */
    protected $_default;

    /** @var bool */
    protected $_autoincrement = false;

    /** @var mixed[] */
    protected $_platformOptions = [];

    /** @var string|null */
    protected $_columnDefinition;

    /** @var string|null */
    protected $_comment;

    /** @var mixed[] */
    protected $_customSchemaOptions = [];

    /**
     * Creates a new Column.
     *
     * @param string  $name
     * @param mixed[] $options
     */
    public function __construct($name, Type $type, array $options = [])
    {
        $this->_setName($name);
        $this->setType($type);
        $this->setOptions($options);
    }

    /**
     * @param mixed[] $options
     *
     * @return Column
     */
    public function setOptions(array $options)
    {
        foreach ($options as $name => $value) {
            $method = 'set' . $name;
            if (! method_exists($this, $method)) {
                // next major: throw an exception
                Deprecation::trigger(
                    'doctrine/dbal',
                    'https://github.com/doctrine/dbal/pull/2846',
                    'The "%s" column option is not supported,' .
                    ' setting unknown options is deprecated and will cause an error in Doctrine DBAL 3.0',
                    $name
                );

                continue;
            }

            $this->$method($value);
        }

        return $this;
    }

    /**
     * @return Column
     */
    public function setType(Type $type)
    {
        $this->_type = $type;

        return $this;
    }

    /**
     * @param int|null $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)
    {
        if (! is_numeric($precision)) {
            $precision = 10; // defaults to 10 when no valid precision is given.
        }

        $this->_precision = (int) $precision;

        return $this;
    }

    /**
     * @param int $scale
     *
     * @return Column
     */
    public function setScale($scale)
    {
        if (! is_numeric($scale)) {
            $scale = 0;
        }

        $this->_scale = (int) $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 mixed[] $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 $value
     *
     * @return Column
     */
    public function setColumnDefinition($value)
    {
        $this->_columnDefinition = $value;

        return $this;
    }

    /**
     * @return Type
     */
    public function getType()
    {
        return $this->_type;
    }

    /**
     * @return int|null
     */
    public function getLength()
    {
        return $this->_length;
    }

    /**
     * @return int
     */
    public function getPrecision()
    {
        return $this->_precision;
    }

    /**
     * @return int
     */
    public function getScale()
    {
        return $this->_scale;
    }

    /**
     * @return bool
     */
    public function getUnsigned()
    {
        return $this->_unsigned;
    }

    /**
     * @return bool
     */
    public function getFixed()
    {
        return $this->_fixed;
    }

    /**
     * @return bool
     */
    public function getNotnull()
    {
        return $this->_notnull;
    }

    /**
     * @return string|null
     */
    public function getDefault()
    {
        return $this->_default;
    }

    /**
     * @return mixed[]
     */
    public function getPlatformOptions()
    {
        return $this->_platformOptions;
    }

    /**
     * @param string $name
     *
     * @return bool
     */
    public function hasPlatformOption($name)
    {
        return isset($this->_platformOptions[$name]);
    }

    /**
     * @param string $name
     *
     * @return mixed
     */
    public function getPlatformOption($name)
    {
        return $this->_platformOptions[$name];
    }

    /**
     * @return string|null
     */
    public function getColumnDefinition()
    {
        return $this->_columnDefinition;
    }

    /**
     * @return bool
     */
    public function getAutoincrement()
    {
        return $this->_autoincrement;
    }

    /**
     * @param bool $flag
     *
     * @return Column
     */
    public function setAutoincrement($flag)
    {
        $this->_autoincrement = $flag;

        return $this;
    }

    /**
     * @param string|null $comment
     *
     * @return Column
     */
    public function setComment($comment)
    {
        $this->_comment = $comment;

        return $this;
    }

    /**
     * @return string|null
     */
    public function getComment()
    {
        return $this->_comment;
    }

    /**
     * @param string $name
     * @param mixed  $value
     *
     * @return Column
     */
    public function setCustomSchemaOption($name, $value)
    {
        $this->_customSchemaOptions[$name] = $value;

        return $this;
    }

    /**
     * @param string $name
     *
     * @return bool
     */
    public function hasCustomSchemaOption($name)
    {
        return isset($this->_customSchemaOptions[$name]);
    }

    /**
     * @param string $name
     *
     * @return mixed
     */
    public function getCustomSchemaOption($name)
    {
        return $this->_customSchemaOptions[$name];
    }

    /**
     * @param mixed[] $customSchemaOptions
     *
     * @return Column
     */
    public function setCustomSchemaOptions(array $customSchemaOptions)
    {
        $this->_customSchemaOptions = $customSchemaOptions;

        return $this;
    }

    /**
     * @return mixed[]
     */
    public function getCustomSchemaOptions()
    {
        return $this->_customSchemaOptions;
    }

    /**
     * @return mixed[]
     */
    public function toArray()
    {
        return array_merge([
            '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,
            'comment' => $this->_comment,
        ], $this->_platformOptions, $this->_customSchemaOptions);
    }
}
<?php

namespace Doctrine\DBAL\Schema\Visitor;

use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
use Doctrine\DBAL\Schema\Sequence;
use Doctrine\DBAL\Schema\Table;

use function array_merge;

class CreateSchemaSqlCollector extends AbstractVisitor
{
    /** @var string[] */
    private $createNamespaceQueries = [];

    /** @var string[] */
    private $createTableQueries = [];

    /** @var string[] */
    private $createSequenceQueries = [];

    /** @var string[] */
    private $createFkConstraintQueries = [];

    /** @var AbstractPlatform */
    private $platform;

    public function __construct(AbstractPlatform $platform)
    {
        $this->platform = $platform;
    }

    /**
     * {@inheritdoc}
     */
    public function acceptNamespace($namespaceName)
    {
        if (! $this->platform->supportsSchemas()) {
            return;
        }

        $this->createNamespaceQueries[] = $this->platform->getCreateSchemaSQL($namespaceName);
    }

    /**
     * {@inheritdoc}
     */
    public function acceptTable(Table $table)
    {
        $this->createTableQueries = array_merge($this->createTableQueries, $this->platform->getCreateTableSQL($table));
    }

    /**
     * {@inheritdoc}
     */
    public function acceptForeignKey(Table $localTable, ForeignKeyConstraint $fkConstraint)
    {
        if (! $this->platform->supportsForeignKeyConstraints()) {
            return;
        }

        $this->createFkConstraintQueries[] = $this->platform->getCreateForeignKeySQL($fkConstraint, $localTable);
    }

    /**
     * {@inheritdoc}
     */
    public function acceptSequence(Sequence $sequence)
    {
        $this->createSequenceQueries[] = $this->platform->getCreateSequenceSQL($sequence);
    }

    /**
     * @return void
     */
    public function resetQueries()
    {
        $this->createNamespaceQueries    = [];
        $this->createTableQueries        = [];
        $this->createSequenceQueries     = [];
        $this->createFkConstraintQueries = [];
    }

    /**
     * Gets all queries collected so far.
     *
     * @return string[]
     */
    public function getQueries()
    {
        return array_merge(
            $this->createNamespaceQueries,
            $this->createTableQueries,
            $this->createSequenceQueries,
            $this->createFkConstraintQueries
        );
    }
}
<?php

namespace Doctrine\DBAL\Schema\Visitor;

use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
use Doctrine\DBAL\Schema\Index;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\Sequence;
use Doctrine\DBAL\Schema\Table;

/**
 * Abstract Visitor with empty methods for easy extension.
 */
class AbstractVisitor implements Visitor, NamespaceVisitor
{
    public function acceptSchema(Schema $schema)
    {
    }

    /**
     * {@inheritdoc}
     */
    public function acceptNamespace($namespaceName)
    {
    }

    public function acceptTable(Table $table)
    {
    }

    public function acceptColumn(Table $table, Column $column)
    {
    }

    public function acceptForeignKey(Table $localTable, ForeignKeyConstraint $fkConstraint)
    {
    }

    public function acceptIndex(Table $table, Index $index)
    {
    }

    public function acceptSequence(Sequence $sequence)
    {
    }
}
<?php

namespace Doctrine\DBAL\Schema\Visitor;

use Doctrine\DBAL\Schema\ForeignKeyConstraint;
use Doctrine\DBAL\Schema\Sequence;
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Schema\TableDiff;

/**
 * Visit a SchemaDiff.
 */
interface SchemaDiffVisitor
{
    /**
     * Visit an orphaned foreign key whose table was deleted.
     *
     * @return void
     */
    public function visitOrphanedForeignKey(ForeignKeyConstraint $foreignKey);

    /**
     * Visit a sequence that has changed.
     *
     * @return void
     */
    public function visitChangedSequence(Sequence $sequence);

    /**
     * Visit a sequence that has been removed.
     *
     * @return void
     */
    public function visitRemovedSequence(Sequence $sequence);

    /** @return void */
    public function visitNewSequence(Sequence $sequence);

    /** @return void */
    public function visitNewTable(Table $table);

    /** @return void */
    public function visitNewTableForeignKey(Table $table, ForeignKeyConstraint $foreignKey);

    /** @return void */
    public function visitRemovedTable(Table $table);

    /** @return void */
    public function visitChangedTable(TableDiff $tableDiff);
}
<?php

namespace Doctrine\DBAL\Schema\Visitor;

use Doctrine\DBAL\Schema\ForeignKeyConstraint;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\Table;

use function current;
use function file_put_contents;
use function in_array;
use function strtolower;

/**
 * Create a Graphviz output of a Schema.
 */
class Graphviz extends AbstractVisitor
{
    /** @var string */
    private $output = '';

    /**
     * {@inheritdoc}
     */
    public function acceptForeignKey(Table $localTable, ForeignKeyConstraint $fkConstraint)
    {
        $this->output .= $this->createNodeRelation(
            $fkConstraint->getLocalTableName() . ':col' . current($fkConstraint->getLocalColumns()) . ':se',
            $fkConstraint->getForeignTableName() . ':col' . current($fkConstraint->getForeignColumns()) . ':se',
            [
                'dir'       => 'back',
                'arrowtail' => 'dot',
                'arrowhead' => 'normal',
            ]
        );
    }

    /**
     * {@inheritdoc}
     */
    public function acceptSchema(Schema $schema)
    {
        $this->output  = 'digraph "' . $schema->getName() . '" {' . "\n";
        $this->output .= 'splines = true;' . "\n";
        $this->output .= 'overlap = false;' . "\n";
        $this->output .= 'outputorder=edgesfirst;' . "\n";
        $this->output .= 'mindist = 0.6;' . "\n";
        $this->output .= 'sep = .2;' . "\n";
    }

    /**
     * {@inheritdoc}
     */
    public function acceptTable(Table $table)
    {
        $this->output .= $this->createNode(
            $table->getName(),
            [
                'label' => $this->createTableLabel($table),
                'shape' => 'plaintext',
            ]
        );
    }

    /**
     * @return string
     */
    private function createTableLabel(Table $table)
    {
        // Start the table
        $label = '<<TABLE CELLSPACING="0" BORDER="1" ALIGN="LEFT">';

        // The title
        $label .= '<TR><TD BORDER="1" COLSPAN="3" ALIGN="CENTER" BGCOLOR="#fcaf3e">'
            . '<FONT COLOR="#2e3436" FACE="Helvetica" POINT-SIZE="12">' . $table->getName() . '</FONT></TD></TR>';

        // The attributes block
        foreach ($table->getColumns() as $column) {
            $columnLabel = $column->getName();

            $label .= '<TR>'
                . '<TD BORDER="0" ALIGN="LEFT" BGCOLOR="#eeeeec">'
                . '<FONT COLOR="#2e3436" FACE="Helvetica" POINT-SIZE="12">' . $columnLabel . '</FONT>'
                . '</TD>'
                . '<TD BORDER="0" ALIGN="LEFT" BGCOLOR="#eeeeec">'
                . '<FONT COLOR="#2e3436" FACE="Helvetica" POINT-SIZE="10">' . strtolower($column->getType()) . '</FONT>'
                . '</TD>'
                . '<TD BORDER="0" ALIGN="RIGHT" BGCOLOR="#eeeeec" PORT="col' . $column->getName() . '">';

            $primaryKey = $table->getPrimaryKey();

            if ($primaryKey !== null && in_array($column->getName(), $primaryKey->getColumns())) {
                $label .= "\xe2\x9c\xb7";
            }

            $label .= '</TD></TR>';
        }

        // End the table
        $label .= '</TABLE>>';

        return $label;
    }

    /**
     * @param string   $name
     * @param string[] $options
     *
     * @return string
     */
    private function createNode($name, $options)
    {
        $node = $name . ' [';
        foreach ($options as $key => $value) {
            $node .= $key . '=' . $value . ' ';
        }

        $node .= "]\n";

        return $node;
    }

    /**
     * @param string   $node1
     * @param string   $node2
     * @param string[] $options
     *
     * @return string
     */
    private function createNodeRelation($node1, $node2, $options)
    {
        $relation = $node1 . ' -> ' . $node2 . ' [';
        foreach ($options as $key => $value) {
            $relation .= $key . '=' . $value . ' ';
        }

        $relation .= "]\n";

        return $relation;
    }

    /**
     * Get Graphviz Output
     *
     * @return string
     */
    public function getOutput()
    {
        return $this->output . '}';
    }

    /**
     * Writes dot language output to a file. This should usually be a *.dot file.
     *
     * You have to convert the output into a viewable format. For example use "neato" on linux systems
     * and execute:
     *
     *  neato -Tpng -o er.png er.dot
     *
     * @param string $filename
     *
     * @return void
     */
    public function write($filename)
    {
        file_put_contents($filename, $this->getOutput());
    }
}
<?php

namespace Doctrine\DBAL\Schema\Visitor;

/**
 * Visitor that can visit schema namespaces.
 */
interface NamespaceVisitor
{
    /**
     * Accepts a schema namespace name.
     *
     * @param string $namespaceName The schema namespace name to accept.
     *
     * @return void
     */
    public function acceptNamespace($namespaceName);
}
<?php

namespace Doctrine\DBAL\Schema\Visitor;

use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
use Doctrine\DBAL\Schema\SchemaException;
use Doctrine\DBAL\Schema\Sequence;
use Doctrine\DBAL\Schema\Table;
use SplObjectStorage;

use function assert;
use function strlen;

/**
 * Gathers SQL statements that allow to completely drop the current schema.
 */
class DropSchemaSqlCollector extends AbstractVisitor
{
    /** @var SplObjectStorage */
    private $constraints;

    /** @var SplObjectStorage */
    private $sequences;

    /** @var SplObjectStorage */
    private $tables;

    /** @var AbstractPlatform */
    private $platform;

    public function __construct(AbstractPlatform $platform)
    {
        $this->platform = $platform;
        $this->initializeQueries();
    }

    /**
     * {@inheritdoc}
     */
    public function acceptTable(Table $table)
    {
        $this->tables->attach($table);
    }

    /**
     * {@inheritdoc}
     */
    public function acceptForeignKey(Table $localTable, ForeignKeyConstraint $fkConstraint)
    {
        if (strlen($fkConstraint->getName()) === 0) {
            throw SchemaException::namedForeignKeyRequired($localTable, $fkConstraint);
        }

        $this->constraints->attach($fkConstraint, $localTable);
    }

    /**
     * {@inheritdoc}
     */
    public function acceptSequence(Sequence $sequence)
    {
        $this->sequences->attach($sequence);
    }

    /**
     * @return void
     */
    public function clearQueries()
    {
        $this->initializeQueries();
    }

    /**
     * @return string[]
     */
    public function getQueries()
    {
        $sql = [];

        foreach ($this->constraints as $fkConstraint) {
            assert($fkConstraint instanceof ForeignKeyConstraint);
            $localTable = $this->constraints[$fkConstraint];
            $sql[]      = $this->platform->getDropForeignKeySQL($fkConstraint, $localTable);
        }

        foreach ($this->sequences as $sequence) {
            assert($sequence instanceof Sequence);
            $sql[] = $this->platform->getDropSequenceSQL($sequence);
        }

        foreach ($this->tables as $table) {
            assert($table instanceof Table);
            $sql[] = $this->platform->getDropTableSQL($table);
        }

        return $sql;
    }

    private function initializeQueries(): void
    {
        $this->constraints = new SplObjectStorage();
        $this->sequences   = new SplObjectStorage();
        $this->tables      = new SplObjectStorage();
    }
}
<?php

namespace Doctrine\DBAL\Schema\Visitor;

use Doctrine\DBAL\Schema\ForeignKeyConstraint;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\Sequence;
use Doctrine\DBAL\Schema\Table;

/**
 * Removes assets from a schema that are not in the default namespace.
 *
 * Some databases such as MySQL support cross databases joins, but don't
 * allow to call DDLs to a database from another connected database.
 * Before a schema is serialized into SQL this visitor can cleanup schemas with
 * non default namespaces.
 *
 * This visitor filters all these non-default namespaced tables and sequences
 * and removes them from the SChema instance.
 */
class RemoveNamespacedAssets extends AbstractVisitor
{
    /** @var Schema|null */
    private $schema;

    /**
     * {@inheritdoc}
     */
    public function acceptSchema(Schema $schema)
    {
        $this->schema = $schema;
    }

    /**
     * {@inheritdoc}
     */
    public function acceptTable(Table $table)
    {
        if ($this->schema === null) {
            return;
        }

        if ($table->isInDefaultNamespace($this->schema->getName())) {
            return;
        }

        $this->schema->dropTable($table->getName());
    }

    /**
     * {@inheritdoc}
     */
    public function acceptSequence(Sequence $sequence)
    {
        if ($this->schema === null) {
            return;
        }

        if ($sequence->isInDefaultNamespace($this->schema->getName())) {
            return;
        }

        $this->schema->dropSequence($sequence->getName());
    }

    /**
     * {@inheritdoc}
     */
    public function acceptForeignKey(Table $localTable, ForeignKeyConstraint $fkConstraint)
    {
        if ($this->schema === null) {
            return;
        }

        // The table may already be deleted in a previous
        // RemoveNamespacedAssets#acceptTable call. Removing Foreign keys that
        // point to nowhere.
        if (! $this->schema->hasTable($fkConstraint->getForeignTableName())) {
            $localTable->removeForeignKey($fkConstraint->getName());

            return;
        }

        $foreignTable = $this->schema->getTable($fkConstraint->getForeignTableName());
        if ($foreignTable->isInDefaultNamespace($this->schema->getName())) {
            return;
        }

        $localTable->removeForeignKey($fkConstraint->getName());
    }
}
<?php

namespace Doctrine\DBAL\Schema\Visitor;

use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
use Doctrine\DBAL\Schema\Index;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\Sequence;
use Doctrine\DBAL\Schema\Table;

/**
 * Schema Visitor used for Validation or Generation purposes.
 */
interface Visitor
{
    /**
     * @return void
     */
    public function acceptSchema(Schema $schema);

    /**
     * @return void
     */
    public function acceptTable(Table $table);

    /**
     * @return void
     */
    public function acceptColumn(Table $table, Column $column);

    /**
     * @return void
     */
    public function acceptForeignKey(Table $localTable, ForeignKeyConstraint $fkConstraint);

    /**
     * @return void
     */
    public function acceptIndex(Table $table, Index $index);

    /**
     * @return void
     */
    public function acceptSequence(Sequence $sequence);
}
<?php

namespace Doctrine\DBAL\Exception;

/**
 * Exception for an already existing table referenced in a statement detected in the driver.
 *
 * @psalm-immutable
 */
class TableExistsException extends DatabaseObjectExistsException
{
}
<?php

namespace Doctrine\DBAL\Exception;

/**
 * Exception for an invalid specified field name in a statement detected in the driver.
 *
 * @psalm-immutable
 */
class InvalidFieldNameException extends ServerException
{
}
<?php

namespace Doctrine\DBAL\Exception;

/**
 * Base class for all already existing database object related errors detected in the driver.
 *
 * A database object is considered any asset that can be created in a database
 * such as schemas, tables, views, sequences, triggers,  constraints, indexes,
 * functions, stored procedures etc.
 *
 * @psalm-immutable
 */
class DatabaseObjectExistsException extends ServerException
{
}
<?php

namespace Doctrine\DBAL\Exception;

/**
 * Exception for a write operation attempt on a read-only database element detected in the driver.
 *
 * @psalm-immutable
 */
class ReadOnlyException extends ServerException
{
}
<?php

namespace Doctrine\DBAL\Exception;

/**
 * Exception for a foreign key constraint violation detected in the driver.
 *
 * @psalm-immutable
 */
class ForeignKeyConstraintViolationException extends ConstraintViolationException
{
}
<?php

namespace Doctrine\DBAL\Exception;

/**
 * Exception for a deadlock error of a transaction detected in the driver.
 *
 * @psalm-immutable
 */
class DeadlockException extends ServerException implements RetryableException
{
}
<?php

namespace Doctrine\DBAL\Exception;

/**
 * Base class for all unknown database object related errors detected in the driver.
 *
 * A database object is considered any asset that can be created in a database
 * such as schemas, tables, views, sequences, triggers,  constraints, indexes,
 * functions, stored procedures etc.
 *
 * @psalm-immutable
 */
class DatabaseObjectNotFoundException extends ServerException
{
}
<?php

namespace Doctrine\DBAL\Exception;

/**
 * Base class for all connection related errors detected in the driver.
 *
 * @psalm-immutable
 */
class ConnectionException extends DriverException
{
}
<?php

namespace Doctrine\DBAL\Exception;

/**
 * Exception for a unique constraint violation detected in the driver.
 *
 * @psalm-immutable
 */
class UniqueConstraintViolationException extends ConstraintViolationException
{
}
<?php

namespace Doctrine\DBAL\Exception;

/**
 * Exception for a NOT NULL constraint violation detected in the driver.
 *
 * @psalm-immutable
 */
class NotNullConstraintViolationException extends ConstraintViolationException
{
}
<?php

namespace Doctrine\DBAL\Exception;

/**
 * Base class for all constraint violation related errors detected in the driver.
 *
 * @psalm-immutable
 */
class ConstraintViolationException extends ServerException
{
}
<?php

namespace Doctrine\DBAL\Exception;

/**
 * @psalm-immutable
 */
final class ConnectionLost extends ConnectionException
{
}
<?php

namespace Doctrine\DBAL\Exception;

use Doctrine\DBAL\Driver\DriverException as DeprecatedDriverException;
use Doctrine\DBAL\Exception;

/**
 * Base class for all errors detected in the driver.
 *
 * @psalm-immutable
 */
class DriverException extends Exception
{
    /**
     * The previous DBAL driver exception.
     *
     * @var DeprecatedDriverException
     */
    private $driverException;

    /**
     * @param string                    $message         The exception message.
     * @param DeprecatedDriverException $driverException The DBAL driver exception to chain.
     */
    public function __construct($message, DeprecatedDriverException $driverException)
    {
        parent::__construct($message, 0, $driverException);

        $this->driverException = $driverException;
    }

    /**
     * Returns the driver specific error code if given.
     *
     * Returns null if no error code was given by the driver.
     *
     * @return int|string|null
     */
    public function getErrorCode()
    {
        return $this->driverException->getErrorCode();
    }

    /**
     * Returns the SQLSTATE the driver was in at the time the error occurred, if given.
     *
     * Returns null if no SQLSTATE was given by the driver.
     *
     * @return string|null
     */
    public function getSQLState()
    {
        return $this->driverException->getSQLState();
    }
}
<?php

namespace Doctrine\DBAL\Exception;

/**
 * Exception for an unknown table referenced in a statement detected in the driver.
 *
 * @psalm-immutable
 */
class TableNotFoundException extends DatabaseObjectNotFoundException
{
}
<?php

namespace Doctrine\DBAL\Exception;

/**
 * Exception for a non-unique/ambiguous specified field name in a statement detected in the driver.
 *
 * @psalm-immutable
 */
class NonUniqueFieldNameException extends ServerException
{
}
<?php

namespace Doctrine\DBAL\Exception;

/**
 * Exception for a lock wait timeout error of a transaction detected in the driver.
 *
 * @psalm-immutable
 */
class LockWaitTimeoutException extends ServerException implements RetryableException
{
}
<?php

namespace Doctrine\DBAL\Exception;

use Throwable;

/**
 * Marker interface for all exceptions where retrying the transaction makes sense.
 *
 * @psalm-immutable
 */
interface RetryableException extends Throwable
{
}
<?php

namespace Doctrine\DBAL\Exception;

use Doctrine\DBAL\Exception;

use function sprintf;

/**
 * @internal
 *
 * @psalm-immutable
 */
final class NoKeyValue extends Exception
{
    public static function fromColumnCount(int $columnCount): self
    {
        return new self(
            sprintf(
                'Fetching as key-value pairs requires the result to contain at least 2 columns, %d given.',
                $columnCount
            )
        );
    }
}
<?php

namespace Doctrine\DBAL\Exception;

/**
 * Exception for a syntax error in a statement detected in the driver.
 *
 * @psalm-immutable
 */
class SyntaxErrorException extends ServerException
{
}
<?php

namespace Doctrine\DBAL\Exception;

/**
 * Base class for all server related errors detected in the driver.
 *
 * @psalm-immutable
 */
class ServerException extends DriverException
{
}
<?php

namespace Doctrine\DBAL\Exception;

use Doctrine\DBAL\Exception;

/**
 * Exception to be thrown when invalid arguments are passed to any DBAL API
 *
 * @psalm-immutable
 */
class InvalidArgumentException extends Exception
{
    /**
     * @return self
     */
    public static function fromEmptyCriteria()
    {
        return new self('Empty criteria was used, expected non-empty criteria');
    }
}
<?php

namespace Doctrine\DBAL;

use Doctrine\DBAL\Abstraction\Result;
use Doctrine\DBAL\Driver\Exception;
use Doctrine\DBAL\Driver\Statement as DriverStatement;
use Doctrine\DBAL\Exception\NoKeyValue;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Result as BaseResult;
use Doctrine\DBAL\Types\Type;
use Doctrine\Deprecations\Deprecation;
use IteratorAggregate;
use PDO;
use PDOStatement;
use ReturnTypeWillChange;
use Throwable;
use Traversable;

use function array_shift;
use function func_get_args;
use function is_array;
use function is_string;

/**
 * A thin wrapper around a Doctrine\DBAL\Driver\Statement that adds support
 * for logging, DBAL mapping types, etc.
 */
class Statement implements IteratorAggregate, DriverStatement, Result
{
    /**
     * The SQL statement.
     *
     * @var string
     */
    protected $sql;

    /**
     * The bound parameters.
     *
     * @var mixed[]
     */
    protected $params = [];

    /**
     * The parameter types.
     *
     * @var int[]|string[]
     */
    protected $types = [];

    /**
     * The underlying driver statement.
     *
     * @var \Doctrine\DBAL\Driver\Statement
     */
    protected $stmt;

    /**
     * The underlying database platform.
     *
     * @var AbstractPlatform
     */
    protected $platform;

    /**
     * The connection this statement is bound to and executed on.
     *
     * @var Connection
     */
    protected $conn;

    /**
     * Creates a new <tt>Statement</tt> for the given SQL and <tt>Connection</tt>.
     *
     * @internal The statement can be only instantiated by {@link Connection}.
     *
     * @param string     $sql  The SQL of the statement.
     * @param Connection $conn 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 string|int $param The name or position of the parameter.
     * @param mixed      $value The value of the parameter.
     * @param mixed      $type  Either a PDO binding type or a DBAL mapping type name or instance.
     *
     * @return bool TRUE on success, FALSE on failure.
     */
    public function bindValue($param, $value, $type = ParameterType::STRING)
    {
        $this->params[$param] = $value;
        $this->types[$param]  = $type;
        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;
            }

            return $this->stmt->bindValue($param, $value, $bindingType);
        }

        return $this->stmt->bindValue($param, $value);
    }

    /**
     * Binds a parameter to a value by reference.
     *
     * Binding a parameter by reference does not support DBAL mapping types.
     *
     * @param string|int $param    The name or position of the parameter.
     * @param mixed      $variable The reference to the variable to bind.
     * @param int        $type     The PDO binding type.
     * @param int|null   $length   Must be specified when using an OUT bind
     *                             so that PHP allocates enough memory to hold the returned value.
     *
     * @return bool TRUE on success, FALSE on failure.
     */
    public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null)
    {
        $this->params[$param] = $variable;
        $this->types[$param]  = $type;

        if ($this->stmt instanceof PDOStatement) {
            $length = $length ?? 0;
        }

        return $this->stmt->bindParam($param, $variable, $type, $length);
    }

    /**
     * Executes the statement with the currently bound parameters.
     *
     * @deprecated Statement::execute() is deprecated, use Statement::executeQuery() or executeStatement() instead
     *
     * @param mixed[]|null $params
     *
     * @return bool TRUE on success, FALSE on failure.
     *
     * @throws Exception
     */
    public function execute($params = null)
    {
        Deprecation::triggerIfCalledFromOutside(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/4580',
            'Statement::execute() is deprecated, use Statement::executeQuery() or Statement::executeStatement() instead'
        );

        if (is_array($params)) {
            $this->params = $params;
        }

        $logger = $this->conn->getConfiguration()->getSQLLogger();
        if ($logger) {
            $logger->startQuery($this->sql, $this->params, $this->types);
        }

        try {
            $stmt = $this->stmt->execute($params);
        } catch (Throwable $ex) {
            if ($logger) {
                $logger->stopQuery();
            }

            $this->conn->handleExceptionDuringQuery($ex, $this->sql, $this->params, $this->types);
        }

        if ($logger) {
            $logger->stopQuery();
        }

        return $stmt;
    }

    /**
     * Executes the statement with the currently bound parameters and return result.
     *
     * @param mixed[] $params
     *
     * @throws Exception
     */
    public function executeQuery(array $params = []): BaseResult
    {
        if ($params === []) {
            $params = null; // Workaround as long execute() exists and used internally.
        }

        $this->execute($params);

        return new ForwardCompatibility\Result($this);
    }

    /**
     * Executes the statement with the currently bound parameters and return affected rows.
     *
     * @param mixed[] $params
     *
     * @throws Exception
     */
    public function executeStatement(array $params = []): int
    {
        if ($params === []) {
            $params = null; // Workaround as long execute() exists and used internally.
        }

        $this->execute($params);

        return $this->rowCount();
    }

    /**
     * Closes the cursor, freeing the database resources used by this statement.
     *
     * @deprecated Use Result::free() instead.
     *
     * @return bool TRUE on success, FALSE on failure.
     */
    public function closeCursor()
    {
        Deprecation::triggerIfCalledFromOutside(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/4049',
            'Statement::closeCursor() is deprecated, use Result::free() instead.'
        );

        return $this->stmt->closeCursor();
    }

    /**
     * Returns the number of columns in the result set.
     *
     * @return int
     */
    public function columnCount()
    {
        return $this->stmt->columnCount();
    }

    /**
     * Fetches the SQLSTATE associated with the last operation on the statement.
     *
     * @deprecated The error information is available via exceptions.
     *
     * @return string|int|bool
     */
    public function errorCode()
    {
        Deprecation::triggerIfCalledFromOutside(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/3507',
            'Connection::errorCode() is deprecated, use getCode() or getSQLState() on Exception instead.'
        );

        return $this->stmt->errorCode();
    }

    /**
     * {@inheritDoc}
     *
     * @deprecated The error information is available via exceptions.
     */
    public function errorInfo()
    {
        Deprecation::triggerIfCalledFromOutside(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/3507',
            'Connection::errorInfo() is deprecated, use getCode() or getSQLState() on Exception instead.'
        );

        return $this->stmt->errorInfo();
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use one of the fetch- or iterate-related methods.
     */
    public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null)
    {
        Deprecation::triggerIfCalledFromOutside(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/4019',
            'Statement::setFetchMode() is deprecated, use explicit Result::fetch*() APIs instead.'
        );

        if ($arg2 === null) {
            return $this->stmt->setFetchMode($fetchMode);
        }

        if ($arg3 === null) {
            return $this->stmt->setFetchMode($fetchMode, $arg2);
        }

        return $this->stmt->setFetchMode($fetchMode, $arg2, $arg3);
    }

    /**
     * Required by interface IteratorAggregate.
     *
     * @deprecated Use iterateNumeric(), iterateAssociative() or iterateColumn() instead.
     *
     * {@inheritdoc}
     */
    #[ReturnTypeWillChange]
    public function getIterator()
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/4019',
            'Statement::getIterator() is deprecated, use Result::iterateNumeric(), iterateAssociative() ' .
            'or iterateColumn() instead.'
        );

        return $this->stmt;
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use fetchNumeric(), fetchAssociative() or fetchOne() instead.
     */
    public function fetch($fetchMode = null, $cursorOrientation = PDO::FETCH_ORI_NEXT, $cursorOffset = 0)
    {
        Deprecation::triggerIfCalledFromOutside(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/4019',
            'Statement::fetch() is deprecated, use Result::fetchNumeric(), fetchAssociative() or fetchOne() instead.'
        );

        return $this->stmt->fetch(...func_get_args());
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use fetchAllNumeric(), fetchAllAssociative() or fetchFirstColumn() instead.
     */
    public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null)
    {
        Deprecation::triggerIfCalledFromOutside(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/4019',
            'Statement::fetchAll() is deprecated, use Result::fetchAllNumeric(), fetchAllAssociative() or ' .
            'fetchFirstColumn() instead.'
        );

        if ($ctorArgs !== null) {
            return $this->stmt->fetchAll($fetchMode, $fetchArgument, $ctorArgs);
        }

        if ($fetchArgument !== null) {
            return $this->stmt->fetchAll($fetchMode, $fetchArgument);
        }

        return $this->stmt->fetchAll($fetchMode);
    }

    /**
     * {@inheritDoc}
     *
     * @deprecated Use fetchOne() instead.
     */
    public function fetchColumn($columnIndex = 0)
    {
        Deprecation::triggerIfCalledFromOutside(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/4019',
            'Statement::fetchColumn() is deprecated, use Result::fetchOne() instead.'
        );

        return $this->stmt->fetchColumn($columnIndex);
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use Result::fetchNumeric() instead
     *
     * @throws Exception
     */
    public function fetchNumeric()
    {
        Deprecation::triggerIfCalledFromOutside(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/issues/4554',
            'Statement::%s() is deprecated, use Result::%s() instead.',
            __FUNCTION__,
            __FUNCTION__
        );

        try {
            if ($this->stmt instanceof Result) {
                return $this->stmt->fetchNumeric();
            }

            return $this->stmt->fetch(FetchMode::NUMERIC);
        } catch (Exception $e) {
            $this->conn->handleDriverException($e);
        }
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use Result::fetchAssociative() instead
     *
     * @throws Exception
     */
    public function fetchAssociative()
    {
        Deprecation::triggerIfCalledFromOutside(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/issues/4554',
            'Statement::%s() is deprecated, use Result::%s() instead.',
            __FUNCTION__,
            __FUNCTION__
        );

        try {
            if ($this->stmt instanceof Result) {
                return $this->stmt->fetchAssociative();
            }

            return $this->stmt->fetch(FetchMode::ASSOCIATIVE);
        } catch (Exception $e) {
            $this->conn->handleDriverException($e);
        }
    }

    /**
     * {@inheritDoc}
     *
     * @deprecated Use Result::fetchOne() instead
     *
     * @throws Exception
     */
    public function fetchOne()
    {
        Deprecation::triggerIfCalledFromOutside(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/issues/4554',
            'Statement::%s() is deprecated, use Result::%s() instead.',
            __FUNCTION__,
            __FUNCTION__
        );

        try {
            if ($this->stmt instanceof Result) {
                return $this->stmt->fetchOne();
            }

            return $this->stmt->fetch(FetchMode::COLUMN);
        } catch (Exception $e) {
            $this->conn->handleDriverException($e);
        }
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use Result::fetchAllNumeric() instead
     *
     * @throws Exception
     */
    public function fetchAllNumeric(): array
    {
        Deprecation::triggerIfCalledFromOutside(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/issues/4554',
            'Statement::%s() is deprecated, use Result::%s() instead.',
            __FUNCTION__,
            __FUNCTION__
        );

        try {
            if ($this->stmt instanceof Result) {
                return $this->stmt->fetchAllNumeric();
            }

            return $this->stmt->fetchAll(FetchMode::NUMERIC);
        } catch (Exception $e) {
            $this->conn->handleDriverException($e);
        }
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use Result::fetchAllAssociative() instead
     *
     * @throws Exception
     */
    public function fetchAllAssociative(): array
    {
        Deprecation::triggerIfCalledFromOutside(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/issues/4554',
            'Statement::%s() is deprecated, use Result::%s() instead.',
            __FUNCTION__,
            __FUNCTION__
        );

        try {
            if ($this->stmt instanceof Result) {
                return $this->stmt->fetchAllAssociative();
            }

            return $this->stmt->fetchAll(FetchMode::ASSOCIATIVE);
        } catch (Exception $e) {
            $this->conn->handleDriverException($e);
        }
    }

    /**
     * Returns an associative array with the keys mapped to the first column and the values mapped to the second column.
     *
     * The result must contain at least two columns.
     *
     * @deprecated Use Result::fetchAllKeyValue() instead
     *
     * @return array<mixed,mixed>
     *
     * @throws Exception
     */
    public function fetchAllKeyValue(): array
    {
        Deprecation::triggerIfCalledFromOutside(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/issues/4554',
            'Statement::%s() is deprecated, use Result::%s() instead.',
            __FUNCTION__,
            __FUNCTION__
        );

        $this->ensureHasKeyValue();

        $data = [];

        foreach ($this->fetchAllNumeric() as [$key, $value]) {
            $data[$key] = $value;
        }

        return $data;
    }

    /**
     * Returns an associative array with the keys mapped to the first column and the values being
     * an associative array representing the rest of the columns and their values.
     *
     * @deprecated Use Result::fetchAllAssociativeIndexed() instead
     *
     * @return array<mixed,array<string,mixed>>
     *
     * @throws Exception
     */
    public function fetchAllAssociativeIndexed(): array
    {
        Deprecation::triggerIfCalledFromOutside(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/issues/4554',
            'Statement::%s() is deprecated, use Result::%s() instead.',
            __FUNCTION__,
            __FUNCTION__
        );

        $data = [];

        foreach ($this->fetchAll(FetchMode::ASSOCIATIVE) as $row) {
            $data[array_shift($row)] = $row;
        }

        return $data;
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use Result::fetchFirstColumn() instead
     *
     * @throws Exception
     */
    public function fetchFirstColumn(): array
    {
        Deprecation::triggerIfCalledFromOutside(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/issues/4554',
            'Statement::%s() is deprecated, use Result::%s() instead.',
            __FUNCTION__,
            __FUNCTION__
        );

        try {
            if ($this->stmt instanceof Result) {
                return $this->stmt->fetchFirstColumn();
            }

            return $this->stmt->fetchAll(FetchMode::COLUMN);
        } catch (Exception $e) {
            $this->conn->handleDriverException($e);
        }
    }

    /**
     * {@inheritDoc}
     *
     * @deprecated Use Result::iterateNumeric() instead
     *
     * @return Traversable<int,array<int,mixed>>
     *
     * @throws Exception
     */
    public function iterateNumeric(): Traversable
    {
        Deprecation::triggerIfCalledFromOutside(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/issues/4554',
            'Statement::%s() is deprecated, use Result::%s() instead.',
            __FUNCTION__,
            __FUNCTION__
        );

        try {
            if ($this->stmt instanceof Result) {
                while (($row = $this->stmt->fetchNumeric()) !== false) {
                    yield $row;
                }
            } else {
                while (($row = $this->stmt->fetch(FetchMode::NUMERIC)) !== false) {
                    yield $row;
                }
            }
        } catch (Exception $e) {
            $this->conn->handleDriverException($e);
        }
    }

    /**
     * {@inheritDoc}
     *
     * @deprecated Use Result::iterateAssociative() instead
     *
     * @return Traversable<int,array<string,mixed>>
     *
     * @throws Exception
     */
    public function iterateAssociative(): Traversable
    {
        Deprecation::triggerIfCalledFromOutside(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/issues/4554',
            'Statement::%s() is deprecated, use Result::%s() instead.',
            __FUNCTION__,
            __FUNCTION__
        );

        try {
            if ($this->stmt instanceof Result) {
                while (($row = $this->stmt->fetchAssociative()) !== false) {
                    yield $row;
                }
            } else {
                while (($row = $this->stmt->fetch(FetchMode::ASSOCIATIVE)) !== false) {
                    yield $row;
                }
            }
        } catch (Exception $e) {
            $this->conn->handleDriverException($e);
        }
    }

    /**
     * Returns an iterator over the result set with the keys mapped to the first column
     * and the values mapped to the second column.
     *
     * The result must contain at least two columns.
     *
     * @deprecated Use Result::iterateKeyValue() instead
     *
     * @return Traversable<mixed,mixed>
     *
     * @throws Exception
     */
    public function iterateKeyValue(): Traversable
    {
        Deprecation::triggerIfCalledFromOutside(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/issues/4554',
            'Statement::%s() is deprecated, use Result::%s() instead.',
            __FUNCTION__,
            __FUNCTION__
        );

        $this->ensureHasKeyValue();

        foreach ($this->iterateNumeric() as [$key, $value]) {
            yield $key => $value;
        }
    }

    /**
     * Returns an iterator over the result set with the keys mapped to the first column and the values being
     * an associative array representing the rest of the columns and their values.
     *
     * @deprecated Use Result::iterateAssociativeIndexed() instead
     *
     * @return Traversable<mixed,array<string,mixed>>
     *
     * @throws Exception
     */
    public function iterateAssociativeIndexed(): Traversable
    {
        Deprecation::triggerIfCalledFromOutside(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/issues/4554',
            'Statement::%s() is deprecated, use Result::%s() instead.',
            __FUNCTION__,
            __FUNCTION__
        );

        while (($row = $this->stmt->fetch(FetchMode::ASSOCIATIVE)) !== false) {
            yield array_shift($row) => $row;
        }
    }

    /**
     * {@inheritDoc}
     *
     * @deprecated Use Result::iterateColumn() instead
     *
     * @return Traversable<int,mixed>
     *
     * @throws Exception
     */
    public function iterateColumn(): Traversable
    {
        Deprecation::triggerIfCalledFromOutside(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/issues/4554',
            'Statement::%s() is deprecated, use Result::%s() instead.',
            __FUNCTION__,
            __FUNCTION__
        );

        try {
            if ($this->stmt instanceof Result) {
                while (($value = $this->stmt->fetchOne()) !== false) {
                    yield $value;
                }
            } else {
                while (($value = $this->stmt->fetch(FetchMode::COLUMN)) !== false) {
                    yield $value;
                }
            }
        } catch (Exception $e) {
            $this->conn->handleDriverException($e);
        }
    }

    /**
     * Returns the number of rows affected by the last execution of this statement.
     *
     * @return int|string The number of affected rows.
     */
    public function rowCount()
    {
        return $this->stmt->rowCount();
    }

    public function free(): void
    {
        if ($this->stmt instanceof Result) {
            $this->stmt->free();

            return;
        }

        $this->stmt->closeCursor();
    }

    /**
     * Gets the wrapped driver statement.
     *
     * @return \Doctrine\DBAL\Driver\Statement
     */
    public function getWrappedStatement()
    {
        return $this->stmt;
    }

    private function ensureHasKeyValue(): void
    {
        $columnCount = $this->columnCount();

        if ($columnCount < 2) {
            throw NoKeyValue::fromColumnCount($columnCount);
        }
    }
}
<?php

namespace Doctrine\DBAL;

/**
 * Container for all DBAL events.
 *
 * This class cannot be instantiated.
 */
final class Events
{
    /**
     * Private constructor. This class cannot be instantiated.
     *
     * @codeCoverageIgnore
     */
    private function __construct()
    {
    }

    public const postConnect = 'postConnect';

    public const onSchemaCreateTable            = 'onSchemaCreateTable';
    public const onSchemaCreateTableColumn      = 'onSchemaCreateTableColumn';
    public const onSchemaDropTable              = 'onSchemaDropTable';
    public const onSchemaAlterTable             = 'onSchemaAlterTable';
    public const onSchemaAlterTableAddColumn    = 'onSchemaAlterTableAddColumn';
    public const onSchemaAlterTableRemoveColumn = 'onSchemaAlterTableRemoveColumn';
    public const onSchemaAlterTableChangeColumn = 'onSchemaAlterTableChangeColumn';
    public const onSchemaAlterTableRenameColumn = 'onSchemaAlterTableRenameColumn';
    public const onSchemaColumnDefinition       = 'onSchemaColumnDefinition';
    public const onSchemaIndexDefinition        = 'onSchemaIndexDefinition';
}
<?php

namespace Doctrine\DBAL;

use PDO;

/**
 * Contains statement fetch modes.
 *
 * @deprecated Use one of the fetch- or iterate-related methods on the Statement.
 */
final class FetchMode
{
    /**
     * Specifies that the fetch method shall return each row as an array indexed
     * by column name as returned in the corresponding result set. If the result
     * set contains multiple columns with the same name, the statement returns
     * only a single value per column name.
     *
     * @see \PDO::FETCH_ASSOC
     */
    public const ASSOCIATIVE = PDO::FETCH_ASSOC;

    /**
     * Specifies that the fetch method shall return each row as an array indexed
     * by column number as returned in the corresponding result set, starting at
     * column 0.
     *
     * @see \PDO::FETCH_NUM
     */
    public const NUMERIC = PDO::FETCH_NUM;

    /**
     * Specifies that the fetch method shall return each row as an array indexed
     * by both column name and number as returned in the corresponding result set,
     * starting at column 0.
     *
     * @see \PDO::FETCH_BOTH
     */
    public const MIXED = PDO::FETCH_BOTH;

    /**
     * Specifies that the fetch method shall return each row as an object with
     * property names that correspond to the column names returned in the result
     * set.
     *
     * @see \PDO::FETCH_OBJ
     */
    public const STANDARD_OBJECT = PDO::FETCH_OBJ;

    /**
     * Specifies that the fetch method shall return only a single requested
     * column from the next row in the result set.
     *
     * @see \PDO::FETCH_COLUMN
     */
    public const COLUMN = PDO::FETCH_COLUMN;

    /**
     * Specifies that the fetch method shall return a new instance of the
     * requested class, mapping the columns to named properties in the class.
     *
     * @see \PDO::FETCH_CLASS
     */
    public const CUSTOM_OBJECT = PDO::FETCH_CLASS;

    /**
     * This class cannot be instantiated.
     *
     * @codeCoverageIgnore
     */
    private function __construct()
    {
    }
}
<?php

namespace Doctrine\DBAL;

use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Schema\AbstractSchemaManager;

/**
 * Driver interface.
 * Interface that all DBAL drivers must implement.
 */
interface Driver
{
    /**
     * Attempts to create a connection with the database.
     *
     * The usage of NULL to indicate empty username or password is deprecated. Use an empty string instead.
     *
     * @param mixed[]     $params        All connection parameters passed by the user.
     * @param string|null $username      The username to use when connecting.
     * @param string|null $password      The password to use when connecting.
     * @param mixed[]     $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 = []);

    /**
     * Gets the DatabasePlatform instance that provides all the metadata about
     * the platform this driver connects to.
     *
     * @return 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.
     *
     * @return AbstractSchemaManager
     */
    public function getSchemaManager(Connection $conn);

    /**
     * Gets the name of the driver.
     *
     * @deprecated
     *
     * @return string The name of the driver.
     */
    public function getName();

    /**
     * Gets the name of the database connected to for this driver.
     *
     * @deprecated Use Connection::getDatabase() instead.
     *
     * @return string The name of the database.
     */
    public function getDatabase(Connection $conn);
}
<?php

namespace Doctrine\DBAL\Id;

use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
use Doctrine\DBAL\Schema\Index;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\Sequence;
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Schema\Visitor\Visitor;

class TableGeneratorSchemaVisitor implements Visitor
{
    /** @var string */
    private $generatorTableName;

    /**
     * @param string $generatorTableName
     */
    public function __construct($generatorTableName = 'sequences')
    {
        $this->generatorTableName = $generatorTableName;
    }

    /**
     * {@inheritdoc}
     */
    public function acceptSchema(Schema $schema)
    {
        $table = $schema->createTable($this->generatorTableName);
        $table->addColumn('sequence_name', 'string');
        $table->addColumn('sequence_value', 'integer', ['default' => 1]);
        $table->addColumn('sequence_increment_by', 'integer', ['default' => 1]);
    }

    /**
     * {@inheritdoc}
     */
    public function acceptTable(Table $table)
    {
    }

    /**
     * {@inheritdoc}
     */
    public function acceptColumn(Table $table, Column $column)
    {
    }

    /**
     * {@inheritdoc}
     */
    public function acceptForeignKey(Table $localTable, ForeignKeyConstraint $fkConstraint)
    {
    }

    /**
     * {@inheritdoc}
     */
    public function acceptIndex(Table $table, Index $index)
    {
    }

    /**
     * {@inheritdoc}
     */
    public function acceptSequence(Sequence $sequence)
    {
    }
}
<?php

namespace Doctrine\DBAL\Id;

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Exception;
use Doctrine\DBAL\LockMode;
use Throwable;

use function array_change_key_case;
use function assert;
use function is_int;

use const CASE_LOWER;

/**
 * Table ID Generator for those poor languages that are missing sequences.
 *
 * WARNING: The Table Id Generator clones a second independent database
 * connection to work correctly. This means using the generator requests that
 * generate IDs will have two open database connections. This is necessary to
 * be safe from transaction failures in the main connection. Make sure to only
 * ever use one TableGenerator otherwise you end up with many connections.
 *
 * TableID Generator does not work with SQLite.
 *
 * The TableGenerator does not take care of creating the SQL Table itself. You
 * should look at the `TableGeneratorSchemaVisitor` to do this for you.
 * Otherwise the schema for a table looks like:
 *
 * CREATE sequences (
 *   sequence_name VARCHAR(255) NOT NULL,
 *   sequence_value INT NOT NULL DEFAULT 1,
 *   sequence_increment_by INT NOT NULL DEFAULT 1,
 *   PRIMARY KEY (sequence_name)
 * );
 *
 * Technically this generator works as follows:
 *
 * 1. Use a robust transaction serialization level.
 * 2. Open transaction
 * 3. Acquire a read lock on the table row (SELECT .. FOR UPDATE)
 * 4. Increment current value by one and write back to database
 * 5. Commit transaction
 *
 * If you are using a sequence_increment_by value that is larger than one the
 * ID Generator will keep incrementing values until it hits the incrementation
 * gap before issuing another query.
 *
 * If no row is present for a given sequence a new one will be created with the
 * default values 'value' = 1 and 'increment_by' = 1
 */
class TableGenerator
{
    /** @var Connection */
    private $conn;

    /** @var string */
    private $generatorTableName;

    /** @var mixed[][] */
    private $sequences = [];

    /**
     * @param string $generatorTableName
     *
     * @throws Exception
     */
    public function __construct(Connection $conn, $generatorTableName = 'sequences')
    {
        if ($conn->getDriver() instanceof Driver\PDOSqlite\Driver) {
            throw new Exception('Cannot use TableGenerator with SQLite.');
        }

        $this->conn = DriverManager::getConnection(
            $conn->getParams(),
            $conn->getConfiguration(),
            $conn->getEventManager()
        );

        $this->generatorTableName = $generatorTableName;
    }

    /**
     * Generates the next unused value for the given sequence name.
     *
     * @param string $sequence
     *
     * @return int
     *
     * @throws Exception
     */
    public function nextValue($sequence)
    {
        if (isset($this->sequences[$sequence])) {
            $value = $this->sequences[$sequence]['value'];
            $this->sequences[$sequence]['value']++;
            if ($this->sequences[$sequence]['value'] >= $this->sequences[$sequence]['max']) {
                unset($this->sequences[$sequence]);
            }

            return $value;
        }

        $this->conn->beginTransaction();

        try {
            $platform = $this->conn->getDatabasePlatform();
            $sql      = 'SELECT sequence_value, sequence_increment_by'
                . ' FROM ' . $platform->appendLockHint($this->generatorTableName, LockMode::PESSIMISTIC_WRITE)
                . ' WHERE sequence_name = ? ' . $platform->getWriteLockSQL();
            $row      = $this->conn->fetchAssociative($sql, [$sequence]);

            if ($row !== false) {
                $row = array_change_key_case($row, CASE_LOWER);

                $value = $row['sequence_value'];
                $value++;

                assert(is_int($value));

                if ($row['sequence_increment_by'] > 1) {
                    $this->sequences[$sequence] = [
                        'value' => $value,
                        'max' => $row['sequence_value'] + $row['sequence_increment_by'],
                    ];
                }

                $sql  = 'UPDATE ' . $this->generatorTableName . ' ' .
                       'SET sequence_value = sequence_value + sequence_increment_by ' .
                       'WHERE sequence_name = ? AND sequence_value = ?';
                $rows = $this->conn->executeStatement($sql, [$sequence, $row['sequence_value']]);

                if ($rows !== 1) {
                    throw new Exception('Race-condition detected while updating sequence. Aborting generation');
                }
            } else {
                $this->conn->insert(
                    $this->generatorTableName,
                    ['sequence_name' => $sequence, 'sequence_value' => 1, 'sequence_increment_by' => 1]
                );
                $value = 1;
            }

            $this->conn->commit();
        } catch (Throwable $e) {
            $this->conn->rollBack();

            throw new Exception(
                'Error occurred while generating ID with TableGenerator, aborted generation: ' . $e->getMessage(),
                0,
                $e
            );
        }

        return $value;
    }
}
<?php

namespace Doctrine\DBAL\Tools\Console\Command;

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Platforms\Keywords\DB2Keywords;
use Doctrine\DBAL\Platforms\Keywords\KeywordList;
use Doctrine\DBAL\Platforms\Keywords\MySQL57Keywords;
use Doctrine\DBAL\Platforms\Keywords\MySQL80Keywords;
use Doctrine\DBAL\Platforms\Keywords\MySQLKeywords;
use Doctrine\DBAL\Platforms\Keywords\OracleKeywords;
use Doctrine\DBAL\Platforms\Keywords\PostgreSQL91Keywords;
use Doctrine\DBAL\Platforms\Keywords\PostgreSQL92Keywords;
use Doctrine\DBAL\Platforms\Keywords\PostgreSQLKeywords;
use Doctrine\DBAL\Platforms\Keywords\ReservedKeywordsValidator;
use Doctrine\DBAL\Platforms\Keywords\SQLAnywhere11Keywords;
use Doctrine\DBAL\Platforms\Keywords\SQLAnywhere12Keywords;
use Doctrine\DBAL\Platforms\Keywords\SQLAnywhere16Keywords;
use Doctrine\DBAL\Platforms\Keywords\SQLAnywhereKeywords;
use Doctrine\DBAL\Platforms\Keywords\SQLiteKeywords;
use Doctrine\DBAL\Platforms\Keywords\SQLServer2005Keywords;
use Doctrine\DBAL\Platforms\Keywords\SQLServer2008Keywords;
use Doctrine\DBAL\Platforms\Keywords\SQLServer2012Keywords;
use Doctrine\DBAL\Platforms\Keywords\SQLServerKeywords;
use Doctrine\DBAL\Tools\Console\ConnectionProvider;
use Doctrine\Deprecations\Deprecation;
use Exception;
use InvalidArgumentException;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

use function array_keys;
use function assert;
use function count;
use function implode;
use function is_array;
use function is_string;

class ReservedWordsCommand extends Command
{
    /** @var array<string,class-string<KeywordList>> */
    private $keywordListClasses = [
        'mysql'         => MySQLKeywords::class,
        'mysql57'       => MySQL57Keywords::class,
        'mysql80'       => MySQL80Keywords::class,
        'sqlserver'     => SQLServerKeywords::class,
        'sqlserver2005' => SQLServer2005Keywords::class,
        'sqlserver2008' => SQLServer2008Keywords::class,
        'sqlserver2012' => SQLServer2012Keywords::class,
        'sqlite'        => SQLiteKeywords::class,
        'pgsql'         => PostgreSQLKeywords::class,
        'pgsql91'       => PostgreSQL91Keywords::class,
        'pgsql92'       => PostgreSQL92Keywords::class,
        'oracle'        => OracleKeywords::class,
        'db2'           => DB2Keywords::class,
        'sqlanywhere'   => SQLAnywhereKeywords::class,
        'sqlanywhere11' => SQLAnywhere11Keywords::class,
        'sqlanywhere12' => SQLAnywhere12Keywords::class,
        'sqlanywhere16' => SQLAnywhere16Keywords::class,
    ];

    /** @var ConnectionProvider|null */
    private $connectionProvider;

    public function __construct(?ConnectionProvider $connectionProvider = null)
    {
        parent::__construct();
        $this->connectionProvider = $connectionProvider;
        if ($connectionProvider !== null) {
            return;
        }

        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/3956',
            'Not passing a connection provider as the first constructor argument is deprecated'
        );
    }

    /**
     * If you want to add or replace a keywords list use this command.
     *
     * @param string                    $name
     * @param class-string<KeywordList> $class
     *
     * @return void
     */
    public function setKeywordListClass($name, $class)
    {
        $this->keywordListClasses[$name] = $class;
    }

    /** @return void */
    protected function configure()
    {
        $this
        ->setName('dbal:reserved-words')
        ->setDescription('Checks if the current database contains identifiers that are reserved.')
        ->setDefinition([
            new InputOption('connection', null, InputOption::VALUE_REQUIRED, 'The named database connection'),
            new InputOption(
                'list',
                'l',
                InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY,
                'Keyword-List name.'
            ),
        ])
        ->setHelp(<<<EOT
Checks if the current database contains tables and columns
with names that are identifiers in this dialect or in other SQL dialects.

By default SQLite, MySQL, PostgreSQL, Microsoft SQL Server, Oracle
and SQL Anywhere keywords are checked:

    <info>%command.full_name%</info>

If you want to check against specific dialects you can
pass them to the command:

    <info>%command.full_name% -l mysql -l pgsql</info>

The following keyword lists are currently shipped with Doctrine:

    * mysql
    * mysql57
    * mysql80
    * pgsql
    * pgsql92
    * sqlite
    * oracle
    * sqlserver
    * sqlserver2005
    * sqlserver2008
    * sqlserver2012
    * sqlanywhere
    * sqlanywhere11
    * sqlanywhere12
    * sqlanywhere16
    * db2 (Not checked by default)
EOT
        );
    }

    /**
     * {@inheritdoc}
     *
     * @return int
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $conn = $this->getConnection($input);

        $keywordLists = $input->getOption('list');

        if (is_string($keywordLists)) {
            $keywordLists = [$keywordLists];
        } elseif (! is_array($keywordLists)) {
            $keywordLists = [];
        }

        if (! $keywordLists) {
            $keywordLists = [
                'mysql',
                'mysql57',
                'mysql80',
                'pgsql',
                'pgsql92',
                'sqlite',
                'oracle',
                'sqlserver',
                'sqlserver2005',
                'sqlserver2008',
                'sqlserver2012',
                'sqlanywhere',
                'sqlanywhere11',
                'sqlanywhere12',
                'sqlanywhere16',
            ];
        }

        $keywords = [];
        foreach ($keywordLists as $keywordList) {
            if (! isset($this->keywordListClasses[$keywordList])) {
                throw new InvalidArgumentException(
                    "There exists no keyword list with name '" . $keywordList . "'. " .
                    'Known lists: ' . implode(', ', array_keys($this->keywordListClasses))
                );
            }

            $class      = $this->keywordListClasses[$keywordList];
            $keywords[] = new $class();
        }

        $output->write(
            'Checking keyword violations for <comment>' . implode(', ', $keywordLists) . '</comment>...',
            true
        );

        $schema  = $conn->getSchemaManager()->createSchema();
        $visitor = new ReservedKeywordsValidator($keywords);
        $schema->visit($visitor);

        $violations = $visitor->getViolations();
        if (count($violations) !== 0) {
            $output->write(
                'There are <error>' . count($violations) . '</error> reserved keyword violations'
                    . ' in your database schema:',
                true
            );

            foreach ($violations as $violation) {
                $output->write('  - ' . $violation, true);
            }

            return 1;
        }

        $output->write('No reserved keywords violations have been found!', true);

        return 0;
    }

    private function getConnection(InputInterface $input): Connection
    {
        $connectionName = $input->getOption('connection');
        assert(is_string($connectionName) || $connectionName === null);

        if ($this->connectionProvider === null) {
            if ($connectionName !== null) {
                throw new Exception('Specifying a connection is only supported when a ConnectionProvider is used.');
            }

            return $this->getHelper('db')->getConnection();
        }

        if ($connectionName !== null) {
            return $this->connectionProvider->getConnection($connectionName);
        }

        return $this->connectionProvider->getDefaultConnection();
    }
}
<?php

namespace Doctrine\DBAL\Tools\Console\Command;

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Tools\Console\ConnectionProvider;
use Doctrine\DBAL\Tools\Dumper;
use Doctrine\Deprecations\Deprecation;
use Exception;
use LogicException;
use RuntimeException;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

use function assert;
use function is_numeric;
use function is_string;
use function stripos;

/**
 * Task for executing arbitrary SQL that can come from a file or directly from
 * the command line.
 */
class RunSqlCommand extends Command
{
    /** @var ConnectionProvider|null */
    private $connectionProvider;

    public function __construct(?ConnectionProvider $connectionProvider = null)
    {
        parent::__construct();
        $this->connectionProvider = $connectionProvider;
        if ($connectionProvider !== null) {
            return;
        }

        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/3956',
            'Not passing a connection provider as the first constructor argument is deprecated'
        );
    }

    /** @return void */
    protected function configure()
    {
        $this
        ->setName('dbal:run-sql')
        ->setDescription('Executes arbitrary SQL directly from the command line.')
        ->setDefinition([
            new InputOption('connection', null, InputOption::VALUE_REQUIRED, 'The named database connection'),
            new InputArgument('sql', InputArgument::REQUIRED, 'The SQL statement to execute.'),
            new InputOption('depth', null, InputOption::VALUE_REQUIRED, 'Dumping depth of result set.', '7'),
            new InputOption('force-fetch', null, InputOption::VALUE_NONE, 'Forces fetching the result.'),
        ])
        ->setHelp(<<<EOT
The <info>%command.name%</info> command executes the given SQL query and
outputs the results:

<info>php %command.full_name% "SELECT * FROM users"</info>
EOT
        );
    }

    /**
     * {@inheritdoc}
     *
     * @return int
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $conn = $this->getConnection($input);

        $sql = $input->getArgument('sql');

        if ($sql === null) {
            throw new RuntimeException("Argument 'SQL' is required in order to execute this command correctly.");
        }

        assert(is_string($sql));

        $depth = $input->getOption('depth');

        if (! is_numeric($depth)) {
            throw new LogicException("Option 'depth' must contains an integer value");
        }

        if (stripos($sql, 'select') === 0 || $input->getOption('force-fetch')) {
            $resultSet = $conn->fetchAllAssociative($sql);
        } else {
            $resultSet = $conn->executeStatement($sql);
        }

        $output->write(Dumper::dump($resultSet, (int) $depth));

        return 0;
    }

    private function getConnection(InputInterface $input): Connection
    {
        $connectionName = $input->getOption('connection');
        assert(is_string($connectionName) || $connectionName === null);

        if ($this->connectionProvider === null) {
            if ($connectionName !== null) {
                throw new Exception('Specifying a connection is only supported when a ConnectionProvider is used.');
            }

            return $this->getHelper('db')->getConnection();
        }

        if ($connectionName !== null) {
            return $this->connectionProvider->getConnection($connectionName);
        }

        return $this->connectionProvider->getDefaultConnection();
    }
}
<?php

namespace Doctrine\DBAL\Tools\Console\Command;

use Doctrine\DBAL\Driver\PDO\Connection as PDOConnection;
use Doctrine\DBAL\Driver\PDO\Statement as PDOStatement;
use InvalidArgumentException;
use PDOException;
use RuntimeException;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

use function assert;
use function error_get_last;
use function file_exists;
use function file_get_contents;
use function is_readable;
use function realpath;
use function sprintf;

use const PHP_EOL;

/**
 * Task for executing arbitrary SQL that can come from a file or directly from
 * the command line.
 *
 * @deprecated Use a database client application instead
 */
class ImportCommand extends Command
{
    /** @return void */
    protected function configure()
    {
        $this
        ->setName('dbal:import')
        ->setDescription('Import SQL file(s) directly to Database.')
        ->setDefinition([new InputArgument(
            'file',
            InputArgument::REQUIRED | InputArgument::IS_ARRAY,
            'File path(s) of SQL to be executed.'
        ),
        ])
        ->setHelp(<<<EOT
Import SQL file(s) directly to Database.
EOT
        );
    }

    /**
     * {@inheritdoc}
     *
     * @return int
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $conn = $this->getHelper('db')->getConnection();

        $fileNames = $input->getArgument('file');

        if ($fileNames === null) {
            return 0;
        }

        foreach ((array) $fileNames as $fileName) {
            $filePath = realpath($fileName);

            // Phar compatibility.
            if ($filePath === false) {
                $filePath = $fileName;
            }

            if (! file_exists($filePath)) {
                throw new InvalidArgumentException(
                    sprintf("SQL file '<info>%s</info>' does not exist.", $filePath)
                );
            }

            if (! is_readable($filePath)) {
                throw new InvalidArgumentException(
                    sprintf("SQL file '<info>%s</info>' does not have read permissions.", $filePath)
                );
            }

            $output->write(sprintf("Processing file '<info>%s</info>'... ", $filePath));
            $sql = @file_get_contents($filePath);

            if ($sql === false) {
                $message = sprintf("Unable to read SQL file '<info>%s</info>'", $filePath);
                $error   = error_get_last();

                if ($error !== null) {
                    $message .= ': ' . $error['message'];
                }

                throw new RuntimeException($message);
            }

            if ($conn instanceof PDOConnection) {
                // PDO Drivers
                try {
                    $lines = 0;

                    $stmt = $conn->prepare($sql);
                    assert($stmt instanceof PDOStatement);

                    $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) {
                    $error = $stmt->errorInfo();

                    $output->write('error!' . PHP_EOL);

                    throw new RuntimeException($error[2], $error[0]);
                }

                $output->writeln('OK!' . PHP_EOL);

                $stmt->closeCursor();
            }
        }

        return 0;
    }
}
<?php

namespace Doctrine\DBAL\Tools\Console\Helper;

use Doctrine\DBAL\Connection;
use Symfony\Component\Console\Helper\Helper;

/**
 * Doctrine CLI Connection Helper.
 *
 * @deprecated use a ConnectionProvider instead.
 */
class ConnectionHelper extends Helper
{
    /**
     * The Doctrine database Connection.
     *
     * @var Connection
     */
    protected $_connection;

    /**
     * @param Connection $connection The Doctrine database Connection.
     */
    public function __construct(Connection $connection)
    {
        $this->_connection = $connection;
    }

    /**
     * Retrieves the Doctrine database Connection.
     *
     * @return Connection
     */
    public function getConnection()
    {
        return $this->_connection;
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'connection';
    }
}
<?php

namespace Doctrine\DBAL\Tools\Console;

use Doctrine\DBAL\Connection;

interface ConnectionProvider
{
    public function getDefaultConnection(): Connection;

    /**
     * @throws ConnectionNotFound in case a connection with the given name does not exist.
     */
    public function getConnection(string $name): Connection;
}
<?php

namespace Doctrine\DBAL\Tools\Console;

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Tools\Console\Command\ImportCommand;
use Doctrine\DBAL\Tools\Console\Command\ReservedWordsCommand;
use Doctrine\DBAL\Tools\Console\Command\RunSqlCommand;
use Doctrine\DBAL\Tools\Console\Helper\ConnectionHelper;
use Doctrine\DBAL\Version;
use Doctrine\Deprecations\Deprecation;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\HelperSet;
use TypeError;

use function sprintf;

/**
 * Handles running the Console Tools inside Symfony Console context.
 */
class ConsoleRunner
{
    /**
     * Create a Symfony Console HelperSet
     *
     * @deprecated use a ConnectionProvider instead.
     *
     * @return HelperSet
     */
    public static function createHelperSet(Connection $connection)
    {
        return new HelperSet([
            'db' => new ConnectionHelper($connection),
        ]);
    }

    /**
     * Runs console with the given connection provider or helperset (deprecated).
     *
     * @param ConnectionProvider|HelperSet $helperSetOrConnectionProvider
     * @param Command[]                    $commands
     *
     * @return void
     */
    public static function run($helperSetOrConnectionProvider, $commands = [])
    {
        $cli = new Application('Doctrine Command Line Interface', Version::VERSION);

        $cli->setCatchExceptions(true);

        $connectionProvider = null;
        if ($helperSetOrConnectionProvider instanceof HelperSet) {
            Deprecation::trigger(
                'doctrine/dbal',
                'https://github.com/doctrine/dbal/pull/3956',
                'Passing an instance of "%s" as the first argument is deprecated. Pass an instance of "%s" instead.',
                HelperSet::class,
                ConnectionProvider::class
            );
            $connectionProvider = null;
            $cli->setHelperSet($helperSetOrConnectionProvider);
        } elseif ($helperSetOrConnectionProvider instanceof ConnectionProvider) {
            $connectionProvider = $helperSetOrConnectionProvider;
        } else {
            throw new TypeError(sprintf(
                'First argument must be an instance of "%s" or "%s"',
                HelperSet::class,
                ConnectionProvider::class
            ));
        }

        self::addCommands($cli, $connectionProvider);

        $cli->addCommands($commands);
        $cli->run();
    }

    /**
     * @return void
     */
    public static function addCommands(Application $cli, ?ConnectionProvider $connectionProvider = null)
    {
        $cli->addCommands([
            new RunSqlCommand($connectionProvider),
            new ImportCommand(),
            new ReservedWordsCommand($connectionProvider),
        ]);
    }

    /**
     * Prints the instructions to create a configuration file
     *
     * @return void
     */
    public static function printCliConfigTemplate()
    {
        echo <<<'HELP'
You are missing a "cli-config.php" or "config/cli-config.php" file in your
project, which is required to get the Doctrine-DBAL Console working. You can use the
following sample as a template:

<?php
use Doctrine\DBAL\Tools\Console\ConnectionProvider\SingleConnectionProvider;

// You can append new commands to $commands array, if needed

// replace with the mechanism to retrieve DBAL connection(s) in your app
// and return a Doctrine\DBAL\Tools\Console\ConnectionProvider instance.
$connection = getDBALConnection();

// in case you have a single connection you can use SingleConnectionProvider
// otherwise you need to implement the Doctrine\DBAL\Tools\Console\ConnectionProvider interface with your custom logic
return new SingleConnectionProvider($connection);

HELP;
    }
}
<?php

namespace Doctrine\DBAL\Tools\Console\ConnectionProvider;

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Tools\Console\ConnectionNotFound;
use Doctrine\DBAL\Tools\Console\ConnectionProvider;

use function sprintf;

class SingleConnectionProvider implements ConnectionProvider
{
    /** @var Connection */
    private $connection;

    /** @var string */
    private $defaultConnectionName;

    public function __construct(Connection $connection, string $defaultConnectionName = 'default')
    {
        $this->connection            = $connection;
        $this->defaultConnectionName = $defaultConnectionName;
    }

    public function getDefaultConnection(): Connection
    {
        return $this->connection;
    }

    public function getConnection(string $name): Connection
    {
        if ($name !== $this->defaultConnectionName) {
            throw new ConnectionNotFound(sprintf('Connection with name "%s" does not exist.', $name));
        }

        return $this->connection;
    }
}
<?php

namespace Doctrine\DBAL\Tools\Console;

use OutOfBoundsException;

final class ConnectionNotFound extends OutOfBoundsException
{
}
<?php

namespace Doctrine\DBAL\Tools;

use ArrayIterator;
use ArrayObject;
use DateTimeInterface;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Persistence\Proxy;
use stdClass;

use function array_keys;
use function assert;
use function class_exists;
use function count;
use function end;
use function explode;
use function extension_loaded;
use function get_class;
use function html_entity_decode;
use function ini_set;
use function is_array;
use function is_object;
use function is_string;
use function ob_get_clean;
use function ob_start;
use function strip_tags;
use function strlen;
use function strrpos;
use function substr;
use function var_dump;

/**
 * Static class used to dump the variable to be used on output.
 * Simplified port of Util\Debug from doctrine/common.
 *
 * @internal
 */
final class Dumper
{
    /**
     * Private constructor (prevents instantiation).
     *
     * @codeCoverageIgnore
     */
    private function __construct()
    {
    }

    /**
     * Returns a dump of the public, protected and private properties of $var.
     *
     * @link https://xdebug.org/
     *
     * @param mixed $var      The variable to dump.
     * @param int   $maxDepth The maximum nesting level for object properties.
     */
    public static function dump($var, int $maxDepth = 2): string
    {
        $html = ini_set('html_errors', '1');
        assert(is_string($html));

        if (extension_loaded('xdebug')) {
            ini_set('xdebug.var_display_max_depth', (string) $maxDepth);
        }

        $var = self::export($var, $maxDepth);

        ob_start();
        var_dump($var);

        try {
            $output = ob_get_clean();
            assert(is_string($output));

            return strip_tags(html_entity_decode($output));
        } finally {
            ini_set('html_errors', $html);
        }
    }

    /**
     * @param mixed $var
     *
     * @return mixed
     */
    public static function export($var, int $maxDepth)
    {
        $return = null;
        $isObj  = is_object($var);

        if ($var instanceof Collection) {
            $var = $var->toArray();
        }

        if ($maxDepth === 0) {
            return is_object($var) ? get_class($var)
                : (is_array($var) ? 'Array(' . count($var) . ')' : $var);
        }

        if (is_array($var)) {
            $return = [];

            foreach ($var as $k => $v) {
                $return[$k] = self::export($v, $maxDepth - 1);
            }

            return $return;
        }

        if (! $isObj) {
            return $var;
        }

        $return = new stdClass();
        if ($var instanceof DateTimeInterface) {
            $return->__CLASS__ = get_class($var);
            $return->date      = $var->format('c');
            $return->timezone  = $var->getTimezone()->getName();

            return $return;
        }

        $return->__CLASS__ = self::getClass($var);

        if ($var instanceof Proxy) {
            $return->__IS_PROXY__          = true;
            $return->__PROXY_INITIALIZED__ = $var->__isInitialized();
        }

        if ($var instanceof ArrayObject || $var instanceof ArrayIterator) {
            $return->__STORAGE__ = self::export($var->getArrayCopy(), $maxDepth - 1);
        }

        return self::fillReturnWithClassAttributes($var, $return, $maxDepth);
    }

    /**
     * Fill the $return variable with class attributes
     * Based on obj2array function from {@see https://secure.php.net/manual/en/function.get-object-vars.php#47075}
     *
     * @param object $var
     *
     * @return mixed
     */
    private static function fillReturnWithClassAttributes($var, stdClass $return, int $maxDepth)
    {
        $clone = (array) $var;

        foreach (array_keys($clone) as $key) {
            $aux  = explode("\0", $key);
            $name = end($aux);
            if ($aux[0] === '') {
                $name .= ':' . ($aux[1] === '*' ? 'protected' : $aux[1] . ':private');
            }

            $return->$name = self::export($clone[$key], $maxDepth - 1);
        }

        return $return;
    }

    /**
     * @param object $object
     */
    private static function getClass($object): string
    {
        $class = get_class($object);

        if (! class_exists(Proxy::class)) {
            return $class;
        }

        $pos = strrpos($class, '\\' . Proxy::MARKER . '\\');

        if ($pos === false) {
            return $class;
        }

        return substr($class, $pos + strlen(Proxy::MARKER) + 2);
    }
}
<?php

namespace Doctrine\DBAL\Sharding\SQLAzure\Schema;

use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
use Doctrine\DBAL\Schema\Index;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\Sequence;
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Schema\Visitor\Visitor;
use RuntimeException;

use function in_array;

/**
 * Converts a single tenant schema into a multi-tenant schema for SQL Azure
 * Federations under the following assumptions:
 *
 * - Every table is part of the multi-tenant application, only explicitly
 *   excluded tables are non-federated. The behavior of the tables being in
 *   global or federated database is undefined. It depends on you selecting a
 *   federation before DDL statements or not.
 * - Every Primary key of a federated table is extended by another column
 *   'tenant_id' with a default value of the SQLAzure function
 *   `federation_filtering_value('tenant_id')`.
 * - You always have to work with `filtering=On` when using federations with this
 *   multi-tenant approach.
 * - Primary keys are either using globally unique ids (GUID, Table Generator)
 *   or you explicitly add the tenant_id in every UPDATE or DELETE statement
 *   (otherwise they will affect the same-id rows from other tenants as well).
 *   SQLAzure throws errors when you try to create IDENTIY columns on federated
 *   tables.
 *
 * @deprecated
 */
class MultiTenantVisitor implements Visitor
{
    /** @var string[] */
    private $excludedTables = [];

    /** @var string */
    private $tenantColumnName;

    /** @var string */
    private $tenantColumnType = 'integer';

    /**
     * Name of the federation distribution, defaulting to the tenantColumnName
     * if not specified.
     *
     * @var string
     */
    private $distributionName;

    /**
     * @param string[]    $excludedTables
     * @param string      $tenantColumnName
     * @param string|null $distributionName
     */
    public function __construct(array $excludedTables = [], $tenantColumnName = 'tenant_id', $distributionName = null)
    {
        $this->excludedTables   = $excludedTables;
        $this->tenantColumnName = $tenantColumnName;
        $this->distributionName = $distributionName ?: $tenantColumnName;
    }

    /**
     * {@inheritdoc}
     */
    public function acceptTable(Table $table)
    {
        if (in_array($table->getName(), $this->excludedTables)) {
            return;
        }

        $table->addColumn($this->tenantColumnName, $this->tenantColumnType, [
            'default' => "federation_filtering_value('" . $this->distributionName . "')",
        ]);

        $clusteredIndex = $this->getClusteredIndex($table);

        $indexColumns   = $clusteredIndex->getColumns();
        $indexColumns[] = $this->tenantColumnName;

        if ($clusteredIndex->isPrimary()) {
            $table->dropPrimaryKey();
            $table->setPrimaryKey($indexColumns);
        } else {
            $table->dropIndex($clusteredIndex->getName());
            $table->addIndex($indexColumns, $clusteredIndex->getName());
            $table->getIndex($clusteredIndex->getName())->addFlag('clustered');
        }
    }

    /**
     * @param Table $table
     *
     * @return Index
     *
     * @throws RuntimeException
     */
    private function getClusteredIndex($table)
    {
        foreach ($table->getIndexes() as $index) {
            if ($index->isPrimary() && ! $index->hasFlag('nonclustered')) {
                return $index;
            }

            if ($index->hasFlag('clustered')) {
                return $index;
            }
        }

        throw new RuntimeException('No clustered index found on table ' . $table->getName());
    }

    /**
     * {@inheritdoc}
     */
    public function acceptSchema(Schema $schema)
    {
    }

    /**
     * {@inheritdoc}
     */
    public function acceptColumn(Table $table, Column $column)
    {
    }

    /**
     * {@inheritdoc}
     */
    public function acceptForeignKey(Table $localTable, ForeignKeyConstraint $fkConstraint)
    {
    }

    /**
     * {@inheritdoc}
     */
    public function acceptIndex(Table $table, Index $index)
    {
    }

    /**
     * {@inheritdoc}
     */
    public function acceptSequence(Sequence $sequence)
    {
    }
}
<?php

namespace Doctrine\DBAL\Sharding\SQLAzure;

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Sharding\ShardingException;
use Doctrine\DBAL\Sharding\ShardManager;
use Doctrine\DBAL\Types\Type;
use Doctrine\Deprecations\Deprecation;
use RuntimeException;

use function sprintf;

/**
 * Sharding using the SQL Azure Federations support.
 *
 * @deprecated
 */
class SQLAzureShardManager implements ShardManager
{
    /** @var string */
    private $federationName;

    /** @var bool */
    private $filteringEnabled;

    /** @var string */
    private $distributionKey;

    /** @var string */
    private $distributionType;

    /** @var Connection */
    private $conn;

    /** @var string|null */
    private $currentDistributionValue;

    /**
     * @throws ShardingException
     */
    public function __construct(Connection $conn)
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/issues/3595',
            'Native Sharding support in DBAL is removed without replacement.'
        );

        $this->conn = $conn;
        $params     = $conn->getParams();

        if (! isset($params['sharding']['federationName'])) {
            throw ShardingException::missingDefaultFederationName();
        }

        if (! isset($params['sharding']['distributionKey'])) {
            throw ShardingException::missingDefaultDistributionKey();
        }

        if (! isset($params['sharding']['distributionType'])) {
            throw ShardingException::missingDistributionType();
        }

        $this->federationName   = $params['sharding']['federationName'];
        $this->distributionKey  = $params['sharding']['distributionKey'];
        $this->distributionType = $params['sharding']['distributionType'];
        $this->filteringEnabled = (bool) ($params['sharding']['filteringEnabled'] ?? false);
    }

    /**
     * Gets the name of the federation.
     *
     * @return string
     */
    public function getFederationName()
    {
        return $this->federationName;
    }

    /**
     * Gets the distribution key.
     *
     * @return string
     */
    public function getDistributionKey()
    {
        return $this->distributionKey;
    }

    /**
     * Gets the Doctrine Type name used for the distribution.
     *
     * @return string
     */
    public function getDistributionType()
    {
        return $this->distributionType;
    }

    /**
     * Sets Enabled/Disable filtering on the fly.
     *
     * @param bool $flag
     *
     * @return void
     */
    public function setFilteringEnabled($flag)
    {
        $this->filteringEnabled = (bool) $flag;
    }

    /**
     * {@inheritDoc}
     */
    public function selectGlobal()
    {
        if ($this->conn->isTransactionActive()) {
            throw ShardingException::activeTransaction();
        }

        $sql = 'USE FEDERATION ROOT WITH RESET';
        $this->conn->exec($sql);
        $this->currentDistributionValue = null;
    }

    /**
     * {@inheritDoc}
     */
    public function selectShard($distributionValue)
    {
        if ($this->conn->isTransactionActive()) {
            throw ShardingException::activeTransaction();
        }

        $platform = $this->conn->getDatabasePlatform();
        $sql      = sprintf(
            'USE FEDERATION %s (%s = %s) WITH RESET, FILTERING = %s;',
            $platform->quoteIdentifier($this->federationName),
            $platform->quoteIdentifier($this->distributionKey),
            $this->conn->quote($distributionValue),
            ($this->filteringEnabled ? 'ON' : 'OFF')
        );

        $this->conn->exec($sql);
        $this->currentDistributionValue = $distributionValue;
    }

    /**
     * {@inheritDoc}
     */
    public function getCurrentDistributionValue()
    {
        return $this->currentDistributionValue;
    }

    /**
     * {@inheritDoc}
     */
    public function getShards()
    {
        $sql = 'SELECT member_id as id,
                      distribution_name as distribution_key,
                      CAST(range_low AS CHAR) AS rangeLow,
                      CAST(range_high AS CHAR) AS rangeHigh
                      FROM sys.federation_member_distributions d
                      INNER JOIN sys.federations f ON f.federation_id = d.federation_id
                      WHERE f.name = ' . $this->conn->quote($this->federationName);

        return $this->conn->fetchAllAssociative($sql);
    }

     /**
      * {@inheritDoc}
      */
    public function queryAll($sql, array $params = [], array $types = [])
    {
        $shards = $this->getShards();
        if (! $shards) {
            throw new RuntimeException('No shards found for ' . $this->federationName);
        }

        $result          = [];
        $oldDistribution = $this->getCurrentDistributionValue();

        foreach ($shards as $shard) {
            $this->selectShard($shard['rangeLow']);
            foreach ($this->conn->fetchAllAssociative($sql, $params, $types) as $row) {
                $result[] = $row;
            }
        }

        if ($oldDistribution === null) {
            $this->selectGlobal();
        } else {
            $this->selectShard($oldDistribution);
        }

        return $result;
    }

    /**
     * Splits Federation at a given distribution value.
     *
     * @param mixed $splitDistributionValue
     *
     * @return void
     */
    public function splitFederation($splitDistributionValue)
    {
        $type = Type::getType($this->distributionType);

        $sql = 'ALTER FEDERATION ' . $this->getFederationName() . ' ' .
               'SPLIT AT (' . $this->getDistributionKey() . ' = ' .
               $this->conn->quote($splitDistributionValue, $type->getBindingType()) . ')';
        $this->conn->exec($sql);
    }
}
<?php

namespace Doctrine\DBAL\Sharding\SQLAzure;

use Closure;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\Synchronizer\AbstractSchemaSynchronizer;
use Doctrine\DBAL\Schema\Synchronizer\SchemaSynchronizer;
use Doctrine\DBAL\Schema\Synchronizer\SingleDatabaseSynchronizer;
use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Types\Types;
use RuntimeException;

use function array_merge;

/**
 * SQL Azure Schema Synchronizer.
 *
 * Will iterate over all shards when performing schema operations. This is done
 * by partitioning the passed schema into subschemas for the federation and the
 * global database and then applying the operations step by step using the
 * {@see \Doctrine\DBAL\Schema\Synchronizer\SingleDatabaseSynchronizer}.
 *
 * @deprecated
 */
class SQLAzureFederationsSynchronizer extends AbstractSchemaSynchronizer
{
    public const FEDERATION_TABLE_FEDERATED   = 'azure.federated';
    public const FEDERATION_DISTRIBUTION_NAME = 'azure.federatedOnDistributionName';

    /** @var SQLAzureShardManager */
    private $shardManager;

    /** @var SchemaSynchronizer */
    private $synchronizer;

    public function __construct(Connection $conn, SQLAzureShardManager $shardManager, ?SchemaSynchronizer $sync = null)
    {
        parent::__construct($conn);
        $this->shardManager = $shardManager;
        $this->synchronizer = $sync ?: new SingleDatabaseSynchronizer($conn);
    }

    /**
     * {@inheritdoc}
     */
    public function getCreateSchema(Schema $createSchema)
    {
        $sql = [];

        [$global, $federation] = $this->partitionSchema($createSchema);

        $globalSql = $this->synchronizer->getCreateSchema($global);
        if ($globalSql) {
            $sql[] = "-- Create Root Federation\n" .
                     'USE FEDERATION ROOT WITH RESET;';
            $sql   = array_merge($sql, $globalSql);
        }

        $federationSql = $this->synchronizer->getCreateSchema($federation);

        if ($federationSql) {
            $defaultValue = $this->getFederationTypeDefaultValue();

            $sql[] = $this->getCreateFederationStatement();
            $sql[] = 'USE FEDERATION ' . $this->shardManager->getFederationName()
                . ' (' . $this->shardManager->getDistributionKey() . ' = ' . $defaultValue . ')'
                . ' WITH RESET, FILTERING = OFF;';
            $sql   = array_merge($sql, $federationSql);
        }

        return $sql;
    }

    /**
     * {@inheritdoc}
     */
    public function getUpdateSchema(Schema $toSchema, $noDrops = false)
    {
        return $this->work($toSchema, static function ($synchronizer, $schema) use ($noDrops) {
            return $synchronizer->getUpdateSchema($schema, $noDrops);
        });
    }

    /**
     * {@inheritdoc}
     */
    public function getDropSchema(Schema $dropSchema)
    {
        return $this->work($dropSchema, static function ($synchronizer, $schema) {
            return $synchronizer->getDropSchema($schema);
        });
    }

    /**
     * {@inheritdoc}
     */
    public function createSchema(Schema $createSchema)
    {
        $this->processSql($this->getCreateSchema($createSchema));
    }

    /**
     * {@inheritdoc}
     */
    public function updateSchema(Schema $toSchema, $noDrops = false)
    {
        $this->processSql($this->getUpdateSchema($toSchema, $noDrops));
    }

    /**
     * {@inheritdoc}
     */
    public function dropSchema(Schema $dropSchema)
    {
        $this->processSqlSafely($this->getDropSchema($dropSchema));
    }

    /**
     * {@inheritdoc}
     */
    public function getDropAllSchema()
    {
        $this->shardManager->selectGlobal();
        $globalSql = $this->synchronizer->getDropAllSchema();

        $sql = [];

        if ($globalSql) {
            $sql[] = "-- Work on Root Federation\nUSE FEDERATION ROOT WITH RESET;";
            $sql   = array_merge($sql, $globalSql);
        }

        $shards = $this->shardManager->getShards();
        foreach ($shards as $shard) {
            $this->shardManager->selectShard($shard['rangeLow']);

            $federationSql = $this->synchronizer->getDropAllSchema();
            if (! $federationSql) {
                continue;
            }

            $sql[] = '-- Work on Federation ID ' . $shard['id'] . "\n" .
                     'USE FEDERATION ' . $this->shardManager->getFederationName()
                . ' (' . $this->shardManager->getDistributionKey() . ' = ' . $shard['rangeLow'] . ')'
                . ' WITH RESET, FILTERING = OFF;';
            $sql   = array_merge($sql, $federationSql);
        }

        $sql[] = 'USE FEDERATION ROOT WITH RESET;';
        $sql[] = 'DROP FEDERATION ' . $this->shardManager->getFederationName();

        return $sql;
    }

    /**
     * {@inheritdoc}
     */
    public function dropAllSchema()
    {
        $this->processSqlSafely($this->getDropAllSchema());
    }

    /**
     * @return Schema[]
     */
    private function partitionSchema(Schema $schema)
    {
        return [
            $this->extractSchemaFederation($schema, false),
            $this->extractSchemaFederation($schema, true),
        ];
    }

    /**
     * @param bool $isFederation
     *
     * @return Schema
     *
     * @throws RuntimeException
     */
    private function extractSchemaFederation(Schema $schema, $isFederation)
    {
        $partitionedSchema = clone $schema;

        foreach ($partitionedSchema->getTables() as $table) {
            if ($isFederation) {
                $table->addOption(self::FEDERATION_DISTRIBUTION_NAME, $this->shardManager->getDistributionKey());
            }

            if ($table->hasOption(self::FEDERATION_TABLE_FEDERATED) !== $isFederation) {
                $partitionedSchema->dropTable($table->getName());
            } else {
                foreach ($table->getForeignKeys() as $fk) {
                    $foreignTable = $schema->getTable($fk->getForeignTableName());
                    if ($foreignTable->hasOption(self::FEDERATION_TABLE_FEDERATED) !== $isFederation) {
                        throw new RuntimeException('Cannot have foreign key between global/federation.');
                    }
                }
            }
        }

        return $partitionedSchema;
    }

    /**
     * Work on the Global/Federation based on currently existing shards and
     * perform the given operation on the underlying schema synchronizer given
     * the different partitioned schema instances.
     *
     * @return string[]
     */
    private function work(Schema $schema, Closure $operation)
    {
        [$global, $federation] = $this->partitionSchema($schema);
        $sql                   = [];

        $this->shardManager->selectGlobal();
        $globalSql = $operation($this->synchronizer, $global);

        if ($globalSql) {
            $sql[] = "-- Work on Root Federation\nUSE FEDERATION ROOT WITH RESET;";
            $sql   = array_merge($sql, $globalSql);
        }

        $shards = $this->shardManager->getShards();

        foreach ($shards as $shard) {
            $this->shardManager->selectShard($shard['rangeLow']);

            $federationSql = $operation($this->synchronizer, $federation);
            if (! $federationSql) {
                continue;
            }

            $sql[] = '-- Work on Federation ID ' . $shard['id'] . "\n"
                . 'USE FEDERATION ' . $this->shardManager->getFederationName()
                . ' (' . $this->shardManager->getDistributionKey() . ' = ' . $shard['rangeLow'] . ')'
                . ' WITH RESET, FILTERING = OFF;';
            $sql   = array_merge($sql, $federationSql);
        }

        return $sql;
    }

    /**
     * @return string
     */
    private function getFederationTypeDefaultValue()
    {
        $federationType = Type::getType($this->shardManager->getDistributionType());

        switch ($federationType->getName()) {
            case Types::GUID:
                $defaultValue = '00000000-0000-0000-0000-000000000000';
                break;
            case Types::INTEGER:
            case Types::SMALLINT:
            case Types::BIGINT:
                $defaultValue = '0';
                break;
            default:
                $defaultValue = '';
                break;
        }

        return $defaultValue;
    }

    /**
     * @return string
     */
    private function getCreateFederationStatement()
    {
        $federationType    = Type::getType($this->shardManager->getDistributionType());
        $federationTypeSql = $federationType->getSQLDeclaration([], $this->conn->getDatabasePlatform());

        return "--Create Federation\n"
            . 'CREATE FEDERATION ' . $this->shardManager->getFederationName()
            . ' (' . $this->shardManager->getDistributionKey()
            . ' ' . $federationTypeSql . '  RANGE)';
    }
}
<?php

namespace Doctrine\DBAL\Sharding;

use Doctrine\Common\EventManager;
use Doctrine\DBAL\Configuration;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver\Connection as DriverConnection;
use Doctrine\DBAL\Event\ConnectionEventArgs;
use Doctrine\DBAL\Events;
use Doctrine\DBAL\Sharding\ShardChoser\ShardChoser;
use InvalidArgumentException;

use function array_merge;
use function is_numeric;
use function is_string;

/**
 * Sharding implementation that pools many different connections
 * internally and serves data from the currently active connection.
 *
 * The internals of this class are:
 *
 * - All sharding clients are specified and given a shard-id during
 *   configuration.
 * - By default, the global shard is selected. If no global shard is configured
 *   an exception is thrown on access.
 * - Selecting a shard by distribution value delegates the mapping
 *   "distributionValue" => "client" to the ShardChoser interface.
 * - An exception is thrown if trying to switch shards during an open
 *   transaction.
 *
 * Instantiation through the DriverManager looks like:
 *
 * @deprecated
 *
 * @example
 *
 * $conn = DriverManager::getConnection(array(
 *    'wrapperClass' => 'Doctrine\DBAL\Sharding\PoolingShardConnection',
 *    'driver' => 'pdo_mysql',
 *    'global' => array('user' => '', 'password' => '', 'host' => '', 'dbname' => ''),
 *    'shards' => array(
 *        array('id' => 1, 'user' => 'slave1', 'password', 'host' => '', 'dbname' => ''),
 *        array('id' => 2, 'user' => 'slave2', 'password', 'host' => '', 'dbname' => ''),
 *    ),
 *    'shardChoser' => 'Doctrine\DBAL\Sharding\ShardChoser\MultiTenantShardChoser',
 * ));
 * $shardManager = $conn->getShardManager();
 * $shardManager->selectGlobal();
 * $shardManager->selectShard($value);
 */
class PoolingShardConnection extends Connection
{
    /** @var DriverConnection[] */
    private $activeConnections = [];

    /** @var string|int|null */
    private $activeShardId;

    /** @var mixed[] */
    private $connectionParameters = [];

    /**
     * {@inheritDoc}
     *
     * @internal The connection can be only instantiated by the driver manager.
     *
     * @throws InvalidArgumentException
     */
    public function __construct(
        array $params,
        Driver $driver,
        ?Configuration $config = null,
        ?EventManager $eventManager = null
    ) {
        if (! isset($params['global'], $params['shards'])) {
            throw new InvalidArgumentException("Connection Parameters require 'global' and 'shards' configurations.");
        }

        if (! isset($params['shardChoser'])) {
            throw new InvalidArgumentException("Missing Shard Choser configuration 'shardChoser'");
        }

        if (is_string($params['shardChoser'])) {
            $params['shardChoser'] = new $params['shardChoser']();
        }

        if (! ($params['shardChoser'] instanceof ShardChoser)) {
            throw new InvalidArgumentException(
                "The 'shardChoser' configuration is not a valid instance of " . ShardChoser::class
            );
        }

        $this->connectionParameters[0] = array_merge($params, $params['global']);

        foreach ($params['shards'] as $shard) {
            if (! isset($shard['id'])) {
                throw new InvalidArgumentException(
                    "Missing 'id' for one configured shard. Please specify a unique shard-id."
                );
            }

            if (! is_numeric($shard['id']) || $shard['id'] < 1) {
                throw new InvalidArgumentException('Shard Id has to be a non-negative number.');
            }

            if (isset($this->connectionParameters[$shard['id']])) {
                throw new InvalidArgumentException('Shard ' . $shard['id'] . ' is duplicated in the configuration.');
            }

            $this->connectionParameters[$shard['id']] = array_merge($params, $shard);
        }

        parent::__construct($params, $driver, $config, $eventManager);
    }

    /**
     * Get active shard id.
     *
     * @return string|int|null
     */
    public function getActiveShardId()
    {
        return $this->activeShardId;
    }

    /**
     * {@inheritdoc}
     */
    public function getParams()
    {
        return $this->activeShardId
            ? $this->connectionParameters[$this->activeShardId]
            : $this->connectionParameters[0];
    }

    /**
     * {@inheritdoc}
     */
    public function getHost()
    {
        $params = $this->getParams();

        return $params['host'] ?? parent::getHost();
    }

    /**
     * {@inheritdoc}
     */
    public function getPort()
    {
        $params = $this->getParams();

        return $params['port'] ?? parent::getPort();
    }

    /**
     * {@inheritdoc}
     */
    public function getUsername()
    {
        $params = $this->getParams();

        return $params['user'] ?? parent::getUsername();
    }

    /**
     * {@inheritdoc}
     */
    public function getPassword()
    {
        $params = $this->getParams();

        return $params['password'] ?? parent::getPassword();
    }

    /**
     * Connects to a given shard.
     *
     * @param string|int|null $shardId
     *
     * @return bool
     *
     * @throws ShardingException
     */
    public function connect($shardId = null)
    {
        if ($shardId === null && $this->_conn) {
            return false;
        }

        if ($shardId !== null && $shardId === $this->activeShardId) {
            return false;
        }

        if ($this->getTransactionNestingLevel() > 0) {
            throw new ShardingException('Cannot switch shard when transaction is active.');
        }

        $activeShardId = $this->activeShardId = (int) $shardId;

        if (isset($this->activeConnections[$activeShardId])) {
            $this->_conn = $this->activeConnections[$activeShardId];

            return false;
        }

        $this->_conn = $this->activeConnections[$activeShardId] = $this->connectTo($activeShardId);

        if ($this->_eventManager->hasListeners(Events::postConnect)) {
            $eventArgs = new ConnectionEventArgs($this);
            $this->_eventManager->dispatchEvent(Events::postConnect, $eventArgs);
        }

        return true;
    }

    /**
     * Connects to a specific connection.
     *
     * @param string|int $shardId
     *
     * @return \Doctrine\DBAL\Driver\Connection
     */
    protected function connectTo($shardId)
    {
        $params = $this->getParams();

        $driverOptions = $params['driverOptions'] ?? [];

        $connectionParams = $this->connectionParameters[$shardId];

        $user     = $connectionParams['user'] ?? null;
        $password = $connectionParams['password'] ?? null;

        return $this->_driver->connect($connectionParams, $user, $password, $driverOptions);
    }

    /**
     * @param string|int|null $shardId
     *
     * @return bool
     */
    public function isConnected($shardId = null)
    {
        if ($shardId === null) {
            return $this->_conn !== null;
        }

        return isset($this->activeConnections[$shardId]);
    }

    /**
     * @return void
     */
    public function close()
    {
        $this->_conn             = null;
        $this->activeConnections = [];
        $this->activeShardId     = null;
    }
}
<?php

namespace Doctrine\DBAL\Sharding;

/**
 * Sharding Manager gives access to APIs to implementing sharding on top of
 * Doctrine\DBAL\Connection instances.
 *
 * For simplicity and developer ease-of-use (and understanding) the sharding
 * API only covers single shard queries, no fan-out support. It is primarily
 * suited for multi-tenant applications.
 *
 * The assumption about sharding here
 * is that a distribution value can be found that gives access to all the
 * necessary data for all use-cases. Switching between shards should be done with
 * caution, especially if lazy loading is implemented. Any query is always
 * executed against the last shard that was selected. If a query is created for
 * a shard Y but then a shard X is selected when its actually executed you
 * will hit the wrong shard.
 *
 * @deprecated
 */
interface ShardManager
{
    /**
     * Selects global database with global data.
     *
     * This is the default database that is connected when no shard is
     * selected.
     *
     * @return void
     */
    public function selectGlobal();

    /**
     * Selects the shard against which the queries after this statement will be issued.
     *
     * @param string $distributionValue
     *
     * @return void
     *
     * @throws ShardingException If no value is passed as shard identifier.
     */
    public function selectShard($distributionValue);

    /**
     * Gets the distribution value currently used for sharding.
     *
     * @return string|null
     */
    public function getCurrentDistributionValue();

    /**
     * Gets information about the amount of shards and other details.
     *
     * Format is implementation specific, each shard is one element and has an
     * 'id' attribute at least.
     *
     * @return mixed[][]
     */
    public function getShards();

    /**
     * Queries all shards in undefined order and return the results appended to
     * each other. Restore the previous distribution value after execution.
     *
     * Using {@link Connection::fetchAll()} to retrieve rows internally.
     *
     * @param string         $sql
     * @param mixed[]        $params
     * @param int[]|string[] $types
     *
     * @return mixed[]
     */
    public function queryAll($sql, array $params, array $types);
}
<?php

namespace Doctrine\DBAL\Sharding\ShardChoser;

use Doctrine\DBAL\Sharding\PoolingShardConnection;

/**
 * The MultiTenant Shard choser assumes that the distribution value directly
 * maps to the shard id.
 *
 * @deprecated
 */
class MultiTenantShardChoser implements ShardChoser
{
    /**
     * {@inheritdoc}
     */
    public function pickShard($distributionValue, PoolingShardConnection $conn)
    {
        return $distributionValue;
    }
}
<?php

namespace Doctrine\DBAL\Sharding\ShardChoser;

use Doctrine\DBAL\Sharding\PoolingShardConnection;

/**
 * Given a distribution value this shard-choser strategy will pick the shard to
 * connect to for retrieving rows with the distribution value.
 *
 * @deprecated
 */
interface ShardChoser
{
    /**
     * Picks a shard for the given distribution value.
     *
     * @param string|int $distributionValue
     *
     * @return string|int
     */
    public function pickShard($distributionValue, PoolingShardConnection $conn);
}
<?php

namespace Doctrine\DBAL\Sharding;

use Doctrine\DBAL\Sharding\ShardChoser\ShardChoser;
use Doctrine\Deprecations\Deprecation;
use RuntimeException;

/**
 * Shard Manager for the Connection Pooling Shard Strategy
 *
 * @deprecated
 */
class PoolingShardManager implements ShardManager
{
    /** @var PoolingShardConnection */
    private $conn;

    /** @var ShardChoser */
    private $choser;

    /** @var string|null */
    private $currentDistributionValue;

    public function __construct(PoolingShardConnection $conn)
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/issues/3595',
            'Native Sharding support in DBAL is removed without replacement.'
        );

        $params       = $conn->getParams();
        $this->conn   = $conn;
        $this->choser = $params['shardChoser'];
    }

    /**
     * {@inheritDoc}
     */
    public function selectGlobal()
    {
        $this->conn->connect(0);
        $this->currentDistributionValue = null;
    }

    /**
     * {@inheritDoc}
     */
    public function selectShard($distributionValue)
    {
        $shardId = $this->choser->pickShard($distributionValue, $this->conn);
        $this->conn->connect($shardId);
        $this->currentDistributionValue = $distributionValue;
    }

    /**
     * {@inheritDoc}
     */
    public function getCurrentDistributionValue()
    {
        return $this->currentDistributionValue;
    }

    /**
     * {@inheritDoc}
     */
    public function getShards()
    {
        $params = $this->conn->getParams();
        $shards = [];

        foreach ($params['shards'] as $shard) {
            $shards[] = ['id' => $shard['id']];
        }

        return $shards;
    }

    /**
     * {@inheritDoc}
     *
     * @throws RuntimeException
     */
    public function queryAll($sql, array $params, array $types)
    {
        $shards = $this->getShards();
        if (! $shards) {
            throw new RuntimeException('No shards found.');
        }

        $result          = [];
        $oldDistribution = $this->getCurrentDistributionValue();

        foreach ($shards as $shard) {
            $this->conn->connect($shard['id']);
            foreach ($this->conn->fetchAllAssociative($sql, $params, $types) as $row) {
                $result[] = $row;
            }
        }

        if ($oldDistribution === null) {
            $this->selectGlobal();
        } else {
            $this->selectShard($oldDistribution);
        }

        return $result;
    }
}
<?php

namespace Doctrine\DBAL\Sharding;

use Doctrine\DBAL\Exception;

/**
 * Sharding related Exceptions
 *
 * @deprecated
 *
 * @psalm-immutable
 */
class ShardingException extends Exception
{
    /**
     * @return ShardingException
     */
    public static function notImplemented()
    {
        return new self('This functionality is not implemented with this sharding provider.', 1331557937);
    }

    /**
     * @return ShardingException
     */
    public static function missingDefaultFederationName()
    {
        return new self('SQLAzure requires a federation name to be set during sharding configuration.', 1332141280);
    }

    /**
     * @return ShardingException
     */
    public static function missingDefaultDistributionKey()
    {
        return new self('SQLAzure requires a distribution key to be set during sharding configuration.', 1332141329);
    }

    /**
     * @return ShardingException
     */
    public static function activeTransaction()
    {
        return new self('Cannot switch shard during an active transaction.', 1332141766);
    }

    /**
     * @return ShardingException
     */
    public static function noShardDistributionValue()
    {
        return new self('You have to specify a string or integer as shard distribution value.', 1332142103);
    }

    /**
     * @return ShardingException
     */
    public static function missingDistributionType()
    {
        return new self("You have to specify a sharding distribution type such as 'integer', 'string', 'guid'.");
    }
}
<?php

namespace Doctrine\DBAL\Types;

use Doctrine\DBAL\Platforms\AbstractPlatform;

use function explode;
use function implode;
use function is_resource;
use function stream_get_contents;

/**
 * Array Type which can be used for simple values.
 *
 * Only use this type if you are sure that your values cannot contain a ",".
 */
class SimpleArrayType extends Type
{
    /**
     * {@inheritdoc}
     */
    public function getSQLDeclaration(array $column, AbstractPlatform $platform)
    {
        return $platform->getClobTypeDeclarationSQL($column);
    }

    /**
     * {@inheritdoc}
     */
    public function convertToDatabaseValue($value, AbstractPlatform $platform)
    {
        if (! $value) {
            return null;
        }

        return implode(',', $value);
    }

    /**
     * {@inheritdoc}
     */
    public function convertToPHPValue($value, AbstractPlatform $platform)
    {
        if ($value === null) {
            return [];
        }

        $value = is_resource($value) ? stream_get_contents($value) : $value;

        return explode(',', $value);
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return Types::SIMPLE_ARRAY;
    }

    /**
     * {@inheritdoc}
     */
    public function requiresSQLCommentHint(AbstractPlatform $platform)
    {
        return true;
    }
}
<?php

namespace Doctrine\DBAL\Types;

use Doctrine\DBAL\ParameterType;
use Doctrine\DBAL\Platforms\AbstractPlatform;

use function assert;
use function fopen;
use function fseek;
use function fwrite;
use function is_resource;
use function is_string;

/**
 * Type that maps ab SQL BINARY/VARBINARY to a PHP resource stream.
 */
class BinaryType extends Type
{
    /**
     * {@inheritdoc}
     */
    public function getSQLDeclaration(array $column, AbstractPlatform $platform)
    {
        return $platform->getBinaryTypeDeclarationSQL($column);
    }

    /**
     * {@inheritdoc}
     */
    public function convertToPHPValue($value, AbstractPlatform $platform)
    {
        if ($value === null) {
            return null;
        }

        if (is_string($value)) {
            $fp = fopen('php://temp', 'rb+');
            assert(is_resource($fp));
            fwrite($fp, $value);
            fseek($fp, 0);
            $value = $fp;
        }

        if (! is_resource($value)) {
            throw ConversionException::conversionFailed($value, Types::BINARY);
        }

        return $value;
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return Types::BINARY;
    }

    /**
     * {@inheritdoc}
     */
    public function getBindingType()
    {
        return ParameterType::BINARY;
    }
}
<?php

namespace Doctrine\DBAL\Types;

use Doctrine\DBAL\ParameterType;
use Doctrine\DBAL\Platforms\AbstractPlatform;

/**
 * Type that maps an SQL INT to a PHP integer.
 */
class IntegerType extends Type implements PhpIntegerMappingType
{
    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return Types::INTEGER;
    }

    /**
     * {@inheritdoc}
     */
    public function getSQLDeclaration(array $column, AbstractPlatform $platform)
    {
        return $platform->getIntegerTypeDeclarationSQL($column);
    }

    /**
     * {@inheritdoc}
     */
    public function convertToPHPValue($value, AbstractPlatform $platform)
    {
        return $value === null ? null : (int) $value;
    }

    /**
     * {@inheritdoc}
     */
    public function getBindingType()
    {
        return ParameterType::INTEGER;
    }
}
<?php

namespace Doctrine\DBAL\Types;

use Doctrine\DBAL\Exception;
use Throwable;

use function get_class;
use function gettype;
use function implode;
use function is_object;
use function is_scalar;
use function sprintf;
use function strlen;
use function substr;

/**
 * Conversion Exception is thrown when the database to PHP conversion fails.
 *
 * @psalm-immutable
 */
class ConversionException extends Exception
{
    /**
     * Thrown when a Database to Doctrine Type Conversion fails.
     *
     * @param string $value
     * @param string $toType
     *
     * @return ConversionException
     */
    public static function conversionFailed($value, $toType, ?Throwable $previous = null)
    {
        $value = strlen($value) > 32 ? substr($value, 0, 20) . '...' : $value;

        return new self('Could not convert database value "' . $value . '" to Doctrine Type ' . $toType, 0, $previous);
    }

    /**
     * Thrown when a Database to Doctrine Type Conversion fails and we can make a statement
     * about the expected format.
     *
     * @param string $value
     * @param string $toType
     * @param string $expectedFormat
     *
     * @return ConversionException
     */
    public static function conversionFailedFormat($value, $toType, $expectedFormat, ?Throwable $previous = null)
    {
        $value = strlen($value) > 32 ? substr($value, 0, 20) . '...' : $value;

        return new self(
            'Could not convert database value "' . $value . '" to Doctrine Type ' .
            $toType . '. Expected format: ' . $expectedFormat,
            0,
            $previous
        );
    }

    /**
     * Thrown when the PHP value passed to the converter was not of the expected type.
     *
     * @param mixed    $value
     * @param string   $toType
     * @param string[] $possibleTypes
     *
     * @return ConversionException
     */
    public static function conversionFailedInvalidType(
        $value,
        $toType,
        array $possibleTypes,
        ?Throwable $previous = null
    ) {
        $actualType = is_object($value) ? get_class($value) : gettype($value);

        if (is_scalar($value)) {
            return new self(sprintf(
                "Could not convert PHP value '%s' of type '%s' to type '%s'. Expected one of the following types: %s",
                $value,
                $actualType,
                $toType,
                implode(', ', $possibleTypes)
            ), 0, $previous);
        }

        return new self(sprintf(
            "Could not convert PHP value of type '%s' to type '%s'. Expected one of the following types: %s",
            $actualType,
            $toType,
            implode(', ', $possibleTypes)
        ), 0, $previous);
    }

    /**
     * @param mixed  $value
     * @param string $format
     * @param string $error
     *
     * @return ConversionException
     */
    public static function conversionFailedSerialization($value, $format, $error)
    {
        $actualType = is_object($value) ? get_class($value) : gettype($value);

        return new self(sprintf(
            "Could not convert PHP type '%s' to '%s', as an '%s' error was triggered by the serialization",
            $actualType,
            $format,
            $error
        ));
    }

    public static function conversionFailedUnserialization(string $format, string $error): self
    {
        return new self(sprintf(
            "Could not convert database value to '%s' as an error was triggered by the unserialization: '%s'",
            $format,
            $error
        ));
    }
}
<?php

namespace Doctrine\DBAL\Types;

use Doctrine\DBAL\Platforms\AbstractPlatform;

use function is_resource;
use function json_decode;
use function stream_get_contents;

/**
 * Array Type which can be used to generate json arrays.
 *
 * @deprecated Use JsonType instead
 */
class JsonArrayType extends JsonType
{
    /**
     * {@inheritdoc}
     */
    public function convertToPHPValue($value, AbstractPlatform $platform)
    {
        if ($value === null || $value === '') {
            return [];
        }

        $value = is_resource($value) ? stream_get_contents($value) : $value;

        return json_decode($value, true);
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return Types::JSON_ARRAY;
    }

    /**
     * {@inheritdoc}
     */
    public function requiresSQLCommentHint(AbstractPlatform $platform)
    {
        return true;
    }
}
<?php

namespace Doctrine\DBAL\Types;

use DateTimeImmutable;
use Doctrine\DBAL\Platforms\AbstractPlatform;

/**
 * Immutable type of {@see DateTimeTzType}.
 */
class DateTimeTzImmutableType extends DateTimeTzType
{
    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return Types::DATETIMETZ_IMMUTABLE;
    }

    /**
     * {@inheritdoc}
     */
    public function convertToDatabaseValue($value, AbstractPlatform $platform)
    {
        if ($value === null) {
            return $value;
        }

        if ($value instanceof DateTimeImmutable) {
            return $value->format($platform->getDateTimeTzFormatString());
        }

        throw ConversionException::conversionFailedInvalidType(
            $value,
            $this->getName(),
            ['null', DateTimeImmutable::class]
        );
    }

    /**
     * {@inheritdoc}
     */
    public function convertToPHPValue($value, AbstractPlatform $platform)
    {
        if ($value === null || $value instanceof DateTimeImmutable) {
            return $value;
        }

        $dateTime = DateTimeImmutable::createFromFormat($platform->getDateTimeTzFormatString(), $value);

        if (! $dateTime) {
            throw ConversionException::conversionFailedFormat(
                $value,
                $this->getName(),
                $platform->getDateTimeTzFormatString()
            );
        }

        return $dateTime;
    }

    /**
     * {@inheritdoc}
     */
    public function requiresSQLCommentHint(AbstractPlatform $platform)
    {
        return true;
    }
}
<?php

namespace Doctrine\DBAL\Types;

use DateTimeImmutable;
use Doctrine\DBAL\Platforms\AbstractPlatform;

use function date_create_immutable;

/**
 * Immutable type of {@see DateTimeType}.
 */
class DateTimeImmutableType extends DateTimeType
{
    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return Types::DATETIME_IMMUTABLE;
    }

    /**
     * {@inheritdoc}
     */
    public function convertToDatabaseValue($value, AbstractPlatform $platform)
    {
        if ($value === null) {
            return $value;
        }

        if ($value instanceof DateTimeImmutable) {
            return $value->format($platform->getDateTimeFormatString());
        }

        throw ConversionException::conversionFailedInvalidType(
            $value,
            $this->getName(),
            ['null', DateTimeImmutable::class]
        );
    }

    /**
     * {@inheritdoc}
     */
    public function convertToPHPValue($value, AbstractPlatform $platform)
    {
        if ($value === null || $value instanceof DateTimeImmutable) {
            return $value;
        }

        $dateTime = DateTimeImmutable::createFromFormat($platform->getDateTimeFormatString(), $value);

        if (! $dateTime) {
            $dateTime = date_create_immutable($value);
        }

        if (! $dateTime) {
            throw ConversionException::conversionFailedFormat(
                $value,
                $this->getName(),
                $platform->getDateTimeFormatString()
            );
        }

        return $dateTime;
    }

    /**
     * {@inheritdoc}
     */
    public function requiresSQLCommentHint(AbstractPlatform $platform)
    {
        return true;
    }
}
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Types;

/**
 * Default built-in types provided by Doctrine DBAL.
 */
final class Types
{
    public const ARRAY                = 'array';
    public const ASCII_STRING         = 'ascii_string';
    public const BIGINT               = 'bigint';
    public const BINARY               = 'binary';
    public const BLOB                 = 'blob';
    public const BOOLEAN              = 'boolean';
    public const DATE_MUTABLE         = 'date';
    public const DATE_IMMUTABLE       = 'date_immutable';
    public const DATEINTERVAL         = 'dateinterval';
    public const DATETIME_MUTABLE     = 'datetime';
    public const DATETIME_IMMUTABLE   = 'datetime_immutable';
    public const DATETIMETZ_MUTABLE   = 'datetimetz';
    public const DATETIMETZ_IMMUTABLE = 'datetimetz_immutable';
    public const DECIMAL              = 'decimal';
    public const FLOAT                = 'float';
    public const GUID                 = 'guid';
    public const INTEGER              = 'integer';
    public const JSON                 = 'json';
    public const OBJECT               = 'object';
    public const SIMPLE_ARRAY         = 'simple_array';
    public const SMALLINT             = 'smallint';
    public const STRING               = 'string';
    public const TEXT                 = 'text';
    public const TIME_MUTABLE         = 'time';
    public const TIME_IMMUTABLE       = 'time_immutable';

    /** @deprecated json_array type is deprecated, use {@see self::JSON} instead. */
    public const JSON_ARRAY = 'json_array';

    /**
     * @codeCoverageIgnore
     */
    private function __construct()
    {
    }
}
<?php

namespace Doctrine\DBAL\Types;

use Doctrine\DBAL\Platforms\AbstractPlatform;

use function is_resource;
use function restore_error_handler;
use function serialize;
use function set_error_handler;
use function stream_get_contents;
use function unserialize;

/**
 * Type that maps a PHP array to a clob SQL type.
 */
class ArrayType extends Type
{
    /**
     * {@inheritdoc}
     */
    public function getSQLDeclaration(array $column, AbstractPlatform $platform)
    {
        return $platform->getClobTypeDeclarationSQL($column);
    }

    /**
     * {@inheritdoc}
     */
    public function convertToDatabaseValue($value, AbstractPlatform $platform)
    {
        // @todo 3.0 - $value === null check to save real NULL in database
        return serialize($value);
    }

    /**
     * {@inheritdoc}
     */
    public function convertToPHPValue($value, AbstractPlatform $platform)
    {
        if ($value === null) {
            return null;
        }

        $value = is_resource($value) ? stream_get_contents($value) : $value;

        set_error_handler(function (int $code, string $message): bool {
            throw ConversionException::conversionFailedUnserialization($this->getName(), $message);
        });

        try {
            return unserialize($value);
        } finally {
            restore_error_handler();
        }
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return Types::ARRAY;
    }

    /**
     * {@inheritdoc}
     */
    public function requiresSQLCommentHint(AbstractPlatform $platform)
    {
        return true;
    }
}
<?php

namespace Doctrine\DBAL\Types;

use DateTimeImmutable;
use Doctrine\DBAL\Platforms\AbstractPlatform;

use function date_create_immutable;

/**
 * Immutable type of {@see VarDateTimeType}.
 */
class VarDateTimeImmutableType extends VarDateTimeType
{
    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return Types::DATETIME_IMMUTABLE;
    }

    /**
     * {@inheritdoc}
     */
    public function convertToDatabaseValue($value, AbstractPlatform $platform)
    {
        if ($value === null) {
            return $value;
        }

        if ($value instanceof DateTimeImmutable) {
            return $value->format($platform->getDateTimeFormatString());
        }

        throw ConversionException::conversionFailedInvalidType(
            $value,
            $this->getName(),
            ['null', DateTimeImmutable::class]
        );
    }

    /**
     * {@inheritdoc}
     */
    public function convertToPHPValue($value, AbstractPlatform $platform)
    {
        if ($value === null || $value instanceof DateTimeImmutable) {
            return $value;
        }

        $dateTime = date_create_immutable($value);

        if (! $dateTime) {
            throw ConversionException::conversionFailed($value, $this->getName());
        }

        return $dateTime;
    }

    /**
     * {@inheritdoc}
     */
    public function requiresSQLCommentHint(AbstractPlatform $platform)
    {
        return true;
    }
}
<?php

namespace Doctrine\DBAL\Types;

use Doctrine\DBAL\Platforms\AbstractPlatform;

use function is_resource;
use function json_decode;
use function json_encode;
use function json_last_error;
use function json_last_error_msg;
use function stream_get_contents;

use const JSON_ERROR_NONE;

/**
 * Type generating json objects values
 */
class JsonType extends Type
{
    /**
     * {@inheritdoc}
     */
    public function getSQLDeclaration(array $column, AbstractPlatform $platform)
    {
        return $platform->getJsonTypeDeclarationSQL($column);
    }

    /**
     * {@inheritdoc}
     */
    public function convertToDatabaseValue($value, AbstractPlatform $platform)
    {
        if ($value === null) {
            return null;
        }

        $encoded = json_encode($value);

        if (json_last_error() !== JSON_ERROR_NONE) {
            throw ConversionException::conversionFailedSerialization($value, 'json', json_last_error_msg());
        }

        return $encoded;
    }

    /**
     * {@inheritdoc}
     */
    public function convertToPHPValue($value, AbstractPlatform $platform)
    {
        if ($value === null || $value === '') {
            return null;
        }

        if (is_resource($value)) {
            $value = stream_get_contents($value);
        }

        $val = json_decode($value, true);

        if (json_last_error() !== JSON_ERROR_NONE) {
            throw ConversionException::conversionFailed($value, $this->getName());
        }

        return $val;
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return Types::JSON;
    }

    /**
     * {@inheritdoc}
     */
    public function requiresSQLCommentHint(AbstractPlatform $platform)
    {
        return ! $platform->hasNativeJsonType();
    }
}
<?php

namespace Doctrine\DBAL\Types;

use DateInterval;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Throwable;

use function substr;

/**
 * Type that maps interval string to a PHP DateInterval Object.
 */
class DateIntervalType extends Type
{
    public const FORMAT = '%RP%YY%MM%DDT%HH%IM%SS';

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return Types::DATEINTERVAL;
    }

    /**
     * {@inheritdoc}
     */
    public function getSQLDeclaration(array $column, AbstractPlatform $platform)
    {
        $column['length'] = 255;

        return $platform->getVarcharTypeDeclarationSQL($column);
    }

    /**
     * {@inheritdoc}
     */
    public function convertToDatabaseValue($value, AbstractPlatform $platform)
    {
        if ($value === null) {
            return null;
        }

        if ($value instanceof DateInterval) {
            return $value->format(self::FORMAT);
        }

        throw ConversionException::conversionFailedInvalidType($value, $this->getName(), ['null', 'DateInterval']);
    }

    /**
     * {@inheritdoc}
     */
    public function convertToPHPValue($value, AbstractPlatform $platform)
    {
        if ($value === null || $value instanceof DateInterval) {
            return $value;
        }

        $negative = false;

        if (isset($value[0]) && ($value[0] === '+' || $value[0] === '-')) {
            $negative = $value[0] === '-';
            $value    = substr($value, 1);
        }

        try {
            $interval = new DateInterval($value);

            if ($negative) {
                $interval->invert = 1;
            }

            return $interval;
        } catch (Throwable $exception) {
            throw ConversionException::conversionFailedFormat($value, $this->getName(), self::FORMAT, $exception);
        }
    }

    /**
     * {@inheritdoc}
     */
    public function requiresSQLCommentHint(AbstractPlatform $platform)
    {
        return true;
    }
}
<?php

namespace Doctrine\DBAL\Types;

use DateTime;
use DateTimeInterface;
use Doctrine\DBAL\Platforms\AbstractPlatform;

/**
 * Type that maps an SQL TIME to a PHP DateTime object.
 */
class TimeType extends Type
{
    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return Types::TIME_MUTABLE;
    }

    /**
     * {@inheritdoc}
     */
    public function getSQLDeclaration(array $column, AbstractPlatform $platform)
    {
        return $platform->getTimeTypeDeclarationSQL($column);
    }

    /**
     * {@inheritdoc}
     */
    public function convertToDatabaseValue($value, AbstractPlatform $platform)
    {
        if ($value === null) {
            return $value;
        }

        if ($value instanceof DateTimeInterface) {
            return $value->format($platform->getTimeFormatString());
        }

        throw ConversionException::conversionFailedInvalidType($value, $this->getName(), ['null', 'DateTime']);
    }

    /**
     * {@inheritdoc}
     */
    public function convertToPHPValue($value, AbstractPlatform $platform)
    {
        if ($value === null || $value instanceof DateTimeInterface) {
            return $value;
        }

        $val = DateTime::createFromFormat('!' . $platform->getTimeFormatString(), $value);
        if (! $val) {
            throw ConversionException::conversionFailedFormat(
                $value,
                $this->getName(),
                $platform->getTimeFormatString()
            );
        }

        return $val;
    }
}
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Types;

use Doctrine\DBAL\Exception;

use function array_search;
use function in_array;

/**
 * The type registry is responsible for holding a map of all known DBAL types.
 * The types are stored using the flyweight pattern so that one type only exists as exactly one instance.
 */
final class TypeRegistry
{
    /** @var array<string, Type> Map of type names and their corresponding flyweight objects. */
    private $instances;

    /**
     * @param array<string, Type> $instances
     */
    public function __construct(array $instances = [])
    {
        $this->instances = $instances;
    }

    /**
     * Finds a type by the given name.
     *
     * @throws Exception
     */
    public function get(string $name): Type
    {
        if (! isset($this->instances[$name])) {
            throw Exception::unknownColumnType($name);
        }

        return $this->instances[$name];
    }

    /**
     * Finds a name for the given type.
     *
     * @throws Exception
     */
    public function lookupName(Type $type): string
    {
        $name = $this->findTypeName($type);

        if ($name === null) {
            throw Exception::typeNotRegistered($type);
        }

        return $name;
    }

    /**
     * Checks if there is a type of the given name.
     */
    public function has(string $name): bool
    {
        return isset($this->instances[$name]);
    }

    /**
     * Registers a custom type to the type map.
     *
     * @throws Exception
     */
    public function register(string $name, Type $type): void
    {
        if (isset($this->instances[$name])) {
            throw Exception::typeExists($name);
        }

        if ($this->findTypeName($type) !== null) {
            throw Exception::typeAlreadyRegistered($type);
        }

        $this->instances[$name] = $type;
    }

    /**
     * Overrides an already defined type to use a different implementation.
     *
     * @throws Exception
     */
    public function override(string $name, Type $type): void
    {
        if (! isset($this->instances[$name])) {
            throw Exception::typeNotFound($name);
        }

        if (! in_array($this->findTypeName($type), [$name, null], true)) {
            throw Exception::typeAlreadyRegistered($type);
        }

        $this->instances[$name] = $type;
    }

    /**
     * Gets the map of all registered types and their corresponding type instances.
     *
     * @internal
     *
     * @return array<string, Type>
     */
    public function getMap(): array
    {
        return $this->instances;
    }

    private function findTypeName(Type $type): ?string
    {
        $name = array_search($type, $this->instances, true);

        if ($name === false) {
            return null;
        }

        return $name;
    }
}
<?php

namespace Doctrine\DBAL\Types;

/**
 * Implementations should map a database type to a PHP integer.
 *
 * @internal
 */
interface PhpIntegerMappingType
{
}
<?php

namespace Doctrine\DBAL\Types;

use Doctrine\DBAL\Exception;
use Doctrine\DBAL\ParameterType;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\Deprecations\Deprecation;

use function array_map;
use function get_class;
use function str_replace;
use function strrpos;
use function substr;

/**
 * The base class for so-called Doctrine mapping types.
 *
 * A Type object is obtained by calling the static {@link getType()} method.
 */
abstract class Type
{
    /** @deprecated Use {@see Types::BIGINT} instead. */
    public const BIGINT = Types::BIGINT;

    /** @deprecated Use {@see Types::BINARY} instead. */
    public const BINARY = Types::BINARY;

    /** @deprecated Use {@see Types::BLOB} instead. */
    public const BLOB = Types::BLOB;

    /** @deprecated Use {@see Types::BOOLEAN} instead. */
    public const BOOLEAN = Types::BOOLEAN;

    /** @deprecated Use {@see Types::DATE_MUTABLE} instead. */
    public const DATE = Types::DATE_MUTABLE;

    /** @deprecated Use {@see Types::DATE_IMMUTABLE} instead. */
    public const DATE_IMMUTABLE = Types::DATE_IMMUTABLE;

    /** @deprecated Use {@see Types::DATEINTERVAL} instead. */
    public const DATEINTERVAL = Types::DATEINTERVAL;

    /** @deprecated Use {@see Types::DATETIME_MUTABLE} instead. */
    public const DATETIME = Types::DATETIME_MUTABLE;

    /** @deprecated Use {@see Types::DATETIME_IMMUTABLE} instead. */
    public const DATETIME_IMMUTABLE = Types::DATETIME_IMMUTABLE;

    /** @deprecated Use {@see Types::DATETIMETZ_MUTABLE} instead. */
    public const DATETIMETZ = Types::DATETIMETZ_MUTABLE;

    /** @deprecated Use {@see Types::DATETIMETZ_IMMUTABLE} instead. */
    public const DATETIMETZ_IMMUTABLE = Types::DATETIMETZ_IMMUTABLE;

    /** @deprecated Use {@see Types::DECIMAL} instead. */
    public const DECIMAL = Types::DECIMAL;

    /** @deprecated Use {@see Types::FLOAT} instead. */
    public const FLOAT = Types::FLOAT;

    /** @deprecated Use {@see Types::GUID} instead. */
    public const GUID = Types::GUID;

    /** @deprecated Use {@see Types::INTEGER} instead. */
    public const INTEGER = Types::INTEGER;

    /** @deprecated Use {@see Types::JSON} instead. */
    public const JSON = Types::JSON;

    /** @deprecated Use {@see Types::JSON_ARRAY} instead. */
    public const JSON_ARRAY = Types::JSON_ARRAY;

    /** @deprecated Use {@see Types::OBJECT} instead. */
    public const OBJECT = Types::OBJECT;

    /** @deprecated Use {@see Types::SIMPLE_ARRAY} instead. */
    public const SIMPLE_ARRAY = Types::SIMPLE_ARRAY;

    /** @deprecated Use {@see Types::SMALLINT} instead. */
    public const SMALLINT = Types::SMALLINT;

    /** @deprecated Use {@see Types::STRING} instead. */
    public const STRING = Types::STRING;

    /** @deprecated Use {@see Types::ARRAY} instead. */
    public const TARRAY = Types::ARRAY;

    /** @deprecated Use {@see Types::TEXT} instead. */
    public const TEXT = Types::TEXT;

    /** @deprecated Use {@see Types::TIME_MUTABLE} instead. */
    public const TIME = Types::TIME_MUTABLE;

    /** @deprecated Use {@see Types::TIME_IMMUTABLE} instead. */
    public const TIME_IMMUTABLE = Types::TIME_IMMUTABLE;

    /**
     * The map of supported doctrine mapping types.
     */
    private const BUILTIN_TYPES_MAP = [
        Types::ARRAY                => ArrayType::class,
        Types::ASCII_STRING         => AsciiStringType::class,
        Types::BIGINT               => BigIntType::class,
        Types::BINARY               => BinaryType::class,
        Types::BLOB                 => BlobType::class,
        Types::BOOLEAN              => BooleanType::class,
        Types::DATE_MUTABLE         => DateType::class,
        Types::DATE_IMMUTABLE       => DateImmutableType::class,
        Types::DATEINTERVAL         => DateIntervalType::class,
        Types::DATETIME_MUTABLE     => DateTimeType::class,
        Types::DATETIME_IMMUTABLE   => DateTimeImmutableType::class,
        Types::DATETIMETZ_MUTABLE   => DateTimeTzType::class,
        Types::DATETIMETZ_IMMUTABLE => DateTimeTzImmutableType::class,
        Types::DECIMAL              => DecimalType::class,
        Types::FLOAT                => FloatType::class,
        Types::GUID                 => GuidType::class,
        Types::INTEGER              => IntegerType::class,
        Types::JSON                 => JsonType::class,
        Types::JSON_ARRAY           => JsonArrayType::class,
        Types::OBJECT               => ObjectType::class,
        Types::SIMPLE_ARRAY         => SimpleArrayType::class,
        Types::SMALLINT             => SmallIntType::class,
        Types::STRING               => StringType::class,
        Types::TEXT                 => TextType::class,
        Types::TIME_MUTABLE         => TimeType::class,
        Types::TIME_IMMUTABLE       => TimeImmutableType::class,
    ];

    /** @var TypeRegistry|null */
    private static $typeRegistry;

    /**
     * @internal Do not instantiate directly - use {@see Type::addType()} method instead.
     */
    final public 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.
     *
     * @deprecated Rely on information provided by the platform instead.
     *
     * @return int|null
     */
    public function getDefaultLength(AbstractPlatform $platform)
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/3255',
            'Type::getDefaultLength() is deprecated, use AbstractPlatform directly.'
        );

        return null;
    }

    /**
     * Gets the SQL declaration snippet for a column of this type.
     *
     * @param mixed[]          $column   The column definition
     * @param AbstractPlatform $platform The currently used database platform.
     *
     * @return string
     */
    abstract public function getSQLDeclaration(array $column, AbstractPlatform $platform);

    /**
     * Gets the name of this type.
     *
     * @return string
     *
     * @todo Needed?
     */
    abstract public function getName();

    final public static function getTypeRegistry(): TypeRegistry
    {
        if (self::$typeRegistry === null) {
            self::$typeRegistry = self::createTypeRegistry();
        }

        return self::$typeRegistry;
    }

    private static function createTypeRegistry(): TypeRegistry
    {
        $instances = [];

        foreach (self::BUILTIN_TYPES_MAP as $name => $class) {
            $instances[$name] = new $class();
        }

        return new TypeRegistry($instances);
    }

    /**
     * Factory method to create type instances.
     * Type instances are implemented as flyweights.
     *
     * @param string $name The name of the type (as returned by getName()).
     *
     * @return Type
     *
     * @throws Exception
     */
    public static function getType($name)
    {
        return self::getTypeRegistry()->get($name);
    }

    /**
     * Adds a custom type to the type map.
     *
     * @param string             $name      The name of the type. This should correspond to what getName() returns.
     * @param class-string<Type> $className The class name of the custom type.
     *
     * @return void
     *
     * @throws Exception
     */
    public static function addType($name, $className)
    {
        self::getTypeRegistry()->register($name, new $className());
    }

    /**
     * Checks if exists support for a type.
     *
     * @param string $name The name of the type.
     *
     * @return bool TRUE if type is supported; FALSE otherwise.
     */
    public static function hasType($name)
    {
        return self::getTypeRegistry()->has($name);
    }

    /**
     * Overrides an already defined type to use a different implementation.
     *
     * @param string             $name
     * @param class-string<Type> $className
     *
     * @return void
     *
     * @throws Exception
     */
    public static function overrideType($name, $className)
    {
        self::getTypeRegistry()->override($name, new $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 {@link ParameterType} constants.
     *
     * @return int
     */
    public function getBindingType()
    {
        return ParameterType::STRING;
    }

    /**
     * Gets the types array map which holds all registered types and the corresponding
     * type class
     *
     * @return array<string, string>
     */
    public static function getTypesMap()
    {
        return array_map(
            static function (Type $type): string {
                return get_class($type);
            },
            self::getTypeRegistry()->getMap()
        );
    }

    /**
     * @deprecated Relying on string representation is discouraged and will be removed in DBAL 3.0.
     *
     * @return string
     */
    public function __toString()
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/3258',
            'Type::__toString() is deprecated, use Type::getName() or get_class($type) instead.'
        );

        $type     = static::class;
        $position = strrpos($type, '\\');

        if ($position !== false) {
            $type = substr($type, $position);
        }

        return str_replace('Type', '', $type);
    }

    /**
     * Does working with this column require SQL conversion functions?
     *
     * This is a metadata function that is required for example in the ORM.
     * Usage of {@link convertToDatabaseValueSQL} and
     * {@link convertToPHPValueSQL} works for any type and mostly
     * does nothing. This method can additionally be used for optimization purposes.
     *
     * @return bool
     */
    public function canRequireSQLConversion()
    {
        return false;
    }

    /**
     * Modifies the SQL expression (identifier, parameter) to convert to a database value.
     *
     * @param string $sqlExpr
     *
     * @return string
     */
    public function convertToDatabaseValueSQL($sqlExpr, AbstractPlatform $platform)
    {
        return $sqlExpr;
    }

    /**
     * Modifies the SQL expression (identifier, parameter) to convert to a PHP value.
     *
     * @param string           $sqlExpr
     * @param AbstractPlatform $platform
     *
     * @return string
     */
    public function convertToPHPValueSQL($sqlExpr, $platform)
    {
        return $sqlExpr;
    }

    /**
     * Gets an array of database types that map to this Doctrine type.
     *
     * @return string[]
     */
    public function getMappedDatabaseTypes(AbstractPlatform $platform)
    {
        return [];
    }

    /**
     * If this Doctrine Type maps to an already mapped database type,
     * reverse schema engineering can't tell them apart. You need to mark
     * one of those types as commented, which will have Doctrine use an SQL
     * comment to typehint the actual Doctrine Type.
     *
     * @return bool
     */
    public function requiresSQLCommentHint(AbstractPlatform $platform)
    {
        return false;
    }
}
<?php

namespace Doctrine\DBAL\Types;

use Doctrine\DBAL\ParameterType;
use Doctrine\DBAL\Platforms\AbstractPlatform;

use function assert;
use function fopen;
use function fseek;
use function fwrite;
use function is_resource;
use function is_string;

/**
 * Type that maps an SQL BLOB to a PHP resource stream.
 */
class BlobType extends Type
{
    /**
     * {@inheritdoc}
     */
    public function getSQLDeclaration(array $column, AbstractPlatform $platform)
    {
        return $platform->getBlobTypeDeclarationSQL($column);
    }

    /**
     * {@inheritdoc}
     */
    public function convertToPHPValue($value, AbstractPlatform $platform)
    {
        if ($value === null) {
            return null;
        }

        if (is_string($value)) {
            $fp = fopen('php://temp', 'rb+');
            assert(is_resource($fp));
            fwrite($fp, $value);
            fseek($fp, 0);
            $value = $fp;
        }

        if (! is_resource($value)) {
            throw ConversionException::conversionFailed($value, Types::BLOB);
        }

        return $value;
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return Types::BLOB;
    }

    /**
     * {@inheritdoc}
     */
    public function getBindingType()
    {
        return ParameterType::LARGE_OBJECT;
    }
}
<?php

namespace Doctrine\DBAL\Types;

use Doctrine\DBAL\Platforms\AbstractPlatform;

use function is_float;
use function is_int;

use const PHP_VERSION_ID;

/**
 * Type that maps an SQL DECIMAL to a PHP string.
 */
class DecimalType extends Type
{
    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return Types::DECIMAL;
    }

    /**
     * {@inheritdoc}
     */
    public function getSQLDeclaration(array $column, AbstractPlatform $platform)
    {
        return $platform->getDecimalTypeDeclarationSQL($column);
    }

    /**
     * {@inheritdoc}
     */
    public function convertToPHPValue($value, AbstractPlatform $platform)
    {
        // Some drivers starting from PHP 8.1 can represent decimals as float/int
        // See also: https://github.com/doctrine/dbal/pull/4818
        if (PHP_VERSION_ID >= 80100 && (is_float($value) || is_int($value))) {
            return (string) $value;
        }

        return $value;
    }
}
<?php

namespace Doctrine\DBAL\Types;

use DateTime;
use DateTimeInterface;
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.
 */
class DateTimeTzType extends Type implements PhpDateTimeMappingType
{
    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return Types::DATETIMETZ_MUTABLE;
    }

    /**
     * {@inheritdoc}
     */
    public function getSQLDeclaration(array $column, AbstractPlatform $platform)
    {
        return $platform->getDateTimeTzTypeDeclarationSQL($column);
    }

    /**
     * {@inheritdoc}
     */
    public function convertToDatabaseValue($value, AbstractPlatform $platform)
    {
        if ($value === null) {
            return $value;
        }

        if ($value instanceof DateTimeInterface) {
            return $value->format($platform->getDateTimeTzFormatString());
        }

        throw ConversionException::conversionFailedInvalidType($value, $this->getName(), ['null', 'DateTime']);
    }

    /**
     * {@inheritdoc}
     */
    public function convertToPHPValue($value, AbstractPlatform $platform)
    {
        if ($value === null || $value instanceof DateTimeInterface) {
            return $value;
        }

        $val = DateTime::createFromFormat($platform->getDateTimeTzFormatString(), $value);
        if (! $val) {
            throw ConversionException::conversionFailedFormat(
                $value,
                $this->getName(),
                $platform->getDateTimeTzFormatString()
            );
        }

        return $val;
    }
}
<?php

namespace Doctrine\DBAL\Types;

/**
 * Implementations should map a database type to a PHP DateTimeInterface instance.
 *
 * @internal
 */
interface PhpDateTimeMappingType
{
}
<?php

namespace Doctrine\DBAL\Types;

use DateTime;
use Doctrine\DBAL\Platforms\AbstractPlatform;

use function date_create;

/**
 * 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.
 */
class VarDateTimeType extends DateTimeType
{
    /**
     * {@inheritdoc}
     */
    public function convertToPHPValue($value, AbstractPlatform $platform)
    {
        if ($value === null || $value instanceof DateTime) {
            return $value;
        }

        $val = date_create($value);
        if (! $val) {
            throw ConversionException::conversionFailed($value, $this->getName());
        }

        return $val;
    }
}
<?php

namespace Doctrine\DBAL\Types;

use Doctrine\DBAL\Platforms\AbstractPlatform;

/**
 * Type that maps an SQL VARCHAR to a PHP string.
 */
class StringType extends Type
{
    /**
     * {@inheritdoc}
     */
    public function getSQLDeclaration(array $column, AbstractPlatform $platform)
    {
        return $platform->getVarcharTypeDeclarationSQL($column);
    }

    /**
     * {@inheritdoc}
     */
    public function getDefaultLength(AbstractPlatform $platform)
    {
        return $platform->getVarcharDefaultLength();
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return Types::STRING;
    }
}
<?php

namespace Doctrine\DBAL\Types;

use Doctrine\DBAL\Platforms\AbstractPlatform;

use function is_resource;
use function restore_error_handler;
use function serialize;
use function set_error_handler;
use function stream_get_contents;
use function unserialize;

/**
 * Type that maps a PHP object to a clob SQL type.
 */
class ObjectType extends Type
{
    /**
     * {@inheritdoc}
     */
    public function getSQLDeclaration(array $column, AbstractPlatform $platform)
    {
        return $platform->getClobTypeDeclarationSQL($column);
    }

    /**
     * {@inheritdoc}
     */
    public function convertToDatabaseValue($value, AbstractPlatform $platform)
    {
        return serialize($value);
    }

    /**
     * {@inheritdoc}
     */
    public function convertToPHPValue($value, AbstractPlatform $platform)
    {
        if ($value === null) {
            return null;
        }

        $value = is_resource($value) ? stream_get_contents($value) : $value;

        set_error_handler(function (int $code, string $message): bool {
            throw ConversionException::conversionFailedUnserialization($this->getName(), $message);
        });

        try {
            return unserialize($value);
        } finally {
            restore_error_handler();
        }
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return Types::OBJECT;
    }

    /**
     * {@inheritdoc}
     */
    public function requiresSQLCommentHint(AbstractPlatform $platform)
    {
        return true;
    }
}
<?php

namespace Doctrine\DBAL\Types;

use DateTimeImmutable;
use Doctrine\DBAL\Platforms\AbstractPlatform;

/**
 * Immutable type of {@see TimeType}.
 */
class TimeImmutableType extends TimeType
{
    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return Types::TIME_IMMUTABLE;
    }

    /**
     * {@inheritdoc}
     */
    public function convertToDatabaseValue($value, AbstractPlatform $platform)
    {
        if ($value === null) {
            return $value;
        }

        if ($value instanceof DateTimeImmutable) {
            return $value->format($platform->getTimeFormatString());
        }

        throw ConversionException::conversionFailedInvalidType(
            $value,
            $this->getName(),
            ['null', DateTimeImmutable::class]
        );
    }

    /**
     * {@inheritdoc}
     */
    public function convertToPHPValue($value, AbstractPlatform $platform)
    {
        if ($value === null || $value instanceof DateTimeImmutable) {
            return $value;
        }

        $dateTime = DateTimeImmutable::createFromFormat('!' . $platform->getTimeFormatString(), $value);

        if (! $dateTime) {
            throw ConversionException::conversionFailedFormat(
                $value,
                $this->getName(),
                $platform->getTimeFormatString()
            );
        }

        return $dateTime;
    }

    /**
     * {@inheritdoc}
     */
    public function requiresSQLCommentHint(AbstractPlatform $platform)
    {
        return true;
    }
}
<?php

namespace Doctrine\DBAL\Types;

use Doctrine\DBAL\ParameterType;
use Doctrine\DBAL\Platforms\AbstractPlatform;

/**
 * Type that maps an SQL boolean to a PHP boolean.
 */
class BooleanType extends Type
{
    /**
     * {@inheritdoc}
     */
    public function getSQLDeclaration(array $column, AbstractPlatform $platform)
    {
        return $platform->getBooleanTypeDeclarationSQL($column);
    }

    /**
     * {@inheritdoc}
     */
    public function convertToDatabaseValue($value, AbstractPlatform $platform)
    {
        return $platform->convertBooleansToDatabaseValue($value);
    }

    /**
     * {@inheritdoc}
     */
    public function convertToPHPValue($value, AbstractPlatform $platform)
    {
        return $platform->convertFromBoolean($value);
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return Types::BOOLEAN;
    }

    /**
     * {@inheritdoc}
     */
    public function getBindingType()
    {
        return ParameterType::BOOLEAN;
    }
}
<?php

namespace Doctrine\DBAL\Types;

use Doctrine\DBAL\Platforms\AbstractPlatform;

use function is_resource;
use function stream_get_contents;

/**
 * Type that maps an SQL CLOB to a PHP string.
 */
class TextType extends Type
{
    /**
     * {@inheritdoc}
     */
    public function getSQLDeclaration(array $column, AbstractPlatform $platform)
    {
        return $platform->getClobTypeDeclarationSQL($column);
    }

    /**
     * {@inheritdoc}
     */
    public function convertToPHPValue($value, AbstractPlatform $platform)
    {
        return is_resource($value) ? stream_get_contents($value) : $value;
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return Types::TEXT;
    }
}
<?php

namespace Doctrine\DBAL\Types;

use Doctrine\DBAL\Platforms\AbstractPlatform;

class FloatType extends Type
{
    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return Types::FLOAT;
    }

    /**
     * {@inheritdoc}
     */
    public function getSQLDeclaration(array $column, AbstractPlatform $platform)
    {
        return $platform->getFloatDeclarationSQL($column);
    }

    /**
     * {@inheritdoc}
     */
    public function convertToPHPValue($value, AbstractPlatform $platform)
    {
        return $value === null ? null : (float) $value;
    }
}
<?php

namespace Doctrine\DBAL\Types;

use Doctrine\DBAL\ParameterType;
use Doctrine\DBAL\Platforms\AbstractPlatform;

/**
 * Type that maps a database BIGINT to a PHP string.
 */
class BigIntType extends Type implements PhpIntegerMappingType
{
    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return Types::BIGINT;
    }

    /**
     * {@inheritdoc}
     */
    public function getSQLDeclaration(array $column, AbstractPlatform $platform)
    {
        return $platform->getBigIntTypeDeclarationSQL($column);
    }

    /**
     * {@inheritdoc}
     */
    public function getBindingType()
    {
        return ParameterType::STRING;
    }

    /**
     * {@inheritdoc}
     */
    public function convertToPHPValue($value, AbstractPlatform $platform)
    {
        return $value === null ? null : (string) $value;
    }
}
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Types;

use Doctrine\DBAL\ParameterType;
use Doctrine\DBAL\Platforms\AbstractPlatform;

final class AsciiStringType extends StringType
{
    /**
     * {@inheritdoc}
     */
    public function getSQLDeclaration(array $column, AbstractPlatform $platform)
    {
        return $platform->getAsciiStringTypeDeclarationSQL($column);
    }

    /**
     * {@inheritdoc}
     */
    public function getBindingType()
    {
        return ParameterType::ASCII;
    }

    public function getName(): string
    {
        return Types::ASCII_STRING;
    }
}
<?php

namespace Doctrine\DBAL\Types;

use Doctrine\DBAL\Platforms\AbstractPlatform;

/**
 * Represents a GUID/UUID datatype (both are actually synonyms) in the database.
 */
class GuidType extends StringType
{
    /**
     * {@inheritdoc}
     */
    public function getSQLDeclaration(array $column, AbstractPlatform $platform)
    {
        return $platform->getGuidTypeDeclarationSQL($column);
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return Types::GUID;
    }

    /**
     * {@inheritdoc}
     */
    public function requiresSQLCommentHint(AbstractPlatform $platform)
    {
        return ! $platform->hasNativeGuidType();
    }
}
<?php

namespace Doctrine\DBAL\Types;

use Doctrine\DBAL\ParameterType;
use Doctrine\DBAL\Platforms\AbstractPlatform;

/**
 * Type that maps a database SMALLINT to a PHP integer.
 */
class SmallIntType extends Type implements PhpIntegerMappingType
{
    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return Types::SMALLINT;
    }

    /**
     * {@inheritdoc}
     */
    public function getSQLDeclaration(array $column, AbstractPlatform $platform)
    {
        return $platform->getSmallIntTypeDeclarationSQL($column);
    }

    /**
     * {@inheritdoc}
     */
    public function convertToPHPValue($value, AbstractPlatform $platform)
    {
        return $value === null ? null : (int) $value;
    }

    /**
     * {@inheritdoc}
     */
    public function getBindingType()
    {
        return ParameterType::INTEGER;
    }
}
<?php

namespace Doctrine\DBAL\Types;

use DateTime;
use DateTimeInterface;
use Doctrine\DBAL\Platforms\AbstractPlatform;

/**
 * Type that maps an SQL DATE to a PHP Date object.
 */
class DateType extends Type
{
    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return Types::DATE_MUTABLE;
    }

    /**
     * {@inheritdoc}
     */
    public function getSQLDeclaration(array $column, AbstractPlatform $platform)
    {
        return $platform->getDateTypeDeclarationSQL($column);
    }

    /**
     * {@inheritdoc}
     */
    public function convertToDatabaseValue($value, AbstractPlatform $platform)
    {
        if ($value === null) {
            return $value;
        }

        if ($value instanceof DateTimeInterface) {
            return $value->format($platform->getDateFormatString());
        }

        throw ConversionException::conversionFailedInvalidType($value, $this->getName(), ['null', 'DateTime']);
    }

    /**
     * {@inheritdoc}
     */
    public function convertToPHPValue($value, AbstractPlatform $platform)
    {
        if ($value === null || $value instanceof DateTimeInterface) {
            return $value;
        }

        $val = DateTime::createFromFormat('!' . $platform->getDateFormatString(), $value);
        if (! $val) {
            throw ConversionException::conversionFailedFormat(
                $value,
                $this->getName(),
                $platform->getDateFormatString()
            );
        }

        return $val;
    }
}
<?php

namespace Doctrine\DBAL\Types;

use DateTimeImmutable;
use Doctrine\DBAL\Platforms\AbstractPlatform;

/**
 * Immutable type of {@see DateType}.
 */
class DateImmutableType extends DateType
{
    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return Types::DATE_IMMUTABLE;
    }

    /**
     * {@inheritdoc}
     */
    public function convertToDatabaseValue($value, AbstractPlatform $platform)
    {
        if ($value === null) {
            return $value;
        }

        if ($value instanceof DateTimeImmutable) {
            return $value->format($platform->getDateFormatString());
        }

        throw ConversionException::conversionFailedInvalidType(
            $value,
            $this->getName(),
            ['null', DateTimeImmutable::class]
        );
    }

    /**
     * {@inheritdoc}
     */
    public function convertToPHPValue($value, AbstractPlatform $platform)
    {
        if ($value === null || $value instanceof DateTimeImmutable) {
            return $value;
        }

        $dateTime = DateTimeImmutable::createFromFormat('!' . $platform->getDateFormatString(), $value);

        if (! $dateTime) {
            throw ConversionException::conversionFailedFormat(
                $value,
                $this->getName(),
                $platform->getDateFormatString()
            );
        }

        return $dateTime;
    }

    /**
     * {@inheritdoc}
     */
    public function requiresSQLCommentHint(AbstractPlatform $platform)
    {
        return true;
    }
}
<?php

namespace Doctrine\DBAL\Types;

use DateTime;
use DateTimeInterface;
use Doctrine\DBAL\Platforms\AbstractPlatform;

use function date_create;

/**
 * Type that maps an SQL DATETIME/TIMESTAMP to a PHP DateTime object.
 */
class DateTimeType extends Type implements PhpDateTimeMappingType
{
    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return Types::DATETIME_MUTABLE;
    }

    /**
     * {@inheritdoc}
     */
    public function getSQLDeclaration(array $column, AbstractPlatform $platform)
    {
        return $platform->getDateTimeTypeDeclarationSQL($column);
    }

    /**
     * {@inheritdoc}
     */
    public function convertToDatabaseValue($value, AbstractPlatform $platform)
    {
        if ($value === null) {
            return $value;
        }

        if ($value instanceof DateTimeInterface) {
            return $value->format($platform->getDateTimeFormatString());
        }

        throw ConversionException::conversionFailedInvalidType($value, $this->getName(), ['null', 'DateTime']);
    }

    /**
     * {@inheritdoc}
     */
    public function convertToPHPValue($value, AbstractPlatform $platform)
    {
        if ($value === null || $value instanceof DateTimeInterface) {
            return $value;
        }

        $val = DateTime::createFromFormat($platform->getDateTimeFormatString(), $value);

        if (! $val) {
            $val = date_create($value);
        }

        if (! $val) {
            throw ConversionException::conversionFailedFormat(
                $value,
                $this->getName(),
                $platform->getDateTimeFormatString()
            );
        }

        return $val;
    }
}
<?php

declare(strict_types=1);

namespace Doctrine\DBAL;

final class TransactionIsolationLevel
{
    /**
     * Transaction isolation level READ UNCOMMITTED.
     */
    public const READ_UNCOMMITTED = 1;

    /**
     * Transaction isolation level READ COMMITTED.
     */
    public const READ_COMMITTED = 2;

    /**
     * Transaction isolation level REPEATABLE READ.
     */
    public const REPEATABLE_READ = 3;

    /**
     * Transaction isolation level SERIALIZABLE.
     */
    public const SERIALIZABLE = 4;

    /**
     * @codeCoverageIgnore
     */
    private function __construct()
    {
    }
}
<?php

namespace Doctrine\DBAL;

use Closure;
use Doctrine\Common\EventManager;
use Doctrine\DBAL\Cache\ArrayStatement;
use Doctrine\DBAL\Cache\CacheException;
use Doctrine\DBAL\Cache\QueryCacheProfile;
use Doctrine\DBAL\Cache\ResultCacheStatement;
use Doctrine\DBAL\Driver\Connection as DriverConnection;
use Doctrine\DBAL\Driver\PDO\Statement as PDODriverStatement;
use Doctrine\DBAL\Driver\PingableConnection;
use Doctrine\DBAL\Driver\ResultStatement;
use Doctrine\DBAL\Driver\ServerInfoAwareConnection;
use Doctrine\DBAL\Exception\ConnectionLost;
use Doctrine\DBAL\Exception\InvalidArgumentException;
use Doctrine\DBAL\Exception\NoKeyValue;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Query\Expression\ExpressionBuilder;
use Doctrine\DBAL\Query\QueryBuilder;
use Doctrine\DBAL\Schema\AbstractSchemaManager;
use Doctrine\DBAL\Types\Type;
use Doctrine\Deprecations\Deprecation;
use PDO;
use Throwable;
use Traversable;

use function array_key_exists;
use function array_shift;
use function assert;
use function func_get_args;
use function implode;
use function is_int;
use function is_string;
use function key;

/**
 * A wrapper around a Doctrine\DBAL\Driver\Connection that adds features like
 * events, transaction isolation levels, configuration, emulated transaction nesting,
 * lazy connecting and more.
 *
 * @psalm-import-type Params from DriverManager
 * @psalm-consistent-constructor
 */
class Connection implements DriverConnection
{
    /**
     * Constant for transaction isolation level READ UNCOMMITTED.
     *
     * @deprecated Use TransactionIsolationLevel::READ_UNCOMMITTED.
     */
    public const TRANSACTION_READ_UNCOMMITTED = TransactionIsolationLevel::READ_UNCOMMITTED;

    /**
     * Constant for transaction isolation level READ COMMITTED.
     *
     * @deprecated Use TransactionIsolationLevel::READ_COMMITTED.
     */
    public const TRANSACTION_READ_COMMITTED = TransactionIsolationLevel::READ_COMMITTED;

    /**
     * Constant for transaction isolation level REPEATABLE READ.
     *
     * @deprecated Use TransactionIsolationLevel::REPEATABLE_READ.
     */
    public const TRANSACTION_REPEATABLE_READ = TransactionIsolationLevel::REPEATABLE_READ;

    /**
     * Constant for transaction isolation level SERIALIZABLE.
     *
     * @deprecated Use TransactionIsolationLevel::SERIALIZABLE.
     */
    public const TRANSACTION_SERIALIZABLE = TransactionIsolationLevel::SERIALIZABLE;

    /**
     * Represents an array of ints to be expanded by Doctrine SQL parsing.
     */
    public const PARAM_INT_ARRAY = ParameterType::INTEGER + self::ARRAY_PARAM_OFFSET;

    /**
     * Represents an array of strings to be expanded by Doctrine SQL parsing.
     */
    public const PARAM_STR_ARRAY = ParameterType::STRING + self::ARRAY_PARAM_OFFSET;

    /**
     * Offset by which PARAM_* constants are detected as arrays of the param type.
     */
    public const ARRAY_PARAM_OFFSET = 100;

    /**
     * The wrapped driver connection.
     *
     * @var \Doctrine\DBAL\Driver\Connection|null
     */
    protected $_conn;

    /** @var Configuration */
    protected $_config;

    /** @var EventManager */
    protected $_eventManager;

    /** @var ExpressionBuilder */
    protected $_expr;

    /**
     * The current auto-commit mode of this connection.
     *
     * @var bool
     */
    private $autoCommit = true;

    /**
     * The transaction nesting level.
     *
     * @var int
     */
    private $transactionNestingLevel = 0;

    /**
     * The currently active transaction isolation level or NULL before it has been determined.
     *
     * @var int|null
     */
    private $transactionIsolationLevel;

    /**
     * If nested transactions should use savepoints.
     *
     * @var bool
     */
    private $nestTransactionsWithSavepoints = false;

    /**
     * The parameters used during creation of the Connection instance.
     *
     * @var array<string,mixed>
     * @phpstan-var array<string,mixed>
     * @psalm-var Params
     */
    private $params;

    /**
     * The database platform object used by the connection or NULL before it's initialized.
     *
     * @var AbstractPlatform|null
     */
    private $platform;

    /**
     * The schema manager.
     *
     * @var AbstractSchemaManager|null
     */
    protected $_schemaManager;

    /**
     * The used DBAL driver.
     *
     * @var Driver
     */
    protected $_driver;

    /**
     * Flag that indicates whether the current transaction is marked for rollback only.
     *
     * @var bool
     */
    private $isRollbackOnly = false;

    /** @var int */
    protected $defaultFetchMode = FetchMode::ASSOCIATIVE;

    /**
     * Initializes a new instance of the Connection class.
     *
     * @internal The connection can be only instantiated by the driver manager.
     *
     * @param array<string,mixed> $params       The connection parameters.
     * @param Driver              $driver       The driver to use.
     * @param Configuration|null  $config       The configuration, optional.
     * @param EventManager|null   $eventManager The event manager, optional.
     * @psalm-param Params $params
     * @phpstan-param array<string,mixed> $params
     *
     * @throws Exception
     */
    public function __construct(
        array $params,
        Driver $driver,
        ?Configuration $config = null,
        ?EventManager $eventManager = null
    ) {
        $this->_driver = $driver;
        $this->params  = $params;

        if (isset($params['pdo'])) {
            Deprecation::trigger(
                'doctrine/dbal',
                'https://github.com/doctrine/dbal/pull/3554',
                'Passing a user provided PDO instance directly to Doctrine is deprecated.'
            );

            if (! $params['pdo'] instanceof PDO) {
                throw Exception::invalidPdoInstance();
            }

            $this->_conn = $params['pdo'];
            $this->_conn->setAttribute(PDO::ATTR_STATEMENT_CLASS, [PDODriverStatement::class, []]);
            unset($this->params['pdo']);
        }

        if (isset($params['platform'])) {
            if (! $params['platform'] instanceof Platforms\AbstractPlatform) {
                throw Exception::invalidPlatformType($params['platform']);
            }

            $this->platform = $params['platform'];
        }

        // 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;

        $this->_expr = new Query\Expression\ExpressionBuilder($this);

        $this->autoCommit = $config->getAutoCommit();
    }

    /**
     * Gets the parameters used during instantiation.
     *
     * @internal
     *
     * @return array<string,mixed>
     * @psalm-return Params
     * @phpstan-return array<string,mixed>
     */
    public function getParams()
    {
        return $this->params;
    }

    /**
     * Gets the name of the database this Connection is connected to.
     *
     * @return string
     */
    public function getDatabase()
    {
        return $this->_driver->getDatabase($this);
    }

    /**
     * Gets the hostname of the currently connected database.
     *
     * @deprecated
     *
     * @return string|null
     */
    public function getHost()
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/issues/3580',
            'Connection::getHost() is deprecated, get the database server host from application config ' .
            'or as a last resort from internal Connection::getParams() API.'
        );

        return $this->params['host'] ?? null;
    }

    /**
     * Gets the port of the currently connected database.
     *
     * @deprecated
     *
     * @return mixed
     */
    public function getPort()
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/issues/3580',
            'Connection::getPort() is deprecated, get the database server port from application config ' .
            'or as a last resort from internal Connection::getParams() API.'
        );

        return $this->params['port'] ?? null;
    }

    /**
     * Gets the username used by this connection.
     *
     * @deprecated
     *
     * @return string|null
     */
    public function getUsername()
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/issues/3580',
            'Connection::getUsername() is deprecated, get the username from application config ' .
            'or as a last resort from internal Connection::getParams() API.'
        );

        return $this->params['user'] ?? null;
    }

    /**
     * Gets the password used by this connection.
     *
     * @deprecated
     *
     * @return string|null
     */
    public function getPassword()
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/issues/3580',
            'Connection::getPassword() is deprecated, get the password from application config ' .
            'or as a last resort from internal Connection::getParams() API.'
        );

        return $this->params['password'] ?? null;
    }

    /**
     * Gets the DBAL driver instance.
     *
     * @return Driver
     */
    public function getDriver()
    {
        return $this->_driver;
    }

    /**
     * Gets the Configuration used by the Connection.
     *
     * @return Configuration
     */
    public function getConfiguration()
    {
        return $this->_config;
    }

    /**
     * Gets the EventManager used by the Connection.
     *
     * @return EventManager
     */
    public function getEventManager()
    {
        return $this->_eventManager;
    }

    /**
     * Gets the DatabasePlatform for the connection.
     *
     * @return AbstractPlatform
     *
     * @throws Exception
     */
    public function getDatabasePlatform()
    {
        if ($this->platform === null) {
            $this->platform = $this->detectDatabasePlatform();
            $this->platform->setEventManager($this->_eventManager);
        }

        return $this->platform;
    }

    /**
     * Gets the ExpressionBuilder for the connection.
     *
     * @return ExpressionBuilder
     */
    public function getExpressionBuilder()
    {
        return $this->_expr;
    }

    /**
     * Establishes the connection with the database.
     *
     * @return bool TRUE if the connection was successfully established, FALSE if
     *              the connection is already open.
     */
    public function connect()
    {
        if ($this->_conn !== null) {
            return false;
        }

        $driverOptions = $this->params['driverOptions'] ?? [];
        $user          = $this->params['user'] ?? null;
        $password      = $this->params['password'] ?? null;

        $this->_conn = $this->_driver->connect($this->params, $user, $password, $driverOptions);

        $this->transactionNestingLevel = 0;

        if ($this->autoCommit === false) {
            $this->beginTransaction();
        }

        if ($this->_eventManager->hasListeners(Events::postConnect)) {
            $eventArgs = new Event\ConnectionEventArgs($this);
            $this->_eventManager->dispatchEvent(Events::postConnect, $eventArgs);
        }

        return true;
    }

    /**
     * Detects and sets the database platform.
     *
     * Evaluates custom platform class and version in order to set the correct platform.
     *
     * @throws Exception If an invalid platform was specified for this connection.
     */
    private function detectDatabasePlatform(): AbstractPlatform
    {
        $version = $this->getDatabasePlatformVersion();

        if ($version !== null) {
            assert($this->_driver instanceof VersionAwarePlatformDriver);

            return $this->_driver->createDatabasePlatformForVersion($version);
        }

        return $this->_driver->getDatabasePlatform();
    }

    /**
     * Returns the version of the related platform if applicable.
     *
     * Returns null if either the driver is not capable to create version
     * specific platform instances, no explicit server version was specified
     * or the underlying driver connection cannot determine the platform
     * version without having to query it (performance reasons).
     *
     * @return string|null
     *
     * @throws Throwable
     */
    private function getDatabasePlatformVersion()
    {
        // Driver does not support version specific platforms.
        if (! $this->_driver instanceof VersionAwarePlatformDriver) {
            return null;
        }

        // Explicit platform version requested (supersedes auto-detection).
        if (isset($this->params['serverVersion'])) {
            return $this->params['serverVersion'];
        }

        // If not connected, we need to connect now to determine the platform version.
        if ($this->_conn === null) {
            try {
                $this->connect();
            } catch (Throwable $originalException) {
                if (empty($this->params['dbname'])) {
                    throw $originalException;
                }

                // The database to connect to might not yet exist.
                // Retry detection without database name connection parameter.
                $params = $this->params;

                unset($this->params['dbname']);

                try {
                    $this->connect();
                } catch (Throwable $fallbackException) {
                    // Either the platform does not support database-less connections
                    // or something else went wrong.
                    throw $originalException;
                } finally {
                    $this->params = $params;
                }

                $serverVersion = $this->getServerVersion();

                // Close "temporary" connection to allow connecting to the real database again.
                $this->close();

                return $serverVersion;
            }
        }

        return $this->getServerVersion();
    }

    /**
     * Returns the database server version if the underlying driver supports it.
     *
     * @return string|null
     */
    private function getServerVersion()
    {
        $connection = $this->getWrappedConnection();

        // Automatic platform version detection.
        if ($connection instanceof ServerInfoAwareConnection && ! $connection->requiresQueryForServerVersion()) {
            return $connection->getServerVersion();
        }

        // Unable to detect platform version.
        return null;
    }

    /**
     * Returns the current auto-commit mode for this connection.
     *
     * @see    setAutoCommit
     *
     * @return bool True if auto-commit mode is currently enabled for this connection, false otherwise.
     */
    public function isAutoCommit()
    {
        return $this->autoCommit === true;
    }

    /**
     * Sets auto-commit mode for this connection.
     *
     * If a connection is in auto-commit mode, then all its SQL statements will be executed and committed as individual
     * transactions. Otherwise, its SQL statements are grouped into transactions that are terminated by a call to either
     * the method commit or the method rollback. By default, new connections are in auto-commit mode.
     *
     * NOTE: If this method is called during a transaction and the auto-commit mode is changed, the transaction is
     * committed. If this method is called and the auto-commit mode is not changed, the call is a no-op.
     *
     * @see   isAutoCommit
     *
     * @param bool $autoCommit True to enable auto-commit mode; false to disable it.
     *
     * @return void
     */
    public function setAutoCommit($autoCommit)
    {
        $autoCommit = (bool) $autoCommit;

        // Mode not changed, no-op.
        if ($autoCommit === $this->autoCommit) {
            return;
        }

        $this->autoCommit = $autoCommit;

        // Commit all currently active transactions if any when switching auto-commit mode.
        if ($this->_conn === null || $this->transactionNestingLevel === 0) {
            return;
        }

        $this->commitAll();
    }

    /**
     * Sets the fetch mode.
     *
     * @deprecated Use one of the fetch- or iterate-related methods.
     *
     * @param int $fetchMode
     *
     * @return void
     */
    public function setFetchMode($fetchMode)
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/4019',
            'Default Fetch Mode configuration is deprecated, use explicit Connection::fetch*() APIs instead.'
        );

        $this->defaultFetchMode = $fetchMode;
    }

    /**
     * Prepares and executes an SQL query and returns the first row of the result
     * as an associative array.
     *
     * @deprecated Use fetchAssociative()
     *
     * @param string                                                               $sql    SQL query
     * @param array<int, mixed>|array<string, mixed>                               $params Query parameters
     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
     *
     * @return array<string, mixed>|false False is returned if no rows are found.
     *
     * @throws Exception
     */
    public function fetchAssoc($sql, array $params = [], array $types = [])
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/4019',
            'Connection::fetchAssoc() is deprecated, use Connection::fetchAssociative() API instead.'
        );

        return $this->executeQuery($sql, $params, $types)->fetch(FetchMode::ASSOCIATIVE);
    }

    /**
     * Prepares and executes an SQL query and returns the first row of the result
     * as a numerically indexed array.
     *
     * @deprecated Use fetchNumeric()
     *
     * @param string                                                               $sql    SQL query
     * @param array<int, mixed>|array<string, mixed>                               $params Query parameters
     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
     *
     * @return array<int, mixed>|false False is returned if no rows are found.
     */
    public function fetchArray($sql, array $params = [], array $types = [])
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/4019',
            'Connection::fetchArray() is deprecated, use Connection::fetchNumeric() API instead.'
        );

        return $this->executeQuery($sql, $params, $types)->fetch(FetchMode::NUMERIC);
    }

    /**
     * Prepares and executes an SQL query and returns the value of a single column
     * of the first row of the result.
     *
     * @deprecated Use fetchOne() instead.
     *
     * @param string                                                               $sql    SQL query
     * @param array<int, mixed>|array<string, mixed>                               $params Query parameters
     * @param int                                                                  $column 0-indexed column number
     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
     *
     * @return mixed|false False is returned if no rows are found.
     *
     * @throws Exception
     */
    public function fetchColumn($sql, array $params = [], $column = 0, array $types = [])
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/4019',
            'Connection::fetchColumn() is deprecated, use Connection::fetchOne() API instead.'
        );

        return $this->executeQuery($sql, $params, $types)->fetchColumn($column);
    }

    /**
     * Prepares and executes an SQL query and returns the first row of the result
     * as an associative array.
     *
     * @param string                                                               $query  SQL query
     * @param array<int, mixed>|array<string, mixed>                               $params Query parameters
     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
     *
     * @return array<string, mixed>|false False is returned if no rows are found.
     *
     * @throws Exception
     */
    public function fetchAssociative(string $query, array $params = [], array $types = [])
    {
        try {
            $stmt = $this->ensureForwardCompatibilityStatement(
                $this->executeQuery($query, $params, $types)
            );

            return $stmt->fetchAssociative();
        } catch (Throwable $e) {
            $this->handleExceptionDuringQuery($e, $query, $params, $types);
        }
    }

    /**
     * Prepares and executes an SQL query and returns the first row of the result
     * as a numerically indexed array.
     *
     * @param string                                                               $query  SQL query
     * @param array<int, mixed>|array<string, mixed>                               $params Query parameters
     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
     *
     * @return array<int, mixed>|false False is returned if no rows are found.
     *
     * @throws Exception
     */
    public function fetchNumeric(string $query, array $params = [], array $types = [])
    {
        try {
            $stmt = $this->ensureForwardCompatibilityStatement(
                $this->executeQuery($query, $params, $types)
            );

            return $stmt->fetchNumeric();
        } catch (Throwable $e) {
            $this->handleExceptionDuringQuery($e, $query, $params, $types);
        }
    }

    /**
     * Prepares and executes an SQL query and returns the value of a single column
     * of the first row of the result.
     *
     * @param string                                                               $query  SQL query
     * @param array<int, mixed>|array<string, mixed>                               $params Query parameters
     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
     *
     * @return mixed|false False is returned if no rows are found.
     *
     * @throws Exception
     */
    public function fetchOne(string $query, array $params = [], array $types = [])
    {
        try {
            $stmt = $this->ensureForwardCompatibilityStatement(
                $this->executeQuery($query, $params, $types)
            );

            return $stmt->fetchOne();
        } catch (Throwable $e) {
            $this->handleExceptionDuringQuery($e, $query, $params, $types);
        }
    }

    /**
     * Whether an actual connection to the database is established.
     *
     * @return bool
     */
    public function isConnected()
    {
        return $this->_conn !== null;
    }

    /**
     * Checks whether a transaction is currently active.
     *
     * @return bool TRUE if a transaction is currently active, FALSE otherwise.
     */
    public function isTransactionActive()
    {
        return $this->transactionNestingLevel > 0;
    }

    /**
     * Adds condition based on the criteria to the query components
     *
     * @param array<string,mixed> $criteria   Map of key columns to their values
     * @param string[]            $columns    Column names
     * @param mixed[]             $values     Column values
     * @param string[]            $conditions Key conditions
     *
     * @throws Exception
     */
    private function addCriteriaCondition(
        array $criteria,
        array &$columns,
        array &$values,
        array &$conditions
    ): void {
        $platform = $this->getDatabasePlatform();

        foreach ($criteria as $columnName => $value) {
            if ($value === null) {
                $conditions[] = $platform->getIsNullExpression($columnName);
                continue;
            }

            $columns[]    = $columnName;
            $values[]     = $value;
            $conditions[] = $columnName . ' = ?';
        }
    }

    /**
     * Executes an SQL DELETE statement on a table.
     *
     * Table expression and columns are not escaped and are not safe for user-input.
     *
     * @param string                                                               $table    Table name
     * @param array<string, mixed>                                                 $criteria Deletion criteria
     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types    Parameter types
     *
     * @return int|string The number of affected rows.
     *
     * @throws Exception
     */
    public function delete($table, array $criteria, array $types = [])
    {
        if (empty($criteria)) {
            throw InvalidArgumentException::fromEmptyCriteria();
        }

        $columns = $values = $conditions = [];

        $this->addCriteriaCondition($criteria, $columns, $values, $conditions);

        return $this->executeStatement(
            'DELETE FROM ' . $table . ' WHERE ' . implode(' AND ', $conditions),
            $values,
            is_string(key($types)) ? $this->extractTypeValues($columns, $types) : $types
        );
    }

    /**
     * Closes the connection.
     *
     * @return void
     */
    public function close()
    {
        $this->_conn = null;
    }

    /**
     * Sets the transaction isolation level.
     *
     * @param int $level The level to set.
     *
     * @return int|string
     */
    public function setTransactionIsolation($level)
    {
        $this->transactionIsolationLevel = $level;

        return $this->executeStatement($this->getDatabasePlatform()->getSetTransactionIsolationSQL($level));
    }

    /**
     * Gets the currently active transaction isolation level.
     *
     * @return int The current transaction isolation level.
     */
    public function getTransactionIsolation()
    {
        if ($this->transactionIsolationLevel === null) {
            $this->transactionIsolationLevel = $this->getDatabasePlatform()->getDefaultTransactionIsolationLevel();
        }

        return $this->transactionIsolationLevel;
    }

    /**
     * Executes an SQL UPDATE statement on a table.
     *
     * Table expression and columns are not escaped and are not safe for user-input.
     *
     * @param string                                                               $table    Table name
     * @param array<string, mixed>                                                 $data     Column-value pairs
     * @param array<string, mixed>                                                 $criteria Update criteria
     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types    Parameter types
     *
     * @return int|string The number of affected rows.
     *
     * @throws Exception
     */
    public function update($table, array $data, array $criteria, array $types = [])
    {
        $columns = $values = $conditions = $set = [];

        foreach ($data as $columnName => $value) {
            $columns[] = $columnName;
            $values[]  = $value;
            $set[]     = $columnName . ' = ?';
        }

        $this->addCriteriaCondition($criteria, $columns, $values, $conditions);

        if (is_string(key($types))) {
            $types = $this->extractTypeValues($columns, $types);
        }

        $sql = 'UPDATE ' . $table . ' SET ' . implode(', ', $set)
                . ' WHERE ' . implode(' AND ', $conditions);

        return $this->executeStatement($sql, $values, $types);
    }

    /**
     * Inserts a table row with specified data.
     *
     * Table expression and columns are not escaped and are not safe for user-input.
     *
     * @param string                                                               $table Table name
     * @param array<string, mixed>                                                 $data  Column-value pairs
     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types Parameter types
     *
     * @return int|string The number of affected rows.
     *
     * @throws Exception
     */
    public function insert($table, array $data, array $types = [])
    {
        if (empty($data)) {
            return $this->executeStatement('INSERT INTO ' . $table . ' () VALUES ()');
        }

        $columns = [];
        $values  = [];
        $set     = [];

        foreach ($data as $columnName => $value) {
            $columns[] = $columnName;
            $values[]  = $value;
            $set[]     = '?';
        }

        return $this->executeStatement(
            'INSERT INTO ' . $table . ' (' . implode(', ', $columns) . ')' .
            ' VALUES (' . implode(', ', $set) . ')',
            $values,
            is_string(key($types)) ? $this->extractTypeValues($columns, $types) : $types
        );
    }

    /**
     * Extract ordered type list from an ordered column list and type map.
     *
     * @param array<int, string>                                                   $columnList
     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types
     *
     * @return array<int, int|string|Type|null>|array<string, int|string|Type|null>
     */
    private function extractTypeValues(array $columnList, array $types)
    {
        $typeValues = [];

        foreach ($columnList as $columnIndex => $columnName) {
            $typeValues[] = $types[$columnName] ?? ParameterType::STRING;
        }

        return $typeValues;
    }

    /**
     * Quotes 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->getDatabasePlatform()->quoteIdentifier($str);
    }

    /**
     * {@inheritDoc}
     *
     * @param mixed                $value
     * @param int|string|Type|null $type
     */
    public function quote($value, $type = ParameterType::STRING)
    {
        $connection = $this->getWrappedConnection();

        [$value, $bindingType] = $this->getBindingInfo($value, $type);

        return $connection->quote($value, $bindingType);
    }

    /**
     * Prepares and executes an SQL query and returns the result as an associative array.
     *
     * @deprecated Use fetchAllAssociative()
     *
     * @param string         $sql    The SQL query.
     * @param mixed[]        $params The query parameters.
     * @param int[]|string[] $types  The query parameter types.
     *
     * @return mixed[]
     */
    public function fetchAll($sql, array $params = [], $types = [])
    {
        return $this->executeQuery($sql, $params, $types)->fetchAll();
    }

    /**
     * Prepares and executes an SQL query and returns the result as an array of numeric arrays.
     *
     * @param string                                                               $query  SQL query
     * @param array<int, mixed>|array<string, mixed>                               $params Query parameters
     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
     *
     * @return array<int,array<int,mixed>>
     *
     * @throws Exception
     */
    public function fetchAllNumeric(string $query, array $params = [], array $types = []): array
    {
        try {
            $stmt = $this->ensureForwardCompatibilityStatement(
                $this->executeQuery($query, $params, $types)
            );

            return $stmt->fetchAllNumeric();
        } catch (Throwable $e) {
            $this->handleExceptionDuringQuery($e, $query, $params, $types);
        }
    }

    /**
     * Prepares and executes an SQL query and returns the result as an array of associative arrays.
     *
     * @param string                                                               $query  SQL query
     * @param array<int, mixed>|array<string, mixed>                               $params Query parameters
     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
     *
     * @return array<int,array<string,mixed>>
     *
     * @throws Exception
     */
    public function fetchAllAssociative(string $query, array $params = [], array $types = []): array
    {
        try {
            $stmt = $this->ensureForwardCompatibilityStatement(
                $this->executeQuery($query, $params, $types)
            );

            return $stmt->fetchAllAssociative();
        } catch (Throwable $e) {
            $this->handleExceptionDuringQuery($e, $query, $params, $types);
        }
    }

    /**
     * Prepares and executes an SQL query and returns the result as an associative array with the keys
     * mapped to the first column and the values mapped to the second column.
     *
     * @param string                                           $query  SQL query
     * @param array<int, mixed>|array<string, mixed>           $params Query parameters
     * @param array<int, int|string>|array<string, int|string> $types  Parameter types
     *
     * @return array<mixed,mixed>
     *
     * @throws Exception
     */
    public function fetchAllKeyValue(string $query, array $params = [], array $types = []): array
    {
        $stmt = $this->executeQuery($query, $params, $types);

        $this->ensureHasKeyValue($stmt);

        $data = [];

        foreach ($stmt->fetchAll(FetchMode::NUMERIC) as [$key, $value]) {
            $data[$key] = $value;
        }

        return $data;
    }

    /**
     * Prepares and executes an SQL query and returns the result as an associative array with the keys mapped
     * to the first column and the values being an associative array representing the rest of the columns
     * and their values.
     *
     * @param string                                           $query  SQL query
     * @param array<int, mixed>|array<string, mixed>           $params Query parameters
     * @param array<int, int|string>|array<string, int|string> $types  Parameter types
     *
     * @return array<mixed,array<string,mixed>>
     *
     * @throws Exception
     */
    public function fetchAllAssociativeIndexed(string $query, array $params = [], array $types = []): array
    {
        $stmt = $this->executeQuery($query, $params, $types);

        $data = [];

        foreach ($stmt->fetchAll(FetchMode::ASSOCIATIVE) as $row) {
            $data[array_shift($row)] = $row;
        }

        return $data;
    }

    /**
     * Prepares and executes an SQL query and returns the result as an array of the first column values.
     *
     * @param string                                                               $query  SQL query
     * @param array<int, mixed>|array<string, mixed>                               $params Query parameters
     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
     *
     * @return array<int,mixed>
     *
     * @throws Exception
     */
    public function fetchFirstColumn(string $query, array $params = [], array $types = []): array
    {
        try {
            $stmt = $this->ensureForwardCompatibilityStatement(
                $this->executeQuery($query, $params, $types)
            );

            return $stmt->fetchFirstColumn();
        } catch (Throwable $e) {
            $this->handleExceptionDuringQuery($e, $query, $params, $types);
        }
    }

    /**
     * Prepares and executes an SQL query and returns the result as an iterator over rows represented as numeric arrays.
     *
     * @param string                                                               $query  SQL query
     * @param array<int, mixed>|array<string, mixed>                               $params Query parameters
     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
     *
     * @return Traversable<int,array<int,mixed>>
     *
     * @throws Exception
     */
    public function iterateNumeric(string $query, array $params = [], array $types = []): Traversable
    {
        try {
            $stmt = $this->ensureForwardCompatibilityStatement(
                $this->executeQuery($query, $params, $types)
            );

            yield from $stmt->iterateNumeric();
        } catch (Throwable $e) {
            $this->handleExceptionDuringQuery($e, $query, $params, $types);
        }
    }

    /**
     * Prepares and executes an SQL query and returns the result as an iterator over rows represented
     * as associative arrays.
     *
     * @param string                                                               $query  SQL query
     * @param array<int, mixed>|array<string, mixed>                               $params Query parameters
     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
     *
     * @return Traversable<int,array<string,mixed>>
     *
     * @throws Exception
     */
    public function iterateAssociative(string $query, array $params = [], array $types = []): Traversable
    {
        try {
            $stmt = $this->ensureForwardCompatibilityStatement(
                $this->executeQuery($query, $params, $types)
            );

            yield from $stmt->iterateAssociative();
        } catch (Throwable $e) {
            $this->handleExceptionDuringQuery($e, $query, $params, $types);
        }
    }

    /**
     * Prepares and executes an SQL query and returns the result as an iterator with the keys
     * mapped to the first column and the values mapped to the second column.
     *
     * @param string                                           $query  SQL query
     * @param array<int, mixed>|array<string, mixed>           $params Query parameters
     * @param array<int, int|string>|array<string, int|string> $types  Parameter types
     *
     * @return Traversable<mixed,mixed>
     *
     * @throws Exception
     */
    public function iterateKeyValue(string $query, array $params = [], array $types = []): Traversable
    {
        $stmt = $this->executeQuery($query, $params, $types);

        $this->ensureHasKeyValue($stmt);

        while (($row = $stmt->fetch(FetchMode::NUMERIC)) !== false) {
            yield $row[0] => $row[1];
        }
    }

    /**
     * Prepares and executes an SQL query and returns the result as an iterator with the keys mapped
     * to the first column and the values being an associative array representing the rest of the columns
     * and their values.
     *
     * @param string                                           $query  SQL query
     * @param array<int, mixed>|array<string, mixed>           $params Query parameters
     * @param array<int, int|string>|array<string, int|string> $types  Parameter types
     *
     * @return Traversable<mixed,array<string,mixed>>
     *
     * @throws Exception
     */
    public function iterateAssociativeIndexed(string $query, array $params = [], array $types = []): Traversable
    {
        $stmt = $this->executeQuery($query, $params, $types);

        while (($row = $stmt->fetch(FetchMode::ASSOCIATIVE)) !== false) {
            yield array_shift($row) => $row;
        }
    }

    /**
     * Prepares and executes an SQL query and returns the result as an iterator over the first column values.
     *
     * @param string                                                               $query  SQL query
     * @param array<int, mixed>|array<string, mixed>                               $params Query parameters
     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
     *
     * @return Traversable<int,mixed>
     *
     * @throws Exception
     */
    public function iterateColumn(string $query, array $params = [], array $types = []): Traversable
    {
        try {
            $stmt = $this->ensureForwardCompatibilityStatement(
                $this->executeQuery($query, $params, $types)
            );

            yield from $stmt->iterateColumn();
        } catch (Throwable $e) {
            $this->handleExceptionDuringQuery($e, $query, $params, $types);
        }
    }

    /**
     * Prepares an SQL statement.
     *
     * @param string $sql The SQL statement to prepare.
     *
     * @return Statement The prepared statement.
     *
     * @throws Exception
     */
    public function prepare($sql)
    {
        try {
            $stmt = new Statement($sql, $this);
        } catch (Throwable $e) {
            $this->handleExceptionDuringQuery($e, $sql);
        }

        $stmt->setFetchMode($this->defaultFetchMode);

        return $stmt;
    }

    /**
     * Executes an, optionally parametrized, SQL query.
     *
     * If the query is parametrized, a prepared statement is used.
     * If an SQLLogger is configured, the execution is logged.
     *
     * @param string                                                               $sql    SQL query
     * @param array<int, mixed>|array<string, mixed>                               $params Query parameters
     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
     *
     * @return ForwardCompatibility\DriverStatement|ForwardCompatibility\DriverResultStatement
     *
     * The executed statement or the cached result statement if a query cache profile is used
     *
     * @throws Exception
     */
    public function executeQuery($sql, array $params = [], $types = [], ?QueryCacheProfile $qcp = null)
    {
        if ($qcp !== null) {
            return $this->executeCacheQuery($sql, $params, $types, $qcp);
        }

        $connection = $this->getWrappedConnection();

        $logger = $this->_config->getSQLLogger();
        if ($logger) {
            $logger->startQuery($sql, $params, $types);
        }

        try {
            if ($params) {
                [$sql, $params, $types] = SQLParserUtils::expandListParameters($sql, $params, $types);

                $stmt = $connection->prepare($sql);
                if ($types) {
                    $this->_bindTypedValues($stmt, $params, $types);
                    $stmt->execute();
                } else {
                    $stmt->execute($params);
                }
            } else {
                $stmt = $connection->query($sql);
            }
        } catch (Throwable $e) {
            $this->handleExceptionDuringQuery(
                $e,
                $sql,
                $params,
                $types
            );
        }

        $stmt->setFetchMode($this->defaultFetchMode);

        if ($logger) {
            $logger->stopQuery();
        }

        return $this->ensureForwardCompatibilityStatement($stmt);
    }

    /**
     * Executes a caching query.
     *
     * @param string                                                               $sql    SQL query
     * @param array<int, mixed>|array<string, mixed>                               $params Query parameters
     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
     *
     * @return ForwardCompatibility\DriverResultStatement
     *
     * @throws CacheException
     */
    public function executeCacheQuery($sql, $params, $types, QueryCacheProfile $qcp)
    {
        $resultCache = $qcp->getResultCacheDriver() ?? $this->_config->getResultCacheImpl();

        if ($resultCache === null) {
            throw CacheException::noResultDriverConfigured();
        }

        $connectionParams = $this->params;
        unset($connectionParams['platform']);

        [$cacheKey, $realKey] = $qcp->generateCacheKeys($sql, $params, $types, $connectionParams);

        // fetch the row pointers entry
        $data = $resultCache->fetch($cacheKey);

        if ($data !== false) {
            // is the real key part of this row pointers map or is the cache only pointing to other cache keys?
            if (isset($data[$realKey])) {
                $stmt = new ArrayStatement($data[$realKey]);
            } elseif (array_key_exists($realKey, $data)) {
                $stmt = new ArrayStatement([]);
            }
        }

        if (! isset($stmt)) {
            $stmt = new ResultCacheStatement(
                $this->executeQuery($sql, $params, $types),
                $resultCache,
                $cacheKey,
                $realKey,
                $qcp->getLifetime()
            );
        }

        $stmt->setFetchMode($this->defaultFetchMode);

        return $this->ensureForwardCompatibilityStatement($stmt);
    }

    /**
     * @return ForwardCompatibility\Result
     */
    private function ensureForwardCompatibilityStatement(ResultStatement $stmt)
    {
        return ForwardCompatibility\Result::ensure($stmt);
    }

    /**
     * Executes an, optionally parametrized, SQL query and returns the result,
     * applying a given projection/transformation function on each row of the result.
     *
     * @deprecated
     *
     * @param string  $sql      The SQL query to execute.
     * @param mixed[] $params   The parameters, if any.
     * @param Closure $function The transformation function that is applied on each row.
     *                           The function receives a single parameter, an array, that
     *                           represents a row of the result set.
     *
     * @return mixed[] The projected result of the query.
     */
    public function project($sql, array $params, Closure $function)
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/3823',
            'Connection::project() is deprecated without replacement, implement data projections in your own code.'
        );

        $result = [];
        $stmt   = $this->executeQuery($sql, $params);

        while ($row = $stmt->fetch()) {
            $result[] = $function($row);
        }

        $stmt->closeCursor();

        return $result;
    }

    /**
     * Executes an SQL statement, returning a result set as a Statement object.
     *
     * @deprecated Use {@link executeQuery()} instead.
     *
     * @return \Doctrine\DBAL\Driver\Statement
     *
     * @throws Exception
     */
    public function query()
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/4163',
            'Connection::query() is deprecated, use Connection::executeQuery() instead.'
        );

        $connection = $this->getWrappedConnection();

        $args = func_get_args();

        $logger = $this->_config->getSQLLogger();
        if ($logger) {
            $logger->startQuery($args[0]);
        }

        try {
            $statement = $connection->query(...$args);
        } catch (Throwable $e) {
            $this->handleExceptionDuringQuery($e, $args[0]);
        }

        $statement->setFetchMode($this->defaultFetchMode);

        if ($logger) {
            $logger->stopQuery();
        }

        return $statement;
    }

    /**
     * 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.
     *
     * @deprecated Use {@link executeStatement()} instead.
     *
     * @param string                                                               $sql    SQL statement
     * @param array<int, mixed>|array<string, mixed>                               $params Statement parameters
     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
     *
     * @return int|string The number of affected rows.
     *
     * @throws Exception
     */
    public function executeUpdate($sql, array $params = [], array $types = [])
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/4163',
            'Connection::executeUpdate() is deprecated, use Connection::executeStatement() instead.'
        );

        return $this->executeStatement($sql, $params, $types);
    }

    /**
     * Executes an SQL statement with the given parameters and returns the number of affected rows.
     *
     * Could be used for:
     *  - DML statements: INSERT, UPDATE, DELETE, etc.
     *  - DDL statements: CREATE, DROP, ALTER, etc.
     *  - DCL statements: GRANT, REVOKE, etc.
     *  - Session control statements: ALTER SESSION, SET, DECLARE, etc.
     *  - Other statements that don't yield a row set.
     *
     * This method supports PDO binding types as well as DBAL mapping types.
     *
     * @param string                                                               $sql    SQL statement
     * @param array<int, mixed>|array<string, mixed>                               $params Statement parameters
     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
     *
     * @return int|string The number of affected rows.
     *
     * @throws Exception
     */
    public function executeStatement($sql, array $params = [], array $types = [])
    {
        $connection = $this->getWrappedConnection();

        $logger = $this->_config->getSQLLogger();
        if ($logger) {
            $logger->startQuery($sql, $params, $types);
        }

        try {
            if ($params) {
                [$sql, $params, $types] = SQLParserUtils::expandListParameters($sql, $params, $types);

                $stmt = $connection->prepare($sql);

                if ($types) {
                    $this->_bindTypedValues($stmt, $params, $types);
                    $stmt->execute();
                } else {
                    $stmt->execute($params);
                }

                $result = $stmt->rowCount();
            } else {
                $result = $connection->exec($sql);
            }
        } catch (Throwable $e) {
            $this->handleExceptionDuringQuery(
                $e,
                $sql,
                $params,
                $types
            );
        }

        if ($logger) {
            $logger->stopQuery();
        }

        return $result;
    }

    /**
     * Executes an SQL statement and return the number of affected rows.
     *
     * @deprecated Use {@link executeStatement()} instead.
     *
     * @param string $sql
     *
     * @return int|string The number of affected rows.
     *
     * @throws Exception
     */
    public function exec($sql)
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/4163',
            'Connection::exec() is deprecated, use Connection::executeStatement() instead.'
        );

        $connection = $this->getWrappedConnection();

        $logger = $this->_config->getSQLLogger();
        if ($logger) {
            $logger->startQuery($sql);
        }

        try {
            $result = $connection->exec($sql);
        } catch (Throwable $e) {
            $this->handleExceptionDuringQuery($e, $sql);
        }

        if ($logger) {
            $logger->stopQuery();
        }

        return $result;
    }

    /**
     * Returns the current transaction nesting level.
     *
     * @return int The nesting level. A value of 0 means there's no active transaction.
     */
    public function getTransactionNestingLevel()
    {
        return $this->transactionNestingLevel;
    }

    /**
     * Fetches the SQLSTATE associated with the last database operation.
     *
     * @deprecated The error information is available via exceptions.
     *
     * @return string|null The last error code.
     */
    public function errorCode()
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/3507',
            'Connection::errorCode() is deprecated, use getCode() or getSQLState() on Exception instead.'
        );

        return $this->getWrappedConnection()->errorCode();
    }

    /**
     * {@inheritDoc}
     *
     * @deprecated The error information is available via exceptions.
     */
    public function errorInfo()
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/3507',
            'Connection::errorInfo() is deprecated, use getCode() or getSQLState() on Exception instead.'
        );

        return $this->getWrappedConnection()->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|null $name Name of the sequence object from which the ID should be returned.
     *
     * @return string|int|false A string representation of the last inserted ID.
     */
    public function lastInsertId($name = null)
    {
        return $this->getWrappedConnection()->lastInsertId($name);
    }

    /**
     * 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.
     *
     * @return mixed The value returned by $func
     *
     * @throws Throwable
     */
    public function transactional(Closure $func)
    {
        $this->beginTransaction();
        try {
            $res = $func($this);
            $this->commit();

            return $res;
        } catch (Throwable $e) {
            $this->rollBack();

            throw $e;
        }
    }

    /**
     * Sets if nested transactions should use savepoints.
     *
     * @param bool $nestTransactionsWithSavepoints
     *
     * @return void
     *
     * @throws ConnectionException
     */
    public function setNestTransactionsWithSavepoints($nestTransactionsWithSavepoints)
    {
        if ($this->transactionNestingLevel > 0) {
            throw ConnectionException::mayNotAlterNestedTransactionWithSavepointsInTransaction();
        }

        if (! $this->getDatabasePlatform()->supportsSavepoints()) {
            throw ConnectionException::savepointsNotSupported();
        }

        $this->nestTransactionsWithSavepoints = (bool) $nestTransactionsWithSavepoints;
    }

    /**
     * Gets if nested transactions should use savepoints.
     *
     * @return bool
     */
    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;
    }

    /**
     * {@inheritDoc}
     */
    public function beginTransaction()
    {
        $connection = $this->getWrappedConnection();

        ++$this->transactionNestingLevel;

        $logger = $this->_config->getSQLLogger();

        if ($this->transactionNestingLevel === 1) {
            if ($logger) {
                $logger->startQuery('"START TRANSACTION"');
            }

            $connection->beginTransaction();

            if ($logger) {
                $logger->stopQuery();
            }
        } elseif ($this->nestTransactionsWithSavepoints) {
            if ($logger) {
                $logger->startQuery('"SAVEPOINT"');
            }

            $this->createSavepoint($this->_getNestedTransactionSavePointName());
            if ($logger) {
                $logger->stopQuery();
            }
        }

        return true;
    }

    /**
     * {@inheritDoc}
     *
     * @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();
        }

        $result = true;

        $connection = $this->getWrappedConnection();

        $logger = $this->_config->getSQLLogger();

        if ($this->transactionNestingLevel === 1) {
            if ($logger) {
                $logger->startQuery('"COMMIT"');
            }

            $result = $connection->commit();

            if ($logger) {
                $logger->stopQuery();
            }
        } elseif ($this->nestTransactionsWithSavepoints) {
            if ($logger) {
                $logger->startQuery('"RELEASE SAVEPOINT"');
            }

            $this->releaseSavepoint($this->_getNestedTransactionSavePointName());
            if ($logger) {
                $logger->stopQuery();
            }
        }

        --$this->transactionNestingLevel;

        if ($this->autoCommit !== false || $this->transactionNestingLevel !== 0) {
            return $result;
        }

        $this->beginTransaction();

        return $result;
    }

    /**
     * Commits all current nesting transactions.
     */
    private function commitAll(): void
    {
        while ($this->transactionNestingLevel !== 0) {
            if ($this->autoCommit === false && $this->transactionNestingLevel === 1) {
                // When in no auto-commit mode, the last nesting commit immediately starts a new transaction.
                // Therefore we need to do the final commit here and then leave to avoid an infinite loop.
                $this->commit();

                return;
            }

            $this->commit();
        }
    }

    /**
     * Cancels any database changes done during the current transaction.
     *
     * @return bool
     *
     * @throws ConnectionException If the rollback operation failed.
     */
    public function rollBack()
    {
        if ($this->transactionNestingLevel === 0) {
            throw ConnectionException::noActiveTransaction();
        }

        $connection = $this->getWrappedConnection();

        $logger = $this->_config->getSQLLogger();

        if ($this->transactionNestingLevel === 1) {
            if ($logger) {
                $logger->startQuery('"ROLLBACK"');
            }

            $this->transactionNestingLevel = 0;
            $connection->rollBack();
            $this->isRollbackOnly = false;
            if ($logger) {
                $logger->stopQuery();
            }

            if ($this->autoCommit === false) {
                $this->beginTransaction();
            }
        } elseif ($this->nestTransactionsWithSavepoints) {
            if ($logger) {
                $logger->startQuery('"ROLLBACK TO SAVEPOINT"');
            }

            $this->rollbackSavepoint($this->_getNestedTransactionSavePointName());
            --$this->transactionNestingLevel;
            if ($logger) {
                $logger->stopQuery();
            }
        } else {
            $this->isRollbackOnly = true;
            --$this->transactionNestingLevel;
        }

        return true;
    }

    /**
     * Creates a new savepoint.
     *
     * @param string $savepoint The name of the savepoint to create.
     *
     * @return void
     *
     * @throws ConnectionException
     */
    public function createSavepoint($savepoint)
    {
        $platform = $this->getDatabasePlatform();

        if (! $platform->supportsSavepoints()) {
            throw ConnectionException::savepointsNotSupported();
        }

        $this->getWrappedConnection()->exec($platform->createSavePoint($savepoint));
    }

    /**
     * Releases the given savepoint.
     *
     * @param string $savepoint The name of the savepoint to release.
     *
     * @return void
     *
     * @throws ConnectionException
     */
    public function releaseSavepoint($savepoint)
    {
        $platform = $this->getDatabasePlatform();

        if (! $platform->supportsSavepoints()) {
            throw ConnectionException::savepointsNotSupported();
        }

        if (! $platform->supportsReleaseSavepoints()) {
            return;
        }

        $this->getWrappedConnection()->exec($platform->releaseSavePoint($savepoint));
    }

    /**
     * Rolls back to the given savepoint.
     *
     * @param string $savepoint The name of the savepoint to rollback to.
     *
     * @return void
     *
     * @throws ConnectionException
     */
    public function rollbackSavepoint($savepoint)
    {
        $platform = $this->getDatabasePlatform();

        if (! $platform->supportsSavepoints()) {
            throw ConnectionException::savepointsNotSupported();
        }

        $this->getWrappedConnection()->exec($platform->rollbackSavePoint($savepoint));
    }

    /**
     * Gets the wrapped driver connection.
     *
     * @return DriverConnection
     */
    public function getWrappedConnection()
    {
        $this->connect();

        assert($this->_conn !== null);

        return $this->_conn;
    }

    /**
     * Gets the SchemaManager that can be used to inspect or change the
     * database schema through the connection.
     *
     * @return AbstractSchemaManager
     */
    public function getSchemaManager()
    {
        if ($this->_schemaManager === null) {
            $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.
     *
     * @return void
     *
     * @throws ConnectionException If no transaction is active.
     */
    public function setRollbackOnly()
    {
        if ($this->transactionNestingLevel === 0) {
            throw ConnectionException::noActiveTransaction();
        }

        $this->isRollbackOnly = true;
    }

    /**
     * Checks whether the current transaction is marked for rollback only.
     *
     * @return bool
     *
     * @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->getDatabasePlatform());
    }

    /**
     * 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->getDatabasePlatform());
    }

    /**
     * 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.
     *
     * @internal Duck-typing used on the $stmt parameter to support driver statements as well as
     *           raw PDOStatement instances.
     *
     * @param \Doctrine\DBAL\Driver\Statement                                      $stmt   Prepared statement
     * @param array<int, mixed>|array<string, mixed>                               $params Statement parameters
     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
     *
     * @return void
     */
    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 = array_key_exists(0, $types) ? -1 : 0;
            $bindIndex  = 1;
            foreach ($params as $value) {
                $typeIndex = $bindIndex + $typeOffset;
                if (isset($types[$typeIndex])) {
                    $type                  = $types[$typeIndex];
                    [$value, $bindingType] = $this->getBindingInfo($value, $type);
                    $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];
                    [$value, $bindingType] = $this->getBindingInfo($value, $type);
                    $stmt->bindValue($name, $value, $bindingType);
                } else {
                    $stmt->bindValue($name, $value);
                }
            }
        }
    }

    /**
     * Gets the binding type of a given type. The given type can be a PDO or DBAL mapping type.
     *
     * @param mixed                $value The value to bind.
     * @param int|string|Type|null $type  The type to bind (PDO or DBAL).
     *
     * @return array{mixed, int} [0] => the (escaped) value, [1] => the binding type.
     */
    private function getBindingInfo($value, $type): array
    {
        if (is_string($type)) {
            $type = Type::getType($type);
        }

        if ($type instanceof Type) {
            $value       = $type->convertToDatabaseValue($value, $this->getDatabasePlatform());
            $bindingType = $type->getBindingType();
        } else {
            $bindingType = $type ?? ParameterType::STRING;
        }

        return [$value, $bindingType];
    }

    /**
     * Resolves the parameters to a format which can be displayed.
     *
     * @internal This is a purely internal method. If you rely on this method, you are advised to
     *           copy/paste the code as this method may change, or be removed without prior notice.
     *
     * @param array<int, mixed>|array<string, mixed>                               $params Query parameters
     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
     *
     * @return array<int, int|string|Type|null>|array<string, int|string|Type|null>
     */
    public function resolveParams(array $params, array $types)
    {
        $resolvedParams = [];

        // Check whether parameters are positional or named. Mixing is not allowed, just like in PDO.
        if (is_int(key($params))) {
            // Positional parameters
            $typeOffset = array_key_exists(0, $types) ? -1 : 0;
            $bindIndex  = 1;
            foreach ($params as $value) {
                $typeIndex = $bindIndex + $typeOffset;
                if (isset($types[$typeIndex])) {
                    $type                       = $types[$typeIndex];
                    [$value]                    = $this->getBindingInfo($value, $type);
                    $resolvedParams[$bindIndex] = $value;
                } else {
                    $resolvedParams[$bindIndex] = $value;
                }

                ++$bindIndex;
            }
        } else {
            // Named parameters
            foreach ($params as $name => $value) {
                if (isset($types[$name])) {
                    $type                  = $types[$name];
                    [$value]               = $this->getBindingInfo($value, $type);
                    $resolvedParams[$name] = $value;
                } else {
                    $resolvedParams[$name] = $value;
                }
            }
        }

        return $resolvedParams;
    }

    /**
     * Creates a new instance of a SQL query builder.
     *
     * @return QueryBuilder
     */
    public function createQueryBuilder()
    {
        return new Query\QueryBuilder($this);
    }

    /**
     * Ping the server
     *
     * When the server is not available the method returns FALSE.
     * It is responsibility of the developer to handle this case
     * and abort the request or reconnect manually:
     *
     * @deprecated
     *
     * @return bool
     *
     * @example
     *
     *   if ($conn->ping() === false) {
     *      $conn->close();
     *      $conn->connect();
     *   }
     *
     * It is undefined if the underlying driver attempts to reconnect
     * or disconnect when the connection is not available anymore
     * as long it returns TRUE when a reconnect succeeded and
     * FALSE when the connection was dropped.
     */
    public function ping()
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/4119',
            'Retry and reconnecting lost connections now happens automatically, ping() will be removed in DBAL 3.'
        );

        $connection = $this->getWrappedConnection();

        if ($connection instanceof PingableConnection) {
            return $connection->ping();
        }

        try {
            $this->query($this->getDatabasePlatform()->getDummySelectSQL());

            return true;
        } catch (DBALException $e) {
            return false;
        }
    }

    /**
     * @internal
     *
     * @param array<int, mixed>|array<string, mixed>                               $params
     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types
     *
     * @psalm-return never-return
     *
     * @throws Exception
     */
    public function handleExceptionDuringQuery(Throwable $e, string $sql, array $params = [], array $types = []): void
    {
        $this->throw(
            Exception::driverExceptionDuringQuery(
                $this->_driver,
                $e,
                $sql,
                $this->resolveParams($params, $types)
            )
        );
    }

    /**
     * @internal
     *
     * @psalm-return never-return
     *
     * @throws Exception
     */
    public function handleDriverException(Throwable $e): void
    {
        $this->throw(
            Exception::driverException(
                $this->_driver,
                $e
            )
        );
    }

    /**
     * @internal
     *
     * @psalm-return never-return
     *
     * @throws Exception
     */
    private function throw(Exception $e): void
    {
        if ($e instanceof ConnectionLost) {
            $this->close();
        }

        throw $e;
    }

    private function ensureHasKeyValue(ResultStatement $stmt): void
    {
        $columnCount = $stmt->columnCount();

        if ($columnCount < 2) {
            throw NoKeyValue::fromColumnCount($columnCount);
        }
    }
}
<?php

namespace Doctrine\DBAL;

/**
 * Contains all DBAL LockModes.
 */
class LockMode
{
    public const NONE              = 0;
    public const OPTIMISTIC        = 1;
    public const PESSIMISTIC_READ  = 2;
    public const PESSIMISTIC_WRITE = 4;

    /**
     * Private constructor. This class cannot be instantiated.
     *
     * @codeCoverageIgnore
     */
    final private function __construct()
    {
    }
}
<?php

namespace Doctrine\DBAL;

use Doctrine\Common\EventManager;
use Doctrine\DBAL\Driver\DrizzlePDOMySql;
use Doctrine\DBAL\Driver\IBMDB2;
use Doctrine\DBAL\Driver\Mysqli;
use Doctrine\DBAL\Driver\OCI8;
use Doctrine\DBAL\Driver\PDO;
use Doctrine\DBAL\Driver\SQLAnywhere;
use Doctrine\DBAL\Driver\SQLSrv;

use function array_keys;
use function array_merge;
use function class_implements;
use function in_array;
use function is_string;
use function is_subclass_of;
use function parse_str;
use function parse_url;
use function preg_replace;
use function rawurldecode;
use function str_replace;
use function strpos;
use function substr;

/**
 * Factory for creating {@link Connection} instances.
 *
 * @psalm-type OverrideParams = array{
 *     charset?: string,
 *     dbname?: string,
 *     default_dbname?: string,
 *     driver?: key-of<self::DRIVER_MAP>,
 *     driverClass?: class-string<Driver>,
 *     driverOptions?: array<mixed>,
 *     host?: string,
 *     password?: string,
 *     path?: string,
 *     pdo?: \PDO,
 *     platform?: Platforms\AbstractPlatform,
 *     port?: int,
 *     user?: string,
 *     unix_socket?: string,
 * }
 * @psalm-type Params = array{
 *     charset?: string,
 *     dbname?: string,
 *     default_dbname?: string,
 *     driver?: key-of<self::DRIVER_MAP>,
 *     driverClass?: class-string<Driver>,
 *     driverOptions?: array<mixed>,
 *     host?: string,
 *     keepSlave?: bool,
 *     keepReplica?: bool,
 *     master?: OverrideParams,
 *     memory?: bool,
 *     password?: string,
 *     path?: string,
 *     pdo?: \PDO,
 *     platform?: Platforms\AbstractPlatform,
 *     port?: int,
 *     primary?: OverrideParams,
 *     replica?: array<OverrideParams>,
 *     sharding?: array<string,mixed>,
 *     slaves?: array<OverrideParams>,
 *     user?: string,
 *     wrapperClass?: class-string<Connection>,
 *     unix_socket?: string,
 * }
 */
final class DriverManager
{
    /**
     * List of supported drivers and their mappings to the driver classes.
     *
     * To add your own driver use the 'driverClass' parameter to {@link DriverManager::getConnection()}.
     */
    private const DRIVER_MAP = [
        'pdo_mysql'          => PDO\MySQL\Driver::class,
        'pdo_sqlite'         => PDO\SQLite\Driver::class,
        'pdo_pgsql'          => PDO\PgSQL\Driver::class,
        'pdo_oci'            => PDO\OCI\Driver::class,
        'oci8'               => OCI8\Driver::class,
        'ibm_db2'            => IBMDB2\Driver::class,
        'pdo_sqlsrv'         => PDO\SQLSrv\Driver::class,
        'mysqli'             => Mysqli\Driver::class,
        'drizzle_pdo_mysql'  => DrizzlePDOMySql\Driver::class,
        'sqlanywhere'        => SQLAnywhere\Driver::class,
        'sqlsrv'             => SQLSrv\Driver::class,
    ];

    /**
     * List of URL schemes from a database URL and their mappings to driver.
     *
     * @var string[]
     */
    private static $driverSchemeAliases = [
        'db2'        => 'ibm_db2',
        'mssql'      => 'pdo_sqlsrv',
        'mysql'      => 'pdo_mysql',
        'mysql2'     => 'pdo_mysql', // Amazon RDS, for some weird reason
        'postgres'   => 'pdo_pgsql',
        'postgresql' => 'pdo_pgsql',
        'pgsql'      => 'pdo_pgsql',
        'sqlite'     => 'pdo_sqlite',
        'sqlite3'    => 'pdo_sqlite',
    ];

    /**
     * Private constructor. This class cannot be instantiated.
     *
     * @codeCoverageIgnore
     */
    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 array keys of {@link DRIVER_MAP},
     * OR 'driverClass' that contains the full class name (with namespace) of the
     * driver class to instantiate.
     *
     * Other (optional) parameters:
     *
     * <b>user (string)</b>:
     * The username to use when connecting.
     *
     * <b>password (string)</b>:
     * The password to use when connecting.
     *
     * <b>driverOptions (array)</b>:
     * Any additional driver-specific options for the driver. These are just passed
     * through to the driver.
     *
     * <b>pdo</b>:
     * You can pass an existing PDO instance through this parameter. The PDO
     * instance will be wrapped in a Doctrine\DBAL\Connection.
     * This feature is deprecated and no longer supported in 3.0.x version.
     *
     * <b>wrapperClass</b>:
     * You may specify a custom wrapper class through the 'wrapperClass'
     * parameter but this class MUST inherit from Doctrine\DBAL\Connection.
     *
     * <b>driverClass</b>:
     * The driver class to use.
     *
     * @param array<string,mixed> $params
     * @param Configuration|null  $config       The configuration to use.
     * @param EventManager|null   $eventManager The event manager to use.
     * @psalm-param array{
     *     charset?: string,
     *     dbname?: string,
     *     default_dbname?: string,
     *     driver?: key-of<self::DRIVER_MAP>,
     *     driverClass?: class-string<Driver>,
     *     driverOptions?: array<mixed>,
     *     host?: string,
     *     keepSlave?: bool,
     *     keepReplica?: bool,
     *     master?: OverrideParams,
     *     memory?: bool,
     *     password?: string,
     *     path?: string,
     *     pdo?: \PDO,
     *     platform?: Platforms\AbstractPlatform,
     *     port?: int,
     *     primary?: OverrideParams,
     *     replica?: array<OverrideParams>,
     *     sharding?: array<string,mixed>,
     *     slaves?: array<OverrideParams>,
     *     user?: string,
     *     wrapperClass?: class-string<T>,
     * } $params
     * @phpstan-param array<string,mixed> $params
     *
     * @psalm-return ($params is array{wrapperClass:mixed} ? T : Connection)
     *
     * @throws Exception
     *
     * @template T of Connection
     */
    public static function getConnection(
        array $params,
        ?Configuration $config = null,
        ?EventManager $eventManager = null
    ): Connection {
        // create default config and event manager, if not set
        if (! $config) {
            $config = new Configuration();
        }

        if (! $eventManager) {
            $eventManager = new EventManager();
        }

        $params = self::parseDatabaseUrl($params);

        // @todo: deprecated, notice thrown by connection constructor
        if (isset($params['master'])) {
            $params['master'] = self::parseDatabaseUrl($params['master']);
        }

        // @todo: deprecated, notice thrown by connection constructor
        if (isset($params['slaves'])) {
            foreach ($params['slaves'] as $key => $slaveParams) {
                $params['slaves'][$key] = self::parseDatabaseUrl($slaveParams);
            }
        }

        // URL support for PrimaryReplicaConnection
        if (isset($params['primary'])) {
            $params['primary'] = self::parseDatabaseUrl($params['primary']);
        }

        if (isset($params['replica'])) {
            foreach ($params['replica'] as $key => $replicaParams) {
                $params['replica'][$key] = self::parseDatabaseUrl($replicaParams);
            }
        }

        // URL support for PoolingShardConnection
        if (isset($params['global'])) {
            $params['global'] = self::parseDatabaseUrl($params['global']);
        }

        if (isset($params['shards'])) {
            foreach ($params['shards'] as $key => $shardParams) {
                $params['shards'][$key] = self::parseDatabaseUrl($shardParams);
            }
        }

        // check for existing pdo object
        if (isset($params['pdo']) && ! $params['pdo'] instanceof \PDO) {
            throw Exception::invalidPdoInstance();
        }

        if (isset($params['pdo'])) {
            $params['pdo']->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
            $params['driver'] = 'pdo_' . $params['pdo']->getAttribute(\PDO::ATTR_DRIVER_NAME);
        }

        $driver = self::createDriver($params);

        $wrapperClass = Connection::class;
        if (isset($params['wrapperClass'])) {
            if (! is_subclass_of($params['wrapperClass'], $wrapperClass)) {
                throw Exception::invalidWrapperClass($params['wrapperClass']);
            }

            /** @var class-string<Connection> $wrapperClass */
            $wrapperClass = $params['wrapperClass'];
        }

        return new $wrapperClass($params, $driver, $config, $eventManager);
    }

    /**
     * Returns the list of supported drivers.
     *
     * @return string[]
     */
    public static function getAvailableDrivers(): array
    {
        return array_keys(self::DRIVER_MAP);
    }

    /**
     * @param array<string,mixed> $params
     * @psalm-param Params $params
     * @phpstan-param array<string,mixed> $params
     *
     * @throws Exception
     */
    private static function createDriver(array $params): Driver
    {
        if (isset($params['driverClass'])) {
            $interfaces = class_implements($params['driverClass'], true);

            if ($interfaces === false || ! in_array(Driver::class, $interfaces)) {
                throw Exception::invalidDriverClass($params['driverClass']);
            }

            return new $params['driverClass']();
        }

        if (isset($params['driver'])) {
            if (! isset(self::DRIVER_MAP[$params['driver']])) {
                throw Exception::unknownDriver($params['driver'], array_keys(self::DRIVER_MAP));
            }

            $class = self::DRIVER_MAP[$params['driver']];

            return new $class();
        }

        throw Exception::driverRequired();
    }

    /**
     * Normalizes the given connection URL path.
     *
     * @return string The normalized connection URL path
     */
    private static function normalizeDatabaseUrlPath(string $urlPath): string
    {
        // Trim leading slash from URL path.
        return substr($urlPath, 1);
    }

    /**
     * Extracts parts from a database URL, if present, and returns an
     * updated list of parameters.
     *
     * @param mixed[] $params The list of parameters.
     * @psalm-param Params $params
     * @phpstan-param array<string,mixed> $params
     *
     * @return mixed[] A modified list of parameters with info from a database
     *                 URL extracted into indidivual parameter parts.
     * @psalm-return Params
     * @phpstan-return array<string,mixed>
     *
     * @throws Exception
     */
    private static function parseDatabaseUrl(array $params): array
    {
        if (! isset($params['url'])) {
            return $params;
        }

        // (pdo_)?sqlite3?:///... => (pdo_)?sqlite3?://localhost/... or else the URL will be invalid
        $url = preg_replace('#^((?:pdo_)?sqlite3?):///#', '$1://localhost/', $params['url']);
        $url = parse_url($url);

        if ($url === false) {
            throw new Exception('Malformed parameter "url".');
        }

        foreach ($url as $param => $value) {
            if (! is_string($value)) {
                continue;
            }

            $url[$param] = rawurldecode($value);
        }

        // If we have a connection URL, we have to unset the default PDO instance connection parameter (if any)
        // as we cannot merge connection details from the URL into the PDO instance (URL takes precedence).
        unset($params['pdo']);

        $params = self::parseDatabaseUrlScheme($url['scheme'] ?? null, $params);

        if (isset($url['host'])) {
            $params['host'] = $url['host'];
        }

        if (isset($url['port'])) {
            $params['port'] = $url['port'];
        }

        if (isset($url['user'])) {
            $params['user'] = $url['user'];
        }

        if (isset($url['pass'])) {
            $params['password'] = $url['pass'];
        }

        $params = self::parseDatabaseUrlPath($url, $params);
        $params = self::parseDatabaseUrlQuery($url, $params);

        return $params;
    }

    /**
     * Parses the given connection URL and resolves the given connection parameters.
     *
     * Assumes that the connection URL scheme is already parsed and resolved into the given connection parameters
     * via {@link parseDatabaseUrlScheme}.
     *
     * @see parseDatabaseUrlScheme
     *
     * @param mixed[] $url    The URL parts to evaluate.
     * @param mixed[] $params The connection parameters to resolve.
     *
     * @return mixed[] The resolved connection parameters.
     */
    private static function parseDatabaseUrlPath(array $url, array $params): array
    {
        if (! isset($url['path'])) {
            return $params;
        }

        $url['path'] = self::normalizeDatabaseUrlPath($url['path']);

        // If we do not have a known DBAL driver, we do not know any connection URL path semantics to evaluate
        // and therefore treat the path as regular DBAL connection URL path.
        if (! isset($params['driver'])) {
            return self::parseRegularDatabaseUrlPath($url, $params);
        }

        if (strpos($params['driver'], 'sqlite') !== false) {
            return self::parseSqliteDatabaseUrlPath($url, $params);
        }

        return self::parseRegularDatabaseUrlPath($url, $params);
    }

    /**
     * Parses the query part of the given connection URL and resolves the given connection parameters.
     *
     * @param mixed[] $url    The connection URL parts to evaluate.
     * @param mixed[] $params The connection parameters to resolve.
     *
     * @return mixed[] The resolved connection parameters.
     */
    private static function parseDatabaseUrlQuery(array $url, array $params): array
    {
        if (! isset($url['query'])) {
            return $params;
        }

        $query = [];

        parse_str($url['query'], $query); // simply ingest query as extra params, e.g. charset or sslmode

        return array_merge($params, $query); // parse_str wipes existing array elements
    }

    /**
     * Parses the given regular connection URL and resolves the given connection parameters.
     *
     * Assumes that the "path" URL part is already normalized via {@link normalizeDatabaseUrlPath}.
     *
     * @see normalizeDatabaseUrlPath
     *
     * @param mixed[] $url    The regular connection URL parts to evaluate.
     * @param mixed[] $params The connection parameters to resolve.
     *
     * @return mixed[] The resolved connection parameters.
     */
    private static function parseRegularDatabaseUrlPath(array $url, array $params): array
    {
        $params['dbname'] = $url['path'];

        return $params;
    }

    /**
     * Parses the given SQLite connection URL and resolves the given connection parameters.
     *
     * Assumes that the "path" URL part is already normalized via {@link normalizeDatabaseUrlPath}.
     *
     * @see normalizeDatabaseUrlPath
     *
     * @param mixed[] $url    The SQLite connection URL parts to evaluate.
     * @param mixed[] $params The connection parameters to resolve.
     *
     * @return mixed[] The resolved connection parameters.
     */
    private static function parseSqliteDatabaseUrlPath(array $url, array $params): array
    {
        if ($url['path'] === ':memory:') {
            $params['memory'] = true;

            return $params;
        }

        $params['path'] = $url['path']; // pdo_sqlite driver uses 'path' instead of 'dbname' key

        return $params;
    }

    /**
     * Parses the scheme part from given connection URL and resolves the given connection parameters.
     *
     * @param string|null $scheme The connection URL scheme, if available
     * @param mixed[]     $params The connection parameters to resolve.
     *
     * @return mixed[] The resolved connection parameters.
     *
     * @throws Exception If parsing failed or resolution is not possible.
     */
    private static function parseDatabaseUrlScheme($scheme, array $params): array
    {
        if ($scheme !== null) {
            // The requested driver from the URL scheme takes precedence
            // over the default custom driver from the connection parameters (if any).
            unset($params['driverClass']);

            // URL schemes must not contain underscores, but dashes are ok
            $driver = str_replace('-', '_', $scheme);

            // The requested driver from the URL scheme takes precedence over the
            // default driver from the connection parameters. If the driver is
            // an alias (e.g. "postgres"), map it to the actual name ("pdo-pgsql").
            // Otherwise, let checkParams decide later if the driver exists.
            $params['driver'] = self::$driverSchemeAliases[$driver] ?? $driver;

            return $params;
        }

        // If a schemeless connection URL is given, we require a default driver or default custom driver
        // as connection parameter.
        if (! isset($params['driverClass']) && ! isset($params['driver'])) {
            throw Exception::driverRequired($params['url']);
        }

        return $params;
    }
}
<?php

namespace Doctrine\DBAL;

/**
 * @psalm-immutable
 */
class ConnectionException extends Exception
{
    /**
     * @return ConnectionException
     */
    public static function commitFailedRollbackOnly()
    {
        return new self('Transaction commit failed because the transaction has been marked for rollback only.');
    }

    /**
     * @return ConnectionException
     */
    public static function noActiveTransaction()
    {
        return new self('There is no active transaction.');
    }

    /**
     * @return ConnectionException
     */
    public static function savepointsNotSupported()
    {
        return new self('Savepoints are not supported by this driver.');
    }

    /**
     * @return ConnectionException
     */
    public static function mayNotAlterNestedTransactionWithSavepointsInTransaction()
    {
        return new self('May not alter the nested transaction with savepoints behavior while a transaction is open.');
    }
}
<?php

namespace Doctrine\DBAL;

use Doctrine\DBAL\Platforms\AbstractPlatform;

/**
 * Contract for a driver that is able to create platform instances by version.
 *
 * Doctrine uses different platform classes for different vendor versions to
 * support the correct features and SQL syntax of each version.
 * This interface should be implemented by drivers that are capable to do this
 * distinction.
 */
interface VersionAwarePlatformDriver
{
    /**
     * Factory method for creating the appropriate platform instance for the given version.
     *
     * @param string $version The platform/server version string to evaluate. This should be given in the notation
     *                        the underlying database vendor uses.
     *
     * @return AbstractPlatform
     *
     * @throws Exception If the given version string could not be evaluated.
     */
    public function createDatabasePlatformForVersion($version);
}
<?php

namespace Doctrine\DBAL;

use Doctrine\DBAL\Driver\DriverException as DeprecatedDriverException;
use Doctrine\DBAL\Driver\ExceptionConverterDriver;
use Doctrine\DBAL\Exception\DriverException;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\Type;
use Throwable;

use function array_map;
use function bin2hex;
use function get_class;
use function gettype;
use function implode;
use function is_object;
use function is_resource;
use function is_string;
use function json_encode;
use function preg_replace;
use function spl_object_hash;
use function sprintf;

/**
 * @deprecated Use {@link Exception} instead
 *
 * @psalm-immutable
 */
class DBALException extends \Exception
{
    /**
     * @param string $method
     *
     * @return Exception
     */
    public static function notSupported($method)
    {
        return new Exception(sprintf("Operation '%s' is not supported by platform.", $method));
    }

    /**
     * @deprecated Use {@link invalidPlatformType()} instead.
     */
    public static function invalidPlatformSpecified(): self
    {
        return new Exception(
            "Invalid 'platform' option specified, need to give an instance of " . AbstractPlatform::class . '.'
        );
    }

    /**
     * @param mixed $invalidPlatform
     */
    public static function invalidPlatformType($invalidPlatform): self
    {
        if (is_object($invalidPlatform)) {
            return new Exception(
                sprintf(
                    "Option 'platform' must be a subtype of '%s', instance of '%s' given",
                    AbstractPlatform::class,
                    get_class($invalidPlatform)
                )
            );
        }

        return new Exception(
            sprintf(
                "Option 'platform' must be an object and subtype of '%s'. Got '%s'",
                AbstractPlatform::class,
                gettype($invalidPlatform)
            )
        );
    }

    /**
     * Returns a new instance for an invalid specified platform version.
     *
     * @param string $version        The invalid platform version given.
     * @param string $expectedFormat The expected platform version format.
     *
     * @return Exception
     */
    public static function invalidPlatformVersionSpecified($version, $expectedFormat)
    {
        return new Exception(
            sprintf(
                'Invalid platform version "%s" specified. ' .
                'The platform version has to be specified in the format: "%s".',
                $version,
                $expectedFormat
            )
        );
    }

    /**
     * @deprecated Passing a PDO instance in connection parameters is deprecated.
     *
     * @return Exception
     */
    public static function invalidPdoInstance()
    {
        return new Exception(
            "The 'pdo' option was used in DriverManager::getConnection() but no " .
            'instance of PDO was given.'
        );
    }

    /**
     * @param string|null $url The URL that was provided in the connection parameters (if any).
     *
     * @return Exception
     */
    public static function driverRequired($url = null)
    {
        if ($url) {
            return new Exception(
                sprintf(
                    "The options 'driver' or 'driverClass' are mandatory if a connection URL without scheme " .
                    'is given to DriverManager::getConnection(). Given URL: %s',
                    $url
                )
            );
        }

        return new Exception("The options 'driver' or 'driverClass' are mandatory if no PDO " .
            'instance is given to DriverManager::getConnection().');
    }

    /**
     * @param string   $unknownDriverName
     * @param string[] $knownDrivers
     *
     * @return Exception
     */
    public static function unknownDriver($unknownDriverName, array $knownDrivers)
    {
        return new Exception("The given 'driver' " . $unknownDriverName . ' is unknown, ' .
            'Doctrine currently supports only the following drivers: ' . implode(', ', $knownDrivers));
    }

    /**
     * @deprecated
     *
     * @param string  $sql
     * @param mixed[] $params
     *
     * @return Exception
     */
    public static function driverExceptionDuringQuery(Driver $driver, Throwable $driverEx, $sql, array $params = [])
    {
        $msg = "An exception occurred while executing '" . $sql . "'";
        if ($params) {
            $msg .= ' with params ' . self::formatParameters($params);
        }

        $msg .= ":\n\n" . $driverEx->getMessage();

        return self::wrapException($driver, $driverEx, $msg);
    }

    /**
     * @deprecated
     *
     * @return Exception
     */
    public static function driverException(Driver $driver, Throwable $driverEx)
    {
        return self::wrapException($driver, $driverEx, 'An exception occurred in driver: ' . $driverEx->getMessage());
    }

    /**
     * @return Exception
     */
    private static function wrapException(Driver $driver, Throwable $driverEx, string $msg)
    {
        if ($driverEx instanceof DriverException) {
            return $driverEx;
        }

        if ($driver instanceof ExceptionConverterDriver && $driverEx instanceof DeprecatedDriverException) {
            return $driver->convertException($msg, $driverEx);
        }

        return new Exception($msg, 0, $driverEx);
    }

    /**
     * Returns a human-readable representation of an array of parameters.
     * This properly handles binary data by returning a hex representation.
     *
     * @param mixed[] $params
     *
     * @return string
     */
    private static function formatParameters(array $params)
    {
        return '[' . implode(', ', array_map(static function ($param) {
            if (is_resource($param)) {
                return (string) $param;
            }

            $json = @json_encode($param);

            if (! is_string($json) || $json === 'null' && is_string($param)) {
                // JSON encoding failed, this is not a UTF-8 string.
                return sprintf('"%s"', preg_replace('/.{2}/', '\\x$0', bin2hex($param)));
            }

            return $json;
        }, $params)) . ']';
    }

    /**
     * @param string $wrapperClass
     *
     * @return Exception
     */
    public static function invalidWrapperClass($wrapperClass)
    {
        return new Exception("The given 'wrapperClass' " . $wrapperClass . ' has to be a ' .
            'subtype of \Doctrine\DBAL\Connection.');
    }

    /**
     * @param string $driverClass
     *
     * @return Exception
     */
    public static function invalidDriverClass($driverClass)
    {
        return new Exception(
            "The given 'driverClass' " . $driverClass . ' has to implement the ' . Driver::class . ' interface.'
        );
    }

    /**
     * @param string $tableName
     *
     * @return Exception
     */
    public static function invalidTableName($tableName)
    {
        return new Exception('Invalid table name specified: ' . $tableName);
    }

    /**
     * @param string $tableName
     *
     * @return Exception
     */
    public static function noColumnsSpecifiedForTable($tableName)
    {
        return new Exception('No columns specified for table ' . $tableName);
    }

    /**
     * @return Exception
     */
    public static function limitOffsetInvalid()
    {
        return new Exception('Invalid Offset in Limit Query, it has to be larger than or equal to 0.');
    }

    /**
     * @param string $name
     *
     * @return Exception
     */
    public static function typeExists($name)
    {
        return new Exception('Type ' . $name . ' already exists.');
    }

    /**
     * @param string $name
     *
     * @return Exception
     */
    public static function unknownColumnType($name)
    {
        return new Exception('Unknown column type "' . $name . '" requested. Any Doctrine type that you use has ' .
            'to be registered with \Doctrine\DBAL\Types\Type::addType(). You can get a list of all the ' .
            'known types with \Doctrine\DBAL\Types\Type::getTypesMap(). If this error occurs during database ' .
            'introspection then you might have forgotten to register all database types for a Doctrine Type. Use ' .
            'AbstractPlatform#registerDoctrineTypeMapping() or have your custom types implement ' .
            'Type#getMappedDatabaseTypes(). If the type name is empty you might ' .
            'have a problem with the cache or forgot some mapping information.');
    }

    /**
     * @param string $name
     *
     * @return Exception
     */
    public static function typeNotFound($name)
    {
        return new Exception('Type to be overwritten ' . $name . ' does not exist.');
    }

    public static function typeNotRegistered(Type $type): self
    {
        return new Exception(
            sprintf('Type of the class %s@%s is not registered.', get_class($type), spl_object_hash($type))
        );
    }

    public static function typeAlreadyRegistered(Type $type): self
    {
        return new Exception(
            sprintf('Type of the class %s@%s is already registered.', get_class($type), spl_object_hash($type))
        );
    }
}
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Driver\SQLSrv\Exception;

use Doctrine\DBAL\Driver\SQLSrv\SQLSrvException;

use function rtrim;
use function sqlsrv_errors;

use const SQLSRV_ERR_ERRORS;

/**
 * @internal
 *
 * @psalm-immutable
 */
final class Error extends SQLSrvException
{
    public static function new(): self
    {
        $message   = '';
        $sqlState  = null;
        $errorCode = null;

        foreach ((array) sqlsrv_errors(SQLSRV_ERR_ERRORS) as $error) {
            $message .= 'SQLSTATE [' . $error['SQLSTATE'] . ', ' . $error['code'] . ']: ' . $error['message'] . "\n";

            if ($sqlState === null) {
                $sqlState = $error['SQLSTATE'];
            }

            if ($errorCode !== null) {
                continue;
            }

            $errorCode = $error['code'];
        }

        if (! $message) {
            $message = 'SQL Server error occurred but no error message was retrieved from driver.';
        }

        return new self(rtrim($message), $sqlState, $errorCode);
    }
}
<?php

namespace Doctrine\DBAL\Driver\SQLSrv;

final class Statement extends SQLSrvStatement
{
}
<?php

namespace Doctrine\DBAL\Driver\SQLSrv;

use Doctrine\DBAL\Driver\AbstractSQLServerDriver;
use Doctrine\DBAL\Driver\AbstractSQLServerDriver\Exception\PortWithoutHost;
use Doctrine\Deprecations\Deprecation;

/**
 * Driver for ext/sqlsrv.
 */
class Driver extends AbstractSQLServerDriver
{
    /**
     * {@inheritdoc}
     */
    public function connect(array $params, $username = null, $password = null, array $driverOptions = [])
    {
        $serverName = '';

        if (isset($params['host'])) {
            $serverName = $params['host'];

            if (isset($params['port'])) {
                $serverName .= ',' . $params['port'];
            }
        } elseif (isset($params['port'])) {
            throw PortWithoutHost::new();
        }

        if (isset($params['dbname'])) {
            $driverOptions['Database'] = $params['dbname'];
        }

        if (isset($params['charset'])) {
            $driverOptions['CharacterSet'] = $params['charset'];
        }

        if ($username !== null) {
            $driverOptions['UID'] = $username;
        }

        if ($password !== null) {
            $driverOptions['PWD'] = $password;
        }

        if (! isset($driverOptions['ReturnDatesAsStrings'])) {
            $driverOptions['ReturnDatesAsStrings'] = 1;
        }

        return new Connection($serverName, $driverOptions);
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated
     */
    public function getName()
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/issues/3580',
            'Driver::getName() is deprecated'
        );

        return 'sqlsrv';
    }
}
<?php

namespace Doctrine\DBAL\Driver\SQLSrv;

/**
 * Last Id Data Container.
 *
 * @internal
 */
class LastInsertId
{
    /** @var int */
    private $id;

    /**
     * @param int $id
     *
     * @return void
     */
    public function setId($id)
    {
        $this->id = $id;
    }

    /**
     * @return int
     */
    public function getId()
    {
        return $this->id;
    }
}
<?php

namespace Doctrine\DBAL\Driver\SQLSrv;

use Doctrine\DBAL\Driver\AbstractDriverException;
use Doctrine\DBAL\Driver\SQLSrv\Exception\Error;

/**
 * @deprecated Use {@link \Doctrine\DBAL\Driver\Exception} instead
 *
 * @psalm-immutable
 */
class SQLSrvException extends AbstractDriverException
{
    /**
     * Helper method to turn sql server errors into exception.
     *
     * @return SQLSrvException
     */
    public static function fromSqlSrvErrors()
    {
        return Error::new();
    }
}
<?php

namespace Doctrine\DBAL\Driver\SQLSrv;

final class Connection extends SQLSrvConnection
{
}
<?php

namespace Doctrine\DBAL\Driver\SQLSrv;

use Doctrine\DBAL\Driver\FetchUtils;
use Doctrine\DBAL\Driver\Result;
use Doctrine\DBAL\Driver\SQLSrv\Exception\Error;
use Doctrine\DBAL\Driver\Statement as StatementInterface;
use Doctrine\DBAL\Driver\StatementIterator;
use Doctrine\DBAL\FetchMode;
use Doctrine\DBAL\ParameterType;
use IteratorAggregate;
use PDO;
use ReturnTypeWillChange;

use function array_key_exists;
use function count;
use function func_get_args;
use function in_array;
use function is_int;
use function is_numeric;
use function sqlsrv_errors;
use function sqlsrv_execute;
use function sqlsrv_fetch;
use function sqlsrv_fetch_array;
use function sqlsrv_fetch_object;
use function sqlsrv_get_field;
use function sqlsrv_next_result;
use function sqlsrv_num_fields;
use function SQLSRV_PHPTYPE_STREAM;
use function SQLSRV_PHPTYPE_STRING;
use function sqlsrv_prepare;
use function sqlsrv_rows_affected;
use function SQLSRV_SQLTYPE_VARBINARY;
use function stripos;

use const SQLSRV_ENC_BINARY;
use const SQLSRV_ENC_CHAR;
use const SQLSRV_ERR_ERRORS;
use const SQLSRV_FETCH_ASSOC;
use const SQLSRV_FETCH_BOTH;
use const SQLSRV_FETCH_NUMERIC;
use const SQLSRV_PARAM_IN;

/**
 * SQL Server Statement.
 *
 * @deprecated Use {@link Statement} instead
 */
class SQLSrvStatement implements IteratorAggregate, StatementInterface, Result
{
    /**
     * The SQLSRV Resource.
     *
     * @var resource
     */
    private $conn;

    /**
     * The SQL statement to execute.
     *
     * @var string
     */
    private $sql;

    /**
     * The SQLSRV statement resource.
     *
     * @var resource|null
     */
    private $stmt;

    /**
     * References to the variables bound as statement parameters.
     *
     * @var mixed
     */
    private $variables = [];

    /**
     * Bound parameter types.
     *
     * @var int[]
     */
    private $types = [];

    /**
     * Translations.
     *
     * @var int[]
     */
    private static $fetchMap = [
        FetchMode::MIXED       => SQLSRV_FETCH_BOTH,
        FetchMode::ASSOCIATIVE => SQLSRV_FETCH_ASSOC,
        FetchMode::NUMERIC     => SQLSRV_FETCH_NUMERIC,
    ];

    /**
     * The name of the default class to instantiate when fetching class instances.
     *
     * @var string
     */
    private $defaultFetchClass = '\stdClass';

    /**
     * The constructor arguments for the default class to instantiate when fetching class instances.
     *
     * @var mixed[]
     */
    private $defaultFetchClassCtorArgs = [];

    /**
     * The fetch style.
     *
     * @var int
     */
    private $defaultFetchMode = FetchMode::MIXED;

    /**
     * The last insert ID.
     *
     * @var LastInsertId|null
     */
    private $lastInsertId;

    /**
     * Indicates whether the statement is in the state when fetching results is possible
     *
     * @var bool
     */
    private $result = false;

    /**
     * Append to any INSERT query to retrieve the last insert id.
     *
     * @deprecated This constant has been deprecated and will be made private in 3.0
     */
    public const LAST_INSERT_ID_SQL = ';SELECT SCOPE_IDENTITY() AS LastInsertId;';

    /**
     * @internal The statement can be only instantiated by its driver connection.
     *
     * @param resource $conn
     * @param string   $sql
     */
    public function __construct($conn, $sql, ?LastInsertId $lastInsertId = null)
    {
        $this->conn = $conn;
        $this->sql  = $sql;

        if (stripos($sql, 'INSERT INTO ') !== 0) {
            return;
        }

        $this->sql         .= self::LAST_INSERT_ID_SQL;
        $this->lastInsertId = $lastInsertId;
    }

    /**
     * {@inheritdoc}
     */
    public function bindValue($param, $value, $type = ParameterType::STRING)
    {
        if (! is_numeric($param)) {
            throw new SQLSrvException(
                'sqlsrv does not support named parameters to queries, use question mark (?) placeholders instead.'
            );
        }

        $this->variables[$param] = $value;
        $this->types[$param]     = $type;

        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null)
    {
        if (! is_numeric($param)) {
            throw new SQLSrvException(
                'sqlsrv does not support named parameters to queries, use question mark (?) placeholders instead.'
            );
        }

        $this->variables[$param] =& $variable;
        $this->types[$param]     = $type;

        // unset the statement resource if it exists as the new one will need to be bound to the new variable
        $this->stmt = null;

        return true;
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use free() instead.
     */
    public function closeCursor()
    {
        $this->free();

        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function columnCount()
    {
        if ($this->stmt === null) {
            return 0;
        }

        return sqlsrv_num_fields($this->stmt) ?: 0;
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated The error information is available via exceptions.
     */
    public function errorCode()
    {
        $errors = sqlsrv_errors(SQLSRV_ERR_ERRORS);
        if ($errors) {
            return $errors[0]['code'];
        }

        return false;
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated The error information is available via exceptions.
     */
    public function errorInfo()
    {
        return (array) sqlsrv_errors(SQLSRV_ERR_ERRORS);
    }

    /**
     * {@inheritdoc}
     */
    public function execute($params = null)
    {
        if ($params) {
            $hasZeroIndex = array_key_exists(0, $params);

            foreach ($params as $key => $val) {
                if ($hasZeroIndex && is_int($key)) {
                    $this->bindValue($key + 1, $val);
                } else {
                    $this->bindValue($key, $val);
                }
            }
        }

        if (! $this->stmt) {
            $this->stmt = $this->prepare();
        }

        if (! sqlsrv_execute($this->stmt)) {
            throw Error::new();
        }

        if ($this->lastInsertId) {
            sqlsrv_next_result($this->stmt);
            sqlsrv_fetch($this->stmt);
            $this->lastInsertId->setId(sqlsrv_get_field($this->stmt, 0));
        }

        $this->result = true;

        return true;
    }

    /**
     * Prepares SQL Server statement resource
     *
     * @return resource
     *
     * @throws SQLSrvException
     */
    private function prepare()
    {
        $params = [];

        foreach ($this->variables as $column => &$variable) {
            switch ($this->types[$column]) {
                case ParameterType::LARGE_OBJECT:
                    $params[$column - 1] = [
                        &$variable,
                        SQLSRV_PARAM_IN,
                        SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY),
                        SQLSRV_SQLTYPE_VARBINARY('max'),
                    ];
                    break;

                case ParameterType::BINARY:
                    $params[$column - 1] = [
                        &$variable,
                        SQLSRV_PARAM_IN,
                        SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_BINARY),
                    ];
                    break;

                case ParameterType::ASCII:
                    $params[$column - 1] = [
                        &$variable,
                        SQLSRV_PARAM_IN,
                        SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_CHAR),
                    ];
                    break;

                default:
                    $params[$column - 1] =& $variable;
                    break;
            }
        }

        $stmt = sqlsrv_prepare($this->conn, $this->sql, $params);

        if (! $stmt) {
            throw Error::new();
        }

        return $stmt;
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use one of the fetch- or iterate-related methods.
     */
    public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null)
    {
        $this->defaultFetchMode          = $fetchMode;
        $this->defaultFetchClass         = $arg2 ?: $this->defaultFetchClass;
        $this->defaultFetchClassCtorArgs = $arg3 ? (array) $arg3 : $this->defaultFetchClassCtorArgs;

        return true;
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use iterateNumeric(), iterateAssociative() or iterateColumn() instead.
     */
    #[ReturnTypeWillChange]
    public function getIterator()
    {
        return new StatementIterator($this);
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use fetchNumeric(), fetchAssociative() or fetchOne() instead.
     *
     * @throws SQLSrvException
     */
    public function fetch($fetchMode = null, $cursorOrientation = PDO::FETCH_ORI_NEXT, $cursorOffset = 0)
    {
        // do not try fetching from the statement if it's not expected to contain result
        // in order to prevent exceptional situation
        if ($this->stmt === null || ! $this->result) {
            return false;
        }

        $args      = func_get_args();
        $fetchMode = $fetchMode ?: $this->defaultFetchMode;

        if ($fetchMode === FetchMode::COLUMN) {
            return $this->fetchColumn();
        }

        if (isset(self::$fetchMap[$fetchMode])) {
            return sqlsrv_fetch_array($this->stmt, self::$fetchMap[$fetchMode]) ?: false;
        }

        if (in_array($fetchMode, [FetchMode::STANDARD_OBJECT, FetchMode::CUSTOM_OBJECT], true)) {
            $className = $this->defaultFetchClass;
            $ctorArgs  = $this->defaultFetchClassCtorArgs;

            if (count($args) >= 2) {
                $className = $args[1];
                $ctorArgs  = $args[2] ?? [];
            }

            return sqlsrv_fetch_object($this->stmt, $className, $ctorArgs) ?: false;
        }

        throw new SQLSrvException('Fetch mode is not supported!');
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use fetchAllNumeric(), fetchAllAssociative() or fetchFirstColumn() instead.
     */
    public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null)
    {
        $rows = [];

        switch ($fetchMode) {
            case FetchMode::CUSTOM_OBJECT:
                while (($row = $this->fetch(...func_get_args())) !== false) {
                    $rows[] = $row;
                }

                break;

            case FetchMode::COLUMN:
                while (($row = $this->fetchColumn()) !== false) {
                    $rows[] = $row;
                }

                break;

            default:
                while (($row = $this->fetch($fetchMode)) !== false) {
                    $rows[] = $row;
                }
        }

        return $rows;
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use fetchOne() instead.
     */
    public function fetchColumn($columnIndex = 0)
    {
        $row = $this->fetch(FetchMode::NUMERIC);

        if ($row === false) {
            return false;
        }

        return $row[$columnIndex] ?? null;
    }

    /**
     * {@inheritdoc}
     */
    public function fetchNumeric()
    {
        return $this->doFetch(SQLSRV_FETCH_NUMERIC);
    }

    /**
     * {@inheritdoc}
     */
    public function fetchAssociative()
    {
        return $this->doFetch(SQLSRV_FETCH_ASSOC);
    }

    /**
     * {@inheritdoc}
     */
    public function fetchOne()
    {
        return FetchUtils::fetchOne($this);
    }

    /**
     * {@inheritdoc}
     */
    public function fetchAllNumeric(): array
    {
        return FetchUtils::fetchAllNumeric($this);
    }

    /**
     * {@inheritdoc}
     */
    public function fetchAllAssociative(): array
    {
        return FetchUtils::fetchAllAssociative($this);
    }

    /**
     * {@inheritdoc}
     */
    public function fetchFirstColumn(): array
    {
        return FetchUtils::fetchFirstColumn($this);
    }

    /**
     * {@inheritdoc}
     */
    public function rowCount()
    {
        if ($this->stmt === null) {
            return 0;
        }

        return sqlsrv_rows_affected($this->stmt) ?: 0;
    }

    public function free(): void
    {
        // not having the result means there's nothing to close
        if ($this->stmt === null || ! $this->result) {
            return;
        }

        // emulate it by fetching and discarding rows, similarly to what PDO does in this case
        // @link http://php.net/manual/en/pdostatement.closecursor.php
        // @link https://github.com/php/php-src/blob/php-7.0.11/ext/pdo/pdo_stmt.c#L2075
        // deliberately do not consider multiple result sets, since doctrine/dbal doesn't support them
        while (sqlsrv_fetch($this->stmt)) {
        }

        $this->result = false;
    }

    /**
     * @return mixed|false
     */
    private function doFetch(int $fetchType)
    {
        // do not try fetching from the statement if it's not expected to contain the result
        // in order to prevent exceptional situation
        if ($this->stmt === null || ! $this->result) {
            return false;
        }

        return sqlsrv_fetch_array($this->stmt, $fetchType) ?? false;
    }
}
<?php

namespace Doctrine\DBAL\Driver\SQLSrv;

use Doctrine\DBAL\Driver\Connection as ConnectionInterface;
use Doctrine\DBAL\Driver\Result;
use Doctrine\DBAL\Driver\ServerInfoAwareConnection;
use Doctrine\DBAL\Driver\SQLSrv\Exception\Error;
use Doctrine\DBAL\ParameterType;
use Doctrine\Deprecations\Deprecation;

use function func_get_args;
use function is_float;
use function is_int;
use function sprintf;
use function sqlsrv_begin_transaction;
use function sqlsrv_commit;
use function sqlsrv_configure;
use function sqlsrv_connect;
use function sqlsrv_errors;
use function sqlsrv_query;
use function sqlsrv_rollback;
use function sqlsrv_rows_affected;
use function sqlsrv_server_info;
use function str_replace;

use const SQLSRV_ERR_ERRORS;

/**
 * SQL Server implementation for the Connection interface.
 *
 * @deprecated Use {@link Connection} instead
 */
class SQLSrvConnection implements ConnectionInterface, ServerInfoAwareConnection
{
    /** @var resource */
    protected $conn;

    /** @var LastInsertId */
    protected $lastInsertId;

    /**
     * @internal The connection can be only instantiated by its driver.
     *
     * @param string  $serverName
     * @param mixed[] $connectionOptions
     *
     * @throws SQLSrvException
     */
    public function __construct($serverName, $connectionOptions)
    {
        if (! sqlsrv_configure('WarningsReturnAsErrors', 0)) {
            throw Error::new();
        }

        $conn = sqlsrv_connect($serverName, $connectionOptions);

        if ($conn === false) {
            throw Error::new();
        }

        $this->conn         = $conn;
        $this->lastInsertId = new LastInsertId();
    }

    /**
     * {@inheritdoc}
     */
    public function getServerVersion()
    {
        $serverInfo = sqlsrv_server_info($this->conn);

        return $serverInfo['SQLServerVersion'];
    }

    /**
     * {@inheritdoc}
     */
    public function requiresQueryForServerVersion()
    {
        Deprecation::triggerIfCalledFromOutside(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/4114',
            'ServerInfoAwareConnection::requiresQueryForServerVersion() is deprecated and removed in DBAL 3.'
        );

        return false;
    }

    /**
     * {@inheritDoc}
     */
    public function prepare($sql)
    {
        return new Statement($this->conn, $sql, $this->lastInsertId);
    }

    /**
     * {@inheritDoc}
     */
    public function query()
    {
        $args = func_get_args();
        $sql  = $args[0];
        $stmt = $this->prepare($sql);
        $stmt->execute();

        return $stmt;
    }

    /**
     * {@inheritDoc}
     */
    public function quote($value, $type = ParameterType::STRING)
    {
        if (is_int($value)) {
            return $value;
        }

        if (is_float($value)) {
            return sprintf('%F', $value);
        }

        return "'" . str_replace("'", "''", $value) . "'";
    }

    /**
     * {@inheritDoc}
     */
    public function exec($sql)
    {
        $stmt = sqlsrv_query($this->conn, $sql);

        if ($stmt === false) {
            throw Error::new();
        }

        $rowsAffected = sqlsrv_rows_affected($stmt);

        if ($rowsAffected === false) {
            throw Error::new();
        }

        return $rowsAffected;
    }

    /**
     * {@inheritDoc}
     */
    public function lastInsertId($name = null)
    {
        if ($name !== null) {
            $stmt = $this->prepare('SELECT CONVERT(VARCHAR(MAX), current_value) FROM sys.sequences WHERE name = ?');
            $stmt->execute([$name]);
        } else {
            $stmt = $this->query('SELECT @@IDENTITY');
        }

        if ($stmt instanceof Result) {
            return $stmt->fetchOne();
        }

        return $stmt->fetchColumn();
    }

    /**
     * {@inheritDoc}
     */
    public function beginTransaction()
    {
        if (! sqlsrv_begin_transaction($this->conn)) {
            throw Error::new();
        }

        return true;
    }

    /**
     * {@inheritDoc}
     */
    public function commit()
    {
        if (! sqlsrv_commit($this->conn)) {
            throw Error::new();
        }

        return true;
    }

    /**
     * {@inheritDoc}
     */
    public function rollBack()
    {
        if (! sqlsrv_rollback($this->conn)) {
            throw Error::new();
        }

        return true;
    }

    /**
     * {@inheritDoc}
     *
     * @deprecated The error information is available via exceptions.
     */
    public function errorCode()
    {
        $errors = sqlsrv_errors(SQLSRV_ERR_ERRORS);
        if ($errors) {
            return $errors[0]['code'];
        }

        return null;
    }

    /**
     * {@inheritDoc}
     *
     * @deprecated The error information is available via exceptions.
     */
    public function errorInfo()
    {
        return (array) sqlsrv_errors(SQLSRV_ERR_ERRORS);
    }
}
<?php

namespace Doctrine\DBAL\Driver;

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver\DriverException as TheDriverException;
use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Exception\DriverException;
use Doctrine\DBAL\Platforms\SQLServer2005Platform;
use Doctrine\DBAL\Platforms\SQLServer2008Platform;
use Doctrine\DBAL\Platforms\SQLServer2012Platform;
use Doctrine\DBAL\Platforms\SQLServerPlatform;
use Doctrine\DBAL\Schema\SQLServerSchemaManager;
use Doctrine\DBAL\VersionAwarePlatformDriver;

use function assert;
use function preg_match;
use function version_compare;

/**
 * Abstract base implementation of the {@link Driver} interface for Microsoft SQL Server based drivers.
 */
abstract class AbstractSQLServerDriver implements Driver, VersionAwarePlatformDriver
{
    /**
     * {@inheritdoc}
     */
    public function createDatabasePlatformForVersion($version)
    {
        if (
            ! preg_match(
                '/^(?P<major>\d+)(?:\.(?P<minor>\d+)(?:\.(?P<patch>\d+)(?:\.(?P<build>\d+))?)?)?/',
                $version,
                $versionParts
            )
        ) {
            throw Exception::invalidPlatformVersionSpecified(
                $version,
                '<major_version>.<minor_version>.<patch_version>.<build_version>'
            );
        }

        $majorVersion = $versionParts['major'];
        $minorVersion = $versionParts['minor'] ?? 0;
        $patchVersion = $versionParts['patch'] ?? 0;
        $buildVersion = $versionParts['build'] ?? 0;
        $version      = $majorVersion . '.' . $minorVersion . '.' . $patchVersion . '.' . $buildVersion;

        switch (true) {
            case version_compare($version, '11.00.2100', '>='):
                return new SQLServer2012Platform();
            case version_compare($version, '10.00.1600', '>='):
                return new SQLServer2008Platform();
            case version_compare($version, '9.00.1399', '>='):
                return new SQLServer2005Platform();
            default:
                return new SQLServerPlatform();
        }
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use Connection::getDatabase() instead.
     */
    public function getDatabase(Connection $conn)
    {
        $params = $conn->getParams();

        if (isset($params['dbname'])) {
            return $params['dbname'];
        }

        $database = $conn->query('SELECT DB_NAME()')->fetchColumn();

        assert($database !== false);

        return $database;
    }

    /**
     * {@inheritdoc}
     */
    public function getDatabasePlatform()
    {
        return new SQLServer2008Platform();
    }

    /**
     * {@inheritdoc}
     */
    public function getSchemaManager(Connection $conn)
    {
        return new SQLServerSchemaManager($conn);
    }

    /**
     * @param string $message
     *
     * @return DriverException
     */
    public function convertException($message, TheDriverException $exception)
    {
        return new DriverException($message, $exception);
    }
}
<?php

namespace Doctrine\DBAL\Driver;

use Doctrine\DBAL\ParameterType;

/**
 * Statement interface.
 * Drivers must implement this interface.
 *
 * This resembles (a subset of) the PDOStatement interface.
 */
interface Statement extends ResultStatement
{
    /**
     * Binds a value to a corresponding named (not supported by mysqli driver, see comment below) or positional
     * placeholder in the SQL statement that was used to prepare the statement.
     *
     * As mentioned above, the named parameters are not natively supported by the mysqli driver, use executeQuery(),
     * fetchAll(), fetchArray(), fetchColumn(), fetchAssoc() methods to have the named parameter emulated by doctrine.
     *
     * @param int|string $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 int        $type  Explicit data type for the parameter using the {@link ParameterType}
     *                          constants.
     *
     * @return bool TRUE on success or FALSE on failure.
     */
    public function bindValue($param, $value, $type = ParameterType::STRING);

    /**
     * Binds a PHP variable to a corresponding named (not supported by mysqli driver, see comment below) 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.
     *
     * As mentioned above, the named parameters are not natively supported by the mysqli driver, use executeQuery(),
     * fetchAll(), fetchArray(), fetchColumn(), fetchAssoc() methods to have the named parameter emulated by doctrine.
     *
     * 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 int|string $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 int        $type     Explicit data type for the parameter using the {@link ParameterType}
     *                             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.
     * @param int|null   $length   You must specify maxlength when using an OUT bind
     *                             so that PHP allocates enough memory to hold the returned value.
     *
     * @return bool TRUE on success or FALSE on failure.
     */
    public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null);

    /**
     * Fetches the SQLSTATE associated with the last operation on the statement handle.
     *
     * @deprecated The error information is available via exceptions.
     *
     * @see Doctrine_Adapter_Interface::errorCode()
     *
     * @return string|int|bool The error code string.
     */
    public function errorCode();

    /**
     * Fetches extended error information associated with the last operation on the statement handle.
     *
     * @deprecated The error information is available via exceptions.
     *
     * @return mixed[] The error info array.
     */
    public 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 mixed[]|null $params An array of values with as many elements as there are
     *                             bound parameters in the SQL statement being executed.
     *
     * @return bool TRUE on success or FALSE on failure.
     */
    public function execute($params = null);

    /**
     * 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 int|string The number of rows.
     */
    public function rowCount();
}
<?php

namespace Doctrine\DBAL\Driver;

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver\DriverException as DeprecatedDriverException;
use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Exception\ConnectionException;
use Doctrine\DBAL\Exception\DeadlockException;
use Doctrine\DBAL\Exception\DriverException;
use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
use Doctrine\DBAL\Exception\InvalidFieldNameException;
use Doctrine\DBAL\Exception\LockWaitTimeoutException;
use Doctrine\DBAL\Exception\NonUniqueFieldNameException;
use Doctrine\DBAL\Exception\NotNullConstraintViolationException;
use Doctrine\DBAL\Exception\SyntaxErrorException;
use Doctrine\DBAL\Exception\TableExistsException;
use Doctrine\DBAL\Exception\TableNotFoundException;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use Doctrine\DBAL\Platforms\SQLAnywhere11Platform;
use Doctrine\DBAL\Platforms\SQLAnywhere12Platform;
use Doctrine\DBAL\Platforms\SQLAnywhere16Platform;
use Doctrine\DBAL\Platforms\SQLAnywherePlatform;
use Doctrine\DBAL\Schema\SQLAnywhereSchemaManager;
use Doctrine\DBAL\VersionAwarePlatformDriver;

use function assert;
use function preg_match;
use function version_compare;

/**
 * Abstract base implementation of the {@link Driver} interface for SAP Sybase SQL Anywhere based drivers.
 *
 * @deprecated Support for SQLAnywhere will be removed in 3.0.
 */
abstract class AbstractSQLAnywhereDriver implements Driver, ExceptionConverterDriver, VersionAwarePlatformDriver
{
    /**
     * {@inheritdoc}
     *
     * @deprecated
     *
     * @link http://dcx.sybase.com/index.html#sa160/en/saerrors/sqlerror.html
     */
    public function convertException($message, DeprecatedDriverException $exception)
    {
        switch ($exception->getErrorCode()) {
            case '-306':
            case '-307':
            case '-684':
                return new DeadlockException($message, $exception);

            case '-210':
            case '-1175':
            case '-1281':
                return new LockWaitTimeoutException($message, $exception);

            case '-100':
            case '-103':
            case '-832':
                return new ConnectionException($message, $exception);

            case '-143':
                return new InvalidFieldNameException($message, $exception);

            case '-193':
            case '-196':
                return new UniqueConstraintViolationException($message, $exception);

            case '-194':
            case '-198':
                return new ForeignKeyConstraintViolationException($message, $exception);

            case '-144':
                return new NonUniqueFieldNameException($message, $exception);

            case '-184':
            case '-195':
                return new NotNullConstraintViolationException($message, $exception);

            case '-131':
                return new SyntaxErrorException($message, $exception);

            case '-110':
                return new TableExistsException($message, $exception);

            case '-141':
            case '-1041':
                return new TableNotFoundException($message, $exception);
        }

        return new DriverException($message, $exception);
    }

    /**
     * {@inheritdoc}
     */
    public function createDatabasePlatformForVersion($version)
    {
        if (
            ! preg_match(
                '/^(?P<major>\d+)(?:\.(?P<minor>\d+)(?:\.(?P<patch>\d+)(?:\.(?P<build>\d+))?)?)?/',
                $version,
                $versionParts
            )
        ) {
            throw Exception::invalidPlatformVersionSpecified(
                $version,
                '<major_version>.<minor_version>.<patch_version>.<build_version>'
            );
        }

        $majorVersion = $versionParts['major'];
        $minorVersion = $versionParts['minor'] ?? 0;
        $patchVersion = $versionParts['patch'] ?? 0;
        $buildVersion = $versionParts['build'] ?? 0;
        $version      = $majorVersion . '.' . $minorVersion . '.' . $patchVersion . '.' . $buildVersion;

        switch (true) {
            case version_compare($version, '16', '>='):
                return new SQLAnywhere16Platform();

            case version_compare($version, '12', '>='):
                return new SQLAnywhere12Platform();

            case version_compare($version, '11', '>='):
                return new SQLAnywhere11Platform();

            default:
                return new SQLAnywherePlatform();
        }
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use Connection::getDatabase() instead.
     */
    public function getDatabase(Connection $conn)
    {
        $params = $conn->getParams();

        if (isset($params['dbname'])) {
            return $params['dbname'];
        }

        $database = $conn->query('SELECT DB_NAME()')->fetchColumn();

        assert($database !== false);

        return $database;
    }

    /**
     * {@inheritdoc}
     */
    public function getDatabasePlatform()
    {
        return new SQLAnywhere12Platform();
    }

    /**
     * {@inheritdoc}
     */
    public function getSchemaManager(Connection $conn)
    {
        return new SQLAnywhereSchemaManager($conn);
    }
}
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Driver;

/**
 * @internal
 */
final class FetchUtils
{
    /**
     * @return mixed|false
     *
     * @throws Exception
     */
    public static function fetchOne(Result $result)
    {
        $row = $result->fetchNumeric();

        if ($row === false) {
            return false;
        }

        return $row[0];
    }

    /**
     * @return array<int,array<int,mixed>>
     *
     * @throws Exception
     */
    public static function fetchAllNumeric(Result $result): array
    {
        $rows = [];

        while (($row = $result->fetchNumeric()) !== false) {
            $rows[] = $row;
        }

        return $rows;
    }

    /**
     * @return array<int,array<string,mixed>>
     *
     * @throws Exception
     */
    public static function fetchAllAssociative(Result $result): array
    {
        $rows = [];

        while (($row = $result->fetchAssociative()) !== false) {
            $rows[] = $row;
        }

        return $rows;
    }

    /**
     * @return array<int,mixed>
     *
     * @throws Exception
     */
    public static function fetchFirstColumn(Result $result): array
    {
        $rows = [];

        while (($row = $result->fetchOne()) !== false) {
            $rows[] = $row;
        }

        return $rows;
    }
}
<?php

namespace Doctrine\DBAL\Driver\PDOPgSql;

use Doctrine\DBAL\Driver\AbstractPostgreSQLDriver;
use Doctrine\DBAL\Driver\PDO;
use Doctrine\DBAL\Exception;
use Doctrine\Deprecations\Deprecation;
use PDOException;

use function defined;

/**
 * Driver that connects through pdo_pgsql.
 *
 * @deprecated Use {@link PDO\PgSQL\Driver} instead.
 */
class Driver extends AbstractPostgreSQLDriver
{
    /**
     * {@inheritdoc}
     */
    public function connect(array $params, $username = null, $password = null, array $driverOptions = [])
    {
        try {
            $pdo = new PDO\Connection(
                $this->_constructPdoDsn($params),
                $username,
                $password,
                $driverOptions
            );

            if (
                defined('PDO::PGSQL_ATTR_DISABLE_PREPARES')
                && (! isset($driverOptions[\PDO::PGSQL_ATTR_DISABLE_PREPARES])
                    || $driverOptions[\PDO::PGSQL_ATTR_DISABLE_PREPARES] === true
                )
            ) {
                $pdo->setAttribute(\PDO::PGSQL_ATTR_DISABLE_PREPARES, true);
            }

            /* defining client_encoding via SET NAMES to avoid inconsistent DSN support
             * - the 'client_encoding' connection param only works with postgres >= 9.1
             * - passing client_encoding via the 'options' param breaks pgbouncer support
             */
            if (isset($params['charset'])) {
                $pdo->exec('SET NAMES \'' . $params['charset'] . '\'');
            }

            return $pdo;
        } catch (PDOException $e) {
            throw Exception::driverException($this, $e);
        }
    }

    /**
     * Constructs the Postgres PDO DSN.
     *
     * @param mixed[] $params
     *
     * @return string The DSN.
     */
    private function _constructPdoDsn(array $params)
    {
        $dsn = 'pgsql:';

        if (isset($params['host']) && $params['host'] !== '') {
            $dsn .= 'host=' . $params['host'] . ';';
        }

        if (isset($params['port']) && $params['port'] !== '') {
            $dsn .= 'port=' . $params['port'] . ';';
        }

        if (isset($params['dbname'])) {
            $dsn .= 'dbname=' . $params['dbname'] . ';';
        } elseif (isset($params['default_dbname'])) {
            $dsn .= 'dbname=' . $params['default_dbname'] . ';';
        } else {
            // Used for temporary connections to allow operations like dropping the database currently connected to.
            // Connecting without an explicit database does not work, therefore "postgres" database is used
            // as it is mostly present in every server setup.
            $dsn .= 'dbname=postgres;';
        }

        if (isset($params['sslmode'])) {
            $dsn .= 'sslmode=' . $params['sslmode'] . ';';
        }

        if (isset($params['sslrootcert'])) {
            $dsn .= 'sslrootcert=' . $params['sslrootcert'] . ';';
        }

        if (isset($params['sslcert'])) {
            $dsn .= 'sslcert=' . $params['sslcert'] . ';';
        }

        if (isset($params['sslkey'])) {
            $dsn .= 'sslkey=' . $params['sslkey'] . ';';
        }

        if (isset($params['sslcrl'])) {
            $dsn .= 'sslcrl=' . $params['sslcrl'] . ';';
        }

        if (isset($params['application_name'])) {
            $dsn .= 'application_name=' . $params['application_name'] . ';';
        }

        return $dsn;
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated
     */
    public function getName()
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/issues/3580',
            'Driver::getName() is deprecated'
        );

        return 'pdo_pgsql';
    }
}
<?php

namespace Doctrine\DBAL\Driver;

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver\AbstractOracleDriver\EasyConnectString;
use Doctrine\DBAL\Driver\DriverException as DeprecatedDriverException;
use Doctrine\DBAL\Exception\ConnectionException;
use Doctrine\DBAL\Exception\DriverException;
use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
use Doctrine\DBAL\Exception\InvalidFieldNameException;
use Doctrine\DBAL\Exception\NonUniqueFieldNameException;
use Doctrine\DBAL\Exception\NotNullConstraintViolationException;
use Doctrine\DBAL\Exception\SyntaxErrorException;
use Doctrine\DBAL\Exception\TableExistsException;
use Doctrine\DBAL\Exception\TableNotFoundException;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use Doctrine\DBAL\Platforms\OraclePlatform;
use Doctrine\DBAL\Schema\OracleSchemaManager;

/**
 * Abstract base implementation of the {@link Driver} interface for Oracle based drivers.
 */
abstract class AbstractOracleDriver implements Driver, ExceptionConverterDriver
{
    /**
     * {@inheritdoc}
     *
     * @deprecated
     */
    public function convertException($message, DeprecatedDriverException $exception)
    {
        switch ($exception->getErrorCode()) {
            case '1':
            case '2299':
            case '38911':
                return new UniqueConstraintViolationException($message, $exception);

            case '904':
                return new InvalidFieldNameException($message, $exception);

            case '918':
            case '960':
                return new NonUniqueFieldNameException($message, $exception);

            case '923':
                return new SyntaxErrorException($message, $exception);

            case '942':
                return new TableNotFoundException($message, $exception);

            case '955':
                return new TableExistsException($message, $exception);

            case '1017':
            case '12545':
                return new ConnectionException($message, $exception);

            case '1400':
                return new NotNullConstraintViolationException($message, $exception);

            case '2266':
            case '2291':
            case '2292':
                return new ForeignKeyConstraintViolationException($message, $exception);
        }

        return new DriverException($message, $exception);
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use Connection::getDatabase() instead.
     */
    public function getDatabase(Connection $conn)
    {
        $params = $conn->getParams();

        return $params['user'];
    }

    /**
     * {@inheritdoc}
     */
    public function getDatabasePlatform()
    {
        return new OraclePlatform();
    }

    /**
     * {@inheritdoc}
     */
    public function getSchemaManager(Connection $conn)
    {
        return new OracleSchemaManager($conn);
    }

    /**
     * Returns an appropriate Easy Connect String for the given parameters.
     *
     * @param mixed[] $params The connection parameters to return the Easy Connect String for.
     *
     * @return string
     */
    protected function getEasyConnectString(array $params)
    {
        return (string) EasyConnectString::fromConnectionParameters($params);
    }
}
<?php

namespace Doctrine\DBAL\Driver;

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver\DriverException as TheDriverException;
use Doctrine\DBAL\Exception\DriverException;
use Doctrine\DBAL\Platforms\DB2Platform;
use Doctrine\DBAL\Schema\DB2SchemaManager;

/**
 * Abstract base implementation of the {@link Driver} interface for IBM DB2 based drivers.
 */
abstract class AbstractDB2Driver implements Driver
{
    /**
     * {@inheritdoc}
     *
     * @deprecated Use Connection::getDatabase() instead.
     */
    public function getDatabase(Connection $conn)
    {
        $params = $conn->getParams();

        return $params['dbname'];
    }

    /**
     * {@inheritdoc}
     */
    public function getDatabasePlatform()
    {
        return new DB2Platform();
    }

    /**
     * {@inheritdoc}
     */
    public function getSchemaManager(Connection $conn)
    {
        return new DB2SchemaManager($conn);
    }

    /**
     * @param string $message
     *
     * @return DriverException
     */
    public function convertException($message, TheDriverException $exception)
    {
        return new DriverException($message, $exception);
    }
}
<?php

namespace Doctrine\DBAL\Driver;

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver\DriverException as DeprecatedDriverException;
use Doctrine\DBAL\Exception\ConnectionException;
use Doctrine\DBAL\Exception\DriverException;
use Doctrine\DBAL\Exception\InvalidFieldNameException;
use Doctrine\DBAL\Exception\LockWaitTimeoutException;
use Doctrine\DBAL\Exception\NonUniqueFieldNameException;
use Doctrine\DBAL\Exception\NotNullConstraintViolationException;
use Doctrine\DBAL\Exception\ReadOnlyException;
use Doctrine\DBAL\Exception\SyntaxErrorException;
use Doctrine\DBAL\Exception\TableExistsException;
use Doctrine\DBAL\Exception\TableNotFoundException;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use Doctrine\DBAL\Platforms\SqlitePlatform;
use Doctrine\DBAL\Schema\SqliteSchemaManager;

use function strpos;

/**
 * Abstract base implementation of the {@link Driver} interface for SQLite based drivers.
 */
abstract class AbstractSQLiteDriver implements Driver, ExceptionConverterDriver
{
    /**
     * {@inheritdoc}
     *
     * @deprecated
     *
     * @link http://www.sqlite.org/c3ref/c_abort.html
     */
    public function convertException($message, DeprecatedDriverException $exception)
    {
        if (strpos($exception->getMessage(), 'database is locked') !== false) {
            return new LockWaitTimeoutException($message, $exception);
        }

        if (
            strpos($exception->getMessage(), 'must be unique') !== false ||
            strpos($exception->getMessage(), 'is not unique') !== false ||
            strpos($exception->getMessage(), 'are not unique') !== false ||
            strpos($exception->getMessage(), 'UNIQUE constraint failed') !== false
        ) {
            return new UniqueConstraintViolationException($message, $exception);
        }

        if (
            strpos($exception->getMessage(), 'may not be NULL') !== false ||
            strpos($exception->getMessage(), 'NOT NULL constraint failed') !== false
        ) {
            return new NotNullConstraintViolationException($message, $exception);
        }

        if (strpos($exception->getMessage(), 'no such table:') !== false) {
            return new TableNotFoundException($message, $exception);
        }

        if (strpos($exception->getMessage(), 'already exists') !== false) {
            return new TableExistsException($message, $exception);
        }

        if (strpos($exception->getMessage(), 'has no column named') !== false) {
            return new InvalidFieldNameException($message, $exception);
        }

        if (strpos($exception->getMessage(), 'ambiguous column name') !== false) {
            return new NonUniqueFieldNameException($message, $exception);
        }

        if (strpos($exception->getMessage(), 'syntax error') !== false) {
            return new SyntaxErrorException($message, $exception);
        }

        if (strpos($exception->getMessage(), 'attempt to write a readonly database') !== false) {
            return new ReadOnlyException($message, $exception);
        }

        if (strpos($exception->getMessage(), 'unable to open database file') !== false) {
            return new ConnectionException($message, $exception);
        }

        return new DriverException($message, $exception);
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use Connection::getDatabase() instead.
     */
    public function getDatabase(Connection $conn)
    {
        $params = $conn->getParams();

        return $params['path'] ?? null;
    }

    /**
     * {@inheritdoc}
     */
    public function getDatabasePlatform()
    {
        return new SqlitePlatform();
    }

    /**
     * {@inheritdoc}
     */
    public function getSchemaManager(Connection $conn)
    {
        return new SqliteSchemaManager($conn);
    }
}
<?php

namespace Doctrine\DBAL\Driver;

/**
 * @deprecated
 *
 * @psalm-immutable
 */
class AbstractDriverException extends AbstractException
{
}
<?php

namespace Doctrine\DBAL\Driver;

/**
 * Contract for a connection that is able to provide information about the server it is connected to.
 */
interface ServerInfoAwareConnection
{
    /**
     * Returns the version number of the database server connected to.
     *
     * @return string
     */
    public function getServerVersion();

    /**
     * Checks whether a query is required to retrieve the database server version.
     *
     * @deprecated
     *
     * @return bool True if a query is required to retrieve the database server version, false otherwise.
     */
    public function requiresQueryForServerVersion();
}
<?php

namespace Doctrine\DBAL\Driver;

use PDOStatement;
use ReturnTypeWillChange;

use function func_get_args;

use const PHP_VERSION_ID;

if (PHP_VERSION_ID >= 80000) {
    /**
     * @internal
     */
    trait PDOQueryImplementation
    {
        /**
         * @return PDOStatement
         */
        #[ReturnTypeWillChange]
        public function query(?string $query = null, ?int $fetchMode = null, mixed ...$fetchModeArgs)
        {
            return $this->doQuery($query, $fetchMode, ...$fetchModeArgs);
        }
    }
} else {
    /**
     * @internal
     */
    trait PDOQueryImplementation
    {
        /**
         * @return PDOStatement
         */
        public function query()
        {
            return $this->doQuery(...func_get_args());
        }
    }
}
<?php

namespace Doctrine\DBAL\Driver\PDOSqlsrv;

use Doctrine\DBAL\Driver\PDO;
use Doctrine\DBAL\ParameterType;

/**
 * PDO SQL Server Statement
 *
 * @deprecated Use {@link PDO\SQLSrv\Statement} instead.
 */
class Statement extends PDO\Statement
{
    /**
     * {@inheritdoc}
     */
    public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null, $driverOptions = null)
    {
        switch ($type) {
            case ParameterType::LARGE_OBJECT:
            case ParameterType::BINARY:
                if ($driverOptions === null) {
                    $driverOptions = \PDO::SQLSRV_ENCODING_BINARY;
                }

                break;

            case ParameterType::ASCII:
                $type          = ParameterType::STRING;
                $length        = 0;
                $driverOptions = \PDO::SQLSRV_ENCODING_SYSTEM;
                break;
        }

        return parent::bindParam($param, $variable, $type, $length ?? 0, $driverOptions);
    }

    /**
     * {@inheritdoc}
     */
    public function bindValue($param, $value, $type = ParameterType::STRING)
    {
        return $this->bindParam($param, $value, $type);
    }
}
<?php

namespace Doctrine\DBAL\Driver\PDOSqlsrv;

use Doctrine\DBAL\Driver\AbstractSQLServerDriver;
use Doctrine\DBAL\Driver\AbstractSQLServerDriver\Exception\PortWithoutHost;
use Doctrine\DBAL\Driver\PDO;
use Doctrine\Deprecations\Deprecation;

use function is_int;
use function sprintf;

/**
 * The PDO-based Sqlsrv driver.
 *
 * @deprecated Use {@link PDO\SQLSrv\Driver} instead.
 */
class Driver extends AbstractSQLServerDriver
{
    /**
     * {@inheritdoc}
     */
    public function connect(array $params, $username = null, $password = null, array $driverOptions = [])
    {
        $pdoOptions = $dsnOptions = [];

        foreach ($driverOptions as $option => $value) {
            if (is_int($option)) {
                $pdoOptions[$option] = $value;
            } else {
                $dsnOptions[$option] = $value;
            }
        }

        return new PDO\SQLSrv\Connection(
            $this->_constructPdoDsn($params, $dsnOptions),
            $username,
            $password,
            $pdoOptions
        );
    }

    /**
     * Constructs the Sqlsrv PDO DSN.
     *
     * @param mixed[]  $params
     * @param string[] $connectionOptions
     *
     * @return string The DSN.
     */
    private function _constructPdoDsn(array $params, array $connectionOptions)
    {
        $dsn = 'sqlsrv:server=';

        if (isset($params['host'])) {
            $dsn .= $params['host'];

            if (isset($params['port'])) {
                $dsn .= ',' . $params['port'];
            }
        } elseif (isset($params['port'])) {
            throw PortWithoutHost::new();
        }

        if (isset($params['dbname'])) {
            $connectionOptions['Database'] = $params['dbname'];
        }

        if (isset($params['MultipleActiveResultSets'])) {
            $connectionOptions['MultipleActiveResultSets'] = $params['MultipleActiveResultSets'] ? 'true' : 'false';
        }

        return $dsn . $this->getConnectionOptionsDsn($connectionOptions);
    }

    /**
     * Converts a connection options array to the DSN
     *
     * @param string[] $connectionOptions
     */
    private function getConnectionOptionsDsn(array $connectionOptions): string
    {
        $connectionOptionsDsn = '';

        foreach ($connectionOptions as $paramName => $paramValue) {
            $connectionOptionsDsn .= sprintf(';%s=%s', $paramName, $paramValue);
        }

        return $connectionOptionsDsn;
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated
     */
    public function getName()
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/issues/3580',
            'Driver::getName() is deprecated'
        );

        return 'pdo_sqlsrv';
    }
}
<?php

namespace Doctrine\DBAL\Driver\PDOSqlsrv;

use Doctrine\DBAL\Driver\PDO;
use Doctrine\DBAL\Driver\Result;

/**
 * Sqlsrv Connection implementation.
 *
 * @deprecated Use {@link PDO\SQLSrv\Connection} instead.
 */
class Connection extends PDO\Connection
{
    /**
     * {@inheritdoc}
     *
     * @internal The connection can be only instantiated by its driver.
     *
     * @param string       $dsn
     * @param string|null  $user
     * @param string|null  $password
     * @param mixed[]|null $options
     */
    public function __construct($dsn, $user = null, $password = null, ?array $options = null)
    {
        parent::__construct($dsn, $user, $password, $options);
        $this->setAttribute(\PDO::ATTR_STATEMENT_CLASS, [PDO\SQLSrv\Statement::class, []]);
    }

    /**
     * {@inheritDoc}
     */
    public function lastInsertId($name = null)
    {
        if ($name === null) {
            return parent::lastInsertId($name);
        }

        $stmt = $this->prepare('SELECT CONVERT(VARCHAR(MAX), current_value) FROM sys.sequences WHERE name = ?');
        $stmt->execute([$name]);

        if ($stmt instanceof Result) {
            return $stmt->fetchOne();
        }

        return $stmt->fetchColumn();
    }
}
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Driver\IBMDB2\Exception;

use Doctrine\DBAL\Driver\AbstractException;

/**
 * @internal
 *
 * @psalm-immutable
 */
final class PrepareFailed extends AbstractException
{
    /**
     * @psalm-param array{message: string}|null $error
     */
    public static function new(?array $error): self
    {
        if ($error === null) {
            return new self('Unknown error');
        }

        return new self($error['message']);
    }
}
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Driver\IBMDB2\Exception;

use Doctrine\DBAL\Driver\AbstractException;

use function db2_conn_error;
use function db2_conn_errormsg;

/**
 * @internal
 *
 * @psalm-immutable
 */
final class ConnectionError extends AbstractException
{
    /**
     * @param resource $connection
     */
    public static function new($connection): self
    {
        return new self(db2_conn_errormsg($connection), db2_conn_error($connection));
    }
}
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Driver\IBMDB2\Exception;

use Doctrine\DBAL\Driver\IBMDB2\DB2Exception;

/**
 * @internal
 *
 * @psalm-immutable
 */
final class CannotCreateTemporaryFile extends DB2Exception
{
    /**
     * @psalm-param array{message: string}|null $error
     */
    public static function new(?array $error): self
    {
        $message = 'Could not create temporary file';

        if ($error !== null) {
            $message .= ': ' . $error['message'];
        }

        return new self($message);
    }
}
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Driver\IBMDB2\Exception;

use Doctrine\DBAL\Driver\AbstractException;

use function db2_conn_error;
use function db2_conn_errormsg;

/**
 * @internal
 *
 * @psalm-immutable
 */
final class ConnectionFailed extends AbstractException
{
    public static function new(): self
    {
        return new self(db2_conn_errormsg(), db2_conn_error());
    }
}
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Driver\IBMDB2\Exception;

use Doctrine\DBAL\Driver\IBMDB2\DB2Exception;

/**
 * @internal
 *
 * @psalm-immutable
 */
final class CannotCopyStreamToStream extends DB2Exception
{
    /**
     * @psalm-param array{message: string}|null $error
     */
    public static function new(?array $error): self
    {
        $message = 'Could not copy source stream to temporary file';

        if ($error !== null) {
            $message .= ': ' . $error['message'];
        }

        return new self($message);
    }
}
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Driver\IBMDB2\Exception;

use Doctrine\DBAL\Driver\IBMDB2\DB2Exception;

/**
 * @internal
 *
 * @psalm-immutable
 */
final class CannotWriteToTemporaryFile extends DB2Exception
{
    /**
     * @psalm-param array{message: string}|null $error
     */
    public static function new(?array $error): self
    {
        $message = 'Could not write string to temporary file';

        if ($error !== null) {
            $message .= ': ' . $error['message'];
        }

        return new self($message);
    }
}
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Driver\IBMDB2\Exception;

use Doctrine\DBAL\Driver\AbstractException;

use function db2_stmt_error;
use function db2_stmt_errormsg;

/**
 * @internal
 *
 * @psalm-immutable
 */
final class StatementError extends AbstractException
{
    /**
     * @param resource $statement
     */
    public static function new($statement): self
    {
        return new self(db2_stmt_errormsg($statement), db2_stmt_error($statement));
    }
}
<?php

namespace Doctrine\DBAL\Driver\IBMDB2;

final class Statement extends DB2Statement
{
}
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Driver\IBMDB2;

final class Driver extends DB2Driver
{
}
<?php

namespace Doctrine\DBAL\Driver\IBMDB2;

use Doctrine\DBAL\Driver\AbstractDB2Driver;
use Doctrine\Deprecations\Deprecation;

/**
 * IBM DB2 Driver.
 *
 * @deprecated Use {@link Driver} instead
 */
class DB2Driver extends AbstractDB2Driver
{
    /**
     * {@inheritdoc}
     */
    public function connect(array $params, $username = null, $password = null, array $driverOptions = [])
    {
        $params['user']     = $username;
        $params['password'] = $password;
        $params['dbname']   = DataSourceName::fromConnectionParameters($params)->toString();

        return new Connection(
            $params,
            (string) $username,
            (string) $password,
            $driverOptions
        );
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated
     */
    public function getName()
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/issues/3580',
            'Driver::getName() is deprecated'
        );

        return 'ibm_db2';
    }
}
<?php

namespace Doctrine\DBAL\Driver\IBMDB2;

final class Connection extends DB2Connection
{
}
<?php

namespace Doctrine\DBAL\Driver\IBMDB2;

use Doctrine\DBAL\Driver\AbstractDriverException;

/**
 * @deprecated Use {@link \Doctrine\DBAL\Driver\Exception} instead
 *
 * @psalm-immutable
 */
class DB2Exception extends AbstractDriverException
{
}
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Driver\IBMDB2;

use function implode;
use function sprintf;
use function strpos;

/**
 * IBM DB2 DSN
 */
final class DataSourceName
{
    /** @var string */
    private $string;

    private function __construct(string $string)
    {
        $this->string = $string;
    }

    public function toString(): string
    {
        return $this->string;
    }

    /**
     * Creates the object from an array representation
     *
     * @param array<string,mixed> $params
     */
    public static function fromArray(array $params): self
    {
        $chunks = [];

        foreach ($params as $key => $value) {
            $chunks[] = sprintf('%s=%s', $key, $value);
        }

        return new self(implode(';', $chunks));
    }

    /**
     * Creates the object from the given DBAL connection parameters.
     *
     * @param array<string,mixed> $params
     */
    public static function fromConnectionParameters(array $params): self
    {
        if (isset($params['dbname']) && strpos($params['dbname'], '=') !== false) {
            return new self($params['dbname']);
        }

        $dsnParams = [];

        foreach (
            [
                'host'     => 'HOSTNAME',
                'port'     => 'PORT',
                'protocol' => 'PROTOCOL',
                'dbname'   => 'DATABASE',
                'user'     => 'UID',
                'password' => 'PWD',
            ] as $dbalParam => $dsnParam
        ) {
            if (! isset($params[$dbalParam])) {
                continue;
            }

            $dsnParams[$dsnParam] = $params[$dbalParam];
        }

        return self::fromArray($dsnParams);
    }
}
<?php

namespace Doctrine\DBAL\Driver\IBMDB2;

use Doctrine\DBAL\Driver\Connection as ConnectionInterface;
use Doctrine\DBAL\Driver\IBMDB2\Exception\ConnectionError;
use Doctrine\DBAL\Driver\IBMDB2\Exception\ConnectionFailed;
use Doctrine\DBAL\Driver\IBMDB2\Exception\PrepareFailed;
use Doctrine\DBAL\Driver\ServerInfoAwareConnection;
use Doctrine\DBAL\ParameterType;
use Doctrine\Deprecations\Deprecation;
use stdClass;

use function assert;
use function db2_autocommit;
use function db2_commit;
use function db2_conn_error;
use function db2_conn_errormsg;
use function db2_connect;
use function db2_escape_string;
use function db2_exec;
use function db2_last_insert_id;
use function db2_num_rows;
use function db2_pconnect;
use function db2_prepare;
use function db2_rollback;
use function db2_server_info;
use function error_get_last;
use function func_get_args;
use function is_bool;

use const DB2_AUTOCOMMIT_OFF;
use const DB2_AUTOCOMMIT_ON;

/**
 * @deprecated Use {@link Connection} instead
 */
class DB2Connection implements ConnectionInterface, ServerInfoAwareConnection
{
    /** @var resource */
    private $conn;

    /**
     * @internal The connection can be only instantiated by its driver.
     *
     * @param mixed[] $params
     * @param string  $username
     * @param string  $password
     * @param mixed[] $driverOptions
     *
     * @throws DB2Exception
     */
    public function __construct(array $params, $username, $password, $driverOptions = [])
    {
        $isPersistent = (isset($params['persistent']) && $params['persistent'] === true);

        if ($isPersistent) {
            $conn = db2_pconnect($params['dbname'], $username, $password, $driverOptions);
        } else {
            $conn = db2_connect($params['dbname'], $username, $password, $driverOptions);
        }

        if ($conn === false) {
            throw ConnectionFailed::new();
        }

        $this->conn = $conn;
    }

    /**
     * {@inheritdoc}
     */
    public function getServerVersion()
    {
        $serverInfo = db2_server_info($this->conn);
        assert($serverInfo instanceof stdClass);

        return $serverInfo->DBMS_VER;
    }

    /**
     * {@inheritdoc}
     */
    public function requiresQueryForServerVersion()
    {
        Deprecation::triggerIfCalledFromOutside(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/4114',
            'ServerInfoAwareConnection::requiresQueryForServerVersion() is deprecated and removed in DBAL 3.'
        );

        return false;
    }

    /**
     * {@inheritdoc}
     */
    public function prepare($sql)
    {
        $stmt = @db2_prepare($this->conn, $sql);

        if ($stmt === false) {
            throw PrepareFailed::new(error_get_last());
        }

        return new Statement($stmt);
    }

    /**
     * {@inheritdoc}
     */
    public function query()
    {
        $args = func_get_args();
        $sql  = $args[0];
        $stmt = $this->prepare($sql);
        $stmt->execute();

        return $stmt;
    }

    /**
     * {@inheritdoc}
     */
    public function quote($value, $type = ParameterType::STRING)
    {
        $value = db2_escape_string($value);

        if ($type === ParameterType::INTEGER) {
            return $value;
        }

        return "'" . $value . "'";
    }

    /**
     * {@inheritdoc}
     */
    public function exec($sql)
    {
        $stmt = @db2_exec($this->conn, $sql);

        if ($stmt === false) {
            throw ConnectionError::new($this->conn);
        }

        return db2_num_rows($stmt);
    }

    /**
     * {@inheritdoc}
     */
    public function lastInsertId($name = null)
    {
        return db2_last_insert_id($this->conn);
    }

    /**
     * {@inheritdoc}
     */
    public function beginTransaction()
    {
        $result = db2_autocommit($this->conn, DB2_AUTOCOMMIT_OFF);
        assert(is_bool($result));

        return $result;
    }

    /**
     * {@inheritdoc}
     */
    public function commit()
    {
        if (! db2_commit($this->conn)) {
            throw ConnectionError::new($this->conn);
        }

        $result = db2_autocommit($this->conn, DB2_AUTOCOMMIT_ON);
        assert(is_bool($result));

        return $result;
    }

    /**
     * {@inheritdoc}
     */
    public function rollBack()
    {
        if (! db2_rollback($this->conn)) {
            throw ConnectionError::new($this->conn);
        }

        $result = db2_autocommit($this->conn, DB2_AUTOCOMMIT_ON);
        assert(is_bool($result));

        return $result;
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated The error information is available via exceptions.
     */
    public function errorCode()
    {
        return db2_conn_error($this->conn);
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated The error information is available via exceptions.
     */
    public function errorInfo()
    {
        return [
            0 => db2_conn_errormsg($this->conn),
            1 => $this->errorCode(),
        ];
    }
}
<?php

namespace Doctrine\DBAL\Driver\IBMDB2;

use Doctrine\DBAL\Driver\FetchUtils;
use Doctrine\DBAL\Driver\IBMDB2\Exception\CannotCopyStreamToStream;
use Doctrine\DBAL\Driver\IBMDB2\Exception\CannotCreateTemporaryFile;
use Doctrine\DBAL\Driver\IBMDB2\Exception\CannotWriteToTemporaryFile;
use Doctrine\DBAL\Driver\IBMDB2\Exception\StatementError;
use Doctrine\DBAL\Driver\Result;
use Doctrine\DBAL\Driver\Statement as StatementInterface;
use Doctrine\DBAL\Driver\StatementIterator;
use Doctrine\DBAL\FetchMode;
use Doctrine\DBAL\ParameterType;
use IteratorAggregate;
use PDO;
use ReflectionClass;
use ReflectionObject;
use ReflectionProperty;
use ReturnTypeWillChange;
use stdClass;

use function array_change_key_case;
use function assert;
use function db2_bind_param;
use function db2_execute;
use function db2_fetch_array;
use function db2_fetch_assoc;
use function db2_fetch_both;
use function db2_fetch_object;
use function db2_free_result;
use function db2_num_fields;
use function db2_num_rows;
use function db2_stmt_error;
use function db2_stmt_errormsg;
use function error_get_last;
use function fclose;
use function func_get_args;
use function func_num_args;
use function fwrite;
use function gettype;
use function is_int;
use function is_object;
use function is_resource;
use function is_string;
use function ksort;
use function sprintf;
use function stream_copy_to_stream;
use function stream_get_meta_data;
use function strtolower;
use function tmpfile;

use const CASE_LOWER;
use const DB2_BINARY;
use const DB2_CHAR;
use const DB2_LONG;
use const DB2_PARAM_FILE;
use const DB2_PARAM_IN;

/**
 * @deprecated Use {@link Statement} instead
 */
class DB2Statement implements IteratorAggregate, StatementInterface, Result
{
    /** @var resource */
    private $stmt;

    /** @var mixed[] */
    private $bindParam = [];

    /**
     * Map of LOB parameter positions to the tuples containing reference to the variable bound to the driver statement
     * and the temporary file handle bound to the underlying statement
     *
     * @var mixed[][]
     */
    private $lobs = [];

    /** @var string Name of the default class to instantiate when fetching class instances. */
    private $defaultFetchClass = '\stdClass';

    /** @var mixed[] Constructor arguments for the default class to instantiate when fetching class instances. */
    private $defaultFetchClassCtorArgs = [];

    /** @var int */
    private $defaultFetchMode = FetchMode::MIXED;

    /**
     * Indicates whether the statement is in the state when fetching results is possible
     *
     * @var bool
     */
    private $result = false;

    /**
     * @internal The statement can be only instantiated by its driver connection.
     *
     * @param resource $stmt
     */
    public function __construct($stmt)
    {
        $this->stmt = $stmt;
    }

    /**
     * {@inheritdoc}
     */
    public function bindValue($param, $value, $type = ParameterType::STRING)
    {
        assert(is_int($param));

        return $this->bindParam($param, $value, $type);
    }

    /**
     * {@inheritdoc}
     */
    public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null)
    {
        assert(is_int($param));

        switch ($type) {
            case ParameterType::INTEGER:
                $this->bind($param, $variable, DB2_PARAM_IN, DB2_LONG);
                break;

            case ParameterType::LARGE_OBJECT:
                if (isset($this->lobs[$param])) {
                    [, $handle] = $this->lobs[$param];
                    fclose($handle);
                }

                $handle = $this->createTemporaryFile();
                $path   = stream_get_meta_data($handle)['uri'];

                $this->bind($param, $path, DB2_PARAM_FILE, DB2_BINARY);

                $this->lobs[$param] = [&$variable, $handle];
                break;

            default:
                $this->bind($param, $variable, DB2_PARAM_IN, DB2_CHAR);
                break;
        }

        return true;
    }

    /**
     * @param int   $position Parameter position
     * @param mixed $variable
     *
     * @throws DB2Exception
     */
    private function bind($position, &$variable, int $parameterType, int $dataType): void
    {
        $this->bindParam[$position] =& $variable;

        if (! db2_bind_param($this->stmt, $position, 'variable', $parameterType, $dataType)) {
            throw StatementError::new($this->stmt);
        }
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use free() instead.
     */
    public function closeCursor()
    {
        $this->bindParam = [];

        if (! db2_free_result($this->stmt)) {
            return false;
        }

        $this->result = false;

        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function columnCount()
    {
        return db2_num_fields($this->stmt) ?: 0;
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated The error information is available via exceptions.
     */
    public function errorCode()
    {
        return db2_stmt_error();
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated The error information is available via exceptions.
     */
    public function errorInfo()
    {
        return [
            db2_stmt_errormsg(),
            db2_stmt_error(),
        ];
    }

    /**
     * {@inheritdoc}
     */
    public function execute($params = null)
    {
        if ($params === null) {
            ksort($this->bindParam);

            $params = [];

            foreach ($this->bindParam as $column => $value) {
                $params[] = $value;
            }
        }

        foreach ($this->lobs as [$source, $target]) {
            if (is_resource($source)) {
                $this->copyStreamToStream($source, $target);

                continue;
            }

            $this->writeStringToStream($source, $target);
        }

        $retval = db2_execute($this->stmt, $params);

        foreach ($this->lobs as [, $handle]) {
            fclose($handle);
        }

        $this->lobs = [];

        if ($retval === false) {
            throw StatementError::new($this->stmt);
        }

        $this->result = true;

        return $retval;
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use one of the fetch- or iterate-related methods.
     */
    public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null)
    {
        $this->defaultFetchMode          = $fetchMode;
        $this->defaultFetchClass         = $arg2 ?: $this->defaultFetchClass;
        $this->defaultFetchClassCtorArgs = $arg3 ? (array) $arg3 : $this->defaultFetchClassCtorArgs;

        return true;
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use iterateNumeric(), iterateAssociative() or iterateColumn() instead.
     */
    #[ReturnTypeWillChange]
    public function getIterator()
    {
        return new StatementIterator($this);
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use fetchNumeric(), fetchAssociative() or fetchOne() instead.
     */
    public function fetch($fetchMode = null, $cursorOrientation = PDO::FETCH_ORI_NEXT, $cursorOffset = 0)
    {
        // do not try fetching from the statement if it's not expected to contain result
        // in order to prevent exceptional situation
        if (! $this->result) {
            return false;
        }

        $fetchMode = $fetchMode ?: $this->defaultFetchMode;
        switch ($fetchMode) {
            case FetchMode::COLUMN:
                return $this->fetchColumn();

            case FetchMode::MIXED:
                return db2_fetch_both($this->stmt);

            case FetchMode::ASSOCIATIVE:
                return db2_fetch_assoc($this->stmt);

            case FetchMode::CUSTOM_OBJECT:
                $className = $this->defaultFetchClass;
                $ctorArgs  = $this->defaultFetchClassCtorArgs;

                if (func_num_args() >= 2) {
                    $args      = func_get_args();
                    $className = $args[1];
                    $ctorArgs  = $args[2] ?? [];
                }

                $result = db2_fetch_object($this->stmt);

                if ($result instanceof stdClass) {
                    $result = $this->castObject($result, $className, $ctorArgs);
                }

                return $result;

            case FetchMode::NUMERIC:
                return db2_fetch_array($this->stmt);

            case FetchMode::STANDARD_OBJECT:
                return db2_fetch_object($this->stmt);

            default:
                throw new DB2Exception('Given Fetch-Style ' . $fetchMode . ' is not supported.');
        }
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use fetchAllNumeric(), fetchAllAssociative() or fetchFirstColumn() instead.
     */
    public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null)
    {
        $rows = [];

        switch ($fetchMode) {
            case FetchMode::CUSTOM_OBJECT:
                while (($row = $this->fetch(...func_get_args())) !== false) {
                    $rows[] = $row;
                }

                break;

            case FetchMode::COLUMN:
                while (($row = $this->fetchColumn()) !== false) {
                    $rows[] = $row;
                }

                break;

            default:
                while (($row = $this->fetch($fetchMode)) !== false) {
                    $rows[] = $row;
                }
        }

        return $rows;
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use fetchOne() instead.
     */
    public function fetchColumn($columnIndex = 0)
    {
        $row = $this->fetch(FetchMode::NUMERIC);

        if ($row === false) {
            return false;
        }

        return $row[$columnIndex] ?? null;
    }

    /**
     * {@inheritDoc}
     */
    public function fetchNumeric()
    {
        if (! $this->result) {
            return false;
        }

        return db2_fetch_array($this->stmt);
    }

    /**
     * {@inheritdoc}
     */
    public function fetchAssociative()
    {
        // do not try fetching from the statement if it's not expected to contain the result
        // in order to prevent exceptional situation
        if (! $this->result) {
            return false;
        }

        return db2_fetch_assoc($this->stmt);
    }

    /**
     * {@inheritdoc}
     */
    public function fetchOne()
    {
        return FetchUtils::fetchOne($this);
    }

    /**
     * {@inheritdoc}
     */
    public function fetchAllNumeric(): array
    {
        return FetchUtils::fetchAllNumeric($this);
    }

    /**
     * {@inheritdoc}
     */
    public function fetchAllAssociative(): array
    {
        return FetchUtils::fetchAllAssociative($this);
    }

    /**
     * {@inheritdoc}
     */
    public function fetchFirstColumn(): array
    {
        return FetchUtils::fetchFirstColumn($this);
    }

    /**
     * {@inheritdoc}
     */
    public function rowCount()
    {
        return @db2_num_rows($this->stmt) ? : 0;
    }

    public function free(): void
    {
        $this->bindParam = [];

        db2_free_result($this->stmt);

        $this->result = false;
    }

    /**
     * Casts a stdClass object to the given class name mapping its' properties.
     *
     * @param stdClass            $sourceObject     Object to cast from.
     * @param class-string|object $destinationClass Name of the class or class instance to cast to.
     * @param mixed[]             $ctorArgs         Arguments to use for constructing the destination class instance.
     *
     * @return object
     *
     * @throws DB2Exception
     */
    private function castObject(stdClass $sourceObject, $destinationClass, array $ctorArgs = [])
    {
        if (! is_string($destinationClass)) {
            if (! is_object($destinationClass)) {
                throw new DB2Exception(sprintf(
                    'Destination class has to be of type string or object, %s given.',
                    gettype($destinationClass)
                ));
            }
        } else {
            $destinationClass = new ReflectionClass($destinationClass);
            $destinationClass = $destinationClass->newInstanceArgs($ctorArgs);
        }

        $sourceReflection           = new ReflectionObject($sourceObject);
        $destinationClassReflection = new ReflectionObject($destinationClass);
        /** @var ReflectionProperty[] $destinationProperties */
        $destinationProperties = array_change_key_case($destinationClassReflection->getProperties(), CASE_LOWER);

        foreach ($sourceReflection->getProperties() as $sourceProperty) {
            $sourceProperty->setAccessible(true);

            $name  = $sourceProperty->getName();
            $value = $sourceProperty->getValue($sourceObject);

            // Try to find a case-matching property.
            if ($destinationClassReflection->hasProperty($name)) {
                $destinationProperty = $destinationClassReflection->getProperty($name);

                $destinationProperty->setAccessible(true);
                $destinationProperty->setValue($destinationClass, $value);

                continue;
            }

            $name = strtolower($name);

            // Try to find a property without matching case.
            // Fallback for the driver returning either all uppercase or all lowercase column names.
            if (isset($destinationProperties[$name])) {
                $destinationProperty = $destinationProperties[$name];

                $destinationProperty->setAccessible(true);
                $destinationProperty->setValue($destinationClass, $value);

                continue;
            }

            $destinationClass->$name = $value;
        }

        return $destinationClass;
    }

    /**
     * @return resource
     *
     * @throws DB2Exception
     */
    private function createTemporaryFile()
    {
        $handle = @tmpfile();

        if ($handle === false) {
            throw CannotCreateTemporaryFile::new(error_get_last());
        }

        return $handle;
    }

    /**
     * @param resource $source
     * @param resource $target
     *
     * @throws DB2Exception
     */
    private function copyStreamToStream($source, $target): void
    {
        if (@stream_copy_to_stream($source, $target) === false) {
            throw CannotCopyStreamToStream::new(error_get_last());
        }
    }

    /**
     * @param resource $target
     *
     * @throws DB2Exception
     */
    private function writeStringToStream(string $string, $target): void
    {
        if (@fwrite($target, $string) === false) {
            throw CannotWriteToTemporaryFile::new(error_get_last());
        }
    }
}
<?php

namespace Doctrine\DBAL\Driver\DrizzlePDOMySql;

use Doctrine\DBAL\Platforms\DrizzlePlatform;
use Doctrine\DBAL\Schema\DrizzleSchemaManager;

/**
 * Drizzle driver using PDO MySql.
 *
 * @deprecated
 */
class Driver extends \Doctrine\DBAL\Driver\PDOMySql\Driver
{
    /**
     * {@inheritdoc}
     */
    public function connect(array $params, $username = null, $password = null, array $driverOptions = [])
    {
        return new Connection(
            $this->constructPdoDsn($params),
            $username,
            $password,
            $driverOptions
        );
    }

    /**
     * {@inheritdoc}
     */
    public function createDatabasePlatformForVersion($version)
    {
        return $this->getDatabasePlatform();
    }

    /**
     * {@inheritdoc}
     *
     * @return DrizzlePlatform
     */
    public function getDatabasePlatform()
    {
        return new DrizzlePlatform();
    }

    /**
     * {@inheritdoc}
     *
     * @return DrizzleSchemaManager
     */
    public function getSchemaManager(\Doctrine\DBAL\Connection $conn)
    {
        return new DrizzleSchemaManager($conn);
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated
     */
    public function getName()
    {
        return 'drizzle_pdo_mysql';
    }
}
<?php

namespace Doctrine\DBAL\Driver\DrizzlePDOMySql;

use Doctrine\DBAL\Driver\PDOConnection;
use Doctrine\DBAL\ParameterType;

/**
 * @deprecated
 */
class Connection extends PDOConnection
{
    /**
     * {@inheritdoc}
     */
    public function quote($value, $type = ParameterType::STRING)
    {
        if ($type === ParameterType::BOOLEAN) {
            return $value ? 'true' : 'false';
        }

        return parent::quote($value, $type);
    }
}
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Driver\AbstractSQLServerDriver\Exception;

use Doctrine\DBAL\Driver\AbstractDriverException;

/**
 * @internal
 *
 * @psalm-immutable
 */
final class PortWithoutHost extends AbstractDriverException
{
    public static function new(): self
    {
        return new self('Connection port specified without the host');
    }
}
<?php

namespace Doctrine\DBAL\Driver\PDOIbm;

use Doctrine\DBAL\Driver\AbstractDB2Driver;
use Doctrine\DBAL\Driver\PDO\Connection;
use Doctrine\Deprecations\Deprecation;

/**
 * Driver for the PDO IBM extension.
 *
 * @deprecated Use the driver based on the ibm_db2 extension instead.
 */
class Driver extends AbstractDB2Driver
{
    /**
     * {@inheritdoc}
     */
    public function connect(array $params, $username = null, $password = null, array $driverOptions = [])
    {
        return new Connection(
            $this->_constructPdoDsn($params),
            $username,
            $password,
            $driverOptions
        );
    }

    /**
     * Constructs the IBM PDO DSN.
     *
     * @param mixed[] $params
     *
     * @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;
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated
     */
    public function getName()
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/issues/3580',
            'Driver::getName() is deprecated'
        );

        return 'pdo_ibm';
    }
}
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Driver;

use Doctrine\Deprecations\Deprecation;
use Exception as BaseException;
use Throwable;

/**
 * Base implementation of the {@link Exception} interface.
 *
 * @internal
 *
 * @psalm-immutable
 */
abstract class AbstractException extends BaseException implements DriverException
{
    /**
     * The driver specific error code.
     *
     * @var int|string|null
     */
    private $errorCode;

    /**
     * The SQLSTATE of the driver.
     *
     * @var string|null
     */
    private $sqlState;

    /**
     * @param string          $message   The driver error message.
     * @param string|null     $sqlState  The SQLSTATE the driver is in at the time the error occurred, if any.
     * @param int|string|null $errorCode The driver specific error code if any.
     */
    public function __construct($message, $sqlState = null, $errorCode = null, ?Throwable $previous = null)
    {
        parent::__construct($message, 0, $previous);

        $this->errorCode = $errorCode;
        $this->sqlState  = $sqlState;
    }

    /**
     * {@inheritdoc}
     */
    public function getErrorCode()
    {
        /** @psalm-suppress ImpureMethodCall */
        Deprecation::triggerIfCalledFromOutside(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/4112',
            'Driver\AbstractException::getErrorCode() is deprecated, use getSQLState() or getCode() instead.'
        );

        return $this->errorCode;
    }

    /**
     * {@inheritdoc}
     */
    public function getSQLState()
    {
        return $this->sqlState;
    }
}
<?php

namespace Doctrine\DBAL\Driver;

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver\DriverException as DeprecatedDriverException;
use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Exception\ConnectionException;
use Doctrine\DBAL\Exception\ConnectionLost;
use Doctrine\DBAL\Exception\DeadlockException;
use Doctrine\DBAL\Exception\DriverException;
use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
use Doctrine\DBAL\Exception\InvalidFieldNameException;
use Doctrine\DBAL\Exception\LockWaitTimeoutException;
use Doctrine\DBAL\Exception\NonUniqueFieldNameException;
use Doctrine\DBAL\Exception\NotNullConstraintViolationException;
use Doctrine\DBAL\Exception\SyntaxErrorException;
use Doctrine\DBAL\Exception\TableExistsException;
use Doctrine\DBAL\Exception\TableNotFoundException;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use Doctrine\DBAL\Platforms\MariaDb1027Platform;
use Doctrine\DBAL\Platforms\MySQL57Platform;
use Doctrine\DBAL\Platforms\MySQL80Platform;
use Doctrine\DBAL\Platforms\MySqlPlatform;
use Doctrine\DBAL\Schema\MySqlSchemaManager;
use Doctrine\DBAL\VersionAwarePlatformDriver;

use function assert;
use function preg_match;
use function stripos;
use function version_compare;

/**
 * Abstract base implementation of the {@link Driver} interface for MySQL based drivers.
 */
abstract class AbstractMySQLDriver implements Driver, ExceptionConverterDriver, VersionAwarePlatformDriver
{
    /**
     * {@inheritdoc}
     *
     * @deprecated
     *
     * @link https://dev.mysql.com/doc/mysql-errors/8.0/en/client-error-reference.html
     * @link https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html
     */
    public function convertException($message, DeprecatedDriverException $exception)
    {
        switch ($exception->getErrorCode()) {
            case '1213':
                return new DeadlockException($message, $exception);

            case '1205':
                return new LockWaitTimeoutException($message, $exception);

            case '1050':
                return new TableExistsException($message, $exception);

            case '1051':
            case '1146':
                return new TableNotFoundException($message, $exception);

            case '1216':
            case '1217':
            case '1451':
            case '1452':
            case '1701':
                return new ForeignKeyConstraintViolationException($message, $exception);

            case '1062':
            case '1557':
            case '1569':
            case '1586':
                return new UniqueConstraintViolationException($message, $exception);

            case '1054':
            case '1166':
            case '1611':
                return new InvalidFieldNameException($message, $exception);

            case '1052':
            case '1060':
            case '1110':
                return new NonUniqueFieldNameException($message, $exception);

            case '1064':
            case '1149':
            case '1287':
            case '1341':
            case '1342':
            case '1343':
            case '1344':
            case '1382':
            case '1479':
            case '1541':
            case '1554':
            case '1626':
                return new SyntaxErrorException($message, $exception);

            case '1044':
            case '1045':
            case '1046':
            case '1049':
            case '1095':
            case '1142':
            case '1143':
            case '1227':
            case '1370':
            case '1429':
            case '2002':
            case '2005':
                return new ConnectionException($message, $exception);

            case '2006':
                return new ConnectionLost($message, $exception);

            case '1048':
            case '1121':
            case '1138':
            case '1171':
            case '1252':
            case '1263':
            case '1364':
            case '1566':
                return new NotNullConstraintViolationException($message, $exception);
        }

        return new DriverException($message, $exception);
    }

    /**
     * {@inheritdoc}
     *
     * @throws Exception
     */
    public function createDatabasePlatformForVersion($version)
    {
        $mariadb = stripos($version, 'mariadb') !== false;
        if ($mariadb && version_compare($this->getMariaDbMysqlVersionNumber($version), '10.2.7', '>=')) {
            return new MariaDb1027Platform();
        }

        if (! $mariadb) {
            $oracleMysqlVersion = $this->getOracleMysqlVersionNumber($version);
            if (version_compare($oracleMysqlVersion, '8', '>=')) {
                return new MySQL80Platform();
            }

            if (version_compare($oracleMysqlVersion, '5.7.9', '>=')) {
                return new MySQL57Platform();
            }
        }

        return $this->getDatabasePlatform();
    }

    /**
     * Get a normalized 'version number' from the server string
     * returned by Oracle MySQL servers.
     *
     * @param string $versionString Version string returned by the driver, i.e. '5.7.10'
     *
     * @throws Exception
     */
    private function getOracleMysqlVersionNumber(string $versionString): string
    {
        if (
            ! preg_match(
                '/^(?P<major>\d+)(?:\.(?P<minor>\d+)(?:\.(?P<patch>\d+))?)?/',
                $versionString,
                $versionParts
            )
        ) {
            throw Exception::invalidPlatformVersionSpecified(
                $versionString,
                '<major_version>.<minor_version>.<patch_version>'
            );
        }

        $majorVersion = $versionParts['major'];
        $minorVersion = $versionParts['minor'] ?? 0;
        $patchVersion = $versionParts['patch'] ?? null;

        if ($majorVersion === '5' && $minorVersion === '7' && $patchVersion === null) {
            $patchVersion = '9';
        }

        return $majorVersion . '.' . $minorVersion . '.' . $patchVersion;
    }

    /**
     * Detect MariaDB server version, including hack for some mariadb distributions
     * that starts with the prefix '5.5.5-'
     *
     * @param string $versionString Version string as returned by mariadb server, i.e. '5.5.5-Mariadb-10.0.8-xenial'
     *
     * @throws Exception
     */
    private function getMariaDbMysqlVersionNumber(string $versionString): string
    {
        if (
            ! preg_match(
                '/^(?:5\.5\.5-)?(mariadb-)?(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)/i',
                $versionString,
                $versionParts
            )
        ) {
            throw Exception::invalidPlatformVersionSpecified(
                $versionString,
                '^(?:5\.5\.5-)?(mariadb-)?<major_version>.<minor_version>.<patch_version>'
            );
        }

        return $versionParts['major'] . '.' . $versionParts['minor'] . '.' . $versionParts['patch'];
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use Connection::getDatabase() instead.
     */
    public function getDatabase(Connection $conn)
    {
        $params = $conn->getParams();

        if (isset($params['dbname'])) {
            return $params['dbname'];
        }

        $database = $conn->query('SELECT DATABASE()')->fetchColumn();

        assert($database !== false);

        return $database;
    }

    /**
     * {@inheritdoc}
     *
     * @return MySqlPlatform
     */
    public function getDatabasePlatform()
    {
        return new MySqlPlatform();
    }

    /**
     * {@inheritdoc}
     *
     * @return MySqlSchemaManager
     */
    public function getSchemaManager(Connection $conn)
    {
        return new MySqlSchemaManager($conn);
    }
}
<?php

namespace Doctrine\DBAL\Driver;

use Doctrine\DBAL\ParameterType;

/**
 * Connection interface.
 * Driver connections must implement this interface.
 *
 * This resembles (a subset of) the PDO interface.
 */
interface Connection
{
    /**
     * Prepares a statement for execution and returns a Statement object.
     *
     * @param string $sql
     *
     * @return Statement
     */
    public function prepare($sql);

    /**
     * Executes an SQL statement, returning a result set as a Statement object.
     *
     * @return Statement
     */
    public function query();

    /**
     * Quotes a string for use in a query.
     *
     * @param mixed $value
     * @param int   $type
     *
     * @return mixed
     */
    public function quote($value, $type = ParameterType::STRING);

    /**
     * Executes an SQL statement and return the number of affected rows.
     *
     * @param string $sql
     *
     * @return int|string
     */
    public function exec($sql);

    /**
     * Returns the ID of the last inserted row or sequence value.
     *
     * @param string|null $name
     *
     * @return string|int|false
     */
    public function lastInsertId($name = null);

    /**
     * Initiates a transaction.
     *
     * @return bool TRUE on success or FALSE on failure.
     */
    public function beginTransaction();

    /**
     * Commits a transaction.
     *
     * @return bool TRUE on success or FALSE on failure.
     */
    public function commit();

    /**
     * Rolls back the current transaction, as initiated by beginTransaction().
     *
     * @return bool TRUE on success or FALSE on failure.
     */
    public function rollBack();

    /**
     * Returns the error code associated with the last operation on the database handle.
     *
     * @deprecated The error information is available via exceptions.
     *
     * @return string|null The error code, or null if no operation has been run on the database handle.
     */
    public function errorCode();

    /**
     * Returns extended error information associated with the last operation on the database handle.
     *
     * @deprecated The error information is available via exceptions.
     *
     * @return mixed[]
     */
    public function errorInfo();
}
<?php

namespace Doctrine\DBAL\Driver;

use Doctrine\DBAL\Driver\DriverException as TheDriverException;
use Doctrine\DBAL\Exception\DriverException;

/**
 * Contract for a driver that is capable of converting DBAL driver exceptions into standardized DBAL driver exceptions.
 *
 * @deprecated
 */
interface ExceptionConverterDriver
{
    /**
     * Converts a given DBAL driver exception into a standardized DBAL driver exception.
     *
     * It evaluates the vendor specific error code and SQLSTATE and transforms
     * it into a unified {@link DriverException} subclass.
     *
     * @deprecated
     *
     * @param string             $message   The DBAL exception message to use.
     * @param TheDriverException $exception The DBAL driver exception to convert.
     *
     * @return DriverException An instance of one of the DriverException subclasses.
     */
    public function convertException($message, TheDriverException $exception);
}
<?php

namespace Doctrine\DBAL\Driver;

/**
 * An interface for connections which support a "native" ping method.
 *
 * @deprecated
 */
interface PingableConnection
{
    /**
     * Pings the database server to determine if the connection is still
     * available. Return true/false based on if that was successful or not.
     *
     * @return bool
     */
    public function ping();
}
<?php

namespace Doctrine\DBAL\Driver\PDOOracle;

use Doctrine\DBAL\Driver\AbstractOracleDriver;
use Doctrine\DBAL\Driver\PDO;
use Doctrine\DBAL\Exception;
use Doctrine\Deprecations\Deprecation;
use PDOException;

/**
 * PDO Oracle driver.
 *
 * @deprecated Use {@link PDO\OCI\Driver} instead.
 */
class Driver extends AbstractOracleDriver
{
    /**
     * {@inheritdoc}
     */
    public function connect(array $params, $username = null, $password = null, array $driverOptions = [])
    {
        try {
            return new PDO\Connection(
                $this->constructPdoDsn($params),
                $username,
                $password,
                $driverOptions
            );
        } catch (PDOException $e) {
            throw Exception::driverException($this, $e);
        }
    }

    /**
     * Constructs the Oracle PDO DSN.
     *
     * @param mixed[] $params
     *
     * @return string The DSN.
     */
    private function constructPdoDsn(array $params)
    {
        $dsn = 'oci:dbname=' . $this->getEasyConnectString($params);

        if (isset($params['charset'])) {
            $dsn .= ';charset=' . $params['charset'];
        }

        return $dsn;
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/issues/3580',
            'Driver::getName() is deprecated'
        );

        return 'pdo_oracle';
    }
}
<?php

namespace Doctrine\DBAL\Driver\PDOSqlite;

use Doctrine\DBAL\Driver\AbstractSQLiteDriver;
use Doctrine\DBAL\Driver\PDO;
use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Platforms\SqlitePlatform;
use Doctrine\Deprecations\Deprecation;
use PDOException;

use function array_merge;

/**
 * The PDO Sqlite driver.
 *
 * @deprecated Use {@link PDO\SQLite\Driver} instead.
 */
class Driver extends AbstractSQLiteDriver
{
    /** @var mixed[] */
    protected $_userDefinedFunctions = [
        'sqrt' => ['callback' => [SqlitePlatform::class, 'udfSqrt'], 'numArgs' => 1],
        'mod'  => ['callback' => [SqlitePlatform::class, 'udfMod'], 'numArgs' => 2],
        'locate'  => ['callback' => [SqlitePlatform::class, 'udfLocate'], 'numArgs' => -1],
    ];

    /**
     * {@inheritdoc}
     */
    public function connect(array $params, $username = null, $password = null, array $driverOptions = [])
    {
        if (isset($driverOptions['userDefinedFunctions'])) {
            $this->_userDefinedFunctions = array_merge(
                $this->_userDefinedFunctions,
                $driverOptions['userDefinedFunctions']
            );
            unset($driverOptions['userDefinedFunctions']);
        }

        try {
            $pdo = new PDO\Connection(
                $this->_constructPdoDsn($params),
                $username,
                $password,
                $driverOptions
            );
        } catch (PDOException $ex) {
            throw Exception::driverException($this, $ex);
        }

        foreach ($this->_userDefinedFunctions as $fn => $data) {
            $pdo->sqliteCreateFunction($fn, $data['callback'], $data['numArgs']);
        }

        return $pdo;
    }

    /**
     * Constructs the Sqlite PDO DSN.
     *
     * @param mixed[] $params
     *
     * @return string The DSN.
     */
    protected function _constructPdoDsn(array $params)
    {
        $dsn = 'sqlite:';
        if (isset($params['path'])) {
            $dsn .= $params['path'];
        } elseif (isset($params['memory'])) {
            $dsn .= ':memory:';
        }

        return $dsn;
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated
     */
    public function getName()
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/issues/3580',
            'Driver::getName() is deprecated'
        );

        return 'pdo_sqlite';
    }
}
<?php

namespace Doctrine\DBAL\Driver;

/**
 * @deprecated Use {@link Exception} instead
 *
 * @psalm-immutable
 */
interface DriverException extends Exception
{
}
<?php

namespace Doctrine\DBAL\Driver\PDO\SQLSrv;

use Doctrine\DBAL\Driver\PDOSqlsrv;

final class Statement extends PDOSqlsrv\Statement
{
}
<?php

namespace Doctrine\DBAL\Driver\PDO\SQLSrv;

use Doctrine\DBAL\Driver\PDOSqlsrv;

final class Driver extends PDOSqlsrv\Driver
{
}
<?php

namespace Doctrine\DBAL\Driver\PDO\SQLSrv;

use Doctrine\DBAL\Driver\PDOSqlsrv;

final class Connection extends PDOSqlsrv\Connection
{
}
<?php

namespace Doctrine\DBAL\Driver\PDO;

use Doctrine\DBAL\Driver\PDOStatement;

class Statement extends PDOStatement
{
}
<?php

namespace Doctrine\DBAL\Driver\PDO\SQLite;

use Doctrine\DBAL\Driver\PDOSqlite;

final class Driver extends PDOSqlite\Driver
{
}
<?php

namespace Doctrine\DBAL\Driver\PDO;

use Doctrine\DBAL\Driver\PDOConnection;

class Connection extends PDOConnection
{
}
<?php

namespace Doctrine\DBAL\Driver\PDO\OCI;

use Doctrine\DBAL\Driver\PDOOracle;

final class Driver extends PDOOracle\Driver
{
}
<?php

namespace Doctrine\DBAL\Driver\PDO\PgSQL;

use Doctrine\DBAL\Driver\PDOPgSql;

final class Driver extends PDOPgSql\Driver
{
}
<?php

namespace Doctrine\DBAL\Driver\PDO\MySQL;

use Doctrine\DBAL\Driver\PDOMySql;

final class Driver extends PDOMySql\Driver
{
}
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Driver\PDO;

use Doctrine\DBAL\Driver\PDOException;

/**
 * @internal
 *
 * @psalm-immutable
 */
final class Exception extends PDOException
{
    public static function new(\PDOException $exception): self
    {
        return new self($exception);
    }
}
<?php

namespace Doctrine\DBAL\Driver\SQLAnywhere;

use Doctrine\DBAL\Driver\AbstractSQLAnywhereDriver;
use Doctrine\DBAL\Exception;
use Doctrine\Deprecations\Deprecation;

use function array_keys;
use function array_map;
use function implode;

/**
 * A Doctrine DBAL driver for the SAP Sybase SQL Anywhere PHP extension.
 *
 * @deprecated Support for SQLAnywhere will be removed in 3.0.
 */
class Driver extends AbstractSQLAnywhereDriver
{
    /**
     * {@inheritdoc}
     *
     * @throws Exception If there was a problem establishing the connection.
     */
    public function connect(array $params, $username = null, $password = null, array $driverOptions = [])
    {
        try {
            return new SQLAnywhereConnection(
                $this->buildDsn(
                    $params['host'] ?? null,
                    $params['port'] ?? null,
                    $params['server'] ?? null,
                    $params['dbname'] ?? null,
                    $username,
                    $password,
                    $driverOptions
                ),
                $params['persistent'] ?? false
            );
        } catch (SQLAnywhereException $e) {
            throw Exception::driverException($this, $e);
        }
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated
     */
    public function getName()
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/issues/3580',
            'Driver::getName() is deprecated'
        );

        return 'sqlanywhere';
    }

    /**
     * Build the connection string for given connection parameters and driver options.
     *
     * @param string|null $host          Host address to connect to.
     * @param int|null    $port          Port to use for the connection (default to SQL Anywhere standard port 2638).
     * @param string|null $server        Database server name on the host to connect to.
     *                              SQL Anywhere allows multiple database server instances on the same host,
     *                              therefore specifying the server instance name to use is mandatory.
     * @param string|null $dbname        Name of the database on the server instance to connect to.
     * @param string      $username      User name to use for connection authentication.
     * @param string      $password      Password to use for connection authentication.
     * @param mixed[]     $driverOptions Additional parameters to use for the connection.
     *
     * @return string
     */
    private function buildDsn(
        $host,
        $port,
        $server,
        $dbname,
        $username = null,
        $password = null,
        array $driverOptions = []
    ) {
        $host = $host ?: 'localhost';
        $port = $port ?: 2638;

        if (! empty($server)) {
            $server = ';ServerName=' . $server;
        }

        return 'HOST=' . $host . ':' . $port .
            $server .
            ';DBN=' . $dbname .
            ';UID=' . $username .
            ';PWD=' . $password .
            ';' . implode(
                ';',
                array_map(static function ($key, $value) {
                    return $key . '=' . $value;
                }, array_keys($driverOptions), $driverOptions)
            );
    }
}
<?php

namespace Doctrine\DBAL\Driver\SQLAnywhere;

use Doctrine\DBAL\Driver\AbstractDriverException;
use InvalidArgumentException;

use function sasql_error;
use function sasql_errorcode;
use function sasql_sqlstate;
use function sasql_stmt_errno;
use function sasql_stmt_error;

/**
 * SAP Sybase SQL Anywhere driver exception.
 *
 * @deprecated Support for SQLAnywhere will be removed in 3.0.
 *
 * @psalm-immutable
 */
class SQLAnywhereException extends AbstractDriverException
{
    /**
     * Helper method to turn SQL Anywhere error into exception.
     *
     * @param resource|null $conn The SQL Anywhere connection resource to retrieve the last error from.
     * @param resource|null $stmt The SQL Anywhere statement resource to retrieve the last error from.
     *
     * @return SQLAnywhereException
     *
     * @throws InvalidArgumentException
     */
    public static function fromSQLAnywhereError($conn = null, $stmt = null)
    {
        $state   = $conn ? sasql_sqlstate($conn) : sasql_sqlstate();
        $code    = null;
        $message = null;

        /**
         * Try retrieving the last error from statement resource if given
         */
        if ($stmt) {
            $code    = sasql_stmt_errno($stmt);
            $message = sasql_stmt_error($stmt);
        }

        /**
         * Try retrieving the last error from the connection resource
         * if either the statement resource is not given or the statement
         * resource is given but the last error could not be retrieved from it (fallback).
         * Depending on the type of error, it is sometimes necessary to retrieve
         * it from the connection resource even though it occurred during
         * a prepared statement.
         */
        if ($conn && ! $code) {
            $code    = sasql_errorcode($conn);
            $message = sasql_error($conn);
        }

        /**
         * Fallback mode if either no connection resource is given
         * or the last error could not be retrieved from the given
         * connection / statement resource.
         */
        if (! $conn || ! $code) {
            $code    = sasql_errorcode();
            $message = sasql_error();
        }

        if ($message) {
            return new self('SQLSTATE [' . $state . '] [' . $code . '] ' . $message, $state, $code);
        }

        return new self('SQL Anywhere error occurred but no error message was retrieved from driver.', $state, $code);
    }
}
<?php

namespace Doctrine\DBAL\Driver\SQLAnywhere;

use Doctrine\DBAL\Driver\Exception;
use Doctrine\DBAL\Driver\FetchUtils;
use Doctrine\DBAL\Driver\Result;
use Doctrine\DBAL\Driver\Statement;
use Doctrine\DBAL\Driver\StatementIterator;
use Doctrine\DBAL\FetchMode;
use Doctrine\DBAL\ParameterType;
use IteratorAggregate;
use PDO;
use ReflectionClass;
use ReflectionObject;
use ReturnTypeWillChange;
use stdClass;

use function array_key_exists;
use function assert;
use function func_get_args;
use function func_num_args;
use function gettype;
use function is_array;
use function is_int;
use function is_object;
use function is_resource;
use function is_string;
use function sasql_fetch_array;
use function sasql_fetch_assoc;
use function sasql_fetch_object;
use function sasql_fetch_row;
use function sasql_prepare;
use function sasql_stmt_affected_rows;
use function sasql_stmt_bind_param_ex;
use function sasql_stmt_errno;
use function sasql_stmt_error;
use function sasql_stmt_execute;
use function sasql_stmt_field_count;
use function sasql_stmt_reset;
use function sasql_stmt_result_metadata;
use function sprintf;

use const SASQL_BOTH;

/**
 * SAP SQL Anywhere implementation of the Statement interface.
 *
 * @deprecated Support for SQLAnywhere will be removed in 3.0.
 */
class SQLAnywhereStatement implements IteratorAggregate, Statement, Result
{
    /** @var resource The connection resource. */
    private $conn;

    /** @var string Name of the default class to instantiate when fetching class instances. */
    private $defaultFetchClass = '\stdClass';

    /** @var mixed[] Constructor arguments for the default class to instantiate when fetching class instances. */
    private $defaultFetchClassCtorArgs = [];

    /** @var int Default fetch mode to use. */
    private $defaultFetchMode = FetchMode::MIXED;

    /** @var resource|null The result set resource to fetch. */
    private $result;

    /** @var resource The prepared SQL statement to execute. */
    private $stmt;

    /** @var mixed[] The references to bound parameter values. */
    private $boundValues = [];

    /**
     * Prepares given statement for given connection.
     *
     * @internal The statement can be only instantiated by its driver connection.
     *
     * @param resource $conn The connection resource to use.
     * @param string   $sql  The SQL statement to prepare.
     *
     * @throws SQLAnywhereException
     */
    public function __construct($conn, $sql)
    {
        if (! is_resource($conn)) {
            throw new SQLAnywhereException('Invalid SQL Anywhere connection resource: ' . $conn);
        }

        $this->conn = $conn;
        $this->stmt = sasql_prepare($conn, $sql);

        if (! is_resource($this->stmt)) {
            throw SQLAnywhereException::fromSQLAnywhereError($conn);
        }
    }

    /**
     * {@inheritdoc}
     *
     * @throws SQLAnywhereException
     */
    public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null)
    {
        assert(is_int($param));

        switch ($type) {
            case ParameterType::INTEGER:
            case ParameterType::BOOLEAN:
                $type = 'i';
                break;

            case ParameterType::LARGE_OBJECT:
                $type = 'b';
                break;

            case ParameterType::NULL:
            case ParameterType::STRING:
            case ParameterType::BINARY:
                $type = 's';
                break;

            default:
                throw new SQLAnywhereException('Unknown type: ' . $type);
        }

        $this->boundValues[$param] =& $variable;

        if (! sasql_stmt_bind_param_ex($this->stmt, $param - 1, $variable, $type, $variable === null)) {
            throw SQLAnywhereException::fromSQLAnywhereError($this->conn, $this->stmt);
        }

        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function bindValue($param, $value, $type = ParameterType::STRING)
    {
        assert(is_int($param));

        return $this->bindParam($param, $value, $type);
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use free() instead.
     *
     * @throws SQLAnywhereException
     */
    public function closeCursor()
    {
        if (! sasql_stmt_reset($this->stmt)) {
            throw SQLAnywhereException::fromSQLAnywhereError($this->conn, $this->stmt);
        }

        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function columnCount()
    {
        return sasql_stmt_field_count($this->stmt);
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated The error information is available via exceptions.
     */
    public function errorCode()
    {
        return sasql_stmt_errno($this->stmt);
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated The error information is available via exceptions.
     */
    public function errorInfo()
    {
        return sasql_stmt_error($this->stmt);
    }

    /**
     * {@inheritdoc}
     *
     * @throws SQLAnywhereException
     */
    public function execute($params = null)
    {
        if (is_array($params)) {
            $hasZeroIndex = array_key_exists(0, $params);

            foreach ($params as $key => $val) {
                if ($hasZeroIndex && is_int($key)) {
                    $this->bindValue($key + 1, $val);
                } else {
                    $this->bindValue($key, $val);
                }
            }
        }

        if (! sasql_stmt_execute($this->stmt)) {
            throw SQLAnywhereException::fromSQLAnywhereError($this->conn, $this->stmt);
        }

        $this->result = sasql_stmt_result_metadata($this->stmt);

        return true;
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use fetchNumeric(), fetchAssociative() or fetchOne() instead.
     *
     * @throws SQLAnywhereException
     */
    public function fetch($fetchMode = null, $cursorOrientation = PDO::FETCH_ORI_NEXT, $cursorOffset = 0)
    {
        if (! is_resource($this->result)) {
            return false;
        }

        $fetchMode = $fetchMode ?: $this->defaultFetchMode;

        switch ($fetchMode) {
            case FetchMode::COLUMN:
                return $this->fetchColumn();

            case FetchMode::ASSOCIATIVE:
                return sasql_fetch_assoc($this->result);

            case FetchMode::MIXED:
                return sasql_fetch_array($this->result, SASQL_BOTH);

            case FetchMode::CUSTOM_OBJECT:
                $className = $this->defaultFetchClass;
                $ctorArgs  = $this->defaultFetchClassCtorArgs;

                if (func_num_args() >= 2) {
                    $args      = func_get_args();
                    $className = $args[1];
                    $ctorArgs  = $args[2] ?? [];
                }

                $result = sasql_fetch_object($this->result);

                if ($result instanceof stdClass) {
                    $result = $this->castObject($result, $className, $ctorArgs);
                }

                return $result;

            case FetchMode::NUMERIC:
                return sasql_fetch_row($this->result);

            case FetchMode::STANDARD_OBJECT:
                return sasql_fetch_object($this->result);

            default:
                throw new SQLAnywhereException('Fetch mode is not supported: ' . $fetchMode);
        }
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use fetchAllNumeric(), fetchAllAssociative() or fetchFirstColumn() instead.
     */
    public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null)
    {
        $rows = [];

        switch ($fetchMode) {
            case FetchMode::CUSTOM_OBJECT:
                while (($row = $this->fetch(...func_get_args())) !== false) {
                    $rows[] = $row;
                }

                break;

            case FetchMode::COLUMN:
                while (($row = $this->fetchColumn()) !== false) {
                    $rows[] = $row;
                }

                break;

            default:
                while (($row = $this->fetch($fetchMode)) !== false) {
                    $rows[] = $row;
                }
        }

        return $rows;
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use fetchOne() instead.
     */
    public function fetchColumn($columnIndex = 0)
    {
        $row = $this->fetch(FetchMode::NUMERIC);

        if ($row === false) {
            return false;
        }

        return $row[$columnIndex] ?? null;
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use iterateNumeric(), iterateAssociative() or iterateColumn() instead.
     */
    #[ReturnTypeWillChange]
    public function getIterator()
    {
        return new StatementIterator($this);
    }

    /**
     * {@inheritDoc}
     */
    public function fetchNumeric()
    {
        if (! is_resource($this->result)) {
            return false;
        }

        return sasql_fetch_row($this->result);
    }

    /**
     * {@inheritdoc}
     */
    public function fetchAssociative()
    {
        if (! is_resource($this->result)) {
            return false;
        }

        return sasql_fetch_assoc($this->result);
    }

    /**
     * {@inheritdoc}
     *
     * @throws Exception
     */
    public function fetchOne()
    {
        return FetchUtils::fetchOne($this);
    }

    /**
     * @return array<int,array<int,mixed>>
     *
     * @throws Exception
     */
    public function fetchAllNumeric(): array
    {
        return FetchUtils::fetchAllNumeric($this);
    }

    /**
     * @return array<int,array<string,mixed>>
     *
     * @throws Exception
     */
    public function fetchAllAssociative(): array
    {
        return FetchUtils::fetchAllAssociative($this);
    }

    /**
     * @return array<int,mixed>
     *
     * @throws Exception
     */
    public function fetchFirstColumn(): array
    {
        return FetchUtils::fetchFirstColumn($this);
    }

    /**
     * {@inheritdoc}
     */
    public function rowCount()
    {
        return sasql_stmt_affected_rows($this->stmt);
    }

    public function free(): void
    {
        sasql_stmt_reset($this->stmt);
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use one of the fetch- or iterate-related methods.
     */
    public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null)
    {
        $this->defaultFetchMode          = $fetchMode;
        $this->defaultFetchClass         = $arg2 ?: $this->defaultFetchClass;
        $this->defaultFetchClassCtorArgs = $arg3 ? (array) $arg3 : $this->defaultFetchClassCtorArgs;

        return true;
    }

    /**
     * Casts a stdClass object to the given class name mapping its' properties.
     *
     * @param stdClass            $sourceObject     Object to cast from.
     * @param class-string|object $destinationClass Name of the class or class instance to cast to.
     * @param mixed[]             $ctorArgs         Arguments to use for constructing the destination class instance.
     *
     * @return object
     *
     * @throws SQLAnywhereException
     */
    private function castObject(stdClass $sourceObject, $destinationClass, array $ctorArgs = [])
    {
        if (! is_string($destinationClass)) {
            if (! is_object($destinationClass)) {
                throw new SQLAnywhereException(sprintf(
                    'Destination class has to be of type string or object, %s given.',
                    gettype($destinationClass)
                ));
            }
        } else {
            $destinationClass = new ReflectionClass($destinationClass);
            $destinationClass = $destinationClass->newInstanceArgs($ctorArgs);
        }

        $sourceReflection           = new ReflectionObject($sourceObject);
        $destinationClassReflection = new ReflectionObject($destinationClass);

        foreach ($sourceReflection->getProperties() as $sourceProperty) {
            $sourceProperty->setAccessible(true);

            $name  = $sourceProperty->getName();
            $value = $sourceProperty->getValue($sourceObject);

            if ($destinationClassReflection->hasProperty($name)) {
                $destinationProperty = $destinationClassReflection->getProperty($name);

                $destinationProperty->setAccessible(true);
                $destinationProperty->setValue($destinationClass, $value);
            } else {
                $destinationClass->$name = $value;
            }
        }

        return $destinationClass;
    }
}
<?php

namespace Doctrine\DBAL\Driver\SQLAnywhere;

use Doctrine\DBAL\Driver\Connection;
use Doctrine\DBAL\Driver\Result;
use Doctrine\DBAL\Driver\ServerInfoAwareConnection;
use Doctrine\DBAL\ParameterType;
use Doctrine\Deprecations\Deprecation;

use function assert;
use function func_get_args;
use function is_float;
use function is_int;
use function is_resource;
use function is_string;
use function sasql_affected_rows;
use function sasql_commit;
use function sasql_connect;
use function sasql_error;
use function sasql_errorcode;
use function sasql_escape_string;
use function sasql_insert_id;
use function sasql_pconnect;
use function sasql_real_query;
use function sasql_rollback;
use function sasql_set_option;

/**
 * SAP Sybase SQL Anywhere implementation of the Connection interface.
 *
 * @deprecated Support for SQLAnywhere will be removed in 3.0.
 */
class SQLAnywhereConnection implements Connection, ServerInfoAwareConnection
{
    /** @var resource The SQL Anywhere connection resource. */
    private $connection;

    /**
     * Connects to database with given connection string.
     *
     * @internal The connection can be only instantiated by its driver.
     *
     * @param string $dsn        The connection string.
     * @param bool   $persistent Whether or not to establish a persistent connection.
     *
     * @throws SQLAnywhereException
     */
    public function __construct($dsn, $persistent = false)
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/4077',
            'The SQLAnywhere driver is deprecated'
        );

        $this->connection = $persistent ? @sasql_pconnect($dsn) : @sasql_connect($dsn);

        if (! is_resource($this->connection)) {
            throw SQLAnywhereException::fromSQLAnywhereError();
        }

        // Disable PHP warnings on error.
        if (! sasql_set_option($this->connection, 'verbose_errors', false)) {
            throw SQLAnywhereException::fromSQLAnywhereError($this->connection);
        }

        // Enable auto committing by default.
        if (! sasql_set_option($this->connection, 'auto_commit', 'on')) {
            throw SQLAnywhereException::fromSQLAnywhereError($this->connection);
        }
    }

    /**
     * {@inheritdoc}
     *
     * @throws SQLAnywhereException
     */
    public function beginTransaction()
    {
        if (! sasql_set_option($this->connection, 'auto_commit', 'off')) {
            throw SQLAnywhereException::fromSQLAnywhereError($this->connection);
        }

        return true;
    }

    /**
     * {@inheritdoc}
     *
     * @throws SQLAnywhereException
     */
    public function commit()
    {
        if (! sasql_commit($this->connection)) {
            throw SQLAnywhereException::fromSQLAnywhereError($this->connection);
        }

        $this->endTransaction();

        return true;
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated The error information is available via exceptions.
     */
    public function errorCode()
    {
        return sasql_errorcode($this->connection);
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated The error information is available via exceptions.
     */
    public function errorInfo()
    {
        return sasql_error($this->connection);
    }

    /**
     * {@inheritdoc}
     */
    public function exec($sql)
    {
        if (sasql_real_query($this->connection, $sql) === false) {
            throw SQLAnywhereException::fromSQLAnywhereError($this->connection);
        }

        return sasql_affected_rows($this->connection);
    }

    /**
     * {@inheritdoc}
     */
    public function getServerVersion()
    {
        $stmt = $this->query("SELECT PROPERTY('ProductVersion')");

        if ($stmt instanceof Result) {
            $version = $stmt->fetchOne();
        } else {
            $version = $stmt->fetchColumn();
        }

        assert(is_string($version));

        return $version;
    }

    /**
     * {@inheritdoc}
     */
    public function lastInsertId($name = null)
    {
        if ($name === null) {
            return sasql_insert_id($this->connection);
        }

        $stmt = $this->query('SELECT ' . $name . '.CURRVAL');

        if ($stmt instanceof Result) {
            return $stmt->fetchOne();
        }

        return $stmt->fetchColumn();
    }

    /**
     * {@inheritdoc}
     */
    public function prepare($sql)
    {
        return new SQLAnywhereStatement($this->connection, $sql);
    }

    /**
     * {@inheritdoc}
     */
    public function query()
    {
        $args = func_get_args();
        $stmt = $this->prepare($args[0]);

        $stmt->execute();

        return $stmt;
    }

    /**
     * {@inheritdoc}
     */
    public function quote($value, $type = ParameterType::STRING)
    {
        if (is_int($value) || is_float($value)) {
            return $value;
        }

        return "'" . sasql_escape_string($this->connection, $value) . "'";
    }

    /**
     * {@inheritdoc}
     */
    public function requiresQueryForServerVersion()
    {
        Deprecation::triggerIfCalledFromOutside(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/4114',
            'ServerInfoAwareConnection::requiresQueryForServerVersion() is deprecated and removed in DBAL 3.'
        );

        return true;
    }

    /**
     * {@inheritdoc}
     *
     * @throws SQLAnywhereException
     */
    public function rollBack()
    {
        if (! sasql_rollback($this->connection)) {
            throw SQLAnywhereException::fromSQLAnywhereError($this->connection);
        }

        $this->endTransaction();

        return true;
    }

    /**
     * Ends transactional mode and enables auto commit again.
     *
     * @return bool Whether or not ending transactional mode succeeded.
     *
     * @throws SQLAnywhereException
     */
    private function endTransaction()
    {
        if (! sasql_set_option($this->connection, 'auto_commit', 'on')) {
            throw SQLAnywhereException::fromSQLAnywhereError($this->connection);
        }

        return true;
    }
}
<?php

namespace Doctrine\DBAL\Driver;

use PDO;
use Traversable;

/**
 * Interface for the reading part of a prepare statement only.
 */
interface ResultStatement extends Traversable
{
    /**
     * Closes the cursor, enabling the statement to be executed again.
     *
     * @deprecated Use Result::free() instead.
     *
     * @return bool TRUE on success or FALSE on failure.
     */
    public function closeCursor();

    /**
     * Returns the number of columns in the result set
     *
     * @return int The number of columns in the result set represented
     *                 by the PDOStatement object. If there is no result set,
     *                 this method should return 0.
     */
    public function columnCount();

    /**
     * Sets the fetch mode to use while iterating this statement.
     *
     * @deprecated Use one of the fetch- or iterate-related methods.
     *
     * @param int   $fetchMode The fetch mode must be one of the {@link FetchMode} constants.
     * @param mixed $arg2
     * @param mixed $arg3
     *
     * @return bool
     */
    public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null);

    /**
     * Returns the next row of a result set.
     *
     * @deprecated Use fetchNumeric(), fetchAssociative() or fetchOne() instead.
     *
     * @param int|null $fetchMode         Controls how the next row will be returned to the caller.
     *                                    The value must be one of the {@link FetchMode} constants,
     *                                    defaulting to {@link FetchMode::MIXED}.
     * @param int      $cursorOrientation For a ResultStatement object representing a scrollable cursor,
     *                                    this value determines which row will be returned to the caller.
     *                                    This value must be one of the \PDO::FETCH_ORI_* constants,
     *                                    defaulting to \PDO::FETCH_ORI_NEXT. To request a scrollable
     *                                    cursor for your ResultStatement object, you must set the \PDO::ATTR_CURSOR
     *                                    attribute to \PDO::CURSOR_SCROLL when you prepare the SQL statement with
     *                                    \PDO::prepare().
     * @param int      $cursorOffset      For a ResultStatement object representing a scrollable cursor for which the
     *                                    cursorOrientation parameter is set to \PDO::FETCH_ORI_ABS, this value
     *                                    specifies the absolute number of the row in the result set that shall be
     *                                    fetched.
     *                                    For a ResultStatement object representing a scrollable cursor for which the
     *                                    cursorOrientation parameter is set to \PDO::FETCH_ORI_REL, this value
     *                                    specifies the row to fetch relative to the cursor position before
     *                                    ResultStatement::fetch() was called.
     *
     * @return mixed The return value of this method on success depends on the fetch mode. In all cases, FALSE is
     *               returned on failure.
     */
    public function fetch($fetchMode = null, $cursorOrientation = PDO::FETCH_ORI_NEXT, $cursorOffset = 0);

    /**
     * Returns an array containing all of the result set rows.
     *
     * @deprecated Use fetchAllNumeric(), fetchAllAssociative() or fetchFirstColumn() instead.
     *
     * @param int|null        $fetchMode     Controls how the next row will be returned to the caller.
     *                                       The value must be one of the {@link FetchMode} constants,
     *                                       defaulting to {@link FetchMode::MIXED}.
     * @param int|string|null $fetchArgument This argument has a different meaning depending on the value
     *                                       of the $fetchMode parameter:
     *                                       * {@link FetchMode::COLUMN}:
     *                                         Returns the indicated 0-indexed column.
     *                                       * {@link FetchMode::CUSTOM_OBJECT}:
     *                                         Returns instances of the specified class, mapping the columns of each row
     *                                         to named properties in the class.
     *                                       * {@link PDO::FETCH_FUNC}: Returns the results of calling
     *                                         the specified function, using each row's
     *                                         columns as parameters in the call.
     * @param mixed[]|null    $ctorArgs      Controls how the next row will be returned to the caller.
     *                                       The value must be one of the {@link FetchMode} constants,
     *                                       defaulting to {@link FetchMode::MIXED}.
     *
     * @return mixed[]
     */
    public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null);

    /**
     * Returns a single column from the next row of a result set or FALSE if there are no more rows.
     *
     * @deprecated Use fetchOne() instead.
     *
     * @param int $columnIndex 0-indexed number of the column you wish to retrieve from the row.
     *                         If no value is supplied, fetches the first column.
     *
     * @return mixed|false A single column in the next row of a result set, or FALSE if there are no more rows.
     */
    public function fetchColumn($columnIndex = 0);
}
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Driver\Mysqli\Exception;

use Doctrine\DBAL\Driver\Mysqli\MysqliException;
use mysqli;
use mysqli_sql_exception;
use ReflectionProperty;

/**
 * @internal
 *
 * @psalm-immutable
 */
final class ConnectionError extends MysqliException
{
    public static function new(mysqli $connection): self
    {
        return new self($connection->error, $connection->sqlstate, $connection->errno);
    }

    public static function upcast(mysqli_sql_exception $exception): self
    {
        $p = new ReflectionProperty(mysqli_sql_exception::class, 'sqlstate');
        $p->setAccessible(true);

        return new self($exception->getMessage(), $p->getValue($exception), $exception->getCode(), $exception);
    }
}
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Driver\Mysqli\Exception;

use Doctrine\DBAL\Driver\Mysqli\MysqliException;

use function sprintf;

/**
 * @internal
 *
 * @psalm-immutable
 */
final class InvalidOption extends MysqliException
{
    /**
     * @param mixed $option
     * @param mixed $value
     */
    public static function fromOption($option, $value): self
    {
        return new self(
            sprintf('Failed to set option %d with value "%s"', $option, $value)
        );
    }
}
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Driver\Mysqli\Exception;

use Doctrine\DBAL\Driver\Mysqli\MysqliException;
use mysqli;
use mysqli_sql_exception;
use ReflectionProperty;

use function assert;

/**
 * @internal
 *
 * @psalm-immutable
 */
final class ConnectionFailed extends MysqliException
{
    public static function new(mysqli $connection): self
    {
        $error = $connection->connect_error;
        assert($error !== null);

        return new self($error, 'HY000', $connection->connect_errno);
    }

    public static function upcast(mysqli_sql_exception $exception): self
    {
        $p = new ReflectionProperty(mysqli_sql_exception::class, 'sqlstate');
        $p->setAccessible(true);

        return new self($exception->getMessage(), $p->getValue($exception), $exception->getCode(), $exception);
    }
}
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Driver\Mysqli\Exception;

use Doctrine\DBAL\Driver\Mysqli\MysqliException;

use function sprintf;

/**
 * @internal
 *
 * @psalm-immutable
 */
final class FailedReadingStreamOffset extends MysqliException
{
    public static function new(int $offset): self
    {
        return new self(sprintf('Failed reading the stream resource for parameter offset %d.', $offset));
    }
}
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Driver\Mysqli\Exception;

use Doctrine\DBAL\Driver\Mysqli\MysqliException;

use function sprintf;

/**
 * @internal
 *
 * @psalm-immutable
 */
final class UnknownType extends MysqliException
{
    /**
     * @param mixed $type
     */
    public static function new($type): self
    {
        return new self(sprintf('Unknown type, %d given.', $type));
    }
}
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Driver\Mysqli\Exception;

use Doctrine\DBAL\Driver\Mysqli\MysqliException;
use mysqli_sql_exception;
use mysqli_stmt;
use ReflectionProperty;

/**
 * @internal
 *
 * @psalm-immutable
 */
final class StatementError extends MysqliException
{
    public static function new(mysqli_stmt $statement): self
    {
        return new self($statement->error, $statement->sqlstate, $statement->errno);
    }

    public static function upcast(mysqli_sql_exception $exception): self
    {
        $p = new ReflectionProperty(mysqli_sql_exception::class, 'sqlstate');
        $p->setAccessible(true);

        return new self($exception->getMessage(), $p->getValue($exception), $exception->getCode(), $exception);
    }
}
<?php

namespace Doctrine\DBAL\Driver\Mysqli;

final class Statement extends MysqliStatement
{
}
<?php

namespace Doctrine\DBAL\Driver\Mysqli;

use Doctrine\DBAL\Driver\Connection as ConnectionInterface;
use Doctrine\DBAL\Driver\Mysqli\Exception\ConnectionError;
use Doctrine\DBAL\Driver\Mysqli\Exception\ConnectionFailed;
use Doctrine\DBAL\Driver\Mysqli\Exception\InvalidOption;
use Doctrine\DBAL\Driver\PingableConnection;
use Doctrine\DBAL\Driver\ServerInfoAwareConnection;
use Doctrine\DBAL\ParameterType;
use Doctrine\Deprecations\Deprecation;
use mysqli;
use mysqli_sql_exception;

use function assert;
use function floor;
use function func_get_args;
use function in_array;
use function ini_get;
use function mysqli_errno;
use function mysqli_error;
use function mysqli_init;
use function mysqli_options;
use function sprintf;
use function stripos;

use const MYSQLI_INIT_COMMAND;
use const MYSQLI_OPT_CONNECT_TIMEOUT;
use const MYSQLI_OPT_LOCAL_INFILE;
use const MYSQLI_OPT_READ_TIMEOUT;
use const MYSQLI_READ_DEFAULT_FILE;
use const MYSQLI_READ_DEFAULT_GROUP;
use const MYSQLI_SERVER_PUBLIC_KEY;

/**
 * @deprecated Use {@link Connection} instead
 */
class MysqliConnection implements ConnectionInterface, PingableConnection, ServerInfoAwareConnection
{
    /**
     * Name of the option to set connection flags
     */
    public const OPTION_FLAGS = 'flags';

    /** @var mysqli */
    private $conn;

    /**
     * @internal The connection can be only instantiated by its driver.
     *
     * @param mixed[] $params
     * @param string  $username
     * @param string  $password
     * @param mixed[] $driverOptions
     *
     * @throws MysqliException
     */
    public function __construct(array $params, $username, $password, array $driverOptions = [])
    {
        $port = $params['port'] ?? ini_get('mysqli.default_port');

        // Fallback to default MySQL port if not given.
        if (! $port) {
            $port = 3306;
        }

        $socket = $params['unix_socket'] ?? ini_get('mysqli.default_socket');
        $dbname = $params['dbname'] ?? null;

        $flags = $driverOptions[static::OPTION_FLAGS] ?? 0;

        $conn = mysqli_init();
        assert($conn !== false);

        $this->conn = $conn;

        $this->setSecureConnection($params);
        $this->setDriverOptions($driverOptions);

        try {
            $success = @$this->conn
                ->real_connect($params['host'], $username, $password, $dbname, $port, $socket, $flags);
        } catch (mysqli_sql_exception $e) {
            throw ConnectionFailed::upcast($e);
        }

        if (! $success) {
            throw ConnectionFailed::new($this->conn);
        }

        if (! isset($params['charset'])) {
            return;
        }

        $this->conn->set_charset($params['charset']);
    }

    /**
     * Retrieves mysqli native resource handle.
     *
     * Could be used if part of your application is not using DBAL.
     *
     * @return mysqli
     */
    public function getWrappedResourceHandle()
    {
        return $this->conn;
    }

    /**
     * {@inheritdoc}
     *
     * The server version detection includes a special case for MariaDB
     * to support '5.5.5-' prefixed versions introduced in Maria 10+
     *
     * @link https://jira.mariadb.org/browse/MDEV-4088
     */
    public function getServerVersion()
    {
        $serverInfos = $this->conn->get_server_info();
        if (stripos($serverInfos, 'mariadb') !== false) {
            return $serverInfos;
        }

        $majorVersion = floor($this->conn->server_version / 10000);
        $minorVersion = floor(($this->conn->server_version - $majorVersion * 10000) / 100);
        $patchVersion = floor($this->conn->server_version - $majorVersion * 10000 - $minorVersion * 100);

        return $majorVersion . '.' . $minorVersion . '.' . $patchVersion;
    }

    /**
     * {@inheritdoc}
     */
    public function requiresQueryForServerVersion()
    {
        Deprecation::triggerIfCalledFromOutside(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/4114',
            'ServerInfoAwareConnection::requiresQueryForServerVersion() is deprecated and removed in DBAL 3.'
        );

        return false;
    }

    /**
     * {@inheritdoc}
     */
    public function prepare($sql)
    {
        return new Statement($this->conn, $sql);
    }

    /**
     * {@inheritdoc}
     */
    public function query()
    {
        $args = func_get_args();
        $sql  = $args[0];
        $stmt = $this->prepare($sql);
        $stmt->execute();

        return $stmt;
    }

    /**
     * {@inheritdoc}
     */
    public function quote($value, $type = ParameterType::STRING)
    {
        return "'" . $this->conn->escape_string($value) . "'";
    }

    /**
     * {@inheritdoc}
     */
    public function exec($sql)
    {
        try {
            $result = $this->conn->query($sql);
        } catch (mysqli_sql_exception $e) {
            throw ConnectionError::upcast($e);
        }

        if ($result === false) {
            throw ConnectionError::new($this->conn);
        }

        return $this->conn->affected_rows;
    }

    /**
     * {@inheritdoc}
     */
    public function lastInsertId($name = null)
    {
        return $this->conn->insert_id;
    }

    /**
     * {@inheritdoc}
     */
    public function beginTransaction()
    {
        $this->conn->query('START TRANSACTION');

        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function commit()
    {
        try {
            return $this->conn->commit();
        } catch (mysqli_sql_exception $e) {
            return false;
        }
    }

    /**
     * {@inheritdoc}non-PHPdoc)
     */
    public function rollBack()
    {
        try {
            return $this->conn->rollback();
        } catch (mysqli_sql_exception $e) {
            return false;
        }
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated The error information is available via exceptions.
     *
     * @return int
     */
    public function errorCode()
    {
        return $this->conn->errno;
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated The error information is available via exceptions.
     *
     * @return string
     */
    public function errorInfo()
    {
        return $this->conn->error;
    }

    /**
     * Apply the driver options to the connection.
     *
     * @param mixed[] $driverOptions
     *
     * @throws MysqliException When one of of the options is not supported.
     * @throws MysqliException When applying doesn't work - e.g. due to incorrect value.
     */
    private function setDriverOptions(array $driverOptions = []): void
    {
        $supportedDriverOptions = [
            MYSQLI_OPT_CONNECT_TIMEOUT,
            MYSQLI_OPT_LOCAL_INFILE,
            MYSQLI_OPT_READ_TIMEOUT,
            MYSQLI_INIT_COMMAND,
            MYSQLI_READ_DEFAULT_FILE,
            MYSQLI_READ_DEFAULT_GROUP,
            MYSQLI_SERVER_PUBLIC_KEY,
        ];

        $exceptionMsg = "%s option '%s' with value '%s'";

        foreach ($driverOptions as $option => $value) {
            if ($option === static::OPTION_FLAGS) {
                continue;
            }

            if (! in_array($option, $supportedDriverOptions, true)) {
                throw InvalidOption::fromOption($option, $value);
            }

            if (@mysqli_options($this->conn, $option, $value)) {
                continue;
            }

            $msg  = sprintf($exceptionMsg, 'Failed to set', $option, $value);
            $msg .= sprintf(', error: %s (%d)', mysqli_error($this->conn), mysqli_errno($this->conn));

            throw new MysqliException(
                $msg,
                $this->conn->sqlstate,
                $this->conn->errno
            );
        }
    }

    /**
     * Pings the server and re-connects when `mysqli.reconnect = 1`
     *
     * @deprecated
     *
     * @return bool
     */
    public function ping()
    {
        return $this->conn->ping();
    }

    /**
     * Establish a secure connection
     *
     * @param array<string,string> $params
     *
     * @throws MysqliException
     */
    private function setSecureConnection(array $params): void
    {
        if (
            ! isset($params['ssl_key']) &&
            ! isset($params['ssl_cert']) &&
            ! isset($params['ssl_ca']) &&
            ! isset($params['ssl_capath']) &&
            ! isset($params['ssl_cipher'])
        ) {
            return;
        }

        $this->conn->ssl_set(
            $params['ssl_key']    ?? '',
            $params['ssl_cert']   ?? '',
            $params['ssl_ca']     ?? '',
            $params['ssl_capath'] ?? '',
            $params['ssl_cipher'] ?? ''
        );
    }
}
<?php

namespace Doctrine\DBAL\Driver\Mysqli;

use Doctrine\DBAL\Driver\AbstractMySQLDriver;
use Doctrine\DBAL\Exception;
use Doctrine\Deprecations\Deprecation;

class Driver extends AbstractMySQLDriver
{
    /**
     * {@inheritdoc}
     */
    public function connect(array $params, $username = null, $password = null, array $driverOptions = [])
    {
        try {
            return new Connection($params, (string) $username, (string) $password, $driverOptions);
        } catch (MysqliException $e) {
            throw Exception::driverException($this, $e);
        }
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/issues/3580',
            'Driver::getName() is deprecated'
        );

        return 'mysqli';
    }
}
<?php

namespace Doctrine\DBAL\Driver\Mysqli;

final class Connection extends MysqliConnection
{
}
<?php

namespace Doctrine\DBAL\Driver\Mysqli;

use Doctrine\DBAL\Driver\FetchUtils;
use Doctrine\DBAL\Driver\Mysqli\Exception\ConnectionError;
use Doctrine\DBAL\Driver\Mysqli\Exception\FailedReadingStreamOffset;
use Doctrine\DBAL\Driver\Mysqli\Exception\StatementError;
use Doctrine\DBAL\Driver\Mysqli\Exception\UnknownType;
use Doctrine\DBAL\Driver\Result;
use Doctrine\DBAL\Driver\Statement as StatementInterface;
use Doctrine\DBAL\Driver\StatementIterator;
use Doctrine\DBAL\Exception\InvalidArgumentException;
use Doctrine\DBAL\FetchMode;
use Doctrine\DBAL\ParameterType;
use IteratorAggregate;
use mysqli;
use mysqli_sql_exception;
use mysqli_stmt;
use PDO;
use ReturnTypeWillChange;

use function array_combine;
use function array_fill;
use function assert;
use function count;
use function feof;
use function fread;
use function get_resource_type;
use function is_array;
use function is_int;
use function is_resource;
use function sprintf;
use function str_repeat;

/**
 * @deprecated Use {@link Statement} instead
 */
class MysqliStatement implements IteratorAggregate, StatementInterface, Result
{
    /** @var string[] */
    protected static $_paramTypeMap = [
        ParameterType::ASCII        => 's',
        ParameterType::STRING       => 's',
        ParameterType::BINARY       => 's',
        ParameterType::BOOLEAN      => 'i',
        ParameterType::NULL         => 's',
        ParameterType::INTEGER      => 'i',
        ParameterType::LARGE_OBJECT => 'b',
    ];

    /** @var mysqli */
    protected $_conn;

    /** @var mysqli_stmt */
    protected $_stmt;

    /** @var string[]|false|null */
    protected $_columnNames;

    /** @var mixed[] */
    protected $_rowBindedValues = [];

    /** @var mixed[] */
    protected $_bindedValues;

    /** @var string */
    protected $types;

    /**
     * Contains ref values for bindValue().
     *
     * @var mixed[]
     */
    protected $_values = [];

    /** @var int */
    protected $_defaultFetchMode = FetchMode::MIXED;

    /**
     * Indicates whether the statement is in the state when fetching results is possible
     *
     * @var bool
     */
    private $result = false;

    /**
     * @internal The statement can be only instantiated by its driver connection.
     *
     * @param string $prepareString
     *
     * @throws MysqliException
     */
    public function __construct(mysqli $conn, $prepareString)
    {
        $this->_conn = $conn;

        try {
            $stmt = $conn->prepare($prepareString);
        } catch (mysqli_sql_exception $e) {
            throw ConnectionError::upcast($e);
        }

        if ($stmt === false) {
            throw ConnectionError::new($this->_conn);
        }

        $this->_stmt = $stmt;

        $paramCount          = $this->_stmt->param_count;
        $this->types         = str_repeat('s', $paramCount);
        $this->_bindedValues = array_fill(1, $paramCount, null);
    }

    /**
     * {@inheritdoc}
     */
    public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null)
    {
        assert(is_int($param));

        if (! isset(self::$_paramTypeMap[$type])) {
            throw UnknownType::new($type);
        }

        $this->_bindedValues[$param] =& $variable;
        $this->types[$param - 1]     = self::$_paramTypeMap[$type];

        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function bindValue($param, $value, $type = ParameterType::STRING)
    {
        assert(is_int($param));

        if (! isset(self::$_paramTypeMap[$type])) {
            throw UnknownType::new($type);
        }

        $this->_values[$param]       = $value;
        $this->_bindedValues[$param] =& $this->_values[$param];
        $this->types[$param - 1]     = self::$_paramTypeMap[$type];

        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function execute($params = null)
    {
        if ($params !== null && count($params) > 0) {
            if (! $this->bindUntypedValues($params)) {
                throw StatementError::new($this->_stmt);
            }
        } elseif (count($this->_bindedValues) > 0) {
            $this->bindTypedParameters();
        }

        try {
            $result = $this->_stmt->execute();
        } catch (mysqli_sql_exception $e) {
            throw StatementError::upcast($e);
        }

        if (! $result) {
            throw StatementError::new($this->_stmt);
        }

        if ($this->_columnNames === null) {
            $meta = $this->_stmt->result_metadata();
            if ($meta !== false) {
                $fields = $meta->fetch_fields();
                assert(is_array($fields));

                $columnNames = [];
                foreach ($fields as $col) {
                    $columnNames[] = $col->name;
                }

                $meta->free();

                $this->_columnNames = $columnNames;
            } else {
                $this->_columnNames = false;
            }
        }

        if ($this->_columnNames !== false) {
            // Store result of every execution which has it. Otherwise it will be impossible
            // to execute a new statement in case if the previous one has non-fetched rows
            // @link http://dev.mysql.com/doc/refman/5.7/en/commands-out-of-sync.html
            $this->_stmt->store_result();

            // Bind row values _after_ storing the result. Otherwise, if mysqli is compiled with libmysql,
            // it will have to allocate as much memory as it may be needed for the given column type
            // (e.g. for a LONGBLOB column it's 4 gigabytes)
            // @link https://bugs.php.net/bug.php?id=51386#1270673122
            //
            // Make sure that the values are bound after each execution. Otherwise, if closeCursor() has been
            // previously called on the statement, the values are unbound making the statement unusable.
            //
            // It's also important that row values are bound after _each_ call to store_result(). Otherwise,
            // if mysqli is compiled with libmysql, subsequently fetched string values will get truncated
            // to the length of the ones fetched during the previous execution.
            $this->_rowBindedValues = array_fill(0, count($this->_columnNames), null);

            $refs = [];
            foreach ($this->_rowBindedValues as $key => &$value) {
                $refs[$key] =& $value;
            }

            if (! $this->_stmt->bind_result(...$refs)) {
                throw StatementError::new($this->_stmt);
            }
        }

        $this->result = true;

        return true;
    }

    /**
     * Binds parameters with known types previously bound to the statement
     */
    private function bindTypedParameters(): void
    {
        $streams = $values = [];
        $types   = $this->types;

        foreach ($this->_bindedValues as $parameter => $value) {
            assert(is_int($parameter));

            if (! isset($types[$parameter - 1])) {
                $types[$parameter - 1] = static::$_paramTypeMap[ParameterType::STRING];
            }

            if ($types[$parameter - 1] === static::$_paramTypeMap[ParameterType::LARGE_OBJECT]) {
                if (is_resource($value)) {
                    if (get_resource_type($value) !== 'stream') {
                        throw new InvalidArgumentException(
                            'Resources passed with the LARGE_OBJECT parameter type must be stream resources.'
                        );
                    }

                    $streams[$parameter] = $value;
                    $values[$parameter]  = null;
                    continue;
                }

                $types[$parameter - 1] = static::$_paramTypeMap[ParameterType::STRING];
            }

            $values[$parameter] = $value;
        }

        if (! $this->_stmt->bind_param($types, ...$values)) {
            throw StatementError::new($this->_stmt);
        }

        $this->sendLongData($streams);
    }

    /**
     * Handle $this->_longData after regular query parameters have been bound
     *
     * @param array<int, resource> $streams
     *
     * @throws MysqliException
     */
    private function sendLongData(array $streams): void
    {
        foreach ($streams as $paramNr => $stream) {
            while (! feof($stream)) {
                $chunk = fread($stream, 8192);

                if ($chunk === false) {
                    throw FailedReadingStreamOffset::new($paramNr);
                }

                if (! $this->_stmt->send_long_data($paramNr - 1, $chunk)) {
                    throw StatementError::new($this->_stmt);
                }
            }
        }
    }

    /**
     * Binds a array of values to bound parameters.
     *
     * @param mixed[] $values
     *
     * @return bool
     */
    private function bindUntypedValues(array $values)
    {
        $params = [];
        $types  = str_repeat('s', count($values));

        foreach ($values as &$v) {
            $params[] =& $v;
        }

        return $this->_stmt->bind_param($types, ...$params);
    }

    /**
     * @return mixed[]|false|null
     *
     * @throws StatementError
     */
    private function _fetch()
    {
        try {
            $ret = $this->_stmt->fetch();
        } catch (mysqli_sql_exception $e) {
            throw StatementError::upcast($e);
        }

        if ($ret === true) {
            $values = [];
            foreach ($this->_rowBindedValues as $v) {
                $values[] = $v;
            }

            return $values;
        }

        return $ret;
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use fetchNumeric(), fetchAssociative() or fetchOne() instead.
     */
    public function fetch($fetchMode = null, $cursorOrientation = PDO::FETCH_ORI_NEXT, $cursorOffset = 0)
    {
        // do not try fetching from the statement if it's not expected to contain result
        // in order to prevent exceptional situation
        if (! $this->result) {
            return false;
        }

        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;

        if ($fetchMode === FetchMode::COLUMN) {
            return $this->fetchColumn();
        }

        $values = $this->_fetch();

        if ($values === null) {
            return false;
        }

        if ($values === false) {
            throw StatementError::new($this->_stmt);
        }

        if ($fetchMode === FetchMode::NUMERIC) {
            return $values;
        }

        assert(is_array($this->_columnNames));
        $assoc = array_combine($this->_columnNames, $values);
        assert(is_array($assoc));

        switch ($fetchMode) {
            case FetchMode::ASSOCIATIVE:
                return $assoc;

            case FetchMode::MIXED:
                return $assoc + $values;

            case FetchMode::STANDARD_OBJECT:
                return (object) $assoc;

            default:
                throw new MysqliException(sprintf("Unknown fetch type '%s'", $fetchMode));
        }
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use fetchAllNumeric(), fetchAllAssociative() or fetchFirstColumn() instead.
     */
    public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null)
    {
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;

        $rows = [];

        if ($fetchMode === FetchMode::COLUMN) {
            while (($row = $this->fetchColumn()) !== false) {
                $rows[] = $row;
            }
        } else {
            while (($row = $this->fetch($fetchMode)) !== false) {
                $rows[] = $row;
            }
        }

        return $rows;
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use fetchOne() instead.
     */
    public function fetchColumn($columnIndex = 0)
    {
        $row = $this->fetch(FetchMode::NUMERIC);

        if ($row === false) {
            return false;
        }

        return $row[$columnIndex] ?? null;
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated The error information is available via exceptions.
     */
    public function fetchNumeric()
    {
        // do not try fetching from the statement if it's not expected to contain the result
        // in order to prevent exceptional situation
        if (! $this->result) {
            return false;
        }

        $values = $this->_fetch();

        if ($values === null) {
            return false;
        }

        if ($values === false) {
            throw StatementError::new($this->_stmt);
        }

        return $values;
    }

    /**
     * {@inheritDoc}
     */
    public function fetchAssociative()
    {
        $values = $this->fetchNumeric();

        if ($values === false) {
            return false;
        }

        assert(is_array($this->_columnNames));
        $row = array_combine($this->_columnNames, $values);
        assert(is_array($row));

        return $row;
    }

    /**
     * {@inheritdoc}
     */
    public function fetchOne()
    {
        return FetchUtils::fetchOne($this);
    }

    /**
     * {@inheritdoc}
     */
    public function fetchAllNumeric(): array
    {
        return FetchUtils::fetchAllNumeric($this);
    }

    /**
     * {@inheritdoc}
     */
    public function fetchAllAssociative(): array
    {
        return FetchUtils::fetchAllAssociative($this);
    }

    /**
     * {@inheritdoc}
     */
    public function fetchFirstColumn(): array
    {
        return FetchUtils::fetchFirstColumn($this);
    }

    /**
     * {@inheritdoc}
     */
    public function errorCode()
    {
        return $this->_stmt->errno;
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated The error information is available via exceptions.
     *
     * @return string
     */
    public function errorInfo()
    {
        return $this->_stmt->error;
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use free() instead.
     */
    public function closeCursor()
    {
        $this->free();

        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function rowCount()
    {
        if ($this->_columnNames === false) {
            return $this->_stmt->affected_rows;
        }

        return $this->_stmt->num_rows;
    }

    /**
     * {@inheritdoc}
     */
    public function columnCount()
    {
        return $this->_stmt->field_count;
    }

    public function free(): void
    {
        $this->_stmt->free_result();
        $this->result = false;
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use one of the fetch- or iterate-related methods.
     */
    public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null)
    {
        $this->_defaultFetchMode = $fetchMode;

        return true;
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use iterateNumeric(), iterateAssociative() or iterateColumn() instead.
     */
    #[ReturnTypeWillChange]
    public function getIterator()
    {
        return new StatementIterator($this);
    }
}
<?php

namespace Doctrine\DBAL\Driver\Mysqli;

use Doctrine\DBAL\Driver\AbstractDriverException;

/**
 * @deprecated Use {@link \Doctrine\DBAL\Driver\Exception} instead
 *
 * @psalm-immutable
 */
class MysqliException extends AbstractDriverException
{
}
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Driver;

/**
 * Driver-level result statement execution result.
 */
interface Result
{
    /**
     * Returns the next row of the result as a numeric array or FALSE if there are no more rows.
     *
     * @return array<int,mixed>|false
     *
     * @throws Exception
     */
    public function fetchNumeric();

    /**
     * Returns the next row of the result as an associative array or FALSE if there are no more rows.
     *
     * @return array<string,mixed>|false
     *
     * @throws Exception
     */
    public function fetchAssociative();

    /**
     * Returns the first value of the next row of the result or FALSE if there are no more rows.
     *
     * @return mixed|false
     *
     * @throws Exception
     */
    public function fetchOne();

    /**
     * Returns an array containing all of the result rows represented as numeric arrays.
     *
     * @return array<int,array<int,mixed>>
     *
     * @throws Exception
     */
    public function fetchAllNumeric(): array;

    /**
     * Returns an array containing all of the result rows represented as associative arrays.
     *
     * @return array<int,array<string,mixed>>
     *
     * @throws Exception
     */
    public function fetchAllAssociative(): array;

    /**
     * Returns an array containing the values of the first column of the result.
     *
     * @return array<int,mixed>
     *
     * @throws Exception
     */
    public function fetchFirstColumn(): array;

    /**
     * Returns the number of rows affected by the DELETE, INSERT, or UPDATE statement that produced the result.
     *
     * If the statement executed a SELECT query or a similar platform-specific SQL (e.g. DESCRIBE, SHOW, etc.),
     * some database drivers may return the number of rows returned by that query. However, this behaviour
     * is not guaranteed for all drivers and should not be relied on in portable applications.
     *
     * @return int|string The number of rows.
     */
    public function rowCount();

    /**
     * Returns the number of columns in the result
     *
     * @return int The number of columns in the result. If the columns cannot be counted,
     *             this method must return 0.
     */
    public function columnCount();

    /**
     * Discards the non-fetched portion of the result, enabling the originating statement to be executed again.
     */
    public function free(): void;
}
<?php

namespace Doctrine\DBAL\Driver;

use IteratorAggregate;
use ReturnTypeWillChange;

/**
 * @deprecated Use iterateNumeric(), iterateAssociative() or iterateColumn().
 */
class StatementIterator implements IteratorAggregate
{
    /** @var ResultStatement */
    private $statement;

    public function __construct(ResultStatement $statement)
    {
        $this->statement = $statement;
    }

    /**
     * {@inheritdoc}
     */
    #[ReturnTypeWillChange]
    public function getIterator()
    {
        while (($result = $this->statement->fetch()) !== false) {
            yield $result;
        }
    }
}
<?php

namespace Doctrine\DBAL\Driver;

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver\DriverException as DeprecatedDriverException;
use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Exception\ConnectionException;
use Doctrine\DBAL\Exception\DeadlockException;
use Doctrine\DBAL\Exception\DriverException;
use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
use Doctrine\DBAL\Exception\InvalidFieldNameException;
use Doctrine\DBAL\Exception\NonUniqueFieldNameException;
use Doctrine\DBAL\Exception\NotNullConstraintViolationException;
use Doctrine\DBAL\Exception\SyntaxErrorException;
use Doctrine\DBAL\Exception\TableExistsException;
use Doctrine\DBAL\Exception\TableNotFoundException;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use Doctrine\DBAL\Platforms\PostgreSQL100Platform;
use Doctrine\DBAL\Platforms\PostgreSQL91Platform;
use Doctrine\DBAL\Platforms\PostgreSQL92Platform;
use Doctrine\DBAL\Platforms\PostgreSQL94Platform;
use Doctrine\DBAL\Platforms\PostgreSqlPlatform;
use Doctrine\DBAL\Schema\PostgreSqlSchemaManager;
use Doctrine\DBAL\VersionAwarePlatformDriver;

use function assert;
use function preg_match;
use function strpos;
use function version_compare;

/**
 * Abstract base implementation of the {@link Driver} interface for PostgreSQL based drivers.
 */
abstract class AbstractPostgreSQLDriver implements Driver, ExceptionConverterDriver, VersionAwarePlatformDriver
{
    /**
     * {@inheritdoc}
     *
     * @deprecated
     *
     * @link http://www.postgresql.org/docs/9.3/static/errcodes-appendix.html
     */
    public function convertException($message, DeprecatedDriverException $exception)
    {
        $sqlState = $exception->getSQLState();

        switch ($sqlState) {
            case '40001':
            case '40P01':
                return new DeadlockException($message, $exception);

            case '0A000':
                // Foreign key constraint violations during a TRUNCATE operation
                // are considered "feature not supported" in PostgreSQL.
                if (strpos($exception->getMessage(), 'truncate') !== false) {
                    return new ForeignKeyConstraintViolationException($message, $exception);
                }

                break;

            case '23502':
                return new NotNullConstraintViolationException($message, $exception);

            case '23503':
                return new ForeignKeyConstraintViolationException($message, $exception);

            case '23505':
                return new UniqueConstraintViolationException($message, $exception);

            case '42601':
                return new SyntaxErrorException($message, $exception);

            case '42702':
                return new NonUniqueFieldNameException($message, $exception);

            case '42703':
                return new InvalidFieldNameException($message, $exception);

            case '42P01':
                return new TableNotFoundException($message, $exception);

            case '42P07':
                return new TableExistsException($message, $exception);

            case '08006':
                return new Exception\ConnectionException($message, $exception);

            case '7':
                // Prior to fixing https://bugs.php.net/bug.php?id=64705 (PHP 7.3.22 and PHP 7.4.10),
                // in some cases (mainly connection errors) the PDO exception wouldn't provide a SQLSTATE via its code.
                // The exception code would be always set to 7 here.
                // We have to match against the SQLSTATE in the error message in these cases.
                if (strpos($exception->getMessage(), 'SQLSTATE[08006]') !== false) {
                    return new ConnectionException($message, $exception);
                }

                break;
        }

        return new DriverException($message, $exception);
    }

    /**
     * {@inheritdoc}
     */
    public function createDatabasePlatformForVersion($version)
    {
        if (! preg_match('/^(?P<major>\d+)(?:\.(?P<minor>\d+)(?:\.(?P<patch>\d+))?)?/', $version, $versionParts)) {
            throw Exception::invalidPlatformVersionSpecified(
                $version,
                '<major_version>.<minor_version>.<patch_version>'
            );
        }

        $majorVersion = $versionParts['major'];
        $minorVersion = $versionParts['minor'] ?? 0;
        $patchVersion = $versionParts['patch'] ?? 0;
        $version      = $majorVersion . '.' . $minorVersion . '.' . $patchVersion;

        switch (true) {
            case version_compare($version, '10.0', '>='):
                return new PostgreSQL100Platform();
            case version_compare($version, '9.4', '>='):
                return new PostgreSQL94Platform();
            case version_compare($version, '9.2', '>='):
                return new PostgreSQL92Platform();
            case version_compare($version, '9.1', '>='):
                return new PostgreSQL91Platform();
            default:
                return new PostgreSqlPlatform();
        }
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use Connection::getDatabase() instead.
     */
    public function getDatabase(Connection $conn)
    {
        $params = $conn->getParams();

        if (isset($params['dbname'])) {
            return $params['dbname'];
        }

        $database = $conn->query('SELECT CURRENT_DATABASE()')->fetchColumn();

        assert($database !== false);

        return $database;
    }

    /**
     * {@inheritdoc}
     */
    public function getDatabasePlatform()
    {
        return new PostgreSqlPlatform();
    }

    /**
     * {@inheritdoc}
     */
    public function getSchemaManager(Connection $conn)
    {
        return new PostgreSqlSchemaManager($conn);
    }
}
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Driver;

use Throwable;

/**
 * @psalm-immutable
 */
interface Exception extends Throwable
{
    /**
     * Returns the driver specific error code if available.
     *
     * @deprecated Use {@link getCode()} or {@link getSQLState()} instead
     *
     * Returns null if no driver specific error code is available
     * for the error raised by the driver.
     *
     * @return int|string|null
     */
    public function getErrorCode();

    /**
     * Returns the SQLSTATE the driver was in at the time the error occurred.
     *
     * Returns null if the driver does not provide a SQLSTATE for the error occurred.
     *
     * @return string|null
     */
    public function getSQLState();
}
<?php

namespace Doctrine\DBAL\Driver;

use Doctrine\DBAL\Driver\Connection as ConnectionInterface;
use Doctrine\DBAL\Driver\PDO\Exception;
use Doctrine\DBAL\Driver\PDO\Statement;
use Doctrine\DBAL\ParameterType;
use Doctrine\Deprecations\Deprecation;
use PDO;
use PDOException;
use PDOStatement;
use ReturnTypeWillChange;

use function assert;

/**
 * PDO implementation of the Connection interface.
 * Used by all PDO-based drivers.
 *
 * @deprecated Use {@link Connection} instead
 */
class PDOConnection extends PDO implements ConnectionInterface, ServerInfoAwareConnection
{
    use PDOQueryImplementation;

    /**
     * @internal The connection can be only instantiated by its driver.
     *
     * @param string       $dsn
     * @param string|null  $user
     * @param string|null  $password
     * @param mixed[]|null $options
     *
     * @throws PDOException In case of an error.
     */
    public function __construct($dsn, $user = null, $password = null, ?array $options = null)
    {
        try {
            parent::__construct($dsn, (string) $user, (string) $password, (array) $options);
            $this->setAttribute(PDO::ATTR_STATEMENT_CLASS, [Statement::class, []]);
            $this->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        } catch (PDOException $exception) {
            throw Exception::new($exception);
        }
    }

    /**
     * {@inheritdoc}
     */
    #[ReturnTypeWillChange]
    public function exec($sql)
    {
        try {
            $result = parent::exec($sql);
            assert($result !== false);

            return $result;
        } catch (PDOException $exception) {
            throw Exception::new($exception);
        }
    }

    /**
     * {@inheritdoc}
     */
    public function getServerVersion()
    {
        return PDO::getAttribute(PDO::ATTR_SERVER_VERSION);
    }

    /**
     * @param string          $sql
     * @param array<int, int> $driverOptions
     *
     * @return PDOStatement
     */
    #[ReturnTypeWillChange]
    public function prepare($sql, $driverOptions = [])
    {
        try {
            $statement = parent::prepare($sql, $driverOptions);
            assert($statement instanceof PDOStatement);

            return $statement;
        } catch (PDOException $exception) {
            throw Exception::new($exception);
        }
    }

    /**
     * {@inheritdoc}
     */
    #[ReturnTypeWillChange]
    public function quote($value, $type = ParameterType::STRING)
    {
        return parent::quote($value, $type);
    }

    /**
     * {@inheritdoc}
     *
     * @param string|null $name
     *
     * @return string|int|false
     */
    #[ReturnTypeWillChange]
    public function lastInsertId($name = null)
    {
        try {
            if ($name === null) {
                return parent::lastInsertId();
            }

            return parent::lastInsertId($name);
        } catch (PDOException $exception) {
            throw Exception::new($exception);
        }
    }

    /**
     * {@inheritdoc}
     */
    public function requiresQueryForServerVersion()
    {
        Deprecation::triggerIfCalledFromOutside(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/4114',
            'ServerInfoAwareConnection::requiresQueryForServerVersion() is deprecated and removed in DBAL 3.'
        );

        return false;
    }

    /**
     * @param mixed ...$args
     */
    private function doQuery(...$args): PDOStatement
    {
        try {
            $stmt = parent::query(...$args);
        } catch (PDOException $exception) {
            throw Exception::new($exception);
        }

        assert($stmt instanceof PDOStatement);

        return $stmt;
    }
}
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Driver\AbstractOracleDriver;

use function implode;
use function is_array;
use function sprintf;

/**
 * Represents an Oracle Easy Connect string
 *
 * @link https://docs.oracle.com/database/121/NETAG/naming.htm
 */
final class EasyConnectString
{
    /** @var string */
    private $string;

    private function __construct(string $string)
    {
        $this->string = $string;
    }

    public function __toString(): string
    {
        return $this->string;
    }

    /**
     * Creates the object from an array representation
     *
     * @param mixed[] $params
     */
    public static function fromArray(array $params): self
    {
        return new self(self::renderParams($params));
    }

    /**
     * Creates the object from the given DBAL connection parameters.
     *
     * @param mixed[] $params
     */
    public static function fromConnectionParameters(array $params): self
    {
        if (! empty($params['connectstring'])) {
            return new self($params['connectstring']);
        }

        if (empty($params['host'])) {
            return new self($params['dbname'] ?? '');
        }

        $connectData = [];

        if (isset($params['servicename']) || isset($params['dbname'])) {
            $serviceKey = 'SID';

            if (! empty($params['service'])) {
                $serviceKey = 'SERVICE_NAME';
            }

            $serviceName = $params['servicename'] ?? $params['dbname'];

            $connectData[$serviceKey] = $serviceName;
        }

        if (! empty($params['instancename'])) {
            $connectData['INSTANCE_NAME'] = $params['instancename'];
        }

        if (! empty($params['pooled'])) {
            $connectData['SERVER'] = 'POOLED';
        }

        return self::fromArray([
            'DESCRIPTION' => [
                'ADDRESS' => [
                    'PROTOCOL' => 'TCP',
                    'HOST' => $params['host'],
                    'PORT' => $params['port'] ?? 1521,
                ],
                'CONNECT_DATA' => $connectData,
            ],
        ]);
    }

    /**
     * @param mixed[] $params
     */
    private static function renderParams(array $params): string
    {
        $chunks = [];

        foreach ($params as $key => $value) {
            $string = self::renderValue($value);

            if ($string === '') {
                continue;
            }

            $chunks[] = sprintf('(%s=%s)', $key, $string);
        }

        return implode('', $chunks);
    }

    /**
     * @param mixed $value
     */
    private static function renderValue($value): string
    {
        if (is_array($value)) {
            return self::renderParams($value);
        }

        return (string) $value;
    }
}
<?php

namespace Doctrine\DBAL\Driver;

use Doctrine\DBAL\Driver\PDO\Exception;
use Doctrine\DBAL\Driver\Statement as StatementInterface;
use Doctrine\DBAL\FetchMode;
use Doctrine\DBAL\ParameterType;
use Doctrine\Deprecations\Deprecation;
use PDO;
use PDOException;
use ReturnTypeWillChange;

use function array_slice;
use function assert;
use function func_get_args;
use function is_array;

/**
 * The PDO implementation of the Statement interface.
 * Used by all PDO-based drivers.
 *
 * @deprecated Use {@link Statement} instead
 */
class PDOStatement extends \PDOStatement implements StatementInterface, Result
{
    use PDOStatementImplementations;

    private const PARAM_TYPE_MAP = [
        ParameterType::NULL         => PDO::PARAM_NULL,
        ParameterType::INTEGER      => PDO::PARAM_INT,
        ParameterType::STRING       => PDO::PARAM_STR,
        ParameterType::ASCII        => PDO::PARAM_STR,
        ParameterType::BINARY       => PDO::PARAM_LOB,
        ParameterType::LARGE_OBJECT => PDO::PARAM_LOB,
        ParameterType::BOOLEAN      => PDO::PARAM_BOOL,
    ];

    private const FETCH_MODE_MAP = [
        FetchMode::ASSOCIATIVE     => PDO::FETCH_ASSOC,
        FetchMode::NUMERIC         => PDO::FETCH_NUM,
        FetchMode::MIXED           => PDO::FETCH_BOTH,
        FetchMode::STANDARD_OBJECT => PDO::FETCH_OBJ,
        FetchMode::COLUMN          => PDO::FETCH_COLUMN,
        FetchMode::CUSTOM_OBJECT   => PDO::FETCH_CLASS,
    ];

    /**
     * Protected constructor.
     *
     * @internal The statement can be only instantiated by its driver connection.
     */
    protected function __construct()
    {
    }

    /**
     * {@inheritdoc}
     */
    #[ReturnTypeWillChange]
    public function bindValue($param, $value, $type = ParameterType::STRING)
    {
        $type = $this->convertParamType($type);

        try {
            return parent::bindValue($param, $value, $type);
        } catch (PDOException $exception) {
            throw Exception::new($exception);
        }
    }

    /**
     * @param mixed    $param
     * @param mixed    $variable
     * @param int      $type
     * @param int|null $length
     * @param mixed    $driverOptions
     *
     * @return bool
     */
    #[ReturnTypeWillChange]
    public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null, $driverOptions = null)
    {
        $type = $this->convertParamType($type);

        try {
            return parent::bindParam($param, $variable, $type, ...array_slice(func_get_args(), 3));
        } catch (PDOException $exception) {
            throw Exception::new($exception);
        }
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use free() instead.
     */
    #[ReturnTypeWillChange]
    public function closeCursor()
    {
        try {
            return parent::closeCursor();
        } catch (PDOException $exception) {
            // Exceptions not allowed by the interface.
            // In case driver implementations do not adhere to the interface, silence exceptions here.
            return true;
        }
    }

    /**
     * {@inheritdoc}
     */
    #[ReturnTypeWillChange]
    public function execute($params = null)
    {
        try {
            return parent::execute($params);
        } catch (PDOException $exception) {
            throw Exception::new($exception);
        }
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use fetchNumeric(), fetchAssociative() or fetchOne() instead.
     */
    #[ReturnTypeWillChange]
    public function fetch($fetchMode = null, $cursorOrientation = PDO::FETCH_ORI_NEXT, $cursorOffset = 0)
    {
        $args = func_get_args();

        if (isset($args[0])) {
            $args[0] = $this->convertFetchMode($args[0]);
        }

        try {
            return parent::fetch(...$args);
        } catch (PDOException $exception) {
            throw Exception::new($exception);
        }
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use fetchOne() instead.
     */
    #[ReturnTypeWillChange]
    public function fetchColumn($columnIndex = 0)
    {
        try {
            return parent::fetchColumn($columnIndex);
        } catch (PDOException $exception) {
            throw Exception::new($exception);
        }
    }

    /**
     * {@inheritdoc}
     */
    public function fetchNumeric()
    {
        return $this->fetch(PDO::FETCH_NUM);
    }

    /**
     * {@inheritdoc}
     */
    public function fetchAssociative()
    {
        return $this->fetch(PDO::FETCH_ASSOC);
    }

    /**
     * {@inheritdoc}
     */
    public function fetchOne()
    {
        return $this->fetch(PDO::FETCH_COLUMN);
    }

    /**
     * {@inheritdoc}
     */
    public function fetchAllNumeric(): array
    {
        return $this->fetchAll(PDO::FETCH_NUM);
    }

    /**
     * {@inheritdoc}
     */
    public function fetchAllAssociative(): array
    {
        return $this->fetchAll(PDO::FETCH_ASSOC);
    }

    /**
     * {@inheritdoc}
     */
    public function fetchFirstColumn(): array
    {
        return $this->fetchAll(PDO::FETCH_COLUMN);
    }

    public function free(): void
    {
        parent::closeCursor();
    }

    /**
     * @param mixed ...$args
     */
    private function doSetFetchMode(int $fetchMode, ...$args): bool
    {
        $fetchMode = $this->convertFetchMode($fetchMode);

        // This thin wrapper is necessary to shield against the weird signature
        // of PDOStatement::setFetchMode(): even if the second and third
        // parameters are optional, PHP will not let us remove it from this
        // declaration.
        $slice = [];

        foreach ($args as $arg) {
            if ($arg === null) {
                break;
            }

            $slice[] = $arg;
        }

        try {
            return parent::setFetchMode($fetchMode, ...$slice);
        } catch (PDOException $exception) {
            throw Exception::new($exception);
        }
    }

    /**
     * @param mixed ...$args
     *
     * @return mixed[]
     */
    private function doFetchAll(...$args): array
    {
        if (isset($args[0])) {
            $args[0] = $this->convertFetchMode($args[0]);
        }

        $slice = [];

        foreach ($args as $arg) {
            if ($arg === null) {
                break;
            }

            $slice[] = $arg;
        }

        try {
            $data = parent::fetchAll(...$slice);
        } catch (PDOException $exception) {
            throw Exception::new($exception);
        }

        assert(is_array($data));

        return $data;
    }

    /**
     * Converts DBAL parameter type to PDO parameter type
     *
     * @param int $type Parameter type
     */
    private function convertParamType(int $type): int
    {
        if (! isset(self::PARAM_TYPE_MAP[$type])) {
            // TODO: next major: throw an exception
            Deprecation::trigger(
                'doctrine/dbal',
                'https://github.com/doctrine/dbal/pull/3088',
                'Using a PDO parameter type (%d given) is deprecated, ' .
                'use \Doctrine\DBAL\Types\Types constants instead.',
                $type
            );

            return $type;
        }

        return self::PARAM_TYPE_MAP[$type];
    }

    /**
     * Converts DBAL fetch mode to PDO fetch mode
     *
     * @param int $fetchMode Fetch mode
     */
    private function convertFetchMode(int $fetchMode): int
    {
        if (! isset(self::FETCH_MODE_MAP[$fetchMode])) {
            Deprecation::trigger(
                'doctrine/dbal',
                'https://github.com/doctrine/dbal/pull/3088',
                'Using an unsupported PDO fetch mode or a bitmask of fetch modes (%d given)' .
                ' is deprecated and will cause an error in Doctrine DBAL 3.0',
                $fetchMode
            );

            return $fetchMode;
        }

        return self::FETCH_MODE_MAP[$fetchMode];
    }
}
<?php

namespace Doctrine\DBAL\Driver;

use ReturnTypeWillChange;

use function func_get_args;

use const PHP_VERSION_ID;

if (PHP_VERSION_ID >= 80000) {
    /**
     * @internal
     */
    trait PDOStatementImplementations
    {
        /**
         * @deprecated Use one of the fetch- or iterate-related methods.
         *
         * @param int   $mode
         * @param mixed ...$args
         *
         * @return bool
         */
        #[ReturnTypeWillChange]
        public function setFetchMode($mode, ...$args)
        {
            return $this->doSetFetchMode($mode, ...$args);
        }

        /**
         * @deprecated Use fetchAllNumeric(), fetchAllAssociative() or fetchFirstColumn() instead.
         *
         * @param int|null $mode
         * @param mixed    ...$args
         *
         * @return mixed[]
         */
        #[ReturnTypeWillChange]
        public function fetchAll($mode = null, ...$args)
        {
            return $this->doFetchAll($mode, ...$args);
        }
    }
} else {
    /**
     * @internal
     */
    trait PDOStatementImplementations
    {
        /**
         * @deprecated Use one of the fetch- or iterate-related methods.
         *
         * @param int   $fetchMode
         * @param mixed $arg2
         * @param mixed $arg3
         */
        public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null): bool
        {
            return $this->doSetFetchMode(...func_get_args());
        }

        /**
         * @deprecated Use fetchAllNumeric(), fetchAllAssociative() or fetchFirstColumn() instead.
         *
         * @param int|null $fetchMode
         * @param mixed    $fetchArgument
         * @param mixed    $ctorArgs
         *
         * @return mixed[]
         */
        public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null)
        {
            return $this->doFetchAll(...func_get_args());
        }
    }
}
<?php

namespace Doctrine\DBAL\Driver\PDOMySql;

use Doctrine\DBAL\Driver\AbstractMySQLDriver;
use Doctrine\DBAL\Driver\PDO;
use Doctrine\DBAL\Exception;
use Doctrine\Deprecations\Deprecation;
use PDOException;

/**
 * PDO MySql driver.
 *
 * @deprecated Use {@link PDO\MySQL\Driver} instead.
 */
class Driver extends AbstractMySQLDriver
{
    /**
     * {@inheritdoc}
     */
    public function connect(array $params, $username = null, $password = null, array $driverOptions = [])
    {
        try {
            $conn = new PDO\Connection(
                $this->constructPdoDsn($params),
                $username,
                $password,
                $driverOptions
            );
        } catch (PDOException $e) {
            throw Exception::driverException($this, $e);
        }

        return $conn;
    }

    /**
     * Constructs the MySql PDO DSN.
     *
     * @param mixed[] $params
     *
     * @return string The DSN.
     */
    protected function constructPdoDsn(array $params)
    {
        $dsn = 'mysql:';
        if (isset($params['host']) && $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'] . ';';
        }

        if (isset($params['charset'])) {
            $dsn .= 'charset=' . $params['charset'] . ';';
        }

        return $dsn;
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated
     */
    public function getName()
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/issues/3580',
            'Driver::getName() is deprecated'
        );

        return 'pdo_mysql';
    }
}
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Driver\OCI8\Exception;

use Doctrine\DBAL\Driver\OCI8\OCI8Exception;

/**
 * @internal
 *
 * @psalm-immutable
 */
final class SequenceDoesNotExist extends OCI8Exception
{
    public static function new(): self
    {
        return new self('lastInsertId failed: Query was executed but no result was returned.');
    }
}
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Driver\OCI8\Exception;

use Doctrine\DBAL\Driver\OCI8\OCI8Exception;

use function sprintf;

/**
 * @internal
 *
 * @psalm-immutable
 */
final class NonTerminatedStringLiteral extends OCI8Exception
{
    public static function new(int $offset): self
    {
        return new self(
            sprintf(
                'The statement contains non-terminated string literal starting at offset %d.',
                $offset
            )
        );
    }
}
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Driver\OCI8\Exception;

use Doctrine\DBAL\Driver\OCI8\OCI8Exception;

use function sprintf;

/**
 * @internal
 *
 * @psalm-immutable
 */
final class UnknownParameterIndex extends OCI8Exception
{
    public static function new(int $index): self
    {
        return new self(
            sprintf('Could not find variable mapping with index %d, in the SQL statement', $index)
        );
    }
}
<?php

namespace Doctrine\DBAL\Driver\OCI8;

final class Statement extends OCI8Statement
{
}
<?php

namespace Doctrine\DBAL\Driver\OCI8;

use Doctrine\DBAL\Driver\AbstractDriverException;

/**
 * @deprecated Use {@link \Doctrine\DBAL\Driver\Exception} instead
 *
 * @psalm-immutable
 */
class OCI8Exception extends AbstractDriverException
{
    /**
     * @param mixed[]|false $error
     *
     * @return OCI8Exception
     */
    public static function fromErrorInfo($error)
    {
        if ($error === false) {
            return new self('Database error occurred but no error information was retrieved from the driver.');
        }

        return new self($error['message'], null, $error['code']);
    }
}
<?php

namespace Doctrine\DBAL\Driver\OCI8;

use Doctrine\DBAL\Driver\AbstractOracleDriver;
use Doctrine\DBAL\Exception;
use Doctrine\Deprecations\Deprecation;

use const OCI_NO_AUTO_COMMIT;

/**
 * A Doctrine DBAL driver for the Oracle OCI8 PHP extensions.
 */
class Driver extends AbstractOracleDriver
{
    /**
     * {@inheritdoc}
     */
    public function connect(array $params, $username = null, $password = null, array $driverOptions = [])
    {
        try {
            return new Connection(
                (string) $username,
                (string) $password,
                $this->_constructDsn($params),
                $params['charset'] ?? '',
                $params['sessionMode'] ?? OCI_NO_AUTO_COMMIT,
                $params['persistent'] ?? false
            );
        } catch (OCI8Exception $e) {
            throw Exception::driverException($this, $e);
        }
    }

    /**
     * Constructs the Oracle DSN.
     *
     * @param mixed[] $params
     *
     * @return string The DSN.
     */
    protected function _constructDsn(array $params)
    {
        return $this->getEasyConnectString($params);
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated
     */
    public function getName()
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/issues/3580',
            'Driver::getName() is deprecated'
        );

        return 'oci8';
    }
}
<?php

namespace Doctrine\DBAL\Driver\OCI8;

use Doctrine\DBAL\Driver\FetchUtils;
use Doctrine\DBAL\Driver\OCI8\Exception\NonTerminatedStringLiteral;
use Doctrine\DBAL\Driver\OCI8\Exception\UnknownParameterIndex;
use Doctrine\DBAL\Driver\Result;
use Doctrine\DBAL\Driver\Statement as StatementInterface;
use Doctrine\DBAL\Driver\StatementIterator;
use Doctrine\DBAL\FetchMode;
use Doctrine\DBAL\ParameterType;
use InvalidArgumentException;
use IteratorAggregate;
use PDO;
use ReturnTypeWillChange;

use function array_key_exists;
use function assert;
use function count;
use function implode;
use function is_int;
use function is_resource;
use function oci_bind_by_name;
use function oci_cancel;
use function oci_error;
use function oci_execute;
use function oci_fetch_all;
use function oci_fetch_array;
use function oci_fetch_object;
use function oci_new_descriptor;
use function oci_num_fields;
use function oci_num_rows;
use function oci_parse;
use function preg_match;
use function preg_quote;
use function substr;

use const OCI_ASSOC;
use const OCI_B_BIN;
use const OCI_B_BLOB;
use const OCI_BOTH;
use const OCI_D_LOB;
use const OCI_FETCHSTATEMENT_BY_COLUMN;
use const OCI_FETCHSTATEMENT_BY_ROW;
use const OCI_NUM;
use const OCI_RETURN_LOBS;
use const OCI_RETURN_NULLS;
use const OCI_TEMP_BLOB;
use const PREG_OFFSET_CAPTURE;
use const SQLT_CHR;

/**
 * The OCI8 implementation of the Statement interface.
 *
 * @deprecated Use {@link Statement} instead
 */
class OCI8Statement implements IteratorAggregate, StatementInterface, Result
{
    /** @var resource */
    protected $_dbh;

    /** @var resource */
    protected $_sth;

    /** @var OCI8Connection */
    protected $_conn;

    /**
     * @deprecated
     *
     * @var string
     */
    protected static $_PARAM = ':param';

    /** @var int[] */
    protected static $fetchModeMap = [
        FetchMode::MIXED       => OCI_BOTH,
        FetchMode::ASSOCIATIVE => OCI_ASSOC,
        FetchMode::NUMERIC     => OCI_NUM,
        FetchMode::COLUMN      => OCI_NUM,
    ];

    /** @var int */
    protected $_defaultFetchMode = FetchMode::MIXED;

    /** @var string[] */
    protected $_paramMap = [];

    /**
     * Holds references to bound parameter values.
     *
     * This is a new requirement for PHP7's oci8 extension that prevents bound values from being garbage collected.
     *
     * @var mixed[]
     */
    private $boundValues = [];

    /**
     * Indicates whether the statement is in the state when fetching results is possible
     *
     * @var bool
     */
    private $result = false;

    /**
     * Creates a new OCI8Statement that uses the given connection handle and SQL statement.
     *
     * @internal The statement can be only instantiated by its driver connection.
     *
     * @param resource $dbh   The connection handle.
     * @param string   $query The SQL query.
     */
    public function __construct($dbh, $query, OCI8Connection $conn)
    {
        [$query, $paramMap] = self::convertPositionalToNamedPlaceholders($query);

        $stmt = oci_parse($dbh, $query);
        assert(is_resource($stmt));

        $this->_sth      = $stmt;
        $this->_dbh      = $dbh;
        $this->_paramMap = $paramMap;
        $this->_conn     = $conn;
    }

    /**
     * Converts positional (?) into named placeholders (:param<num>).
     *
     * 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.
     *
     * @internal
     *
     * @param string $statement The SQL statement to convert.
     *
     * @return mixed[] [0] => the statement value (string), [1] => the paramMap value (array).
     *
     * @throws OCI8Exception
     *
     * @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.
     */
    public static function convertPositionalToNamedPlaceholders($statement)
    {
        $fragmentOffset          = $tokenOffset = 0;
        $fragments               = $paramMap = [];
        $currentLiteralDelimiter = null;

        do {
            if (! $currentLiteralDelimiter) {
                $result = self::findPlaceholderOrOpeningQuote(
                    $statement,
                    $tokenOffset,
                    $fragmentOffset,
                    $fragments,
                    $currentLiteralDelimiter,
                    $paramMap
                );
            } else {
                $result = self::findClosingQuote($statement, $tokenOffset, $currentLiteralDelimiter);
            }
        } while ($result);

        if ($currentLiteralDelimiter !== null) {
            throw NonTerminatedStringLiteral::new($tokenOffset - 1);
        }

        $fragments[] = substr($statement, $fragmentOffset);
        $statement   = implode('', $fragments);

        return [$statement, $paramMap];
    }

    /**
     * Finds next placeholder or opening quote.
     *
     * @param string             $statement               The SQL statement to parse
     * @param int                $tokenOffset             The offset to start searching from
     * @param int                $fragmentOffset          The offset to build the next fragment from
     * @param string[]           $fragments               Fragments of the original statement
     *                                                    not containing placeholders
     * @param string|null        $currentLiteralDelimiter The delimiter of the current string literal
     *                                                    or NULL if not currently in a literal
     * @param array<int, string> $paramMap                Mapping of the original parameter positions
     *                                                    to their named replacements
     *
     * @return bool Whether the token was found
     */
    private static function findPlaceholderOrOpeningQuote(
        $statement,
        &$tokenOffset,
        &$fragmentOffset,
        &$fragments,
        &$currentLiteralDelimiter,
        &$paramMap
    ) {
        $token = self::findToken($statement, $tokenOffset, '/[?\'"]/');

        if (! $token) {
            return false;
        }

        if ($token === '?') {
            $position            = count($paramMap) + 1;
            $param               = ':param' . $position;
            $fragments[]         = substr($statement, $fragmentOffset, $tokenOffset - $fragmentOffset);
            $fragments[]         = $param;
            $paramMap[$position] = $param;
            $tokenOffset        += 1;
            $fragmentOffset      = $tokenOffset;

            return true;
        }

        $currentLiteralDelimiter = $token;
        ++$tokenOffset;

        return true;
    }

    /**
     * Finds closing quote
     *
     * @param string $statement               The SQL statement to parse
     * @param int    $tokenOffset             The offset to start searching from
     * @param string $currentLiteralDelimiter The delimiter of the current string literal
     *
     * @return bool Whether the token was found
     *
     * @param-out string|null $currentLiteralDelimiter
     */
    private static function findClosingQuote(
        $statement,
        &$tokenOffset,
        &$currentLiteralDelimiter
    ) {
        $token = self::findToken(
            $statement,
            $tokenOffset,
            '/' . preg_quote($currentLiteralDelimiter, '/') . '/'
        );

        if (! $token) {
            return false;
        }

        $currentLiteralDelimiter = null;
        ++$tokenOffset;

        return true;
    }

    /**
     * Finds the token described by regex starting from the given offset. Updates the offset with the position
     * where the token was found.
     *
     * @param string $statement The SQL statement to parse
     * @param int    $offset    The offset to start searching from
     * @param string $regex     The regex containing token pattern
     *
     * @return string|null Token or NULL if not found
     */
    private static function findToken($statement, &$offset, $regex)
    {
        if (preg_match($regex, $statement, $matches, PREG_OFFSET_CAPTURE, $offset)) {
            $offset = $matches[0][1];

            return $matches[0][0];
        }

        return null;
    }

    /**
     * {@inheritdoc}
     */
    public function bindValue($param, $value, $type = ParameterType::STRING)
    {
        return $this->bindParam($param, $value, $type, null);
    }

    /**
     * {@inheritdoc}
     */
    public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null)
    {
        if (is_int($param)) {
            if (! isset($this->_paramMap[$param])) {
                throw UnknownParameterIndex::new($param);
            }

            $param = $this->_paramMap[$param];
        }

        if ($type === ParameterType::LARGE_OBJECT) {
            $lob = oci_new_descriptor($this->_dbh, OCI_D_LOB);
            $lob->writeTemporary($variable, OCI_TEMP_BLOB);

            $variable =& $lob;
        }

        $this->boundValues[$param] =& $variable;

        return oci_bind_by_name(
            $this->_sth,
            $param,
            $variable,
            $length ?? -1,
            $this->convertParameterType($type)
        );
    }

    /**
     * Converts DBAL parameter type to oci8 parameter type
     */
    private function convertParameterType(int $type): int
    {
        switch ($type) {
            case ParameterType::BINARY:
                return OCI_B_BIN;

            case ParameterType::LARGE_OBJECT:
                return OCI_B_BLOB;

            default:
                return SQLT_CHR;
        }
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use free() instead.
     */
    public function closeCursor()
    {
        $this->free();

        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function columnCount()
    {
        return oci_num_fields($this->_sth) ?: 0;
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated The error information is available via exceptions.
     */
    public function errorCode()
    {
        $error = oci_error($this->_sth);
        if ($error !== false) {
            $error = $error['code'];
        }

        return $error;
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated The error information is available via exceptions.
     */
    public function errorInfo()
    {
        $error = oci_error($this->_sth);

        if ($error === false) {
            return [];
        }

        return $error;
    }

    /**
     * {@inheritdoc}
     */
    public function execute($params = null)
    {
        if ($params) {
            $hasZeroIndex = array_key_exists(0, $params);

            foreach ($params as $key => $val) {
                if ($hasZeroIndex && is_int($key)) {
                    $this->bindValue($key + 1, $val);
                } else {
                    $this->bindValue($key, $val);
                }
            }
        }

        $ret = @oci_execute($this->_sth, $this->_conn->getExecuteMode());
        if (! $ret) {
            throw OCI8Exception::fromErrorInfo($this->errorInfo());
        }

        $this->result = true;

        return $ret;
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use one of the fetch- or iterate-related methods.
     */
    public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null)
    {
        $this->_defaultFetchMode = $fetchMode;

        return true;
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use iterateNumeric(), iterateAssociative() or iterateColumn() instead.
     */
    #[ReturnTypeWillChange]
    public function getIterator()
    {
        return new StatementIterator($this);
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use fetchNumeric(), fetchAssociative() or fetchOne() instead.
     */
    public function fetch($fetchMode = null, $cursorOrientation = PDO::FETCH_ORI_NEXT, $cursorOffset = 0)
    {
        // do not try fetching from the statement if it's not expected to contain result
        // in order to prevent exceptional situation
        if (! $this->result) {
            return false;
        }

        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;

        if ($fetchMode === FetchMode::COLUMN) {
            return $this->fetchColumn();
        }

        if ($fetchMode === FetchMode::STANDARD_OBJECT) {
            return oci_fetch_object($this->_sth);
        }

        if (! isset(self::$fetchModeMap[$fetchMode])) {
            throw new InvalidArgumentException('Invalid fetch style: ' . $fetchMode);
        }

        return oci_fetch_array(
            $this->_sth,
            self::$fetchModeMap[$fetchMode] | OCI_RETURN_NULLS | OCI_RETURN_LOBS
        );
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use fetchAllNumeric(), fetchAllAssociative() or fetchFirstColumn() instead.
     */
    public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null)
    {
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;

        $result = [];

        if ($fetchMode === FetchMode::STANDARD_OBJECT) {
            while ($row = $this->fetch($fetchMode)) {
                $result[] = $row;
            }

            return $result;
        }

        if (! isset(self::$fetchModeMap[$fetchMode])) {
            throw new InvalidArgumentException('Invalid fetch style: ' . $fetchMode);
        }

        if (self::$fetchModeMap[$fetchMode] === OCI_BOTH) {
            while ($row = $this->fetch($fetchMode)) {
                $result[] = $row;
            }
        } else {
            $fetchStructure = OCI_FETCHSTATEMENT_BY_ROW;

            if ($fetchMode === FetchMode::COLUMN) {
                $fetchStructure = OCI_FETCHSTATEMENT_BY_COLUMN;
            }

            // do not try fetching from the statement if it's not expected to contain result
            // in order to prevent exceptional situation
            if (! $this->result) {
                return [];
            }

            oci_fetch_all(
                $this->_sth,
                $result,
                0,
                -1,
                self::$fetchModeMap[$fetchMode] | OCI_RETURN_NULLS | $fetchStructure | OCI_RETURN_LOBS
            );

            if ($fetchMode === FetchMode::COLUMN) {
                $result = $result[0];
            }
        }

        return $result;
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use fetchOne() instead.
     */
    public function fetchColumn($columnIndex = 0)
    {
        // do not try fetching from the statement if it's not expected to contain result
        // in order to prevent exceptional situation
        if (! $this->result) {
            return false;
        }

        $row = oci_fetch_array($this->_sth, OCI_NUM | OCI_RETURN_NULLS | OCI_RETURN_LOBS);

        if ($row === false) {
            return false;
        }

        return $row[$columnIndex] ?? null;
    }

    /**
     * {@inheritdoc}
     */
    public function rowCount()
    {
        return oci_num_rows($this->_sth) ?: 0;
    }

    /**
     * {@inheritdoc}
     */
    public function fetchNumeric()
    {
        return $this->doFetch(OCI_NUM);
    }

    /**
     * {@inheritdoc}
     */
    public function fetchAssociative()
    {
        return $this->doFetch(OCI_ASSOC);
    }

    /**
     * {@inheritdoc}
     */
    public function fetchOne()
    {
        return FetchUtils::fetchOne($this);
    }

    /**
     * {@inheritdoc}
     */
    public function fetchAllNumeric(): array
    {
        return $this->doFetchAll(OCI_NUM, OCI_FETCHSTATEMENT_BY_ROW);
    }

    /**
     * {@inheritdoc}
     */
    public function fetchAllAssociative(): array
    {
        return $this->doFetchAll(OCI_ASSOC, OCI_FETCHSTATEMENT_BY_ROW);
    }

    /**
     * {@inheritdoc}
     */
    public function fetchFirstColumn(): array
    {
        return $this->doFetchAll(OCI_NUM, OCI_FETCHSTATEMENT_BY_COLUMN)[0];
    }

    public function free(): void
    {
        // not having the result means there's nothing to close
        if (! $this->result) {
            return;
        }

        oci_cancel($this->_sth);

        $this->result = false;
    }

    /**
     * @return mixed|false
     */
    private function doFetch(int $mode)
    {
        // do not try fetching from the statement if it's not expected to contain the result
        // in order to prevent exceptional situation
        if (! $this->result) {
            return false;
        }

        return oci_fetch_array(
            $this->_sth,
            $mode | OCI_RETURN_NULLS | OCI_RETURN_LOBS
        );
    }

    /**
     * @return array<mixed>
     */
    private function doFetchAll(int $mode, int $fetchStructure): array
    {
        // do not try fetching from the statement if it's not expected to contain the result
        // in order to prevent exceptional situation
        if (! $this->result) {
            return [];
        }

        oci_fetch_all(
            $this->_sth,
            $result,
            0,
            -1,
            $mode | OCI_RETURN_NULLS | $fetchStructure | OCI_RETURN_LOBS
        );

        return $result;
    }
}
<?php

namespace Doctrine\DBAL\Driver\OCI8;

use Doctrine\DBAL\Driver\Connection as ConnectionInterface;
use Doctrine\DBAL\Driver\OCI8\Exception\SequenceDoesNotExist;
use Doctrine\DBAL\Driver\ServerInfoAwareConnection;
use Doctrine\DBAL\ParameterType;
use Doctrine\Deprecations\Deprecation;
use UnexpectedValueException;

use function addcslashes;
use function func_get_args;
use function is_float;
use function is_int;
use function oci_commit;
use function oci_connect;
use function oci_error;
use function oci_pconnect;
use function oci_rollback;
use function oci_server_version;
use function preg_match;
use function sprintf;
use function str_replace;

use const OCI_COMMIT_ON_SUCCESS;
use const OCI_NO_AUTO_COMMIT;

/**
 * OCI8 implementation of the Connection interface.
 *
 * @deprecated Use {@link Connection} instead
 */
class OCI8Connection implements ConnectionInterface, ServerInfoAwareConnection
{
    /** @var resource */
    protected $dbh;

    /** @var int */
    protected $executeMode = OCI_COMMIT_ON_SUCCESS;

    /**
     * Creates a Connection to an Oracle Database using oci8 extension.
     *
     * @internal The connection can be only instantiated by its driver.
     *
     * @param string $username
     * @param string $password
     * @param string $db
     * @param string $charset
     * @param int    $sessionMode
     * @param bool   $persistent
     *
     * @throws OCI8Exception
     */
    public function __construct(
        $username,
        $password,
        $db,
        $charset = '',
        $sessionMode = OCI_NO_AUTO_COMMIT,
        $persistent = false
    ) {
        $dbh = $persistent
            ? @oci_pconnect($username, $password, $db, $charset, $sessionMode)
            : @oci_connect($username, $password, $db, $charset, $sessionMode);

        if ($dbh === false) {
            throw OCI8Exception::fromErrorInfo(oci_error());
        }

        $this->dbh = $dbh;
    }

    /**
     * {@inheritdoc}
     *
     * @throws UnexpectedValueException If the version string returned by the database server
     *                                  does not contain a parsable version number.
     */
    public function getServerVersion()
    {
        $version = oci_server_version($this->dbh);

        if ($version === false) {
            throw OCI8Exception::fromErrorInfo(oci_error($this->dbh));
        }

        if (! preg_match('/\s+(\d+\.\d+\.\d+\.\d+\.\d+)\s+/', $version, $matches)) {
            throw new UnexpectedValueException(
                sprintf(
                    'Unexpected database version string "%s". Cannot parse an appropriate version number from it. ' .
                    'Please report this database version string to the Doctrine team.',
                    $version
                )
            );
        }

        return $matches[1];
    }

    /**
     * {@inheritdoc}
     */
    public function requiresQueryForServerVersion()
    {
        Deprecation::triggerIfCalledFromOutside(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/4114',
            'ServerInfoAwareConnection::requiresQueryForServerVersion() is deprecated and removed in DBAL 3.'
        );

        return false;
    }

    /**
     * {@inheritdoc}
     */
    public function prepare($sql)
    {
        return new Statement($this->dbh, $sql, $this);
    }

    /**
     * {@inheritdoc}
     */
    public function query()
    {
        $args = func_get_args();
        $sql  = $args[0];
        //$fetchMode = $args[1];
        $stmt = $this->prepare($sql);
        $stmt->execute();

        return $stmt;
    }

    /**
     * {@inheritdoc}
     */
    public function quote($value, $type = ParameterType::STRING)
    {
        if (is_int($value) || is_float($value)) {
            return $value;
        }

        $value = str_replace("'", "''", $value);

        return "'" . addcslashes($value, "\000\n\r\\\032") . "'";
    }

    /**
     * {@inheritdoc}
     */
    public function exec($sql)
    {
        $stmt = $this->prepare($sql);
        $stmt->execute();

        return $stmt->rowCount();
    }

    /**
     * {@inheritdoc}
     *
     * @param string|null $name
     *
     * @return int|false
     */
    public function lastInsertId($name = null)
    {
        if ($name === null) {
            return false;
        }

        $sql    = 'SELECT ' . $name . '.CURRVAL FROM DUAL';
        $stmt   = $this->query($sql);
        $result = $stmt->fetchColumn();

        if ($result === false) {
            throw SequenceDoesNotExist::new();
        }

        return (int) $result;
    }

    /**
     * Returns the current execution mode.
     *
     * @internal
     *
     * @return int
     */
    public function getExecuteMode()
    {
        return $this->executeMode;
    }

    /**
     * {@inheritdoc}
     */
    public function beginTransaction()
    {
        $this->executeMode = OCI_NO_AUTO_COMMIT;

        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function commit()
    {
        if (! oci_commit($this->dbh)) {
            throw OCI8Exception::fromErrorInfo($this->errorInfo());
        }

        $this->executeMode = OCI_COMMIT_ON_SUCCESS;

        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function rollBack()
    {
        if (! oci_rollback($this->dbh)) {
            throw OCI8Exception::fromErrorInfo($this->errorInfo());
        }

        $this->executeMode = OCI_COMMIT_ON_SUCCESS;

        return true;
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated The error information is available via exceptions.
     */
    public function errorCode()
    {
        $error = oci_error($this->dbh);

        if ($error !== false) {
            return $error['code'];
        }

        return null;
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated The error information is available via exceptions.
     */
    public function errorInfo()
    {
        $error = oci_error($this->dbh);

        if ($error === false) {
            return [];
        }

        return $error;
    }
}
<?php

namespace Doctrine\DBAL\Driver\OCI8;

final class Connection extends OCI8Connection
{
}
<?php

namespace Doctrine\DBAL\Driver;

use Doctrine\DBAL\Driver\PDO\Exception;
use Doctrine\Deprecations\Deprecation;

/**
 * @deprecated Use {@link Exception} instead
 *
 * @psalm-immutable
 */
class PDOException extends \PDOException implements DriverException
{
    /**
     * The driver specific error code.
     *
     * @var int|string|null
     */
    private $errorCode;

    /**
     * The SQLSTATE of the driver.
     *
     * @var string|null
     */
    private $sqlState;

    /**
     * @param \PDOException $exception The PDO exception to wrap.
     */
    public function __construct(\PDOException $exception)
    {
        parent::__construct($exception->getMessage(), 0, $exception);

        $this->code      = $exception->getCode();
        $this->errorInfo = $exception->errorInfo;
        $this->errorCode = $exception->errorInfo[1] ?? $exception->getCode();
        $this->sqlState  = $exception->errorInfo[0] ?? $exception->getCode();
    }

    /**
     * {@inheritdoc}
     */
    public function getErrorCode()
    {
        /** @psalm-suppress ImpureMethodCall */
        Deprecation::triggerIfCalledFromOutside(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/4112',
            'Driver\AbstractException::getErrorCode() is deprecated, use getSQLState() or getCode() instead.'
        );

        return $this->errorCode;
    }

    /**
     * {@inheritdoc}
     */
    public function getSQLState()
    {
        return $this->sqlState;
    }
}
<?php

namespace Doctrine\DBAL;

use PDO;

/**
 * Contains statement parameter types.
 */
final class ParameterType
{
    /**
     * Represents the SQL NULL data type.
     *
     * @see \PDO::PARAM_NULL
     */
    public const NULL = PDO::PARAM_NULL;

    /**
     * Represents the SQL INTEGER data type.
     *
     * @see \PDO::PARAM_INT
     */
    public const INTEGER = PDO::PARAM_INT;

    /**
     * Represents the SQL CHAR, VARCHAR, or other string data type.
     *
     * @see \PDO::PARAM_STR
     */
    public const STRING = PDO::PARAM_STR;

    /**
     * Represents the SQL large object data type.
     *
     * @see \PDO::PARAM_LOB
     */
    public const LARGE_OBJECT = PDO::PARAM_LOB;

    /**
     * Represents a boolean data type.
     *
     * @see \PDO::PARAM_BOOL
     */
    public const BOOLEAN = PDO::PARAM_BOOL;

    /**
     * Represents a binary string data type.
     */
    public const BINARY = 16;

    /**
     * Represents an ASCII string data type
     */
    public const ASCII = 17;

    /**
     * This class cannot be instantiated.
     *
     * @codeCoverageIgnore
     */
    private function __construct()
    {
    }
}
<?php

namespace Doctrine\DBAL;

use PDO;

/**
 * Contains portable column case conversions.
 */
final class ColumnCase
{
    /**
     * Convert column names to upper case.
     *
     * @see \PDO::CASE_UPPER
     */
    public const UPPER = PDO::CASE_UPPER;

    /**
     * Convert column names to lower case.
     *
     * @see \PDO::CASE_LOWER
     */
    public const LOWER = PDO::CASE_LOWER;

    /**
     * This class cannot be instantiated.
     *
     * @codeCoverageIgnore
     */
    private function __construct()
    {
    }
}
<?php

namespace Doctrine\DBAL\Query\Expression;

use Doctrine\DBAL\Connection;
use Doctrine\Deprecations\Deprecation;

use function func_get_arg;
use function func_get_args;
use function func_num_args;
use function implode;
use function sprintf;

/**
 * ExpressionBuilder class is responsible to dynamically create SQL query parts.
 */
class ExpressionBuilder
{
    public const EQ  = '=';
    public const NEQ = '<>';
    public const LT  = '<';
    public const LTE = '<=';
    public const GT  = '>';
    public const GTE = '>=';

    /**
     * The DBAL Connection.
     *
     * @var Connection
     */
    private $connection;

    /**
     * Initializes a new <tt>ExpressionBuilder</tt>.
     *
     * @param Connection $connection The DBAL Connection.
     */
    public function __construct(Connection $connection)
    {
        $this->connection = $connection;
    }

    /**
     * Creates a conjunction of the given expressions.
     *
     * @param string|CompositeExpression $expression
     * @param string|CompositeExpression ...$expressions
     */
    public function and($expression, ...$expressions): CompositeExpression
    {
        return CompositeExpression::and($expression, ...$expressions);
    }

    /**
     * Creates a disjunction of the given expressions.
     *
     * @param string|CompositeExpression $expression
     * @param string|CompositeExpression ...$expressions
     */
    public function or($expression, ...$expressions): CompositeExpression
    {
        return CompositeExpression::or($expression, ...$expressions);
    }

    /**
     * @deprecated Use `and()` instead.
     *
     * @param mixed $x Optional clause. Defaults = null, but requires
     *                 at least one defined when converting to string.
     *
     * @return CompositeExpression
     */
    public function andX($x = null)
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/3851',
            'ExpressionBuilder::andX() is deprecated, use ExpressionBuilder::and() instead.'
        );

        return new CompositeExpression(CompositeExpression::TYPE_AND, func_get_args());
    }

    /**
     * @deprecated Use `or()` instead.
     *
     * @param mixed $x Optional clause. Defaults = null, but requires
     *                 at least one defined when converting to string.
     *
     * @return CompositeExpression
     */
    public function orX($x = null)
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/3851',
            'ExpressionBuilder::orX() is deprecated, use ExpressionBuilder::or() instead.'
        );

        return new CompositeExpression(CompositeExpression::TYPE_OR, func_get_args());
    }

    /**
     * Creates a comparison expression.
     *
     * @param mixed  $x        The left expression.
     * @param string $operator One of the ExpressionBuilder::* constants.
     * @param mixed  $y        The right expression.
     *
     * @return string
     */
    public function comparison($x, $operator, $y)
    {
        return $x . ' ' . $operator . ' ' . $y;
    }

    /**
     * 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 <left expr> = <right expr>. Example:
     *
     *     [php]
     *     // u.id = ?
     *     $expr->eq('u.id', '?');
     *
     * @param mixed $x The left expression.
     * @param mixed $y The right expression.
     *
     * @return string
     */
    public function eq($x, $y)
    {
        return $this->comparison($x, self::EQ, $y);
    }

    /**
     * Creates a non 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 <left expr> <> <right expr>. Example:
     *
     *     [php]
     *     // u.id <> 1
     *     $q->where($q->expr()->neq('u.id', '1'));
     *
     * @param mixed $x The left expression.
     * @param mixed $y The right expression.
     *
     * @return string
     */
    public function neq($x, $y)
    {
        return $this->comparison($x, self::NEQ, $y);
    }

    /**
     * Creates a lower-than 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 <left expr> < <right expr>. Example:
     *
     *     [php]
     *     // u.id < ?
     *     $q->where($q->expr()->lt('u.id', '?'));
     *
     * @param mixed $x The left expression.
     * @param mixed $y The right expression.
     *
     * @return string
     */
    public function lt($x, $y)
    {
        return $this->comparison($x, self::LT, $y);
    }

    /**
     * Creates a lower-than-equal 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 <left expr> <= <right expr>. Example:
     *
     *     [php]
     *     // u.id <= ?
     *     $q->where($q->expr()->lte('u.id', '?'));
     *
     * @param mixed $x The left expression.
     * @param mixed $y The right expression.
     *
     * @return string
     */
    public function lte($x, $y)
    {
        return $this->comparison($x, self::LTE, $y);
    }

    /**
     * Creates a greater-than 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 <left expr> > <right expr>. Example:
     *
     *     [php]
     *     // u.id > ?
     *     $q->where($q->expr()->gt('u.id', '?'));
     *
     * @param mixed $x The left expression.
     * @param mixed $y The right expression.
     *
     * @return string
     */
    public function gt($x, $y)
    {
        return $this->comparison($x, self::GT, $y);
    }

    /**
     * Creates a greater-than-equal 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 <left expr> >= <right expr>. Example:
     *
     *     [php]
     *     // u.id >= ?
     *     $q->where($q->expr()->gte('u.id', '?'));
     *
     * @param mixed $x The left expression.
     * @param mixed $y The right expression.
     *
     * @return string
     */
    public function gte($x, $y)
    {
        return $this->comparison($x, self::GTE, $y);
    }

    /**
     * Creates an IS NULL expression with the given arguments.
     *
     * @param string $x The expression to be restricted by IS NULL.
     *
     * @return string
     */
    public function isNull($x)
    {
        return $x . ' IS NULL';
    }

    /**
     * Creates an IS NOT NULL expression with the given arguments.
     *
     * @param string $x The expression to be restricted by IS NOT NULL.
     *
     * @return string
     */
    public function isNotNull($x)
    {
        return $x . ' IS NOT NULL';
    }

    /**
     * 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 string
     */
    public function like($x, $y/*, ?string $escapeChar = null */)
    {
        return $this->comparison($x, 'LIKE', $y) .
            (func_num_args() >= 3 ? sprintf(' ESCAPE %s', func_get_arg(2)) : '');
    }

    /**
     * Creates a NOT LIKE() comparison expression with the given arguments.
     *
     * @param string $x Field in string format to be inspected by NOT LIKE() comparison.
     * @param mixed  $y Argument to be used in NOT LIKE() comparison.
     *
     * @return string
     */
    public function notLike($x, $y/*, ?string $escapeChar = null */)
    {
        return $this->comparison($x, 'NOT LIKE', $y) .
            (func_num_args() >= 3 ? sprintf(' ESCAPE %s', func_get_arg(2)) : '');
    }

    /**
     * Creates a IN () comparison expression with the given arguments.
     *
     * @param string          $x The field in string format to be inspected by IN() comparison.
     * @param string|string[] $y The placeholder or the array of values to be used by IN() comparison.
     *
     * @return string
     */
    public function in($x, $y)
    {
        return $this->comparison($x, 'IN', '(' . implode(', ', (array) $y) . ')');
    }

    /**
     * Creates a NOT IN () comparison expression with the given arguments.
     *
     * @param string          $x The expression to be inspected by NOT IN() comparison.
     * @param string|string[] $y The placeholder or the array of values to be used by NOT IN() comparison.
     *
     * @return string
     */
    public function notIn($x, $y)
    {
        return $this->comparison($x, 'NOT IN', '(' . implode(', ', (array) $y) . ')');
    }

    /**
     * Quotes a given input parameter.
     *
     * @param mixed    $input The parameter to be quoted.
     * @param int|null $type  The type of the parameter.
     *
     * @return string
     */
    public function literal($input, $type = null)
    {
        return $this->connection->quote($input, $type);
    }
}
<?php

namespace Doctrine\DBAL\Query\Expression;

use Countable;
use Doctrine\Deprecations\Deprecation;
use ReturnTypeWillChange;

use function array_merge;
use function count;
use function implode;

/**
 * Composite expression is responsible to build a group of similar expression.
 */
class CompositeExpression implements Countable
{
    /**
     * Constant that represents an AND composite expression.
     */
    public const TYPE_AND = 'AND';

    /**
     * Constant that represents an OR composite expression.
     */
    public const TYPE_OR = 'OR';

    /**
     * The instance type of composite expression.
     *
     * @var string
     */
    private $type;

    /**
     * Each expression part of the composite expression.
     *
     * @var self[]|string[]
     */
    private $parts = [];

    /**
     * @internal Use the and() / or() factory methods.
     *
     * @param string          $type  Instance type of composite expression.
     * @param self[]|string[] $parts Composition of expressions to be joined on composite expression.
     */
    public function __construct($type, array $parts = [])
    {
        $this->type = $type;

        $this->addMultiple($parts);

        Deprecation::triggerIfCalledFromOutside(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/3864',
            'Do not use CompositeExpression constructor directly, use static and() and or() factory methods.'
        );
    }

    /**
     * @param self|string $part
     * @param self|string ...$parts
     */
    public static function and($part, ...$parts): self
    {
        return new self(self::TYPE_AND, array_merge([$part], $parts));
    }

    /**
     * @param self|string $part
     * @param self|string ...$parts
     */
    public static function or($part, ...$parts): self
    {
        return new self(self::TYPE_OR, array_merge([$part], $parts));
    }

    /**
     * Adds multiple parts to composite expression.
     *
     * @deprecated This class will be made immutable. Use with() instead.
     *
     * @param self[]|string[] $parts
     *
     * @return CompositeExpression
     */
    public function addMultiple(array $parts = [])
    {
        Deprecation::triggerIfCalledFromOutside(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/issues/3844',
            'CompositeExpression::addMultiple() is deprecated, use CompositeExpression::with() instead.'
        );

        foreach ($parts as $part) {
            $this->add($part);
        }

        return $this;
    }

    /**
     * Adds an expression to composite expression.
     *
     * @deprecated This class will be made immutable. Use with() instead.
     *
     * @param mixed $part
     *
     * @return CompositeExpression
     */
    public function add($part)
    {
        Deprecation::triggerIfCalledFromOutside(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/issues/3844',
            'CompositeExpression::add() is deprecated, use CompositeExpression::with() instead.'
        );

        if (empty($part)) {
            return $this;
        }

        if ($part instanceof self && count($part) === 0) {
            return $this;
        }

        $this->parts[] = $part;

        return $this;
    }

    /**
     * Returns a new CompositeExpression with the given parts added.
     *
     * @param self|string $part
     * @param self|string ...$parts
     */
    public function with($part, ...$parts): self
    {
        $that = clone $this;

        $that->parts[] = $part;

        foreach ($parts as $part) {
            $that->parts[] = $part;
        }

        return $that;
    }

    /**
     * Retrieves the amount of expressions on composite expression.
     *
     * @return int
     */
    #[ReturnTypeWillChange]
    public function count()
    {
        return count($this->parts);
    }

    /**
     * Retrieves the string representation of this composite expression.
     *
     * @return string
     */
    public function __toString()
    {
        if ($this->count() === 1) {
            return (string) $this->parts[0];
        }

        return '(' . implode(') ' . $this->type . ' (', $this->parts) . ')';
    }

    /**
     * Returns the type of this composite expression (AND/OR).
     *
     * @return string
     */
    public function getType()
    {
        return $this->type;
    }
}
<?php

namespace Doctrine\DBAL\Query;

use Doctrine\DBAL\Exception;

use function implode;

/**
 * @psalm-immutable
 */
class QueryException extends Exception
{
    /**
     * @param string   $alias
     * @param string[] $registeredAliases
     *
     * @return QueryException
     */
    public static function unknownAlias($alias, $registeredAliases)
    {
        return new self("The given alias '" . $alias . "' is not part of " .
            'any FROM or JOIN clause table. The currently registered ' .
            'aliases are: ' . implode(', ', $registeredAliases) . '.');
    }

    /**
     * @param string   $alias
     * @param string[] $registeredAliases
     *
     * @return QueryException
     */
    public static function nonUniqueAlias($alias, $registeredAliases)
    {
        return new self("The given alias '" . $alias . "' is not unique " .
            'in FROM and JOIN clause table. The currently registered ' .
            'aliases are: ' . implode(', ', $registeredAliases) . '.');
    }
}
<?php

namespace Doctrine\DBAL\Query;

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Exception;
use Doctrine\DBAL\ForwardCompatibility;
use Doctrine\DBAL\ParameterType;
use Doctrine\DBAL\Query\Expression\CompositeExpression;
use Doctrine\DBAL\Query\Expression\ExpressionBuilder;
use Doctrine\DBAL\Types\Type;
use Doctrine\Deprecations\Deprecation;

use function array_filter;
use function array_key_exists;
use function array_keys;
use function array_unshift;
use function count;
use function func_get_args;
use function func_num_args;
use function implode;
use function is_array;
use function is_object;
use function key;
use function strtoupper;
use function substr;

/**
 * QueryBuilder class is responsible to dynamically create SQL queries.
 *
 * Important: Verify that every feature you use will work with your database vendor.
 * SQL Query Builder does not attempt to validate the generated SQL at all.
 *
 * The query builder does no validation whatsoever if certain features even work with the
 * underlying database vendor. Limit queries and joins are NOT applied to UPDATE and DELETE statements
 * even if some vendors such as MySQL support it.
 */
class QueryBuilder
{
    /*
     * The query types.
     */
    public const SELECT = 0;
    public const DELETE = 1;
    public const UPDATE = 2;
    public const INSERT = 3;

    /*
     * The builder states.
     */
    public const STATE_DIRTY = 0;
    public const STATE_CLEAN = 1;

    /**
     * The DBAL Connection.
     *
     * @var Connection
     */
    private $connection;

    /*
     * The default values of SQL parts collection
     */
    private const SQL_PARTS_DEFAULTS = [
        'select'   => [],
        'distinct' => false,
        'from'     => [],
        'join'     => [],
        'set'      => [],
        'where'    => null,
        'groupBy'  => [],
        'having'   => null,
        'orderBy'  => [],
        'values'   => [],
    ];

    /**
     * The array of SQL parts collected.
     *
     * @var mixed[]
     */
    private $sqlParts = self::SQL_PARTS_DEFAULTS;

    /**
     * The complete SQL string for this query.
     *
     * @var string|null
     */
    private $sql;

    /**
     * The query parameters.
     *
     * @var array<int, mixed>|array<string, mixed>
     */
    private $params = [];

    /**
     * The parameter type map of this query.
     *
     * @var array<int, int|string|Type|null>|array<string, int|string|Type|null>
     */
    private $paramTypes = [];

    /**
     * The type of query this is. Can be select, update or delete.
     *
     * @var int
     */
    private $type = self::SELECT;

    /**
     * The state of the query object. Can be dirty or clean.
     *
     * @var int
     */
    private $state = self::STATE_CLEAN;

    /**
     * The index of the first result to retrieve.
     *
     * @var int
     */
    private $firstResult = 0;

    /**
     * The maximum number of results to retrieve or NULL to retrieve all results.
     *
     * @var int|null
     */
    private $maxResults;

    /**
     * The counter of bound parameters used with {@see bindValue).
     *
     * @var int
     */
    private $boundCounter = 0;

    /**
     * Initializes a new <tt>QueryBuilder</tt>.
     *
     * @param Connection $connection The DBAL Connection.
     */
    public function __construct(Connection $connection)
    {
        $this->connection = $connection;
    }

    /**
     * Gets an ExpressionBuilder used for object-oriented construction of query expressions.
     * This producer method is intended for convenient inline usage. Example:
     *
     * <code>
     *     $qb = $conn->createQueryBuilder()
     *         ->select('u')
     *         ->from('users', 'u')
     *         ->where($qb->expr()->eq('u.id', 1));
     * </code>
     *
     * For more complex expression construction, consider storing the expression
     * builder object in a local variable.
     *
     * @return ExpressionBuilder
     */
    public function expr()
    {
        return $this->connection->getExpressionBuilder();
    }

    /**
     * Gets the type of the currently built query.
     *
     * @return int
     */
    public function getType()
    {
        return $this->type;
    }

    /**
     * Gets the associated DBAL Connection for this query builder.
     *
     * @return Connection
     */
    public function getConnection()
    {
        return $this->connection;
    }

    /**
     * Gets the state of this query builder instance.
     *
     * @return int Either QueryBuilder::STATE_DIRTY or QueryBuilder::STATE_CLEAN.
     */
    public function getState()
    {
        return $this->state;
    }

    /**
     * Executes this query using the bound parameters and their types.
     *
     * @return ForwardCompatibility\Result|int|string
     *
     * @throws Exception
     */
    public function execute()
    {
        if ($this->type === self::SELECT) {
            return ForwardCompatibility\Result::ensure(
                $this->connection->executeQuery($this->getSQL(), $this->params, $this->paramTypes)
            );
        }

        return $this->connection->executeStatement($this->getSQL(), $this->params, $this->paramTypes);
    }

    /**
     * Gets the complete SQL string formed by the current specifications of this QueryBuilder.
     *
     * <code>
     *     $qb = $em->createQueryBuilder()
     *         ->select('u')
     *         ->from('User', 'u')
     *     echo $qb->getSQL(); // SELECT u FROM User u
     * </code>
     *
     * @return string The SQL query string.
     */
    public function getSQL()
    {
        if ($this->sql !== null && $this->state === self::STATE_CLEAN) {
            return $this->sql;
        }

        switch ($this->type) {
            case self::INSERT:
                $sql = $this->getSQLForInsert();
                break;

            case self::DELETE:
                $sql = $this->getSQLForDelete();
                break;

            case self::UPDATE:
                $sql = $this->getSQLForUpdate();
                break;

            case self::SELECT:
            default:
                $sql = $this->getSQLForSelect();
                break;
        }

        $this->state = self::STATE_CLEAN;
        $this->sql   = $sql;

        return $sql;
    }

    /**
     * Sets a query parameter for the query being constructed.
     *
     * <code>
     *     $qb = $conn->createQueryBuilder()
     *         ->select('u')
     *         ->from('users', 'u')
     *         ->where('u.id = :user_id')
     *         ->setParameter(':user_id', 1);
     * </code>
     *
     * @param int|string           $key   Parameter position or name
     * @param mixed                $value Parameter value
     * @param int|string|Type|null $type  Parameter type
     *
     * @return $this 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.
     *
     * <code>
     *     $qb = $conn->createQueryBuilder()
     *         ->select('u')
     *         ->from('users', 'u')
     *         ->where('u.id = :user_id1 OR u.id = :user_id2')
     *         ->setParameters(array(
     *             ':user_id1' => 1,
     *             ':user_id2' => 2
     *         ));
     * </code>
     *
     * @param array<int, mixed>|array<string, mixed>                               $params Parameters to set
     * @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types  Parameter types
     *
     * @return $this This QueryBuilder instance.
     */
    public function setParameters(array $params, array $types = [])
    {
        $this->paramTypes = $types;
        $this->params     = $params;

        return $this;
    }

    /**
     * Gets all defined query parameters for the query being constructed indexed by parameter index or name.
     *
     * @return array<int, mixed>|array<string, mixed> 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 $this->params[$key] ?? null;
    }

    /**
     * Gets all defined query parameter types for the query being constructed indexed by parameter index or name.
     *
     * @return array<int, int|string|Type|null>|array<string, int|string|Type|null> The currently defined
     *                                                                              query parameter types
     */
    public function getParameterTypes()
    {
        return $this->paramTypes;
    }

    /**
     * Gets a (previously set) query parameter type of the query being constructed.
     *
     * @param int|string $key The key of the bound parameter type
     *
     * @return int|string|Type|null The value of the bound parameter type
     */
    public function getParameterType($key)
    {
        return $this->paramTypes[$key] ?? null;
    }

    /**
     * Sets the position of the first result to retrieve (the "offset").
     *
     * @param int $firstResult The first result to return.
     *
     * @return $this This QueryBuilder instance.
     */
    public function setFirstResult($firstResult)
    {
        $this->state       = self::STATE_DIRTY;
        $this->firstResult = $firstResult;

        return $this;
    }

    /**
     * Gets the position of the first result the query object was set to retrieve (the "offset").
     *
     * @return int The position of the first result.
     */
    public function getFirstResult()
    {
        return $this->firstResult;
    }

    /**
     * Sets the maximum number of results to retrieve (the "limit").
     *
     * @param int|null $maxResults The maximum number of results to retrieve or NULL to retrieve all results.
     *
     * @return $this This QueryBuilder instance.
     */
    public function setMaxResults($maxResults)
    {
        $this->state      = self::STATE_DIRTY;
        $this->maxResults = $maxResults;

        return $this;
    }

    /**
     * Gets the maximum number of results the query object was set to retrieve (the "limit").
     * Returns NULL if all results will be returned.
     *
     * @return int|null The 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', 'set', 'where',
     * 'groupBy', 'having' and 'orderBy'.
     *
     * @param string $sqlPartName
     * @param mixed  $sqlPart
     * @param bool   $append
     *
     * @return $this This QueryBuilder instance.
     */
    public function add($sqlPartName, $sqlPart, $append = false)
    {
        $isArray    = is_array($sqlPart);
        $isMultiple = is_array($this->sqlParts[$sqlPartName]);

        if ($isMultiple && ! $isArray) {
            $sqlPart = [$sqlPart];
        }

        $this->state = self::STATE_DIRTY;

        if ($append) {
            if (
                $sqlPartName === 'orderBy'
                || $sqlPartName === 'groupBy'
                || $sqlPartName === 'select'
                || $sqlPartName === 'set'
            ) {
                foreach ($sqlPart as $part) {
                    $this->sqlParts[$sqlPartName][] = $part;
                }
            } elseif ($isArray && is_array($sqlPart[key($sqlPart)])) {
                $key                                  = key($sqlPart);
                $this->sqlParts[$sqlPartName][$key][] = $sqlPart[$key];
            } elseif ($isMultiple) {
                $this->sqlParts[$sqlPartName][] = $sqlPart;
            } else {
                $this->sqlParts[$sqlPartName] = $sqlPart;
            }

            return $this;
        }

        $this->sqlParts[$sqlPartName] = $sqlPart;

        return $this;
    }

    /**
     * Specifies an item that is to be returned in the query result.
     * Replaces any previously specified selections, if any.
     *
     * USING AN ARRAY ARGUMENT IS DEPRECATED. Pass each value as an individual argument.
     *
     * <code>
     *     $qb = $conn->createQueryBuilder()
     *         ->select('u.id', 'p.id')
     *         ->from('users', 'u')
     *         ->leftJoin('u', 'phonenumbers', 'p', 'u.id = p.user_id');
     * </code>
     *
     * @param string|string[]|null $select The selection expression. USING AN ARRAY OR NULL IS DEPRECATED.
     *                                     Pass each value as an individual argument.
     *
     * @return $this This QueryBuilder instance.
     */
    public function select($select = null/*, string ...$selects*/)
    {
        $this->type = self::SELECT;

        if (empty($select)) {
            return $this;
        }

        if (is_array($select)) {
            Deprecation::trigger(
                'doctrine/dbal',
                'https://github.com/doctrine/dbal/issues/3837',
                'Passing an array for the first argument to QueryBuilder::select is deprecated, ' .
                'pass each value as an individual variadic argument instead.'
            );
        }

        $selects = is_array($select) ? $select : func_get_args();

        return $this->add('select', $selects);
    }

    /**
     * Adds DISTINCT to the query.
     *
     * <code>
     *     $qb = $conn->createQueryBuilder()
     *         ->select('u.id')
     *         ->distinct()
     *         ->from('users', 'u')
     * </code>
     *
     * @return $this This QueryBuilder instance.
     */
    public function distinct(): self
    {
        $this->sqlParts['distinct'] = true;

        return $this;
    }

    /**
     * Adds an item that is to be returned in the query result.
     *
     * USING AN ARRAY ARGUMENT IS DEPRECATED. Pass each value as an individual argument.
     *
     * <code>
     *     $qb = $conn->createQueryBuilder()
     *         ->select('u.id')
     *         ->addSelect('p.id')
     *         ->from('users', 'u')
     *         ->leftJoin('u', 'phonenumbers', 'u.id = p.user_id');
     * </code>
     *
     * @param string|string[]|null $select The selection expression. USING AN ARRAY OR NULL IS DEPRECATED.
     *                                     Pass each value as an individual argument.
     *
     * @return $this This QueryBuilder instance.
     */
    public function addSelect($select = null/*, string ...$selects*/)
    {
        $this->type = self::SELECT;

        if (empty($select)) {
            return $this;
        }

        if (is_array($select)) {
            Deprecation::trigger(
                'doctrine/dbal',
                'https://github.com/doctrine/dbal/issues/3837',
                'Passing an array for the first argument to QueryBuilder::addSelect is deprecated, ' .
                'pass each value as an individual variadic argument instead.'
            );
        }

        $selects = is_array($select) ? $select : func_get_args();

        return $this->add('select', $selects, true);
    }

    /**
     * Turns the query being built into a bulk delete query that ranges over
     * a certain table.
     *
     * <code>
     *     $qb = $conn->createQueryBuilder()
     *         ->delete('users', 'u')
     *         ->where('u.id = :user_id')
     *         ->setParameter(':user_id', 1);
     * </code>
     *
     * @param string $delete The table whose rows are subject to the deletion.
     * @param string $alias  The table alias used in the constructed query.
     *
     * @return $this This QueryBuilder instance.
     */
    public function delete($delete = null, $alias = null)
    {
        $this->type = self::DELETE;

        if (! $delete) {
            return $this;
        }

        return $this->add('from', [
            'table' => $delete,
            'alias' => $alias,
        ]);
    }

    /**
     * Turns the query being built into a bulk update query that ranges over
     * a certain table
     *
     * <code>
     *     $qb = $conn->createQueryBuilder()
     *         ->update('counters', 'c')
     *         ->set('c.value', 'c.value + 1')
     *         ->where('c.id = ?');
     * </code>
     *
     * @param string $update The table whose rows are subject to the update.
     * @param string $alias  The table alias used in the constructed query.
     *
     * @return $this This QueryBuilder instance.
     */
    public function update($update = null, $alias = null)
    {
        $this->type = self::UPDATE;

        if (! $update) {
            return $this;
        }

        return $this->add('from', [
            'table' => $update,
            'alias' => $alias,
        ]);
    }

    /**
     * Turns the query being built into an insert query that inserts into
     * a certain table
     *
     * <code>
     *     $qb = $conn->createQueryBuilder()
     *         ->insert('users')
     *         ->values(
     *             array(
     *                 'name' => '?',
     *                 'password' => '?'
     *             )
     *         );
     * </code>
     *
     * @param string $insert The table into which the rows should be inserted.
     *
     * @return $this This QueryBuilder instance.
     */
    public function insert($insert = null)
    {
        $this->type = self::INSERT;

        if (! $insert) {
            return $this;
        }

        return $this->add('from', ['table' => $insert]);
    }

    /**
     * Creates and adds a query root corresponding to the table identified by the
     * given alias, forming a cartesian product with any existing query roots.
     *
     * <code>
     *     $qb = $conn->createQueryBuilder()
     *         ->select('u.id')
     *         ->from('users', 'u')
     * </code>
     *
     * @param string      $from  The table.
     * @param string|null $alias The alias of the table.
     *
     * @return $this This QueryBuilder instance.
     */
    public function from($from, $alias = null)
    {
        return $this->add('from', [
            'table' => $from,
            'alias' => $alias,
        ], true);
    }

    /**
     * Creates and adds a join to the query.
     *
     * <code>
     *     $qb = $conn->createQueryBuilder()
     *         ->select('u.name')
     *         ->from('users', 'u')
     *         ->join('u', 'phonenumbers', 'p', 'p.is_primary = 1');
     * </code>
     *
     * @param string $fromAlias The alias that points to a from clause.
     * @param string $join      The table name to join.
     * @param string $alias     The alias of the join table.
     * @param string $condition The condition for the join.
     *
     * @return $this This QueryBuilder instance.
     */
    public function join($fromAlias, $join, $alias, $condition = null)
    {
        return $this->innerJoin($fromAlias, $join, $alias, $condition);
    }

    /**
     * Creates and adds a join to the query.
     *
     * <code>
     *     $qb = $conn->createQueryBuilder()
     *         ->select('u.name')
     *         ->from('users', 'u')
     *         ->innerJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1');
     * </code>
     *
     * @param string $fromAlias The alias that points to a from clause.
     * @param string $join      The table name to join.
     * @param string $alias     The alias of the join table.
     * @param string $condition The condition for the join.
     *
     * @return $this This QueryBuilder instance.
     */
    public function innerJoin($fromAlias, $join, $alias, $condition = null)
    {
        return $this->add('join', [
            $fromAlias => [
                'joinType'      => 'inner',
                'joinTable'     => $join,
                'joinAlias'     => $alias,
                'joinCondition' => $condition,
            ],
        ], true);
    }

    /**
     * Creates and adds a left join to the query.
     *
     * <code>
     *     $qb = $conn->createQueryBuilder()
     *         ->select('u.name')
     *         ->from('users', 'u')
     *         ->leftJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1');
     * </code>
     *
     * @param string $fromAlias The alias that points to a from clause.
     * @param string $join      The table name to join.
     * @param string $alias     The alias of the join table.
     * @param string $condition The condition for the join.
     *
     * @return $this This QueryBuilder instance.
     */
    public function leftJoin($fromAlias, $join, $alias, $condition = null)
    {
        return $this->add('join', [
            $fromAlias => [
                'joinType'      => 'left',
                'joinTable'     => $join,
                'joinAlias'     => $alias,
                'joinCondition' => $condition,
            ],
        ], true);
    }

    /**
     * Creates and adds a right join to the query.
     *
     * <code>
     *     $qb = $conn->createQueryBuilder()
     *         ->select('u.name')
     *         ->from('users', 'u')
     *         ->rightJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1');
     * </code>
     *
     * @param string $fromAlias The alias that points to a from clause.
     * @param string $join      The table name to join.
     * @param string $alias     The alias of the join table.
     * @param string $condition The condition for the join.
     *
     * @return $this This QueryBuilder instance.
     */
    public function rightJoin($fromAlias, $join, $alias, $condition = null)
    {
        return $this->add('join', [
            $fromAlias => [
                'joinType'      => 'right',
                'joinTable'     => $join,
                'joinAlias'     => $alias,
                'joinCondition' => $condition,
            ],
        ], true);
    }

    /**
     * Sets a new value for a column in a bulk update query.
     *
     * <code>
     *     $qb = $conn->createQueryBuilder()
     *         ->update('counters', 'c')
     *         ->set('c.value', 'c.value + 1')
     *         ->where('c.id = ?');
     * </code>
     *
     * @param string $key   The column to set.
     * @param string $value The value, expression, placeholder, etc.
     *
     * @return $this This QueryBuilder instance.
     */
    public function set($key, $value)
    {
        return $this->add('set', $key . ' = ' . $value, true);
    }

    /**
     * Specifies one or more restrictions to the query result.
     * Replaces any previously specified restrictions, if any.
     *
     * <code>
     *     $qb = $conn->createQueryBuilder()
     *         ->select('c.value')
     *         ->from('counters', 'c')
     *         ->where('c.id = ?');
     *
     *     // You can optionally programatically build and/or expressions
     *     $qb = $conn->createQueryBuilder();
     *
     *     $or = $qb->expr()->orx();
     *     $or->add($qb->expr()->eq('c.id', 1));
     *     $or->add($qb->expr()->eq('c.id', 2));
     *
     *     $qb->update('counters', 'c')
     *         ->set('c.value', 'c.value + 1')
     *         ->where($or);
     * </code>
     *
     * @param mixed $predicates The restriction predicates.
     *
     * @return $this This QueryBuilder instance.
     */
    public function where($predicates)
    {
        if (! (func_num_args() === 1 && $predicates instanceof CompositeExpression)) {
            $predicates = CompositeExpression::and(...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.
     *
     * <code>
     *     $qb = $conn->createQueryBuilder()
     *         ->select('u')
     *         ->from('users', 'u')
     *         ->where('u.username LIKE ?')
     *         ->andWhere('u.is_active = 1');
     * </code>
     *
     * @see where()
     *
     * @param mixed $where The query restrictions.
     *
     * @return $this This QueryBuilder instance.
     */
    public function andWhere($where)
    {
        $args  = func_get_args();
        $args  = array_filter($args); // https://github.com/doctrine/dbal/issues/4282
        $where = $this->getQueryPart('where');

        if ($where instanceof CompositeExpression && $where->getType() === CompositeExpression::TYPE_AND) {
            if (count($args) > 0) {
                $where = $where->with(...$args);
            }
        } else {
            array_unshift($args, $where);
            $where = CompositeExpression::and(...$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.
     *
     * <code>
     *     $qb = $em->createQueryBuilder()
     *         ->select('u.name')
     *         ->from('users', 'u')
     *         ->where('u.id = 1')
     *         ->orWhere('u.id = 2');
     * </code>
     *
     * @see where()
     *
     * @param mixed $where The WHERE statement.
     *
     * @return $this This QueryBuilder instance.
     */
    public function orWhere($where)
    {
        $args  = func_get_args();
        $args  = array_filter($args); // https://github.com/doctrine/dbal/issues/4282
        $where = $this->getQueryPart('where');

        if ($where instanceof CompositeExpression && $where->getType() === CompositeExpression::TYPE_OR) {
            if (count($args) > 0) {
                $where = $where->with(...$args);
            }
        } else {
            array_unshift($args, $where);
            $where = CompositeExpression::or(...$args);
        }

        return $this->add('where', $where, true);
    }

    /**
     * Specifies a grouping over the results of the query.
     * Replaces any previously specified groupings, if any.
     *
     * USING AN ARRAY ARGUMENT IS DEPRECATED. Pass each value as an individual argument.
     *
     * <code>
     *     $qb = $conn->createQueryBuilder()
     *         ->select('u.name')
     *         ->from('users', 'u')
     *         ->groupBy('u.id');
     * </code>
     *
     * @param string|string[] $groupBy The grouping expression. USING AN ARRAY IS DEPRECATED.
     *                                 Pass each value as an individual argument.
     *
     * @return $this This QueryBuilder instance.
     */
    public function groupBy($groupBy/*, string ...$groupBys*/)
    {
        if (empty($groupBy)) {
            return $this;
        }

        if (is_array($groupBy)) {
            Deprecation::trigger(
                'doctrine/dbal',
                'https://github.com/doctrine/dbal/issues/3837',
                'Passing an array for the first argument to QueryBuilder::groupBy is deprecated, ' .
                'pass each value as an individual variadic argument instead.'
            );
        }

        $groupBy = is_array($groupBy) ? $groupBy : func_get_args();

        return $this->add('groupBy', $groupBy, false);
    }

    /**
     * Adds a grouping expression to the query.
     *
     * USING AN ARRAY ARGUMENT IS DEPRECATED. Pass each value as an individual argument.
     *
     * <code>
     *     $qb = $conn->createQueryBuilder()
     *         ->select('u.name')
     *         ->from('users', 'u')
     *         ->groupBy('u.lastLogin')
     *         ->addGroupBy('u.createdAt');
     * </code>
     *
     * @param string|string[] $groupBy The grouping expression. USING AN ARRAY IS DEPRECATED.
     *                                 Pass each value as an individual argument.
     *
     * @return $this This QueryBuilder instance.
     */
    public function addGroupBy($groupBy/*, string ...$groupBys*/)
    {
        if (empty($groupBy)) {
            return $this;
        }

        if (is_array($groupBy)) {
            Deprecation::trigger(
                'doctrine/dbal',
                'https://github.com/doctrine/dbal/issues/3837',
                'Passing an array for the first argument to QueryBuilder::addGroupBy is deprecated, ' .
                'pass each value as an individual variadic argument instead.'
            );
        }

        $groupBy = is_array($groupBy) ? $groupBy : func_get_args();

        return $this->add('groupBy', $groupBy, true);
    }

    /**
     * Sets a value for a column in an insert query.
     *
     * <code>
     *     $qb = $conn->createQueryBuilder()
     *         ->insert('users')
     *         ->values(
     *             array(
     *                 'name' => '?'
     *             )
     *         )
     *         ->setValue('password', '?');
     * </code>
     *
     * @param string $column The column into which the value should be inserted.
     * @param string $value  The value that should be inserted into the column.
     *
     * @return $this This QueryBuilder instance.
     */
    public function setValue($column, $value)
    {
        $this->sqlParts['values'][$column] = $value;

        return $this;
    }

    /**
     * Specifies values for an insert query indexed by column names.
     * Replaces any previous values, if any.
     *
     * <code>
     *     $qb = $conn->createQueryBuilder()
     *         ->insert('users')
     *         ->values(
     *             array(
     *                 'name' => '?',
     *                 'password' => '?'
     *             )
     *         );
     * </code>
     *
     * @param mixed[] $values The values to specify for the insert query indexed by column names.
     *
     * @return $this This QueryBuilder instance.
     */
    public function values(array $values)
    {
        return $this->add('values', $values);
    }

    /**
     * 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 $this This QueryBuilder instance.
     */
    public function having($having)
    {
        if (! (func_num_args() === 1 && $having instanceof CompositeExpression)) {
            $having = CompositeExpression::and(...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 $this This QueryBuilder instance.
     */
    public function andHaving($having)
    {
        $args   = func_get_args();
        $args   = array_filter($args); // https://github.com/doctrine/dbal/issues/4282
        $having = $this->getQueryPart('having');

        if ($having instanceof CompositeExpression && $having->getType() === CompositeExpression::TYPE_AND) {
            $having = $having->with(...$args);
        } else {
            array_unshift($args, $having);
            $having = CompositeExpression::and(...$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 $this This QueryBuilder instance.
     */
    public function orHaving($having)
    {
        $args   = func_get_args();
        $args   = array_filter($args); // https://github.com/doctrine/dbal/issues/4282
        $having = $this->getQueryPart('having');

        if ($having instanceof CompositeExpression && $having->getType() === CompositeExpression::TYPE_OR) {
            $having = $having->with(...$args);
        } else {
            array_unshift($args, $having);
            $having = CompositeExpression::or(...$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 $this This QueryBuilder instance.
     */
    public function orderBy($sort, $order = null)
    {
        return $this->add('orderBy', $sort . ' ' . (! $order ? 'ASC' : $order), false);
    }

    /**
     * Adds an ordering to the query results.
     *
     * @param string $sort  The ordering expression.
     * @param string $order The ordering direction.
     *
     * @return $this This QueryBuilder instance.
     */
    public function addOrderBy($sort, $order = null)
    {
        return $this->add('orderBy', $sort . ' ' . (! $order ? 'ASC' : $order), true);
    }

    /**
     * Gets a query part by its name.
     *
     * @param string $queryPartName
     *
     * @return mixed
     */
    public function getQueryPart($queryPartName)
    {
        return $this->sqlParts[$queryPartName];
    }

    /**
     * Gets all query parts.
     *
     * @return mixed[]
     */
    public function getQueryParts()
    {
        return $this->sqlParts;
    }

    /**
     * Resets SQL parts.
     *
     * @param string[]|null $queryPartNames
     *
     * @return $this This QueryBuilder instance.
     */
    public function resetQueryParts($queryPartNames = null)
    {
        if ($queryPartNames === null) {
            $queryPartNames = array_keys($this->sqlParts);
        }

        foreach ($queryPartNames as $queryPartName) {
            $this->resetQueryPart($queryPartName);
        }

        return $this;
    }

    /**
     * Resets a single SQL part.
     *
     * @param string $queryPartName
     *
     * @return $this This QueryBuilder instance.
     */
    public function resetQueryPart($queryPartName)
    {
        $this->sqlParts[$queryPartName] = self::SQL_PARTS_DEFAULTS[$queryPartName];

        $this->state = self::STATE_DIRTY;

        return $this;
    }

    /**
     * @return string
     *
     * @throws QueryException
     */
    private function getSQLForSelect()
    {
        $query = 'SELECT ' . ($this->sqlParts['distinct'] ? 'DISTINCT ' : '') .
                  implode(', ', $this->sqlParts['select']);

        $query .= ($this->sqlParts['from'] ? ' FROM ' . implode(', ', $this->getFromClauses()) : '')
            . ($this->sqlParts['where'] !== null ? ' WHERE ' . ((string) $this->sqlParts['where']) : '')
            . ($this->sqlParts['groupBy'] ? ' GROUP BY ' . implode(', ', $this->sqlParts['groupBy']) : '')
            . ($this->sqlParts['having'] !== null ? ' HAVING ' . ((string) $this->sqlParts['having']) : '')
            . ($this->sqlParts['orderBy'] ? ' ORDER BY ' . implode(', ', $this->sqlParts['orderBy']) : '');

        if ($this->isLimitQuery()) {
            return $this->connection->getDatabasePlatform()->modifyLimitQuery(
                $query,
                $this->maxResults,
                $this->firstResult
            );
        }

        return $query;
    }

    /**
     * @return string[]
     */
    private function getFromClauses()
    {
        $fromClauses  = [];
        $knownAliases = [];

        // Loop through all FROM clauses
        foreach ($this->sqlParts['from'] as $from) {
            if ($from['alias'] === null) {
                $tableSql       = $from['table'];
                $tableReference = $from['table'];
            } else {
                $tableSql       = $from['table'] . ' ' . $from['alias'];
                $tableReference = $from['alias'];
            }

            $knownAliases[$tableReference] = true;

            $fromClauses[$tableReference] = $tableSql . $this->getSQLForJoins($tableReference, $knownAliases);
        }

        $this->verifyAllAliasesAreKnown($knownAliases);

        return $fromClauses;
    }

    /**
     * @param array<string,true> $knownAliases
     *
     * @throws QueryException
     */
    private function verifyAllAliasesAreKnown(array $knownAliases): void
    {
        foreach ($this->sqlParts['join'] as $fromAlias => $joins) {
            if (! isset($knownAliases[$fromAlias])) {
                throw QueryException::unknownAlias($fromAlias, array_keys($knownAliases));
            }
        }
    }

    /**
     * @return bool
     */
    private function isLimitQuery()
    {
        return $this->maxResults !== null || $this->firstResult !== 0;
    }

    /**
     * Converts this instance into an INSERT string in SQL.
     *
     * @return string
     */
    private function getSQLForInsert()
    {
        return 'INSERT INTO ' . $this->sqlParts['from']['table'] .
        ' (' . implode(', ', array_keys($this->sqlParts['values'])) . ')' .
        ' VALUES(' . implode(', ', $this->sqlParts['values']) . ')';
    }

    /**
     * Converts this instance into an UPDATE string in SQL.
     *
     * @return string
     */
    private function getSQLForUpdate()
    {
        $table = $this->sqlParts['from']['table']
            . ($this->sqlParts['from']['alias'] ? ' ' . $this->sqlParts['from']['alias'] : '');

        return 'UPDATE ' . $table
            . ' SET ' . implode(', ', $this->sqlParts['set'])
            . ($this->sqlParts['where'] !== null ? ' WHERE ' . ((string) $this->sqlParts['where']) : '');
    }

    /**
     * Converts this instance into a DELETE string in SQL.
     *
     * @return string
     */
    private function getSQLForDelete()
    {
        $table = $this->sqlParts['from']['table']
            . ($this->sqlParts['from']['alias'] ? ' ' . $this->sqlParts['from']['alias'] : '');

        return 'DELETE FROM ' . $table
            . ($this->sqlParts['where'] !== null ? ' WHERE ' . ((string) $this->sqlParts['where']) : '');
    }

    /**
     * Gets a string representation of this QueryBuilder which corresponds to
     * the final SQL query being constructed.
     *
     * @return string The string representation of this QueryBuilder.
     */
    public function __toString()
    {
        return $this->getSQL();
    }

    /**
     * Creates a new named parameter and bind the value $value to it.
     *
     * This method provides a shortcut for PDOStatement::bindValue
     * when using prepared statements.
     *
     * The parameter $value specifies the value that you want to bind. If
     * $placeholder is not provided bindValue() will automatically create a
     * placeholder for you. An automatic placeholder will be of the name
     * ':dcValue1', ':dcValue2' etc.
     *
     * For more information see {@link http://php.net/pdostatement-bindparam}
     *
     * Example:
     * <code>
     * $value = 2;
     * $q->eq( 'id', $q->bindValue( $value ) );
     * $stmt = $q->executeQuery(); // executed with 'id = 2'
     * </code>
     *
     * @link http://www.zetacomponents.org
     *
     * @param mixed                $value
     * @param int|string|Type|null $type
     * @param string               $placeHolder The name to bind with. The string must start with a colon ':'.
     *
     * @return string the placeholder name used.
     */
    public function createNamedParameter($value, $type = ParameterType::STRING, $placeHolder = null)
    {
        if ($placeHolder === null) {
            $this->boundCounter++;
            $placeHolder = ':dcValue' . $this->boundCounter;
        }

        $this->setParameter(substr($placeHolder, 1), $value, $type);

        return $placeHolder;
    }

    /**
     * Creates a new positional parameter and bind the given value to it.
     *
     * Attention: If you are using positional parameters with the query builder you have
     * to be very careful to bind all parameters in the order they appear in the SQL
     * statement , otherwise they get bound in the wrong order which can lead to serious
     * bugs in your code.
     *
     * Example:
     * <code>
     *  $qb = $conn->createQueryBuilder();
     *  $qb->select('u.*')
     *     ->from('users', 'u')
     *     ->where('u.username = ' . $qb->createPositionalParameter('Foo', ParameterType::STRING))
     *     ->orWhere('u.username = ' . $qb->createPositionalParameter('Bar', ParameterType::STRING))
     * </code>
     *
     * @param mixed                $value
     * @param int|string|Type|null $type
     *
     * @return string
     */
    public function createPositionalParameter($value, $type = ParameterType::STRING)
    {
        $this->boundCounter++;
        $this->setParameter($this->boundCounter, $value, $type);

        return '?';
    }

    /**
     * @param string             $fromAlias
     * @param array<string,true> $knownAliases
     *
     * @return string
     *
     * @throws QueryException
     */
    private function getSQLForJoins($fromAlias, array &$knownAliases)
    {
        $sql = '';

        if (isset($this->sqlParts['join'][$fromAlias])) {
            foreach ($this->sqlParts['join'][$fromAlias] as $join) {
                if (array_key_exists($join['joinAlias'], $knownAliases)) {
                    throw QueryException::nonUniqueAlias($join['joinAlias'], array_keys($knownAliases));
                }

                $sql .= ' ' . strtoupper($join['joinType'])
                    . ' JOIN ' . $join['joinTable'] . ' ' . $join['joinAlias'];
                if ($join['joinCondition'] !== null) {
                    $sql .= ' ON ' . $join['joinCondition'];
                }

                $knownAliases[$join['joinAlias']] = true;
            }

            foreach ($this->sqlParts['join'][$fromAlias] as $join) {
                $sql .= $this->getSQLForJoins($join['joinAlias'], $knownAliases);
            }
        }

        return $sql;
    }

    /**
     * Deep clone of all expression objects in the SQL parts.
     *
     * @return void
     */
    public function __clone()
    {
        foreach ($this->sqlParts as $part => $elements) {
            if (is_array($this->sqlParts[$part])) {
                foreach ($this->sqlParts[$part] as $idx => $element) {
                    if (! is_object($element)) {
                        continue;
                    }

                    $this->sqlParts[$part][$idx] = clone $element;
                }
            } elseif (is_object($elements)) {
                $this->sqlParts[$part] = clone $elements;
            }
        }

        foreach ($this->params as $name => $param) {
            if (! is_object($param)) {
                continue;
            }

            $this->params[$name] = clone $param;
        }
    }
}
<?php

namespace Doctrine\DBAL;

use Doctrine\DBAL\Types\Type;

use function array_fill;
use function array_fill_keys;
use function array_key_exists;
use function array_keys;
use function array_merge;
use function array_slice;
use function array_values;
use function count;
use function implode;
use function is_int;
use function key;
use function ksort;
use function preg_match_all;
use function sprintf;
use function strlen;
use function strpos;
use function substr;

use const PREG_OFFSET_CAPTURE;

/**
 * Utility class that parses sql statements with regard to types and parameters.
 *
 * @internal
 */
class SQLParserUtils
{
    /**#@+
     *
     * @deprecated Will be removed as internal implementation details.
     */
    public const POSITIONAL_TOKEN = '\?';
    public const NAMED_TOKEN      = '(?<!:):[a-zA-Z_][a-zA-Z0-9_]*';
    // Quote characters within string literals can be preceded by a backslash.
    public const ESCAPED_SINGLE_QUOTED_TEXT   = "(?:'(?:\\\\)+'|'(?:[^'\\\\]|\\\\'?|'')*')";
    public const ESCAPED_DOUBLE_QUOTED_TEXT   = '(?:"(?:\\\\)+"|"(?:[^"\\\\]|\\\\"?)*")';
    public const ESCAPED_BACKTICK_QUOTED_TEXT = '(?:`(?:\\\\)+`|`(?:[^`\\\\]|\\\\`?)*`)';
    /**#@-*/

    private const ESCAPED_BRACKET_QUOTED_TEXT = '(?<!\b(?i:ARRAY))\[(?:[^\]])*\]';

    /**
     * Gets an array of the placeholders in an sql statements as keys and their positions in the query string.
     *
     * For a statement with positional parameters, returns a zero-indexed list of placeholder position.
     * For a statement with named parameters, returns a map of placeholder positions to their parameter names.
     *
     * @deprecated Will be removed as internal implementation detail.
     *
     * @param string $statement
     * @param bool   $isPositional
     *
     * @return int[]|string[]
     */
    public static function getPlaceholderPositions($statement, $isPositional = true)
    {
        return $isPositional
            ? self::getPositionalPlaceholderPositions($statement)
            : self::getNamedPlaceholderPositions($statement);
    }

    /**
     * Returns a zero-indexed list of placeholder position.
     *
     * @return list<int>
     */
    private static function getPositionalPlaceholderPositions(string $statement): array
    {
        return self::collectPlaceholders(
            $statement,
            '?',
            self::POSITIONAL_TOKEN,
            static function (string $_, int $placeholderPosition, int $fragmentPosition, array &$carry): void {
                $carry[] = $placeholderPosition + $fragmentPosition;
            }
        );
    }

    /**
     * Returns a map of placeholder positions to their parameter names.
     *
     * @return array<int,string>
     */
    private static function getNamedPlaceholderPositions(string $statement): array
    {
        return self::collectPlaceholders(
            $statement,
            ':',
            self::NAMED_TOKEN,
            static function (
                string $placeholder,
                int $placeholderPosition,
                int $fragmentPosition,
                array &$carry
            ): void {
                $carry[$placeholderPosition + $fragmentPosition] = substr($placeholder, 1);
            }
        );
    }

    /**
     * @return mixed[]
     */
    private static function collectPlaceholders(
        string $statement,
        string $match,
        string $token,
        callable $collector
    ): array {
        if (strpos($statement, $match) === false) {
            return [];
        }

        $carry = [];

        foreach (self::getUnquotedStatementFragments($statement) as $fragment) {
            preg_match_all('/' . $token . '/', $fragment[0], $matches, PREG_OFFSET_CAPTURE);
            foreach ($matches[0] as $placeholder) {
                $collector($placeholder[0], $placeholder[1], $fragment[1], $carry);
            }
        }

        return $carry;
    }

    /**
     * For a positional query this method can rewrite the sql statement with regard to array parameters.
     *
     * @param string                                                               $query  SQL query
     * @param mixed[]                                                              $params Query parameters
     * @param array<int, Type|int|string|null>|array<string, Type|int|string|null> $types  Parameter types
     *
     * @return mixed[]
     *
     * @throws SQLParserUtilsException
     */
    public static function expandListParameters($query, $params, $types)
    {
        $isPositional   = is_int(key($params));
        $arrayPositions = [];
        $bindIndex      = -1;

        if ($isPositional) {
            // make sure that $types has the same keys as $params
            // to allow omitting parameters with unspecified types
            $types += array_fill_keys(array_keys($params), null);

            ksort($params);
            ksort($types);
        }

        foreach ($types as $name => $type) {
            ++$bindIndex;

            if ($type !== Connection::PARAM_INT_ARRAY && $type !== Connection::PARAM_STR_ARRAY) {
                continue;
            }

            if ($isPositional) {
                $name = $bindIndex;
            }

            $arrayPositions[$name] = false;
        }

        if (( ! $arrayPositions && $isPositional)) {
            return [$query, $params, $types];
        }

        if ($isPositional) {
            $paramOffset = 0;
            $queryOffset = 0;
            $params      = array_values($params);
            $types       = array_values($types);

            $paramPos = self::getPositionalPlaceholderPositions($query);

            foreach ($paramPos as $needle => $needlePos) {
                if (! isset($arrayPositions[$needle])) {
                    continue;
                }

                $needle    += $paramOffset;
                $needlePos += $queryOffset;
                $count      = count($params[$needle]);

                $params = array_merge(
                    array_slice($params, 0, $needle),
                    $params[$needle],
                    array_slice($params, $needle + 1)
                );

                $types = array_merge(
                    array_slice($types, 0, $needle),
                    $count ?
                        // array needles are at {@link \Doctrine\DBAL\ParameterType} constants
                        // + {@link \Doctrine\DBAL\Connection::ARRAY_PARAM_OFFSET}
                        array_fill(0, $count, $types[$needle] - Connection::ARRAY_PARAM_OFFSET) :
                        [],
                    array_slice($types, $needle + 1)
                );

                $expandStr = $count ? implode(', ', array_fill(0, $count, '?')) : 'NULL';
                $query     = substr($query, 0, $needlePos) . $expandStr . substr($query, $needlePos + 1);

                $paramOffset += $count - 1; // Grows larger by number of parameters minus the replaced needle.
                $queryOffset += strlen($expandStr) - 1;
            }

            return [$query, $params, $types];
        }

        $queryOffset = 0;
        $typesOrd    = [];
        $paramsOrd   = [];

        $paramPos = self::getNamedPlaceholderPositions($query);

        foreach ($paramPos as $pos => $paramName) {
            $paramLen = strlen($paramName) + 1;
            $value    = self::extractParam($paramName, $params, true);

            if (! isset($arrayPositions[$paramName]) && ! isset($arrayPositions[':' . $paramName])) {
                $pos         += $queryOffset;
                $queryOffset -= $paramLen - 1;
                $paramsOrd[]  = $value;
                $typesOrd[]   = self::extractParam($paramName, $types, false, ParameterType::STRING);
                $query        = substr($query, 0, $pos) . '?' . substr($query, $pos + $paramLen);

                continue;
            }

            $count     = count($value);
            $expandStr = $count > 0 ? implode(', ', array_fill(0, $count, '?')) : 'NULL';

            foreach ($value as $val) {
                $paramsOrd[] = $val;
                $typesOrd[]  = self::extractParam($paramName, $types, false) - Connection::ARRAY_PARAM_OFFSET;
            }

            $pos         += $queryOffset;
            $queryOffset += strlen($expandStr) - $paramLen;
            $query        = substr($query, 0, $pos) . $expandStr . substr($query, $pos + $paramLen);
        }

        return [$query, $paramsOrd, $typesOrd];
    }

    /**
     * Slice the SQL statement around pairs of quotes and
     * return string fragments of SQL outside of quoted literals.
     * Each fragment is captured as a 2-element array:
     *
     * 0 => matched fragment string,
     * 1 => offset of fragment in $statement
     *
     * @param string $statement
     *
     * @return mixed[][]
     */
    private static function getUnquotedStatementFragments($statement)
    {
        $literal    = self::ESCAPED_SINGLE_QUOTED_TEXT . '|' .
            self::ESCAPED_DOUBLE_QUOTED_TEXT . '|' .
            self::ESCAPED_BACKTICK_QUOTED_TEXT . '|' .
            self::ESCAPED_BRACKET_QUOTED_TEXT;
        $expression = sprintf('/((.+(?i:ARRAY)\\[.+\\])|([^\'"`\\[]+))(?:%s)?/s', $literal);

        preg_match_all($expression, $statement, $fragments, PREG_OFFSET_CAPTURE);

        return $fragments[1];
    }

    /**
     * @param string $paramName     The name of the parameter (without a colon in front)
     * @param mixed  $paramsOrTypes A hash of parameters or types
     * @param bool   $isParam
     * @param mixed  $defaultValue  An optional default value. If omitted, an exception is thrown
     *
     * @return mixed
     *
     * @throws SQLParserUtilsException
     */
    private static function extractParam($paramName, $paramsOrTypes, $isParam, $defaultValue = null)
    {
        if (array_key_exists($paramName, $paramsOrTypes)) {
            return $paramsOrTypes[$paramName];
        }

        // Hash keys can be prefixed with a colon for compatibility
        if (array_key_exists(':' . $paramName, $paramsOrTypes)) {
            return $paramsOrTypes[':' . $paramName];
        }

        if ($defaultValue !== null) {
            return $defaultValue;
        }

        if ($isParam) {
            throw SQLParserUtilsException::missingParam($paramName);
        }

        throw SQLParserUtilsException::missingType($paramName);
    }
}
<?php

namespace Doctrine\DBAL\Portability;

use Doctrine\DBAL\Driver\Result;
use Doctrine\DBAL\Driver\ResultStatement;
use Doctrine\DBAL\Driver\Statement as DriverStatement;
use Doctrine\DBAL\Driver\StatementIterator;
use Doctrine\DBAL\FetchMode;
use Doctrine\DBAL\ParameterType;
use IteratorAggregate;
use PDO;
use ReturnTypeWillChange;

use function array_change_key_case;
use function assert;
use function is_string;
use function rtrim;

/**
 * Portability wrapper for a Statement.
 */
class Statement implements IteratorAggregate, DriverStatement, Result
{
    /** @var int */
    private $portability;

    /** @var DriverStatement|ResultStatement */
    private $stmt;

    /** @var int|null */
    private $case;

    /** @var int */
    private $defaultFetchMode = FetchMode::MIXED;

    /**
     * Wraps <tt>Statement</tt> and applies portability measures.
     *
     * @param DriverStatement|ResultStatement $stmt
     */
    public function __construct($stmt, Connection $conn)
    {
        $this->stmt        = $stmt;
        $this->portability = $conn->getPortability();
        $this->case        = $conn->getFetchCase();
    }

    /**
     * {@inheritdoc}
     */
    public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null)
    {
        assert($this->stmt instanceof DriverStatement);

        return $this->stmt->bindParam($param, $variable, $type, $length);
    }

    /**
     * {@inheritdoc}
     */
    public function bindValue($param, $value, $type = ParameterType::STRING)
    {
        assert($this->stmt instanceof DriverStatement);

        return $this->stmt->bindValue($param, $value, $type);
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use free() instead.
     */
    public function closeCursor()
    {
        return $this->stmt->closeCursor();
    }

    /**
     * {@inheritdoc}
     */
    public function columnCount()
    {
        return $this->stmt->columnCount();
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated The error information is available via exceptions.
     */
    public function errorCode()
    {
        assert($this->stmt instanceof DriverStatement);

        return $this->stmt->errorCode();
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated The error information is available via exceptions.
     */
    public function errorInfo()
    {
        assert($this->stmt instanceof DriverStatement);

        return $this->stmt->errorInfo();
    }

    /**
     * {@inheritdoc}
     */
    public function execute($params = null)
    {
        assert($this->stmt instanceof DriverStatement);

        return $this->stmt->execute($params);
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use one of the fetch- or iterate-related methods.
     */
    public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null)
    {
        $this->defaultFetchMode = $fetchMode;

        return $this->stmt->setFetchMode($fetchMode, $arg2, $arg3);
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use iterateNumeric(), iterateAssociative() or iterateColumn() instead.
     */
    #[ReturnTypeWillChange]
    public function getIterator()
    {
        return new StatementIterator($this);
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use fetchNumeric(), fetchAssociative() or fetchOne() instead.
     */
    public function fetch($fetchMode = null, $cursorOrientation = PDO::FETCH_ORI_NEXT, $cursorOffset = 0)
    {
        $fetchMode = $fetchMode ?: $this->defaultFetchMode;

        $row = $this->stmt->fetch($fetchMode);

        $iterateRow = (
            $this->portability & (Connection::PORTABILITY_EMPTY_TO_NULL | Connection::PORTABILITY_RTRIM)
        ) !== 0;

        $fixCase = $this->case !== null
            && ($fetchMode === FetchMode::ASSOCIATIVE || $fetchMode === FetchMode::MIXED)
            && ($this->portability & Connection::PORTABILITY_FIX_CASE);

        $row = $this->fixRow($row, $iterateRow, $fixCase);

        return $row;
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use fetchAllNumeric(), fetchAllAssociative() or fetchFirstColumn() instead.
     */
    public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null)
    {
        $fetchMode = $fetchMode ?: $this->defaultFetchMode;

        if ($fetchArgument) {
            $rows = $this->stmt->fetchAll($fetchMode, $fetchArgument);
        } else {
            $rows = $this->stmt->fetchAll($fetchMode);
        }

        $fixCase = $this->case !== null
            && ($fetchMode === FetchMode::ASSOCIATIVE || $fetchMode === FetchMode::MIXED)
            && ($this->portability & Connection::PORTABILITY_FIX_CASE);

        return $this->fixResultSet($rows, $fixCase, $fetchMode !== FetchMode::COLUMN);
    }

    /**
     * {@inheritdoc}
     */
    public function fetchNumeric()
    {
        if ($this->stmt instanceof Result) {
            $row = $this->stmt->fetchNumeric();
        } else {
            $row = $this->stmt->fetch(FetchMode::NUMERIC);
        }

        return $this->fixResult($row, false);
    }

    /**
     * {@inheritdoc}
     */
    public function fetchAssociative()
    {
        if ($this->stmt instanceof Result) {
            $row = $this->stmt->fetchAssociative();
        } else {
            $row = $this->stmt->fetch(FetchMode::ASSOCIATIVE);
        }

        return $this->fixResult($row, true);
    }

    /**
     * {@inheritdoc}
     */
    public function fetchOne()
    {
        if ($this->stmt instanceof Result) {
            $value = $this->stmt->fetchOne();
        } else {
            $value = $this->stmt->fetch(FetchMode::COLUMN);
        }

        if (($this->portability & Connection::PORTABILITY_EMPTY_TO_NULL) !== 0 && $value === '') {
            $value = null;
        } elseif (($this->portability & Connection::PORTABILITY_RTRIM) !== 0 && is_string($value)) {
            $value = rtrim($value);
        }

        return $value;
    }

    /**
     * {@inheritdoc}
     */
    public function fetchAllNumeric(): array
    {
        if ($this->stmt instanceof Result) {
            $data = $this->stmt->fetchAllNumeric();
        } else {
            $data = $this->stmt->fetchAll(FetchMode::NUMERIC);
        }

        return $this->fixResultSet($data, false, true);
    }

    /**
     * {@inheritdoc}
     */
    public function fetchAllAssociative(): array
    {
        if ($this->stmt instanceof Result) {
            $data = $this->stmt->fetchAllAssociative();
        } else {
            $data = $this->stmt->fetchAll(FetchMode::ASSOCIATIVE);
        }

        return $this->fixResultSet($data, true, true);
    }

    /**
     * {@inheritdoc}
     */
    public function fetchFirstColumn(): array
    {
        if ($this->stmt instanceof Result) {
            $data = $this->stmt->fetchFirstColumn();
        } else {
            $data = $this->stmt->fetchAll(FetchMode::COLUMN);
        }

        return $this->fixResultSet($data, true, false);
    }

    public function free(): void
    {
        if ($this->stmt instanceof Result) {
            $this->stmt->free();

            return;
        }

        $this->stmt->closeCursor();
    }

    /**
     * @param mixed $result
     *
     * @return mixed
     */
    private function fixResult($result, bool $fixCase)
    {
        $iterateRow = (
            $this->portability & (Connection::PORTABILITY_EMPTY_TO_NULL | Connection::PORTABILITY_RTRIM)
        ) !== 0;

        $fixCase = $fixCase && $this->case !== null && ($this->portability & Connection::PORTABILITY_FIX_CASE) !== 0;

        return $this->fixRow($result, $iterateRow, $fixCase);
    }

    /**
     * @param array<int,mixed> $resultSet
     *
     * @return array<int,mixed>
     */
    private function fixResultSet(array $resultSet, bool $fixCase, bool $isArray): array
    {
        $iterateRow = (
            $this->portability & (Connection::PORTABILITY_EMPTY_TO_NULL | Connection::PORTABILITY_RTRIM)
        ) !== 0;

        $fixCase = $fixCase && $this->case !== null && ($this->portability & Connection::PORTABILITY_FIX_CASE) !== 0;

        if (! $iterateRow && ! $fixCase) {
            return $resultSet;
        }

        if (! $isArray) {
            foreach ($resultSet as $num => $value) {
                $resultSet[$num] = [$value];
            }
        }

        foreach ($resultSet as $num => $row) {
            $resultSet[$num] = $this->fixRow($row, $iterateRow, $fixCase);
        }

        if (! $isArray) {
            foreach ($resultSet as $num => $row) {
                $resultSet[$num] = $row[0];
            }
        }

        return $resultSet;
    }

    /**
     * @param mixed $row
     * @param bool  $iterateRow
     * @param bool  $fixCase
     *
     * @return mixed
     */
    protected function fixRow($row, $iterateRow, $fixCase)
    {
        if (! $row) {
            return $row;
        }

        if ($fixCase) {
            assert($this->case !== null);
            $row = array_change_key_case($row, $this->case);
        }

        if ($iterateRow) {
            foreach ($row as $k => $v) {
                if (($this->portability & Connection::PORTABILITY_EMPTY_TO_NULL) && $v === '') {
                    $row[$k] = null;
                } elseif (($this->portability & Connection::PORTABILITY_RTRIM) && is_string($v)) {
                    $row[$k] = rtrim($v);
                }
            }
        }

        return $row;
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use fetchOne() instead.
     */
    public function fetchColumn($columnIndex = 0)
    {
        $value = $this->stmt->fetchColumn($columnIndex);

        if ($this->portability & (Connection::PORTABILITY_EMPTY_TO_NULL | Connection::PORTABILITY_RTRIM)) {
            if (($this->portability & Connection::PORTABILITY_EMPTY_TO_NULL) && $value === '') {
                $value = null;
            } elseif (($this->portability & Connection::PORTABILITY_RTRIM) && is_string($value)) {
                $value = rtrim($value);
            }
        }

        return $value;
    }

    /**
     * {@inheritdoc}
     */
    public function rowCount()
    {
        assert($this->stmt instanceof DriverStatement);

        return $this->stmt->rowCount();
    }
}
<?php

namespace Doctrine\DBAL\Portability;

use Doctrine\DBAL\Cache\QueryCacheProfile;
use Doctrine\DBAL\ColumnCase;
use Doctrine\DBAL\Connection as BaseConnection;
use Doctrine\DBAL\Driver\PDO\Connection as PDOConnection;
use Doctrine\DBAL\ForwardCompatibility;
use PDO;

use function func_get_args;

use const CASE_LOWER;
use const CASE_UPPER;

/**
 * Portability wrapper for a Connection.
 */
class Connection extends BaseConnection
{
    public const PORTABILITY_ALL           = 255;
    public const PORTABILITY_NONE          = 0;
    public const PORTABILITY_RTRIM         = 1;
    public const PORTABILITY_EMPTY_TO_NULL = 4;
    public const PORTABILITY_FIX_CASE      = 8;

    /**#@+
     *
     * @deprecated Will be removed as internal implementation details.
     */
    public const PORTABILITY_DB2          = 13;
    public const PORTABILITY_ORACLE       = 9;
    public const PORTABILITY_POSTGRESQL   = 13;
    public const PORTABILITY_SQLITE       = 13;
    public const PORTABILITY_OTHERVENDORS = 12;
    public const PORTABILITY_DRIZZLE      = 13;
    public const PORTABILITY_SQLANYWHERE  = 13;
    public const PORTABILITY_SQLSRV       = 13;
    /**#@-*/

    /** @var int */
    private $portability = self::PORTABILITY_NONE;

    /** @var int|null */
    private $case;

    /**
     * {@inheritdoc}
     */
    public function connect()
    {
        $ret = parent::connect();
        if ($ret) {
            $params = $this->getParams();
            if (isset($params['portability'])) {
                $this->portability = $params['portability'] = (new OptimizeFlags())(
                    $this->getDatabasePlatform(),
                    $params['portability']
                );
            }

            if (isset($params['fetch_case']) && $this->portability & self::PORTABILITY_FIX_CASE) {
                if ($this->_conn instanceof PDOConnection) {
                    // make use of c-level support for case handling
                    $this->_conn->setAttribute(PDO::ATTR_CASE, $params['fetch_case']);
                } else {
                    $this->case = $params['fetch_case'] === ColumnCase::LOWER ? CASE_LOWER : CASE_UPPER;
                }
            }
        }

        return $ret;
    }

    /**
     * @return int
     */
    public function getPortability()
    {
        return $this->portability;
    }

    /**
     * @return int|null
     */
    public function getFetchCase()
    {
        return $this->case;
    }

    /**
     * {@inheritdoc}
     */
    public function executeQuery($sql, array $params = [], $types = [], ?QueryCacheProfile $qcp = null)
    {
        $stmt = new Statement(parent::executeQuery($sql, $params, $types, $qcp), $this);
        $stmt->setFetchMode($this->defaultFetchMode);

        return new ForwardCompatibility\Result($stmt);
    }

    /**
     * {@inheritdoc}
     *
     * @param string $sql
     *
     * @return Statement
     */
    public function prepare($sql)
    {
        $stmt = new Statement(parent::prepare($sql), $this);
        $stmt->setFetchMode($this->defaultFetchMode);

        return $stmt;
    }

    /**
     * {@inheritdoc}
     */
    public function query()
    {
        $connection = $this->getWrappedConnection();

        $stmt = $connection->query(...func_get_args());
        $stmt = new Statement($stmt, $this);
        $stmt->setFetchMode($this->defaultFetchMode);

        return $stmt;
    }
}
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Portability;

use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Platforms\DB2Platform;
use Doctrine\DBAL\Platforms\OraclePlatform;
use Doctrine\DBAL\Platforms\PostgreSQL94Platform;
use Doctrine\DBAL\Platforms\SQLAnywhere16Platform;
use Doctrine\DBAL\Platforms\SqlitePlatform;
use Doctrine\DBAL\Platforms\SQLServer2012Platform;

final class OptimizeFlags
{
    /**
     * Platform-specific portability flags that need to be excluded from the user-provided mode
     * since the platform already operates in this mode to avoid unnecessary conversion overhead.
     *
     * @var array<string,int>
     */
    private static $platforms = [
        DB2Platform::class           => 0,
        OraclePlatform::class        => Connection::PORTABILITY_EMPTY_TO_NULL,
        PostgreSQL94Platform::class  => 0,
        SQLAnywhere16Platform::class => 0,
        SqlitePlatform::class        => 0,
        SQLServer2012Platform::class => 0,
    ];

    public function __invoke(AbstractPlatform $platform, int $flags): int
    {
        foreach (self::$platforms as $class => $mask) {
            if ($platform instanceof $class) {
                $flags &= ~$mask;

                break;
            }
        }

        return $flags;
    }
}
<?php

namespace Doctrine\DBAL\Cache;

use ArrayIterator;
use Doctrine\Common\Cache\Cache;
use Doctrine\DBAL\Driver\Exception;
use Doctrine\DBAL\Driver\FetchUtils;
use Doctrine\DBAL\Driver\Result;
use Doctrine\DBAL\Driver\ResultStatement;
use Doctrine\DBAL\Driver\Statement;
use Doctrine\DBAL\FetchMode;
use InvalidArgumentException;
use IteratorAggregate;
use PDO;
use ReturnTypeWillChange;

use function array_map;
use function array_merge;
use function array_values;
use function assert;
use function reset;

/**
 * Cache statement for SQL results.
 *
 * A result is saved in multiple cache keys, there is the originally specified
 * cache key which is just pointing to result rows by key. The following things
 * have to be ensured:
 *
 * 1. lifetime of the original key has to be longer than that of all the individual rows keys
 * 2. if any one row key is missing the query has to be re-executed.
 *
 * Also you have to realize that the cache will load the whole result into memory at once to ensure 2.
 * This means that the memory usage for cached results might increase by using this feature.
 *
 * @deprecated
 */
class ResultCacheStatement implements IteratorAggregate, ResultStatement, Result
{
    /** @var Cache */
    private $resultCache;

    /** @var string */
    private $cacheKey;

    /** @var string */
    private $realKey;

    /** @var int */
    private $lifetime;

    /** @var ResultStatement */
    private $statement;

    /** @var array<int,array<string,mixed>>|null */
    private $data;

    /** @var int */
    private $defaultFetchMode = FetchMode::MIXED;

    /**
     * @param string $cacheKey
     * @param string $realKey
     * @param int    $lifetime
     */
    public function __construct(ResultStatement $stmt, Cache $resultCache, $cacheKey, $realKey, $lifetime)
    {
        $this->statement   = $stmt;
        $this->resultCache = $resultCache;
        $this->cacheKey    = $cacheKey;
        $this->realKey     = $realKey;
        $this->lifetime    = $lifetime;
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use free() instead.
     */
    public function closeCursor()
    {
        $this->free();

        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function columnCount()
    {
        return $this->statement->columnCount();
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use one of the fetch- or iterate-related methods.
     */
    public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null)
    {
        $this->defaultFetchMode = $fetchMode;

        return true;
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use iterateNumeric(), iterateAssociative() or iterateColumn() instead.
     */
    #[ReturnTypeWillChange]
    public function getIterator()
    {
        $data = $this->fetchAll();

        return new ArrayIterator($data);
    }

    /**
     * Be warned that you will need to call this method until no rows are
     * available for caching to happen.
     *
     * {@inheritdoc}
     *
     * @deprecated Use fetchNumeric(), fetchAssociative() or fetchOne() instead.
     */
    public function fetch($fetchMode = null, $cursorOrientation = PDO::FETCH_ORI_NEXT, $cursorOffset = 0)
    {
        if ($this->data === null) {
            $this->data = [];
        }

        $row = $this->statement->fetch(FetchMode::ASSOCIATIVE);

        if ($row) {
            $this->data[] = $row;

            $fetchMode = $fetchMode ?: $this->defaultFetchMode;

            if ($fetchMode === FetchMode::ASSOCIATIVE) {
                return $row;
            }

            if ($fetchMode === FetchMode::NUMERIC) {
                return array_values($row);
            }

            if ($fetchMode === FetchMode::MIXED) {
                return array_merge($row, array_values($row));
            }

            if ($fetchMode === FetchMode::COLUMN) {
                return reset($row);
            }

            throw new InvalidArgumentException('Invalid fetch-style given for caching result.');
        }

        $this->saveToCache();

        return false;
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use fetchAllNumeric(), fetchAllAssociative() or fetchFirstColumn() instead.
     */
    public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null)
    {
        $data = $this->statement->fetchAll(FetchMode::ASSOCIATIVE, $fetchArgument, $ctorArgs);

        $this->data = $data;

        $this->saveToCache();

        if ($fetchMode === FetchMode::NUMERIC) {
            foreach ($data as $i => $row) {
                $data[$i] = array_values($row);
            }
        } elseif ($fetchMode === FetchMode::MIXED) {
            foreach ($data as $i => $row) {
                $data[$i] = array_merge($row, array_values($row));
            }
        } elseif ($fetchMode === FetchMode::COLUMN) {
            foreach ($data as $i => $row) {
                $data[$i] = reset($row);
            }
        }

        return $data;
    }

    /**
     * Be warned that you will need to call this method until no rows are
     * available for caching to happen.
     *
     * {@inheritdoc}
     *
     * @deprecated Use fetchOne() instead.
     */
    public function fetchColumn($columnIndex = 0)
    {
        $row = $this->fetch(FetchMode::NUMERIC);

        // TODO: verify that return false is the correct behavior
        return $row[$columnIndex] ?? false;
    }

    /**
     * Be warned that you will need to call this method until no rows are
     * available for caching to happen.
     *
     * {@inheritdoc}
     */
    public function fetchNumeric()
    {
        $row = $this->doFetch();

        if ($row === false) {
            return false;
        }

        return array_values($row);
    }

    /**
     * Be warned that you will need to call this method until no rows are
     * available for caching to happen.
     *
     * {@inheritdoc}
     */
    public function fetchAssociative()
    {
        return $this->doFetch();
    }

    /**
     * Be warned that you will need to call this method until no rows are
     * available for caching to happen.
     *
     * {@inheritdoc}
     */
    public function fetchOne()
    {
        return FetchUtils::fetchOne($this);
    }

    /**
     * {@inheritdoc}
     */
    public function fetchAllNumeric(): array
    {
        if ($this->statement instanceof Result) {
            $data = $this->statement->fetchAllAssociative();
        } else {
            $data = $this->statement->fetchAll(FetchMode::ASSOCIATIVE);
        }

        $this->data = $data;

        $this->saveToCache();

        return array_map('array_values', $data);
    }

    /**
     * {@inheritdoc}
     */
    public function fetchAllAssociative(): array
    {
        if ($this->statement instanceof Result) {
            $data = $this->statement->fetchAllAssociative();
        } else {
            $data = $this->statement->fetchAll(FetchMode::ASSOCIATIVE);
        }

        $this->data = $data;

        $this->saveToCache();

        return $data;
    }

    /**
     * {@inheritdoc}
     */
    public function fetchFirstColumn(): array
    {
        return FetchUtils::fetchFirstColumn($this);
    }

    /**
     * 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 int|string The number of rows.
     */
    public function rowCount()
    {
        assert($this->statement instanceof Statement);

        return $this->statement->rowCount();
    }

    public function free(): void
    {
        $this->data = null;
    }

    /**
     * @return array<string,mixed>|false
     *
     * @throws Exception
     */
    private function doFetch()
    {
        if ($this->data === null) {
            $this->data = [];
        }

        if ($this->statement instanceof Result) {
            $row = $this->statement->fetchAssociative();
        } else {
            $row = $this->statement->fetch(FetchMode::ASSOCIATIVE);
        }

        if ($row !== false) {
            $this->data[] = $row;

            return $row;
        }

        $this->saveToCache();

        return false;
    }

    private function saveToCache(): void
    {
        if ($this->data === null) {
            return;
        }

        $data = $this->resultCache->fetch($this->cacheKey);
        if (! $data) {
            $data = [];
        }

        $data[$this->realKey] = $this->data;

        $this->resultCache->save($this->cacheKey, $data, $this->lifetime);
    }
}
<?php

namespace Doctrine\DBAL\Cache;

use ArrayIterator;
use Doctrine\DBAL\Driver\FetchUtils;
use Doctrine\DBAL\Driver\Result;
use Doctrine\DBAL\Driver\ResultStatement;
use Doctrine\DBAL\FetchMode;
use InvalidArgumentException;
use IteratorAggregate;
use PDO;
use ReturnTypeWillChange;

use function array_merge;
use function array_values;
use function count;
use function reset;

/**
 * @deprecated
 */
class ArrayStatement implements IteratorAggregate, ResultStatement, Result
{
    /** @var mixed[] */
    private $data;

    /** @var int */
    private $columnCount = 0;

    /** @var int */
    private $num = 0;

    /** @var int */
    private $defaultFetchMode = FetchMode::MIXED;

    /**
     * @param mixed[] $data
     */
    public function __construct(array $data)
    {
        $this->data = $data;
        if (! count($data)) {
            return;
        }

        $this->columnCount = count($data[0]);
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use free() instead.
     */
    public function closeCursor()
    {
        $this->free();

        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function rowCount()
    {
        return count($this->data);
    }

    /**
     * {@inheritdoc}
     */
    public function columnCount()
    {
        return $this->columnCount;
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use one of the fetch- or iterate-related methods.
     */
    public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null)
    {
        if ($arg2 !== null || $arg3 !== null) {
            throw new InvalidArgumentException('Caching layer does not support 2nd/3rd argument to setFetchMode()');
        }

        $this->defaultFetchMode = $fetchMode;

        return true;
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use iterateNumeric(), iterateAssociative() or iterateColumn() instead.
     */
    #[ReturnTypeWillChange]
    public function getIterator()
    {
        $data = $this->fetchAll();

        return new ArrayIterator($data);
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use fetchNumeric(), fetchAssociative() or fetchOne() instead.
     */
    public function fetch($fetchMode = null, $cursorOrientation = PDO::FETCH_ORI_NEXT, $cursorOffset = 0)
    {
        if (! isset($this->data[$this->num])) {
            return false;
        }

        $row       = $this->data[$this->num++];
        $fetchMode = $fetchMode ?: $this->defaultFetchMode;

        if ($fetchMode === FetchMode::ASSOCIATIVE) {
            return $row;
        }

        if ($fetchMode === FetchMode::NUMERIC) {
            return array_values($row);
        }

        if ($fetchMode === FetchMode::MIXED) {
            return array_merge($row, array_values($row));
        }

        if ($fetchMode === FetchMode::COLUMN) {
            return reset($row);
        }

        throw new InvalidArgumentException('Invalid fetch-style given for fetching result.');
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use fetchAllNumeric(), fetchAllAssociative() or fetchFirstColumn() instead.
     */
    public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null)
    {
        $rows = [];
        while ($row = $this->fetch($fetchMode)) {
            $rows[] = $row;
        }

        return $rows;
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use fetchOne() instead.
     */
    public function fetchColumn($columnIndex = 0)
    {
        $row = $this->fetch(FetchMode::NUMERIC);

        // TODO: verify that return false is the correct behavior
        return $row[$columnIndex] ?? false;
    }

    /**
     * {@inheritdoc}
     */
    public function fetchNumeric()
    {
        $row = $this->doFetch();

        if ($row === false) {
            return false;
        }

        return array_values($row);
    }

    /**
     * {@inheritdoc}
     */
    public function fetchAssociative()
    {
        return $this->doFetch();
    }

    /**
     * {@inheritdoc}
     */
    public function fetchOne()
    {
        $row = $this->doFetch();

        if ($row === false) {
            return false;
        }

        return reset($row);
    }

    /**
     * {@inheritdoc}
     */
    public function fetchAllNumeric(): array
    {
        return FetchUtils::fetchAllNumeric($this);
    }

    /**
     * {@inheritdoc}
     */
    public function fetchAllAssociative(): array
    {
        return FetchUtils::fetchAllAssociative($this);
    }

    /**
     * {@inheritdoc}
     */
    public function fetchFirstColumn(): array
    {
        return FetchUtils::fetchFirstColumn($this);
    }

    public function free(): void
    {
        $this->data = [];
    }

    /**
     * @return mixed|false
     */
    private function doFetch()
    {
        if (! isset($this->data[$this->num])) {
            return false;
        }

        return $this->data[$this->num++];
    }
}
<?php

namespace Doctrine\DBAL\Cache;

use Doctrine\Common\Cache\Cache;
use Doctrine\DBAL\Types\Type;

use function hash;
use function serialize;
use function sha1;

/**
 * Query Cache Profile handles the data relevant for query caching.
 *
 * It is a value object, setter methods return NEW instances.
 */
class QueryCacheProfile
{
    /** @var Cache|null */
    private $resultCacheDriver;

    /** @var int */
    private $lifetime = 0;

    /** @var string|null */
    private $cacheKey;

    /**
     * @param int         $lifetime
     * @param string|null $cacheKey
     */
    public function __construct($lifetime = 0, $cacheKey = null, ?Cache $resultCache = null)
    {
        $this->lifetime          = $lifetime;
        $this->cacheKey          = $cacheKey;
        $this->resultCacheDriver = $resultCache;
    }

    /**
     * @return Cache|null
     */
    public function getResultCacheDriver()
    {
        return $this->resultCacheDriver;
    }

    /**
     * @return int
     */
    public function getLifetime()
    {
        return $this->lifetime;
    }

    /**
     * @return string
     *
     * @throws CacheException
     */
    public function getCacheKey()
    {
        if ($this->cacheKey === null) {
            throw CacheException::noCacheKey();
        }

        return $this->cacheKey;
    }

    /**
     * Generates the real cache key from query, params, types and connection parameters.
     *
     * @param string                                                               $sql
     * @param array<int, mixed>|array<string, mixed>                               $params
     * @param array<int, Type|int|string|null>|array<string, Type|int|string|null> $types
     * @param array<string, mixed>                                                 $connectionParams
     *
     * @return string[]
     */
    public function generateCacheKeys($sql, $params, $types, array $connectionParams = [])
    {
        $realCacheKey = 'query=' . $sql .
            '&params=' . serialize($params) .
            '&types=' . serialize($types) .
            '&connectionParams=' . hash('sha256', serialize($connectionParams));

        // should the key be automatically generated using the inputs or is the cache key set?
        if ($this->cacheKey === null) {
            $cacheKey = sha1($realCacheKey);
        } else {
            $cacheKey = $this->cacheKey;
        }

        return [$cacheKey, $realCacheKey];
    }

    /**
     * @return QueryCacheProfile
     */
    public function setResultCacheDriver(Cache $cache)
    {
        return new QueryCacheProfile($this->lifetime, $this->cacheKey, $cache);
    }

    /**
     * @param string|null $cacheKey
     *
     * @return QueryCacheProfile
     */
    public function setCacheKey($cacheKey)
    {
        return new QueryCacheProfile($this->lifetime, $cacheKey, $this->resultCacheDriver);
    }

    /**
     * @param int $lifetime
     *
     * @return QueryCacheProfile
     */
    public function setLifetime($lifetime)
    {
        return new QueryCacheProfile($lifetime, $this->cacheKey, $this->resultCacheDriver);
    }
}
<?php

namespace Doctrine\DBAL\Cache;

use Doctrine\DBAL\Exception;

/**
 * @psalm-immutable
 */
class CacheException extends Exception
{
    /**
     * @return CacheException
     */
    public static function noCacheKey()
    {
        return new self('No cache key was set.');
    }

    /**
     * @return CacheException
     */
    public static function noResultDriverConfigured()
    {
        return new self('Trying to cache a query but no result driver is configured.');
    }
}
<?php

namespace Doctrine\DBAL;

use Doctrine\DBAL\Driver\Exception;
use Traversable;

/**
 * This interfaces contains methods allowing forward compatibility with v3.0 Result
 *
 * @see https://github.com/doctrine/dbal/blob/3.0.x/src/Result.php
 */
interface Result extends Abstraction\Result
{
    /**
     * Returns an array containing the values of the first column of the result.
     *
     * @return array<mixed,mixed>
     *
     * @throws Exception
     */
    public function fetchAllKeyValue(): array;

    /**
     * Returns an associative array with the keys mapped to the first column and the values being
     * an associative array representing the rest of the columns and their values.
     *
     * @return array<mixed,array<string,mixed>>
     *
     * @throws Exception
     */
    public function fetchAllAssociativeIndexed(): array;

    /**
     * Returns an iterator over the result set with the values of the first column of the result
     *
     * @return Traversable<mixed,mixed>
     *
     * @throws Exception
     */
    public function iterateKeyValue(): Traversable;

    /**
     * Returns an iterator over the result set with the keys mapped to the first column and the values being
     * an associative array representing the rest of the columns and their values.
     *
     * @return Traversable<mixed,array<string,mixed>>
     *
     * @throws Exception
     */
    public function iterateAssociativeIndexed(): Traversable;
}
<?php

namespace Doctrine\DBAL\ForwardCompatibility;

use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Exception\NoKeyValue;
use Doctrine\DBAL\ParameterType;
use Doctrine\Deprecations\Deprecation;
use IteratorAggregate;
use PDO;
use ReturnTypeWillChange;
use Traversable;

use function array_shift;
use function func_get_args;
use function method_exists;

/**
 * A wrapper around a Doctrine\DBAL\Driver\ResultStatement that adds 3.0 features
 * defined in Result interface
 */
class Result implements IteratorAggregate, DriverStatement, DriverResultStatement
{
    /** @var Driver\ResultStatement */
    private $stmt;

    public static function ensure(Driver\ResultStatement $stmt): Result
    {
        if ($stmt instanceof Result) {
            return $stmt;
        }

        return new Result($stmt);
    }

    public function __construct(Driver\ResultStatement $stmt)
    {
        $this->stmt = $stmt;
    }

    /**
     * @return Driver\ResultStatement
     */
    #[ReturnTypeWillChange]
    public function getIterator()
    {
        return $this->stmt;
    }

    /**
     * {@inheritDoc}
     *
     * @deprecated Use Result::free() instead.
     */
    public function closeCursor()
    {
        return $this->stmt->closeCursor();
    }

    /**
     * {@inheritDoc}
     */
    public function columnCount()
    {
        return $this->stmt->columnCount();
    }

    /**
     * {@inheritDoc}
     *
     * @deprecated Use one of the fetch- or iterate-related methods.
     */
    public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null)
    {
        return $this->stmt->setFetchMode($fetchMode, $arg2, $arg3);
    }

    /**
     * {@inheritDoc}
     *
     * @deprecated Use fetchNumeric(), fetchAssociative() or fetchOne() instead.
     */
    public function fetch($fetchMode = null, $cursorOrientation = PDO::FETCH_ORI_NEXT, $cursorOffset = 0)
    {
        Deprecation::triggerIfCalledFromOutside(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/4019',
            'Result::fetch() is deprecated, use Result::fetchNumeric(), fetchAssociative() or fetchOne() instead.'
        );

        return $this->stmt->fetch(...func_get_args());
    }

    /**
     * {@inheritDoc}
     *
     * @deprecated Use fetchAllNumeric(), fetchAllAssociative() or fetchFirstColumn() instead.
     */
    public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null)
    {
        Deprecation::triggerIfCalledFromOutside(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/4019',
            'Result::fetchAll() is deprecated, use Result::fetchAllNumeric(), fetchAllAssociative() or ' .
            'fetchFirstColumn() instead.'
        );

        return $this->stmt->fetchAll($fetchMode, $fetchArgument, $ctorArgs);
    }

    /**
     * {@inheritDoc}
     *
     * @deprecated Use fetchOne() instead.
     */
    public function fetchColumn($columnIndex = 0)
    {
        Deprecation::triggerIfCalledFromOutside(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/4019',
            'Result::fetchColumn() is deprecated, use Result::fetchOne() instead.'
        );

        return $this->stmt->fetchColumn($columnIndex);
    }

    /**
     * {@inheritDoc}
     */
    public function fetchNumeric()
    {
        return $this->stmt->fetch(PDO::FETCH_NUM);
    }

    /**
     * {@inheritDoc}
     */
    public function fetchAssociative()
    {
        return $this->stmt->fetch(PDO::FETCH_ASSOC);
    }

    /**
     * {@inheritDoc}
     */
    public function fetchOne()
    {
        $row = $this->fetchNumeric();

        if ($row === false) {
            return false;
        }

        return $row[0];
    }

    /**
     * {@inheritDoc}
     */
    public function fetchAllNumeric(): array
    {
        $rows = [];

        while (($row = $this->fetchNumeric()) !== false) {
            $rows[] = $row;
        }

        return $rows;
    }

    /**
     * {@inheritDoc}
     */
    public function fetchAllAssociative(): array
    {
        $rows = [];

        while (($row = $this->fetchAssociative()) !== false) {
            $rows[] = $row;
        }

        return $rows;
    }

    /**
     * {@inheritDoc}
     */
    public function fetchAllKeyValue(): array
    {
        $this->ensureHasKeyValue();
        $data = [];

        foreach ($this->fetchAllNumeric() as [$key, $value]) {
            $data[$key] = $value;
        }

        return $data;
    }

    /**
     * {@inheritDoc}
     */
    public function fetchAllAssociativeIndexed(): array
    {
        $data = [];

        foreach ($this->fetchAllAssociative() as $row) {
            $data[array_shift($row)] = $row;
        }

        return $data;
    }

    /**
     * {@inheritDoc}
     */
    public function fetchFirstColumn(): array
    {
        $rows = [];

        while (($row = $this->fetchOne()) !== false) {
            $rows[] = $row;
        }

        return $rows;
    }

    /**
     * {@inheritdoc}
     *
     * @return Traversable<int,array<int,mixed>>
     */
    public function iterateNumeric(): Traversable
    {
        while (($row = $this->fetchNumeric()) !== false) {
            yield $row;
        }
    }

    /**
     * {@inheritDoc}
     *
     * @return Traversable<int,array<string,mixed>>
     */
    public function iterateAssociative(): Traversable
    {
        while (($row = $this->fetchAssociative()) !== false) {
            yield $row;
        }
    }

    /**
     * {@inheritDoc}
     *
     * @return Traversable<mixed,mixed>
     */
    public function iterateKeyValue(): Traversable
    {
        $this->ensureHasKeyValue();

        foreach ($this->iterateNumeric() as [$key, $value]) {
            yield $key => $value;
        }
    }

    /**
     * {@inheritDoc}
     *
     * @return Traversable<mixed,array<string,mixed>>
     */
    public function iterateAssociativeIndexed(): Traversable
    {
        foreach ($this->iterateAssociative() as $row) {
            yield array_shift($row) => $row;
        }
    }

    /**
     * {@inheritDoc}
     *
     * @return Traversable<int,mixed>
     */
    public function iterateColumn(): Traversable
    {
        while (($value = $this->fetchOne()) !== false) {
            yield $value;
        }
    }

    /**
     * {@inheritDoc}
     */
    public function rowCount()
    {
        if (method_exists($this->stmt, 'rowCount')) {
            return $this->stmt->rowCount();
        }

        throw Exception::notSupported('rowCount');
    }

    public function free(): void
    {
        $this->closeCursor();
    }

    private function ensureHasKeyValue(): void
    {
        $columnCount = $this->columnCount();

        if ($columnCount < 2) {
            throw NoKeyValue::fromColumnCount($columnCount);
        }
    }

    /**
     * {@inheritDoc}
     *
     * @deprecated This feature will no longer be available on Result object in 3.0.x version.
     */
    public function bindValue($param, $value, $type = ParameterType::STRING)
    {
        Deprecation::triggerIfCalledFromOutside(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/4019',
            'Result::bindValue() is deprecated, no replacement.'
        );

        if ($this->stmt instanceof Driver\Statement) {
            return $this->stmt->bindValue($param, $value, $type);
        }

        throw Exception::notSupported('bindValue');
    }

    /**
     * {@inheritDoc}
     *
     * @deprecated This feature will no longer be available on Result object in 3.0.x version.
     */
    public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null)
    {
        Deprecation::triggerIfCalledFromOutside(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/4019',
            'Result::bindParam() is deprecated, no replacement.'
        );

        if ($this->stmt instanceof Driver\Statement) {
            return $this->stmt->bindParam($param, $variable, $type, $length);
        }

        throw Exception::notSupported('bindParam');
    }

    /**
     * {@inheritDoc}
     *
     * @deprecated The error information is available via exceptions.
     */
    public function errorCode()
    {
        Deprecation::triggerIfCalledFromOutside(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/4019',
            'Result::errorCode() is deprecated, the error information is available via exceptions.'
        );

        if ($this->stmt instanceof Driver\Statement) {
            return $this->stmt->errorCode();
        }

        throw Exception::notSupported('errorCode');
    }

    /**
     * {@inheritDoc}
     *
     * @deprecated The error information is available via exceptions.
     */
    public function errorInfo()
    {
        Deprecation::triggerIfCalledFromOutside(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/4019',
            'Result::errorInfo() is deprecated, the error information is available via exceptions.'
        );

        if ($this->stmt instanceof Driver\Statement) {
            return $this->stmt->errorInfo();
        }

        throw Exception::notSupported('errorInfo');
    }

    /**
     * {@inheritDoc}
     *
     * @deprecated This feature will no longer be available on Result object in 3.0.x version.
     */
    public function execute($params = null)
    {
        Deprecation::triggerIfCalledFromOutside(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/4019',
            'Result::execute() is deprecated, no replacement.'
        );

        if ($this->stmt instanceof Driver\Statement) {
            return $this->stmt->execute($params);
        }

        throw Exception::notSupported('execute');
    }
}
<?php

namespace Doctrine\DBAL\ForwardCompatibility;

use Doctrine\DBAL;

interface DriverResultStatement extends DBAL\Driver\ResultStatement, DBAL\Result
{
}
<?php

namespace Doctrine\DBAL\ForwardCompatibility;

use Doctrine\DBAL;

interface DriverStatement extends DBAL\Driver\Statement, DBAL\Result
{
}
<?php

namespace Doctrine\DBAL;

use Doctrine\Common\Cache\Cache;
use Doctrine\DBAL\Logging\SQLLogger;
use Doctrine\DBAL\Schema\AbstractAsset;
use Doctrine\Deprecations\Deprecation;

use function preg_match;

/**
 * Configuration container for the Doctrine DBAL.
 *
 * Internal note: 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 mixed[]
     */
    protected $_attributes = [];

    /**
     * Sets the SQL logger to use. Defaults to NULL which means SQL logging is disabled.
     *
     * @return void
     */
    public function setSQLLogger(?SQLLogger $logger = null)
    {
        $this->_attributes['sqlLogger'] = $logger;
    }

    /**
     * Gets the SQL logger that is used.
     *
     * @return SQLLogger|null
     */
    public function getSQLLogger()
    {
        return $this->_attributes['sqlLogger'] ?? null;
    }

    /**
     * Gets the cache driver implementation that is used for query result caching.
     *
     * @return Cache|null
     */
    public function getResultCacheImpl()
    {
        return $this->_attributes['resultCacheImpl'] ?? null;
    }

    /**
     * Sets the cache driver implementation that is used for query result caching.
     *
     * @return void
     */
    public function setResultCacheImpl(Cache $cacheImpl)
    {
        $this->_attributes['resultCacheImpl'] = $cacheImpl;
    }

    /**
     * Sets the filter schema assets expression.
     *
     * Only include tables/sequences matching the filter expression regexp in
     * schema instances generated for the active connection when calling
     * {AbstractSchemaManager#createSchema()}.
     *
     * @deprecated Use Configuration::setSchemaAssetsFilter() instead
     *
     * @param string|null $filterExpression
     *
     * @return void
     */
    public function setFilterSchemaAssetsExpression($filterExpression)
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/3316',
            'Configuration::setFilterSchemaAssetsExpression() is deprecated, use setSchemaAssetsFilter() instead.'
        );

        $this->_attributes['filterSchemaAssetsExpression'] = $filterExpression;
        if ($filterExpression) {
            $this->_attributes['filterSchemaAssetsExpressionCallable']
                = $this->buildSchemaAssetsFilterFromExpression($filterExpression);
        } else {
            $this->_attributes['filterSchemaAssetsExpressionCallable'] = null;
        }
    }

    /**
     * Returns filter schema assets expression.
     *
     * @deprecated Use Configuration::getSchemaAssetsFilter() instead
     *
     * @return string|null
     */
    public function getFilterSchemaAssetsExpression()
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/3316',
            'Configuration::getFilterSchemaAssetsExpression() is deprecated, use getSchemaAssetsFilter() instead.'
        );

        return $this->_attributes['filterSchemaAssetsExpression'] ?? null;
    }

    /**
     * @param string $filterExpression
     *
     * @return callable(string|AbstractAsset)
     */
    private function buildSchemaAssetsFilterFromExpression($filterExpression): callable
    {
        return static function ($assetName) use ($filterExpression) {
            if ($assetName instanceof AbstractAsset) {
                $assetName = $assetName->getName();
            }

            return preg_match($filterExpression, $assetName);
        };
    }

    /**
     * Sets the callable to use to filter schema assets.
     */
    public function setSchemaAssetsFilter(?callable $callable = null): ?callable
    {
        $this->_attributes['filterSchemaAssetsExpression'] = null;

        return $this->_attributes['filterSchemaAssetsExpressionCallable'] = $callable;
    }

    /**
     * Returns the callable to use to filter schema assets.
     */
    public function getSchemaAssetsFilter(): ?callable
    {
        return $this->_attributes['filterSchemaAssetsExpressionCallable'] ?? null;
    }

    /**
     * Sets the default auto-commit mode for connections.
     *
     * If a connection is in auto-commit mode, then all its SQL statements will be executed and committed as individual
     * transactions. Otherwise, its SQL statements are grouped into transactions that are terminated by a call to either
     * the method commit or the method rollback. By default, new connections are in auto-commit mode.
     *
     * @see   getAutoCommit
     *
     * @param bool $autoCommit True to enable auto-commit mode; false to disable it.
     *
     * @return void
     */
    public function setAutoCommit($autoCommit)
    {
        $this->_attributes['autoCommit'] = (bool) $autoCommit;
    }

    /**
     * Returns the default auto-commit mode for connections.
     *
     * @see    setAutoCommit
     *
     * @return bool True if auto-commit mode is enabled by default for connections, false otherwise.
     */
    public function getAutoCommit()
    {
        return $this->_attributes['autoCommit'] ?? true;
    }
}
<?php

namespace Doctrine\DBAL;

/**
 * @psalm-immutable
 */
class Exception extends DBALException
{
}
<?php

namespace Doctrine\DBAL\Platforms;

use Doctrine\DBAL\Types\Types;

use function sprintf;

/**
 * Provides the behavior, features and SQL dialect of the PostgreSQL 9.2 database platform.
 *
 * @deprecated Use PostgreSQL 9.4 or newer
 */
class PostgreSQL92Platform extends PostgreSQL91Platform
{
    /**
     * {@inheritdoc}
     */
    public function getJsonTypeDeclarationSQL(array $column)
    {
        return 'JSON';
    }

    /**
     * {@inheritdoc}
     */
    public function getSmallIntTypeDeclarationSQL(array $column)
    {
        if (! empty($column['autoincrement'])) {
            return 'SMALLSERIAL';
        }

        return parent::getSmallIntTypeDeclarationSQL($column);
    }

    /**
     * {@inheritdoc}
     */
    public function hasNativeJsonType()
    {
        return true;
    }

    /**
     * {@inheritdoc}
     */
    protected function getReservedKeywordsClass()
    {
        return Keywords\PostgreSQL92Keywords::class;
    }

    /**
     * {@inheritdoc}
     */
    protected function initializeDoctrineTypeMappings()
    {
        parent::initializeDoctrineTypeMappings();

        $this->doctrineTypeMapping['json'] = Types::JSON;
    }

    /**
     * {@inheritdoc}
     */
    public function getCloseActiveDatabaseConnectionsSQL($database)
    {
        return sprintf(
            'SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = %s',
            $this->quoteStringLiteral($database)
        );
    }
}
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Platforms;

use Doctrine\DBAL\Platforms\Keywords\PostgreSQL100Keywords;

/**
 * Provides the behavior, features and SQL dialect of the PostgreSQL 10.0 database platform.
 */
class PostgreSQL100Platform extends PostgreSQL94Platform
{
    protected function getReservedKeywordsClass(): string
    {
        return PostgreSQL100Keywords::class;
    }

    /**
     * {@inheritDoc}
     */
    public function getListSequencesSQL($database): string
    {
        return 'SELECT sequence_name AS relname,
                       sequence_schema AS schemaname,
                       minimum_value AS min_value, 
                       increment AS increment_by
                FROM   information_schema.sequences
                WHERE  sequence_catalog = ' . $this->quoteStringLiteral($database) . "
                AND    sequence_schema NOT LIKE 'pg\_%'
                AND    sequence_schema != 'information_schema'";
    }
}
<?php

namespace Doctrine\DBAL\Platforms;

use Doctrine\Common\EventManager;
use Doctrine\DBAL\Event\SchemaAlterTableAddColumnEventArgs;
use Doctrine\DBAL\Event\SchemaAlterTableChangeColumnEventArgs;
use Doctrine\DBAL\Event\SchemaAlterTableEventArgs;
use Doctrine\DBAL\Event\SchemaAlterTableRemoveColumnEventArgs;
use Doctrine\DBAL\Event\SchemaAlterTableRenameColumnEventArgs;
use Doctrine\DBAL\Event\SchemaCreateTableColumnEventArgs;
use Doctrine\DBAL\Event\SchemaCreateTableEventArgs;
use Doctrine\DBAL\Event\SchemaDropTableEventArgs;
use Doctrine\DBAL\Events;
use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Platforms\Keywords\KeywordList;
use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Schema\ColumnDiff;
use Doctrine\DBAL\Schema\Constraint;
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
use Doctrine\DBAL\Schema\Identifier;
use Doctrine\DBAL\Schema\Index;
use Doctrine\DBAL\Schema\Sequence;
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Schema\TableDiff;
use Doctrine\DBAL\TransactionIsolationLevel;
use Doctrine\DBAL\Types;
use Doctrine\DBAL\Types\Type;
use Doctrine\Deprecations\Deprecation;
use InvalidArgumentException;
use UnexpectedValueException;

use function addcslashes;
use function array_map;
use function array_merge;
use function array_unique;
use function array_values;
use function assert;
use function count;
use function explode;
use function func_get_arg;
use function func_get_args;
use function func_num_args;
use function implode;
use function in_array;
use function is_array;
use function is_bool;
use function is_int;
use function is_string;
use function preg_quote;
use function preg_replace;
use function sprintf;
use function str_replace;
use function strlen;
use function strpos;
use function strtolower;
use function strtoupper;

/**
 * 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.
 *
 * @todo Remove any unnecessary methods.
 */
abstract class AbstractPlatform
{
    public const CREATE_INDEXES = 1;

    public const CREATE_FOREIGNKEYS = 2;

    /**
     * @deprecated Use DateIntervalUnit::INTERVAL_UNIT_SECOND.
     */
    public const DATE_INTERVAL_UNIT_SECOND = DateIntervalUnit::SECOND;

    /**
     * @deprecated Use DateIntervalUnit::MINUTE.
     */
    public const DATE_INTERVAL_UNIT_MINUTE = DateIntervalUnit::MINUTE;

    /**
     * @deprecated Use DateIntervalUnit::HOUR.
     */
    public const DATE_INTERVAL_UNIT_HOUR = DateIntervalUnit::HOUR;

    /**
     * @deprecated Use DateIntervalUnit::DAY.
     */
    public const DATE_INTERVAL_UNIT_DAY = DateIntervalUnit::DAY;

    /**
     * @deprecated Use DateIntervalUnit::WEEK.
     */
    public const DATE_INTERVAL_UNIT_WEEK = DateIntervalUnit::WEEK;

    /**
     * @deprecated Use DateIntervalUnit::MONTH.
     */
    public const DATE_INTERVAL_UNIT_MONTH = DateIntervalUnit::MONTH;

    /**
     * @deprecated Use DateIntervalUnit::QUARTER.
     */
    public const DATE_INTERVAL_UNIT_QUARTER = DateIntervalUnit::QUARTER;

    /**
     * @deprecated Use DateIntervalUnit::QUARTER.
     */
    public const DATE_INTERVAL_UNIT_YEAR = DateIntervalUnit::YEAR;

    /**
     * @deprecated Use TrimMode::UNSPECIFIED.
     */
    public const TRIM_UNSPECIFIED = TrimMode::UNSPECIFIED;

    /**
     * @deprecated Use TrimMode::LEADING.
     */
    public const TRIM_LEADING = TrimMode::LEADING;

    /**
     * @deprecated Use TrimMode::TRAILING.
     */
    public const TRIM_TRAILING = TrimMode::TRAILING;

    /**
     * @deprecated Use TrimMode::BOTH.
     */
    public const TRIM_BOTH = TrimMode::BOTH;

    /** @var string[]|null */
    protected $doctrineTypeMapping;

    /**
     * Contains a list of all columns that should generate parseable column comments for type-detection
     * in reverse engineering scenarios.
     *
     * @var string[]|null
     */
    protected $doctrineTypeComments;

    /** @var EventManager|null */
    protected $_eventManager;

    /**
     * Holds the KeywordList instance for the current platform.
     *
     * @var KeywordList|null
     */
    protected $_keywords;

    public function __construct()
    {
    }

    /**
     * Sets the EventManager used by the Platform.
     *
     * @return void
     */
    public function setEventManager(EventManager $eventManager)
    {
        $this->_eventManager = $eventManager;
    }

    /**
     * Gets the EventManager used by the Platform.
     *
     * @return EventManager|null
     */
    public function getEventManager()
    {
        return $this->_eventManager;
    }

    /**
     * Returns the SQL snippet that declares a boolean column.
     *
     * @param mixed[] $column
     *
     * @return string
     */
    abstract public function getBooleanTypeDeclarationSQL(array $column);

    /**
     * Returns the SQL snippet that declares a 4 byte integer column.
     *
     * @param mixed[] $column
     *
     * @return string
     */
    abstract public function getIntegerTypeDeclarationSQL(array $column);

    /**
     * Returns the SQL snippet that declares an 8 byte integer column.
     *
     * @param mixed[] $column
     *
     * @return string
     */
    abstract public function getBigIntTypeDeclarationSQL(array $column);

    /**
     * Returns the SQL snippet that declares a 2 byte integer column.
     *
     * @param mixed[] $column
     *
     * @return string
     */
    abstract public function getSmallIntTypeDeclarationSQL(array $column);

    /**
     * Returns the SQL snippet that declares common properties of an integer column.
     *
     * @param mixed[] $column
     *
     * @return string
     */
    abstract protected function _getCommonIntegerTypeDeclarationSQL(array $column);

    /**
     * Lazy load Doctrine Type Mappings.
     *
     * @return void
     */
    abstract protected function initializeDoctrineTypeMappings();

    /**
     * Initializes Doctrine Type Mappings with the platform defaults
     * and with all additional type mappings.
     *
     * @return void
     */
    private function initializeAllDoctrineTypeMappings()
    {
        $this->initializeDoctrineTypeMappings();

        foreach (Type::getTypesMap() as $typeName => $className) {
            foreach (Type::getType($typeName)->getMappedDatabaseTypes($this) as $dbType) {
                $this->doctrineTypeMapping[$dbType] = $typeName;
            }
        }
    }

    /**
     * Returns the SQL snippet used to declare a column that can
     * store characters in the ASCII character set
     *
     * @param mixed[] $column
     */
    public function getAsciiStringTypeDeclarationSQL(array $column): string
    {
        return $this->getVarcharTypeDeclarationSQL($column);
    }

    /**
     * Returns the SQL snippet used to declare a VARCHAR column type.
     *
     * @param mixed[] $column
     *
     * @return string
     */
    public function getVarcharTypeDeclarationSQL(array $column)
    {
        if (! isset($column['length'])) {
            $column['length'] = $this->getVarcharDefaultLength();
        }

        $fixed = $column['fixed'] ?? false;

        $maxLength = $fixed
            ? $this->getCharMaxLength()
            : $this->getVarcharMaxLength();

        if ($column['length'] > $maxLength) {
            return $this->getClobTypeDeclarationSQL($column);
        }

        return $this->getVarcharTypeDeclarationSQLSnippet($column['length'], $fixed);
    }

    /**
     * Returns the SQL snippet used to declare a BINARY/VARBINARY column type.
     *
     * @param mixed[] $column The column definition.
     *
     * @return string
     */
    public function getBinaryTypeDeclarationSQL(array $column)
    {
        if (! isset($column['length'])) {
            $column['length'] = $this->getBinaryDefaultLength();
        }

        $fixed = $column['fixed'] ?? false;

        $maxLength = $this->getBinaryMaxLength();

        if ($column['length'] > $maxLength) {
            if ($maxLength > 0) {
                Deprecation::trigger(
                    'doctrine/dbal',
                    'https://github.com/doctrine/dbal/issues/3187',
                    'Binary column length %d is greater than supported by the platform (%d).'
                    . ' Reduce the column length or use a BLOB column instead.',
                    $column['length'],
                    $maxLength
                );
            }

            return $this->getBlobTypeDeclarationSQL($column);
        }

        return $this->getBinaryTypeDeclarationSQLSnippet($column['length'], $fixed);
    }

    /**
     * Returns the SQL snippet to declare a GUID/UUID column.
     *
     * By default this maps directly to a CHAR(36) and only maps to more
     * special datatypes when the underlying databases support this datatype.
     *
     * @param mixed[] $column
     *
     * @return string
     */
    public function getGuidTypeDeclarationSQL(array $column)
    {
        $column['length'] = 36;
        $column['fixed']  = true;

        return $this->getVarcharTypeDeclarationSQL($column);
    }

    /**
     * Returns the SQL snippet to declare a JSON column.
     *
     * By default this maps directly to a CLOB and only maps to more
     * special datatypes when the underlying databases support this datatype.
     *
     * @param mixed[] $column
     *
     * @return string
     */
    public function getJsonTypeDeclarationSQL(array $column)
    {
        return $this->getClobTypeDeclarationSQL($column);
    }

    /**
     * @param int  $length
     * @param bool $fixed
     *
     * @return string
     *
     * @throws Exception If not supported on this platform.
     */
    protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed)
    {
        throw Exception::notSupported('VARCHARs not supported by Platform.');
    }

    /**
     * Returns the SQL snippet used to declare a BINARY/VARBINARY column type.
     *
     * @param int  $length The length of the column.
     * @param bool $fixed  Whether the column length is fixed.
     *
     * @return string
     *
     * @throws Exception If not supported on this platform.
     */
    protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed)
    {
        throw Exception::notSupported('BINARY/VARBINARY column types are not supported by this platform.');
    }

    /**
     * Returns the SQL snippet used to declare a CLOB column type.
     *
     * @param mixed[] $column
     *
     * @return string
     */
    abstract public function getClobTypeDeclarationSQL(array $column);

    /**
     * Returns the SQL Snippet used to declare a BLOB column type.
     *
     * @param mixed[] $column
     *
     * @return string
     */
    abstract public function getBlobTypeDeclarationSQL(array $column);

    /**
     * Gets the name of the platform.
     *
     * @return string
     */
    abstract public function getName();

    /**
     * Registers a doctrine type to be used in conjunction with a column type of this platform.
     *
     * @param string $dbType
     * @param string $doctrineType
     *
     * @return void
     *
     * @throws Exception If the type is not found.
     */
    public function registerDoctrineTypeMapping($dbType, $doctrineType)
    {
        if ($this->doctrineTypeMapping === null) {
            $this->initializeAllDoctrineTypeMappings();
        }

        if (! Types\Type::hasType($doctrineType)) {
            throw Exception::typeNotFound($doctrineType);
        }

        $dbType                             = strtolower($dbType);
        $this->doctrineTypeMapping[$dbType] = $doctrineType;

        $doctrineType = Type::getType($doctrineType);

        if (! $doctrineType->requiresSQLCommentHint($this)) {
            return;
        }

        $this->markDoctrineTypeCommented($doctrineType);
    }

    /**
     * Gets the Doctrine type that is mapped for the given database column type.
     *
     * @param string $dbType
     *
     * @return string
     *
     * @throws Exception
     */
    public function getDoctrineTypeMapping($dbType)
    {
        if ($this->doctrineTypeMapping === null) {
            $this->initializeAllDoctrineTypeMappings();
        }

        $dbType = strtolower($dbType);

        if (! isset($this->doctrineTypeMapping[$dbType])) {
            throw new Exception(
                'Unknown database type ' . $dbType . ' requested, ' . static::class . ' may not support it.'
            );
        }

        return $this->doctrineTypeMapping[$dbType];
    }

    /**
     * Checks if a database type is currently supported by this platform.
     *
     * @param string $dbType
     *
     * @return bool
     */
    public function hasDoctrineTypeMappingFor($dbType)
    {
        if ($this->doctrineTypeMapping === null) {
            $this->initializeAllDoctrineTypeMappings();
        }

        $dbType = strtolower($dbType);

        return isset($this->doctrineTypeMapping[$dbType]);
    }

    /**
     * Initializes the Doctrine Type comments instance variable for in_array() checks.
     *
     * @return void
     */
    protected function initializeCommentedDoctrineTypes()
    {
        $this->doctrineTypeComments = [];

        foreach (Type::getTypesMap() as $typeName => $className) {
            $type = Type::getType($typeName);

            if (! $type->requiresSQLCommentHint($this)) {
                continue;
            }

            $this->doctrineTypeComments[] = $typeName;
        }
    }

    /**
     * Is it necessary for the platform to add a parsable type comment to allow reverse engineering the given type?
     *
     * @return bool
     */
    public function isCommentedDoctrineType(Type $doctrineType)
    {
        if ($this->doctrineTypeComments === null) {
            $this->initializeCommentedDoctrineTypes();
        }

        assert(is_array($this->doctrineTypeComments));

        return in_array($doctrineType->getName(), $this->doctrineTypeComments);
    }

    /**
     * Marks this type as to be commented in ALTER TABLE and CREATE TABLE statements.
     *
     * @param string|Type $doctrineType
     *
     * @return void
     */
    public function markDoctrineTypeCommented($doctrineType)
    {
        if ($this->doctrineTypeComments === null) {
            $this->initializeCommentedDoctrineTypes();
        }

        assert(is_array($this->doctrineTypeComments));

        $this->doctrineTypeComments[] = $doctrineType instanceof Type ? $doctrineType->getName() : $doctrineType;
    }

    /**
     * Gets the comment to append to a column comment that helps parsing this type in reverse engineering.
     *
     * @return string
     */
    public function getDoctrineTypeComment(Type $doctrineType)
    {
        return '(DC2Type:' . $doctrineType->getName() . ')';
    }

    /**
     * Gets the comment of a passed column modified by potential doctrine type comment hints.
     *
     * @return string|null
     */
    protected function getColumnComment(Column $column)
    {
        $comment = $column->getComment();

        if ($this->isCommentedDoctrineType($column->getType())) {
            $comment .= $this->getDoctrineTypeComment($column->getType());
        }

        return $comment;
    }

    /**
     * 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 char column.
     */
    public function getCharMaxLength(): int
    {
        return $this->getVarcharMaxLength();
    }

    /**
     * Gets the maximum length of a varchar column.
     *
     * @return int
     */
    public function getVarcharMaxLength()
    {
        return 4000;
    }

    /**
     * Gets the default length of a varchar column.
     *
     * @return int
     */
    public function getVarcharDefaultLength()
    {
        return 255;
    }

    /**
     * Gets the maximum length of a binary column.
     *
     * @return int
     */
    public function getBinaryMaxLength()
    {
        return 4000;
    }

    /**
     * Gets the default length of a binary column.
     *
     * @return int
     */
    public function getBinaryDefaultLength()
    {
        return 255;
    }

    /**
     * Gets all SQL wildcard characters of the platform.
     *
     * @return string[]
     */
    public function getWildcards()
    {
        return ['%', '_'];
    }

    /**
     * Returns the regular expression operator.
     *
     * @return string
     *
     * @throws Exception If not supported on this platform.
     */
    public function getRegexpExpression()
    {
        throw Exception::notSupported(__METHOD__);
    }

    /**
     * Returns the global unique identifier expression.
     *
     * @deprecated Use application-generated UUIDs instead
     *
     * @return string
     *
     * @throws Exception If not supported on this platform.
     */
    public function getGuidExpression()
    {
        throw Exception::notSupported(__METHOD__);
    }

    /**
     * Returns the SQL snippet to get 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 SQL snippet to get 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|int $column The column to use.
     *
     * @return string Generated SQL including a COUNT aggregate function.
     */
    public function getCountExpression($column)
    {
        return 'COUNT(' . $column . ')';
    }

    /**
     * Returns the SQL snippet to get 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 SQL snippet to get the lowest value of a column.
     *
     * @param string $column The column to use.
     *
     * @return string Generated SQL including a MIN aggregate function.
     */
    public function getMinExpression($column)
    {
        return 'MIN(' . $column . ')';
    }

    /**
     * Returns the SQL snippet to get the total sum of a column.
     *
     * @param string $column The column to use.
     *
     * @return string Generated SQL including a SUM aggregate function.
     */
    public function getSumExpression($column)
    {
        return 'SUM(' . $column . ')';
    }

    // scalar functions

    /**
     * Returns the SQL snippet to get the md5 sum of a column.
     *
     * Note: Not SQL92, but common functionality.
     *
     * @param string $column
     *
     * @return string
     */
    public function getMd5Expression($column)
    {
        return 'MD5(' . $column . ')';
    }

    /**
     * Returns the SQL snippet to get the length of a text column.
     *
     * @param string $column
     *
     * @return string
     */
    public function getLengthExpression($column)
    {
        return 'LENGTH(' . $column . ')';
    }

    /**
     * Returns the SQL snippet to get the squared value of a column.
     *
     * @param string $column The column to use.
     *
     * @return string Generated SQL including an SQRT aggregate function.
     */
    public function getSqrtExpression($column)
    {
        return 'SQRT(' . $column . ')';
    }

    /**
     * Returns the SQL snippet to round a numeric column to the number of decimals specified.
     *
     * @param string $column
     * @param int    $decimals
     *
     * @return string
     */
    public function getRoundExpression($column, $decimals = 0)
    {
        return 'ROUND(' . $column . ', ' . $decimals . ')';
    }

    /**
     * Returns the SQL snippet to get 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 . ')';
    }

    /**
     * Returns the SQL snippet to trim a string.
     *
     * @param string      $str  The expression to apply the trim to.
     * @param int         $mode The position of the trim (leading/trailing/both).
     * @param string|bool $char The char to trim, has to be quoted already. Defaults to space.
     *
     * @return string
     */
    public function getTrimExpression($str, $mode = TrimMode::UNSPECIFIED, $char = false)
    {
        $expression = '';

        switch ($mode) {
            case TrimMode::LEADING:
                $expression = 'LEADING ';
                break;

            case TrimMode::TRAILING:
                $expression = 'TRAILING ';
                break;

            case TrimMode::BOTH:
                $expression = 'BOTH ';
                break;
        }

        if ($char !== false) {
            $expression .= $char . ' ';
        }

        if ($mode || $char !== false) {
            $expression .= 'FROM ';
        }

        return 'TRIM(' . $expression . $str . ')';
    }

    /**
     * Returns the SQL snippet to trim trailing space characters from the expression.
     *
     * @param string $str Literal string or column name.
     *
     * @return string
     */
    public function getRtrimExpression($str)
    {
        return 'RTRIM(' . $str . ')';
    }

    /**
     * Returns the SQL snippet to trim leading space characters from the expression.
     *
     * @param string $str Literal string or column name.
     *
     * @return string
     */
    public function getLtrimExpression($str)
    {
        return 'LTRIM(' . $str . ')';
    }

    /**
     * Returns the SQL snippet to change all characters from the expression 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 . ')';
    }

    /**
     * Returns the SQL snippet to change all characters from the expression 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 SQL snippet to get the position of the first occurrence of substring $substr in string $str.
     *
     * @param string    $str      Literal string.
     * @param string    $substr   Literal string to find.
     * @param int|false $startPos Position to start at, beginning of string by default.
     *
     * @return string
     *
     * @throws Exception If not supported on this platform.
     */
    public function getLocateExpression($str, $substr, $startPos = false)
    {
        throw Exception::notSupported(__METHOD__);
    }

    /**
     * Returns the SQL snippet to get the current system date.
     *
     * @return string
     */
    public function getNowExpression()
    {
        return 'NOW()';
    }

    /**
     * Returns a SQL snippet 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   $string An sql string literal or column name/alias.
     * @param int      $start  Where to start the substring portion.
     * @param int|null $length The substring portion length.
     *
     * @return string
     */
    public function getSubstringExpression($string, $start, $length = null)
    {
        if ($length === null) {
            return 'SUBSTRING(' . $string . ' FROM ' . $start . ')';
        }

        return 'SUBSTRING(' . $string . ' FROM ' . $start . ' FOR ' . $length . ')';
    }

    /**
     * Returns a SQL snippet to concatenate the given expressions.
     *
     * Accepts an arbitrary number of string parameters. Each parameter must contain an expression.
     *
     * @return string
     */
    public function getConcatExpression()
    {
        return implode(' || ', func_get_args());
    }

    /**
     * Returns the SQL for a logical not.
     *
     * Example:
     * <code>
     * $q = new Doctrine_Query();
     * $e = $q->expr;
     * $q->select('*')->from('table')
     *   ->where($e->eq('id', $e->not('null'));
     * </code>
     *
     * @param string $expression
     *
     * @return string The logical expression.
     */
    public function getNotExpression($expression)
    {
        return 'NOT(' . $expression . ')';
    }

    /**
     * Returns the SQL that checks if an expression is null.
     *
     * @param string $expression The expression that should be compared to null.
     *
     * @return string The logical expression.
     */
    public function getIsNullExpression($expression)
    {
        return $expression . ' IS NULL';
    }

    /**
     * Returns the SQL that checks if an expression is not null.
     *
     * @param string $expression The expression that should be compared to null.
     *
     * @return string The logical expression.
     */
    public function getIsNotNullExpression($expression)
    {
        return $expression . ' IS NOT NULL';
    }

    /**
     * Returns the 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 The logical expression.
     */
    public function getBetweenExpression($expression, $value1, $value2)
    {
        return $expression . ' BETWEEN ' . $value1 . ' AND ' . $value2;
    }

    /**
     * Returns the SQL to get the arccosine of a value.
     *
     * @param string $value
     *
     * @return string
     */
    public function getAcosExpression($value)
    {
        return 'ACOS(' . $value . ')';
    }

    /**
     * Returns the SQL to get the sine of a value.
     *
     * @param string $value
     *
     * @return string
     */
    public function getSinExpression($value)
    {
        return 'SIN(' . $value . ')';
    }

    /**
     * Returns the SQL to get the PI value.
     *
     * @return string
     */
    public function getPiExpression()
    {
        return 'PI()';
    }

    /**
     * Returns the SQL to get the cosine of a value.
     *
     * @param string $value
     *
     * @return string
     */
    public function getCosExpression($value)
    {
        return 'COS(' . $value . ')';
    }

    /**
     * Returns the SQL to calculate the difference in days between the two passed dates.
     *
     * Computes diff = date1 - date2.
     *
     * @param string $date1
     * @param string $date2
     *
     * @return string
     *
     * @throws Exception If not supported on this platform.
     */
    public function getDateDiffExpression($date1, $date2)
    {
        throw Exception::notSupported(__METHOD__);
    }

    /**
     * Returns the SQL to add the number of given seconds to a date.
     *
     * @param string $date
     * @param int    $seconds
     *
     * @return string
     *
     * @throws Exception If not supported on this platform.
     */
    public function getDateAddSecondsExpression($date, $seconds)
    {
        return $this->getDateArithmeticIntervalExpression($date, '+', $seconds, DateIntervalUnit::SECOND);
    }

    /**
     * Returns the SQL to subtract the number of given seconds from a date.
     *
     * @param string $date
     * @param int    $seconds
     *
     * @return string
     *
     * @throws Exception If not supported on this platform.
     */
    public function getDateSubSecondsExpression($date, $seconds)
    {
        return $this->getDateArithmeticIntervalExpression($date, '-', $seconds, DateIntervalUnit::SECOND);
    }

    /**
     * Returns the SQL to add the number of given minutes to a date.
     *
     * @param string $date
     * @param int    $minutes
     *
     * @return string
     *
     * @throws Exception If not supported on this platform.
     */
    public function getDateAddMinutesExpression($date, $minutes)
    {
        return $this->getDateArithmeticIntervalExpression($date, '+', $minutes, DateIntervalUnit::MINUTE);
    }

    /**
     * Returns the SQL to subtract the number of given minutes from a date.
     *
     * @param string $date
     * @param int    $minutes
     *
     * @return string
     *
     * @throws Exception If not supported on this platform.
     */
    public function getDateSubMinutesExpression($date, $minutes)
    {
        return $this->getDateArithmeticIntervalExpression($date, '-', $minutes, DateIntervalUnit::MINUTE);
    }

    /**
     * Returns the SQL to add the number of given hours to a date.
     *
     * @param string $date
     * @param int    $hours
     *
     * @return string
     *
     * @throws Exception If not supported on this platform.
     */
    public function getDateAddHourExpression($date, $hours)
    {
        return $this->getDateArithmeticIntervalExpression($date, '+', $hours, DateIntervalUnit::HOUR);
    }

    /**
     * Returns the SQL to subtract the number of given hours to a date.
     *
     * @param string $date
     * @param int    $hours
     *
     * @return string
     *
     * @throws Exception If not supported on this platform.
     */
    public function getDateSubHourExpression($date, $hours)
    {
        return $this->getDateArithmeticIntervalExpression($date, '-', $hours, DateIntervalUnit::HOUR);
    }

    /**
     * Returns the SQL to add the number of given days to a date.
     *
     * @param string $date
     * @param int    $days
     *
     * @return string
     *
     * @throws Exception If not supported on this platform.
     */
    public function getDateAddDaysExpression($date, $days)
    {
        return $this->getDateArithmeticIntervalExpression($date, '+', $days, DateIntervalUnit::DAY);
    }

    /**
     * Returns the SQL to subtract the number of given days to a date.
     *
     * @param string $date
     * @param int    $days
     *
     * @return string
     *
     * @throws Exception If not supported on this platform.
     */
    public function getDateSubDaysExpression($date, $days)
    {
        return $this->getDateArithmeticIntervalExpression($date, '-', $days, DateIntervalUnit::DAY);
    }

    /**
     * Returns the SQL to add the number of given weeks to a date.
     *
     * @param string $date
     * @param int    $weeks
     *
     * @return string
     *
     * @throws Exception If not supported on this platform.
     */
    public function getDateAddWeeksExpression($date, $weeks)
    {
        return $this->getDateArithmeticIntervalExpression($date, '+', $weeks, DateIntervalUnit::WEEK);
    }

    /**
     * Returns the SQL to subtract the number of given weeks from a date.
     *
     * @param string $date
     * @param int    $weeks
     *
     * @return string
     *
     * @throws Exception If not supported on this platform.
     */
    public function getDateSubWeeksExpression($date, $weeks)
    {
        return $this->getDateArithmeticIntervalExpression($date, '-', $weeks, DateIntervalUnit::WEEK);
    }

    /**
     * Returns the SQL to add the number of given months to a date.
     *
     * @param string $date
     * @param int    $months
     *
     * @return string
     *
     * @throws Exception If not supported on this platform.
     */
    public function getDateAddMonthExpression($date, $months)
    {
        return $this->getDateArithmeticIntervalExpression($date, '+', $months, DateIntervalUnit::MONTH);
    }

    /**
     * Returns the SQL to subtract the number of given months to a date.
     *
     * @param string $date
     * @param int    $months
     *
     * @return string
     *
     * @throws Exception If not supported on this platform.
     */
    public function getDateSubMonthExpression($date, $months)
    {
        return $this->getDateArithmeticIntervalExpression($date, '-', $months, DateIntervalUnit::MONTH);
    }

    /**
     * Returns the SQL to add the number of given quarters to a date.
     *
     * @param string $date
     * @param int    $quarters
     *
     * @return string
     *
     * @throws Exception If not supported on this platform.
     */
    public function getDateAddQuartersExpression($date, $quarters)
    {
        return $this->getDateArithmeticIntervalExpression($date, '+', $quarters, DateIntervalUnit::QUARTER);
    }

    /**
     * Returns the SQL to subtract the number of given quarters from a date.
     *
     * @param string $date
     * @param int    $quarters
     *
     * @return string
     *
     * @throws Exception If not supported on this platform.
     */
    public function getDateSubQuartersExpression($date, $quarters)
    {
        return $this->getDateArithmeticIntervalExpression($date, '-', $quarters, DateIntervalUnit::QUARTER);
    }

    /**
     * Returns the SQL to add the number of given years to a date.
     *
     * @param string $date
     * @param int    $years
     *
     * @return string
     *
     * @throws Exception If not supported on this platform.
     */
    public function getDateAddYearsExpression($date, $years)
    {
        return $this->getDateArithmeticIntervalExpression($date, '+', $years, DateIntervalUnit::YEAR);
    }

    /**
     * Returns the SQL to subtract the number of given years from a date.
     *
     * @param string $date
     * @param int    $years
     *
     * @return string
     *
     * @throws Exception If not supported on this platform.
     */
    public function getDateSubYearsExpression($date, $years)
    {
        return $this->getDateArithmeticIntervalExpression($date, '-', $years, DateIntervalUnit::YEAR);
    }

    /**
     * Returns the SQL for a date arithmetic expression.
     *
     * @param string $date     The column or literal representing a date to perform the arithmetic operation on.
     * @param string $operator The arithmetic operator (+ or -).
     * @param int    $interval The interval that shall be calculated into the date.
     * @param string $unit     The unit of the interval that shall be calculated into the date.
     *                         One of the DATE_INTERVAL_UNIT_* constants.
     *
     * @return string
     *
     * @throws Exception If not supported on this platform.
     */
    protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit)
    {
        throw Exception::notSupported(__METHOD__);
    }

    /**
     * Returns the SQL bit AND comparison expression.
     *
     * @param string $value1
     * @param string $value2
     *
     * @return string
     */
    public function getBitAndComparisonExpression($value1, $value2)
    {
        return '(' . $value1 . ' & ' . $value2 . ')';
    }

    /**
     * Returns the SQL bit OR comparison expression.
     *
     * @param string $value1
     * @param string $value2
     *
     * @return string
     */
    public function getBitOrComparisonExpression($value1, $value2)
    {
        return '(' . $value1 . ' | ' . $value2 . ')';
    }

    /**
     * Returns the FOR UPDATE expression.
     *
     * @return string
     */
    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 The FROM clause to append the hint for the given lock mode to.
     * @param int|null $lockMode   One of the Doctrine\DBAL\LockMode::* constants. If null is given, nothing will
     *                             be appended to the FROM clause.
     *
     * @return string
     */
    public function appendLockHint($fromClause, $lockMode)
    {
        return $fromClause;
    }

    /**
     * Returns the SQL snippet to append to any SELECT statement which locks rows in shared read lock.
     *
     * This defaults to the ANSI 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();
    }

    /**
     * Returns 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 ANSI SQL standard.
     *
     * @return string
     */
    public function getWriteLockSQL()
    {
        return $this->getForUpdateSQL();
    }

    /**
     * Returns the SQL snippet to drop an existing database.
     *
     * @param string $name The name of the database that should be dropped.
     *
     * @return string
     */
    public function getDropDatabaseSQL($name)
    {
        return 'DROP DATABASE ' . $name;
    }

    /**
     * Returns the SQL snippet to drop an existing table.
     *
     * @param Table|string $table
     *
     * @return string
     *
     * @throws InvalidArgumentException
     */
    public function getDropTableSQL($table)
    {
        $tableArg = $table;

        if ($table instanceof Table) {
            $table = $table->getQuotedName($this);
        }

        if (! is_string($table)) {
            throw new InvalidArgumentException(
                __METHOD__ . '() expects $table parameter to be string or ' . Table::class . '.'
            );
        }

        if ($this->_eventManager !== null && $this->_eventManager->hasListeners(Events::onSchemaDropTable)) {
            $eventArgs = new SchemaDropTableEventArgs($tableArg, $this);
            $this->_eventManager->dispatchEvent(Events::onSchemaDropTable, $eventArgs);

            if ($eventArgs->isDefaultPrevented()) {
                $sql = $eventArgs->getSql();

                if ($sql === null) {
                    throw new UnexpectedValueException('Default implementation of DROP TABLE was overridden with NULL');
                }

                return $sql;
            }
        }

        return 'DROP TABLE ' . $table;
    }

    /**
     * Returns the SQL to safely drop a temporary table WITHOUT implicitly committing an open transaction.
     *
     * @param Table|string $table
     *
     * @return string
     */
    public function getDropTemporaryTableSQL($table)
    {
        return $this->getDropTableSQL($table);
    }

    /**
     * Returns the SQL to drop an index from a table.
     *
     * @param Index|string $index
     * @param Table|string $table
     *
     * @return string
     *
     * @throws InvalidArgumentException
     */
    public function getDropIndexSQL($index, $table = null)
    {
        if ($index instanceof Index) {
            $index = $index->getQuotedName($this);
        } elseif (! is_string($index)) {
            throw new InvalidArgumentException(
                __METHOD__ . '() expects $index parameter to be string or ' . Index::class . '.'
            );
        }

        return 'DROP INDEX ' . $index;
    }

    /**
     * Returns the SQL to drop a constraint.
     *
     * @param Constraint|string $constraint
     * @param Table|string      $table
     *
     * @return string
     */
    public function getDropConstraintSQL($constraint, $table)
    {
        if (! $constraint instanceof Constraint) {
            $constraint = new Identifier($constraint);
        }

        if (! $table instanceof Table) {
            $table = new Identifier($table);
        }

        $constraint = $constraint->getQuotedName($this);
        $table      = $table->getQuotedName($this);

        return 'ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $constraint;
    }

    /**
     * Returns the SQL to drop a foreign key.
     *
     * @param ForeignKeyConstraint|string $foreignKey
     * @param Table|string                $table
     *
     * @return string
     */
    public function getDropForeignKeySQL($foreignKey, $table)
    {
        if (! $foreignKey instanceof ForeignKeyConstraint) {
            $foreignKey = new Identifier($foreignKey);
        }

        if (! $table instanceof Table) {
            $table = new Identifier($table);
        }

        $foreignKey = $foreignKey->getQuotedName($this);
        $table      = $table->getQuotedName($this);

        return 'ALTER TABLE ' . $table . ' DROP FOREIGN KEY ' . $foreignKey;
    }

    /**
     * Returns the SQL statement(s) to create a table with the specified name, columns and constraints
     * on this platform.
     *
     * @param int $createFlags
     *
     * @return string[] The sequence of SQL statements.
     *
     * @throws Exception
     * @throws InvalidArgumentException
     */
    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 Exception::noColumnsSpecifiedForTable($table->getName());
        }

        $tableName                    = $table->getQuotedName($this);
        $options                      = $table->getOptions();
        $options['uniqueConstraints'] = [];
        $options['indexes']           = [];
        $options['primary']           = [];

        if (($createFlags & self::CREATE_INDEXES) > 0) {
            foreach ($table->getIndexes() as $index) {
                if ($index->isPrimary()) {
                    $options['primary']       = $index->getQuotedColumns($this);
                    $options['primary_index'] = $index;
                } else {
                    $options['indexes'][$index->getQuotedName($this)] = $index;
                }
            }
        }

        $columnSql = [];
        $columns   = [];

        foreach ($table->getColumns() as $column) {
            if (
                $this->_eventManager !== null
                && $this->_eventManager->hasListeners(Events::onSchemaCreateTableColumn)
            ) {
                $eventArgs = new SchemaCreateTableColumnEventArgs($column, $table, $this);
                $this->_eventManager->dispatchEvent(Events::onSchemaCreateTableColumn, $eventArgs);

                $columnSql = array_merge($columnSql, $eventArgs->getSql());

                if ($eventArgs->isDefaultPrevented()) {
                    continue;
                }
            }

            $name = $column->getQuotedName($this);

            $columnData = array_merge($column->toArray(), [
                'name' => $name,
                'version' => $column->hasPlatformOption('version') ? $column->getPlatformOption('version') : false,
                'comment' => $this->getColumnComment($column),
            ]);

            if ($columnData['type'] instanceof Types\StringType && $columnData['length'] === null) {
                $columnData['length'] = 255;
            }

            if (in_array($column->getName(), $options['primary'])) {
                $columnData['primary'] = true;
            }

            $columns[$name] = $columnData;
        }

        if (($createFlags & self::CREATE_FOREIGNKEYS) > 0) {
            $options['foreignKeys'] = [];
            foreach ($table->getForeignKeys() as $fkConstraint) {
                $options['foreignKeys'][] = $fkConstraint;
            }
        }

        if ($this->_eventManager !== null && $this->_eventManager->hasListeners(Events::onSchemaCreateTable)) {
            $eventArgs = new SchemaCreateTableEventArgs($table, $columns, $options, $this);
            $this->_eventManager->dispatchEvent(Events::onSchemaCreateTable, $eventArgs);

            if ($eventArgs->isDefaultPrevented()) {
                return array_merge($eventArgs->getSql(), $columnSql);
            }
        }

        $sql = $this->_getCreateTableSQL($tableName, $columns, $options);
        if ($this->supportsCommentOnStatement()) {
            if ($table->hasOption('comment')) {
                $sql[] = $this->getCommentOnTableSQL($tableName, $table->getOption('comment'));
            }

            foreach ($table->getColumns() as $column) {
                $comment = $this->getColumnComment($column);

                if ($comment === null || $comment === '') {
                    continue;
                }

                $sql[] = $this->getCommentOnColumnSQL($tableName, $column->getQuotedName($this), $comment);
            }
        }

        return array_merge($sql, $columnSql);
    }

    protected function getCommentOnTableSQL(string $tableName, ?string $comment): string
    {
        $tableName = new Identifier($tableName);

        return sprintf(
            'COMMENT ON TABLE %s IS %s',
            $tableName->getQuotedName($this),
            $this->quoteStringLiteral((string) $comment)
        );
    }

    /**
     * @param string      $tableName
     * @param string      $columnName
     * @param string|null $comment
     *
     * @return string
     */
    public function getCommentOnColumnSQL($tableName, $columnName, $comment)
    {
        $tableName  = new Identifier($tableName);
        $columnName = new Identifier($columnName);

        return sprintf(
            'COMMENT ON COLUMN %s.%s IS %s',
            $tableName->getQuotedName($this),
            $columnName->getQuotedName($this),
            $this->quoteStringLiteral((string) $comment)
        );
    }

    /**
     * Returns the SQL to create inline comment on a column.
     *
     * @param string $comment
     *
     * @return string
     *
     * @throws Exception If not supported on this platform.
     */
    public function getInlineColumnCommentSQL($comment)
    {
        if (! $this->supportsInlineColumnComments()) {
            throw Exception::notSupported(__METHOD__);
        }

        return 'COMMENT ' . $this->quoteStringLiteral($comment);
    }

    /**
     * Returns the SQL used to create a table.
     *
     * @param string    $name
     * @param mixed[][] $columns
     * @param mixed[]   $options
     *
     * @return string[]
     */
    protected function _getCreateTableSQL($name, array $columns, array $options = [])
    {
        $columnListSql = $this->getColumnDeclarationListSQL($columns);

        if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) {
            foreach ($options['uniqueConstraints'] as $index => $definition) {
                $columnListSql .= ', ' . $this->getUniqueConstraintDeclarationSQL($index, $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 ' . $name . ' (' . $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, $name);
            }
        }

        return $sql;
    }

    /**
     * @return string
     */
    public function getCreateTemporaryTableSnippetSQL()
    {
        return 'CREATE TEMPORARY TABLE';
    }

    /**
     * Returns the SQL to create a sequence on this platform.
     *
     * @return string
     *
     * @throws Exception If not supported on this platform.
     */
    public function getCreateSequenceSQL(Sequence $sequence)
    {
        throw Exception::notSupported(__METHOD__);
    }

    /**
     * Returns the SQL to change a sequence on this platform.
     *
     * @return string
     *
     * @throws Exception If not supported on this platform.
     */
    public function getAlterSequenceSQL(Sequence $sequence)
    {
        throw Exception::notSupported(__METHOD__);
    }

    /**
     * Returns the SQL to create a constraint on a table on this platform.
     *
     * @param Table|string $table
     *
     * @return string
     *
     * @throws InvalidArgumentException
     */
    public function getCreateConstraintSQL(Constraint $constraint, $table)
    {
        if ($table instanceof Table) {
            $table = $table->getQuotedName($this);
        }

        $query = 'ALTER TABLE ' . $table . ' ADD CONSTRAINT ' . $constraint->getQuotedName($this);

        $columnList = '(' . implode(', ', $constraint->getQuotedColumns($this)) . ')';

        $referencesClause = '';
        if ($constraint instanceof 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().'
                );
            }
        } elseif ($constraint instanceof ForeignKeyConstraint) {
            $query .= ' FOREIGN KEY';

            $referencesClause = ' REFERENCES ' . $constraint->getQuotedForeignTableName($this) .
                ' (' . implode(', ', $constraint->getQuotedForeignColumns($this)) . ')';
        }

        $query .= ' ' . $columnList . $referencesClause;

        return $query;
    }

    /**
     * Returns the SQL to create an index on a table on this platform.
     *
     * @param Table|string $table The name of the table on which the index is to be created.
     *
     * @return string
     *
     * @throws InvalidArgumentException
     */
    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.");
        }

        if ($index->isPrimary()) {
            return $this->getCreatePrimaryKeySQL($index, $table);
        }

        $query  = 'CREATE ' . $this->getCreateIndexSQLFlags($index) . 'INDEX ' . $name . ' ON ' . $table;
        $query .= ' (' . $this->getIndexFieldDeclarationListSQL($index) . ')' . $this->getPartialIndexSQL($index);

        return $query;
    }

    /**
     * Adds condition for partial index.
     *
     * @return string
     */
    protected function getPartialIndexSQL(Index $index)
    {
        if ($this->supportsPartialIndexes() && $index->hasOption('where')) {
            return ' WHERE ' . $index->getOption('where');
        }

        return '';
    }

    /**
     * Adds additional flags for index generation.
     *
     * @return string
     */
    protected function getCreateIndexSQLFlags(Index $index)
    {
        return $index->isUnique() ? 'UNIQUE ' : '';
    }

    /**
     * Returns the SQL to create an unnamed primary key constraint.
     *
     * @param Table|string $table
     *
     * @return string
     */
    public function getCreatePrimaryKeySQL(Index $index, $table)
    {
        if ($table instanceof Table) {
            $table = $table->getQuotedName($this);
        }

        return 'ALTER TABLE ' . $table . ' ADD PRIMARY KEY (' . $this->getIndexFieldDeclarationListSQL($index) . ')';
    }

    /**
     * Returns the SQL to create a named schema.
     *
     * @param string $schemaName
     *
     * @return string
     *
     * @throws Exception If not supported on this platform.
     */
    public function getCreateSchemaSQL($schemaName)
    {
        throw Exception::notSupported(__METHOD__);
    }

    /**
     * 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. This also detects identifier
     * chains separated by dot and quotes them independently.
     *
     * 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 The identifier name to be quoted.
     *
     * @return string The quoted identifier string.
     */
    public function quoteIdentifier($str)
    {
        if (strpos($str, '.') !== false) {
            $parts = array_map([$this, 'quoteSingleIdentifier'], explode('.', $str));

            return implode('.', $parts);
        }

        return $this->quoteSingleIdentifier($str);
    }

    /**
     * Quotes a single identifier (no dot chain separation).
     *
     * @param string $str The identifier name to be quoted.
     *
     * @return string The quoted identifier string.
     */
    public function quoteSingleIdentifier($str)
    {
        $c = $this->getIdentifierQuoteCharacter();

        return $c . str_replace($c, $c . $c, $str) . $c;
    }

    /**
     * Returns the SQL to create a new foreign key.
     *
     * @param ForeignKeyConstraint $foreignKey The foreign key constraint.
     * @param Table|string         $table      The name of the table on which the foreign key is to be created.
     *
     * @return string
     */
    public function getCreateForeignKeySQL(ForeignKeyConstraint $foreignKey, $table)
    {
        if ($table instanceof Table) {
            $table = $table->getQuotedName($this);
        }

        return 'ALTER TABLE ' . $table . ' ADD ' . $this->getForeignKeyDeclarationSQL($foreignKey);
    }

    /**
     * Gets the SQL statements for altering an existing table.
     *
     * This method returns an array of SQL statements, since some platforms need several statements.
     *
     * @return string[]
     *
     * @throws Exception If not supported on this platform.
     */
    public function getAlterTableSQL(TableDiff $diff)
    {
        throw Exception::notSupported(__METHOD__);
    }

    /**
     * @param mixed[] $columnSql
     *
     * @return bool
     */
    protected function onSchemaAlterTableAddColumn(Column $column, TableDiff $diff, &$columnSql)
    {
        if ($this->_eventManager === null) {
            return false;
        }

        if (! $this->_eventManager->hasListeners(Events::onSchemaAlterTableAddColumn)) {
            return false;
        }

        $eventArgs = new SchemaAlterTableAddColumnEventArgs($column, $diff, $this);
        $this->_eventManager->dispatchEvent(Events::onSchemaAlterTableAddColumn, $eventArgs);

        $columnSql = array_merge($columnSql, $eventArgs->getSql());

        return $eventArgs->isDefaultPrevented();
    }

    /**
     * @param string[] $columnSql
     *
     * @return bool
     */
    protected function onSchemaAlterTableRemoveColumn(Column $column, TableDiff $diff, &$columnSql)
    {
        if ($this->_eventManager === null) {
            return false;
        }

        if (! $this->_eventManager->hasListeners(Events::onSchemaAlterTableRemoveColumn)) {
            return false;
        }

        $eventArgs = new SchemaAlterTableRemoveColumnEventArgs($column, $diff, $this);
        $this->_eventManager->dispatchEvent(Events::onSchemaAlterTableRemoveColumn, $eventArgs);

        $columnSql = array_merge($columnSql, $eventArgs->getSql());

        return $eventArgs->isDefaultPrevented();
    }

    /**
     * @param string[] $columnSql
     *
     * @return bool
     */
    protected function onSchemaAlterTableChangeColumn(ColumnDiff $columnDiff, TableDiff $diff, &$columnSql)
    {
        if ($this->_eventManager === null) {
            return false;
        }

        if (! $this->_eventManager->hasListeners(Events::onSchemaAlterTableChangeColumn)) {
            return false;
        }

        $eventArgs = new SchemaAlterTableChangeColumnEventArgs($columnDiff, $diff, $this);
        $this->_eventManager->dispatchEvent(Events::onSchemaAlterTableChangeColumn, $eventArgs);

        $columnSql = array_merge($columnSql, $eventArgs->getSql());

        return $eventArgs->isDefaultPrevented();
    }

    /**
     * @param string   $oldColumnName
     * @param string[] $columnSql
     *
     * @return bool
     */
    protected function onSchemaAlterTableRenameColumn($oldColumnName, Column $column, TableDiff $diff, &$columnSql)
    {
        if ($this->_eventManager === null) {
            return false;
        }

        if (! $this->_eventManager->hasListeners(Events::onSchemaAlterTableRenameColumn)) {
            return false;
        }

        $eventArgs = new SchemaAlterTableRenameColumnEventArgs($oldColumnName, $column, $diff, $this);
        $this->_eventManager->dispatchEvent(Events::onSchemaAlterTableRenameColumn, $eventArgs);

        $columnSql = array_merge($columnSql, $eventArgs->getSql());

        return $eventArgs->isDefaultPrevented();
    }

    /**
     * @param string[] $sql
     *
     * @return bool
     */
    protected function onSchemaAlterTable(TableDiff $diff, &$sql)
    {
        if ($this->_eventManager === null) {
            return false;
        }

        if (! $this->_eventManager->hasListeners(Events::onSchemaAlterTable)) {
            return false;
        }

        $eventArgs = new SchemaAlterTableEventArgs($diff, $this);
        $this->_eventManager->dispatchEvent(Events::onSchemaAlterTable, $eventArgs);

        $sql = array_merge($sql, $eventArgs->getSql());

        return $eventArgs->isDefaultPrevented();
    }

    /**
     * @return string[]
     */
    protected function getPreAlterTableIndexForeignKeySQL(TableDiff $diff)
    {
        $tableName = $diff->getName($this)->getQuotedName($this);

        $sql = [];
        if ($this->supportsForeignKeyConstraints()) {
            foreach ($diff->removedForeignKeys as $foreignKey) {
                $sql[] = $this->getDropForeignKeySQL($foreignKey, $tableName);
            }

            foreach ($diff->changedForeignKeys as $foreignKey) {
                $sql[] = $this->getDropForeignKeySQL($foreignKey, $tableName);
            }
        }

        foreach ($diff->removedIndexes as $index) {
            $sql[] = $this->getDropIndexSQL($index, $tableName);
        }

        foreach ($diff->changedIndexes as $index) {
            $sql[] = $this->getDropIndexSQL($index, $tableName);
        }

        return $sql;
    }

    /**
     * @return string[]
     */
    protected function getPostAlterTableIndexForeignKeySQL(TableDiff $diff)
    {
        $sql     = [];
        $newName = $diff->getNewName();

        if ($newName !== false) {
            $tableName = $newName->getQuotedName($this);
        } else {
            $tableName = $diff->getName($this)->getQuotedName($this);
        }

        if ($this->supportsForeignKeyConstraints()) {
            foreach ($diff->addedForeignKeys as $foreignKey) {
                $sql[] = $this->getCreateForeignKeySQL($foreignKey, $tableName);
            }

            foreach ($diff->changedForeignKeys as $foreignKey) {
                $sql[] = $this->getCreateForeignKeySQL($foreignKey, $tableName);
            }
        }

        foreach ($diff->addedIndexes as $index) {
            $sql[] = $this->getCreateIndexSQL($index, $tableName);
        }

        foreach ($diff->changedIndexes as $index) {
            $sql[] = $this->getCreateIndexSQL($index, $tableName);
        }

        foreach ($diff->renamedIndexes as $oldIndexName => $index) {
            $oldIndexName = new Identifier($oldIndexName);
            $sql          = array_merge(
                $sql,
                $this->getRenameIndexSQL($oldIndexName->getQuotedName($this), $index, $tableName)
            );
        }

        return $sql;
    }

    /**
     * Returns the SQL for renaming an index on a table.
     *
     * @param string $oldIndexName The name of the index to rename from.
     * @param Index  $index        The definition of the index to rename to.
     * @param string $tableName    The table to rename the given index on.
     *
     * @return string[] The sequence of SQL statements for renaming the given index.
     */
    protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName)
    {
        return [
            $this->getDropIndexSQL($oldIndexName, $tableName),
            $this->getCreateIndexSQL($index, $tableName),
        ];
    }

    /**
     * Common code for alter table statement generation that updates the changed Index and Foreign Key definitions.
     *
     * @deprecated
     *
     * @return string[]
     */
    protected function _getAlterTableIndexForeignKeySQL(TableDiff $diff)
    {
        return array_merge(
            $this->getPreAlterTableIndexForeignKeySQL($diff),
            $this->getPostAlterTableIndexForeignKeySQL($diff)
        );
    }

    /**
     * Gets declaration of a number of columns in bulk.
     *
     * @param mixed[][] $columns A multidimensional associative array.
     *                           The first dimension determines the column name, while the second
     *                           dimension is keyed with the name of the properties
     *                           of the column being declared as array indexes. Currently, the types
     *                           of supported column properties are as follows:
     *
     *      length
     *          Integer value that determines the maximum length of the text
     *          column. If this argument is missing the column should be
     *          declared to have the longest length allowed by the DBMS.
     *
     *      default
     *          Text value to be used as default for this column.
     *
     *      notnull
     *          Boolean flag that indicates whether this column is constrained
     *          to not be set to null.
     *      charset
     *          Text value with the default CHARACTER SET for this column.
     *      collation
     *          Text value with the default COLLATION for this column.
     *      unique
     *          unique constraint
     *
     * @return string
     */
    public function getColumnDeclarationListSQL(array $columns)
    {
        $declarations = [];

        foreach ($columns as $name => $column) {
            $declarations[] = $this->getColumnDeclarationSQL($name, $column);
        }

        return implode(', ', $declarations);
    }

    /**
     * Obtains DBMS specific SQL code portion needed to declare a generic type
     * column to be used in statements like CREATE TABLE.
     *
     * @param string  $name   The name the column to be declared.
     * @param mixed[] $column An associative array with the name of the properties
     *                        of the column being declared as array indexes. Currently, the types
     *                        of supported column properties are as follows:
     *
     *      length
     *          Integer value that determines the maximum length of the text
     *          column. If this argument is missing the column should be
     *          declared to have the longest length allowed by the DBMS.
     *
     *      default
     *          Text value to be used as default for this column.
     *
     *      notnull
     *          Boolean flag that indicates whether this column is constrained
     *          to not be set to null.
     *      charset
     *          Text value with the default CHARACTER SET for this column.
     *      collation
     *          Text value with the default COLLATION for this column.
     *      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 $column)
    {
        if (isset($column['columnDefinition'])) {
            $declaration = $this->getCustomTypeDeclarationSQL($column);
        } else {
            $default = $this->getDefaultValueDeclarationSQL($column);

            $charset = isset($column['charset']) && $column['charset'] ?
                ' ' . $this->getColumnCharsetDeclarationSQL($column['charset']) : '';

            $collation = isset($column['collation']) && $column['collation'] ?
                ' ' . $this->getColumnCollationDeclarationSQL($column['collation']) : '';

            $notnull = isset($column['notnull']) && $column['notnull'] ? ' NOT NULL' : '';

            $unique = isset($column['unique']) && $column['unique'] ?
                ' ' . $this->getUniqueFieldDeclarationSQL() : '';

            $check = isset($column['check']) && $column['check'] ?
                ' ' . $column['check'] : '';

            $typeDecl    = $column['type']->getSQLDeclaration($column, $this);
            $declaration = $typeDecl . $charset . $default . $notnull . $unique . $check . $collation;

            if ($this->supportsInlineColumnComments() && isset($column['comment']) && $column['comment'] !== '') {
                $declaration .= ' ' . $this->getInlineColumnCommentSQL($column['comment']);
            }
        }

        return $name . ' ' . $declaration;
    }

    /**
     * Returns the SQL snippet that declares a floating point column of arbitrary precision.
     *
     * @param mixed[] $column
     *
     * @return string
     */
    public function getDecimalTypeDeclarationSQL(array $column)
    {
        $column['precision'] = ! isset($column['precision']) || empty($column['precision'])
            ? 10 : $column['precision'];
        $column['scale']     = ! isset($column['scale']) || empty($column['scale'])
            ? 0 : $column['scale'];

        return 'NUMERIC(' . $column['precision'] . ', ' . $column['scale'] . ')';
    }

    /**
     * Obtains DBMS specific SQL code portion needed to set a default value
     * declaration to be used in statements like CREATE TABLE.
     *
     * @param mixed[] $column The column definition array.
     *
     * @return string DBMS specific SQL code portion needed to set a default value.
     */
    public function getDefaultValueDeclarationSQL($column)
    {
        if (! isset($column['default'])) {
            return empty($column['notnull']) ? ' DEFAULT NULL' : '';
        }

        $default = $column['default'];

        if (! isset($column['type'])) {
            return " DEFAULT '" . $default . "'";
        }

        $type = $column['type'];

        if ($type instanceof Types\PhpIntegerMappingType) {
            return ' DEFAULT ' . $default;
        }

        if ($type instanceof Types\PhpDateTimeMappingType && $default === $this->getCurrentTimestampSQL()) {
            return ' DEFAULT ' . $this->getCurrentTimestampSQL();
        }

        if ($type instanceof Types\TimeType && $default === $this->getCurrentTimeSQL()) {
            return ' DEFAULT ' . $this->getCurrentTimeSQL();
        }

        if ($type instanceof Types\DateType && $default === $this->getCurrentDateSQL()) {
            return ' DEFAULT ' . $this->getCurrentDateSQL();
        }

        if ($type instanceof Types\BooleanType) {
            return " DEFAULT '" . $this->convertBooleans($default) . "'";
        }

        return ' DEFAULT ' . $this->quoteStringLiteral($default);
    }

    /**
     * Obtains DBMS specific SQL code portion needed to set a CHECK constraint
     * declaration to be used in statements like CREATE TABLE.
     *
     * @param string[]|mixed[][] $definition The check definition.
     *
     * @return string DBMS specific SQL code portion needed to set a CHECK constraint.
     */
    public function getCheckDeclarationSQL(array $definition)
    {
        $constraints = [];
        foreach ($definition as $column => $def) {
            if (is_string($def)) {
                $constraints[] = 'CHECK (' . $def . ')';
            } else {
                if (isset($def['min'])) {
                    $constraints[] = 'CHECK (' . $column . ' >= ' . $def['min'] . ')';
                }

                if (isset($def['max'])) {
                    $constraints[] = 'CHECK (' . $column . ' <= ' . $def['max'] . ')';
                }
            }
        }

        return implode(', ', $constraints);
    }

    /**
     * Obtains DBMS specific SQL code portion needed to set a unique
     * constraint declaration to be used in statements like CREATE TABLE.
     *
     * @param string $name  The name of the unique constraint.
     * @param Index  $index The index definition.
     *
     * @return string DBMS specific SQL code portion needed to set a constraint.
     *
     * @throws InvalidArgumentException
     */
    public function getUniqueConstraintDeclarationSQL($name, Index $index)
    {
        $columns = $index->getColumns();
        $name    = new Identifier($name);

        if (count($columns) === 0) {
            throw new InvalidArgumentException("Incomplete definition. 'columns' required.");
        }

        return 'CONSTRAINT ' . $name->getQuotedName($this) . ' UNIQUE ('
            . $this->getIndexFieldDeclarationListSQL($index)
            . ')' . $this->getPartialIndexSQL($index);
    }

    /**
     * Obtains DBMS specific SQL code portion needed to set an index
     * declaration to be used in statements like CREATE TABLE.
     *
     * @param string $name  The name of the index.
     * @param Index  $index The index definition.
     *
     * @return string DBMS specific SQL code portion needed to set an index.
     *
     * @throws InvalidArgumentException
     */
    public function getIndexDeclarationSQL($name, Index $index)
    {
        $columns = $index->getColumns();
        $name    = new Identifier($name);

        if (count($columns) === 0) {
            throw new InvalidArgumentException("Incomplete definition. 'columns' required.");
        }

        return $this->getCreateIndexSQLFlags($index) . 'INDEX ' . $name->getQuotedName($this) . ' ('
            . $this->getIndexFieldDeclarationListSQL($index)
            . ')' . $this->getPartialIndexSQL($index);
    }

    /**
     * Obtains SQL code portion needed to create a custom column,
     * e.g. when a column has the "columnDefinition" keyword.
     * Only "AUTOINCREMENT" and "PRIMARY KEY" are added if appropriate.
     *
     * @param mixed[] $column
     *
     * @return string
     */
    public function getCustomTypeDeclarationSQL(array $column)
    {
        return $column['columnDefinition'];
    }

    /**
     * Obtains DBMS specific SQL code portion needed to set an index
     * declaration to be used in statements like CREATE TABLE.
     *
     * @param mixed[]|Index $columnsOrIndex array declaration is deprecated, prefer passing Index to this method
     */
    public function getIndexFieldDeclarationListSQL($columnsOrIndex): string
    {
        if ($columnsOrIndex instanceof Index) {
            return implode(', ', $columnsOrIndex->getQuotedColumns($this));
        }

        if (! is_array($columnsOrIndex)) {
            throw new InvalidArgumentException('Fields argument should be an Index or array.');
        }

        $ret = [];

        foreach ($columnsOrIndex as $column => $definition) {
            if (is_array($definition)) {
                $ret[] = $column;
            } else {
                $ret[] = $definition;
            }
        }

        return implode(', ', $ret);
    }

    /**
     * Returns 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;
    }

    /**
     * Obtain DBMS specific SQL code portion needed to set the FOREIGN KEY constraint
     * of a column declaration to be used in statements like CREATE TABLE.
     *
     * @return string DBMS specific SQL code portion needed to set the FOREIGN KEY constraint
     *                of a column declaration.
     */
    public function getForeignKeyDeclarationSQL(ForeignKeyConstraint $foreignKey)
    {
        $sql  = $this->getForeignKeyBaseDeclarationSQL($foreignKey);
        $sql .= $this->getAdvancedForeignKeyOptionsSQL($foreignKey);

        return $sql;
    }

    /**
     * Returns the FOREIGN KEY query section dealing with non-standard options
     * as MATCH, INITIALLY DEFERRED, ON UPDATE, ...
     *
     * @param ForeignKeyConstraint $foreignKey The 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 the given referential action in uppercase if valid, otherwise throws an exception.
     *
     * @param string $action The foreign key referential action.
     *
     * @return string
     *
     * @throws InvalidArgumentException If unknown referential action given.
     */
    public function getForeignKeyReferentialActionSQL($action)
    {
        $upper = strtoupper($action);
        switch ($upper) {
            case 'CASCADE':
            case 'SET NULL':
            case 'NO ACTION':
            case 'RESTRICT':
            case 'SET DEFAULT':
                return $upper;

            default:
                throw new InvalidArgumentException('Invalid foreign key action: ' . $upper);
        }
    }

    /**
     * Obtains DBMS specific SQL code portion needed to set the FOREIGN KEY constraint
     * of a column declaration to be used in statements like CREATE TABLE.
     *
     * @return string
     *
     * @throws InvalidArgumentException
     */
    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.");
        }

        return $sql . implode(', ', $foreignKey->getQuotedLocalColumns($this))
            . ') REFERENCES '
            . $foreignKey->getQuotedForeignTableName($this) . ' ('
            . implode(', ', $foreignKey->getQuotedForeignColumns($this)) . ')';
    }

    /**
     * Obtains DBMS specific SQL code portion needed to set the UNIQUE constraint
     * of a column declaration to be used in statements like CREATE TABLE.
     *
     * @return string DBMS specific SQL code portion needed to set the UNIQUE constraint
     *                of a column declaration.
     */
    public function getUniqueFieldDeclarationSQL()
    {
        return 'UNIQUE';
    }

    /**
     * Obtains DBMS specific SQL code portion needed to set the CHARACTER SET
     * of a column declaration to be used in statements like CREATE TABLE.
     *
     * @param string $charset The name of the charset.
     *
     * @return string DBMS specific SQL code portion needed to set the CHARACTER SET
     *                of a column declaration.
     */
    public function getColumnCharsetDeclarationSQL($charset)
    {
        return '';
    }

    /**
     * Obtains DBMS specific SQL code portion needed to set the COLLATION
     * of a column declaration to be used in statements like CREATE TABLE.
     *
     * @param string $collation The name of the collation.
     *
     * @return string DBMS specific SQL code portion needed to set the COLLATION
     *                of a column declaration.
     */
    public function getColumnCollationDeclarationSQL($collation)
    {
        return $this->supportsColumnCollation() ? 'COLLATE ' . $collation : '';
    }

    /**
     * Whether the platform prefers sequences for ID generation.
     * Subclasses should override this method to return TRUE if they prefer sequences.
     *
     * @deprecated
     *
     * @return bool
     */
    public function prefersSequences()
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/4229',
            'AbstractPlatform::prefersSequences() is deprecated without replacement and removed in DBAL 3.0'
        );

        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 bool
     */
    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).
     *
     * Note: if the input is not a boolean the original input might be returned.
     *
     * There are two contexts when converting booleans: Literals and Prepared Statements.
     * This method should handle the literal case
     *
     * @param mixed $item A boolean or an array of them.
     *
     * @return mixed A boolean database value or an array of them.
     */
    public function convertBooleans($item)
    {
        if (is_array($item)) {
            foreach ($item as $k => $value) {
                if (! is_bool($value)) {
                    continue;
                }

                $item[$k] = (int) $value;
            }
        } elseif (is_bool($item)) {
            $item = (int) $item;
        }

        return $item;
    }

    /**
     * Some platforms have boolean literals that needs to be correctly converted
     *
     * The default conversion tries to convert value into bool "(bool)$item"
     *
     * @param mixed $item
     *
     * @return bool|null
     */
    public function convertFromBoolean($item)
    {
        return $item === null ? null : (bool) $item;
    }

    /**
     * This method should handle the prepared statements case. When there is no
     * distinction, it's OK to use the same method.
     *
     * Note: if the input is not a boolean the original input might be returned.
     *
     * @param mixed $item A boolean or an array of them.
     *
     * @return mixed A boolean database value or an array of them.
     */
    public function convertBooleansToDatabaseValue($item)
    {
        return $this->convertBooleans($item);
    }

    /**
     * Returns the SQL specific for the platform to get the current date.
     *
     * @return string
     */
    public function getCurrentDateSQL()
    {
        return 'CURRENT_DATE';
    }

    /**
     * Returns the SQL specific for the platform to get the current time.
     *
     * @return string
     */
    public function getCurrentTimeSQL()
    {
        return 'CURRENT_TIME';
    }

    /**
     * Returns the SQL specific for the platform to get the current timestamp
     *
     * @return string
     */
    public function getCurrentTimestampSQL()
    {
        return 'CURRENT_TIMESTAMP';
    }

    /**
     * Returns the SQL for a given transaction isolation level Connection constant.
     *
     * @param int $level
     *
     * @return string
     *
     * @throws InvalidArgumentException
     */
    protected function _getTransactionIsolationLevelSQL($level)
    {
        switch ($level) {
            case TransactionIsolationLevel::READ_UNCOMMITTED:
                return 'READ UNCOMMITTED';

            case TransactionIsolationLevel::READ_COMMITTED:
                return 'READ COMMITTED';

            case TransactionIsolationLevel::REPEATABLE_READ:
                return 'REPEATABLE READ';

            case TransactionIsolationLevel::SERIALIZABLE:
                return 'SERIALIZABLE';

            default:
                throw new InvalidArgumentException('Invalid isolation level:' . $level);
        }
    }

    /**
     * @return string
     *
     * @throws Exception If not supported on this platform.
     */
    public function getListDatabasesSQL()
    {
        throw Exception::notSupported(__METHOD__);
    }

    /**
     * Returns the SQL statement for retrieving the namespaces defined in the database.
     *
     * @return string
     *
     * @throws Exception If not supported on this platform.
     */
    public function getListNamespacesSQL()
    {
        throw Exception::notSupported(__METHOD__);
    }

    /**
     * @param string $database
     *
     * @return string
     *
     * @throws Exception If not supported on this platform.
     */
    public function getListSequencesSQL($database)
    {
        throw Exception::notSupported(__METHOD__);
    }

    /**
     * @param string $table
     *
     * @return string
     *
     * @throws Exception If not supported on this platform.
     */
    public function getListTableConstraintsSQL($table)
    {
        throw Exception::notSupported(__METHOD__);
    }

    /**
     * @param string $table
     * @param string $database
     *
     * @return string
     *
     * @throws Exception If not supported on this platform.
     */
    public function getListTableColumnsSQL($table, $database = null)
    {
        throw Exception::notSupported(__METHOD__);
    }

    /**
     * @return string
     *
     * @throws Exception If not supported on this platform.
     */
    public function getListTablesSQL()
    {
        throw Exception::notSupported(__METHOD__);
    }

    /**
     * @return string
     *
     * @throws Exception If not supported on this platform.
     */
    public function getListUsersSQL()
    {
        throw Exception::notSupported(__METHOD__);
    }

    /**
     * Returns the SQL to list all views of a database or user.
     *
     * @param string $database
     *
     * @return string
     *
     * @throws Exception If not supported on this platform.
     */
    public function getListViewsSQL($database)
    {
        throw Exception::notSupported(__METHOD__);
    }

    /**
     * Returns the list of indexes for the current database.
     *
     * The current database parameter is optional but will always be passed
     * when using the SchemaManager API and is the database the given table is in.
     *
     * Attention: Some platforms only support currentDatabase when they
     * are connected with that database. Cross-database information schema
     * requests may be impossible.
     *
     * @param string $table
     * @param string $database
     *
     * @return string
     *
     * @throws Exception If not supported on this platform.
     */
    public function getListTableIndexesSQL($table, $database = null)
    {
        throw Exception::notSupported(__METHOD__);
    }

    /**
     * @param string $table
     *
     * @return string
     *
     * @throws Exception If not supported on this platform.
     */
    public function getListTableForeignKeysSQL($table)
    {
        throw Exception::notSupported(__METHOD__);
    }

    /**
     * @param string $name
     * @param string $sql
     *
     * @return string
     *
     * @throws Exception If not supported on this platform.
     */
    public function getCreateViewSQL($name, $sql)
    {
        throw Exception::notSupported(__METHOD__);
    }

    /**
     * @param string $name
     *
     * @return string
     *
     * @throws Exception If not supported on this platform.
     */
    public function getDropViewSQL($name)
    {
        throw Exception::notSupported(__METHOD__);
    }

    /**
     * Returns the SQL snippet to drop an existing sequence.
     *
     * @param Sequence|string $sequence
     *
     * @return string
     *
     * @throws Exception If not supported on this platform.
     */
    public function getDropSequenceSQL($sequence)
    {
        throw Exception::notSupported(__METHOD__);
    }

    /**
     * @param string $sequence
     *
     * @return string
     *
     * @throws Exception If not supported on this platform.
     */
    public function getSequenceNextValSQL($sequence)
    {
        throw Exception::notSupported(__METHOD__);
    }

    /**
     * Returns the SQL to create a new database.
     *
     * @param string $name The name of the database that should be created.
     *
     * @return string
     *
     * @throws Exception If not supported on this platform.
     */
    public function getCreateDatabaseSQL($name)
    {
        throw Exception::notSupported(__METHOD__);
    }

    /**
     * Returns the SQL to set the transaction isolation level.
     *
     * @param int $level
     *
     * @return string
     *
     * @throws Exception If not supported on this platform.
     */
    public function getSetTransactionIsolationSQL($level)
    {
        throw Exception::notSupported(__METHOD__);
    }

    /**
     * Obtains DBMS specific SQL to be used to create datetime columns in
     * statements like CREATE TABLE.
     *
     * @param mixed[] $column
     *
     * @return string
     *
     * @throws Exception If not supported on this platform.
     */
    public function getDateTimeTypeDeclarationSQL(array $column)
    {
        throw Exception::notSupported(__METHOD__);
    }

    /**
     * Obtains DBMS specific SQL to be used to create datetime with timezone offset columns.
     *
     * @param mixed[] $column
     *
     * @return string
     */
    public function getDateTimeTzTypeDeclarationSQL(array $column)
    {
        return $this->getDateTimeTypeDeclarationSQL($column);
    }

    /**
     * Obtains DBMS specific SQL to be used to create date columns in statements
     * like CREATE TABLE.
     *
     * @param mixed[] $column
     *
     * @return string
     *
     * @throws Exception If not supported on this platform.
     */
    public function getDateTypeDeclarationSQL(array $column)
    {
        throw Exception::notSupported(__METHOD__);
    }

    /**
     * Obtains DBMS specific SQL to be used to create time columns in statements
     * like CREATE TABLE.
     *
     * @param mixed[] $column
     *
     * @return string
     *
     * @throws Exception If not supported on this platform.
     */
    public function getTimeTypeDeclarationSQL(array $column)
    {
        throw Exception::notSupported(__METHOD__);
    }

    /**
     * @param mixed[] $column
     *
     * @return string
     */
    public function getFloatDeclarationSQL(array $column)
    {
        return 'DOUBLE PRECISION';
    }

    /**
     * Gets the default transaction isolation level of the platform.
     *
     * @see TransactionIsolationLevel
     *
     * @return int The default isolation level.
     */
    public function getDefaultTransactionIsolationLevel()
    {
        return TransactionIsolationLevel::READ_COMMITTED;
    }

    /* supports*() methods */

    /**
     * Whether the platform supports sequences.
     *
     * @return bool
     */
    public function supportsSequences()
    {
        return false;
    }

    /**
     * Whether the platform supports identity columns.
     *
     * Identity columns are columns that receive an auto-generated value from the
     * database on insert of a row.
     *
     * @return bool
     */
    public function supportsIdentityColumns()
    {
        return false;
    }

    /**
     * Whether the platform emulates identity columns through sequences.
     *
     * Some platforms that do not support identity columns natively
     * but support sequences can emulate identity columns by using
     * sequences.
     *
     * @return bool
     */
    public function usesSequenceEmulatedIdentityColumns()
    {
        return false;
    }

    /**
     * Returns the name of the sequence for a particular identity column in a particular table.
     *
     * @see    usesSequenceEmulatedIdentityColumns
     *
     * @param string $tableName  The name of the table to return the sequence name for.
     * @param string $columnName The name of the identity column in the table to return the sequence name for.
     *
     * @return string
     *
     * @throws Exception If not supported on this platform.
     */
    public function getIdentitySequenceName($tableName, $columnName)
    {
        throw Exception::notSupported(__METHOD__);
    }

    /**
     * Whether the platform supports indexes.
     *
     * @return bool
     */
    public function supportsIndexes()
    {
        return true;
    }

    /**
     * Whether the platform supports partial indexes.
     *
     * @return bool
     */
    public function supportsPartialIndexes()
    {
        return false;
    }

    /**
     * Whether the platform supports indexes with column length definitions.
     */
    public function supportsColumnLengthIndexes(): bool
    {
        return false;
    }

    /**
     * Whether the platform supports altering tables.
     *
     * @return bool
     */
    public function supportsAlterTable()
    {
        return true;
    }

    /**
     * Whether the platform supports transactions.
     *
     * @return bool
     */
    public function supportsTransactions()
    {
        return true;
    }

    /**
     * Whether the platform supports savepoints.
     *
     * @return bool
     */
    public function supportsSavepoints()
    {
        return true;
    }

    /**
     * Whether the platform supports releasing savepoints.
     *
     * @return bool
     */
    public function supportsReleaseSavepoints()
    {
        return $this->supportsSavepoints();
    }

    /**
     * Whether the platform supports primary key constraints.
     *
     * @return bool
     */
    public function supportsPrimaryConstraints()
    {
        return true;
    }

    /**
     * Whether the platform supports foreign key constraints.
     *
     * @return bool
     */
    public function supportsForeignKeyConstraints()
    {
        return true;
    }

    /**
     * Whether this platform supports onUpdate in foreign key constraints.
     *
     * @deprecated
     *
     * @return bool
     */
    public function supportsForeignKeyOnUpdate()
    {
        Deprecation::triggerIfCalledFromOutside(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/4229',
            'AbstractPlatform::supportsForeignKeyOnUpdate() is deprecated without replacement and removed in DBAL 3.0'
        );

        return $this->supportsForeignKeyConstraints();
    }

    /**
     * Whether the platform supports database schemas.
     *
     * @return bool
     */
    public function supportsSchemas()
    {
        return false;
    }

    /**
     * Whether this platform can emulate schemas.
     *
     * Platforms that either support or emulate schemas don't automatically
     * filter a schema for the namespaced elements in {@link AbstractManager::createSchema()}.
     *
     * @return bool
     */
    public function canEmulateSchemas()
    {
        return false;
    }

    /**
     * Returns the default schema name.
     *
     * @return string
     *
     * @throws Exception If not supported on this platform.
     */
    public function getDefaultSchemaName()
    {
        throw Exception::notSupported(__METHOD__);
    }

    /**
     * Whether this platform supports create database.
     *
     * 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 bool
     */
    public function supportsGettingAffectedRows()
    {
        return true;
    }

    /**
     * Whether this platform support to add inline column comments as postfix.
     *
     * @return bool
     */
    public function supportsInlineColumnComments()
    {
        return false;
    }

    /**
     * Whether this platform support the proprietary syntax "COMMENT ON asset".
     *
     * @return bool
     */
    public function supportsCommentOnStatement()
    {
        return false;
    }

    /**
     * Does this platform have native guid type.
     *
     * @return bool
     */
    public function hasNativeGuidType()
    {
        return false;
    }

    /**
     * Does this platform have native JSON type.
     *
     * @return bool
     */
    public function hasNativeJsonType()
    {
        return false;
    }

    /**
     * @deprecated
     *
     * @return string
     *
     * @todo Remove in 3.0
     */
    public function getIdentityColumnNullInsertSQL()
    {
        return '';
    }

    /**
     * Whether this platform supports views.
     *
     * @return bool
     */
    public function supportsViews()
    {
        return true;
    }

    /**
     * Does this platform support column collation?
     *
     * @return bool
     */
    public function supportsColumnCollation()
    {
        return false;
    }

    /**
     * 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';
    }

    /**
     * Adds an driver-specific LIMIT clause to the query.
     *
     * @param string   $query
     * @param int|null $limit
     * @param int|null $offset
     *
     * @return string
     *
     * @throws Exception
     */
    final public function modifyLimitQuery($query, $limit, $offset = null)
    {
        if ($limit !== null) {
            $limit = (int) $limit;
        }

        $offset = (int) $offset;

        if ($offset < 0) {
            throw new Exception(sprintf(
                'Offset must be a positive integer or zero, %d given',
                $offset
            ));
        }

        if ($offset > 0 && ! $this->supportsLimitOffset()) {
            throw new Exception(sprintf(
                'Platform %s does not support offset values in limit queries.',
                $this->getName()
            ));
        }

        return $this->doModifyLimitQuery($query, $limit, $offset);
    }

    /**
     * Adds an platform-specific LIMIT clause to the query.
     *
     * @param string   $query
     * @param int|null $limit
     * @param int|null $offset
     *
     * @return string
     */
    protected function doModifyLimitQuery($query, $limit, $offset)
    {
        if ($limit !== null) {
            $query .= ' LIMIT ' . $limit;
        }

        if ($offset > 0) {
            $query .= ' OFFSET ' . $offset;
        }

        return $query;
    }

    /**
     * Whether the database platform support offsets in modify limit clauses.
     *
     * @return bool
     */
    public function supportsLimitOffset()
    {
        return true;
    }

    /**
     * Gets the character casing of a column in an SQL result set of this platform.
     *
     * @deprecated
     *
     * @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)
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/4229',
            'AbstractPlatform::getSQLResultCasing is deprecated without replacement and removed in DBAL 3.' .
            'Use Portability\Connection with PORTABILITY_FIX_CASE to get portable result cases.'
        );

        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.
     *
     * @deprecated
     *
     * @param string $schemaElementName
     *
     * @return string
     */
    public function fixSchemaElementName($schemaElementName)
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/4132',
            'AbstractPlatform::fixSchemaElementName is deprecated with no replacement and removed in DBAL 3.0'
        );

        return $schemaElementName;
    }

    /**
     * Maximum length of any given database identifier, like tables or column names.
     *
     * @return int
     */
    public function getMaxIdentifierLength()
    {
        return 63;
    }

    /**
     * Returns the insert SQL for an empty insert statement.
     *
     * @param string $quotedTableName
     * @param string $quotedIdentifierColumnName
     *
     * @return string
     */
    public function getEmptyIdentityInsertSQL($quotedTableName, $quotedIdentifierColumnName)
    {
        return 'INSERT INTO ' . $quotedTableName . ' (' . $quotedIdentifierColumnName . ') VALUES (null)';
    }

    /**
     * Generates 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)
    {
        $tableIdentifier = new Identifier($tableName);

        return 'TRUNCATE ' . $tableIdentifier->getQuotedName($this);
    }

    /**
     * This is for test reasons, many vendors have special requirements for dummy statements.
     *
     * @return string
     */
    public function getDummySelectSQL()
    {
        $expression = func_num_args() > 0 ? func_get_arg(0) : '1';

        return sprintf('SELECT %s', $expression);
    }

    /**
     * Returns the SQL to create a new savepoint.
     *
     * @param string $savepoint
     *
     * @return string
     */
    public function createSavePoint($savepoint)
    {
        return 'SAVEPOINT ' . $savepoint;
    }

    /**
     * Returns the SQL to release a savepoint.
     *
     * @param string $savepoint
     *
     * @return string
     */
    public function releaseSavePoint($savepoint)
    {
        return 'RELEASE SAVEPOINT ' . $savepoint;
    }

    /**
     * Returns the SQL to rollback a savepoint.
     *
     * @param string $savepoint
     *
     * @return string
     */
    public function rollbackSavePoint($savepoint)
    {
        return 'ROLLBACK TO SAVEPOINT ' . $savepoint;
    }

    /**
     * Returns the keyword list instance of this platform.
     *
     * @return KeywordList
     *
     * @throws Exception If no keyword list is specified.
     */
    final public function getReservedKeywordsList()
    {
        // Check for an existing instantiation of the keywords class.
        if ($this->_keywords) {
            return $this->_keywords;
        }

        $class    = $this->getReservedKeywordsClass();
        $keywords = new $class();
        if (! $keywords instanceof KeywordList) {
            throw Exception::notSupported(__METHOD__);
        }

        // Store the instance so it doesn't need to be generated on every request.
        $this->_keywords = $keywords;

        return $keywords;
    }

    /**
     * Returns the class name of the reserved keywords list.
     *
     * @return string
     * @psalm-return class-string<KeywordList>
     *
     * @throws Exception If not supported on this platform.
     */
    protected function getReservedKeywordsClass()
    {
        throw Exception::notSupported(__METHOD__);
    }

    /**
     * Quotes a literal string.
     * This method is NOT meant to fix SQL injections!
     * It is only meant to escape this platform's string literal
     * quote character inside the given literal string.
     *
     * @param string $str The literal string to be quoted.
     *
     * @return string The quoted literal string.
     */
    public function quoteStringLiteral($str)
    {
        $c = $this->getStringLiteralQuoteCharacter();

        return $c . str_replace($c, $c . $c, $str) . $c;
    }

    /**
     * Gets the character used for string literal quoting.
     *
     * @return string
     */
    public function getStringLiteralQuoteCharacter()
    {
        return "'";
    }

    /**
     * Escapes metacharacters in a string intended to be used with a LIKE
     * operator.
     *
     * @param string $inputString a literal, unquoted string
     * @param string $escapeChar  should be reused by the caller in the LIKE
     *                            expression.
     */
    final public function escapeStringForLike(string $inputString, string $escapeChar): string
    {
        return preg_replace(
            '~([' . preg_quote($this->getLikeWildcardCharacters() . $escapeChar, '~') . '])~u',
            addcslashes($escapeChar, '\\') . '$1',
            $inputString
        );
    }

    protected function getLikeWildcardCharacters(): string
    {
        return '%_';
    }
}
<?php

namespace Doctrine\DBAL\Platforms;

/**
 * The SQLAnywhere11Platform provides the behavior, features and SQL dialect of the
 * SAP Sybase SQL Anywhere 11 database platform.
 *
 * @deprecated Support for SQLAnywhere will be removed in 3.0.
 */
class SQLAnywhere11Platform extends SQLAnywherePlatform
{
    /**
     * {@inheritdoc}
     */
    public function getRegexpExpression()
    {
        return 'REGEXP';
    }

    /**
     * {@inheritdoc}
     */
    protected function getReservedKeywordsClass()
    {
        return Keywords\SQLAnywhere11Keywords::class;
    }
}
<?php

namespace Doctrine\DBAL\Platforms;

use Doctrine\DBAL\LockMode;
use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Schema\ColumnDiff;
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
use Doctrine\DBAL\Schema\Identifier;
use Doctrine\DBAL\Schema\Index;
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Schema\TableDiff;
use InvalidArgumentException;

use function array_merge;
use function array_unique;
use function array_values;
use function count;
use function crc32;
use function dechex;
use function explode;
use function func_get_args;
use function implode;
use function is_array;
use function is_bool;
use function is_numeric;
use function is_string;
use function preg_match;
use function sprintf;
use function str_replace;
use function stripos;
use function stristr;
use function strlen;
use function strpos;
use function strtoupper;
use function substr;
use function substr_count;

/**
 * The SQLServerPlatform provides the behavior, features and SQL dialect of the
 * Microsoft SQL Server database platform.
 *
 * @deprecated Use SQL Server 2012 or newer
 */
class SQLServerPlatform extends AbstractPlatform
{
    /**
     * {@inheritdoc}
     */
    public function getCurrentDateSQL()
    {
        return $this->getConvertExpression('date', 'GETDATE()');
    }

    /**
     * {@inheritdoc}
     */
    public function getCurrentTimeSQL()
    {
        return $this->getConvertExpression('time', 'GETDATE()');
    }

    /**
     * Returns an expression that converts an expression of one data type to another.
     *
     * @param string $dataType   The target native data type. Alias data types cannot be used.
     * @param string $expression The SQL expression to convert.
     *
     * @return string
     */
    private function getConvertExpression($dataType, $expression)
    {
        return sprintf('CONVERT(%s, %s)', $dataType, $expression);
    }

    /**
     * {@inheritdoc}
     */
    protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit)
    {
        $factorClause = '';

        if ($operator === '-') {
            $factorClause = '-1 * ';
        }

        return 'DATEADD(' . $unit . ', ' . $factorClause . $interval . ', ' . $date . ')';
    }

    /**
     * {@inheritDoc}
     */
    public function getDateDiffExpression($date1, $date2)
    {
        return 'DATEDIFF(day, ' . $date2 . ',' . $date1 . ')';
    }

    /**
     * {@inheritDoc}
     *
     * Microsoft SQL Server prefers "autoincrement" identity columns
     * since sequences can only be emulated with a table.
     */
    public function prefersIdentityColumns()
    {
        return true;
    }

    /**
     * {@inheritDoc}
     *
     * Microsoft SQL Server supports this through AUTO_INCREMENT columns.
     */
    public function supportsIdentityColumns()
    {
        return true;
    }

    /**
     * {@inheritDoc}
     */
    public function supportsReleaseSavepoints()
    {
        return false;
    }

    /**
     * {@inheritdoc}
     */
    public function supportsSchemas()
    {
        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function getDefaultSchemaName()
    {
        return 'dbo';
    }

    /**
     * {@inheritDoc}
     */
    public function supportsColumnCollation()
    {
        return true;
    }

    /**
     * {@inheritDoc}
     */
    public function hasNativeGuidType()
    {
        return true;
    }

    /**
     * {@inheritDoc}
     */
    public function getCreateDatabaseSQL($name)
    {
        return 'CREATE DATABASE ' . $name;
    }

    /**
     * {@inheritDoc}
     */
    public function getDropDatabaseSQL($name)
    {
        return 'DROP DATABASE ' . $name;
    }

    /**
     * {@inheritDoc}
     */
    public function supportsCreateDropDatabase()
    {
        return true;
    }

    /**
     * {@inheritDoc}
     */
    public function getCreateSchemaSQL($schemaName)
    {
        return 'CREATE SCHEMA ' . $schemaName;
    }

    /**
     * {@inheritDoc}
     */
    public function getDropForeignKeySQL($foreignKey, $table)
    {
        if (! $foreignKey instanceof ForeignKeyConstraint) {
            $foreignKey = new Identifier($foreignKey);
        }

        if (! $table instanceof Table) {
            $table = new Identifier($table);
        }

        $foreignKey = $foreignKey->getQuotedName($this);
        $table      = $table->getQuotedName($this);

        return 'ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $foreignKey;
    }

    /**
     * {@inheritDoc}
     */
    public function getDropIndexSQL($index, $table = null)
    {
        if ($index instanceof Index) {
            $index = $index->getQuotedName($this);
        } elseif (! is_string($index)) {
            throw new InvalidArgumentException(
                __METHOD__ . '() expects $index parameter to be string or ' . Index::class . '.'
            );
        }

        if (! isset($table)) {
            return 'DROP INDEX ' . $index;
        }

        if ($table instanceof Table) {
            $table = $table->getQuotedName($this);
        }

        return sprintf(
            "
                IF EXISTS (SELECT * FROM sysobjects WHERE name = '%s')
                    ALTER TABLE %s DROP CONSTRAINT %s
                ELSE
                    DROP INDEX %s ON %s
            ",
            $index,
            $table,
            $index,
            $index,
            $table
        );
    }

    /**
     * {@inheritDoc}
     */
    protected function _getCreateTableSQL($name, array $columns, array $options = [])
    {
        $defaultConstraintsSql = [];
        $commentsSql           = [];

        $tableComment = $options['comment'] ?? null;
        if ($tableComment !== null) {
            $commentsSql[] = $this->getCommentOnTableSQL($name, $tableComment);
        }

        // @todo does other code breaks because of this?
        // force primary keys to be not null
        foreach ($columns as &$column) {
            if (isset($column['primary']) && $column['primary']) {
                $column['notnull'] = true;
            }

            // Build default constraints SQL statements.
            if (isset($column['default'])) {
                $defaultConstraintsSql[] = 'ALTER TABLE ' . $name .
                    ' ADD' . $this->getDefaultConstraintDeclarationSQL($name, $column);
            }

            if (empty($column['comment']) && ! is_numeric($column['comment'])) {
                continue;
            }

            $commentsSql[] = $this->getCreateColumnCommentSQL($name, $column['name'], $column['comment']);
        }

        $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'])) {
            $flags = '';
            if (isset($options['primary_index']) && $options['primary_index']->hasFlag('nonclustered')) {
                $flags = ' NONCLUSTERED';
            }

            $columnListSql .= ', PRIMARY KEY' . $flags
                . ' (' . implode(', ', array_unique(array_values($options['primary']))) . ')';
        }

        $query = 'CREATE TABLE ' . $name . ' (' . $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, $name);
            }
        }

        if (isset($options['foreignKeys'])) {
            foreach ((array) $options['foreignKeys'] as $definition) {
                $sql[] = $this->getCreateForeignKeySQL($definition, $name);
            }
        }

        return array_merge($sql, $commentsSql, $defaultConstraintsSql);
    }

    /**
     * {@inheritDoc}
     */
    public function getCreatePrimaryKeySQL(Index $index, $table)
    {
        if ($table instanceof Table) {
            $identifier = $table->getQuotedName($this);
        } else {
            $identifier = $table;
        }

        $sql = 'ALTER TABLE ' . $identifier . ' ADD PRIMARY KEY';

        if ($index->hasFlag('nonclustered')) {
            $sql .= ' NONCLUSTERED';
        }

        return $sql . ' (' . $this->getIndexFieldDeclarationListSQL($index) . ')';
    }

    /**
     * Returns the SQL statement for creating a column comment.
     *
     * SQL Server does not support native column comments,
     * therefore the extended properties functionality is used
     * as a workaround to store them.
     * The property name used to store column comments is "MS_Description"
     * which provides compatibility with SQL Server Management Studio,
     * as column comments are stored in the same property there when
     * specifying a column's "Description" attribute.
     *
     * @param string      $tableName  The quoted table name to which the column belongs.
     * @param string      $columnName The quoted column name to create the comment for.
     * @param string|null $comment    The column's comment.
     *
     * @return string
     */
    protected function getCreateColumnCommentSQL($tableName, $columnName, $comment)
    {
        if (strpos($tableName, '.') !== false) {
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
        } else {
            $schemaSQL = "'dbo'";
            $tableSQL  = $this->quoteStringLiteral($tableName);
        }

        return $this->getAddExtendedPropertySQL(
            'MS_Description',
            $comment,
            'SCHEMA',
            $schemaSQL,
            'TABLE',
            $tableSQL,
            'COLUMN',
            $columnName
        );
    }

    /**
     * Returns the SQL snippet for declaring a default constraint.
     *
     * @param string  $table  Name of the table to return the default constraint declaration for.
     * @param mixed[] $column Column definition.
     *
     * @return string
     *
     * @throws InvalidArgumentException
     */
    public function getDefaultConstraintDeclarationSQL($table, array $column)
    {
        if (! isset($column['default'])) {
            throw new InvalidArgumentException("Incomplete column definition. 'default' required.");
        }

        $columnName = new Identifier($column['name']);

        return ' CONSTRAINT ' .
            $this->generateDefaultConstraintName($table, $column['name']) .
            $this->getDefaultValueDeclarationSQL($column) .
            ' FOR ' . $columnName->getQuotedName($this);
    }

    /**
     * {@inheritDoc}
     */
    public function getUniqueConstraintDeclarationSQL($name, Index $index)
    {
        $constraint = parent::getUniqueConstraintDeclarationSQL($name, $index);

        $constraint = $this->_appendUniqueConstraintDefinition($constraint, $index);

        return $constraint;
    }

    /**
     * {@inheritDoc}
     */
    public function getCreateIndexSQL(Index $index, $table)
    {
        $constraint = parent::getCreateIndexSQL($index, $table);

        if ($index->isUnique() && ! $index->isPrimary()) {
            $constraint = $this->_appendUniqueConstraintDefinition($constraint, $index);
        }

        return $constraint;
    }

    /**
     * {@inheritDoc}
     */
    protected function getCreateIndexSQLFlags(Index $index)
    {
        $type = '';
        if ($index->isUnique()) {
            $type .= 'UNIQUE ';
        }

        if ($index->hasFlag('clustered')) {
            $type .= 'CLUSTERED ';
        } elseif ($index->hasFlag('nonclustered')) {
            $type .= 'NONCLUSTERED ';
        }

        return $type;
    }

    /**
     * Extend unique key constraint with required filters
     *
     * @param string $sql
     *
     * @return string
     */
    private function _appendUniqueConstraintDefinition($sql, Index $index)
    {
        $fields = [];

        foreach ($index->getQuotedColumns($this) as $field) {
            $fields[] = $field . ' IS NOT NULL';
        }

        return $sql . ' WHERE ' . implode(' AND ', $fields);
    }

    /**
     * {@inheritDoc}
     */
    public function getAlterTableSQL(TableDiff $diff)
    {
        $queryParts  = [];
        $sql         = [];
        $columnSql   = [];
        $commentsSql = [];

        foreach ($diff->addedColumns as $column) {
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
                continue;
            }

            $columnDef    = $column->toArray();
            $addColumnSql = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);
            if (isset($columnDef['default'])) {
                $addColumnSql .= ' CONSTRAINT ' .
                    $this->generateDefaultConstraintName($diff->name, $column->getQuotedName($this)) .
                    $this->getDefaultValueDeclarationSQL($columnDef);
            }

            $queryParts[] = $addColumnSql;

            $comment = $this->getColumnComment($column);

            if (empty($comment) && ! is_numeric($comment)) {
                continue;
            }

            $commentsSql[] = $this->getCreateColumnCommentSQL(
                $diff->name,
                $column->getQuotedName($this),
                $comment
            );
        }

        foreach ($diff->removedColumns as $column) {
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
                continue;
            }

            $queryParts[] = 'DROP COLUMN ' . $column->getQuotedName($this);
        }

        foreach ($diff->changedColumns as $columnDiff) {
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
                continue;
            }

            $column     = $columnDiff->column;
            $comment    = $this->getColumnComment($column);
            $hasComment = ! empty($comment) || is_numeric($comment);

            if ($columnDiff->fromColumn instanceof Column) {
                $fromComment    = $this->getColumnComment($columnDiff->fromColumn);
                $hasFromComment = ! empty($fromComment) || is_numeric($fromComment);

                if ($hasFromComment && $hasComment && $fromComment !== $comment) {
                    $commentsSql[] = $this->getAlterColumnCommentSQL(
                        $diff->name,
                        $column->getQuotedName($this),
                        $comment
                    );
                } elseif ($hasFromComment && ! $hasComment) {
                    $commentsSql[] = $this->getDropColumnCommentSQL($diff->name, $column->getQuotedName($this));
                } elseif (! $hasFromComment && $hasComment) {
                    $commentsSql[] = $this->getCreateColumnCommentSQL(
                        $diff->name,
                        $column->getQuotedName($this),
                        $comment
                    );
                }
            }

            // Do not add query part if only comment has changed.
            if ($columnDiff->hasChanged('comment') && count($columnDiff->changedProperties) === 1) {
                continue;
            }

            $requireDropDefaultConstraint = $this->alterColumnRequiresDropDefaultConstraint($columnDiff);

            if ($requireDropDefaultConstraint) {
                $queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
                    $diff->name,
                    $columnDiff->oldColumnName
                );
            }

            $columnDef = $column->toArray();

            $queryParts[] = 'ALTER COLUMN ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);

            if (
                ! isset($columnDef['default'])
                || (! $requireDropDefaultConstraint && ! $columnDiff->hasChanged('default'))
            ) {
                continue;
            }

            $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
        }

        foreach ($diff->renamedColumns as $oldColumnName => $column) {
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
                continue;
            }

            $oldColumnName = new Identifier($oldColumnName);

            $sql[] = "sp_rename '" .
                $diff->getName($this)->getQuotedName($this) . '.' . $oldColumnName->getQuotedName($this) .
                "', '" . $column->getQuotedName($this) . "', 'COLUMN'";

            // Recreate default constraint with new column name if necessary (for future reference).
            if ($column->getDefault() === null) {
                continue;
            }

            $queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
                $diff->name,
                $oldColumnName->getQuotedName($this)
            );
            $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
        }

        $tableSql = [];

        if ($this->onSchemaAlterTable($diff, $tableSql)) {
            return array_merge($tableSql, $columnSql);
        }

        foreach ($queryParts as $query) {
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
        }

        $sql = array_merge($sql, $commentsSql);

        $newName = $diff->getNewName();

        if ($newName !== false) {
            $sql[] = "sp_rename '" . $diff->getName($this)->getQuotedName($this) . "', '" . $newName->getName() . "'";

            /**
             * Rename table's default constraints names
             * to match the new table name.
             * This is necessary to ensure that the default
             * constraints can be referenced in future table
             * alterations as the table name is encoded in
             * default constraints' names.
             */
            $sql[] = "DECLARE @sql NVARCHAR(MAX) = N''; " .
                "SELECT @sql += N'EXEC sp_rename N''' + dc.name + ''', N''' " .
                "+ REPLACE(dc.name, '" . $this->generateIdentifierName($diff->name) . "', " .
                "'" . $this->generateIdentifierName($newName->getName()) . "') + ''', ''OBJECT'';' " .
                'FROM sys.default_constraints dc ' .
                'JOIN sys.tables tbl ON dc.parent_object_id = tbl.object_id ' .
                "WHERE tbl.name = '" . $newName->getName() . "';" .
                'EXEC sp_executesql @sql';
        }

        $sql = array_merge(
            $this->getPreAlterTableIndexForeignKeySQL($diff),
            $sql,
            $this->getPostAlterTableIndexForeignKeySQL($diff)
        );

        return array_merge($sql, $tableSql, $columnSql);
    }

    /**
     * Returns the SQL clause for adding a default constraint in an ALTER TABLE statement.
     *
     * @param string $tableName The name of the table to generate the clause for.
     * @param Column $column    The column to generate the clause for.
     *
     * @return string
     */
    private function getAlterTableAddDefaultConstraintClause($tableName, Column $column)
    {
        $columnDef         = $column->toArray();
        $columnDef['name'] = $column->getQuotedName($this);

        return 'ADD' . $this->getDefaultConstraintDeclarationSQL($tableName, $columnDef);
    }

    /**
     * Returns the SQL clause for dropping an existing default constraint in an ALTER TABLE statement.
     *
     * @param string $tableName  The name of the table to generate the clause for.
     * @param string $columnName The name of the column to generate the clause for.
     *
     * @return string
     */
    private function getAlterTableDropDefaultConstraintClause($tableName, $columnName)
    {
        return 'DROP CONSTRAINT ' . $this->generateDefaultConstraintName($tableName, $columnName);
    }

    /**
     * Checks whether a column alteration requires dropping its default constraint first.
     *
     * Different to other database vendors SQL Server implements column default values
     * as constraints and therefore changes in a column's default value as well as changes
     * in a column's type require dropping the default constraint first before being to
     * alter the particular column to the new definition.
     *
     * @param ColumnDiff $columnDiff The column diff to evaluate.
     *
     * @return bool True if the column alteration requires dropping its default constraint first, false otherwise.
     */
    private function alterColumnRequiresDropDefaultConstraint(ColumnDiff $columnDiff)
    {
        // We can only decide whether to drop an existing default constraint
        // if we know the original default value.
        if (! $columnDiff->fromColumn instanceof Column) {
            return false;
        }

        // We only need to drop an existing default constraint if we know the
        // column was defined with a default value before.
        if ($columnDiff->fromColumn->getDefault() === null) {
            return false;
        }

        // We need to drop an existing default constraint if the column was
        // defined with a default value before and it has changed.
        if ($columnDiff->hasChanged('default')) {
            return true;
        }

        // We need to drop an existing default constraint if the column was
        // defined with a default value before and the native column type has changed.
        return $columnDiff->hasChanged('type') || $columnDiff->hasChanged('fixed');
    }

    /**
     * Returns the SQL statement for altering a column comment.
     *
     * SQL Server does not support native column comments,
     * therefore the extended properties functionality is used
     * as a workaround to store them.
     * The property name used to store column comments is "MS_Description"
     * which provides compatibility with SQL Server Management Studio,
     * as column comments are stored in the same property there when
     * specifying a column's "Description" attribute.
     *
     * @param string      $tableName  The quoted table name to which the column belongs.
     * @param string      $columnName The quoted column name to alter the comment for.
     * @param string|null $comment    The column's comment.
     *
     * @return string
     */
    protected function getAlterColumnCommentSQL($tableName, $columnName, $comment)
    {
        if (strpos($tableName, '.') !== false) {
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
        } else {
            $schemaSQL = "'dbo'";
            $tableSQL  = $this->quoteStringLiteral($tableName);
        }

        return $this->getUpdateExtendedPropertySQL(
            'MS_Description',
            $comment,
            'SCHEMA',
            $schemaSQL,
            'TABLE',
            $tableSQL,
            'COLUMN',
            $columnName
        );
    }

    /**
     * Returns the SQL statement for dropping a column comment.
     *
     * SQL Server does not support native column comments,
     * therefore the extended properties functionality is used
     * as a workaround to store them.
     * The property name used to store column comments is "MS_Description"
     * which provides compatibility with SQL Server Management Studio,
     * as column comments are stored in the same property there when
     * specifying a column's "Description" attribute.
     *
     * @param string $tableName  The quoted table name to which the column belongs.
     * @param string $columnName The quoted column name to drop the comment for.
     *
     * @return string
     */
    protected function getDropColumnCommentSQL($tableName, $columnName)
    {
        if (strpos($tableName, '.') !== false) {
            [$schemaSQL, $tableSQL] = explode('.', $tableName);
            $schemaSQL              = $this->quoteStringLiteral($schemaSQL);
            $tableSQL               = $this->quoteStringLiteral($tableSQL);
        } else {
            $schemaSQL = "'dbo'";
            $tableSQL  = $this->quoteStringLiteral($tableName);
        }

        return $this->getDropExtendedPropertySQL(
            'MS_Description',
            'SCHEMA',
            $schemaSQL,
            'TABLE',
            $tableSQL,
            'COLUMN',
            $columnName
        );
    }

    /**
     * {@inheritdoc}
     */
    protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName)
    {
        return [sprintf(
            "EXEC sp_rename N'%s.%s', N'%s', N'INDEX'",
            $tableName,
            $oldIndexName,
            $index->getQuotedName($this)
        ),
        ];
    }

    /**
     * Returns the SQL statement for adding an extended property to a database object.
     *
     * @link http://msdn.microsoft.com/en-us/library/ms180047%28v=sql.90%29.aspx
     *
     * @param string      $name       The name of the property to add.
     * @param string|null $value      The value of the property to add.
     * @param string|null $level0Type The type of the object at level 0 the property belongs to.
     * @param string|null $level0Name The name of the object at level 0 the property belongs to.
     * @param string|null $level1Type The type of the object at level 1 the property belongs to.
     * @param string|null $level1Name The name of the object at level 1 the property belongs to.
     * @param string|null $level2Type The type of the object at level 2 the property belongs to.
     * @param string|null $level2Name The name of the object at level 2 the property belongs to.
     *
     * @return string
     */
    public function getAddExtendedPropertySQL(
        $name,
        $value = null,
        $level0Type = null,
        $level0Name = null,
        $level1Type = null,
        $level1Name = null,
        $level2Type = null,
        $level2Name = null
    ) {
        return 'EXEC sp_addextendedproperty ' .
            'N' . $this->quoteStringLiteral($name) . ', N' . $this->quoteStringLiteral((string) $value) . ', ' .
            'N' . $this->quoteStringLiteral((string) $level0Type) . ', ' . $level0Name . ', ' .
            'N' . $this->quoteStringLiteral((string) $level1Type) . ', ' . $level1Name . ', ' .
            'N' . $this->quoteStringLiteral((string) $level2Type) . ', ' . $level2Name;
    }

    /**
     * Returns the SQL statement for dropping an extended property from a database object.
     *
     * @link http://technet.microsoft.com/en-gb/library/ms178595%28v=sql.90%29.aspx
     *
     * @param string      $name       The name of the property to drop.
     * @param string|null $level0Type The type of the object at level 0 the property belongs to.
     * @param string|null $level0Name The name of the object at level 0 the property belongs to.
     * @param string|null $level1Type The type of the object at level 1 the property belongs to.
     * @param string|null $level1Name The name of the object at level 1 the property belongs to.
     * @param string|null $level2Type The type of the object at level 2 the property belongs to.
     * @param string|null $level2Name The name of the object at level 2 the property belongs to.
     *
     * @return string
     */
    public function getDropExtendedPropertySQL(
        $name,
        $level0Type = null,
        $level0Name = null,
        $level1Type = null,
        $level1Name = null,
        $level2Type = null,
        $level2Name = null
    ) {
        return 'EXEC sp_dropextendedproperty ' .
            'N' . $this->quoteStringLiteral($name) . ', ' .
            'N' . $this->quoteStringLiteral((string) $level0Type) . ', ' . $level0Name . ', ' .
            'N' . $this->quoteStringLiteral((string) $level1Type) . ', ' . $level1Name . ', ' .
            'N' . $this->quoteStringLiteral((string) $level2Type) . ', ' . $level2Name;
    }

    /**
     * Returns the SQL statement for updating an extended property of a database object.
     *
     * @link http://msdn.microsoft.com/en-us/library/ms186885%28v=sql.90%29.aspx
     *
     * @param string      $name       The name of the property to update.
     * @param string|null $value      The value of the property to update.
     * @param string|null $level0Type The type of the object at level 0 the property belongs to.
     * @param string|null $level0Name The name of the object at level 0 the property belongs to.
     * @param string|null $level1Type The type of the object at level 1 the property belongs to.
     * @param string|null $level1Name The name of the object at level 1 the property belongs to.
     * @param string|null $level2Type The type of the object at level 2 the property belongs to.
     * @param string|null $level2Name The name of the object at level 2 the property belongs to.
     *
     * @return string
     */
    public function getUpdateExtendedPropertySQL(
        $name,
        $value = null,
        $level0Type = null,
        $level0Name = null,
        $level1Type = null,
        $level1Name = null,
        $level2Type = null,
        $level2Name = null
    ) {
        return 'EXEC sp_updateextendedproperty ' .
            'N' . $this->quoteStringLiteral($name) . ', N' . $this->quoteStringLiteral((string) $value) . ', ' .
            'N' . $this->quoteStringLiteral((string) $level0Type) . ', ' . $level0Name . ', ' .
            'N' . $this->quoteStringLiteral((string) $level1Type) . ', ' . $level1Name . ', ' .
            'N' . $this->quoteStringLiteral((string) $level2Type) . ', ' . $level2Name;
    }

    /**
     * {@inheritDoc}
     */
    public function getEmptyIdentityInsertSQL($quotedTableName, $quotedIdentifierColumnName)
    {
        return 'INSERT INTO ' . $quotedTableName . ' DEFAULT VALUES';
    }

    /**
     * {@inheritDoc}
     */
    public function getListTablesSQL()
    {
        // "sysdiagrams" table must be ignored as it's internal SQL Server table for Database Diagrams
        // Category 2 must be ignored as it is "MS SQL Server 'pseudo-system' object[s]" for replication
        return "SELECT name FROM sysobjects WHERE type = 'U' AND name != 'sysdiagrams' AND category != 2 ORDER BY name";
    }

    /**
     * {@inheritDoc}
     */
    public function getListTableColumnsSQL($table, $database = null)
    {
        return "SELECT    col.name,
                          type.name AS type,
                          col.max_length AS length,
                          ~col.is_nullable AS notnull,
                          def.definition AS [default],
                          col.scale,
                          col.precision,
                          col.is_identity AS autoincrement,
                          col.collation_name AS collation,
                          CAST(prop.value AS NVARCHAR(MAX)) AS comment -- CAST avoids driver error for sql_variant type
                FROM      sys.columns AS col
                JOIN      sys.types AS type
                ON        col.user_type_id = type.user_type_id
                JOIN      sys.objects AS obj
                ON        col.object_id = obj.object_id
                JOIN      sys.schemas AS scm
                ON        obj.schema_id = scm.schema_id
                LEFT JOIN sys.default_constraints def
                ON        col.default_object_id = def.object_id
                AND       col.object_id = def.parent_object_id
                LEFT JOIN sys.extended_properties AS prop
                ON        obj.object_id = prop.major_id
                AND       col.column_id = prop.minor_id
                AND       prop.name = 'MS_Description'
                WHERE     obj.type = 'U'
                AND       " . $this->getTableWhereClause($table, 'scm.name', 'obj.name');
    }

    /**
     * @param string      $table
     * @param string|null $database
     *
     * @return string
     */
    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 ' .
                $this->getTableWhereClause($table, 'SCHEMA_NAME (f.schema_id)', 'OBJECT_NAME (f.parent_object_id)');
    }

    /**
     * {@inheritDoc}
     */
    public function getListTableIndexesSQL($table, $database = null)
    {
        return "SELECT idx.name AS key_name,
                       col.name AS column_name,
                       ~idx.is_unique AS non_unique,
                       idx.is_primary_key AS [primary],
                       CASE idx.type
                           WHEN '1' THEN 'clustered'
                           WHEN '2' THEN 'nonclustered'
                           ELSE NULL
                       END AS flags
                FROM sys.tables AS tbl
                JOIN sys.schemas AS scm ON tbl.schema_id = scm.schema_id
                JOIN sys.indexes AS idx ON tbl.object_id = idx.object_id
                JOIN sys.index_columns AS idxcol ON idx.object_id = idxcol.object_id AND idx.index_id = idxcol.index_id
                JOIN sys.columns AS col ON idxcol.object_id = col.object_id AND idxcol.column_id = col.column_id
                WHERE " . $this->getTableWhereClause($table, 'scm.name', 'tbl.name') . '
                ORDER BY idx.index_id ASC, idxcol.key_ordinal ASC';
    }

    /**
     * {@inheritDoc}
     */
    public function getCreateViewSQL($name, $sql)
    {
        return 'CREATE VIEW ' . $name . ' AS ' . $sql;
    }

    /**
     * {@inheritDoc}
     */
    public function getListViewsSQL($database)
    {
        return "SELECT name FROM sysobjects WHERE type = 'V' ORDER BY name";
    }

    /**
     * Returns the where clause to filter schema and table name in a query.
     *
     * @param string $table        The full qualified name of the table.
     * @param string $schemaColumn The name of the column to compare the schema to in the where clause.
     * @param string $tableColumn  The name of the column to compare the table to in the where clause.
     *
     * @return string
     */
    private function getTableWhereClause($table, $schemaColumn, $tableColumn)
    {
        if (strpos($table, '.') !== false) {
            [$schema, $table] = explode('.', $table);
            $schema           = $this->quoteStringLiteral($schema);
            $table            = $this->quoteStringLiteral($table);
        } else {
            $schema = 'SCHEMA_NAME()';
            $table  = $this->quoteStringLiteral($table);
        }

        return sprintf('(%s = %s AND %s = %s)', $tableColumn, $table, $schemaColumn, $schema);
    }

    /**
     * {@inheritDoc}
     */
    public function getDropViewSQL($name)
    {
        return 'DROP VIEW ' . $name;
    }

    /**
     * {@inheritDoc}
     *
     * @deprecated Use application-generated UUIDs instead
     */
    public function getGuidExpression()
    {
        return 'NEWID()';
    }

    /**
     * {@inheritDoc}
     */
    public function getLocateExpression($str, $substr, $startPos = false)
    {
        if ($startPos === false) {
            return 'CHARINDEX(' . $substr . ', ' . $str . ')';
        }

        return 'CHARINDEX(' . $substr . ', ' . $str . ', ' . $startPos . ')';
    }

    /**
     * {@inheritDoc}
     */
    public function getModExpression($expression1, $expression2)
    {
        return $expression1 . ' % ' . $expression2;
    }

    /**
     * {@inheritDoc}
     */
    public function getTrimExpression($str, $mode = TrimMode::UNSPECIFIED, $char = false)
    {
        if (! $char) {
            switch ($mode) {
                case TrimMode::LEADING:
                    $trimFn = 'LTRIM';
                    break;

                case TrimMode::TRAILING:
                    $trimFn = 'RTRIM';
                    break;

                default:
                    return 'LTRIM(RTRIM(' . $str . '))';
            }

            return $trimFn . '(' . $str . ')';
        }

        $pattern = "'%[^' + " . $char . " + ']%'";

        if ($mode === TrimMode::LEADING) {
            return 'stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str . ') - 1, null)';
        }

        if ($mode === TrimMode::TRAILING) {
            return 'reverse(stuff(reverse(' . $str . '), 1, '
                . 'patindex(' . $pattern . ', reverse(' . $str . ')) - 1, null))';
        }

        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))';
    }

    /**
     * {@inheritDoc}
     */
    public function getConcatExpression()
    {
        $args = func_get_args();

        return '(' . implode(' + ', $args) . ')';
    }

    /**
     * {@inheritDoc}
     */
    public function getListDatabasesSQL()
    {
        return 'SELECT * FROM sys.databases';
    }

    /**
     * {@inheritDoc}
     */
    public function getListNamespacesSQL()
    {
        return "SELECT name FROM sys.schemas WHERE name NOT IN('guest', 'INFORMATION_SCHEMA', 'sys')";
    }

    /**
     * {@inheritDoc}
     */
    public function getSubstringExpression($string, $start, $length = null)
    {
        if ($length !== null) {
            return 'SUBSTRING(' . $string . ', ' . $start . ', ' . $length . ')';
        }

        return 'SUBSTRING(' . $string . ', ' . $start . ', LEN(' . $string . ') - ' . $start . ' + 1)';
    }

    /**
     * {@inheritDoc}
     */
    public function getLengthExpression($column)
    {
        return 'LEN(' . $column . ')';
    }

    /**
     * {@inheritDoc}
     */
    public function getSetTransactionIsolationSQL($level)
    {
        return 'SET TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level);
    }

    /**
     * {@inheritDoc}
     */
    public function getIntegerTypeDeclarationSQL(array $column)
    {
        return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($column);
    }

    /**
     * {@inheritDoc}
     */
    public function getBigIntTypeDeclarationSQL(array $column)
    {
        return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($column);
    }

    /**
     * {@inheritDoc}
     */
    public function getSmallIntTypeDeclarationSQL(array $column)
    {
        return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($column);
    }

    /**
     * {@inheritDoc}
     */
    public function getGuidTypeDeclarationSQL(array $column)
    {
        return 'UNIQUEIDENTIFIER';
    }

    /**
     * {@inheritDoc}
     */
    public function getAsciiStringTypeDeclarationSQL(array $column): string
    {
        $length = $column['length'] ?? null;

        if (! isset($column['fixed'])) {
            return sprintf('VARCHAR(%d)', $length ?? 255);
        }

        return sprintf('CHAR(%d)', $length ?? 255);
    }

    /**
     * {@inheritDoc}
     */
    protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed)
    {
        return $fixed
            ? ($length ? 'NCHAR(' . $length . ')' : 'CHAR(255)')
            : ($length ? 'NVARCHAR(' . $length . ')' : 'NVARCHAR(255)');
    }

    /**
     * {@inheritdoc}
     */
    protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed)
    {
        return $fixed ? 'BINARY(' . ($length ?: 255) . ')' : 'VARBINARY(' . ($length ?: 255) . ')';
    }

    /**
     * {@inheritdoc}
     */
    public function getBinaryMaxLength()
    {
        return 8000;
    }

    /**
     * {@inheritDoc}
     */
    public function getClobTypeDeclarationSQL(array $column)
    {
        return 'VARCHAR(MAX)';
    }

    /**
     * {@inheritDoc}
     */
    protected function _getCommonIntegerTypeDeclarationSQL(array $column)
    {
        return ! empty($column['autoincrement']) ? ' IDENTITY' : '';
    }

    /**
     * {@inheritDoc}
     */
    public function getDateTimeTypeDeclarationSQL(array $column)
    {
        return 'DATETIME';
    }

    /**
     * {@inheritDoc}
     */
    public function getDateTypeDeclarationSQL(array $column)
    {
        return 'DATETIME';
    }

    /**
     * {@inheritDoc}
     */
    public function getTimeTypeDeclarationSQL(array $column)
    {
        return 'DATETIME';
    }

    /**
     * {@inheritDoc}
     */
    public function getBooleanTypeDeclarationSQL(array $column)
    {
        return 'BIT';
    }

    /**
     * {@inheritDoc}
     */
    protected function doModifyLimitQuery($query, $limit, $offset = null)
    {
        $where = [];

        if ($offset > 0) {
            $where[] = sprintf('doctrine_rownum >= %d', $offset + 1);
        }

        if ($limit !== null) {
            $where[] = sprintf('doctrine_rownum <= %d', $offset + $limit);
            $top     = sprintf('TOP %d', $offset + $limit);
        } else {
            $top = 'TOP 9223372036854775807';
        }

        if (empty($where)) {
            return $query;
        }

        // We'll find a SELECT or SELECT distinct and prepend TOP n to it
        // Even if the TOP n is very large, the use of a CTE will
        // allow the SQL Server query planner to optimize it so it doesn't
        // actually scan the entire range covered by the TOP clause.
        if (! preg_match('/^(\s*SELECT\s+(?:DISTINCT\s+)?)(.*)$/is', $query, $matches)) {
            return $query;
        }

        $query = $matches[1] . $top . ' ' . $matches[2];

        if (stristr($query, 'ORDER BY')) {
            // Inner order by is not valid in SQL Server for our purposes
            // unless it's in a TOP N subquery.
            $query = $this->scrubInnerOrderBy($query);
        }

        // Build a new limited query around the original, using a CTE
        return sprintf(
            'WITH dctrn_cte AS (%s) '
            . 'SELECT * FROM ('
            . 'SELECT *, ROW_NUMBER() OVER (ORDER BY (SELECT 0)) AS doctrine_rownum FROM dctrn_cte'
            . ') AS doctrine_tbl '
            . 'WHERE %s ORDER BY doctrine_rownum ASC',
            $query,
            implode(' AND ', $where)
        );
    }

    /**
     * Remove ORDER BY clauses in subqueries - they're not supported by SQL Server.
     * Caveat: will leave ORDER BY in TOP N subqueries.
     *
     * @param string $query
     *
     * @return string
     */
    private function scrubInnerOrderBy($query)
    {
        $count  = substr_count(strtoupper($query), 'ORDER BY');
        $offset = 0;

        while ($count-- > 0) {
            $orderByPos = stripos($query, ' ORDER BY', $offset);
            if ($orderByPos === false) {
                break;
            }

            $qLen            = strlen($query);
            $parenCount      = 0;
            $currentPosition = $orderByPos;

            while ($parenCount >= 0 && $currentPosition < $qLen) {
                if ($query[$currentPosition] === '(') {
                    $parenCount++;
                } elseif ($query[$currentPosition] === ')') {
                    $parenCount--;
                }

                $currentPosition++;
            }

            if ($this->isOrderByInTopNSubquery($query, $orderByPos)) {
                // If the order by clause is in a TOP N subquery, do not remove
                // it and continue iteration from the current position.
                $offset = $currentPosition;
                continue;
            }

            if ($currentPosition >= $qLen - 1) {
                continue;
            }

            $query  = substr($query, 0, $orderByPos) . substr($query, $currentPosition - 1);
            $offset = $orderByPos;
        }

        return $query;
    }

    /**
     * Check an ORDER BY clause to see if it is in a TOP N query or subquery.
     *
     * @param string $query           The query
     * @param int    $currentPosition Start position of ORDER BY clause
     *
     * @return bool true if ORDER BY is in a TOP N query, false otherwise
     */
    private function isOrderByInTopNSubquery($query, $currentPosition)
    {
        // Grab query text on the same nesting level as the ORDER BY clause we're examining.
        $subQueryBuffer = '';
        $parenCount     = 0;

        // If $parenCount goes negative, we've exited the subquery we're examining.
        // If $currentPosition goes negative, we've reached the beginning of the query.
        while ($parenCount >= 0 && $currentPosition >= 0) {
            if ($query[$currentPosition] === '(') {
                $parenCount--;
            } elseif ($query[$currentPosition] === ')') {
                $parenCount++;
            }

            // Only yank query text on the same nesting level as the ORDER BY clause.
            $subQueryBuffer = ($parenCount === 0 ? $query[$currentPosition] : ' ') . $subQueryBuffer;

            $currentPosition--;
        }

        return (bool) preg_match('/SELECT\s+(DISTINCT\s+)?TOP\s/i', $subQueryBuffer);
    }

    /**
     * {@inheritDoc}
     */
    public function supportsLimitOffset()
    {
        return false;
    }

    /**
     * {@inheritDoc}
     */
    public function convertBooleans($item)
    {
        if (is_array($item)) {
            foreach ($item as $key => $value) {
                if (! is_bool($value) && ! is_numeric($value)) {
                    continue;
                }

                $item[$key] = $value ? 1 : 0;
            }
        } elseif (is_bool($item) || is_numeric($item)) {
            $item = $item ? 1 : 0;
        }

        return $item;
    }

    /**
     * {@inheritDoc}
     */
    public function getCreateTemporaryTableSnippetSQL()
    {
        return 'CREATE TABLE';
    }

    /**
     * {@inheritDoc}
     */
    public function getTemporaryTableName($tableName)
    {
        return '#' . $tableName;
    }

    /**
     * {@inheritDoc}
     */
    public function getDateTimeFormatString()
    {
        return 'Y-m-d H:i:s.000';
    }

    /**
     * {@inheritDoc}
     */
    public function getDateFormatString()
    {
        return 'Y-m-d H:i:s.000';
    }

    /**
     * {@inheritDoc}
     */
    public function getTimeFormatString()
    {
        return 'Y-m-d H:i:s.000';
    }

    /**
     * {@inheritDoc}
     */
    public function getDateTimeTzFormatString()
    {
        return $this->getDateTimeFormatString();
    }

    /**
     * {@inheritDoc}
     */
    public function getName()
    {
        return 'mssql';
    }

    /**
     * {@inheritDoc}
     */
    protected function initializeDoctrineTypeMappings()
    {
        $this->doctrineTypeMapping = [
            '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',
            'smalldatetime' => 'datetime',
            'datetime' => 'datetime',
            'char' => 'string',
            'varchar' => 'string',
            'text' => 'text',
            'nchar' => 'string',
            'nvarchar' => 'string',
            'ntext' => 'text',
            'binary' => 'binary',
            'varbinary' => 'binary',
            'image' => 'blob',
            'uniqueidentifier' => 'guid',
        ];
    }

    /**
     * {@inheritDoc}
     */
    public function createSavePoint($savepoint)
    {
        return 'SAVE TRANSACTION ' . $savepoint;
    }

    /**
     * {@inheritDoc}
     */
    public function releaseSavePoint($savepoint)
    {
        return '';
    }

    /**
     * {@inheritDoc}
     */
    public function rollbackSavePoint($savepoint)
    {
        return 'ROLLBACK TRANSACTION ' . $savepoint;
    }

    /**
     * {@inheritdoc}
     */
    public function getForeignKeyReferentialActionSQL($action)
    {
        // RESTRICT is not supported, therefore falling back to NO ACTION.
        if (strtoupper($action) === 'RESTRICT') {
            return 'NO ACTION';
        }

        return parent::getForeignKeyReferentialActionSQL($action);
    }

    /**
     * {@inheritDoc}
     */
    public function appendLockHint($fromClause, $lockMode)
    {
        switch (true) {
            case $lockMode === LockMode::NONE:
                return $fromClause;

            case $lockMode === LockMode::PESSIMISTIC_READ:
                return $fromClause . ' WITH (HOLDLOCK, ROWLOCK)';

            case $lockMode === LockMode::PESSIMISTIC_WRITE:
                return $fromClause . ' WITH (UPDLOCK, ROWLOCK)';

            default:
                return $fromClause;
        }
    }

    /**
     * {@inheritDoc}
     */
    public function getForUpdateSQL()
    {
        return ' ';
    }

    /**
     * {@inheritDoc}
     */
    protected function getReservedKeywordsClass()
    {
        return Keywords\SQLServerKeywords::class;
    }

    /**
     * {@inheritDoc}
     */
    public function quoteSingleIdentifier($str)
    {
        return '[' . str_replace(']', ']]', $str) . ']';
    }

    /**
     * {@inheritDoc}
     */
    public function getTruncateTableSQL($tableName, $cascade = false)
    {
        $tableIdentifier = new Identifier($tableName);

        return 'TRUNCATE TABLE ' . $tableIdentifier->getQuotedName($this);
    }

    /**
     * {@inheritDoc}
     */
    public function getBlobTypeDeclarationSQL(array $column)
    {
        return 'VARBINARY(MAX)';
    }

    /**
     * {@inheritdoc}
     *
     * Modifies column declaration order as it differs in Microsoft SQL Server.
     */
    public function getColumnDeclarationSQL($name, array $column)
    {
        if (isset($column['columnDefinition'])) {
            $columnDef = $this->getCustomTypeDeclarationSQL($column);
        } else {
            $collation = isset($column['collation']) && $column['collation'] ?
                ' ' . $this->getColumnCollationDeclarationSQL($column['collation']) : '';

            $notnull = isset($column['notnull']) && $column['notnull'] ? ' NOT NULL' : '';

            $unique = isset($column['unique']) && $column['unique'] ?
                ' ' . $this->getUniqueFieldDeclarationSQL() : '';

            $check = isset($column['check']) && $column['check'] ?
                ' ' . $column['check'] : '';

            $typeDecl  = $column['type']->getSQLDeclaration($column, $this);
            $columnDef = $typeDecl . $collation . $notnull . $unique . $check;
        }

        return $name . ' ' . $columnDef;
    }

    /**
     * Returns a unique default constraint name for a table and column.
     *
     * @param string $table  Name of the table to generate the unique default constraint name for.
     * @param string $column Name of the column in the table to generate the unique default constraint name for.
     *
     * @return string
     */
    private function generateDefaultConstraintName($table, $column)
    {
        return 'DF_' . $this->generateIdentifierName($table) . '_' . $this->generateIdentifierName($column);
    }

    /**
     * Returns a hash value for a given identifier.
     *
     * @param string $identifier Identifier to generate a hash value for.
     *
     * @return string
     */
    private function generateIdentifierName($identifier)
    {
        // Always generate name for unquoted identifiers to ensure consistency.
        $identifier = new Identifier($identifier);

        return strtoupper(dechex(crc32($identifier->getName())));
    }

    protected function getCommentOnTableSQL(string $tableName, ?string $comment): string
    {
        return sprintf(
            "
                EXEC sys.sp_addextendedproperty @name=N'MS_Description',
                  @value=N%s, @level0type=N'SCHEMA', @level0name=N'dbo',
                  @level1type=N'TABLE', @level1name=N%s
            ",
            $this->quoteStringLiteral((string) $comment),
            $this->quoteStringLiteral($tableName)
        );
    }

    public function getListTableMetadataSQL(string $table): string
    {
        return sprintf(
            "
                SELECT
                  p.value AS [table_comment]
                FROM
                  sys.tables AS tbl
                  INNER JOIN sys.extended_properties AS p ON p.major_id=tbl.object_id AND p.minor_id=0 AND p.class=1
                WHERE
                  (tbl.name=N%s and SCHEMA_NAME(tbl.schema_id)=N'dbo' and p.name=N'MS_Description')
            ",
            $this->quoteStringLiteral($table)
        );
    }
}
<?php

namespace Doctrine\DBAL\Platforms;

use Doctrine\DBAL\Schema\Identifier;
use Doctrine\DBAL\Schema\Index;
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Schema\TableDiff;
use Doctrine\DBAL\Types\BinaryType;
use InvalidArgumentException;

use function array_merge;
use function array_unique;
use function array_values;
use function count;
use function func_get_args;
use function implode;
use function is_array;
use function is_bool;
use function is_numeric;
use function is_string;
use function sprintf;
use function trim;

/**
 * Drizzle platform
 *
 * @deprecated
 */
class DrizzlePlatform extends AbstractPlatform
{
    /**
     * {@inheritDoc}
     */
    public function getName()
    {
        return 'drizzle';
    }

    /**
     * {@inheritDoc}
     */
    public function getIdentifierQuoteCharacter()
    {
        return '`';
    }

    /**
     * {@inheritDoc}
     */
    public function getConcatExpression()
    {
        return 'CONCAT(' . implode(', ', func_get_args()) . ')';
    }

    /**
     * {@inheritdoc}
     */
    protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit)
    {
        $function = $operator === '+' ? 'DATE_ADD' : 'DATE_SUB';

        return $function . '(' . $date . ', INTERVAL ' . $interval . ' ' . $unit . ')';
    }

    /**
     * {@inheritDoc}
     */
    public function getDateDiffExpression($date1, $date2)
    {
        return 'DATEDIFF(' . $date1 . ', ' . $date2 . ')';
    }

    /**
     * {@inheritDoc}
     */
    public function getBooleanTypeDeclarationSQL(array $column)
    {
        return 'BOOLEAN';
    }

    /**
     * {@inheritDoc}
     */
    public function getIntegerTypeDeclarationSQL(array $column)
    {
        return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($column);
    }

    /**
     * {@inheritDoc}
     */
    protected function _getCommonIntegerTypeDeclarationSQL(array $column)
    {
        $autoinc = '';
        if (! empty($column['autoincrement'])) {
            $autoinc = ' AUTO_INCREMENT';
        }

        return $autoinc;
    }

    /**
     * {@inheritDoc}
     */
    public function getBigIntTypeDeclarationSQL(array $column)
    {
        return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($column);
    }

    /**
     * {@inheritDoc}
     */
    public function getSmallIntTypeDeclarationSQL(array $column)
    {
        return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($column);
    }

    /**
     * {@inheritDoc}
     */
    protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed)
    {
        return $length ? 'VARCHAR(' . $length . ')' : 'VARCHAR(255)';
    }

    /**
     * {@inheritdoc}
     */
    protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed)
    {
        return 'VARBINARY(' . ($length ?: 255) . ')';
    }

    /**
     * {@inheritDoc}
     */
    protected function initializeDoctrineTypeMappings()
    {
        $this->doctrineTypeMapping = [
            'boolean'       => 'boolean',
            'varchar'       => 'string',
            'varbinary'     => 'binary',
            'integer'       => 'integer',
            'blob'          => 'blob',
            'decimal'       => 'decimal',
            'datetime'      => 'datetime',
            'date'          => 'date',
            'time'          => 'time',
            'text'          => 'text',
            'timestamp'     => 'datetime',
            'double'        => 'float',
            'bigint'        => 'bigint',
        ];
    }

    /**
     * {@inheritDoc}
     */
    public function getClobTypeDeclarationSQL(array $column)
    {
        return 'TEXT';
    }

    /**
     * {@inheritDoc}
     */
    public function getBlobTypeDeclarationSQL(array $column)
    {
        return 'BLOB';
    }

    /**
     * {@inheritDoc}
     */
    public function getCreateDatabaseSQL($name)
    {
        return 'CREATE DATABASE ' . $name;
    }

    /**
     * {@inheritDoc}
     */
    public function getDropDatabaseSQL($name)
    {
        return 'DROP DATABASE ' . $name;
    }

    /**
     * {@inheritDoc}
     */
    protected function _getCreateTableSQL($name, array $columns, array $options = [])
    {
        $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 ' . $name . ' (' . $queryFields . ') ';
        $query .= $this->buildTableOptions($options);
        $query .= $this->buildPartitionOptions($options);

        $sql = [$query];

        if (isset($options['foreignKeys'])) {
            foreach ((array) $options['foreignKeys'] as $definition) {
                $sql[] = $this->getCreateForeignKeySQL($definition, $name);
            }
        }

        return $sql;
    }

    /**
     * Build SQL for table options
     *
     * @param mixed[] $options
     *
     * @return string
     */
    private function buildTableOptions(array $options)
    {
        if (isset($options['table_options'])) {
            return $options['table_options'];
        }

        $tableOptions = [];

        // Collate
        if (! isset($options['collate'])) {
            $options['collate'] = 'utf8_unicode_ci';
        }

        $tableOptions[] = sprintf('COLLATE %s', $options['collate']);

        // Engine
        if (! isset($options['engine'])) {
            $options['engine'] = 'InnoDB';
        }

        $tableOptions[] = sprintf('ENGINE = %s', $options['engine']);

        // Auto increment
        if (isset($options['auto_increment'])) {
            $tableOptions[] = sprintf('AUTO_INCREMENT = %s', $options['auto_increment']);
        }

        // Comment
        if (isset($options['comment'])) {
            $comment = trim($options['comment'], " '");

            $tableOptions[] = sprintf('COMMENT = %s ', $this->quoteStringLiteral($comment));
        }

        // Row format
        if (isset($options['row_format'])) {
            $tableOptions[] = sprintf('ROW_FORMAT = %s', $options['row_format']);
        }

        return implode(' ', $tableOptions);
    }

    /**
     * Build SQL for partition options.
     *
     * @param mixed[] $options
     *
     * @return string
     */
    private function buildPartitionOptions(array $options)
    {
        return isset($options['partition_options'])
            ? ' ' . $options['partition_options']
            : '';
    }

    /**
     * {@inheritDoc}
     */
    public function getListDatabasesSQL()
    {
        return "SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE CATALOG_NAME='LOCAL'";
    }

    /**
     * {@inheritDoc}
     */
    protected function getReservedKeywordsClass()
    {
        return Keywords\DrizzleKeywords::class;
    }

    /**
     * {@inheritDoc}
     */
    public function getListTablesSQL()
    {
        return "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE='BASE' AND TABLE_SCHEMA=DATABASE()";
    }

    /**
     * {@inheritDoc}
     */
    public function getListTableColumnsSQL($table, $database = null)
    {
        if ($database) {
            $databaseSQL = $this->quoteStringLiteral($database);
        } else {
            $databaseSQL = 'DATABASE()';
        }

        return 'SELECT COLUMN_NAME, DATA_TYPE, COLUMN_COMMENT, IS_NULLABLE, IS_AUTO_INCREMENT,' .
               ' CHARACTER_MAXIMUM_LENGTH, COLUMN_DEFAULT, NUMERIC_PRECISION, NUMERIC_SCALE, COLLATION_NAME' .
               ' FROM DATA_DICTIONARY.COLUMNS' .
               ' WHERE TABLE_SCHEMA=' . $databaseSQL . ' AND TABLE_NAME = ' . $this->quoteStringLiteral($table);
    }

    /**
     * @param string      $table
     * @param string|null $database
     *
     * @return string
     */
    public function getListTableForeignKeysSQL($table, $database = null)
    {
        if ($database) {
            $databaseSQL = $this->quoteStringLiteral($database);
        } else {
            $databaseSQL = 'DATABASE()';
        }

        return 'SELECT CONSTRAINT_NAME, CONSTRAINT_COLUMNS, REFERENCED_TABLE_NAME, REFERENCED_TABLE_COLUMNS,'
            . ' UPDATE_RULE, DELETE_RULE'
            . ' FROM DATA_DICTIONARY.FOREIGN_KEYS'
            . ' WHERE CONSTRAINT_SCHEMA=' . $databaseSQL
            . ' AND CONSTRAINT_TABLE=' . $this->quoteStringLiteral($table);
    }

    /**
     * {@inheritDoc}
     */
    public function getListTableIndexesSQL($table, $database = null)
    {
        if ($database) {
            $databaseSQL = $this->quoteStringLiteral($database);
        } else {
            $databaseSQL = 'DATABASE()';
        }

        return "SELECT INDEX_NAME AS 'key_name',"
            . " COLUMN_NAME AS 'column_name',"
            . " IS_USED_IN_PRIMARY AS 'primary',"
            . " IS_UNIQUE=0 AS 'non_unique'"
            . ' FROM DATA_DICTIONARY.INDEX_PARTS'
            . ' WHERE TABLE_SCHEMA=' . $databaseSQL . ' AND TABLE_NAME=' . $this->quoteStringLiteral($table);
    }

    /**
     * {@inheritDoc}
     */
    public function prefersIdentityColumns()
    {
        return true;
    }

    /**
     * {@inheritDoc}
     */
    public function supportsIdentityColumns()
    {
        return true;
    }

    /**
     * {@inheritDoc}
     */
    public function supportsInlineColumnComments()
    {
        return true;
    }

    /**
     * {@inheritDoc}
     */
    public function supportsViews()
    {
        return false;
    }

    /**
     * {@inheritdoc}
     */
    public function supportsColumnCollation()
    {
        return true;
    }

    /**
     * {@inheritDoc}
     */
    public function getDropIndexSQL($index, $table = null)
    {
        if ($index instanceof Index) {
            $indexName = $index->getQuotedName($this);
        } elseif (is_string($index)) {
            $indexName = $index;
        } else {
            throw new InvalidArgumentException(
                __METHOD__ . '() expects $index parameter to be string or ' . Index::class . '.'
            );
        }

        if ($table instanceof Table) {
            $table = $table->getQuotedName($this);
        } elseif (! is_string($table)) {
            throw new InvalidArgumentException(
                __METHOD__ . '() expects $table parameter to be string or ' . Table::class . '.'
            );
        }

        if ($index instanceof Index && $index->isPrimary()) {
            // drizzle primary keys are always named "PRIMARY",
            // so we cannot use them in statements because of them being keyword.
            return $this->getDropPrimaryKeySQL($table);
        }

        return 'DROP INDEX ' . $indexName . ' ON ' . $table;
    }

    /**
     * @param string $table
     *
     * @return string
     */
    protected function getDropPrimaryKeySQL($table)
    {
        return 'ALTER TABLE ' . $table . ' DROP PRIMARY KEY';
    }

    /**
     * {@inheritDoc}
     */
    public function getDateTimeTypeDeclarationSQL(array $column)
    {
        if (isset($column['version']) && $column['version'] === true) {
            return 'TIMESTAMP';
        }

        return 'DATETIME';
    }

    /**
     * {@inheritDoc}
     */
    public function getTimeTypeDeclarationSQL(array $column)
    {
        return 'TIME';
    }

    /**
     * {@inheritDoc}
     */
    public function getDateTypeDeclarationSQL(array $column)
    {
        return 'DATE';
    }

    /**
     * {@inheritDoc}
     */
    public function getAlterTableSQL(TableDiff $diff)
    {
        $columnSql  = [];
        $queryParts = [];

        $newName = $diff->getNewName();

        if ($newName !== false) {
            $queryParts[] = 'RENAME TO ' . $newName->getQuotedName($this);
        }

        foreach ($diff->addedColumns as $column) {
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
                continue;
            }

            $columnArray = array_merge($column->toArray(), [
                'comment' => $this->getColumnComment($column),
            ]);

            $queryParts[] = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnArray);
        }

        foreach ($diff->removedColumns as $column) {
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
                continue;
            }

            $queryParts[] =  'DROP ' . $column->getQuotedName($this);
        }

        foreach ($diff->changedColumns as $columnDiff) {
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
                continue;
            }

            $column      = $columnDiff->column;
            $columnArray = $column->toArray();

            // Do not generate column alteration clause if type is binary and only fixed property has changed.
            // Drizzle only supports binary type columns with variable length.
            // Avoids unnecessary table alteration statements.
            if (
                $columnArray['type'] instanceof BinaryType &&
                $columnDiff->hasChanged('fixed') &&
                count($columnDiff->changedProperties) === 1
            ) {
                continue;
            }

            $columnArray['comment'] = $this->getColumnComment($column);
            $queryParts[]           =  'CHANGE ' . ($columnDiff->getOldColumnName()->getQuotedName($this)) . ' '
                    . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnArray);
        }

        foreach ($diff->renamedColumns as $oldColumnName => $column) {
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
                continue;
            }

            $oldColumnName = new Identifier($oldColumnName);

            $columnArray            = $column->toArray();
            $columnArray['comment'] = $this->getColumnComment($column);
            $queryParts[]           =  'CHANGE ' . $oldColumnName->getQuotedName($this) . ' '
                    . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnArray);
        }

        $sql      = [];
        $tableSql = [];

        if (! $this->onSchemaAlterTable($diff, $tableSql)) {
            if (count($queryParts) > 0) {
                $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this)
                    . ' ' . implode(', ', $queryParts);
            }

            $sql = array_merge(
                $this->getPreAlterTableIndexForeignKeySQL($diff),
                $sql,
                $this->getPostAlterTableIndexForeignKeySQL($diff)
            );
        }

        return array_merge($sql, $tableSql, $columnSql);
    }

    /**
     * {@inheritDoc}
     */
    public function getDropTemporaryTableSQL($table)
    {
        if ($table instanceof Table) {
            $table = $table->getQuotedName($this);
        } elseif (! is_string($table)) {
            throw new InvalidArgumentException(
                __METHOD__ . '() expects $table parameter to be string or ' . Table::class . '.'
            );
        }

        return 'DROP TEMPORARY TABLE ' . $table;
    }

    /**
     * {@inheritDoc}
     */
    public function convertBooleans($item)
    {
        if (is_array($item)) {
            foreach ($item as $key => $value) {
                if (! is_bool($value) && ! is_numeric($value)) {
                    continue;
                }

                $item[$key] = $value ? 'true' : 'false';
            }
        } elseif (is_bool($item) || is_numeric($item)) {
            $item = $item ? 'true' : 'false';
        }

        return $item;
    }

    /**
     * {@inheritDoc}
     */
    public function getLocateExpression($str, $substr, $startPos = false)
    {
        if ($startPos === false) {
            return 'LOCATE(' . $substr . ', ' . $str . ')';
        }

        return 'LOCATE(' . $substr . ', ' . $str . ', ' . $startPos . ')';
    }

    /**
     * {@inheritDoc}
     *
     * @deprecated Use application-generated UUIDs instead
     */
    public function getGuidExpression()
    {
        return 'UUID()';
    }

    /**
     * {@inheritDoc}
     */
    public function getRegexpExpression()
    {
        return 'RLIKE';
    }
}
<?php

namespace Doctrine\DBAL\Platforms;

/**
 * Platform to ensure compatibility of Doctrine with Microsoft SQL Server 2005 version and
 * higher.
 *
 * Differences to SQL Server 2008 are:
 *
 * - DATETIME2 datatype does not exist, only DATETIME which has a precision of
 *   3. This is not supported by PHP DateTime, so we are emulating it by
 *   setting .000 manually.
 * - Starting with SQLServer2005 VARCHAR(MAX), VARBINARY(MAX) and
 *   NVARCHAR(max) replace the old TEXT, NTEXT and IMAGE types. See
 *   {@link http://www.sql-server-helper.com/faq/sql-server-2005-varchar-max-p01.aspx}
 *   for more information.
 *
 * @deprecated Use SQL Server 2012 or newer
 */
class SQLServer2005Platform extends SQLServerPlatform
{
    /**
     * {@inheritDoc}
     */
    public function supportsLimitOffset()
    {
        return true;
    }

    /**
     * {@inheritDoc}
     */
    public function getClobTypeDeclarationSQL(array $column)
    {
        return 'VARCHAR(MAX)';
    }

    /**
     * {@inheritdoc}
     *
     * Returns Microsoft SQL Server 2005 specific keywords class
     */
    protected function getReservedKeywordsClass()
    {
        return Keywords\SQLServer2005Keywords::class;
    }
}
<?php

namespace Doctrine\DBAL\Platforms;

/**
 * Provides the behavior, features and SQL dialect of the MySQL 8.0 (8.0 GA) database platform.
 */
class MySQL80Platform extends MySQL57Platform
{
    /**
     * {@inheritdoc}
     */
    protected function getReservedKeywordsClass()
    {
        return Keywords\MySQL80Keywords::class;
    }
}
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Platforms;

final class TrimMode
{
    public const UNSPECIFIED = 0;

    public const LEADING = 1;

    public const TRAILING = 2;

    public const BOTH = 3;

    /**
     * @codeCoverageIgnore
     */
    private function __construct()
    {
    }
}
<?php

namespace Doctrine\DBAL\Platforms;

use Doctrine\DBAL\Schema\Sequence;

use function preg_match;
use function preg_match_all;
use function substr_count;

use const PREG_OFFSET_CAPTURE;

/**
 * Platform to ensure compatibility of Doctrine with Microsoft SQL Server 2012 version.
 *
 * Differences to SQL Server 2008 and before are that sequences are introduced,
 * and support for the new OFFSET... FETCH syntax for result pagination has been added.
 */
class SQLServer2012Platform extends SQLServer2008Platform
{
    /**
     * {@inheritdoc}
     */
    public function getAlterSequenceSQL(Sequence $sequence)
    {
        return 'ALTER SEQUENCE ' . $sequence->getQuotedName($this) .
               ' INCREMENT BY ' . $sequence->getAllocationSize();
    }

    /**
     * {@inheritdoc}
     */
    public function getCreateSequenceSQL(Sequence $sequence)
    {
        return 'CREATE SEQUENCE ' . $sequence->getQuotedName($this) .
               ' START WITH ' . $sequence->getInitialValue() .
               ' INCREMENT BY ' . $sequence->getAllocationSize() .
               ' MINVALUE ' . $sequence->getInitialValue();
    }

    /**
     * {@inheritdoc}
     */
    public function getDropSequenceSQL($sequence)
    {
        if ($sequence instanceof Sequence) {
            $sequence = $sequence->getQuotedName($this);
        }

        return 'DROP SEQUENCE ' . $sequence;
    }

    /**
     * {@inheritdoc}
     */
    public function getListSequencesSQL($database)
    {
        return 'SELECT seq.name,
                       CAST(
                           seq.increment AS VARCHAR(MAX)
                       ) AS increment, -- CAST avoids driver error for sql_variant type
                       CAST(
                           seq.start_value AS VARCHAR(MAX)
                       ) AS start_value -- CAST avoids driver error for sql_variant type
                FROM   sys.sequences AS seq';
    }

    /**
     * {@inheritdoc}
     */
    public function getSequenceNextValSQL($sequence)
    {
        return 'SELECT NEXT VALUE FOR ' . $sequence;
    }

    /**
     * {@inheritdoc}
     */
    public function supportsSequences()
    {
        return true;
    }

    /**
     * {@inheritdoc}
     *
     * Returns Microsoft SQL Server 2012 specific keywords class
     */
    protected function getReservedKeywordsClass()
    {
        return Keywords\SQLServer2012Keywords::class;
    }

    /**
     * {@inheritdoc}
     */
    protected function doModifyLimitQuery($query, $limit, $offset = null)
    {
        if ($limit === null && $offset <= 0) {
            return $query;
        }

        // Queries using OFFSET... FETCH MUST have an ORDER BY clause
        if ($this->shouldAddOrderBy($query)) {
            if (preg_match('/^SELECT\s+DISTINCT/im', $query)) {
                // SQL Server won't let us order by a non-selected column in a DISTINCT query,
                // so we have to do this madness. This says, order by the first column in the
                // result. SQL Server's docs say that a nonordered query's result order is non-
                // deterministic anyway, so this won't do anything that a bunch of update and
                // deletes to the table wouldn't do anyway.
                $query .= ' ORDER BY 1';
            } else {
                // In another DBMS, we could do ORDER BY 0, but SQL Server gets angry if you
                // use constant expressions in the order by list.
                $query .= ' ORDER BY (SELECT 0)';
            }
        }

        if ($offset === null) {
            $offset = 0;
        }

        // This looks somewhat like MYSQL, but limit/offset are in inverse positions
        // Supposedly SQL:2008 core standard.
        // Per TSQL spec, FETCH NEXT n ROWS ONLY is not valid without OFFSET n ROWS.
        $query .= ' OFFSET ' . (int) $offset . ' ROWS';

        if ($limit !== null) {
            $query .= ' FETCH NEXT ' . (int) $limit . ' ROWS ONLY';
        }

        return $query;
    }

    /**
     * @param string $query
     */
    private function shouldAddOrderBy($query): bool
    {
        // Find the position of the last instance of ORDER BY and ensure it is not within a parenthetical statement
        // but can be in a newline
        $matches      = [];
        $matchesCount = preg_match_all('/[\\s]+order\\s+by\\s/im', $query, $matches, PREG_OFFSET_CAPTURE);
        if ($matchesCount === 0) {
            return true;
        }

        // ORDER BY instance may be in a subquery after ORDER BY
        // e.g. SELECT col1 FROM test ORDER BY (SELECT col2 from test ORDER BY col2)
        // if in the searched query ORDER BY clause was found where
        // number of open parentheses after the occurrence of the clause is equal to
        // number of closed brackets after the occurrence of the clause,
        // it means that ORDER BY is included in the query being checked
        while ($matchesCount > 0) {
            $orderByPos          = $matches[0][--$matchesCount][1];
            $openBracketsCount   = substr_count($query, '(', $orderByPos);
            $closedBracketsCount = substr_count($query, ')', $orderByPos);
            if ($openBracketsCount === $closedBracketsCount) {
                return false;
            }
        }

        return true;
    }
}
<?php

namespace Doctrine\DBAL\Platforms;

/**
 * Platform to ensure compatibility of Doctrine with Microsoft SQL Server 2008 version.
 *
 * Differences to SQL Server 2005 and before are that a new DATETIME2 type was
 * introduced that has a higher precision.
 *
 * @deprecated Use SQL Server 2012 or newer
 */
class SQLServer2008Platform extends SQLServer2005Platform
{
    /**
     * {@inheritDoc}
     */
    public function getListTablesSQL()
    {
        // "sysdiagrams" table must be ignored as it's internal SQL Server table for Database Diagrams
        // Category 2 must be ignored as it is "MS SQL Server 'pseudo-system' object[s]" for replication
        return 'SELECT name, SCHEMA_NAME (uid) AS schema_name FROM sysobjects'
            . " WHERE type = 'U' AND name != 'sysdiagrams' AND category != 2 ORDER BY name";
    }

    /**
     * {@inheritDoc}
     */
    public function getDateTimeTypeDeclarationSQL(array $column)
    {
        // 3 - microseconds precision length
        // http://msdn.microsoft.com/en-us/library/ms187819.aspx
        return 'DATETIME2(6)';
    }

    /**
     * {@inheritDoc}
     */
    public function getDateTypeDeclarationSQL(array $column)
    {
        return 'DATE';
    }

    /**
     * {@inheritDoc}
     */
    public function getTimeTypeDeclarationSQL(array $column)
    {
        return 'TIME(0)';
    }

    /**
     * {@inheritDoc}
     */
    public function getDateTimeTzTypeDeclarationSQL(array $column)
    {
        return 'DATETIMEOFFSET(6)';
    }

    /**
     * {@inheritDoc}
     */
    public function getDateTimeFormatString()
    {
        return 'Y-m-d H:i:s.u';
    }

    /**
     * {@inheritDoc}
     */
    public function getDateTimeTzFormatString()
    {
        return 'Y-m-d H:i:s.u P';
    }

    /**
     * {@inheritDoc}
     */
    public function getDateFormatString()
    {
        return 'Y-m-d';
    }

    /**
     * {@inheritDoc}
     */
    public function getTimeFormatString()
    {
        return 'H:i:s';
    }

    /**
     * {@inheritDoc}
     *
     * Adding Datetime2 Type
     */
    protected function initializeDoctrineTypeMappings()
    {
        parent::initializeDoctrineTypeMappings();
        $this->doctrineTypeMapping['datetime2']      = 'datetime';
        $this->doctrineTypeMapping['date']           = 'date';
        $this->doctrineTypeMapping['time']           = 'time';
        $this->doctrineTypeMapping['datetimeoffset'] = 'datetimetz';
    }

    /**
     * {@inheritdoc}
     *
     * Returns Microsoft SQL Server 2008 specific keywords class
     */
    protected function getReservedKeywordsClass()
    {
        return Keywords\SQLServer2008Keywords::class;
    }

    protected function getLikeWildcardCharacters(): string
    {
        return parent::getLikeWildcardCharacters() . '[]^';
    }
}
<?php

namespace Doctrine\DBAL\Platforms;

use Doctrine\DBAL\Schema\Table;

/**
 * Platform to ensure compatibility of Doctrine with SQL Azure
 *
 * On top of SQL Server 2008 the following functionality is added:
 *
 * - Create tables with the FEDERATED ON syntax.
 *
 * @deprecated
 */
class SQLAzurePlatform extends SQLServer2008Platform
{
    /**
     * {@inheritDoc}
     */
    public function getCreateTableSQL(Table $table, $createFlags = self::CREATE_INDEXES)
    {
        $sql = parent::getCreateTableSQL($table, $createFlags);

        if ($table->hasOption('azure.federatedOnColumnName')) {
            $distributionName = $table->getOption('azure.federatedOnDistributionName');
            $columnName       = $table->getOption('azure.federatedOnColumnName');
            $stmt             = ' FEDERATED ON (' . $distributionName . ' = ' . $columnName . ')';

            $sql[0] .= $stmt;
        }

        return $sql;
    }
}
<?php

namespace Doctrine\DBAL\Platforms;

use Doctrine\DBAL\Schema\Index;
use Doctrine\DBAL\Schema\Sequence;

/**
 * The SQLAnywhere12Platform provides the behavior, features and SQL dialect of the
 * SAP Sybase SQL Anywhere 12 database platform.
 *
 * @deprecated Support for SQLAnywhere will be removed in 3.0.
 */
class SQLAnywhere12Platform extends SQLAnywhere11Platform
{
    /**
     * {@inheritdoc}
     */
    public function getCreateSequenceSQL(Sequence $sequence)
    {
        return 'CREATE SEQUENCE ' . $sequence->getQuotedName($this) .
            ' INCREMENT BY ' . $sequence->getAllocationSize() .
            ' START WITH ' . $sequence->getInitialValue() .
            ' MINVALUE ' . $sequence->getInitialValue();
    }

    /**
     * {@inheritdoc}
     */
    public function getAlterSequenceSQL(Sequence $sequence)
    {
        return 'ALTER SEQUENCE ' . $sequence->getQuotedName($this) .
            ' INCREMENT BY ' . $sequence->getAllocationSize();
    }

    /**
     * {@inheritdoc}
     */
    public function getDateTimeTzFormatString()
    {
        return 'Y-m-d H:i:s.uP';
    }

    /**
     * {@inheritdoc}
     */
    public function getDateTimeTzTypeDeclarationSQL(array $column)
    {
        return 'TIMESTAMP WITH TIME ZONE';
    }

    /**
     * {@inheritdoc}
     */
    public function getDropSequenceSQL($sequence)
    {
        if ($sequence instanceof Sequence) {
            $sequence = $sequence->getQuotedName($this);
        }

        return 'DROP SEQUENCE ' . $sequence;
    }

    /**
     * {@inheritdoc}
     */
    public function getListSequencesSQL($database)
    {
        return 'SELECT sequence_name, increment_by, start_with, min_value FROM SYS.SYSSEQUENCE';
    }

    /**
     * {@inheritdoc}
     */
    public function getSequenceNextValSQL($sequence)
    {
        return 'SELECT ' . $sequence . '.NEXTVAL';
    }

    /**
     * {@inheritdoc}
     */
    public function supportsSequences()
    {
        return true;
    }

    /**
     * {@inheritdoc}
     */
    protected function getAdvancedIndexOptionsSQL(Index $index)
    {
        if (! $index->isPrimary() && $index->isUnique() && $index->hasFlag('with_nulls_not_distinct')) {
            return ' WITH NULLS NOT DISTINCT' . parent::getAdvancedIndexOptionsSQL($index);
        }

        return parent::getAdvancedIndexOptionsSQL($index);
    }

    /**
     * {@inheritdoc}
     */
    protected function getReservedKeywordsClass()
    {
        return Keywords\SQLAnywhere12Keywords::class;
    }

    /**
     * {@inheritDoc}
     */
    protected function initializeDoctrineTypeMappings()
    {
        parent::initializeDoctrineTypeMappings();
        $this->doctrineTypeMapping['timestamp with time zone'] = 'datetime';
    }
}
<?php

namespace Doctrine\DBAL\Platforms;

use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Schema\Constraint;
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
use Doctrine\DBAL\Schema\Identifier;
use Doctrine\DBAL\Schema\Index;
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Schema\TableDiff;
use Doctrine\DBAL\TransactionIsolationLevel;
use Doctrine\DBAL\Types;

use function array_merge;
use function array_unique;
use function array_values;
use function implode;
use function is_numeric;
use function sprintf;
use function sqrt;
use function str_replace;
use function strlen;
use function strpos;
use function strtolower;
use function trim;

/**
 * The SqlitePlatform class describes the specifics and dialects of the SQLite
 * database platform.
 *
 * @todo   Rename: SQLitePlatform
 */
class SqlitePlatform extends AbstractPlatform
{
    /**
     * {@inheritDoc}
     */
    public function getRegexpExpression()
    {
        return 'REGEXP';
    }

    /**
     * {@inheritDoc}
     *
     * @deprecated Use application-generated UUIDs instead
     */
    public function getGuidExpression()
    {
        return "HEX(RANDOMBLOB(4)) || '-' || HEX(RANDOMBLOB(2)) || '-4' || "
            . "SUBSTR(HEX(RANDOMBLOB(2)), 2) || '-' || "
            . "SUBSTR('89AB', 1 + (ABS(RANDOM()) % 4), 1) || "
            . "SUBSTR(HEX(RANDOMBLOB(2)), 2) || '-' || HEX(RANDOMBLOB(6))";
    }

    /**
     * @param string $type
     *
     * @return string
     */
    public function getNowExpression($type = 'timestamp')
    {
        switch ($type) {
            case 'time':
                return 'time(\'now\')';

            case 'date':
                return 'date(\'now\')';

            case 'timestamp':
            default:
                return 'datetime(\'now\')';
        }
    }

    /**
     * {@inheritDoc}
     */
    public function getTrimExpression($str, $mode = TrimMode::UNSPECIFIED, $char = false)
    {
        $trimChar = $char !== false ? ', ' . $char : '';

        switch ($mode) {
            case TrimMode::LEADING:
                $trimFn = 'LTRIM';
                break;

            case TrimMode::TRAILING:
                $trimFn = 'RTRIM';
                break;

            default:
                $trimFn = 'TRIM';
        }

        return $trimFn . '(' . $str . $trimChar . ')';
    }

    /**
     * {@inheritDoc}
     *
     * SQLite only supports the 2 parameter variant of this function
     */
    public function getSubstringExpression($string, $start, $length = null)
    {
        if ($length !== null) {
            return 'SUBSTR(' . $string . ', ' . $start . ', ' . $length . ')';
        }

        return 'SUBSTR(' . $string . ', ' . $start . ', LENGTH(' . $string . '))';
    }

    /**
     * {@inheritDoc}
     */
    public function getLocateExpression($str, $substr, $startPos = false)
    {
        if ($startPos === false) {
            return 'LOCATE(' . $str . ', ' . $substr . ')';
        }

        return 'LOCATE(' . $str . ', ' . $substr . ', ' . $startPos . ')';
    }

    /**
     * {@inheritdoc}
     */
    protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit)
    {
        switch ($unit) {
            case DateIntervalUnit::SECOND:
            case DateIntervalUnit::MINUTE:
            case DateIntervalUnit::HOUR:
                return 'DATETIME(' . $date . ",'" . $operator . $interval . ' ' . $unit . "')";
        }

        switch ($unit) {
            case DateIntervalUnit::WEEK:
                $interval *= 7;
                $unit      = DateIntervalUnit::DAY;
                break;

            case DateIntervalUnit::QUARTER:
                $interval *= 3;
                $unit      = DateIntervalUnit::MONTH;
                break;
        }

        if (! is_numeric($interval)) {
            $interval = "' || " . $interval . " || '";
        }

        return 'DATE(' . $date . ",'" . $operator . $interval . ' ' . $unit . "')";
    }

    /**
     * {@inheritDoc}
     */
    public function getDateDiffExpression($date1, $date2)
    {
        return sprintf("JULIANDAY(%s, 'start of day') - JULIANDAY(%s, 'start of day')", $date1, $date2);
    }

    /**
     * {@inheritDoc}
     */
    protected function _getTransactionIsolationLevelSQL($level)
    {
        switch ($level) {
            case TransactionIsolationLevel::READ_UNCOMMITTED:
                return '0';

            case TransactionIsolationLevel::READ_COMMITTED:
            case TransactionIsolationLevel::REPEATABLE_READ:
            case TransactionIsolationLevel::SERIALIZABLE:
                return '1';

            default:
                return parent::_getTransactionIsolationLevelSQL($level);
        }
    }

    /**
     * {@inheritDoc}
     */
    public function getSetTransactionIsolationSQL($level)
    {
        return 'PRAGMA read_uncommitted = ' . $this->_getTransactionIsolationLevelSQL($level);
    }

    /**
     * {@inheritDoc}
     */
    public function prefersIdentityColumns()
    {
        return true;
    }

    /**
     * {@inheritDoc}
     */
    public function getBooleanTypeDeclarationSQL(array $column)
    {
        return 'BOOLEAN';
    }

    /**
     * {@inheritDoc}
     */
    public function getIntegerTypeDeclarationSQL(array $column)
    {
        return 'INTEGER' . $this->_getCommonIntegerTypeDeclarationSQL($column);
    }

    /**
     * {@inheritDoc}
     */
    public function getBigIntTypeDeclarationSQL(array $column)
    {
        // SQLite autoincrement is implicit for INTEGER PKs, but not for BIGINT columns
        if (! empty($column['autoincrement'])) {
            return $this->getIntegerTypeDeclarationSQL($column);
        }

        return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($column);
    }

    /**
     * @param array<string, mixed> $column
     *
     * @return string
     */
    public function getTinyIntTypeDeclarationSql(array $column)
    {
        // SQLite autoincrement is implicit for INTEGER PKs, but not for TINYINT columns
        if (! empty($column['autoincrement'])) {
            return $this->getIntegerTypeDeclarationSQL($column);
        }

        return 'TINYINT' . $this->_getCommonIntegerTypeDeclarationSQL($column);
    }

    /**
     * {@inheritDoc}
     */
    public function getSmallIntTypeDeclarationSQL(array $column)
    {
        // SQLite autoincrement is implicit for INTEGER PKs, but not for SMALLINT columns
        if (! empty($column['autoincrement'])) {
            return $this->getIntegerTypeDeclarationSQL($column);
        }

        return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($column);
    }

    /**
     * @param array<string, mixed> $column
     *
     * @return string
     */
    public function getMediumIntTypeDeclarationSql(array $column)
    {
        // SQLite autoincrement is implicit for INTEGER PKs, but not for MEDIUMINT columns
        if (! empty($column['autoincrement'])) {
            return $this->getIntegerTypeDeclarationSQL($column);
        }

        return 'MEDIUMINT' . $this->_getCommonIntegerTypeDeclarationSQL($column);
    }

    /**
     * {@inheritDoc}
     */
    public function getDateTimeTypeDeclarationSQL(array $column)
    {
        return 'DATETIME';
    }

    /**
     * {@inheritDoc}
     */
    public function getDateTypeDeclarationSQL(array $column)
    {
        return 'DATE';
    }

    /**
     * {@inheritDoc}
     */
    public function getTimeTypeDeclarationSQL(array $column)
    {
        return 'TIME';
    }

    /**
     * {@inheritDoc}
     */
    protected function _getCommonIntegerTypeDeclarationSQL(array $column)
    {
        // sqlite autoincrement is only possible for the primary key
        if (! empty($column['autoincrement'])) {
            return ' PRIMARY KEY AUTOINCREMENT';
        }

        return ! empty($column['unsigned']) ? ' UNSIGNED' : '';
    }

    /**
     * {@inheritDoc}
     */
    public function getForeignKeyDeclarationSQL(ForeignKeyConstraint $foreignKey)
    {
        return parent::getForeignKeyDeclarationSQL(new ForeignKeyConstraint(
            $foreignKey->getQuotedLocalColumns($this),
            str_replace('.', '__', $foreignKey->getQuotedForeignTableName($this)),
            $foreignKey->getQuotedForeignColumns($this),
            $foreignKey->getName(),
            $foreignKey->getOptions()
        ));
    }

    /**
     * {@inheritDoc}
     */
    protected function _getCreateTableSQL($name, array $columns, array $options = [])
    {
        $name        = str_replace('.', '__', $name);
        $queryFields = $this->getColumnDeclarationListSQL($columns);

        if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) {
            foreach ($options['uniqueConstraints'] as $name => $definition) {
                $queryFields .= ', ' . $this->getUniqueConstraintDeclarationSQL($name, $definition);
            }
        }

        $queryFields .= $this->getNonAutoincrementPrimaryKeyDefinition($columns, $options);

        if (isset($options['foreignKeys'])) {
            foreach ($options['foreignKeys'] as $foreignKey) {
                $queryFields .= ', ' . $this->getForeignKeyDeclarationSQL($foreignKey);
            }
        }

        $tableComment = '';
        if (isset($options['comment'])) {
            $comment = trim($options['comment'], " '");

            $tableComment = $this->getInlineTableCommentSQL($comment);
        }

        $query = ['CREATE TABLE ' . $name . ' ' . $tableComment . '(' . $queryFields . ')'];

        if (isset($options['alter']) && $options['alter'] === true) {
            return $query;
        }

        if (isset($options['indexes']) && ! empty($options['indexes'])) {
            foreach ($options['indexes'] as $indexDef) {
                $query[] = $this->getCreateIndexSQL($indexDef, $name);
            }
        }

        if (isset($options['unique']) && ! empty($options['unique'])) {
            foreach ($options['unique'] as $indexDef) {
                $query[] = $this->getCreateIndexSQL($indexDef, $name);
            }
        }

        return $query;
    }

    /**
     * Generate a PRIMARY KEY definition if no autoincrement value is used
     *
     * @param mixed[][] $columns
     * @param mixed[]   $options
     */
    private function getNonAutoincrementPrimaryKeyDefinition(array $columns, array $options): string
    {
        if (empty($options['primary'])) {
            return '';
        }

        $keyColumns = array_unique(array_values($options['primary']));

        foreach ($keyColumns as $keyColumn) {
            if (! empty($columns[$keyColumn]['autoincrement'])) {
                return '';
            }
        }

        return ', PRIMARY KEY(' . implode(', ', $keyColumns) . ')';
    }

    /**
     * {@inheritDoc}
     */
    protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed)
    {
        return $fixed ? ($length ? 'CHAR(' . $length . ')' : 'CHAR(255)')
                : ($length ? 'VARCHAR(' . $length . ')' : 'TEXT');
    }

    /**
     * {@inheritdoc}
     */
    protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed)
    {
        return 'BLOB';
    }

    /**
     * {@inheritdoc}
     */
    public function getBinaryMaxLength()
    {
        return 0;
    }

    /**
     * {@inheritdoc}
     */
    public function getBinaryDefaultLength()
    {
        return 0;
    }

    /**
     * {@inheritDoc}
     */
    public function getClobTypeDeclarationSQL(array $column)
    {
        return 'CLOB';
    }

    /**
     * {@inheritDoc}
     */
    public function getListTableConstraintsSQL($table)
    {
        $table = str_replace('.', '__', $table);

        return sprintf(
            "SELECT sql FROM sqlite_master WHERE type='index' AND tbl_name = %s AND sql NOT NULL ORDER BY name",
            $this->quoteStringLiteral($table)
        );
    }

    /**
     * {@inheritDoc}
     */
    public function getListTableColumnsSQL($table, $database = null)
    {
        $table = str_replace('.', '__', $table);

        return sprintf('PRAGMA table_info(%s)', $this->quoteStringLiteral($table));
    }

    /**
     * {@inheritDoc}
     */
    public function getListTableIndexesSQL($table, $database = null)
    {
        $table = str_replace('.', '__', $table);

        return sprintf('PRAGMA index_list(%s)', $this->quoteStringLiteral($table));
    }

    /**
     * {@inheritDoc}
     */
    public function getListTablesSQL()
    {
        return 'SELECT name FROM sqlite_master'
            . " WHERE type = 'table'"
            . " AND name != 'sqlite_sequence'"
            . " AND name != 'geometry_columns'"
            . " AND name != 'spatial_ref_sys'"
            . ' UNION ALL SELECT name FROM sqlite_temp_master'
            . " WHERE type = 'table' ORDER BY name";
    }

    /**
     * {@inheritDoc}
     */
    public function getListViewsSQL($database)
    {
        return "SELECT name, sql FROM sqlite_master WHERE type='view' AND sql NOT NULL";
    }

    /**
     * {@inheritDoc}
     */
    public function getCreateViewSQL($name, $sql)
    {
        return 'CREATE VIEW ' . $name . ' AS ' . $sql;
    }

    /**
     * {@inheritDoc}
     */
    public function getDropViewSQL($name)
    {
        return 'DROP VIEW ' . $name;
    }

    /**
     * {@inheritDoc}
     */
    public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey)
    {
        $query = parent::getAdvancedForeignKeyOptionsSQL($foreignKey);

        if (! $foreignKey->hasOption('deferrable') || $foreignKey->getOption('deferrable') === false) {
            $query .= ' NOT';
        }

        $query .= ' DEFERRABLE';
        $query .= ' INITIALLY';

        if ($foreignKey->hasOption('deferred') && $foreignKey->getOption('deferred') !== false) {
            $query .= ' DEFERRED';
        } else {
            $query .= ' IMMEDIATE';
        }

        return $query;
    }

    /**
     * {@inheritDoc}
     */
    public function supportsIdentityColumns()
    {
        return true;
    }

    /**
     * {@inheritDoc}
     */
    public function supportsColumnCollation()
    {
        return true;
    }

    /**
     * {@inheritDoc}
     */
    public function supportsInlineColumnComments()
    {
        return true;
    }

    /**
     * {@inheritDoc}
     */
    public function getName()
    {
        return 'sqlite';
    }

    /**
     * {@inheritDoc}
     */
    public function getTruncateTableSQL($tableName, $cascade = false)
    {
        $tableIdentifier = new Identifier($tableName);
        $tableName       = str_replace('.', '__', $tableIdentifier->getQuotedName($this));

        return 'DELETE FROM ' . $tableName;
    }

    /**
     * User-defined function for Sqlite that is used with PDO::sqliteCreateFunction().
     *
     * @param int|float $value
     *
     * @return float
     */
    public static function udfSqrt($value)
    {
        return sqrt($value);
    }

    /**
     * User-defined function for Sqlite that implements MOD(a, b).
     *
     * @param int $a
     * @param int $b
     *
     * @return int
     */
    public static function udfMod($a, $b)
    {
        return $a % $b;
    }

    /**
     * @param string $str
     * @param string $substr
     * @param int    $offset
     *
     * @return int
     */
    public static function udfLocate($str, $substr, $offset = 0)
    {
        // SQL's LOCATE function works on 1-based positions, while PHP's strpos works on 0-based positions.
        // So we have to make them compatible if an offset is given.
        if ($offset > 0) {
            $offset -= 1;
        }

        $pos = strpos($str, $substr, $offset);

        if ($pos !== false) {
            return $pos + 1;
        }

        return 0;
    }

    /**
     * {@inheritDoc}
     */
    public function getForUpdateSQL()
    {
        return '';
    }

    /**
     * {@inheritDoc}
     */
    public function getInlineColumnCommentSQL($comment)
    {
        return '--' . str_replace("\n", "\n--", $comment) . "\n";
    }

    private function getInlineTableCommentSQL(string $comment): string
    {
        return $this->getInlineColumnCommentSQL($comment);
    }

    /**
     * {@inheritDoc}
     */
    protected function initializeDoctrineTypeMappings()
    {
        $this->doctrineTypeMapping = [
            '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',
            'longvarchar'      => 'string',
            'varchar2'         => 'string',
            'nvarchar'         => 'string',
            'image'            => 'string',
            'ntext'            => 'string',
            'char'             => 'string',
            'date'             => 'date',
            'datetime'         => 'datetime',
            'timestamp'        => 'datetime',
            'time'             => 'time',
            'float'            => 'float',
            'double'           => 'float',
            'double precision' => 'float',
            'real'             => 'float',
            'decimal'          => 'decimal',
            'numeric'          => 'decimal',
            'blob'             => 'blob',
        ];
    }

    /**
     * {@inheritDoc}
     */
    protected function getReservedKeywordsClass()
    {
        return Keywords\SQLiteKeywords::class;
    }

    /**
     * {@inheritDoc}
     */
    protected function getPreAlterTableIndexForeignKeySQL(TableDiff $diff)
    {
        if (! $diff->fromTable instanceof Table) {
            throw new Exception(
                'Sqlite platform requires for alter table the table diff with reference to original table schema'
            );
        }

        $sql = [];
        foreach ($diff->fromTable->getIndexes() as $index) {
            if ($index->isPrimary()) {
                continue;
            }

            $sql[] = $this->getDropIndexSQL($index, $diff->name);
        }

        return $sql;
    }

    /**
     * {@inheritDoc}
     */
    protected function getPostAlterTableIndexForeignKeySQL(TableDiff $diff)
    {
        $fromTable = $diff->fromTable;

        if (! $fromTable instanceof Table) {
            throw new Exception(
                'Sqlite platform requires for alter table the table diff with reference to original table schema'
            );
        }

        $sql       = [];
        $tableName = $diff->getNewName();

        if ($tableName === false) {
            $tableName = $diff->getName($this);
        }

        foreach ($this->getIndexesInAlteredTable($diff, $fromTable) as $index) {
            if ($index->isPrimary()) {
                continue;
            }

            $sql[] = $this->getCreateIndexSQL($index, $tableName->getQuotedName($this));
        }

        return $sql;
    }

    /**
     * {@inheritDoc}
     */
    protected function doModifyLimitQuery($query, $limit, $offset)
    {
        if ($limit === null && $offset > 0) {
            return $query . ' LIMIT -1 OFFSET ' . $offset;
        }

        return parent::doModifyLimitQuery($query, $limit, $offset);
    }

    /**
     * {@inheritDoc}
     */
    public function getBlobTypeDeclarationSQL(array $column)
    {
        return 'BLOB';
    }

    /**
     * {@inheritDoc}
     */
    public function getTemporaryTableName($tableName)
    {
        $tableName = str_replace('.', '__', $tableName);

        return $tableName;
    }

    /**
     * {@inheritDoc}
     *
     * Sqlite Platform emulates schema by underscoring each dot and generating tables
     * into the default database.
     *
     * This hack is implemented to be able to use SQLite as testdriver when
     * using schema supporting databases.
     */
    public function canEmulateSchemas()
    {
        return true;
    }

    /**
     * {@inheritDoc}
     */
    public function supportsForeignKeyConstraints()
    {
        return false;
    }

    /**
     * {@inheritDoc}
     */
    public function getCreatePrimaryKeySQL(Index $index, $table)
    {
        throw new Exception('Sqlite platform does not support alter primary key.');
    }

    /**
     * {@inheritdoc}
     */
    public function getCreateForeignKeySQL(ForeignKeyConstraint $foreignKey, $table)
    {
        throw new Exception('Sqlite platform does not support alter foreign key.');
    }

    /**
     * {@inheritdoc}
     */
    public function getDropForeignKeySQL($foreignKey, $table)
    {
        throw new Exception('Sqlite platform does not support alter foreign key.');
    }

    /**
     * {@inheritDoc}
     */
    public function getCreateConstraintSQL(Constraint $constraint, $table)
    {
        throw new Exception('Sqlite platform does not support alter constraint.');
    }

    /**
     * {@inheritDoc}
     *
     * @param int|null $createFlags
     */
    public function getCreateTableSQL(Table $table, $createFlags = null)
    {
        $createFlags = $createFlags ?? self::CREATE_INDEXES | self::CREATE_FOREIGNKEYS;

        return parent::getCreateTableSQL($table, $createFlags);
    }

    /**
     * @param string      $table
     * @param string|null $database
     *
     * @return string
     */
    public function getListTableForeignKeysSQL($table, $database = null)
    {
        $table = str_replace('.', '__', $table);

        return sprintf('PRAGMA foreign_key_list(%s)', $this->quoteStringLiteral($table));
    }

    /**
     * {@inheritDoc}
     */
    public function getAlterTableSQL(TableDiff $diff)
    {
        $sql = $this->getSimpleAlterTableSQL($diff);
        if ($sql !== false) {
            return $sql;
        }

        $fromTable = $diff->fromTable;
        if (! $fromTable instanceof Table) {
            throw new Exception(
                'Sqlite platform requires for alter table the table diff with reference to original table schema'
            );
        }

        $table = clone $fromTable;

        $columns        = [];
        $oldColumnNames = [];
        $newColumnNames = [];
        $columnSql      = [];

        foreach ($table->getColumns() as $columnName => $column) {
            $columnName                  = strtolower($columnName);
            $columns[$columnName]        = $column;
            $oldColumnNames[$columnName] = $newColumnNames[$columnName] = $column->getQuotedName($this);
        }

        foreach ($diff->removedColumns as $columnName => $column) {
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
                continue;
            }

            $columnName = strtolower($columnName);
            if (! isset($columns[$columnName])) {
                continue;
            }

            unset(
                $columns[$columnName],
                $oldColumnNames[$columnName],
                $newColumnNames[$columnName]
            );
        }

        foreach ($diff->renamedColumns as $oldColumnName => $column) {
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
                continue;
            }

            $oldColumnName = strtolower($oldColumnName);
            if (isset($columns[$oldColumnName])) {
                unset($columns[$oldColumnName]);
            }

            $columns[strtolower($column->getName())] = $column;

            if (! isset($newColumnNames[$oldColumnName])) {
                continue;
            }

            $newColumnNames[$oldColumnName] = $column->getQuotedName($this);
        }

        foreach ($diff->changedColumns as $oldColumnName => $columnDiff) {
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
                continue;
            }

            if (isset($columns[$oldColumnName])) {
                unset($columns[$oldColumnName]);
            }

            $columns[strtolower($columnDiff->column->getName())] = $columnDiff->column;

            if (! isset($newColumnNames[$oldColumnName])) {
                continue;
            }

            $newColumnNames[$oldColumnName] = $columnDiff->column->getQuotedName($this);
        }

        foreach ($diff->addedColumns as $columnName => $column) {
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
                continue;
            }

            $columns[strtolower($columnName)] = $column;
        }

        $sql      = [];
        $tableSql = [];
        if (! $this->onSchemaAlterTable($diff, $tableSql)) {
            $dataTable = new Table('__temp__' . $table->getName());

            $newTable = new Table(
                $table->getQuotedName($this),
                $columns,
                $this->getPrimaryIndexInAlteredTable($diff, $fromTable),
                $this->getForeignKeysInAlteredTable($diff, $fromTable),
                0,
                $table->getOptions()
            );
            $newTable->addOption('alter', true);

            $sql = $this->getPreAlterTableIndexForeignKeySQL($diff);

            $sql[] = sprintf(
                'CREATE TEMPORARY TABLE %s AS SELECT %s FROM %s',
                $dataTable->getQuotedName($this),
                implode(', ', $oldColumnNames),
                $table->getQuotedName($this)
            );
            $sql[] = $this->getDropTableSQL($fromTable);

            $sql   = array_merge($sql, $this->getCreateTableSQL($newTable));
            $sql[] = sprintf(
                'INSERT INTO %s (%s) SELECT %s FROM %s',
                $newTable->getQuotedName($this),
                implode(', ', $newColumnNames),
                implode(', ', $oldColumnNames),
                $dataTable->getQuotedName($this)
            );
            $sql[] = $this->getDropTableSQL($dataTable);

            $newName = $diff->getNewName();

            if ($newName !== false) {
                $sql[] = sprintf(
                    'ALTER TABLE %s RENAME TO %s',
                    $newTable->getQuotedName($this),
                    $newName->getQuotedName($this)
                );
            }

            $sql = array_merge($sql, $this->getPostAlterTableIndexForeignKeySQL($diff));
        }

        return array_merge($sql, $tableSql, $columnSql);
    }

    /**
     * @return string[]|false
     */
    private function getSimpleAlterTableSQL(TableDiff $diff)
    {
        // Suppress changes on integer type autoincrement columns.
        foreach ($diff->changedColumns as $oldColumnName => $columnDiff) {
            if (
                ! $columnDiff->fromColumn instanceof Column ||
                ! $columnDiff->column instanceof Column ||
                ! $columnDiff->column->getAutoincrement() ||
                ! $columnDiff->column->getType() instanceof Types\IntegerType
            ) {
                continue;
            }

            if (! $columnDiff->hasChanged('type') && $columnDiff->hasChanged('unsigned')) {
                unset($diff->changedColumns[$oldColumnName]);

                continue;
            }

            $fromColumnType = $columnDiff->fromColumn->getType();

            if (! ($fromColumnType instanceof Types\SmallIntType) && ! ($fromColumnType instanceof Types\BigIntType)) {
                continue;
            }

            unset($diff->changedColumns[$oldColumnName]);
        }

        if (
            ! empty($diff->renamedColumns)
            || ! empty($diff->addedForeignKeys)
            || ! empty($diff->addedIndexes)
            || ! empty($diff->changedColumns)
            || ! empty($diff->changedForeignKeys)
            || ! empty($diff->changedIndexes)
            || ! empty($diff->removedColumns)
            || ! empty($diff->removedForeignKeys)
            || ! empty($diff->removedIndexes)
            || ! empty($diff->renamedIndexes)
        ) {
            return false;
        }

        $table = new Table($diff->name);

        $sql       = [];
        $tableSql  = [];
        $columnSql = [];

        foreach ($diff->addedColumns as $column) {
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
                continue;
            }

            $definition = array_merge([
                'unique' => null,
                'autoincrement' => null,
                'default' => null,
            ], $column->toArray());

            $type = $definition['type'];

            switch (true) {
                case isset($definition['columnDefinition']) || $definition['autoincrement'] || $definition['unique']:
                case $type instanceof Types\DateTimeType && $definition['default'] === $this->getCurrentTimestampSQL():
                case $type instanceof Types\DateType && $definition['default'] === $this->getCurrentDateSQL():
                case $type instanceof Types\TimeType && $definition['default'] === $this->getCurrentTimeSQL():
                    return false;
            }

            $definition['name'] = $column->getQuotedName($this);
            if ($type instanceof Types\StringType && $definition['length'] === null) {
                $definition['length'] = 255;
            }

            $sql[] = 'ALTER TABLE ' . $table->getQuotedName($this) . ' ADD COLUMN '
                . $this->getColumnDeclarationSQL($definition['name'], $definition);
        }

        if (! $this->onSchemaAlterTable($diff, $tableSql)) {
            if ($diff->newName !== false) {
                $newTable = new Identifier($diff->newName);

                $sql[] = 'ALTER TABLE ' . $table->getQuotedName($this) . ' RENAME TO '
                    . $newTable->getQuotedName($this);
            }
        }

        return array_merge($sql, $tableSql, $columnSql);
    }

    /**
     * @return string[]
     */
    private function getColumnNamesInAlteredTable(TableDiff $diff, Table $fromTable)
    {
        $columns = [];

        foreach ($fromTable->getColumns() as $columnName => $column) {
            $columns[strtolower($columnName)] = $column->getName();
        }

        foreach ($diff->removedColumns as $columnName => $column) {
            $columnName = strtolower($columnName);
            if (! isset($columns[$columnName])) {
                continue;
            }

            unset($columns[$columnName]);
        }

        foreach ($diff->renamedColumns as $oldColumnName => $column) {
            $columnName                          = $column->getName();
            $columns[strtolower($oldColumnName)] = $columnName;
            $columns[strtolower($columnName)]    = $columnName;
        }

        foreach ($diff->changedColumns as $oldColumnName => $columnDiff) {
            $columnName                          = $columnDiff->column->getName();
            $columns[strtolower($oldColumnName)] = $columnName;
            $columns[strtolower($columnName)]    = $columnName;
        }

        foreach ($diff->addedColumns as $column) {
            $columnName                       = $column->getName();
            $columns[strtolower($columnName)] = $columnName;
        }

        return $columns;
    }

    /**
     * @return Index[]
     */
    private function getIndexesInAlteredTable(TableDiff $diff, Table $fromTable)
    {
        $indexes     = $fromTable->getIndexes();
        $columnNames = $this->getColumnNamesInAlteredTable($diff, $fromTable);

        foreach ($indexes as $key => $index) {
            foreach ($diff->renamedIndexes as $oldIndexName => $renamedIndex) {
                if (strtolower($key) !== strtolower($oldIndexName)) {
                    continue;
                }

                unset($indexes[$key]);
            }

            $changed      = false;
            $indexColumns = [];
            foreach ($index->getColumns() as $columnName) {
                $normalizedColumnName = strtolower($columnName);
                if (! isset($columnNames[$normalizedColumnName])) {
                    unset($indexes[$key]);
                    continue 2;
                }

                $indexColumns[] = $columnNames[$normalizedColumnName];
                if ($columnName === $columnNames[$normalizedColumnName]) {
                    continue;
                }

                $changed = true;
            }

            if (! $changed) {
                continue;
            }

            $indexes[$key] = new Index(
                $index->getName(),
                $indexColumns,
                $index->isUnique(),
                $index->isPrimary(),
                $index->getFlags()
            );
        }

        foreach ($diff->removedIndexes as $index) {
            $indexName = strtolower($index->getName());
            if (! strlen($indexName) || ! isset($indexes[$indexName])) {
                continue;
            }

            unset($indexes[$indexName]);
        }

        foreach (array_merge($diff->changedIndexes, $diff->addedIndexes, $diff->renamedIndexes) as $index) {
            $indexName = strtolower($index->getName());
            if (strlen($indexName)) {
                $indexes[$indexName] = $index;
            } else {
                $indexes[] = $index;
            }
        }

        return $indexes;
    }

    /**
     * @return ForeignKeyConstraint[]
     */
    private function getForeignKeysInAlteredTable(TableDiff $diff, Table $fromTable)
    {
        $foreignKeys = $fromTable->getForeignKeys();
        $columnNames = $this->getColumnNamesInAlteredTable($diff, $fromTable);

        foreach ($foreignKeys as $key => $constraint) {
            $changed      = false;
            $localColumns = [];
            foreach ($constraint->getLocalColumns() as $columnName) {
                $normalizedColumnName = strtolower($columnName);
                if (! isset($columnNames[$normalizedColumnName])) {
                    unset($foreignKeys[$key]);
                    continue 2;
                }

                $localColumns[] = $columnNames[$normalizedColumnName];
                if ($columnName === $columnNames[$normalizedColumnName]) {
                    continue;
                }

                $changed = true;
            }

            if (! $changed) {
                continue;
            }

            $foreignKeys[$key] = new ForeignKeyConstraint(
                $localColumns,
                $constraint->getForeignTableName(),
                $constraint->getForeignColumns(),
                $constraint->getName(),
                $constraint->getOptions()
            );
        }

        foreach ($diff->removedForeignKeys as $constraint) {
            if (! $constraint instanceof ForeignKeyConstraint) {
                $constraint = new Identifier($constraint);
            }

            $constraintName = strtolower($constraint->getName());
            if (! strlen($constraintName) || ! isset($foreignKeys[$constraintName])) {
                continue;
            }

            unset($foreignKeys[$constraintName]);
        }

        foreach (array_merge($diff->changedForeignKeys, $diff->addedForeignKeys) as $constraint) {
            $constraintName = strtolower($constraint->getName());
            if (strlen($constraintName)) {
                $foreignKeys[$constraintName] = $constraint;
            } else {
                $foreignKeys[] = $constraint;
            }
        }

        return $foreignKeys;
    }

    /**
     * @return Index[]
     */
    private function getPrimaryIndexInAlteredTable(TableDiff $diff, Table $fromTable)
    {
        $primaryIndex = [];

        foreach ($this->getIndexesInAlteredTable($diff, $fromTable) as $index) {
            if (! $index->isPrimary()) {
                continue;
            }

            $primaryIndex = [$index->getName() => $index];
        }

        return $primaryIndex;
    }
}
<?php

namespace Doctrine\DBAL\Platforms\Keywords;

/**
 * MariaDb reserved keywords list.
 *
 * @link https://mariadb.com/kb/en/the-mariadb-library/reserved-words/
 */
final class MariaDb102Keywords extends MySQLKeywords
{
    public function getName(): string
    {
        return 'MariaDb102';
    }

    /**
     * {@inheritdoc}
     */
    protected function getKeywords(): array
    {
        return [
            'ACCESSIBLE',
            'ADD',
            'ALL',
            'ALTER',
            'ANALYZE',
            'AND',
            'AS',
            'ASC',
            'ASENSITIVE',
            'BEFORE',
            'BETWEEN',
            'BIGINT',
            'BINARY',
            'BLOB',
            'BOTH',
            'BY',
            'CALL',
            'CASCADE',
            'CASE',
            'CHANGE',
            'CHAR',
            'CHARACTER',
            'CHECK',
            'COLLATE',
            'COLUMN',
            'CONDITION',
            'CONSTRAINT',
            'CONTINUE',
            'CONVERT',
            'CREATE',
            'CROSS',
            'CURRENT_DATE',
            'CURRENT_TIME',
            'CURRENT_TIMESTAMP',
            'CURRENT_USER',
            'CURSOR',
            'DATABASE',
            'DATABASES',
            'DAY_HOUR',
            'DAY_MICROSECOND',
            'DAY_MINUTE',
            'DAY_SECOND',
            'DEC',
            'DECIMAL',
            'DECLARE',
            'DEFAULT',
            'DELAYED',
            'DELETE',
            'DESC',
            'DESCRIBE',
            'DETERMINISTIC',
            'DISTINCT',
            'DISTINCTROW',
            'DIV',
            'DOUBLE',
            'DROP',
            'DUAL',
            'EACH',
            'ELSE',
            'ELSEIF',
            'ENCLOSED',
            'ESCAPED',
            'EXCEPT',
            'EXISTS',
            'EXIT',
            'EXPLAIN',
            'FALSE',
            'FETCH',
            'FLOAT',
            'FLOAT4',
            'FLOAT8',
            'FOR',
            'FORCE',
            'FOREIGN',
            'FROM',
            'FULLTEXT',
            'GENERATED',
            'GET',
            'GENERAL',
            'GRANT',
            'GROUP',
            'HAVING',
            'HIGH_PRIORITY',
            'HOUR_MICROSECOND',
            'HOUR_MINUTE',
            'HOUR_SECOND',
            'IF',
            'IGNORE',
            'IGNORE_SERVER_IDS',
            'IN',
            'INDEX',
            'INFILE',
            'INNER',
            'INOUT',
            'INSENSITIVE',
            'INSERT',
            'INT',
            'INT1',
            'INT2',
            'INT3',
            'INT4',
            'INT8',
            'INTEGER',
            'INTERSECT',
            'INTERVAL',
            'INTO',
            'IO_AFTER_GTIDS',
            'IO_BEFORE_GTIDS',
            'IS',
            'ITERATE',
            'JOIN',
            'KEY',
            'KEYS',
            'KILL',
            'LEADING',
            'LEAVE',
            'LEFT',
            'LIKE',
            'LIMIT',
            'LINEAR',
            'LINES',
            'LOAD',
            'LOCALTIME',
            'LOCALTIMESTAMP',
            'LOCK',
            'LONG',
            'LONGBLOB',
            'LONGTEXT',
            'LOOP',
            'LOW_PRIORITY',
            'MASTER_BIND',
            'MASTER_HEARTBEAT_PERIOD',
            'MASTER_SSL_VERIFY_SERVER_CERT',
            'MATCH',
            'MAXVALUE',
            'MEDIUMBLOB',
            'MEDIUMINT',
            'MEDIUMTEXT',
            'MIDDLEINT',
            'MINUTE_MICROSECOND',
            'MINUTE_SECOND',
            'MOD',
            'MODIFIES',
            'NATURAL',
            'NO_WRITE_TO_BINLOG',
            'NOT',
            'NULL',
            'NUMERIC',
            'ON',
            'OPTIMIZE',
            'OPTIMIZER_COSTS',
            'OPTION',
            'OPTIONALLY',
            'OR',
            'ORDER',
            'OUT',
            'OUTER',
            'OUTFILE',
            'OVER',
            'PARTITION',
            'PRECISION',
            'PRIMARY',
            'PROCEDURE',
            'PURGE',
            'RANGE',
            'READ',
            'READ_WRITE',
            'READS',
            'REAL',
            'RECURSIVE',
            'REFERENCES',
            'REGEXP',
            'RELEASE',
            'RENAME',
            'REPEAT',
            'REPLACE',
            'REQUIRE',
            'RESIGNAL',
            'RESTRICT',
            'RETURN',
            'RETURNING',
            'REVOKE',
            'RIGHT',
            'RLIKE',
            'ROWS',
            'SCHEMA',
            'SCHEMAS',
            'SECOND_MICROSECOND',
            'SELECT',
            'SENSITIVE',
            'SEPARATOR',
            'SET',
            'SHOW',
            'SIGNAL',
            'SLOW',
            'SMALLINT',
            'SPATIAL',
            'SPECIFIC',
            'SQL',
            'SQL_BIG_RESULT',
            'SQL_CALC_FOUND_ROWS',
            'SQL_SMALL_RESULT',
            'SQLEXCEPTION',
            'SQLSTATE',
            'SQLWARNING',
            'SSL',
            'STARTING',
            'STORED',
            'STRAIGHT_JOIN',
            'TABLE',
            'TERMINATED',
            'THEN',
            'TINYBLOB',
            'TINYINT',
            'TINYTEXT',
            'TO',
            'TRAILING',
            'TRIGGER',
            'TRUE',
            'UNDO',
            'UNION',
            'UNIQUE',
            'UNLOCK',
            'UNSIGNED',
            'UPDATE',
            'USAGE',
            'USE',
            'USING',
            'UTC_DATE',
            'UTC_TIME',
            'UTC_TIMESTAMP',
            'VALUES',
            'VARBINARY',
            'VARCHAR',
            'VARCHARACTER',
            'VARYING',
            'VIRTUAL',
            'WHEN',
            'WHERE',
            'WHILE',
            'WINDOW',
            'WITH',
            'WRITE',
            'XOR',
            'YEAR_MONTH',
            'ZEROFILL',
        ];
    }
}
<?php

namespace Doctrine\DBAL\Platforms\Keywords;

use function array_diff;
use function array_merge;

/**
 * SAP Sybase SQL Anywhere 11 reserved keywords list.
 */
class SQLAnywhere11Keywords extends SQLAnywhereKeywords
{
    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'SQLAnywhere11';
    }

    /**
     * {@inheritdoc}
     *
     * @link http://dcx.sybase.com/1100/en/dbreference_en11/alhakeywords.html
     */
    protected function getKeywords()
    {
        return array_merge(
            array_diff(
                parent::getKeywords(),
                ['IQ']
            ),
            [
                'MERGE',
                'OPENSTRING',
            ]
        );
    }
}
<?php

namespace Doctrine\DBAL\Platforms\Keywords;

/**
 * SQLite Keywordlist.
 */
class SQLiteKeywords extends KeywordList
{
    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'SQLite';
    }

    /**
     * {@inheritdoc}
     */
    protected function getKeywords()
    {
        return [
            'ABORT',
            'ACTION',
            'ADD',
            'AFTER',
            'ALL',
            'ALTER',
            'ANALYZE',
            'AND',
            'AS',
            'ASC',
            'ATTACH',
            'AUTOINCREMENT',
            'BEFORE',
            'BEGIN',
            'BETWEEN',
            'BY',
            'CASCADE',
            'CASE',
            'CAST',
            'CHECK',
            'COLLATE',
            'COLUMN',
            'COMMIT',
            'CONFLICT',
            'CONSTRAINT',
            'CREATE',
            'CROSS',
            'CURRENT_DATE',
            'CURRENT_TIME',
            'CURRENT_TIMESTAMP',
            'DATABASE',
            'DEFAULT',
            'DEFERRABLE',
            'DEFERRED',
            'DELETE',
            'DESC',
            'DETACH',
            'DISTINCT',
            'DROP',
            'EACH',
            'ELSE',
            'END',
            'ESCAPE',
            'EXCEPT',
            'EXCLUSIVE',
            'EXISTS',
            'EXPLAIN',
            'FAIL',
            'FOR',
            'FOREIGN',
            'FROM',
            'FULL',
            'GLOB',
            'GROUP',
            'HAVING',
            'IF',
            'IGNORE',
            'IMMEDIATE',
            'IN',
            'INDEX',
            'INDEXED',
            'INITIALLY',
            'INNER',
            'INSERT',
            'INSTEAD',
            'INTERSECT',
            'INTO',
            'IS',
            'ISNULL',
            'JOIN',
            'KEY',
            'LEFT',
            'LIKE',
            'LIMIT',
            'MATCH',
            'NATURAL',
            'NO',
            'NOT',
            'NOTNULL',
            'NULL',
            'OF',
            'OFFSET',
            'ON',
            'OR',
            'ORDER',
            'OUTER',
            'PLAN',
            'PRAGMA',
            'PRIMARY',
            'QUERY',
            'RAISE',
            'REFERENCES',
            'REGEXP',
            'REINDEX',
            'RELEASE',
            'RENAME',
            'REPLACE',
            'RESTRICT',
            'RIGHT',
            'ROLLBACK',
            'ROW',
            'SAVEPOINT',
            'SELECT',
            'SET',
            'TABLE',
            'TEMP',
            'TEMPORARY',
            'THEN',
            'TO',
            'TRANSACTION',
            'TRIGGER',
            'UNION',
            'UNIQUE',
            'UPDATE',
            'USING',
            'VACUUM',
            'VALUES',
            'VIEW',
            'VIRTUAL',
            'WHEN',
            'WHERE',
        ];
    }
}
<?php

namespace Doctrine\DBAL\Platforms\Keywords;

/**
 * SAP Sybase SQL Anywhere 10 reserved keywords list.
 */
class SQLAnywhereKeywords extends KeywordList
{
    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'SQLAnywhere';
    }

    /**
     * {@inheritdoc}
     *
     * @link http://infocenter.sybase.com/help/topic/com.sybase.dbrfen10/pdf/dbrfen10.pdf?noframes=true
     */
    protected function getKeywords()
    {
        return [
            'ADD',
            'ALL',
            'ALTER',
            'AND',
            'ANY',
            'AS',
            'ASC',
            'ATTACH',
            'BACKUP',
            'BEGIN',
            'BETWEEN',
            'BIGINT',
            'BINARY',
            'BIT',
            'BOTTOM',
            'BREAK',
            'BY',
            'CALL',
            'CAPABILITY',
            'CASCADE',
            'CASE',
            'CAST',
            'CHAR',
            'CHAR_CONVERT',
            'CHARACTER',
            'CHECK',
            'CHECKPOINT',
            'CLOSE',
            'COMMENT',
            'COMMIT',
            'COMPRESSED',
            'CONFLICT',
            'CONNECT',
            'CONSTRAINT',
            'CONTAINS',
            'CONTINUE',
            'CONVERT',
            'CREATE',
            'CROSS',
            'CUBE',
            'CURRENT',
            'CURRENT_TIMESTAMP',
            'CURRENT_USER',
            'CURSOR',
            'DATE',
            'DBSPACE',
            'DEALLOCATE',
            'DEC',
            'DECIMAL',
            'DECLARE',
            'DEFAULT',
            'DELETE',
            'DELETING',
            'DESC',
            'DETACH',
            'DISTINCT',
            'DO',
            'DOUBLE',
            'DROP',
            'DYNAMIC',
            'ELSE',
            'ELSEIF',
            'ENCRYPTED',
            'END',
            'ENDIF',
            'ESCAPE',
            'EXCEPT',
            'EXCEPTION',
            'EXEC',
            'EXECUTE',
            'EXISTING',
            'EXISTS',
            'EXTERNLOGIN',
            'FETCH',
            'FIRST',
            'FLOAT',
            'FOR',
            'FORCE',
            'FOREIGN',
            'FORWARD',
            'FROM',
            'FULL',
            'GOTO',
            'GRANT',
            'GROUP',
            'HAVING',
            'HOLDLOCK',
            'IDENTIFIED',
            'IF',
            'IN',
            'INDEX',
            'INDEX_LPAREN',
            'INNER',
            'INOUT',
            'INSENSITIVE',
            'INSERT',
            'INSERTING',
            'INSTALL',
            'INSTEAD',
            'INT',
            'INTEGER',
            'INTEGRATED',
            'INTERSECT',
            'INTO',
            'IQ',
            'IS',
            'ISOLATION',
            'JOIN',
            'KERBEROS',
            'KEY',
            'LATERAL',
            'LEFT',
            'LIKE',
            'LOCK',
            'LOGIN',
            'LONG',
            'MATCH',
            'MEMBERSHIP',
            'MESSAGE',
            'MODE',
            'MODIFY',
            'NATURAL',
            'NCHAR',
            'NEW',
            'NO',
            'NOHOLDLOCK',
            'NOT',
            'NOTIFY',
            'NULL',
            'NUMERIC',
            'NVARCHAR',
            'OF',
            'OFF',
            'ON',
            'OPEN',
            'OPTION',
            'OPTIONS',
            'OR',
            'ORDER',
            'OTHERS',
            'OUT',
            'OUTER',
            'OVER',
            'PASSTHROUGH',
            'PRECISION',
            'PREPARE',
            'PRIMARY',
            'PRINT',
            'PRIVILEGES',
            'PROC',
            'PROCEDURE',
            'PUBLICATION',
            'RAISERROR',
            'READTEXT',
            'REAL',
            'REFERENCE',
            'REFERENCES',
            'REFRESH',
            'RELEASE',
            'REMOTE',
            'REMOVE',
            'RENAME',
            'REORGANIZE',
            'RESOURCE',
            'RESTORE',
            'RESTRICT',
            'RETURN',
            'REVOKE',
            'RIGHT',
            'ROLLBACK',
            'ROLLUP',
            'SAVE',
            'SAVEPOINT',
            'SCROLL',
            'SELECT',
            'SENSITIVE',
            'SESSION',
            'SET',
            'SETUSER',
            'SHARE',
            'SMALLINT',
            'SOME',
            'SQLCODE',
            'SQLSTATE',
            'START',
            'STOP',
            'SUBTRANS',
            'SUBTRANSACTION',
            'SYNCHRONIZE',
            'SYNTAX_ERROR',
            'TABLE',
            'TEMPORARY',
            'THEN',
            'TIME',
            'TIMESTAMP',
            'TINYINT',
            'TO',
            'TOP',
            'TRAN',
            'TRIGGER',
            'TRUNCATE',
            'TSEQUAL',
            'UNBOUNDED',
            'UNION',
            'UNIQUE',
            'UNIQUEIDENTIFIER',
            'UNKNOWN',
            'UNSIGNED',
            'UPDATE',
            'UPDATING',
            'USER',
            'USING',
            'VALIDATE',
            'VALUES',
            'VARBINARY',
            'VARBIT',
            'VARCHAR',
            'VARIABLE',
            'VARYING',
            'VIEW',
            'WAIT',
            'WAITFOR',
            'WHEN',
            'WHERE',
            'WHILE',
            'WINDOW',
            'WITH',
            'WITH_CUBE',
            'WITH_LPAREN',
            'WITH_ROLLUP',
            'WITHIN',
            'WORK',
            'WRITETEXT',
            'XML',
        ];
    }
}
<?php

namespace Doctrine\DBAL\Platforms\Keywords;

/**
 * PostgreSQL 9.1 reserved keywords list.
 */
class PostgreSQL91Keywords extends PostgreSQLKeywords
{
    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'PostgreSQL91';
    }

    /**
     * {@inheritdoc}
     *
     * @link http://www.postgresql.org/docs/9.1/static/sql-keywords-appendix.html
     */
    protected function getKeywords()
    {
        return [
            'ALL',
            'ANALYSE',
            'ANALYZE',
            'AND',
            'ANY',
            'ARRAY',
            'AS',
            'ASC',
            'ASYMMETRIC',
            'AUTHORIZATION',
            'BINARY',
            'BOTH',
            'CASE',
            'CAST',
            'CHECK',
            'COLLATE',
            'COLUMN',
            'CONCURRENTLY',
            'CONSTRAINT',
            'CREATE',
            'CROSS',
            'CURRENT_CATALOG',
            'CURRENT_DATE',
            'CURRENT_ROLE',
            'CURRENT_SCHEMA',
            'CURRENT_TIME',
            'CURRENT_TIMESTAMP',
            'CURRENT_USER',
            'DEFAULT',
            'DEFERRABLE',
            'DESC',
            'DISTINCT',
            'DO',
            'ELSE',
            'END',
            'EXCEPT',
            'FALSE',
            'FETCH',
            'FOR',
            'FOREIGN',
            'FREEZE',
            'FROM',
            'FULL',
            'GRANT',
            'GROUP',
            'HAVING',
            'ILIKE',
            'IN',
            'INITIALLY',
            'INNER',
            'INTERSECT',
            'INTO',
            'IS',
            'ISNULL',
            'JOIN',
            'LEADING',
            'LEFT',
            'LIKE',
            'LIMIT',
            'LOCALTIME',
            'LOCALTIMESTAMP',
            'NATURAL',
            'NOT',
            'NOTNULL',
            'NULL',
            'OFFSET',
            'ON',
            'ONLY',
            'OR',
            'ORDER',
            'OUTER',
            'OVER',
            'OVERLAPS',
            'PLACING',
            'PRIMARY',
            'REFERENCES',
            'RETURNING',
            'RIGHT',
            'SELECT',
            'SESSION_USER',
            'SIMILAR',
            'SOME',
            'SYMMETRIC',
            'TABLE',
            'THEN',
            'TO',
            'TRAILING',
            'TRUE',
            'UNION',
            'UNIQUE',
            'USER',
            'USING',
            'VARIADIC',
            'VERBOSE',
            'WHEN',
            'WHERE',
            'WINDOW',
            'WITH',
        ];
    }
}
<?php

namespace Doctrine\DBAL\Platforms\Keywords;

/**
 * MsSQL Keywordlist
 *
 * @deprecated Use SQLServerKeywords class instead.
 *
 * @link       www.doctrine-project.com
 */
class MsSQLKeywords extends SQLServerKeywords
{
    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'MsSQL';
    }
}
<?php

namespace Doctrine\DBAL\Platforms\Keywords;

use function array_merge;

/**
 * PostgreSQL 9.2 reserved keywords list.
 */
class PostgreSQL92Keywords extends PostgreSQL91Keywords
{
    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'PostgreSQL92';
    }

    /**
     * {@inheritdoc}
     *
     * @link http://www.postgresql.org/docs/9.2/static/sql-keywords-appendix.html
     */
    protected function getKeywords()
    {
        return array_merge(parent::getKeywords(), ['COLLATION']);
    }
}
<?php

namespace Doctrine\DBAL\Platforms\Keywords;

/**
 * DB2 Keywords.
 */
class DB2Keywords extends KeywordList
{
    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'DB2';
    }

    /**
     * {@inheritdoc}
     */
    protected function getKeywords()
    {
        return [
            'ACTIVATE',
            'ADD',
            'AFTER',
            'ALIAS',
            'ALL',
            'ALLOCATE',
            'DOCUMENT',
            'DOUBLE',
            'DROP',
            'DSSIZE',
            'DYNAMIC',
            'EACH',
            'LOCK',
            'LOCKMAX',
            'LOCKSIZE',
            'LONG',
            'LOOP',
            'MAINTAINED',
            'ROUND_CEILING',
            'ROUND_DOWN',
            'ROUND_FLOOR',
            'ROUND_HALF_DOWN',
            'ROUND_HALF_EVEN',
            'ROUND_HALF_UP',
            'ALLOW',
            'ALTER',
            'AND',
            'ANY',
            'AS',
            'ASENSITIVE',
            'ASSOCIATE',
            'ASUTIME',
            'AT',
            'ATTRIBUTES',
            'AUDIT',
            'AUTHORIZATION',
            'AUX',
            'AUXILIARY',
            'BEFORE',
            'BEGIN',
            'BETWEEN',
            'BINARY',
            'BUFFERPOOL',
            'BY',
            'CACHE',
            'CALL',
            'CALLED',
            'CAPTURE',
            'CARDINALITY',
            'CASCADED',
            'CASE',
            'CAST',
            'CCSID',
            'CHAR',
            'CHARACTER',
            'CHECK',
            'CLONE',
            'CLOSE',
            'CLUSTER',
            'COLLECTION',
            'COLLID',
            'COLUMN',
            'COMMENT',
            'COMMIT',
            'CONCAT',
            'CONDITION',
            'CONNECT',
            'CONNECTION',
            'CONSTRAINT',
            'CONTAINS',
            'CONTINUE',
            'COUNT',
            'COUNT_BIG',
            'CREATE',
            'CROSS',
            'CURRENT',
            'CURRENT_DATE',
            'CURRENT_LC_CTYPE',
            'CURRENT_PATH',
            'CURRENT_SCHEMA',
            'CURRENT_SERVER',
            'CURRENT_TIME',
            'CURRENT_TIMESTAMP',
            'CURRENT_TIMEZONE',
            'CURRENT_USER',
            'CURSOR',
            'CYCLE',
            'DATA',
            'DATABASE',
            'DATAPARTITIONNAME',
            'DATAPARTITIONNUM',
            'EDITPROC',
            'ELSE',
            'ELSEIF',
            'ENABLE',
            'ENCODING',
            'ENCRYPTION',
            'END',
            'END-EXEC',
            'ENDING',
            'ERASE',
            'ESCAPE',
            'EVERY',
            'EXCEPT',
            'EXCEPTION',
            'EXCLUDING',
            'EXCLUSIVE',
            'EXECUTE',
            'EXISTS',
            'EXIT',
            'EXPLAIN',
            'EXTERNAL',
            'EXTRACT',
            'FENCED',
            'FETCH',
            'FIELDPROC',
            'FILE',
            'FINAL',
            'FOR',
            'FOREIGN',
            'FREE',
            'FROM',
            'FULL',
            'FUNCTION',
            'GENERAL',
            'GENERATED',
            'GET',
            'GLOBAL',
            'GO',
            'GOTO',
            'GRANT',
            'GRAPHIC',
            'GROUP',
            'HANDLER',
            'HASH',
            'HASHED_VALUE',
            'HAVING',
            'HINT',
            'HOLD',
            'HOUR',
            'HOURS',
            'IDENTITY',
            'IF',
            'IMMEDIATE',
            'IN',
            'INCLUDING',
            'INCLUSIVE',
            'INCREMENT',
            'INDEX',
            'INDICATOR',
            'INF',
            'INFINITY',
            'INHERIT',
            'INNER',
            'INOUT',
            'INSENSITIVE',
            'INSERT',
            'INTEGRITY',
            'MATERIALIZED',
            'MAXVALUE',
            'MICROSECOND',
            'MICROSECONDS',
            'MINUTE',
            'MINUTES',
            'MINVALUE',
            'MODE',
            'MODIFIES',
            'MONTH',
            'MONTHS',
            'NAN',
            'NEW',
            'NEW_TABLE',
            'NEXTVAL',
            'NO',
            'NOCACHE',
            'NOCYCLE',
            'NODENAME',
            'NODENUMBER',
            'NOMAXVALUE',
            'NOMINVALUE',
            'NONE',
            'NOORDER',
            'NORMALIZED',
            'NOT',
            'NULL',
            'NULLS',
            'NUMPARTS',
            'OBID',
            'OF',
            'OLD',
            'OLD_TABLE',
            'ON',
            'OPEN',
            'OPTIMIZATION',
            'OPTIMIZE',
            'OPTION',
            'OR',
            'ORDER',
            'OUT',
            'OUTER',
            'OVER',
            'OVERRIDING',
            'PACKAGE',
            'PADDED',
            'PAGESIZE',
            'PARAMETER',
            'PART',
            'PARTITION',
            'PARTITIONED',
            'PARTITIONING',
            'PARTITIONS',
            'PASSWORD',
            'PATH',
            'PIECESIZE',
            'PLAN',
            'POSITION',
            'PRECISION',
            'PREPARE',
            'PREVVAL',
            'PRIMARY',
            'PRIQTY',
            'PRIVILEGES',
            'PROCEDURE',
            'PROGRAM',
            'PSID',
            'ROUND_UP',
            'ROUTINE',
            'ROW',
            'ROW_NUMBER',
            'ROWNUMBER',
            'ROWS',
            'ROWSET',
            'RRN',
            'RUN',
            'SAVEPOINT',
            'SCHEMA',
            'SCRATCHPAD',
            'SCROLL',
            'SEARCH',
            'SECOND',
            'SECONDS',
            'SECQTY',
            'SECURITY',
            'SELECT',
            'SENSITIVE',
            'SEQUENCE',
            'SESSION',
            'SESSION_USER',
            'SET',
            'SIGNAL',
            'SIMPLE',
            'SNAN',
            'SOME',
            'SOURCE',
            'SPECIFIC',
            'SQL',
            'SQLID',
            'STACKED',
            'STANDARD',
            'START',
            'STARTING',
            'STATEMENT',
            'STATIC',
            'STATMENT',
            'STAY',
            'STOGROUP',
            'STORES',
            'STYLE',
            'SUBSTRING',
            'SUMMARY',
            'SYNONYM',
            'SYSFUN',
            'SYSIBM',
            'SYSPROC',
            'SYSTEM',
            'SYSTEM_USER',
            'TABLE',
            'TABLESPACE',
            'THEN',
            'TIME',
            'TIMESTAMP',
            'TO',
            'TRANSACTION',
            'TRIGGER',
            'TRIM',
            'TRUNCATE',
            'TYPE',
            'UNDO',
            'UNION',
            'UNIQUE',
            'UNTIL',
            'UPDATE',
            'DATE',
            'DAY',
            'DAYS',
            'DB2GENERAL',
            'DB2GENRL',
            'DB2SQL',
            'DBINFO',
            'DBPARTITIONNAME',
            'DBPARTITIONNUM',
            'DEALLOCATE',
            'DECLARE',
            'DEFAULT',
            'DEFAULTS',
            'DEFINITION',
            'DELETE',
            'DENSE_RANK',
            'DENSERANK',
            'DESCRIBE',
            'DESCRIPTOR',
            'DETERMINISTIC',
            'DIAGNOSTICS',
            'DISABLE',
            'DISALLOW',
            'DISCONNECT',
            'DISTINCT',
            'DO',
            'INTERSECT',
            'PUBLIC',
            'USAGE',
            'INTO',
            'QUERY',
            'USER',
            'IS',
            'QUERYNO',
            'USING',
            'ISOBID',
            'RANGE',
            'VALIDPROC',
            'ISOLATION',
            'RANK',
            'VALUE',
            'ITERATE',
            'READ',
            'VALUES',
            'JAR',
            'READS',
            'VARIABLE',
            'JAVA',
            'RECOVERY',
            'VARIANT',
            'JOIN',
            'REFERENCES',
            'VCAT',
            'KEEP',
            'REFERENCING',
            'VERSION',
            'KEY',
            'REFRESH',
            'VIEW',
            'LABEL',
            'RELEASE',
            'VOLATILE',
            'LANGUAGE',
            'RENAME',
            'VOLUMES',
            'LATERAL',
            'REPEAT',
            'WHEN',
            'LC_CTYPE',
            'RESET',
            'WHENEVER',
            'LEAVE',
            'RESIGNAL',
            'WHERE',
            'LEFT',
            'RESTART',
            'WHILE',
            'LIKE',
            'RESTRICT',
            'WITH',
            'LINKTYPE',
            'RESULT',
            'WITHOUT',
            'LOCAL',
            'RESULT_SET_LOCATOR WLM',
            'LOCALDATE',
            'RETURN',
            'WRITE',
            'LOCALE',
            'RETURNS',
            'XMLELEMENT',
            'LOCALTIME',
            'REVOKE',
            'XMLEXISTS',
            'LOCALTIMESTAMP RIGHT',
            'XMLNAMESPACES',
            'LOCATOR',
            'ROLE',
            'YEAR',
            'LOCATORS',
            'ROLLBACK',
            'YEARS',
        ];
    }
}
<?php

namespace Doctrine\DBAL\Platforms\Keywords;

use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
use Doctrine\DBAL\Schema\Index;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\Sequence;
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Schema\Visitor\Visitor;

use function implode;
use function str_replace;

class ReservedKeywordsValidator implements Visitor
{
    /** @var KeywordList[] */
    private $keywordLists = [];

    /** @var string[] */
    private $violations = [];

    /**
     * @param KeywordList[] $keywordLists
     */
    public function __construct(array $keywordLists)
    {
        $this->keywordLists = $keywordLists;
    }

    /**
     * @return string[]
     */
    public function getViolations()
    {
        return $this->violations;
    }

    /**
     * @param string $word
     *
     * @return string[]
     */
    private function isReservedWord($word)
    {
        if ($word[0] === '`') {
            $word = str_replace('`', '', $word);
        }

        $keywordLists = [];
        foreach ($this->keywordLists as $keywordList) {
            if (! $keywordList->isKeyword($word)) {
                continue;
            }

            $keywordLists[] = $keywordList->getName();
        }

        return $keywordLists;
    }

    /**
     * @param string   $asset
     * @param string[] $violatedPlatforms
     *
     * @return void
     */
    private function addViolation($asset, $violatedPlatforms)
    {
        if (! $violatedPlatforms) {
            return;
        }

        $this->violations[] = $asset . ' keyword violations: ' . implode(', ', $violatedPlatforms);
    }

    /**
     * {@inheritdoc}
     */
    public function acceptColumn(Table $table, Column $column)
    {
        $this->addViolation(
            'Table ' . $table->getName() . ' column ' . $column->getName(),
            $this->isReservedWord($column->getName())
        );
    }

    /**
     * {@inheritdoc}
     */
    public function acceptForeignKey(Table $localTable, ForeignKeyConstraint $fkConstraint)
    {
    }

    /**
     * {@inheritdoc}
     */
    public function acceptIndex(Table $table, Index $index)
    {
    }

    /**
     * {@inheritdoc}
     */
    public function acceptSchema(Schema $schema)
    {
    }

    /**
     * {@inheritdoc}
     */
    public function acceptSequence(Sequence $sequence)
    {
    }

    /**
     * {@inheritdoc}
     */
    public function acceptTable(Table $table)
    {
        $this->addViolation(
            'Table ' . $table->getName(),
            $this->isReservedWord($table->getName())
        );
    }
}
<?php

namespace Doctrine\DBAL\Platforms\Keywords;

/**
 * PostgreSQL Keywordlist.
 */
class PostgreSQLKeywords extends KeywordList
{
    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'PostgreSQL';
    }

    /**
     * {@inheritdoc}
     */
    protected function getKeywords()
    {
        return [
            'ALL',
            'ANALYSE',
            'ANALYZE',
            'AND',
            'ANY',
            'AS',
            'ASC',
            'AUTHORIZATION',
            'BETWEEN',
            'BINARY',
            'BOTH',
            'CASE',
            'CAST',
            'CHECK',
            'COLLATE',
            'COLUMN',
            'CONSTRAINT',
            'CREATE',
            'CURRENT_DATE',
            'CURRENT_TIME',
            'CURRENT_TIMESTAMP',
            'CURRENT_USER',
            'DEFAULT',
            'DEFERRABLE',
            'DESC',
            'DISTINCT',
            'DO',
            'ELSE',
            'END',
            'EXCEPT',
            'FALSE',
            'FOR',
            'FOREIGN',
            'FREEZE',
            'FROM',
            'FULL',
            'GRANT',
            'GROUP',
            'HAVING',
            'ILIKE',
            'IN',
            'INITIALLY',
            'INNER',
            'INTERSECT',
            'INTO',
            'IS',
            'ISNULL',
            'JOIN',
            'LEADING',
            'LEFT',
            'LIKE',
            'LIMIT',
            'LOCALTIME',
            'LOCALTIMESTAMP',
            'NATURAL',
            'NEW',
            'NOT',
            'NOTNULL',
            'NULL',
            'OFF',
            'OFFSET',
            'OLD',
            'ON',
            'ONLY',
            'OR',
            'ORDER',
            'OUTER',
            'OVERLAPS',
            'PLACING',
            'PRIMARY',
            'REFERENCES',
            'SELECT',
            'SESSION_USER',
            'SIMILAR',
            'SOME',
            'TABLE',
            'THEN',
            'TO',
            'TRAILING',
            'TRUE',
            'UNION',
            'UNIQUE',
            'USER',
            'USING',
            'VERBOSE',
            'WHEN',
            'WHERE',
        ];
    }
}
<?php

namespace Doctrine\DBAL\Platforms\Keywords;

use function array_merge;

/**
 * SAP Sybase SQL Anywhere 16 reserved keywords list.
 */
class SQLAnywhere16Keywords extends SQLAnywhere12Keywords
{
    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'SQLAnywhere16';
    }

    /**
     * {@inheritdoc}
     *
     * @link http://dcx.sybase.com/index.html#sa160/en/dbreference/alhakeywords.html
     */
    protected function getKeywords()
    {
        return array_merge(
            parent::getKeywords(),
            [
                'ARRAY',
                'JSON',
                'ROW',
                'ROWTYPE',
                'UNNEST',
                'VARRAY',
            ]
        );
    }
}
<?php

namespace Doctrine\DBAL\Platforms\Keywords;

/**
 * Microsoft SQL Server 2000 reserved keyword dictionary.
 *
 * @link    www.doctrine-project.com
 */
class SQLServerKeywords extends KeywordList
{
    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'SQLServer';
    }

    /**
     * {@inheritdoc}
     *
     * @link http://msdn.microsoft.com/en-us/library/aa238507%28v=sql.80%29.aspx
     */
    protected function getKeywords()
    {
        return [
            'ADD',
            'ALL',
            'ALTER',
            'AND',
            'ANY',
            'AS',
            'ASC',
            'AUTHORIZATION',
            'BACKUP',
            'BEGIN',
            'BETWEEN',
            'BREAK',
            'BROWSE',
            'BULK',
            'BY',
            'CASCADE',
            'CASE',
            'CHECK',
            'CHECKPOINT',
            'CLOSE',
            'CLUSTERED',
            'COALESCE',
            'COLLATE',
            'COLUMN',
            'COMMIT',
            'COMPUTE',
            'CONSTRAINT',
            'CONTAINS',
            'CONTAINSTABLE',
            'CONTINUE',
            'CONVERT',
            'CREATE',
            'CROSS',
            'CURRENT',
            'CURRENT_DATE',
            'CURRENT_TIME',
            'CURRENT_TIMESTAMP',
            'CURRENT_USER',
            'CURSOR',
            'DATABASE',
            'DBCC',
            'DEALLOCATE',
            'DECLARE',
            'DEFAULT',
            'DELETE',
            'DENY',
            'DESC',
            'DISK',
            'DISTINCT',
            'DISTRIBUTED',
            'DOUBLE',
            'DROP',
            'DUMP',
            'ELSE',
            'END',
            'ERRLVL',
            'ESCAPE',
            'EXCEPT',
            'EXEC',
            'EXECUTE',
            'EXISTS',
            'EXIT',
            'EXTERNAL',
            'FETCH',
            'FILE',
            'FILLFACTOR',
            'FOR',
            'FOREIGN',
            'FREETEXT',
            'FREETEXTTABLE',
            'FROM',
            'FULL',
            'FUNCTION',
            'GOTO',
            'GRANT',
            'GROUP',
            'HAVING',
            'HOLDLOCK',
            'IDENTITY',
            'IDENTITY_INSERT',
            'IDENTITYCOL',
            'IF',
            'IN',
            'INDEX',
            'INNER',
            'INSERT',
            'INTERSECT',
            'INTO',
            'IS',
            'JOIN',
            'KEY',
            'KILL',
            'LEFT',
            'LIKE',
            'LINENO',
            'LOAD',
            'NATIONAL',
            'NOCHECK ',
            'NONCLUSTERED',
            'NOT',
            'NULL',
            'NULLIF',
            'OF',
            'OFF',
            'OFFSETS',
            'ON',
            'OPEN',
            'OPENDATASOURCE',
            'OPENQUERY',
            'OPENROWSET',
            'OPENXML',
            'OPTION',
            'OR',
            'ORDER',
            'OUTER',
            'OVER',
            'PERCENT',
            'PIVOT',
            'PLAN',
            'PRECISION',
            'PRIMARY',
            'PRINT',
            'PROC',
            'PROCEDURE',
            'PUBLIC',
            'RAISERROR',
            'READ',
            'READTEXT',
            'RECONFIGURE',
            'REFERENCES',
            'REPLICATION',
            'RESTORE',
            'RESTRICT',
            'RETURN',
            'REVERT',
            'REVOKE',
            'RIGHT',
            'ROLLBACK',
            'ROWCOUNT',
            'ROWGUIDCOL',
            'RULE',
            'SAVE',
            'SCHEMA',
            'SECURITYAUDIT',
            'SELECT',
            'SESSION_USER',
            'SET',
            'SETUSER',
            'SHUTDOWN',
            'SOME',
            'STATISTICS',
            'SYSTEM_USER',
            'TABLE',
            'TABLESAMPLE',
            'TEXTSIZE',
            'THEN',
            'TO',
            'TOP',
            'TRAN',
            'TRANSACTION',
            'TRIGGER',
            'TRUNCATE',
            'TSEQUAL',
            'UNION',
            'UNIQUE',
            'UNPIVOT',
            'UPDATE',
            'UPDATETEXT',
            'USE',
            'USER',
            'VALUES',
            'VARYING',
            'VIEW',
            'WAITFOR',
            'WHEN',
            'WHERE',
            'WHILE',
            'WITH',
            'WRITETEXT',
        ];
    }
}
<?php

namespace Doctrine\DBAL\Platforms\Keywords;

use function array_merge;

/**
 * Microsoft SQL Server 2008 reserved keyword dictionary.
 *
 * @link    www.doctrine-project.com
 */
class SQLServer2008Keywords extends SQLServer2005Keywords
{
    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'SQLServer2008';
    }

    /**
     * {@inheritdoc}
     *
     * @link http://msdn.microsoft.com/en-us/library/ms189822%28v=sql.100%29.aspx
     */
    protected function getKeywords()
    {
        return array_merge(parent::getKeywords(), ['MERGE']);
    }
}
<?php

namespace Doctrine\DBAL\Platforms\Keywords;

/**
 * Drizzle Keywordlist.
 */
class DrizzleKeywords extends KeywordList
{
    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'drizzle';
    }

    /**
     * {@inheritdoc}
     */
    protected function getKeywords()
    {
        return [
            'ABS',
            'ALL',
            'ALLOCATE',
            'ALTER',
            'AND',
            'ANY',
            'ARE',
            'ARRAY',
            'AS',
            'ASENSITIVE',
            'ASYMMETRIC',
            'AT',
            'ATOMIC',
            'AUTHORIZATION',
            'AVG',
            'BEGIN',
            'BETWEEN',
            'BIGINT',
            'BINARY',
            'BLOB',
            'BOOLEAN',
            'BOTH',
            'BY',
            'CALL',
            'CALLED',
            'CARDINALITY',
            'CASCADED',
            'CASE',
            'CAST',
            'CEIL',
            'CEILING',
            'CHAR',
            'CHARACTER',
            'CHARACTER_LENGTH',
            'CHAR_LENGTH',
            'CHECK',
            'CLOB',
            'CLOSE',
            'COALESCE',
            'COLLATE',
            'COLLECT',
            'COLUMN',
            'COMMIT',
            'CONDITION',
            'CONNECT',
            'CONSTRAINT',
            'CONVERT',
            'CORR',
            'CORRESPONDING',
            'COUNT',
            'COVAR_POP',
            'COVAR_SAMP',
            'CREATE',
            'CROSS',
            'CUBE',
            'CUME_DIST',
            'CURRENT',
            'CURRENT_DATE',
            'CURRENT_DEFAULT_TRANSFORM_GROUP',
            'CURRENT_PATH',
            'CURRENT_ROLE',
            'CURRENT_TIME',
            'CURRENT_TIMESTAMP',
            'CURRENT_TRANSFORM_GROUP_FOR_TYPE',
            'CURRENT_USER',
            'CURSOR',
            'CYCLE',
            'DATE',
            'DAY',
            'DEALLOCATE',
            'DEC',
            'DECIMAL',
            'DECLARE',
            'DEFAULT',
            'DELETE',
            'DENSE_RANK',
            'DEREF',
            'DESCRIBE',
            'DETERMINISTIC',
            'DISCONNECT',
            'DISTINCT',
            'DOUBLE',
            'DROP',
            'DYNAMIC',
            'EACH',
            'ELEMENT',
            'ELSE',
            'END',
            'ESCAPE',
            'EVERY',
            'EXCEPT',
            'EXEC',
            'EXECUTE',
            'EXISTS',
            'EXP',
            'EXTERNAL',
            'EXTRACT',
            'FALSE',
            'FETCH',
            'FILTER',
            'FLOAT',
            'FLOOR',
            'FOR',
            'FOREIGN',
            'FREE',
            'FROM',
            'FULL',
            'FUNCTION',
            'FUSION',
            'GET',
            'GLOBAL',
            'GRANT',
            'GROUP',
            'GROUPING',
            'HAVING',
            'HOLD',
            'HOUR',
            'IDENTITY',
            'IN',
            'INDICATOR',
            'INNER',
            'INOUT',
            'INSENSITIVE',
            'INSERT',
            'INT',
            'INTEGER',
            'INTERSECT',
            'INTERSECTION',
            'INTERVAL',
            'INTO',
            'IS',
            'JOIN',
            'LANGUAGE',
            'LARGE',
            'LATERAL',
            'LEADING',
            'LEFT',
            'LIKE',
            'LN',
            'LOCAL',
            'LOCALTIME',
            'LOCALTIMESTAMP',
            'LOWER',
            'MATCH',
            'MAX',
            'MEMBER',
            'MERGE',
            'METHOD',
            'MIN',
            'MINUTE',
            'MOD',
            'MODIFIES',
            'MODULE',
            'MONTH',
            'MULTISET',
            'NATIONAL',
            'NATURAL',
            'NCHAR',
            'NCLOB',
            'NEW',
            'NO',
            'NONE',
            'NORMALIZE',
            'NOT',
            'NULL_SYM',
            'NULLIF',
            'NUMERIC',
            'OCTET_LENGTH',
            'OF',
            'OLD',
            'ON',
            'ONLY',
            'OPEN',
            'OR',
            'ORDER',
            'OUT',
            'OUTER',
            'OVER',
            'OVERLAPS',
            'OVERLAY',
            'PARAMETER',
            'PARTITION',
            'PERCENTILE_CONT',
            'PERCENTILE_DISC',
            'PERCENT_RANK',
            'POSITION',
            'POWER',
            'PRECISION',
            'PREPARE',
            'PRIMARY',
            'PROCEDURE',
            'RANGE',
            'RANK',
            'READS',
            'REAL',
            'RECURSIVE',
            'REF',
            'REFERENCES',
            'REFERENCING',
            'REGR_AVGX',
            'REGR_AVGY',
            'REGR_COUNT',
            'REGR_INTERCEPT',
            'REGR_R2',
            'REGR_SLOPE',
            'REGR_SXX',
            'REGR_SXY',
            'REGR_SYY',
            'RELEASE',
            'RESULT',
            'RETURN',
            'RETURNS',
            'REVOKE',
            'RIGHT',
            'ROLLBACK',
            'ROLLUP',
            'ROW',
            'ROWS',
            'ROW_NUMBER',
            'SAVEPOINT',
            'SCOPE',
            'SCROLL',
            'SEARCH',
            'SECOND',
            'SELECT',
            'SENSITIVE',
            'SESSION_USER',
            'SET',
            'SIMILAR',
            'SMALLINT',
            'SOME',
            'SPECIFIC',
            'SPECIFICTYPE',
            'SQL',
            'SQLEXCEPTION',
            'SQLSTATE',
            'SQLWARNING',
            'SQRT',
            'START',
            'STATIC',
            'STDDEV_POP',
            'STDDEV_SAMP',
            'SUBMULTISET',
            'SUBSTRING',
            'SUM',
            'SYMMETRIC',
            'SYSTEM',
            'SYSTEM_USER',
            'TABLE',
            'TABLESAMPLE',
            'THEN',
            'TIME',
            'TIMESTAMP',
            'TIMEZONE_HOUR',
            'TIMEZONE_MINUTE',
            'TO',
            'TRAILING',
            'TRANSLATE',
            'TRANSLATION',
            'TREAT',
            'TRIGGER',
            'TRIM',
            'TRUE',
            'UESCAPE',
            'UNION',
            'UNIQUE',
            'UNKNOWN',
            'UNNEST',
            'UPDATE',
            'UPPER',
            'USER',
            'USING',
            'VALUE',
            'VALUES',
            'VARCHAR',
            'VARYING',
            'VAR_POP',
            'VAR_SAMP',
            'WHEN',
            'WHENEVER',
            'WHERE',
            'WIDTH_BUCKET',
            'WINDOW',
            'WITH',
            'WITHIN',
            'WITHOUT',
            'XML',
            'XMLAGG',
            'XMLATTRIBUTES',
            'XMLBINARY',
            'XMLCOMMENT',
            'XMLCONCAT',
            'XMLELEMENT',
            'XMLFOREST',
            'XMLNAMESPACES',
            'XMLPARSE',
            'XMLPI',
            'XMLROOT',
            'XMLSERIALIZE',
            'YEAR',
        ];
    }
}
<?php

namespace Doctrine\DBAL\Platforms\Keywords;

/**
 * MySQL 5.7 reserved keywords list.
 */
class MySQL57Keywords extends MySQLKeywords
{
    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'MySQL57';
    }

    /**
     * {@inheritdoc}
     *
     * @link http://dev.mysql.com/doc/mysqld-version-reference/en/mysqld-version-reference-reservedwords-5-7.html
     */
    protected function getKeywords()
    {
        return [
            'ACCESSIBLE',
            'ADD',
            'ALL',
            'ALTER',
            'ANALYZE',
            'AND',
            'AS',
            'ASC',
            'ASENSITIVE',
            'BEFORE',
            'BETWEEN',
            'BIGINT',
            'BINARY',
            'BLOB',
            'BOTH',
            'BY',
            'CALL',
            'CASCADE',
            'CASE',
            'CHANGE',
            'CHAR',
            'CHARACTER',
            'CHECK',
            'COLLATE',
            'COLUMN',
            'CONDITION',
            'CONSTRAINT',
            'CONTINUE',
            'CONVERT',
            'CREATE',
            'CROSS',
            'CURRENT_DATE',
            'CURRENT_TIME',
            'CURRENT_TIMESTAMP',
            'CURRENT_USER',
            'CURSOR',
            'DATABASE',
            'DATABASES',
            'DAY_HOUR',
            'DAY_MICROSECOND',
            'DAY_MINUTE',
            'DAY_SECOND',
            'DEC',
            'DECIMAL',
            'DECLARE',
            'DEFAULT',
            'DELAYED',
            'DELETE',
            'DESC',
            'DESCRIBE',
            'DETERMINISTIC',
            'DISTINCT',
            'DISTINCTROW',
            'DIV',
            'DOUBLE',
            'DROP',
            'DUAL',
            'EACH',
            'ELSE',
            'ELSEIF',
            'ENCLOSED',
            'ESCAPED',
            'EXISTS',
            'EXIT',
            'EXPLAIN',
            'FALSE',
            'FETCH',
            'FLOAT',
            'FLOAT4',
            'FLOAT8',
            'FOR',
            'FORCE',
            'FOREIGN',
            'FROM',
            'FULLTEXT',
            'GENERATED',
            'GET',
            'GRANT',
            'GROUP',
            'HAVING',
            'HIGH_PRIORITY',
            'HOUR_MICROSECOND',
            'HOUR_MINUTE',
            'HOUR_SECOND',
            'IF',
            'IGNORE',
            'IN',
            'INDEX',
            'INFILE',
            'INNER',
            'INOUT',
            'INSENSITIVE',
            'INSERT',
            'INT',
            'INT1',
            'INT2',
            'INT3',
            'INT4',
            'INT8',
            'INTEGER',
            'INTERVAL',
            'INTO',
            'IO_AFTER_GTIDS',
            'IO_BEFORE_GTIDS',
            'IS',
            'ITERATE',
            'JOIN',
            'KEY',
            'KEYS',
            'KILL',
            'LEADING',
            'LEAVE',
            'LEFT',
            'LIKE',
            'LIMIT',
            'LINEAR',
            'LINES',
            'LOAD',
            'LOCALTIME',
            'LOCALTIMESTAMP',
            'LOCK',
            'LONG',
            'LONGBLOB',
            'LONGTEXT',
            'LOOP',
            'LOW_PRIORITY',
            'MASTER_BIND',
            'MASTER_SSL_VERIFY_SERVER_CERT',
            'MATCH',
            'MAXVALUE',
            'MEDIUMBLOB',
            'MEDIUMINT',
            'MEDIUMTEXT',
            'MIDDLEINT',
            'MINUTE_MICROSECOND',
            'MINUTE_SECOND',
            'MOD',
            'MODIFIES',
            'NATURAL',
            'NO_WRITE_TO_BINLOG',
            'NOT',
            'NULL',
            'NUMERIC',
            'ON',
            'OPTIMIZE',
            'OPTIMIZER_COSTS',
            'OPTION',
            'OPTIONALLY',
            'OR',
            'ORDER',
            'OUT',
            'OUTER',
            'OUTFILE',
            'PARTITION',
            'PRECISION',
            'PRIMARY',
            'PROCEDURE',
            'PURGE',
            'RANGE',
            'READ',
            'READ_WRITE',
            'READS',
            'REAL',
            'REFERENCES',
            'REGEXP',
            'RELEASE',
            'RENAME',
            'REPEAT',
            'REPLACE',
            'REQUIRE',
            'RESIGNAL',
            'RESTRICT',
            'RETURN',
            'REVOKE',
            'RIGHT',
            'RLIKE',
            'SCHEMA',
            'SCHEMAS',
            'SECOND_MICROSECOND',
            'SELECT',
            'SENSITIVE',
            'SEPARATOR',
            'SET',
            'SHOW',
            'SIGNAL',
            'SMALLINT',
            'SPATIAL',
            'SPECIFIC',
            'SQL',
            'SQL_BIG_RESULT',
            'SQL_CALC_FOUND_ROWS',
            'SQL_SMALL_RESULT',
            'SQLEXCEPTION',
            'SQLSTATE',
            'SQLWARNING',
            'SSL',
            'STARTING',
            'STORED',
            'STRAIGHT_JOIN',
            'TABLE',
            'TERMINATED',
            'THEN',
            'TINYBLOB',
            'TINYINT',
            'TINYTEXT',
            'TO',
            'TRAILING',
            'TRIGGER',
            'TRUE',
            'UNDO',
            'UNION',
            'UNIQUE',
            'UNLOCK',
            'UNSIGNED',
            'UPDATE',
            'USAGE',
            'USE',
            'USING',
            'UTC_DATE',
            'UTC_TIME',
            'UTC_TIMESTAMP',
            'VALUES',
            'VARBINARY',
            'VARCHAR',
            'VARCHARACTER',
            'VARYING',
            'VIRTUAL',
            'WHEN',
            'WHERE',
            'WHILE',
            'WITH',
            'WRITE',
            'XOR',
            'YEAR_MONTH',
            'ZEROFILL',
        ];
    }
}
<?php

namespace Doctrine\DBAL\Platforms\Keywords;

/**
 * Oracle Keywordlist.
 */
class OracleKeywords extends KeywordList
{
    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'Oracle';
    }

    /**
     * {@inheritdoc}
     */
    protected function getKeywords()
    {
        return [
            'ACCESS',
            'ELSE',
            'MODIFY',
            'START',
            'ADD',
            'EXCLUSIVE',
            'NOAUDIT',
            'SELECT',
            'ALL',
            'EXISTS',
            'NOCOMPRESS',
            'SESSION',
            'ALTER',
            'FILE',
            'NOT',
            'SET',
            'AND',
            'FLOAT',
            'NOTFOUND ',
            'SHARE',
            'ANY',
            'FOR',
            'NOWAIT',
            'SIZE',
            'ARRAYLEN',
            'FROM',
            'NULL',
            'SMALLINT',
            'AS',
            'GRANT',
            'NUMBER',
            'SQLBUF',
            'ASC',
            'GROUP',
            'OF',
            'SUCCESSFUL',
            'AUDIT',
            'HAVING',
            'OFFLINE ',
            'SYNONYM',
            'BETWEEN',
            'IDENTIFIED',
            'ON',
            'SYSDATE',
            'BY',
            'IMMEDIATE',
            'ONLINE',
            'TABLE',
            'CHAR',
            'IN',
            'OPTION',
            'THEN',
            'CHECK',
            'INCREMENT',
            'OR',
            'TO',
            'CLUSTER',
            'INDEX',
            'ORDER',
            'TRIGGER',
            'COLUMN',
            'INITIAL',
            'PCTFREE',
            'UID',
            'COMMENT',
            'INSERT',
            'PRIOR',
            'UNION',
            'COMPRESS',
            'INTEGER',
            'PRIVILEGES',
            'UNIQUE',
            'CONNECT',
            'INTERSECT',
            'PUBLIC',
            'UPDATE',
            'CREATE',
            'INTO',
            'RAW',
            'USER',
            'CURRENT',
            'IS',
            'RENAME',
            'VALIDATE',
            'DATE',
            'LEVEL',
            'RESOURCE',
            'VALUES',
            'DECIMAL',
            'LIKE',
            'REVOKE',
            'VARCHAR',
            'DEFAULT',
            'LOCK',
            'ROW',
            'VARCHAR2',
            'DELETE',
            'LONG',
            'ROWID',
            'VIEW',
            'DESC',
            'MAXEXTENTS',
            'ROWLABEL',
            'WHENEVER',
            'DISTINCT',
            'MINUS',
            'ROWNUM',
            'WHERE',
            'DROP',
            'MODE',
            'ROWS',
            'WITH',
            'RANGE',
        ];
    }
}
<?php

namespace Doctrine\DBAL\Platforms\Keywords;

use function array_diff;
use function array_merge;

/**
 * SAP Sybase SQL Anywhere 12 reserved keywords list.
 */
class SQLAnywhere12Keywords extends SQLAnywhere11Keywords
{
    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'SQLAnywhere12';
    }

    /**
     * {@inheritdoc}
     *
     * @link http://dcx.sybase.com/1200/en/dbreference/alhakeywords.html
     */
    protected function getKeywords()
    {
        return array_merge(
            array_diff(
                parent::getKeywords(),
                [
                    'INDEX_LPAREN',
                    'SYNTAX_ERROR',
                    'WITH_CUBE',
                    'WITH_LPAREN',
                    'WITH_ROLLUP',
                ]
            ),
            [
                'DATETIMEOFFSET',
                'LIMIT',
                'OPENXML',
                'SPATIAL',
                'TREAT',
            ]
        );
    }
}
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Platforms\Keywords;

/**
 * PostgreSQL 10.0 reserved keywords list.
 */
class PostgreSQL100Keywords extends PostgreSQL94Keywords
{
    public function getName(): string
    {
        return 'PostgreSQL100';
    }
}
<?php

namespace Doctrine\DBAL\Platforms\Keywords;

/**
 * MySQL Keywordlist.
 */
class MySQLKeywords extends KeywordList
{
    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'MySQL';
    }

    /**
     * {@inheritdoc}
     */
    protected function getKeywords()
    {
        return [
            'ACCESSIBLE',
            'ADD',
            'ALL',
            'ALTER',
            'ANALYZE',
            'AND',
            'AS',
            'ASC',
            'ASENSITIVE',
            'BEFORE',
            'BETWEEN',
            'BIGINT',
            'BINARY',
            'BLOB',
            'BOTH',
            'BY',
            'CALL',
            'CASCADE',
            'CASE',
            'CHANGE',
            'CHAR',
            'CHARACTER',
            'CHECK',
            'COLLATE',
            'COLUMN',
            'CONDITION',
            'CONNECTION',
            'CONSTRAINT',
            'CONTINUE',
            'CONVERT',
            'CREATE',
            'CROSS',
            'CURRENT_DATE',
            'CURRENT_TIME',
            'CURRENT_TIMESTAMP',
            'CURRENT_USER',
            'CURSOR',
            'DATABASE',
            'DATABASES',
            'DAY_HOUR',
            'DAY_MICROSECOND',
            'DAY_MINUTE',
            'DAY_SECOND',
            'DEC',
            'DECIMAL',
            'DECLARE',
            'DEFAULT',
            'DELAYED',
            'DELETE',
            'DESC',
            'DESCRIBE',
            'DETERMINISTIC',
            'DISTINCT',
            'DISTINCTROW',
            'DIV',
            'DOUBLE',
            'DROP',
            'DUAL',
            'EACH',
            'ELSE',
            'ELSEIF',
            'ENCLOSED',
            'ESCAPED',
            'EXISTS',
            'EXIT',
            'EXPLAIN',
            'FALSE',
            'FETCH',
            'FLOAT',
            'FLOAT4',
            'FLOAT8',
            'FOR',
            'FORCE',
            'FOREIGN',
            'FROM',
            'FULLTEXT',
            'GENERAL',
            'GOTO',
            'GRANT',
            'GROUP',
            'HAVING',
            'HIGH_PRIORITY',
            'HOUR_MICROSECOND',
            'HOUR_MINUTE',
            'HOUR_SECOND',
            'IF',
            'IGNORE',
            'IGNORE_SERVER_IDS',
            'IN',
            'INDEX',
            'INFILE',
            'INNER',
            'INOUT',
            'INSENSITIVE',
            'INSERT',
            'INT',
            'INT1',
            'INT2',
            'INT3',
            'INT4',
            'INT8',
            'INTEGER',
            'INTERVAL',
            'INTO',
            'IS',
            'ITERATE',
            'JOIN',
            'KEY',
            'KEYS',
            'KILL',
            'LABEL',
            'LEADING',
            'LEAVE',
            'LEFT',
            'LIKE',
            'LIMIT',
            'LINEAR',
            'LINES',
            'LOAD',
            'LOCALTIME',
            'LOCALTIMESTAMP',
            'LOCK',
            'LONG',
            'LONGBLOB',
            'LONGTEXT',
            'LOOP',
            'LOW_PRIORITY',
            'MASTER_HEARTBEAT_PERIOD',
            'MASTER_SSL_VERIFY_SERVER_CERT',
            'MATCH',
            'MAXVALUE',
            'MEDIUMBLOB',
            'MEDIUMINT',
            'MEDIUMTEXT',
            'MIDDLEINT',
            'MINUTE_MICROSECOND',
            'MINUTE_SECOND',
            'MOD',
            'MODIFIES',
            'NATURAL',
            'NO_WRITE_TO_BINLOG',
            'NOT',
            'NULL',
            'NUMERIC',
            'ON',
            'OPTIMIZE',
            'OPTION',
            'OPTIONALLY',
            'OR',
            'ORDER',
            'OUT',
            'OUTER',
            'OUTFILE',
            'PARTITION',
            'PRECISION',
            'PRIMARY',
            'PROCEDURE',
            'PURGE',
            'RAID0',
            'RANGE',
            'READ',
            'READ_WRITE',
            'READS',
            'REAL',
            'RECURSIVE',
            'REFERENCES',
            'REGEXP',
            'RELEASE',
            'RENAME',
            'REPEAT',
            'REPLACE',
            'REQUIRE',
            'RESIGNAL',
            'RESTRICT',
            'RETURN',
            'REVOKE',
            'RIGHT',
            'RLIKE',
            'ROWS',
            'SCHEMA',
            'SCHEMAS',
            'SECOND_MICROSECOND',
            'SELECT',
            'SENSITIVE',
            'SEPARATOR',
            'SET',
            'SHOW',
            'SIGNAL',
            'SLOW',
            'SMALLINT',
            'SONAME',
            'SPATIAL',
            'SPECIFIC',
            'SQL',
            'SQL_BIG_RESULT',
            'SQL_CALC_FOUND_ROWS',
            'SQL_SMALL_RESULT',
            'SQLEXCEPTION',
            'SQLSTATE',
            'SQLWARNING',
            'SSL',
            'STARTING',
            'STRAIGHT_JOIN',
            'TABLE',
            'TERMINATED',
            'THEN',
            'TINYBLOB',
            'TINYINT',
            'TINYTEXT',
            'TO',
            'TRAILING',
            'TRIGGER',
            'TRUE',
            'UNDO',
            'UNION',
            'UNIQUE',
            'UNLOCK',
            'UNSIGNED',
            'UPDATE',
            'USAGE',
            'USE',
            'USING',
            'UTC_DATE',
            'UTC_TIME',
            'UTC_TIMESTAMP',
            'VALUES',
            'VARBINARY',
            'VARCHAR',
            'VARCHARACTER',
            'VARYING',
            'WHEN',
            'WHERE',
            'WHILE',
            'WITH',
            'WRITE',
            'X509',
            'XOR',
            'YEAR_MONTH',
            'ZEROFILL',
        ];
    }
}
<?php

namespace Doctrine\DBAL\Platforms\Keywords;

use function array_diff;
use function array_merge;

/**
 * PostgreSQL 9.4 reserved keywords list.
 */
class PostgreSQL94Keywords extends PostgreSQL92Keywords
{
    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'PostgreSQL94';
    }

    /**
     * {@inheritdoc}
     *
     * @link http://www.postgresql.org/docs/9.4/static/sql-keywords-appendix.html
     */
    protected function getKeywords()
    {
        $parentKeywords = array_diff(parent::getKeywords(), ['OVER']);

        return array_merge($parentKeywords, ['LATERAL']);
    }
}
<?php

namespace Doctrine\DBAL\Platforms\Keywords;

use function array_flip;
use function array_map;
use function strtoupper;

/**
 * Abstract interface for a SQL reserved keyword dictionary.
 *
 * @psalm-consistent-constructor
 */
abstract class KeywordList
{
    /** @var string[]|null */
    private $keywords;

    /**
     * Checks if the given word is a keyword of this dialect/vendor platform.
     *
     * @param string $word
     *
     * @return bool
     */
    public function isKeyword($word)
    {
        if ($this->keywords === null) {
            $this->initializeKeywords();
        }

        return isset($this->keywords[strtoupper($word)]);
    }

    /**
     * @return void
     */
    protected function initializeKeywords()
    {
        $this->keywords = array_flip(array_map('strtoupper', $this->getKeywords()));
    }

    /**
     * Returns the list of keywords.
     *
     * @return string[]
     */
    abstract protected function getKeywords();

    /**
     * Returns the name of this keyword list.
     *
     * @return string
     */
    abstract public function getName();
}
<?php

namespace Doctrine\DBAL\Platforms\Keywords;

use function array_merge;

/**
 * MySQL 8.0 reserved keywords list.
 */
class MySQL80Keywords extends MySQL57Keywords
{
    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'MySQL80';
    }

    /**
     * {@inheritdoc}
     *
     * @link https://dev.mysql.com/doc/refman/8.0/en/keywords.html
     */
    protected function getKeywords()
    {
        $keywords = parent::getKeywords();

        $keywords = array_merge($keywords, [
            'ADMIN',
            'ARRAY',
            'CUBE',
            'CUME_DIST',
            'DENSE_RANK',
            'EMPTY',
            'EXCEPT',
            'FIRST_VALUE',
            'FUNCTION',
            'GROUPING',
            'GROUPS',
            'JSON_TABLE',
            'LAG',
            'LAST_VALUE',
            'LATERAL',
            'LEAD',
            'MEMBER',
            'NTH_VALUE',
            'NTILE',
            'OF',
            'OVER',
            'PERCENT_RANK',
            'PERSIST',
            'PERSIST_ONLY',
            'RANK',
            'RECURSIVE',
            'ROW',
            'ROWS',
            'ROW_NUMBER',
            'SYSTEM',
            'WINDOW',
        ]);

        return $keywords;
    }
}
<?php

namespace Doctrine\DBAL\Platforms\Keywords;

use function array_merge;

/**
 * Microsoft SQL Server 2012 reserved keyword dictionary.
 *
 * @link    www.doctrine-project.com
 */
class SQLServer2012Keywords extends SQLServer2008Keywords
{
    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'SQLServer2012';
    }

    /**
     * {@inheritdoc}
     *
     * @link http://msdn.microsoft.com/en-us/library/ms189822.aspx
     */
    protected function getKeywords()
    {
        return array_merge(parent::getKeywords(), [
            'SEMANTICKEYPHRASETABLE',
            'SEMANTICSIMILARITYDETAILSTABLE',
            'SEMANTICSIMILARITYTABLE',
            'TRY_CONVERT',
            'WITHIN GROUP',
        ]);
    }
}
<?php

namespace Doctrine\DBAL\Platforms\Keywords;

use function array_diff;
use function array_merge;

/**
 * Microsoft SQL Server 2005 reserved keyword dictionary.
 *
 * @link    www.doctrine-project.com
 */
class SQLServer2005Keywords extends SQLServerKeywords
{
    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'SQLServer2005';
    }

    /**
     * {@inheritdoc}
     *
     * @link http://msdn.microsoft.com/en-US/library/ms189822%28v=sql.90%29.aspx
     */
    protected function getKeywords()
    {
        return array_merge(array_diff(parent::getKeywords(), ['DUMMY']), [
            'EXTERNAL',
            'PIVOT',
            'REVERT',
            'SECURITYAUDIT',
            'TABLESAMPLE',
            'UNPIVOT',
        ]);
    }
}
<?php

namespace Doctrine\DBAL\Platforms;

use Doctrine\DBAL\Schema\Index;
use Doctrine\DBAL\Schema\TableDiff;
use Doctrine\DBAL\Types\Types;

/**
 * Provides the behavior, features and SQL dialect of the MySQL 5.7 (5.7.9 GA) database platform.
 */
class MySQL57Platform extends MySqlPlatform
{
    /**
     * {@inheritdoc}
     */
    public function hasNativeJsonType()
    {
        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function getJsonTypeDeclarationSQL(array $column)
    {
        return 'JSON';
    }

    /**
     * {@inheritdoc}
     */
    protected function getPreAlterTableRenameIndexForeignKeySQL(TableDiff $diff)
    {
        return [];
    }

    /**
     * {@inheritdoc}
     */
    protected function getPostAlterTableRenameIndexForeignKeySQL(TableDiff $diff)
    {
        return [];
    }

    /**
     * {@inheritdoc}
     */
    protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName)
    {
        return ['ALTER TABLE ' . $tableName . ' RENAME INDEX ' . $oldIndexName . ' TO ' . $index->getQuotedName($this)];
    }

    /**
     * {@inheritdoc}
     */
    protected function getReservedKeywordsClass()
    {
        return Keywords\MySQL57Keywords::class;
    }

    /**
     * {@inheritdoc}
     */
    protected function initializeDoctrineTypeMappings()
    {
        parent::initializeDoctrineTypeMappings();

        $this->doctrineTypeMapping['json'] = Types::JSON;
    }
}
<?php

namespace Doctrine\DBAL\Platforms;

use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Schema\ColumnDiff;
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
use Doctrine\DBAL\Schema\Identifier;
use Doctrine\DBAL\Schema\Index;
use Doctrine\DBAL\Schema\Sequence;
use Doctrine\DBAL\Schema\TableDiff;
use Doctrine\DBAL\Types\BigIntType;
use Doctrine\DBAL\Types\BinaryType;
use Doctrine\DBAL\Types\BlobType;
use Doctrine\DBAL\Types\IntegerType;
use Doctrine\DBAL\Types\Type;
use Doctrine\Deprecations\Deprecation;
use UnexpectedValueException;

use function array_diff;
use function array_merge;
use function array_unique;
use function array_values;
use function count;
use function explode;
use function implode;
use function in_array;
use function is_array;
use function is_bool;
use function is_numeric;
use function is_string;
use function sprintf;
use function strpos;
use function strtolower;
use function trim;

/**
 * PostgreSqlPlatform.
 *
 * @deprecated Use PostgreSQL 9.4 or newer
 *
 * @todo   Rename: PostgreSQLPlatform
 */
class PostgreSqlPlatform extends AbstractPlatform
{
    /** @var bool */
    private $useBooleanTrueFalseStrings = true;

    /** @var string[][] PostgreSQL booleans literals */
    private $booleanLiterals = [
        'true' => [
            't',
            'true',
            'y',
            'yes',
            'on',
            '1',
        ],
        'false' => [
            'f',
            'false',
            'n',
            'no',
            'off',
            '0',
        ],
    ];

    /**
     * PostgreSQL has different behavior with some drivers
     * with regard to how booleans have to be handled.
     *
     * Enables use of 'true'/'false' or otherwise 1 and 0 instead.
     *
     * @param bool $flag
     *
     * @return void
     */
    public function setUseBooleanTrueFalseStrings($flag)
    {
        $this->useBooleanTrueFalseStrings = (bool) $flag;
    }

    /**
     * {@inheritDoc}
     */
    public function getSubstringExpression($string, $start, $length = null)
    {
        if ($length === null) {
            return 'SUBSTRING(' . $string . ' FROM ' . $start . ')';
        }

        return 'SUBSTRING(' . $string . ' FROM ' . $start . ' FOR ' . $length . ')';
    }

    /**
     * {@inheritDoc}
     */
    public function getNowExpression()
    {
        return 'LOCALTIMESTAMP(0)';
    }

    /**
     * {@inheritDoc}
     */
    public function getRegexpExpression()
    {
        return 'SIMILAR TO';
    }

    /**
     * {@inheritDoc}
     */
    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';
        }

        return 'POSITION(' . $substr . ' IN ' . $str . ')';
    }

    /**
     * {@inheritdoc}
     */
    protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit)
    {
        if ($unit === DateIntervalUnit::QUARTER) {
            $interval *= 3;
            $unit      = DateIntervalUnit::MONTH;
        }

        return '(' . $date . ' ' . $operator . ' (' . $interval . " || ' " . $unit . "')::interval)";
    }

    /**
     * {@inheritDoc}
     */
    public function getDateDiffExpression($date1, $date2)
    {
        return '(DATE(' . $date1 . ')-DATE(' . $date2 . '))';
    }

    /**
     * {@inheritDoc}
     */
    public function supportsSequences()
    {
        return true;
    }

    /**
     * {@inheritDoc}
     */
    public function supportsSchemas()
    {
        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function getDefaultSchemaName()
    {
        return 'public';
    }

    /**
     * {@inheritDoc}
     */
    public function supportsIdentityColumns()
    {
        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function supportsPartialIndexes()
    {
        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function usesSequenceEmulatedIdentityColumns()
    {
        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function getIdentitySequenceName($tableName, $columnName)
    {
        return $tableName . '_' . $columnName . '_seq';
    }

    /**
     * {@inheritDoc}
     */
    public function supportsCommentOnStatement()
    {
        return true;
    }

    /**
     * {@inheritDoc}
     *
     * @deprecated
     */
    public function prefersSequences()
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/4229',
            'AbstractPlatform::prefersSequences() is deprecated without replacement and removed in DBAL 3.0'
        );

        return true;
    }

    /**
     * {@inheritDoc}
     */
    public function hasNativeGuidType()
    {
        return true;
    }

    /**
     * {@inheritDoc}
     */
    public function getListDatabasesSQL()
    {
        return 'SELECT datname FROM pg_database';
    }

    /**
     * {@inheritDoc}
     */
    public function getListNamespacesSQL()
    {
        return "SELECT schema_name AS nspname
                FROM   information_schema.schemata
                WHERE  schema_name NOT LIKE 'pg\_%'
                AND    schema_name != 'information_schema'";
    }

    /**
     * {@inheritDoc}
     */
    public function getListSequencesSQL($database)
    {
        return "SELECT sequence_name AS relname,
                       sequence_schema AS schemaname
                FROM   information_schema.sequences
                WHERE  sequence_schema NOT LIKE 'pg\_%'
                AND    sequence_schema != 'information_schema'";
    }

    /**
     * {@inheritDoc}
     */
    public function getListTablesSQL()
    {
        return "SELECT quote_ident(table_name) AS table_name,
                       table_schema AS schema_name
                FROM   information_schema.tables
                WHERE  table_schema NOT LIKE 'pg\_%'
                AND    table_schema != 'information_schema'
                AND    table_name != 'geometry_columns'
                AND    table_name != 'spatial_ref_sys'
                AND    table_type != 'VIEW'";
    }

    /**
     * {@inheritDoc}
     */
    public function getListViewsSQL($database)
    {
        return 'SELECT quote_ident(table_name) AS viewname,
                       table_schema AS schemaname,
                       view_definition AS definition
                FROM   information_schema.views
                WHERE  view_definition IS NOT NULL';
    }

    /**
     * @param string      $table
     * @param string|null $database
     *
     * @return string
     */
    public function getListTableForeignKeysSQL($table, $database = null)
    {
        return 'SELECT quote_ident(r.conname) as 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, pg_catalog.pg_namespace n
                      WHERE ' . $this->getTableWhereClause($table) . " AND n.oid = c.relnamespace
                  )
                  AND r.contype = 'f'";
    }

    /**
     * {@inheritDoc}
     */
    public function getCreateViewSQL($name, $sql)
    {
        return 'CREATE VIEW ' . $name . ' AS ' . $sql;
    }

    /**
     * {@inheritDoc}
     */
    public function getDropViewSQL($name)
    {
        return 'DROP VIEW ' . $name;
    }

    /**
     * {@inheritDoc}
     */
    public function getListTableConstraintsSQL($table)
    {
        $table = new Identifier($table);
        $table = $this->quoteStringLiteral($table->getName());

        return sprintf(
            <<<'SQL'
SELECT
    quote_ident(relname) as relname
FROM
    pg_class
WHERE oid IN (
    SELECT indexrelid
    FROM pg_index, pg_class
    WHERE pg_class.relname = %s
        AND pg_class.oid = pg_index.indrelid
        AND (indisunique = 't' OR indisprimary = 't')
    )
SQL
            ,
            $table
        );
    }

    /**
     * {@inheritDoc}
     *
     * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html
     */
    public function getListTableIndexesSQL($table, $database = null)
    {
        return 'SELECT quote_ident(relname) as relname, pg_index.indisunique, pg_index.indisprimary,
                       pg_index.indkey, pg_index.indrelid,
                       pg_get_expr(indpred, indrelid) AS where
                 FROM pg_class, pg_index
                 WHERE oid IN (
                    SELECT indexrelid
                    FROM pg_index si, pg_class sc, pg_namespace sn
                    WHERE ' . $this->getTableWhereClause($table, 'sc', 'sn') . '
                    AND sc.oid=si.indrelid AND sc.relnamespace = sn.oid
                 ) AND pg_index.indexrelid = oid';
    }

    /**
     * @param string $table
     * @param string $classAlias
     * @param string $namespaceAlias
     *
     * @return string
     */
    private function getTableWhereClause($table, $classAlias = 'c', $namespaceAlias = 'n')
    {
        $whereClause = $namespaceAlias . ".nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast') AND ";
        if (strpos($table, '.') !== false) {
            [$schema, $table] = explode('.', $table);
            $schema           = $this->quoteStringLiteral($schema);
        } else {
            $schema = 'ANY(current_schemas(false))';
        }

        $table = new Identifier($table);
        $table = $this->quoteStringLiteral($table->getName());

        return $whereClause . sprintf(
            '%s.relname = %s AND %s.nspname = %s',
            $classAlias,
            $table,
            $namespaceAlias,
            $schema
        );
    }

    /**
     * {@inheritDoc}
     */
    public function getListTableColumnsSQL($table, $database = null)
    {
        return "SELECT
                    a.attnum,
                    quote_ident(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.oid = a.atttypid) 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_get_expr(adbin, adrelid)
                     FROM pg_attrdef
                     WHERE c.oid = pg_attrdef.adrelid
                        AND pg_attrdef.adnum=a.attnum
                    ) AS default,
                    (SELECT pg_description.description
                        FROM pg_description WHERE pg_description.objoid = c.oid AND a.attnum = pg_description.objsubid
                    ) AS comment
                    FROM pg_attribute a, pg_class c, pg_type t, pg_namespace n
                    WHERE " . $this->getTableWhereClause($table, 'c', 'n') . '
                        AND a.attnum > 0
                        AND a.attrelid = c.oid
                        AND a.atttypid = t.oid
                        AND n.oid = c.relnamespace
                    ORDER BY a.attnum';
    }

    /**
     * {@inheritDoc}
     */
    public function getCreateDatabaseSQL($name)
    {
        return 'CREATE DATABASE ' . $name;
    }

    /**
     * Returns the SQL statement for disallowing new connections on the given database.
     *
     * This is useful to force DROP DATABASE operations which could fail because of active connections.
     *
     * @deprecated
     *
     * @param string $database The name of the database to disallow new connections for.
     *
     * @return string
     */
    public function getDisallowDatabaseConnectionsSQL($database)
    {
        return "UPDATE pg_database SET datallowconn = 'false' WHERE datname = " . $this->quoteStringLiteral($database);
    }

    /**
     * Returns the SQL statement for closing currently active connections on the given database.
     *
     * This is useful to force DROP DATABASE operations which could fail because of active connections.
     *
     * @deprecated
     *
     * @param string $database The name of the database to close currently active connections for.
     *
     * @return string
     */
    public function getCloseActiveDatabaseConnectionsSQL($database)
    {
        return 'SELECT pg_terminate_backend(procpid) FROM pg_stat_activity WHERE datname = '
            . $this->quoteStringLiteral($database);
    }

    /**
     * {@inheritDoc}
     */
    public function getAdvancedForeignKeyOptionsSQL(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)
            || ($foreignKey->hasOption('deferred') && $foreignKey->getOption('deferred') !== false)
        ) {
            $query .= ' INITIALLY DEFERRED';
        } else {
            $query .= ' INITIALLY IMMEDIATE';
        }

        return $query;
    }

    /**
     * {@inheritDoc}
     */
    public function getAlterTableSQL(TableDiff $diff)
    {
        $sql         = [];
        $commentsSQL = [];
        $columnSql   = [];

        foreach ($diff->addedColumns as $column) {
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
                continue;
            }

            $query = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray());
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;

            $comment = $this->getColumnComment($column);

            if ($comment === null || $comment === '') {
                continue;
            }

            $commentsSQL[] = $this->getCommentOnColumnSQL(
                $diff->getName($this)->getQuotedName($this),
                $column->getQuotedName($this),
                $comment
            );
        }

        foreach ($diff->removedColumns as $column) {
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
                continue;
            }

            $query = 'DROP ' . $column->getQuotedName($this);
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
        }

        foreach ($diff->changedColumns as $columnDiff) {
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
                continue;
            }

            if ($this->isUnchangedBinaryColumn($columnDiff)) {
                continue;
            }

            $oldColumnName = $columnDiff->getOldColumnName()->getQuotedName($this);
            $column        = $columnDiff->column;

            if (
                $columnDiff->hasChanged('type')
                || $columnDiff->hasChanged('precision')
                || $columnDiff->hasChanged('scale')
                || $columnDiff->hasChanged('fixed')
            ) {
                $type = $column->getType();

                // SERIAL/BIGSERIAL are not "real" types and we can't alter a column to that type
                $columnDefinition                  = $column->toArray();
                $columnDefinition['autoincrement'] = false;

                // here was a server version check before, but DBAL API does not support this anymore.
                $query = 'ALTER ' . $oldColumnName . ' TYPE ' . $type->getSQLDeclaration($columnDefinition, $this);
                $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
            }

            if ($columnDiff->hasChanged('default') || $this->typeChangeBreaksDefaultValue($columnDiff)) {
                $defaultClause = $column->getDefault() === null
                    ? ' DROP DEFAULT'
                    : ' SET' . $this->getDefaultValueDeclarationSQL($column->toArray());
                $query         = 'ALTER ' . $oldColumnName . $defaultClause;
                $sql[]         = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
            }

            if ($columnDiff->hasChanged('notnull')) {
                $query = 'ALTER ' . $oldColumnName . ' ' . ($column->getNotnull() ? 'SET' : 'DROP') . ' NOT NULL';
                $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
            }

            if ($columnDiff->hasChanged('autoincrement')) {
                if ($column->getAutoincrement()) {
                    // add autoincrement
                    $seqName = $this->getIdentitySequenceName($diff->name, $oldColumnName);

                    $sql[] = 'CREATE SEQUENCE ' . $seqName;
                    $sql[] = "SELECT setval('" . $seqName . "', (SELECT MAX(" . $oldColumnName . ') FROM '
                        . $diff->getName($this)->getQuotedName($this) . '))';
                    $query = 'ALTER ' . $oldColumnName . " SET DEFAULT nextval('" . $seqName . "')";
                    $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $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->getName($this)->getQuotedName($this) . ' ' . $query;
                }
            }

            $newComment = $this->getColumnComment($column);
            $oldComment = $this->getOldColumnComment($columnDiff);

            if (
                $columnDiff->hasChanged('comment')
                || ($columnDiff->fromColumn !== null && $oldComment !== $newComment)
            ) {
                $commentsSQL[] = $this->getCommentOnColumnSQL(
                    $diff->getName($this)->getQuotedName($this),
                    $column->getQuotedName($this),
                    $newComment
                );
            }

            if (! $columnDiff->hasChanged('length')) {
                continue;
            }

            $query = 'ALTER ' . $oldColumnName . ' TYPE '
                . $column->getType()->getSQLDeclaration($column->toArray(), $this);
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
        }

        foreach ($diff->renamedColumns as $oldColumnName => $column) {
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
                continue;
            }

            $oldColumnName = new Identifier($oldColumnName);

            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) .
                ' RENAME COLUMN ' . $oldColumnName->getQuotedName($this) . ' TO ' . $column->getQuotedName($this);
        }

        $tableSql = [];

        if (! $this->onSchemaAlterTable($diff, $tableSql)) {
            $sql = array_merge($sql, $commentsSQL);

            $newName = $diff->getNewName();

            if ($newName !== false) {
                $sql[] = sprintf(
                    'ALTER TABLE %s RENAME TO %s',
                    $diff->getName($this)->getQuotedName($this),
                    $newName->getQuotedName($this)
                );
            }

            $sql = array_merge(
                $this->getPreAlterTableIndexForeignKeySQL($diff),
                $sql,
                $this->getPostAlterTableIndexForeignKeySQL($diff)
            );
        }

        return array_merge($sql, $tableSql, $columnSql);
    }

    /**
     * Checks whether a given column diff is a logically unchanged binary type column.
     *
     * Used to determine whether a column alteration for a binary type column can be skipped.
     * Doctrine's {@link BinaryType} and {@link BlobType} are mapped to the same database column type on this platform
     * as this platform does not have a native VARBINARY/BINARY column type. Therefore the comparator
     * might detect differences for binary type columns which do not have to be propagated
     * to database as there actually is no difference at database level.
     *
     * @param ColumnDiff $columnDiff The column diff to check against.
     *
     * @return bool True if the given column diff is an unchanged binary type column, false otherwise.
     */
    private function isUnchangedBinaryColumn(ColumnDiff $columnDiff)
    {
        $columnType = $columnDiff->column->getType();

        if (! $columnType instanceof BinaryType && ! $columnType instanceof BlobType) {
            return false;
        }

        $fromColumn = $columnDiff->fromColumn instanceof Column ? $columnDiff->fromColumn : null;

        if ($fromColumn) {
            $fromColumnType = $fromColumn->getType();

            if (! $fromColumnType instanceof BinaryType && ! $fromColumnType instanceof BlobType) {
                return false;
            }

            return count(array_diff($columnDiff->changedProperties, ['type', 'length', 'fixed'])) === 0;
        }

        if ($columnDiff->hasChanged('type')) {
            return false;
        }

        return count(array_diff($columnDiff->changedProperties, ['length', 'fixed'])) === 0;
    }

    /**
     * {@inheritdoc}
     */
    protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName)
    {
        if (strpos($tableName, '.') !== false) {
            [$schema]     = explode('.', $tableName);
            $oldIndexName = $schema . '.' . $oldIndexName;
        }

        return ['ALTER INDEX ' . $oldIndexName . ' RENAME TO ' . $index->getQuotedName($this)];
    }

    /**
     * {@inheritdoc}
     */
    public function getCommentOnColumnSQL($tableName, $columnName, $comment)
    {
        $tableName  = new Identifier($tableName);
        $columnName = new Identifier($columnName);
        $comment    = $comment === null ? 'NULL' : $this->quoteStringLiteral($comment);

        return sprintf(
            'COMMENT ON COLUMN %s.%s IS %s',
            $tableName->getQuotedName($this),
            $columnName->getQuotedName($this),
            $comment
        );
    }

    /**
     * {@inheritDoc}
     */
    public function getCreateSequenceSQL(Sequence $sequence)
    {
        return 'CREATE SEQUENCE ' . $sequence->getQuotedName($this) .
            ' INCREMENT BY ' . $sequence->getAllocationSize() .
            ' MINVALUE ' . $sequence->getInitialValue() .
            ' START ' . $sequence->getInitialValue() .
            $this->getSequenceCacheSQL($sequence);
    }

    /**
     * {@inheritDoc}
     */
    public function getAlterSequenceSQL(Sequence $sequence)
    {
        return 'ALTER SEQUENCE ' . $sequence->getQuotedName($this) .
            ' INCREMENT BY ' . $sequence->getAllocationSize() .
            $this->getSequenceCacheSQL($sequence);
    }

    /**
     * Cache definition for sequences
     *
     * @return string
     */
    private function getSequenceCacheSQL(Sequence $sequence)
    {
        if ($sequence->getCache() > 1) {
            return ' CACHE ' . $sequence->getCache();
        }

        return '';
    }

    /**
     * {@inheritDoc}
     */
    public function getDropSequenceSQL($sequence)
    {
        if ($sequence instanceof Sequence) {
            $sequence = $sequence->getQuotedName($this);
        }

        return 'DROP SEQUENCE ' . $sequence . ' CASCADE';
    }

    /**
     * {@inheritDoc}
     */
    public function getCreateSchemaSQL($schemaName)
    {
        return 'CREATE SCHEMA ' . $schemaName;
    }

    /**
     * {@inheritDoc}
     */
    public function getDropForeignKeySQL($foreignKey, $table)
    {
        return $this->getDropConstraintSQL($foreignKey, $table);
    }

    /**
     * {@inheritDoc}
     */
    protected function _getCreateTableSQL($name, array $columns, array $options = [])
    {
        $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 ' . $name . ' (' . $queryFields . ')';

        $sql = [$query];

        if (isset($options['indexes']) && ! empty($options['indexes'])) {
            foreach ($options['indexes'] as $index) {
                $sql[] = $this->getCreateIndexSQL($index, $name);
            }
        }

        if (isset($options['foreignKeys'])) {
            foreach ((array) $options['foreignKeys'] as $definition) {
                $sql[] = $this->getCreateForeignKeySQL($definition, $name);
            }
        }

        return $sql;
    }

    /**
     * Converts a single boolean value.
     *
     * First converts the value to its native PHP boolean type
     * and passes it to the given callback function to be reconverted
     * into any custom representation.
     *
     * @param mixed    $value    The value to convert.
     * @param callable $callback The callback function to use for converting the real boolean value.
     *
     * @return mixed
     *
     * @throws UnexpectedValueException
     */
    private function convertSingleBooleanValue($value, $callback)
    {
        if ($value === null) {
            return $callback(null);
        }

        if (is_bool($value) || is_numeric($value)) {
            return $callback((bool) $value);
        }

        if (! is_string($value)) {
            return $callback(true);
        }

        /**
         * Better safe than sorry: http://php.net/in_array#106319
         */
        if (in_array(strtolower(trim($value)), $this->booleanLiterals['false'], true)) {
            return $callback(false);
        }

        if (in_array(strtolower(trim($value)), $this->booleanLiterals['true'], true)) {
            return $callback(true);
        }

        throw new UnexpectedValueException("Unrecognized boolean literal '${value}'");
    }

    /**
     * Converts one or multiple boolean values.
     *
     * First converts the value(s) to their native PHP boolean type
     * and passes them to the given callback function to be reconverted
     * into any custom representation.
     *
     * @param mixed    $item     The value(s) to convert.
     * @param callable $callback The callback function to use for converting the real boolean value(s).
     *
     * @return mixed
     */
    private function doConvertBooleans($item, $callback)
    {
        if (is_array($item)) {
            foreach ($item as $key => $value) {
                $item[$key] = $this->convertSingleBooleanValue($value, $callback);
            }

            return $item;
        }

        return $this->convertSingleBooleanValue($item, $callback);
    }

    /**
     * {@inheritDoc}
     *
     * Postgres wants boolean values converted to the strings 'true'/'false'.
     */
    public function convertBooleans($item)
    {
        if (! $this->useBooleanTrueFalseStrings) {
            return parent::convertBooleans($item);
        }

        return $this->doConvertBooleans(
            $item,
            /**
             * @param mixed $value
             */
            static function ($value) {
                if ($value === null) {
                    return 'NULL';
                }

                return $value === true ? 'true' : 'false';
            }
        );
    }

    /**
     * {@inheritDoc}
     */
    public function convertBooleansToDatabaseValue($item)
    {
        if (! $this->useBooleanTrueFalseStrings) {
            return parent::convertBooleansToDatabaseValue($item);
        }

        return $this->doConvertBooleans(
            $item,
            /**
             * @param mixed $value
             */
            static function ($value) {
                return $value === null ? null : (int) $value;
            }
        );
    }

    /**
     * {@inheritDoc}
     */
    public function convertFromBoolean($item)
    {
        if ($item !== null && in_array(strtolower($item), $this->booleanLiterals['false'], true)) {
            return false;
        }

        return parent::convertFromBoolean($item);
    }

    /**
     * {@inheritDoc}
     */
    public function getSequenceNextValSQL($sequence)
    {
        return "SELECT NEXTVAL('" . $sequence . "')";
    }

    /**
     * {@inheritDoc}
     */
    public function getSetTransactionIsolationSQL($level)
    {
        return 'SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL '
            . $this->_getTransactionIsolationLevelSQL($level);
    }

    /**
     * {@inheritDoc}
     */
    public function getBooleanTypeDeclarationSQL(array $column)
    {
        return 'BOOLEAN';
    }

    /**
     * {@inheritDoc}
     */
    public function getIntegerTypeDeclarationSQL(array $column)
    {
        if (! empty($column['autoincrement'])) {
            return 'SERIAL';
        }

        return 'INT';
    }

    /**
     * {@inheritDoc}
     */
    public function getBigIntTypeDeclarationSQL(array $column)
    {
        if (! empty($column['autoincrement'])) {
            return 'BIGSERIAL';
        }

        return 'BIGINT';
    }

    /**
     * {@inheritDoc}
     */
    public function getSmallIntTypeDeclarationSQL(array $column)
    {
        return 'SMALLINT';
    }

    /**
     * {@inheritDoc}
     */
    public function getGuidTypeDeclarationSQL(array $column)
    {
        return 'UUID';
    }

    /**
     * {@inheritDoc}
     */
    public function getDateTimeTypeDeclarationSQL(array $column)
    {
        return 'TIMESTAMP(0) WITHOUT TIME ZONE';
    }

    /**
     * {@inheritDoc}
     */
    public function getDateTimeTzTypeDeclarationSQL(array $column)
    {
        return 'TIMESTAMP(0) WITH TIME ZONE';
    }

    /**
     * {@inheritDoc}
     */
    public function getDateTypeDeclarationSQL(array $column)
    {
        return 'DATE';
    }

    /**
     * {@inheritDoc}
     */
    public function getTimeTypeDeclarationSQL(array $column)
    {
        return 'TIME(0) WITHOUT TIME ZONE';
    }

    /**
     * {@inheritDoc}
     *
     * @deprecated Use application-generated UUIDs instead
     */
    public function getGuidExpression()
    {
        return 'UUID_GENERATE_V4()';
    }

    /**
     * {@inheritDoc}
     */
    protected function _getCommonIntegerTypeDeclarationSQL(array $column)
    {
        return '';
    }

    /**
     * {@inheritDoc}
     */
    protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed)
    {
        return $fixed ? ($length ? 'CHAR(' . $length . ')' : 'CHAR(255)')
            : ($length ? 'VARCHAR(' . $length . ')' : 'VARCHAR(255)');
    }

    /**
     * {@inheritdoc}
     */
    protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed)
    {
        return 'BYTEA';
    }

    /**
     * {@inheritDoc}
     */
    public function getClobTypeDeclarationSQL(array $column)
    {
        return 'TEXT';
    }

    /**
     * {@inheritDoc}
     */
    public function getName()
    {
        return 'postgresql';
    }

    /**
     * {@inheritDoc}
     *
     * PostgreSQL returns all column names in SQL result sets in lowercase.
     *
     * @deprecated
     */
    public function getSQLResultCasing($column)
    {
        return strtolower($column);
    }

    /**
     * {@inheritDoc}
     */
    public function getDateTimeTzFormatString()
    {
        return 'Y-m-d H:i:sO';
    }

    /**
     * {@inheritDoc}
     */
    public function getEmptyIdentityInsertSQL($quotedTableName, $quotedIdentifierColumnName)
    {
        return 'INSERT INTO ' . $quotedTableName . ' (' . $quotedIdentifierColumnName . ') VALUES (DEFAULT)';
    }

    /**
     * {@inheritDoc}
     */
    public function getTruncateTableSQL($tableName, $cascade = false)
    {
        $tableIdentifier = new Identifier($tableName);
        $sql             = 'TRUNCATE ' . $tableIdentifier->getQuotedName($this);

        if ($cascade) {
            $sql .= ' CASCADE';
        }

        return $sql;
    }

    /**
     * {@inheritDoc}
     */
    public function getReadLockSQL()
    {
        return 'FOR SHARE';
    }

    /**
     * {@inheritDoc}
     */
    protected function initializeDoctrineTypeMappings()
    {
        $this->doctrineTypeMapping = [
            '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',
            'tsvector'      => 'text',
            'varchar'       => 'string',
            'interval'      => 'string',
            '_varchar'      => 'string',
            'char'          => 'string',
            'bpchar'        => 'string',
            'inet'          => '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',
            'uuid'          => 'guid',
            'bytea'         => 'blob',
        ];
    }

    /**
     * {@inheritDoc}
     */
    public function getVarcharMaxLength()
    {
        return 65535;
    }

    /**
     * {@inheritdoc}
     */
    public function getBinaryMaxLength()
    {
        return 0;
    }

    /**
     * {@inheritdoc}
     */
    public function getBinaryDefaultLength()
    {
        return 0;
    }

    /**
     * {@inheritDoc}
     */
    protected function getReservedKeywordsClass()
    {
        return Keywords\PostgreSQLKeywords::class;
    }

    /**
     * {@inheritDoc}
     */
    public function getBlobTypeDeclarationSQL(array $column)
    {
        return 'BYTEA';
    }

    /**
     * {@inheritdoc}
     */
    public function getDefaultValueDeclarationSQL($column)
    {
        if ($this->isSerialColumn($column)) {
            return '';
        }

        return parent::getDefaultValueDeclarationSQL($column);
    }

    /**
     * @param mixed[] $column
     */
    private function isSerialColumn(array $column): bool
    {
        return isset($column['type'], $column['autoincrement'])
            && $column['autoincrement'] === true
            && $this->isNumericType($column['type']);
    }

    /**
     * Check whether the type of a column is changed in a way that invalidates the default value for the column
     */
    private function typeChangeBreaksDefaultValue(ColumnDiff $columnDiff): bool
    {
        if (! $columnDiff->fromColumn) {
            return $columnDiff->hasChanged('type');
        }

        $oldTypeIsNumeric = $this->isNumericType($columnDiff->fromColumn->getType());
        $newTypeIsNumeric = $this->isNumericType($columnDiff->column->getType());

        // default should not be changed when switching between numeric types and the default comes from a sequence
        return $columnDiff->hasChanged('type')
            && ! ($oldTypeIsNumeric && $newTypeIsNumeric && $columnDiff->column->getAutoincrement());
    }

    private function isNumericType(Type $type): bool
    {
        return $type instanceof IntegerType || $type instanceof BigIntType;
    }

    private function getOldColumnComment(ColumnDiff $columnDiff): ?string
    {
        return $columnDiff->fromColumn ? $this->getColumnComment($columnDiff->fromColumn) : null;
    }

    public function getListTableMetadataSQL(string $table, ?string $schema = null): string
    {
        if ($schema !== null) {
            $table = $schema . '.' . $table;
        }

        return sprintf(
            <<<'SQL'
SELECT obj_description(%s::regclass) AS table_comment;
SQL
            ,
            $this->quoteStringLiteral($table)
        );
    }
}
<?php

namespace Doctrine\DBAL\Platforms;

use Doctrine\DBAL\Types\Types;

/**
 * Provides the behavior, features and SQL dialect of the MariaDB 10.2 (10.2.7 GA) database platform.
 *
 * Note: Should not be used with versions prior to 10.2.7.
 */
class MariaDb1027Platform extends MySqlPlatform
{
    /**
     * {@inheritdoc}
     *
     * @link https://mariadb.com/kb/en/library/json-data-type/
     */
    public function getJsonTypeDeclarationSQL(array $column): string
    {
        return 'LONGTEXT';
    }

    protected function getReservedKeywordsClass(): string
    {
        return Keywords\MariaDb102Keywords::class;
    }

    protected function initializeDoctrineTypeMappings(): void
    {
        parent::initializeDoctrineTypeMappings();

        $this->doctrineTypeMapping['json'] = Types::JSON;
    }
}
<?php

namespace Doctrine\DBAL\Platforms;

use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Schema\ColumnDiff;
use Doctrine\DBAL\Schema\Identifier;
use Doctrine\DBAL\Schema\Index;
use Doctrine\DBAL\Schema\TableDiff;
use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Types\Types;

use function array_merge;
use function count;
use function current;
use function explode;
use function func_get_arg;
use function func_num_args;
use function implode;
use function sprintf;
use function strpos;
use function strtoupper;

class DB2Platform extends AbstractPlatform
{
    public function getCharMaxLength(): int
    {
        return 254;
    }

    /**
     * {@inheritdoc}
     */
    public function getBinaryMaxLength()
    {
        return 32704;
    }

    /**
     * {@inheritdoc}
     */
    public function getBinaryDefaultLength()
    {
        return 1;
    }

    /**
     * {@inheritDoc}
     */
    public function getVarcharTypeDeclarationSQL(array $column)
    {
        // for IBM DB2, the CHAR max length is less than VARCHAR default length
        if (! isset($column['length']) && ! empty($column['fixed'])) {
            $column['length'] = $this->getCharMaxLength();
        }

        return parent::getVarcharTypeDeclarationSQL($column);
    }

    /**
     * {@inheritDoc}
     */
    public function getBlobTypeDeclarationSQL(array $column)
    {
        // todo blob(n) with $column['length'];
        return 'BLOB(1M)';
    }

    /**
     * {@inheritDoc}
     */
    public function initializeDoctrineTypeMappings()
    {
        $this->doctrineTypeMapping = [
            'smallint'      => 'smallint',
            'bigint'        => 'bigint',
            'integer'       => 'integer',
            'time'          => 'time',
            'date'          => 'date',
            'varchar'       => 'string',
            'character'     => 'string',
            'varbinary'     => 'binary',
            'binary'        => 'binary',
            'clob'          => 'text',
            'blob'          => 'blob',
            'decimal'       => 'decimal',
            'double'        => 'float',
            'real'          => 'float',
            'timestamp'     => 'datetime',
        ];
    }

    /**
     * {@inheritdoc}
     */
    public function isCommentedDoctrineType(Type $doctrineType)
    {
        if ($doctrineType->getName() === Types::BOOLEAN) {
            // We require a commented boolean type in order to distinguish between boolean and smallint
            // as both (have to) map to the same native type.
            return true;
        }

        return parent::isCommentedDoctrineType($doctrineType);
    }

    /**
     * {@inheritDoc}
     */
    protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed)
    {
        return $fixed ? ($length ? 'CHAR(' . $length . ')' : 'CHAR(254)')
                : ($length ? 'VARCHAR(' . $length . ')' : 'VARCHAR(255)');
    }

    /**
     * {@inheritdoc}
     */
    protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed)
    {
        return $this->getVarcharTypeDeclarationSQLSnippet($length, $fixed) . ' FOR BIT DATA';
    }

    /**
     * {@inheritDoc}
     */
    public function getClobTypeDeclarationSQL(array $column)
    {
        // todo clob(n) with $column['length'];
        return 'CLOB(1M)';
    }

    /**
     * {@inheritDoc}
     */
    public function getName()
    {
        return 'db2';
    }

    /**
     * {@inheritDoc}
     */
    public function getBooleanTypeDeclarationSQL(array $column)
    {
        return 'SMALLINT';
    }

    /**
     * {@inheritDoc}
     */
    public function getIntegerTypeDeclarationSQL(array $column)
    {
        return 'INTEGER' . $this->_getCommonIntegerTypeDeclarationSQL($column);
    }

    /**
     * {@inheritDoc}
     */
    public function getBigIntTypeDeclarationSQL(array $column)
    {
        return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($column);
    }

    /**
     * {@inheritDoc}
     */
    public function getSmallIntTypeDeclarationSQL(array $column)
    {
        return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($column);
    }

    /**
     * {@inheritDoc}
     */
    protected function _getCommonIntegerTypeDeclarationSQL(array $column)
    {
        $autoinc = '';
        if (! empty($column['autoincrement'])) {
            $autoinc = ' GENERATED BY DEFAULT AS IDENTITY';
        }

        return $autoinc;
    }

    /**
     * {@inheritdoc}
     */
    public function getBitAndComparisonExpression($value1, $value2)
    {
        return 'BITAND(' . $value1 . ', ' . $value2 . ')';
    }

    /**
     * {@inheritdoc}
     */
    public function getBitOrComparisonExpression($value1, $value2)
    {
        return 'BITOR(' . $value1 . ', ' . $value2 . ')';
    }

    /**
     * {@inheritdoc}
     */
    protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit)
    {
        switch ($unit) {
            case DateIntervalUnit::WEEK:
                $interval *= 7;
                $unit      = DateIntervalUnit::DAY;
                break;

            case DateIntervalUnit::QUARTER:
                $interval *= 3;
                $unit      = DateIntervalUnit::MONTH;
                break;
        }

        return $date . ' ' . $operator . ' ' . $interval . ' ' . $unit;
    }

    /**
     * {@inheritdoc}
     */
    public function getDateDiffExpression($date1, $date2)
    {
        return 'DAYS(' . $date1 . ') - DAYS(' . $date2 . ')';
    }

    /**
     * {@inheritDoc}
     */
    public function getDateTimeTypeDeclarationSQL(array $column)
    {
        if (isset($column['version']) && $column['version'] === true) {
            return 'TIMESTAMP(0) WITH DEFAULT';
        }

        return 'TIMESTAMP(0)';
    }

    /**
     * {@inheritDoc}
     */
    public function getDateTypeDeclarationSQL(array $column)
    {
        return 'DATE';
    }

    /**
     * {@inheritDoc}
     */
    public function getTimeTypeDeclarationSQL(array $column)
    {
        return 'TIME';
    }

    /**
     * {@inheritdoc}
     */
    public function getTruncateTableSQL($tableName, $cascade = false)
    {
        $tableIdentifier = new Identifier($tableName);

        return 'TRUNCATE ' . $tableIdentifier->getQuotedName($this) . ' IMMEDIATE';
    }

    /**
     * This code fragment is originally from the Zend_Db_Adapter_Db2 class, but has been edited.
     *
     * @param string $table
     * @param string $database
     *
     * @return string
     */
    public function getListTableColumnsSQL($table, $database = null)
    {
        $table = $this->quoteStringLiteral($table);

        // We do the funky subquery and join syscat.columns.default this crazy way because
        // as of db2 v10, the column is CLOB(64k) and the distinct operator won't allow a CLOB,
        // it wants shorter stuff like a varchar.
        return "
        SELECT
          cols.default,
          subq.*
        FROM (
               SELECT DISTINCT
                 c.tabschema,
                 c.tabname,
                 c.colname,
                 c.colno,
                 c.typename,
                 c.nulls,
                 c.length,
                 c.scale,
                 c.identity,
                 tc.type AS tabconsttype,
                 c.remarks AS comment,
                 k.colseq,
                 CASE
                 WHEN c.generated = 'D' THEN 1
                 ELSE 0
                 END     AS autoincrement
               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
             ) subq
          JOIN syscat.columns cols
            ON subq.tabschema = cols.tabschema
               AND subq.tabname = cols.tabname
               AND subq.colno = cols.colno
        ORDER BY subq.colno
        ';
    }

    /**
     * {@inheritDoc}
     */
    public function getListTablesSQL()
    {
        return "SELECT NAME FROM SYSIBM.SYSTABLES WHERE TYPE = 'T'";
    }

    /**
     * {@inheritDoc}
     */
    public function getListViewsSQL($database)
    {
        return 'SELECT NAME, TEXT FROM SYSIBM.SYSVIEWS';
    }

    /**
     * {@inheritDoc}
     */
    public function getListTableIndexesSQL($table, $database = null)
    {
        $table = $this->quoteStringLiteral($table);

        return "SELECT   idx.INDNAME AS key_name,
                         idxcol.COLNAME AS column_name,
                         CASE
                             WHEN idx.UNIQUERULE = 'P' THEN 1
                             ELSE 0
                         END AS primary,
                         CASE
                             WHEN idx.UNIQUERULE = 'D' THEN 1
                             ELSE 0
                         END AS non_unique
                FROM     SYSCAT.INDEXES AS idx
                JOIN     SYSCAT.INDEXCOLUSE AS idxcol
                ON       idx.INDSCHEMA = idxcol.INDSCHEMA AND idx.INDNAME = idxcol.INDNAME
                WHERE    idx.TABNAME = UPPER(" . $table . ')
                ORDER BY idxcol.COLSEQ ASC';
    }

    /**
     * {@inheritDoc}
     */
    public function getListTableForeignKeysSQL($table)
    {
        $table = $this->quoteStringLiteral($table);

        return "SELECT   fkcol.COLNAME AS local_column,
                         fk.REFTABNAME AS foreign_table,
                         pkcol.COLNAME AS foreign_column,
                         fk.CONSTNAME AS index_name,
                         CASE
                             WHEN fk.UPDATERULE = 'R' THEN 'RESTRICT'
                             ELSE NULL
                         END AS on_update,
                         CASE
                             WHEN fk.DELETERULE = 'C' THEN 'CASCADE'
                             WHEN fk.DELETERULE = 'N' THEN 'SET NULL'
                             WHEN fk.DELETERULE = 'R' THEN 'RESTRICT'
                             ELSE NULL
                         END AS on_delete
                FROM     SYSCAT.REFERENCES AS fk
                JOIN     SYSCAT.KEYCOLUSE AS fkcol
                ON       fk.CONSTNAME = fkcol.CONSTNAME
                AND      fk.TABSCHEMA = fkcol.TABSCHEMA
                AND      fk.TABNAME = fkcol.TABNAME
                JOIN     SYSCAT.KEYCOLUSE AS pkcol
                ON       fk.REFKEYNAME = pkcol.CONSTNAME
                AND      fk.REFTABSCHEMA = pkcol.TABSCHEMA
                AND      fk.REFTABNAME = pkcol.TABNAME
                WHERE    fk.TABNAME = UPPER(" . $table . ')
                ORDER BY fkcol.COLSEQ ASC';
    }

    /**
     * {@inheritDoc}
     */
    public function getCreateViewSQL($name, $sql)
    {
        return 'CREATE VIEW ' . $name . ' AS ' . $sql;
    }

    /**
     * {@inheritDoc}
     */
    public function getDropViewSQL($name)
    {
        return 'DROP VIEW ' . $name;
    }

    /**
     * {@inheritDoc}
     */
    public function getCreateDatabaseSQL($name)
    {
        return 'CREATE DATABASE ' . $name;
    }

    /**
     * {@inheritDoc}
     */
    public function getDropDatabaseSQL($name)
    {
        return 'DROP DATABASE ' . $name;
    }

    /**
     * {@inheritDoc}
     */
    public function supportsCreateDropDatabase()
    {
        return false;
    }

    /**
     * {@inheritDoc}
     */
    public function supportsReleaseSavepoints()
    {
        return false;
    }

    /**
     * {@inheritdoc}
     */
    public function supportsCommentOnStatement()
    {
        return true;
    }

    /**
     * {@inheritDoc}
     */
    public function getCurrentDateSQL()
    {
        return 'CURRENT DATE';
    }

    /**
     * {@inheritDoc}
     */
    public function getCurrentTimeSQL()
    {
        return 'CURRENT TIME';
    }

    /**
     * {@inheritDoc}
     */
    public function getCurrentTimestampSQL()
    {
        return 'CURRENT TIMESTAMP';
    }

    /**
     * {@inheritDoc}
     */
    public function getIndexDeclarationSQL($name, Index $index)
    {
        // Index declaration in statements like CREATE TABLE is not supported.
        throw Exception::notSupported(__METHOD__);
    }

    /**
     * {@inheritDoc}
     */
    protected function _getCreateTableSQL($name, array $columns, array $options = [])
    {
        $indexes = [];
        if (isset($options['indexes'])) {
            $indexes = $options['indexes'];
        }

        $options['indexes'] = [];

        $sqls = parent::_getCreateTableSQL($name, $columns, $options);

        foreach ($indexes as $definition) {
            $sqls[] = $this->getCreateIndexSQL($definition, $name);
        }

        return $sqls;
    }

    /**
     * {@inheritDoc}
     */
    public function getAlterTableSQL(TableDiff $diff)
    {
        $sql         = [];
        $columnSql   = [];
        $commentsSQL = [];

        $queryParts = [];
        foreach ($diff->addedColumns as $column) {
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
                continue;
            }

            $columnDef = $column->toArray();
            $queryPart = 'ADD COLUMN ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);

            // Adding non-nullable columns to a table requires a default value to be specified.
            if (
                ! empty($columnDef['notnull']) &&
                ! isset($columnDef['default']) &&
                empty($columnDef['autoincrement'])
            ) {
                $queryPart .= ' WITH DEFAULT';
            }

            $queryParts[] = $queryPart;

            $comment = $this->getColumnComment($column);

            if ($comment === null || $comment === '') {
                continue;
            }

            $commentsSQL[] = $this->getCommentOnColumnSQL(
                $diff->getName($this)->getQuotedName($this),
                $column->getQuotedName($this),
                $comment
            );
        }

        foreach ($diff->removedColumns as $column) {
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
                continue;
            }

            $queryParts[] =  'DROP COLUMN ' . $column->getQuotedName($this);
        }

        foreach ($diff->changedColumns as $columnDiff) {
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
                continue;
            }

            if ($columnDiff->hasChanged('comment')) {
                $commentsSQL[] = $this->getCommentOnColumnSQL(
                    $diff->getName($this)->getQuotedName($this),
                    $columnDiff->column->getQuotedName($this),
                    $this->getColumnComment($columnDiff->column)
                );

                if (count($columnDiff->changedProperties) === 1) {
                    continue;
                }
            }

            $this->gatherAlterColumnSQL($diff->getName($this), $columnDiff, $sql, $queryParts);
        }

        foreach ($diff->renamedColumns as $oldColumnName => $column) {
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
                continue;
            }

            $oldColumnName = new Identifier($oldColumnName);

            $queryParts[] =  'RENAME COLUMN ' . $oldColumnName->getQuotedName($this) .
                ' TO ' . $column->getQuotedName($this);
        }

        $tableSql = [];

        if (! $this->onSchemaAlterTable($diff, $tableSql)) {
            if (count($queryParts) > 0) {
                $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . implode(' ', $queryParts);
            }

            // Some table alteration operations require a table reorganization.
            if (! empty($diff->removedColumns) || ! empty($diff->changedColumns)) {
                $sql[] = "CALL SYSPROC.ADMIN_CMD ('REORG TABLE " . $diff->getName($this)->getQuotedName($this) . "')";
            }

            $sql = array_merge($sql, $commentsSQL);

            $newName = $diff->getNewName();

            if ($newName !== false) {
                $sql[] = sprintf(
                    'RENAME TABLE %s TO %s',
                    $diff->getName($this)->getQuotedName($this),
                    $newName->getQuotedName($this)
                );
            }

            $sql = array_merge(
                $this->getPreAlterTableIndexForeignKeySQL($diff),
                $sql,
                $this->getPostAlterTableIndexForeignKeySQL($diff)
            );
        }

        return array_merge($sql, $tableSql, $columnSql);
    }

    /**
     * Gathers the table alteration SQL for a given column diff.
     *
     * @param Identifier $table      The table to gather the SQL for.
     * @param ColumnDiff $columnDiff The column diff to evaluate.
     * @param string[]   $sql        The sequence of table alteration statements to fill.
     * @param mixed[]    $queryParts The sequence of column alteration clauses to fill.
     */
    private function gatherAlterColumnSQL(
        Identifier $table,
        ColumnDiff $columnDiff,
        array &$sql,
        array &$queryParts
    ): void {
        $alterColumnClauses = $this->getAlterColumnClausesSQL($columnDiff);

        if (empty($alterColumnClauses)) {
            return;
        }

        // If we have a single column alteration, we can append the clause to the main query.
        if (count($alterColumnClauses) === 1) {
            $queryParts[] = current($alterColumnClauses);

            return;
        }

        // We have multiple alterations for the same column,
        // so we need to trigger a complete ALTER TABLE statement
        // for each ALTER COLUMN clause.
        foreach ($alterColumnClauses as $alterColumnClause) {
            $sql[] = 'ALTER TABLE ' . $table->getQuotedName($this) . ' ' . $alterColumnClause;
        }
    }

    /**
     * Returns the ALTER COLUMN SQL clauses for altering a column described by the given column diff.
     *
     * @param ColumnDiff $columnDiff The column diff to evaluate.
     *
     * @return string[]
     */
    private function getAlterColumnClausesSQL(ColumnDiff $columnDiff)
    {
        $column = $columnDiff->column->toArray();

        $alterClause = 'ALTER COLUMN ' . $columnDiff->column->getQuotedName($this);

        if ($column['columnDefinition']) {
            return [$alterClause . ' ' . $column['columnDefinition']];
        }

        $clauses = [];

        if (
            $columnDiff->hasChanged('type') ||
            $columnDiff->hasChanged('length') ||
            $columnDiff->hasChanged('precision') ||
            $columnDiff->hasChanged('scale') ||
            $columnDiff->hasChanged('fixed')
        ) {
            $clauses[] = $alterClause . ' SET DATA TYPE ' . $column['type']->getSQLDeclaration($column, $this);
        }

        if ($columnDiff->hasChanged('notnull')) {
            $clauses[] = $column['notnull'] ? $alterClause . ' SET NOT NULL' : $alterClause . ' DROP NOT NULL';
        }

        if ($columnDiff->hasChanged('default')) {
            if (isset($column['default'])) {
                $defaultClause = $this->getDefaultValueDeclarationSQL($column);

                if ($defaultClause) {
                    $clauses[] = $alterClause . ' SET' . $defaultClause;
                }
            } else {
                $clauses[] = $alterClause . ' DROP DEFAULT';
            }
        }

        return $clauses;
    }

    /**
     * {@inheritDoc}
     */
    protected function getPreAlterTableIndexForeignKeySQL(TableDiff $diff)
    {
        $sql   = [];
        $table = $diff->getName($this)->getQuotedName($this);

        foreach ($diff->removedIndexes as $remKey => $remIndex) {
            foreach ($diff->addedIndexes as $addKey => $addIndex) {
                if ($remIndex->getColumns() !== $addIndex->getColumns()) {
                    continue;
                }

                if ($remIndex->isPrimary()) {
                    $sql[] = 'ALTER TABLE ' . $table . ' DROP PRIMARY KEY';
                } elseif ($remIndex->isUnique()) {
                    $sql[] = 'ALTER TABLE ' . $table . ' DROP UNIQUE ' . $remIndex->getQuotedName($this);
                } else {
                    $sql[] = $this->getDropIndexSQL($remIndex, $table);
                }

                $sql[] = $this->getCreateIndexSQL($addIndex, $table);

                unset($diff->removedIndexes[$remKey], $diff->addedIndexes[$addKey]);

                break;
            }
        }

        $sql = array_merge($sql, parent::getPreAlterTableIndexForeignKeySQL($diff));

        return $sql;
    }

    /**
     * {@inheritdoc}
     */
    protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName)
    {
        if (strpos($tableName, '.') !== false) {
            [$schema]     = explode('.', $tableName);
            $oldIndexName = $schema . '.' . $oldIndexName;
        }

        return ['RENAME INDEX ' . $oldIndexName . ' TO ' . $index->getQuotedName($this)];
    }

    /**
     * {@inheritDoc}
     */
    public function getDefaultValueDeclarationSQL($column)
    {
        if (! empty($column['autoincrement'])) {
            return '';
        }

        if (isset($column['version']) && $column['version']) {
            if ((string) $column['type'] !== 'DateTime') {
                $column['default'] = '1';
            }
        }

        return parent::getDefaultValueDeclarationSQL($column);
    }

    /**
     * {@inheritDoc}
     */
    public function getEmptyIdentityInsertSQL($quotedTableName, $quotedIdentifierColumnName)
    {
        return 'INSERT INTO ' . $quotedTableName . ' (' . $quotedIdentifierColumnName . ') VALUES (DEFAULT)';
    }

    /**
     * {@inheritDoc}
     */
    public function getCreateTemporaryTableSnippetSQL()
    {
        return 'DECLARE GLOBAL TEMPORARY TABLE';
    }

    /**
     * {@inheritDoc}
     */
    public function getTemporaryTableName($tableName)
    {
        return 'SESSION.' . $tableName;
    }

    /**
     * {@inheritDoc}
     */
    protected function doModifyLimitQuery($query, $limit, $offset = null)
    {
        $where = [];

        if ($offset > 0) {
            $where[] = sprintf('db22.DC_ROWNUM >= %d', $offset + 1);
        }

        if ($limit !== null) {
            $where[] = sprintf('db22.DC_ROWNUM <= %d', $offset + $limit);
        }

        if (empty($where)) {
            return $query;
        }

        // Todo OVER() needs ORDER BY data!
        return sprintf(
            'SELECT db22.* FROM (SELECT db21.*, ROW_NUMBER() OVER() AS DC_ROWNUM FROM (%s) db21) db22 WHERE %s',
            $query,
            implode(' AND ', $where)
        );
    }

    /**
     * {@inheritDoc}
     */
    public function getLocateExpression($str, $substr, $startPos = false)
    {
        if ($startPos === false) {
            return 'LOCATE(' . $substr . ', ' . $str . ')';
        }

        return 'LOCATE(' . $substr . ', ' . $str . ', ' . $startPos . ')';
    }

    /**
     * {@inheritDoc}
     */
    public function getSubstringExpression($string, $start, $length = null)
    {
        if ($length === null) {
            return 'SUBSTR(' . $string . ', ' . $start . ')';
        }

        return 'SUBSTR(' . $string . ', ' . $start . ', ' . $length . ')';
    }

    /**
     * {@inheritDoc}
     */
    public function supportsIdentityColumns()
    {
        return true;
    }

    /**
     * {@inheritDoc}
     */
    public function prefersIdentityColumns()
    {
        return true;
    }

    /**
     * {@inheritDoc}
     *
     * DB2 returns all column names in SQL result sets in uppercase.
     *
     * @deprecated
     */
    public function getSQLResultCasing($column)
    {
        return strtoupper($column);
    }

    /**
     * {@inheritDoc}
     */
    public function getForUpdateSQL()
    {
        return ' WITH RR USE AND KEEP UPDATE LOCKS';
    }

    /**
     * {@inheritDoc}
     */
    public function getDummySelectSQL()
    {
        $expression = func_num_args() > 0 ? func_get_arg(0) : '1';

        return sprintf('SELECT %s FROM sysibm.sysdummy1', $expression);
    }

    /**
     * {@inheritDoc}
     *
     * 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.
     */
    public function supportsSavepoints()
    {
        return false;
    }

    /**
     * {@inheritDoc}
     */
    protected function getReservedKeywordsClass()
    {
        return Keywords\DB2Keywords::class;
    }

    public function getListTableCommentsSQL(string $table): string
    {
        return sprintf(
            <<<'SQL'
SELECT REMARKS
  FROM SYSIBM.SYSTABLES
  WHERE NAME = UPPER( %s )
SQL
            ,
            $this->quoteStringLiteral($table)
        );
    }
}
<?php

namespace Doctrine\DBAL\Platforms;

use Doctrine\DBAL\Types\Types;

/**
 * Provides the behavior, features and SQL dialect of the PostgreSQL 9.4 database platform.
 */
class PostgreSQL94Platform extends PostgreSQL92Platform
{
    /**
     * {@inheritdoc}
     */
    public function getJsonTypeDeclarationSQL(array $column)
    {
        if (! empty($column['jsonb'])) {
            return 'JSONB';
        }

        return 'JSON';
    }

    /**
     * {@inheritdoc}
     */
    protected function getReservedKeywordsClass()
    {
        return Keywords\PostgreSQL94Keywords::class;
    }

    /**
     * {@inheritdoc}
     */
    protected function initializeDoctrineTypeMappings()
    {
        parent::initializeDoctrineTypeMappings();

        $this->doctrineTypeMapping['jsonb'] = Types::JSON;
    }
}
<?php

namespace Doctrine\DBAL\Platforms;

use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
use Doctrine\DBAL\Schema\Identifier;
use Doctrine\DBAL\Schema\Index;
use Doctrine\DBAL\Schema\Sequence;
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Schema\TableDiff;
use Doctrine\DBAL\TransactionIsolationLevel;
use Doctrine\DBAL\Types\BinaryType;
use Doctrine\Deprecations\Deprecation;
use InvalidArgumentException;

use function array_merge;
use function count;
use function explode;
use function func_get_arg;
use function func_num_args;
use function implode;
use function preg_match;
use function sprintf;
use function strlen;
use function strpos;
use function strtoupper;
use function substr;

/**
 * OraclePlatform.
 */
class OraclePlatform extends AbstractPlatform
{
    /**
     * Assertion for Oracle identifiers.
     *
     * @link http://docs.oracle.com/cd/B19306_01/server.102/b14200/sql_elements008.htm
     *
     * @param string $identifier
     *
     * @return void
     *
     * @throws Exception
     */
    public static function assertValidIdentifier($identifier)
    {
        if (! preg_match('(^(([a-zA-Z]{1}[a-zA-Z0-9_$#]{0,})|("[^"]+"))$)', $identifier)) {
            throw new Exception('Invalid Oracle identifier');
        }
    }

    /**
     * {@inheritDoc}
     */
    public function getSubstringExpression($string, $start, $length = null)
    {
        if ($length !== null) {
            return sprintf('SUBSTR(%s, %d, %d)', $string, $start, $length);
        }

        return sprintf('SUBSTR(%s, %d)', $string, $start);
    }

    /**
     * @param string $type
     *
     * @return string
     */
    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\')';
        }
    }

    /**
     * {@inheritDoc}
     */
    public function getLocateExpression($str, $substr, $startPos = false)
    {
        if ($startPos === false) {
            return 'INSTR(' . $str . ', ' . $substr . ')';
        }

        return 'INSTR(' . $str . ', ' . $substr . ', ' . $startPos . ')';
    }

    /**
     * {@inheritDoc}
     *
     * @deprecated Use application-generated UUIDs instead
     */
    public function getGuidExpression()
    {
        return 'SYS_GUID()';
    }

    /**
     * {@inheritdoc}
     */
    protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit)
    {
        switch ($unit) {
            case DateIntervalUnit::MONTH:
            case DateIntervalUnit::QUARTER:
            case DateIntervalUnit::YEAR:
                switch ($unit) {
                    case DateIntervalUnit::QUARTER:
                        $interval *= 3;
                        break;

                    case DateIntervalUnit::YEAR:
                        $interval *= 12;
                        break;
                }

                return 'ADD_MONTHS(' . $date . ', ' . $operator . $interval . ')';

            default:
                $calculationClause = '';

                switch ($unit) {
                    case DateIntervalUnit::SECOND:
                        $calculationClause = '/24/60/60';
                        break;

                    case DateIntervalUnit::MINUTE:
                        $calculationClause = '/24/60';
                        break;

                    case DateIntervalUnit::HOUR:
                        $calculationClause = '/24';
                        break;

                    case DateIntervalUnit::WEEK:
                        $calculationClause = '*7';
                        break;
                }

                return '(' . $date . $operator . $interval . $calculationClause . ')';
        }
    }

    /**
     * {@inheritDoc}
     */
    public function getDateDiffExpression($date1, $date2)
    {
        return sprintf('TRUNC(%s) - TRUNC(%s)', $date1, $date2);
    }

    /**
     * {@inheritDoc}
     */
    public function getBitAndComparisonExpression($value1, $value2)
    {
        return 'BITAND(' . $value1 . ', ' . $value2 . ')';
    }

    /**
     * {@inheritDoc}
     */
    public function getBitOrComparisonExpression($value1, $value2)
    {
        return '(' . $value1 . '-' .
                $this->getBitAndComparisonExpression($value1, $value2)
                . '+' . $value2 . ')';
    }

    /**
     * {@inheritDoc}
     *
     * 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()}
     */
    public function getCreateSequenceSQL(Sequence $sequence)
    {
        return 'CREATE SEQUENCE ' . $sequence->getQuotedName($this) .
               ' START WITH ' . $sequence->getInitialValue() .
               ' MINVALUE ' . $sequence->getInitialValue() .
               ' INCREMENT BY ' . $sequence->getAllocationSize() .
               $this->getSequenceCacheSQL($sequence);
    }

    /**
     * {@inheritDoc}
     */
    public function getAlterSequenceSQL(Sequence $sequence)
    {
        return 'ALTER SEQUENCE ' . $sequence->getQuotedName($this) .
               ' INCREMENT BY ' . $sequence->getAllocationSize()
               . $this->getSequenceCacheSQL($sequence);
    }

    /**
     * Cache definition for sequences
     *
     * @return string
     */
    private function getSequenceCacheSQL(Sequence $sequence)
    {
        if ($sequence->getCache() === 0) {
            return ' NOCACHE';
        }

        if ($sequence->getCache() === 1) {
            return ' NOCACHE';
        }

        if ($sequence->getCache() > 1) {
            return ' CACHE ' . $sequence->getCache();
        }

        return '';
    }

    /**
     * {@inheritDoc}
     */
    public function getSequenceNextValSQL($sequence)
    {
        return 'SELECT ' . $sequence . '.nextval FROM DUAL';
    }

    /**
     * {@inheritDoc}
     */
    public function getSetTransactionIsolationSQL($level)
    {
        return 'SET TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level);
    }

    /**
     * {@inheritDoc}
     */
    protected function _getTransactionIsolationLevelSQL($level)
    {
        switch ($level) {
            case TransactionIsolationLevel::READ_UNCOMMITTED:
                return 'READ UNCOMMITTED';

            case TransactionIsolationLevel::READ_COMMITTED:
                return 'READ COMMITTED';

            case TransactionIsolationLevel::REPEATABLE_READ:
            case TransactionIsolationLevel::SERIALIZABLE:
                return 'SERIALIZABLE';

            default:
                return parent::_getTransactionIsolationLevelSQL($level);
        }
    }

    /**
     * {@inheritDoc}
     */
    public function getBooleanTypeDeclarationSQL(array $column)
    {
        return 'NUMBER(1)';
    }

    /**
     * {@inheritDoc}
     */
    public function getIntegerTypeDeclarationSQL(array $column)
    {
        return 'NUMBER(10)';
    }

    /**
     * {@inheritDoc}
     */
    public function getBigIntTypeDeclarationSQL(array $column)
    {
        return 'NUMBER(20)';
    }

    /**
     * {@inheritDoc}
     */
    public function getSmallIntTypeDeclarationSQL(array $column)
    {
        return 'NUMBER(5)';
    }

    /**
     * {@inheritDoc}
     */
    public function getDateTimeTypeDeclarationSQL(array $column)
    {
        return 'TIMESTAMP(0)';
    }

    /**
     * {@inheritDoc}
     */
    public function getDateTimeTzTypeDeclarationSQL(array $column)
    {
        return 'TIMESTAMP(0) WITH TIME ZONE';
    }

    /**
     * {@inheritDoc}
     */
    public function getDateTypeDeclarationSQL(array $column)
    {
        return 'DATE';
    }

    /**
     * {@inheritDoc}
     */
    public function getTimeTypeDeclarationSQL(array $column)
    {
        return 'DATE';
    }

    /**
     * {@inheritDoc}
     */
    protected function _getCommonIntegerTypeDeclarationSQL(array $column)
    {
        return '';
    }

    /**
     * {@inheritDoc}
     */
    protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed)
    {
        return $fixed ? ($length ? 'CHAR(' . $length . ')' : 'CHAR(2000)')
                : ($length ? 'VARCHAR2(' . $length . ')' : 'VARCHAR2(4000)');
    }

    /**
     * {@inheritdoc}
     */
    protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed)
    {
        return 'RAW(' . ($length ?: $this->getBinaryMaxLength()) . ')';
    }

    /**
     * {@inheritdoc}
     */
    public function getBinaryMaxLength()
    {
        return 2000;
    }

    /**
     * {@inheritDoc}
     */
    public function getClobTypeDeclarationSQL(array $column)
    {
        return 'CLOB';
    }

    /**
     * {@inheritDoc}
     */
    public function getListDatabasesSQL()
    {
        return 'SELECT username FROM all_users';
    }

    /**
     * {@inheritDoc}
     */
    public function getListSequencesSQL($database)
    {
        $database = $this->normalizeIdentifier($database);
        $database = $this->quoteStringLiteral($database->getName());

        return 'SELECT sequence_name, min_value, increment_by FROM sys.all_sequences ' .
               'WHERE SEQUENCE_OWNER = ' . $database;
    }

    /**
     * {@inheritDoc}
     */
    protected function _getCreateTableSQL($name, array $columns, array $options = [])
    {
        $indexes            = $options['indexes'] ?? [];
        $options['indexes'] = [];
        $sql                = parent::_getCreateTableSQL($name, $columns, $options);

        foreach ($columns as $columnName => $column) {
            if (isset($column['sequence'])) {
                $sql[] = $this->getCreateSequenceSQL($column['sequence']);
            }

            if (
                ! isset($column['autoincrement']) || ! $column['autoincrement'] &&
                (! isset($column['autoinc']) || ! $column['autoinc'])
            ) {
                continue;
            }

            $sql = array_merge($sql, $this->getCreateAutoincrementSql($columnName, $name));
        }

        if (isset($indexes) && ! empty($indexes)) {
            foreach ($indexes as $index) {
                $sql[] = $this->getCreateIndexSQL($index, $name);
            }
        }

        return $sql;
    }

    /**
     * {@inheritDoc}
     *
     * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaOracleReader.html
     */
    public function getListTableIndexesSQL($table, $database = null)
    {
        $table = $this->normalizeIdentifier($table);
        $table = $this->quoteStringLiteral($table->getName());

        return "SELECT uind_col.index_name AS name,
                       (
                           SELECT uind.index_type
                           FROM   user_indexes uind
                           WHERE  uind.index_name = uind_col.index_name
                       ) AS type,
                       decode(
                           (
                               SELECT uind.uniqueness
                               FROM   user_indexes uind
                               WHERE  uind.index_name = uind_col.index_name
                           ),
                           '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.index_name = uind_col.index_name
                       ) AS is_primary
             FROM      user_ind_columns uind_col
             WHERE     uind_col.table_name = " . $table . '
             ORDER BY  uind_col.column_position ASC';
    }

    /**
     * {@inheritDoc}
     */
    public function getListTablesSQL()
    {
        return 'SELECT * FROM sys.user_tables';
    }

    /**
     * {@inheritDoc}
     */
    public function getListViewsSQL($database)
    {
        return 'SELECT view_name, text FROM sys.user_views';
    }

    /**
     * {@inheritDoc}
     */
    public function getCreateViewSQL($name, $sql)
    {
        return 'CREATE VIEW ' . $name . ' AS ' . $sql;
    }

    /**
     * {@inheritDoc}
     */
    public function getDropViewSQL($name)
    {
        return 'DROP VIEW ' . $name;
    }

    /**
     * @param string $name
     * @param string $table
     * @param int    $start
     *
     * @return string[]
     */
    public function getCreateAutoincrementSql($name, $table, $start = 1)
    {
        $tableIdentifier   = $this->normalizeIdentifier($table);
        $quotedTableName   = $tableIdentifier->getQuotedName($this);
        $unquotedTableName = $tableIdentifier->getName();

        $nameIdentifier = $this->normalizeIdentifier($name);
        $quotedName     = $nameIdentifier->getQuotedName($this);
        $unquotedName   = $nameIdentifier->getName();

        $sql = [];

        $autoincrementIdentifierName = $this->getAutoincrementIdentifierName($tableIdentifier);

        $idx = new Index($autoincrementIdentifierName, [$quotedName], true, true);

        $sql[] = "DECLARE
  constraints_Count NUMBER;
BEGIN
  SELECT COUNT(CONSTRAINT_NAME) INTO constraints_Count
    FROM USER_CONSTRAINTS
   WHERE TABLE_NAME = '" . $unquotedTableName . "'
     AND CONSTRAINT_TYPE = 'P';
  IF constraints_Count = 0 OR constraints_Count = '' THEN
    EXECUTE IMMEDIATE '" . $this->getCreateConstraintSQL($idx, $quotedTableName) . "';
  END IF;
END;";

        $sequenceName = $this->getIdentitySequenceName(
            $tableIdentifier->isQuoted() ? $quotedTableName : $unquotedTableName,
            $nameIdentifier->isQuoted() ? $quotedName : $unquotedName
        );
        $sequence     = new Sequence($sequenceName, $start);
        $sql[]        = $this->getCreateSequenceSQL($sequence);

        $sql[] = 'CREATE TRIGGER ' . $autoincrementIdentifierName . '
   BEFORE INSERT
   ON ' . $quotedTableName . '
   FOR EACH ROW
DECLARE
   last_Sequence NUMBER;
   last_InsertID NUMBER;
BEGIN
   SELECT ' . $sequenceName . '.NEXTVAL INTO :NEW.' . $quotedName . ' FROM DUAL;
   IF (:NEW.' . $quotedName . ' IS NULL OR :NEW.' . $quotedName . ' = 0) THEN
      SELECT ' . $sequenceName . '.NEXTVAL INTO :NEW.' . $quotedName . ' FROM DUAL;
   ELSE
      SELECT NVL(Last_Number, 0) INTO last_Sequence
        FROM User_Sequences
       WHERE Sequence_Name = \'' . $sequence->getName() . '\';
      SELECT :NEW.' . $quotedName . ' 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;
    }

    /**
     * Returns the SQL statements to drop the autoincrement for the given table name.
     *
     * @param string $table The table name to drop the autoincrement for.
     *
     * @return string[]
     */
    public function getDropAutoincrementSql($table)
    {
        $table                       = $this->normalizeIdentifier($table);
        $autoincrementIdentifierName = $this->getAutoincrementIdentifierName($table);
        $identitySequenceName        = $this->getIdentitySequenceName(
            $table->isQuoted() ? $table->getQuotedName($this) : $table->getName(),
            ''
        );

        return [
            'DROP TRIGGER ' . $autoincrementIdentifierName,
            $this->getDropSequenceSQL($identitySequenceName),
            $this->getDropConstraintSQL($autoincrementIdentifierName, $table->getQuotedName($this)),
        ];
    }

    /**
     * Normalizes the given identifier.
     *
     * Uppercases the given identifier if it is not quoted by intention
     * to reflect Oracle's internal auto uppercasing strategy of unquoted identifiers.
     *
     * @param string $name The identifier to normalize.
     *
     * @return Identifier The normalized identifier.
     */
    private function normalizeIdentifier($name)
    {
        $identifier = new Identifier($name);

        return $identifier->isQuoted() ? $identifier : new Identifier(strtoupper($name));
    }

    /**
     * Adds suffix to identifier,
     *
     * if the new string exceeds max identifier length,
     * keeps $suffix, cuts from $identifier as much as the part exceeding.
     */
    private function addSuffix(string $identifier, string $suffix): string
    {
        $maxPossibleLengthWithoutSuffix = $this->getMaxIdentifierLength() - strlen($suffix);
        if (strlen($identifier) > $maxPossibleLengthWithoutSuffix) {
            $identifier = substr($identifier, 0, $maxPossibleLengthWithoutSuffix);
        }

        return $identifier . $suffix;
    }

    /**
     * Returns the autoincrement primary key identifier name for the given table identifier.
     *
     * Quotes the autoincrement primary key identifier name
     * if the given table name is quoted by intention.
     *
     * @param Identifier $table The table identifier to return the autoincrement primary key identifier name for.
     *
     * @return string
     */
    private function getAutoincrementIdentifierName(Identifier $table)
    {
        $identifierName = $this->addSuffix($table->getName(), '_AI_PK');

        return $table->isQuoted()
            ? $this->quoteSingleIdentifier($identifierName)
            : $identifierName;
    }

    /**
     * {@inheritDoc}
     */
    public function getListTableForeignKeysSQL($table)
    {
        $table = $this->normalizeIdentifier($table);
        $table = $this->quoteStringLiteral($table->getName());

        return "SELECT alc.constraint_name,
          alc.DELETE_RULE,
          cols.column_name \"local_column\",
          cols.position,
          (
              SELECT r_cols.table_name
              FROM   user_cons_columns r_cols
              WHERE  alc.r_constraint_name = r_cols.constraint_name
              AND    r_cols.position = cols.position
          ) AS \"references_table\",
          (
              SELECT r_cols.column_name
              FROM   user_cons_columns r_cols
              WHERE  alc.r_constraint_name = r_cols.constraint_name
              AND    r_cols.position = cols.position
          ) AS \"foreign_column\"
     FROM user_cons_columns cols
     JOIN user_constraints alc
       ON alc.constraint_name = cols.constraint_name
      AND alc.constraint_type = 'R'
      AND alc.table_name = " . $table . '
    ORDER BY cols.constraint_name ASC, cols.position ASC';
    }

    /**
     * {@inheritDoc}
     */
    public function getListTableConstraintsSQL($table)
    {
        $table = $this->normalizeIdentifier($table);
        $table = $this->quoteStringLiteral($table->getName());

        return 'SELECT * FROM user_constraints WHERE table_name = ' . $table;
    }

    /**
     * {@inheritDoc}
     */
    public function getListTableColumnsSQL($table, $database = null)
    {
        $table = $this->normalizeIdentifier($table);
        $table = $this->quoteStringLiteral($table->getName());

        $tabColumnsTableName       = 'user_tab_columns';
        $colCommentsTableName      = 'user_col_comments';
        $tabColumnsOwnerCondition  = '';
        $colCommentsOwnerCondition = '';

        if ($database !== null && $database !== '/') {
            $database                  = $this->normalizeIdentifier($database);
            $database                  = $this->quoteStringLiteral($database->getName());
            $tabColumnsTableName       = 'all_tab_columns';
            $colCommentsTableName      = 'all_col_comments';
            $tabColumnsOwnerCondition  = ' AND c.owner = ' . $database;
            $colCommentsOwnerCondition = ' AND d.OWNER = c.OWNER';
        }

        return sprintf(
            <<<'SQL'
SELECT   c.*,
         (
             SELECT d.comments
             FROM   %s d
             WHERE  d.TABLE_NAME = c.TABLE_NAME%s
             AND    d.COLUMN_NAME = c.COLUMN_NAME
         ) AS comments
FROM     %s c
WHERE    c.table_name = %s%s
ORDER BY c.column_id
SQL
            ,
            $colCommentsTableName,
            $colCommentsOwnerCondition,
            $tabColumnsTableName,
            $table,
            $tabColumnsOwnerCondition
        );
    }

    /**
     * {@inheritDoc}
     */
    public function getDropSequenceSQL($sequence)
    {
        if ($sequence instanceof Sequence) {
            $sequence = $sequence->getQuotedName($this);
        }

        return 'DROP SEQUENCE ' . $sequence;
    }

    /**
     * {@inheritDoc}
     */
    public function getDropForeignKeySQL($foreignKey, $table)
    {
        if (! $foreignKey instanceof ForeignKeyConstraint) {
            $foreignKey = new Identifier($foreignKey);
        }

        if (! $table instanceof Table) {
            $table = new Identifier($table);
        }

        $foreignKey = $foreignKey->getQuotedName($this);
        $table      = $table->getQuotedName($this);

        return 'ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $foreignKey;
    }

    /**
     * {@inheritdoc}
     */
    public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey)
    {
        $referentialAction = null;

        if ($foreignKey->hasOption('onDelete')) {
            $referentialAction = $this->getForeignKeyReferentialActionSQL($foreignKey->getOption('onDelete'));
        }

        return $referentialAction ? ' ON DELETE ' . $referentialAction : '';
    }

    /**
     * {@inheritdoc}
     */
    public function getForeignKeyReferentialActionSQL($action)
    {
        $action = strtoupper($action);

        switch ($action) {
            case 'RESTRICT': // RESTRICT is not supported, therefore falling back to NO ACTION.
            case 'NO ACTION':
                // NO ACTION cannot be declared explicitly,
                // therefore returning empty string to indicate to OMIT the referential clause.
                return '';

            case 'CASCADE':
            case 'SET NULL':
                return $action;

            default:
                // SET DEFAULT is not supported, throw exception instead.
                throw new InvalidArgumentException('Invalid foreign key action: ' . $action);
        }
    }

    /**
     * {@inheritDoc}
     */
    public function getDropDatabaseSQL($name)
    {
        return 'DROP USER ' . $name . ' CASCADE';
    }

    /**
     * {@inheritDoc}
     */
    public function getAlterTableSQL(TableDiff $diff)
    {
        $sql         = [];
        $commentsSQL = [];
        $columnSql   = [];

        $fields = [];

        foreach ($diff->addedColumns as $column) {
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
                continue;
            }

            $fields[] = $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray());
            $comment  = $this->getColumnComment($column);

            if (! $comment) {
                continue;
            }

            $commentsSQL[] = $this->getCommentOnColumnSQL(
                $diff->getName($this)->getQuotedName($this),
                $column->getQuotedName($this),
                $comment
            );
        }

        if (count($fields)) {
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this)
                . ' ADD (' . implode(', ', $fields) . ')';
        }

        $fields = [];
        foreach ($diff->changedColumns as $columnDiff) {
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
                continue;
            }

            $column = $columnDiff->column;

            // Do not generate column alteration clause if type is binary and only fixed property has changed.
            // Oracle only supports binary type columns with variable length.
            // Avoids unnecessary table alteration statements.
            if (
                $column->getType() instanceof BinaryType &&
                $columnDiff->hasChanged('fixed') &&
                count($columnDiff->changedProperties) === 1
            ) {
                continue;
            }

            $columnHasChangedComment = $columnDiff->hasChanged('comment');

            /**
             * Do not add query part if only comment has changed
             */
            if (! ($columnHasChangedComment && count($columnDiff->changedProperties) === 1)) {
                $columnInfo = $column->toArray();

                if (! $columnDiff->hasChanged('notnull')) {
                    unset($columnInfo['notnull']);
                }

                $fields[] = $column->getQuotedName($this) . $this->getColumnDeclarationSQL('', $columnInfo);
            }

            if (! $columnHasChangedComment) {
                continue;
            }

            $commentsSQL[] = $this->getCommentOnColumnSQL(
                $diff->getName($this)->getQuotedName($this),
                $column->getQuotedName($this),
                $this->getColumnComment($column)
            );
        }

        if (count($fields)) {
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this)
                . ' MODIFY (' . implode(', ', $fields) . ')';
        }

        foreach ($diff->renamedColumns as $oldColumnName => $column) {
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
                continue;
            }

            $oldColumnName = new Identifier($oldColumnName);

            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) .
                ' RENAME COLUMN ' . $oldColumnName->getQuotedName($this) . ' TO ' . $column->getQuotedName($this);
        }

        $fields = [];
        foreach ($diff->removedColumns as $column) {
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
                continue;
            }

            $fields[] = $column->getQuotedName($this);
        }

        if (count($fields)) {
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this)
                . ' DROP (' . implode(', ', $fields) . ')';
        }

        $tableSql = [];

        if (! $this->onSchemaAlterTable($diff, $tableSql)) {
            $sql = array_merge($sql, $commentsSQL);

            $newName = $diff->getNewName();

            if ($newName !== false) {
                $sql[] = sprintf(
                    'ALTER TABLE %s RENAME TO %s',
                    $diff->getName($this)->getQuotedName($this),
                    $newName->getQuotedName($this)
                );
            }

            $sql = array_merge(
                $this->getPreAlterTableIndexForeignKeySQL($diff),
                $sql,
                $this->getPostAlterTableIndexForeignKeySQL($diff)
            );
        }

        return array_merge($sql, $tableSql, $columnSql);
    }

    /**
     * {@inheritdoc}
     */
    public function getColumnDeclarationSQL($name, array $column)
    {
        if (isset($column['columnDefinition'])) {
            $columnDef = $this->getCustomTypeDeclarationSQL($column);
        } else {
            $default = $this->getDefaultValueDeclarationSQL($column);

            $notnull = '';

            if (isset($column['notnull'])) {
                $notnull = $column['notnull'] ? ' NOT NULL' : ' NULL';
            }

            $unique = isset($column['unique']) && $column['unique'] ?
                ' ' . $this->getUniqueFieldDeclarationSQL() : '';

            $check = isset($column['check']) && $column['check'] ?
                ' ' . $column['check'] : '';

            $typeDecl  = $column['type']->getSQLDeclaration($column, $this);
            $columnDef = $typeDecl . $default . $notnull . $unique . $check;
        }

        return $name . ' ' . $columnDef;
    }

    /**
     * {@inheritdoc}
     */
    protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName)
    {
        if (strpos($tableName, '.') !== false) {
            [$schema]     = explode('.', $tableName);
            $oldIndexName = $schema . '.' . $oldIndexName;
        }

        return ['ALTER INDEX ' . $oldIndexName . ' RENAME TO ' . $index->getQuotedName($this)];
    }

    /**
     * {@inheritDoc}
     *
     * @deprecated
     */
    public function prefersSequences()
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/4229',
            'AbstractPlatform::prefersSequences() is deprecated without replacement and removed in DBAL 3.0'
        );

        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function usesSequenceEmulatedIdentityColumns()
    {
        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function getIdentitySequenceName($tableName, $columnName)
    {
        $table = new Identifier($tableName);

        // No usage of column name to preserve BC compatibility with <2.5
        $identitySequenceName = $this->addSuffix($table->getName(), '_SEQ');

        if ($table->isQuoted()) {
            $identitySequenceName = '"' . $identitySequenceName . '"';
        }

        $identitySequenceIdentifier = $this->normalizeIdentifier($identitySequenceName);

        return $identitySequenceIdentifier->getQuotedName($this);
    }

    /**
     * {@inheritDoc}
     */
    public function supportsCommentOnStatement()
    {
        return true;
    }

    /**
     * {@inheritDoc}
     */
    public function getName()
    {
        return 'oracle';
    }

    /**
     * {@inheritDoc}
     */
    protected function doModifyLimitQuery($query, $limit, $offset = null)
    {
        if ($limit === null && $offset <= 0) {
            return $query;
        }

        if (preg_match('/^\s*SELECT/i', $query)) {
            if (! preg_match('/\sFROM\s/i', $query)) {
                $query .= ' FROM dual';
            }

            $columns = ['a.*'];

            if ($offset > 0) {
                $columns[] = 'ROWNUM AS doctrine_rownum';
            }

            $query = sprintf('SELECT %s FROM (%s) a', implode(', ', $columns), $query);

            if ($limit !== null) {
                $query .= sprintf(' WHERE ROWNUM <= %d', $offset + $limit);
            }

            if ($offset > 0) {
                $query = sprintf('SELECT * FROM (%s) WHERE doctrine_rownum >= %d', $query, $offset + 1);
            }
        }

        return $query;
    }

    /**
     * {@inheritDoc}
     *
     * Oracle returns all column names in SQL result sets in uppercase.
     *
     * @deprecated
     */
    public function getSQLResultCasing($column)
    {
        return strtoupper($column);
    }

    /**
     * {@inheritDoc}
     */
    public function getCreateTemporaryTableSnippetSQL()
    {
        return 'CREATE GLOBAL TEMPORARY TABLE';
    }

    /**
     * {@inheritDoc}
     */
    public function getDateTimeTzFormatString()
    {
        return 'Y-m-d H:i:sP';
    }

    /**
     * {@inheritDoc}
     */
    public function getDateFormatString()
    {
        return 'Y-m-d 00:00:00';
    }

    /**
     * {@inheritDoc}
     */
    public function getTimeFormatString()
    {
        return '1900-01-01 H:i:s';
    }

    /**
     * {@inheritDoc}
     *
     * @deprecated
     */
    public function fixSchemaElementName($schemaElementName)
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/4132',
            'AbstractPlatform::fixSchemaElementName is deprecated with no replacement and removed in DBAL 3.0'
        );

        if (strlen($schemaElementName) > 30) {
            // Trim it
            return substr($schemaElementName, 0, 30);
        }

        return $schemaElementName;
    }

    /**
     * {@inheritDoc}
     */
    public function getMaxIdentifierLength()
    {
        return 30;
    }

    /**
     * {@inheritDoc}
     */
    public function supportsSequences()
    {
        return true;
    }

    /**
     * {@inheritDoc}
     *
     * @deprecated
     */
    public function supportsForeignKeyOnUpdate()
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/4229',
            'AbstractPlatform::supportsForeignKeyOnUpdate() is deprecated without replacement and removed in DBAL 3.0'
        );

        return false;
    }

    /**
     * {@inheritDoc}
     */
    public function supportsReleaseSavepoints()
    {
        return false;
    }

    /**
     * {@inheritDoc}
     */
    public function getTruncateTableSQL($tableName, $cascade = false)
    {
        $tableIdentifier = new Identifier($tableName);

        return 'TRUNCATE TABLE ' . $tableIdentifier->getQuotedName($this);
    }

    /**
     * {@inheritDoc}
     */
    public function getDummySelectSQL()
    {
        $expression = func_num_args() > 0 ? func_get_arg(0) : '1';

        return sprintf('SELECT %s FROM DUAL', $expression);
    }

    /**
     * {@inheritDoc}
     */
    protected function initializeDoctrineTypeMappings()
    {
        $this->doctrineTypeMapping = [
            'integer'           => 'integer',
            'number'            => 'integer',
            'pls_integer'       => 'boolean',
            'binary_integer'    => 'boolean',
            'varchar'           => 'string',
            'varchar2'          => 'string',
            'nvarchar2'         => 'string',
            'char'              => 'string',
            'nchar'             => 'string',
            'date'              => 'date',
            'timestamp'         => 'datetime',
            'timestamptz'       => 'datetimetz',
            'float'             => 'float',
            'binary_float'      => 'float',
            'binary_double'     => 'float',
            'long'              => 'string',
            'clob'              => 'text',
            'nclob'             => 'text',
            'raw'               => 'binary',
            'long raw'          => 'blob',
            'rowid'             => 'string',
            'urowid'            => 'string',
            'blob'              => 'blob',
        ];
    }

    /**
     * {@inheritDoc}
     */
    public function releaseSavePoint($savepoint)
    {
        return '';
    }

    /**
     * {@inheritDoc}
     */
    protected function getReservedKeywordsClass()
    {
        return Keywords\OracleKeywords::class;
    }

    /**
     * {@inheritDoc}
     */
    public function getBlobTypeDeclarationSQL(array $column)
    {
        return 'BLOB';
    }

    public function getListTableCommentsSQL(string $table, ?string $database = null): string
    {
        $tableCommentsName = 'user_tab_comments';
        $ownerCondition    = '';

        if ($database !== null && $database !== '/') {
            $tableCommentsName = 'all_tab_comments';
            $ownerCondition    = ' AND owner = ' . $this->quoteStringLiteral(
                $this->normalizeIdentifier($database)->getName()
            );
        }

        return sprintf(
            <<<'SQL'
SELECT comments FROM %s WHERE table_name = %s%s
SQL
            ,
            $tableCommentsName,
            $this->quoteStringLiteral($this->normalizeIdentifier($table)->getName()),
            $ownerCondition
        );
    }
}
<?php

namespace Doctrine\DBAL\Platforms;

use function explode;

/**
 * Provides the behavior, features and SQL dialect of the PostgreSQL 9.1 database platform.
 *
 * @deprecated Use PostgreSQL 9.4 or newer
 */
class PostgreSQL91Platform extends PostgreSqlPlatform
{
    /**
     * {@inheritDoc}
     */
    public function supportsColumnCollation()
    {
        return true;
    }

    /**
     * {@inheritdoc}
     */
    protected function getReservedKeywordsClass()
    {
        return Keywords\PostgreSQL91Keywords::class;
    }

    /**
     * {@inheritDoc}
     */
    public function getColumnCollationDeclarationSQL($collation)
    {
        return 'COLLATE ' . $this->quoteSingleIdentifier($collation);
    }

    /**
     * {@inheritDoc}
     */
    public function getListTableColumnsSQL($table, $database = null)
    {
        $sql   = parent::getListTableColumnsSQL($table, $database);
        $parts = explode('AS complete_type,', $sql, 2);

        return $parts[0] . 'AS complete_type, '
            . '(SELECT tc.collcollate FROM pg_catalog.pg_collation tc WHERE tc.oid = a.attcollation) AS collation,'
            . $parts[1];
    }
}
<?php

namespace Doctrine\DBAL\Platforms;

use Doctrine\DBAL\Schema\ForeignKeyConstraint;
use Doctrine\DBAL\Schema\Identifier;
use Doctrine\DBAL\Schema\Index;
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Schema\TableDiff;
use Doctrine\DBAL\TransactionIsolationLevel;
use Doctrine\DBAL\Types\BlobType;
use Doctrine\DBAL\Types\TextType;
use InvalidArgumentException;

use function array_diff_key;
use function array_merge;
use function array_unique;
use function array_values;
use function count;
use function func_get_args;
use function implode;
use function in_array;
use function is_numeric;
use function is_string;
use function sprintf;
use function str_replace;
use function strtoupper;
use function trim;

/**
 * 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.
 *
 * @todo   Rename: MySQLPlatform
 */
class MySqlPlatform extends AbstractPlatform
{
    public const LENGTH_LIMIT_TINYTEXT   = 255;
    public const LENGTH_LIMIT_TEXT       = 65535;
    public const LENGTH_LIMIT_MEDIUMTEXT = 16777215;

    public const LENGTH_LIMIT_TINYBLOB   = 255;
    public const LENGTH_LIMIT_BLOB       = 65535;
    public const LENGTH_LIMIT_MEDIUMBLOB = 16777215;

    /**
     * {@inheritDoc}
     */
    protected function doModifyLimitQuery($query, $limit, $offset)
    {
        if ($limit !== null) {
            $query .= ' LIMIT ' . $limit;

            if ($offset > 0) {
                $query .= ' OFFSET ' . $offset;
            }
        } elseif ($offset > 0) {
            // 2^64-1 is the maximum of unsigned BIGINT, the biggest limit possible
            $query .= ' LIMIT 18446744073709551615 OFFSET ' . $offset;
        }

        return $query;
    }

    /**
     * {@inheritDoc}
     */
    public function getIdentifierQuoteCharacter()
    {
        return '`';
    }

    /**
     * {@inheritDoc}
     */
    public function getRegexpExpression()
    {
        return 'RLIKE';
    }

    /**
     * {@inheritDoc}
     *
     * @deprecated Use application-generated UUIDs instead
     */
    public function getGuidExpression()
    {
        return 'UUID()';
    }

    /**
     * {@inheritDoc}
     */
    public function getLocateExpression($str, $substr, $startPos = false)
    {
        if ($startPos === false) {
            return 'LOCATE(' . $substr . ', ' . $str . ')';
        }

        return 'LOCATE(' . $substr . ', ' . $str . ', ' . $startPos . ')';
    }

    /**
     * {@inheritDoc}
     */
    public function getConcatExpression()
    {
        return sprintf('CONCAT(%s)', implode(', ', func_get_args()));
    }

    /**
     * {@inheritdoc}
     */
    protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit)
    {
        $function = $operator === '+' ? 'DATE_ADD' : 'DATE_SUB';

        return $function . '(' . $date . ', INTERVAL ' . $interval . ' ' . $unit . ')';
    }

    /**
     * {@inheritDoc}
     */
    public function getDateDiffExpression($date1, $date2)
    {
        return 'DATEDIFF(' . $date1 . ', ' . $date2 . ')';
    }

    /**
     * {@inheritDoc}
     */
    public function getListDatabasesSQL()
    {
        return 'SHOW DATABASES';
    }

    /**
     * {@inheritDoc}
     */
    public function getListTableConstraintsSQL($table)
    {
        return 'SHOW INDEX FROM ' . $table;
    }

    /**
     * {@inheritDoc}
     *
     * Two approaches to listing the table indexes. The information_schema is
     * preferred, because it doesn't cause problems with SQL keywords such as "order" or "table".
     */
    public function getListTableIndexesSQL($table, $database = null)
    {
        if ($database) {
            $database = $this->quoteStringLiteral($database);
            $table    = $this->quoteStringLiteral($table);

            return 'SELECT NON_UNIQUE AS Non_Unique, INDEX_NAME AS Key_name, COLUMN_NAME AS Column_Name,' .
                   ' SUB_PART AS Sub_Part, INDEX_TYPE AS Index_Type' .
                   ' FROM information_schema.STATISTICS WHERE TABLE_NAME = ' . $table .
                   ' AND TABLE_SCHEMA = ' . $database .
                   ' ORDER BY SEQ_IN_INDEX ASC';
        }

        return 'SHOW INDEX FROM ' . $table;
    }

    /**
     * {@inheritDoc}
     */
    public function getListViewsSQL($database)
    {
        $database = $this->quoteStringLiteral($database);

        return 'SELECT * FROM information_schema.VIEWS WHERE TABLE_SCHEMA = ' . $database;
    }

    /**
     * @param string      $table
     * @param string|null $database
     *
     * @return string
     */
    public function getListTableForeignKeysSQL($table, $database = null)
    {
        $table = $this->quoteStringLiteral($table);

        if ($database !== null) {
            $database = $this->quoteStringLiteral($database);
        }

        $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;

        $databaseNameSql = $database ?? 'DATABASE()';

        return $sql . ' AND k.table_schema = ' . $databaseNameSql
            . ' /*!50116 AND c.constraint_schema = ' . $databaseNameSql . ' */'
            . ' AND k.`REFERENCED_COLUMN_NAME` is not NULL';
    }

    /**
     * {@inheritDoc}
     */
    public function getCreateViewSQL($name, $sql)
    {
        return 'CREATE VIEW ' . $name . ' AS ' . $sql;
    }

    /**
     * {@inheritDoc}
     */
    public function getDropViewSQL($name)
    {
        return 'DROP VIEW ' . $name;
    }

    /**
     * {@inheritDoc}
     */
    protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed)
    {
        return $fixed ? ($length ? 'CHAR(' . $length . ')' : 'CHAR(255)')
                : ($length ? 'VARCHAR(' . $length . ')' : 'VARCHAR(255)');
    }

    /**
     * {@inheritdoc}
     */
    protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed)
    {
        return $fixed ? 'BINARY(' . ($length ?: 255) . ')' : 'VARBINARY(' . ($length ?: 255) . ')';
    }

    /**
     * Gets the SQL snippet used to declare a CLOB column type.
     *     TINYTEXT   : 2 ^  8 - 1 = 255
     *     TEXT       : 2 ^ 16 - 1 = 65535
     *     MEDIUMTEXT : 2 ^ 24 - 1 = 16777215
     *     LONGTEXT   : 2 ^ 32 - 1 = 4294967295
     *
     * {@inheritDoc}
     */
    public function getClobTypeDeclarationSQL(array $column)
    {
        if (! empty($column['length']) && is_numeric($column['length'])) {
            $length = $column['length'];

            if ($length <= static::LENGTH_LIMIT_TINYTEXT) {
                return 'TINYTEXT';
            }

            if ($length <= static::LENGTH_LIMIT_TEXT) {
                return 'TEXT';
            }

            if ($length <= static::LENGTH_LIMIT_MEDIUMTEXT) {
                return 'MEDIUMTEXT';
            }
        }

        return 'LONGTEXT';
    }

    /**
     * {@inheritDoc}
     */
    public function getDateTimeTypeDeclarationSQL(array $column)
    {
        if (isset($column['version']) && $column['version'] === true) {
            return 'TIMESTAMP';
        }

        return 'DATETIME';
    }

    /**
     * {@inheritDoc}
     */
    public function getDateTypeDeclarationSQL(array $column)
    {
        return 'DATE';
    }

    /**
     * {@inheritDoc}
     */
    public function getTimeTypeDeclarationSQL(array $column)
    {
        return 'TIME';
    }

    /**
     * {@inheritDoc}
     */
    public function getBooleanTypeDeclarationSQL(array $column)
    {
        return 'TINYINT(1)';
    }

    /**
     * Obtain DBMS specific SQL code portion needed to set the COLLATION
     * of a column declaration to be used in statements like CREATE TABLE.
     *
     * @deprecated Deprecated since version 2.5, Use {@link self::getColumnCollationDeclarationSQL()} instead.
     *
     * @param string $collation name of the collation
     *
     * @return string  DBMS specific SQL code portion needed to set the COLLATION
     *                 of a column declaration.
     */
    public function getCollationFieldDeclaration($collation)
    {
        return $this->getColumnCollationDeclarationSQL($collation);
    }

    /**
     * {@inheritDoc}
     *
     * MySql prefers "autoincrement" identity columns since sequences can only
     * be emulated with a table.
     */
    public function prefersIdentityColumns()
    {
        return true;
    }

    /**
     * {@inheritDoc}
     *
     * MySql supports this through AUTO_INCREMENT columns.
     */
    public function supportsIdentityColumns()
    {
        return true;
    }

    /**
     * {@inheritDoc}
     */
    public function supportsInlineColumnComments()
    {
        return true;
    }

    /**
     * {@inheritDoc}
     */
    public function supportsColumnCollation()
    {
        return true;
    }

    /**
     * {@inheritDoc}
     */
    public function getListTablesSQL()
    {
        return "SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'";
    }

    /**
     * {@inheritDoc}
     */
    public function getListTableColumnsSQL($table, $database = null)
    {
        $table = $this->quoteStringLiteral($table);

        if ($database) {
            $database = $this->quoteStringLiteral($database);
        } else {
            $database = 'DATABASE()';
        }

        return 'SELECT COLUMN_NAME AS Field, COLUMN_TYPE AS Type, IS_NULLABLE AS `Null`, ' .
               'COLUMN_KEY AS `Key`, COLUMN_DEFAULT AS `Default`, EXTRA AS Extra, COLUMN_COMMENT AS Comment, ' .
               'CHARACTER_SET_NAME AS CharacterSet, COLLATION_NAME AS Collation ' .
               'FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ' . $database . ' AND TABLE_NAME = ' . $table .
               ' ORDER BY ORDINAL_POSITION ASC';
    }

    public function getListTableMetadataSQL(string $table, ?string $database = null): string
    {
        return sprintf(
            <<<'SQL'
SELECT ENGINE, AUTO_INCREMENT, TABLE_COLLATION, TABLE_COMMENT, CREATE_OPTIONS
FROM information_schema.TABLES
WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_SCHEMA = %s AND TABLE_NAME = %s
SQL
            ,
            $database ? $this->quoteStringLiteral($database) : 'DATABASE()',
            $this->quoteStringLiteral($table)
        );
    }

    /**
     * {@inheritDoc}
     */
    public function getCreateDatabaseSQL($name)
    {
        return 'CREATE DATABASE ' . $name;
    }

    /**
     * {@inheritDoc}
     */
    public function getDropDatabaseSQL($name)
    {
        return 'DROP DATABASE ' . $name;
    }

    /**
     * {@inheritDoc}
     */
    protected function _getCreateTableSQL($name, array $columns, array $options = [])
    {
        $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 ' . $name . ' (' . $queryFields . ') ';
        $query .= $this->buildTableOptions($options);
        $query .= $this->buildPartitionOptions($options);

        $sql    = [$query];
        $engine = 'INNODB';

        if (isset($options['engine'])) {
            $engine = strtoupper(trim($options['engine']));
        }

        // Propagate foreign key constraints only for InnoDB.
        if (isset($options['foreignKeys']) && $engine === 'INNODB') {
            foreach ((array) $options['foreignKeys'] as $definition) {
                $sql[] = $this->getCreateForeignKeySQL($definition, $name);
            }
        }

        return $sql;
    }

    /**
     * {@inheritdoc}
     */
    public function getDefaultValueDeclarationSQL($column)
    {
        // Unset the default value if the given column definition does not allow default values.
        if ($column['type'] instanceof TextType || $column['type'] instanceof BlobType) {
            $column['default'] = null;
        }

        return parent::getDefaultValueDeclarationSQL($column);
    }

    /**
     * Build SQL for table options
     *
     * @param mixed[] $options
     *
     * @return string
     */
    private function buildTableOptions(array $options)
    {
        if (isset($options['table_options'])) {
            return $options['table_options'];
        }

        $tableOptions = [];

        // Charset
        if (! isset($options['charset'])) {
            $options['charset'] = 'utf8';
        }

        $tableOptions[] = sprintf('DEFAULT CHARACTER SET %s', $options['charset']);

        // Collate
        if (! isset($options['collate'])) {
            $options['collate'] = $options['charset'] . '_unicode_ci';
        }

        $tableOptions[] = $this->getColumnCollationDeclarationSQL($options['collate']);

        // Engine
        if (! isset($options['engine'])) {
            $options['engine'] = 'InnoDB';
        }

        $tableOptions[] = sprintf('ENGINE = %s', $options['engine']);

        // Auto increment
        if (isset($options['auto_increment'])) {
            $tableOptions[] = sprintf('AUTO_INCREMENT = %s', $options['auto_increment']);
        }

        // Comment
        if (isset($options['comment'])) {
            $tableOptions[] = sprintf('COMMENT = %s ', $this->quoteStringLiteral($options['comment']));
        }

        // Row format
        if (isset($options['row_format'])) {
            $tableOptions[] = sprintf('ROW_FORMAT = %s', $options['row_format']);
        }

        return implode(' ', $tableOptions);
    }

    /**
     * Build SQL for partition options.
     *
     * @param mixed[] $options
     *
     * @return string
     */
    private function buildPartitionOptions(array $options)
    {
        return isset($options['partition_options'])
            ? ' ' . $options['partition_options']
            : '';
    }

    /**
     * {@inheritDoc}
     */
    public function getAlterTableSQL(TableDiff $diff)
    {
        $columnSql  = [];
        $queryParts = [];
        $newName    = $diff->getNewName();

        if ($newName !== false) {
            $queryParts[] = 'RENAME TO ' . $newName->getQuotedName($this);
        }

        foreach ($diff->addedColumns as $column) {
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
                continue;
            }

            $columnArray = array_merge($column->toArray(), [
                'comment' => $this->getColumnComment($column),
            ]);

            $queryParts[] = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnArray);
        }

        foreach ($diff->removedColumns as $column) {
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
                continue;
            }

            $queryParts[] =  'DROP ' . $column->getQuotedName($this);
        }

        foreach ($diff->changedColumns as $columnDiff) {
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
                continue;
            }

            $column      = $columnDiff->column;
            $columnArray = $column->toArray();

            // Don't propagate default value changes for unsupported column types.
            if (
                $columnDiff->hasChanged('default') &&
                count($columnDiff->changedProperties) === 1 &&
                ($columnArray['type'] instanceof TextType || $columnArray['type'] instanceof BlobType)
            ) {
                continue;
            }

            $columnArray['comment'] = $this->getColumnComment($column);
            $queryParts[]           =  'CHANGE ' . ($columnDiff->getOldColumnName()->getQuotedName($this)) . ' '
                    . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnArray);
        }

        foreach ($diff->renamedColumns as $oldColumnName => $column) {
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
                continue;
            }

            $oldColumnName          = new Identifier($oldColumnName);
            $columnArray            = $column->toArray();
            $columnArray['comment'] = $this->getColumnComment($column);
            $queryParts[]           =  'CHANGE ' . $oldColumnName->getQuotedName($this) . ' '
                    . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnArray);
        }

        if (isset($diff->addedIndexes['primary'])) {
            $keyColumns   = array_unique(array_values($diff->addedIndexes['primary']->getColumns()));
            $queryParts[] = 'ADD PRIMARY KEY (' . implode(', ', $keyColumns) . ')';
            unset($diff->addedIndexes['primary']);
        } elseif (isset($diff->changedIndexes['primary'])) {
            // Necessary in case the new primary key includes a new auto_increment column
            foreach ($diff->changedIndexes['primary']->getColumns() as $columnName) {
                if (isset($diff->addedColumns[$columnName]) && $diff->addedColumns[$columnName]->getAutoincrement()) {
                    $keyColumns   = array_unique(array_values($diff->changedIndexes['primary']->getColumns()));
                    $queryParts[] = 'DROP PRIMARY KEY';
                    $queryParts[] = 'ADD PRIMARY KEY (' . implode(', ', $keyColumns) . ')';
                    unset($diff->changedIndexes['primary']);
                    break;
                }
            }
        }

        $sql      = [];
        $tableSql = [];

        if (! $this->onSchemaAlterTable($diff, $tableSql)) {
            if (count($queryParts) > 0) {
                $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' '
                    . implode(', ', $queryParts);
            }

            $sql = array_merge(
                $this->getPreAlterTableIndexForeignKeySQL($diff),
                $sql,
                $this->getPostAlterTableIndexForeignKeySQL($diff)
            );
        }

        return array_merge($sql, $tableSql, $columnSql);
    }

    /**
     * {@inheritDoc}
     */
    protected function getPreAlterTableIndexForeignKeySQL(TableDiff $diff)
    {
        $sql   = [];
        $table = $diff->getName($this)->getQuotedName($this);

        foreach ($diff->changedIndexes as $changedIndex) {
            $sql = array_merge($sql, $this->getPreAlterTableAlterPrimaryKeySQL($diff, $changedIndex));
        }

        foreach ($diff->removedIndexes as $remKey => $remIndex) {
            $sql = array_merge($sql, $this->getPreAlterTableAlterPrimaryKeySQL($diff, $remIndex));

            foreach ($diff->addedIndexes as $addKey => $addIndex) {
                if ($remIndex->getColumns() !== $addIndex->getColumns()) {
                    continue;
                }

                $indexClause = 'INDEX ' . $addIndex->getName();

                if ($addIndex->isPrimary()) {
                    $indexClause = 'PRIMARY KEY';
                } elseif ($addIndex->isUnique()) {
                    $indexClause = 'UNIQUE INDEX ' . $addIndex->getName();
                }

                $query  = 'ALTER TABLE ' . $table . ' DROP INDEX ' . $remIndex->getName() . ', ';
                $query .= 'ADD ' . $indexClause;
                $query .= ' (' . $this->getIndexFieldDeclarationListSQL($addIndex) . ')';

                $sql[] = $query;

                unset($diff->removedIndexes[$remKey], $diff->addedIndexes[$addKey]);

                break;
            }
        }

        $engine = 'INNODB';

        if ($diff->fromTable instanceof Table && $diff->fromTable->hasOption('engine')) {
            $engine = strtoupper(trim($diff->fromTable->getOption('engine')));
        }

        // Suppress foreign key constraint propagation on non-supporting engines.
        if ($engine !== 'INNODB') {
            $diff->addedForeignKeys   = [];
            $diff->changedForeignKeys = [];
            $diff->removedForeignKeys = [];
        }

        $sql = array_merge(
            $sql,
            $this->getPreAlterTableAlterIndexForeignKeySQL($diff),
            parent::getPreAlterTableIndexForeignKeySQL($diff),
            $this->getPreAlterTableRenameIndexForeignKeySQL($diff)
        );

        return $sql;
    }

    /**
     * @return string[]
     */
    private function getPreAlterTableAlterPrimaryKeySQL(TableDiff $diff, Index $index)
    {
        $sql = [];

        if (! $index->isPrimary() || ! $diff->fromTable instanceof Table) {
            return $sql;
        }

        $tableName = $diff->getName($this)->getQuotedName($this);

        // Dropping primary keys requires to unset autoincrement attribute on the particular column first.
        foreach ($index->getColumns() as $columnName) {
            if (! $diff->fromTable->hasColumn($columnName)) {
                continue;
            }

            $column = $diff->fromTable->getColumn($columnName);

            if ($column->getAutoincrement() !== true) {
                continue;
            }

            $column->setAutoincrement(false);

            $sql[] = 'ALTER TABLE ' . $tableName . ' MODIFY ' .
                $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray());

            // original autoincrement information might be needed later on by other parts of the table alteration
            $column->setAutoincrement(true);
        }

        return $sql;
    }

    /**
     * @param TableDiff $diff The table diff to gather the SQL for.
     *
     * @return string[]
     */
    private function getPreAlterTableAlterIndexForeignKeySQL(TableDiff $diff)
    {
        $sql   = [];
        $table = $diff->getName($this)->getQuotedName($this);

        foreach ($diff->changedIndexes as $changedIndex) {
            // Changed primary key
            if (! $changedIndex->isPrimary() || ! ($diff->fromTable instanceof Table)) {
                continue;
            }

            foreach ($diff->fromTable->getPrimaryKeyColumns() as $columnName) {
                $column = $diff->fromTable->getColumn($columnName);

                // Check if an autoincrement column was dropped from the primary key.
                if (! $column->getAutoincrement() || in_array($columnName, $changedIndex->getColumns())) {
                    continue;
                }

                // The autoincrement attribute needs to be removed from the dropped column
                // before we can drop and recreate the primary key.
                $column->setAutoincrement(false);

                $sql[] = 'ALTER TABLE ' . $table . ' MODIFY ' .
                    $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray());

                // Restore the autoincrement attribute as it might be needed later on
                // by other parts of the table alteration.
                $column->setAutoincrement(true);
            }
        }

        return $sql;
    }

    /**
     * @param TableDiff $diff The table diff to gather the SQL for.
     *
     * @return string[]
     */
    protected function getPreAlterTableRenameIndexForeignKeySQL(TableDiff $diff)
    {
        $sql       = [];
        $tableName = $diff->getName($this)->getQuotedName($this);

        foreach ($this->getRemainingForeignKeyConstraintsRequiringRenamedIndexes($diff) as $foreignKey) {
            if (in_array($foreignKey, $diff->changedForeignKeys, true)) {
                continue;
            }

            $sql[] = $this->getDropForeignKeySQL($foreignKey, $tableName);
        }

        return $sql;
    }

    /**
     * Returns the remaining foreign key constraints that require one of the renamed indexes.
     *
     * "Remaining" here refers to the diff between the foreign keys currently defined in the associated
     * table and the foreign keys to be removed.
     *
     * @param TableDiff $diff The table diff to evaluate.
     *
     * @return ForeignKeyConstraint[]
     */
    private function getRemainingForeignKeyConstraintsRequiringRenamedIndexes(TableDiff $diff)
    {
        if (empty($diff->renamedIndexes) || ! $diff->fromTable instanceof Table) {
            return [];
        }

        $foreignKeys = [];
        /** @var ForeignKeyConstraint[] $remainingForeignKeys */
        $remainingForeignKeys = array_diff_key(
            $diff->fromTable->getForeignKeys(),
            $diff->removedForeignKeys
        );

        foreach ($remainingForeignKeys as $foreignKey) {
            foreach ($diff->renamedIndexes as $index) {
                if ($foreignKey->intersectsIndexColumns($index)) {
                    $foreignKeys[] = $foreignKey;

                    break;
                }
            }
        }

        return $foreignKeys;
    }

    /**
     * {@inheritdoc}
     */
    protected function getPostAlterTableIndexForeignKeySQL(TableDiff $diff)
    {
        return array_merge(
            parent::getPostAlterTableIndexForeignKeySQL($diff),
            $this->getPostAlterTableRenameIndexForeignKeySQL($diff)
        );
    }

    /**
     * @param TableDiff $diff The table diff to gather the SQL for.
     *
     * @return string[]
     */
    protected function getPostAlterTableRenameIndexForeignKeySQL(TableDiff $diff)
    {
        $sql     = [];
        $newName = $diff->getNewName();

        if ($newName !== false) {
            $tableName = $newName->getQuotedName($this);
        } else {
            $tableName = $diff->getName($this)->getQuotedName($this);
        }

        foreach ($this->getRemainingForeignKeyConstraintsRequiringRenamedIndexes($diff) as $foreignKey) {
            if (in_array($foreignKey, $diff->changedForeignKeys, true)) {
                continue;
            }

            $sql[] = $this->getCreateForeignKeySQL($foreignKey, $tableName);
        }

        return $sql;
    }

    /**
     * {@inheritDoc}
     */
    protected function getCreateIndexSQLFlags(Index $index)
    {
        $type = '';
        if ($index->isUnique()) {
            $type .= 'UNIQUE ';
        } elseif ($index->hasFlag('fulltext')) {
            $type .= 'FULLTEXT ';
        } elseif ($index->hasFlag('spatial')) {
            $type .= 'SPATIAL ';
        }

        return $type;
    }

    /**
     * {@inheritDoc}
     */
    public function getIntegerTypeDeclarationSQL(array $column)
    {
        return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($column);
    }

    /**
     * {@inheritDoc}
     */
    public function getBigIntTypeDeclarationSQL(array $column)
    {
        return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($column);
    }

    /**
     * {@inheritDoc}
     */
    public function getSmallIntTypeDeclarationSQL(array $column)
    {
        return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($column);
    }

    /**
     * {@inheritdoc}
     */
    public function getFloatDeclarationSQL(array $column)
    {
        return 'DOUBLE PRECISION' . $this->getUnsignedDeclaration($column);
    }

    /**
     * {@inheritdoc}
     */
    public function getDecimalTypeDeclarationSQL(array $column)
    {
        return parent::getDecimalTypeDeclarationSQL($column) . $this->getUnsignedDeclaration($column);
    }

    /**
     * Get unsigned declaration for a column.
     *
     * @param mixed[] $columnDef
     *
     * @return string
     */
    private function getUnsignedDeclaration(array $columnDef)
    {
        return ! empty($columnDef['unsigned']) ? ' UNSIGNED' : '';
    }

    /**
     * {@inheritDoc}
     */
    protected function _getCommonIntegerTypeDeclarationSQL(array $column)
    {
        $autoinc = '';
        if (! empty($column['autoincrement'])) {
            $autoinc = ' AUTO_INCREMENT';
        }

        return $this->getUnsignedDeclaration($column) . $autoinc;
    }

    /**
     * {@inheritDoc}
     */
    public function getColumnCharsetDeclarationSQL($charset)
    {
        return 'CHARACTER SET ' . $charset;
    }

    /**
     * {@inheritDoc}
     */
    public function getColumnCollationDeclarationSQL($collation)
    {
        return 'COLLATE ' . $this->quoteSingleIdentifier($collation);
    }

    /**
     * {@inheritDoc}
     */
    public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey)
    {
        $query = '';
        if ($foreignKey->hasOption('match')) {
            $query .= ' MATCH ' . $foreignKey->getOption('match');
        }

        $query .= parent::getAdvancedForeignKeyOptionsSQL($foreignKey);

        return $query;
    }

    /**
     * {@inheritDoc}
     */
    public function getDropIndexSQL($index, $table = null)
    {
        if ($index instanceof Index) {
            $indexName = $index->getQuotedName($this);
        } elseif (is_string($index)) {
            $indexName = $index;
        } else {
            throw new InvalidArgumentException(
                __METHOD__ . '() expects $index parameter to be string or ' . Index::class . '.'
            );
        }

        if ($table instanceof Table) {
            $table = $table->getQuotedName($this);
        } elseif (! is_string($table)) {
            throw new InvalidArgumentException(
                __METHOD__ . '() expects $table parameter to be string or ' . Table::class . '.'
            );
        }

        if ($index instanceof Index && $index->isPrimary()) {
            // mysql primary keys are always named "PRIMARY",
            // so we cannot use them in statements because of them being keyword.
            return $this->getDropPrimaryKeySQL($table);
        }

        return 'DROP INDEX ' . $indexName . ' ON ' . $table;
    }

    /**
     * @param string $table
     *
     * @return string
     */
    protected function getDropPrimaryKeySQL($table)
    {
        return 'ALTER TABLE ' . $table . ' DROP PRIMARY KEY';
    }

    /**
     * {@inheritDoc}
     */
    public function getSetTransactionIsolationSQL($level)
    {
        return 'SET SESSION TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level);
    }

    /**
     * {@inheritDoc}
     */
    public function getName()
    {
        return 'mysql';
    }

    /**
     * {@inheritDoc}
     */
    public function getReadLockSQL()
    {
        return 'LOCK IN SHARE MODE';
    }

    /**
     * {@inheritDoc}
     */
    protected function initializeDoctrineTypeMappings()
    {
        $this->doctrineTypeMapping = [
            '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',
            'longblob'      => 'blob',
            'blob'          => 'blob',
            'mediumblob'    => 'blob',
            'tinyblob'      => 'blob',
            'binary'        => 'binary',
            'varbinary'     => 'binary',
            'set'           => 'simple_array',
        ];
    }

    /**
     * {@inheritDoc}
     */
    public function getVarcharMaxLength()
    {
        return 65535;
    }

    /**
     * {@inheritdoc}
     */
    public function getBinaryMaxLength()
    {
        return 65535;
    }

    /**
     * {@inheritDoc}
     */
    protected function getReservedKeywordsClass()
    {
        return Keywords\MySQLKeywords::class;
    }

    /**
     * {@inheritDoc}
     *
     * MySQL commits a transaction implicitly when DROP TABLE is executed, however not
     * if DROP TEMPORARY TABLE is executed.
     */
    public function getDropTemporaryTableSQL($table)
    {
        if ($table instanceof Table) {
            $table = $table->getQuotedName($this);
        } elseif (! is_string($table)) {
            throw new InvalidArgumentException(
                __METHOD__ . '() expects $table parameter to be string or ' . Table::class . '.'
            );
        }

        return 'DROP TEMPORARY TABLE ' . $table;
    }

    /**
     * Gets the SQL Snippet used to declare a BLOB column type.
     *     TINYBLOB   : 2 ^  8 - 1 = 255
     *     BLOB       : 2 ^ 16 - 1 = 65535
     *     MEDIUMBLOB : 2 ^ 24 - 1 = 16777215
     *     LONGBLOB   : 2 ^ 32 - 1 = 4294967295
     *
     * {@inheritDoc}
     */
    public function getBlobTypeDeclarationSQL(array $column)
    {
        if (! empty($column['length']) && is_numeric($column['length'])) {
            $length = $column['length'];

            if ($length <= static::LENGTH_LIMIT_TINYBLOB) {
                return 'TINYBLOB';
            }

            if ($length <= static::LENGTH_LIMIT_BLOB) {
                return 'BLOB';
            }

            if ($length <= static::LENGTH_LIMIT_MEDIUMBLOB) {
                return 'MEDIUMBLOB';
            }
        }

        return 'LONGBLOB';
    }

    /**
     * {@inheritdoc}
     */
    public function quoteStringLiteral($str)
    {
        $str = str_replace('\\', '\\\\', $str); // MySQL requires backslashes to be escaped aswell.

        return parent::quoteStringLiteral($str);
    }

    /**
     * {@inheritdoc}
     */
    public function getDefaultTransactionIsolationLevel()
    {
        return TransactionIsolationLevel::REPEATABLE_READ;
    }

    public function supportsColumnLengthIndexes(): bool
    {
        return true;
    }
}
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Platforms;

final class DateIntervalUnit
{
    public const SECOND = 'SECOND';

    public const MINUTE = 'MINUTE';

    public const HOUR = 'HOUR';

    public const DAY = 'DAY';

    public const WEEK = 'WEEK';

    public const MONTH = 'MONTH';

    public const QUARTER = 'QUARTER';

    public const YEAR = 'YEAR';

    /**
     * @codeCoverageIgnore
     */
    private function __construct()
    {
    }
}
<?php

namespace Doctrine\DBAL\Platforms;

use Doctrine\DBAL\Schema\Index;
use UnexpectedValueException;

/**
 * The SQLAnywhere16Platform provides the behavior, features and SQL dialect of the
 * SAP Sybase SQL Anywhere 16 database platform.
 *
 * @deprecated Support for SQLAnywhere will be removed in 3.0.
 */
class SQLAnywhere16Platform extends SQLAnywhere12Platform
{
    /**
     * {@inheritdoc}
     */
    protected function getAdvancedIndexOptionsSQL(Index $index)
    {
        if ($index->hasFlag('with_nulls_distinct') && $index->hasFlag('with_nulls_not_distinct')) {
            throw new UnexpectedValueException(
                'An Index can either have a "with_nulls_distinct" or "with_nulls_not_distinct" flag but not both.'
            );
        }

        if (! $index->isPrimary() && $index->isUnique() && $index->hasFlag('with_nulls_distinct')) {
            return ' WITH NULLS DISTINCT' . parent::getAdvancedIndexOptionsSQL($index);
        }

        return parent::getAdvancedIndexOptionsSQL($index);
    }

    /**
     * {@inheritdoc}
     */
    protected function getReservedKeywordsClass()
    {
        return Keywords\SQLAnywhere16Keywords::class;
    }
}
<?php

namespace Doctrine\DBAL\Platforms;

use Doctrine\DBAL\Exception;
use Doctrine\DBAL\LockMode;
use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Schema\ColumnDiff;
use Doctrine\DBAL\Schema\Constraint;
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
use Doctrine\DBAL\Schema\Identifier;
use Doctrine\DBAL\Schema\Index;
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Schema\TableDiff;
use Doctrine\DBAL\TransactionIsolationLevel;
use Doctrine\Deprecations\Deprecation;
use InvalidArgumentException;

use function array_merge;
use function array_unique;
use function array_values;
use function assert;
use function count;
use function explode;
use function func_get_args;
use function get_class;
use function implode;
use function is_string;
use function preg_match;
use function sprintf;
use function strlen;
use function strpos;
use function strtoupper;
use function substr;

/**
 * The SQLAnywherePlatform provides the behavior, features and SQL dialect of the
 * SAP Sybase SQL Anywhere 10 database platform.
 *
 * @deprecated Support for SQLAnywhere will be removed in 3.0.
 */
class SQLAnywherePlatform extends AbstractPlatform
{
    public const FOREIGN_KEY_MATCH_SIMPLE        = 1;
    public const FOREIGN_KEY_MATCH_FULL          = 2;
    public const FOREIGN_KEY_MATCH_SIMPLE_UNIQUE = 129;
    public const FOREIGN_KEY_MATCH_FULL_UNIQUE   = 130;

    /**
     * {@inheritdoc}
     */
    public function appendLockHint($fromClause, $lockMode)
    {
        switch (true) {
            case $lockMode === LockMode::NONE:
                return $fromClause;

            case $lockMode === LockMode::PESSIMISTIC_READ:
                return $fromClause . ' WITH (UPDLOCK)';

            case $lockMode === LockMode::PESSIMISTIC_WRITE:
                return $fromClause . ' WITH (XLOCK)';

            default:
                return $fromClause;
        }
    }

    /**
     * {@inheritdoc}
     *
     * SQL Anywhere supports a maximum length of 128 bytes for identifiers.
     */
    public function fixSchemaElementName($schemaElementName)
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/4132',
            'AbstractPlatform::fixSchemaElementName is deprecated with no replacement and removed in DBAL 3.0'
        );

        $maxIdentifierLength = $this->getMaxIdentifierLength();

        if (strlen($schemaElementName) > $maxIdentifierLength) {
            return substr($schemaElementName, 0, $maxIdentifierLength);
        }

        return $schemaElementName;
    }

    /**
     * {@inheritdoc}
     */
    public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey)
    {
        $query = '';

        if ($foreignKey->hasOption('match')) {
            $query = ' MATCH ' . $this->getForeignKeyMatchClauseSQL($foreignKey->getOption('match'));
        }

        $query .= parent::getAdvancedForeignKeyOptionsSQL($foreignKey);

        if ($foreignKey->hasOption('check_on_commit') && (bool) $foreignKey->getOption('check_on_commit')) {
            $query .= ' CHECK ON COMMIT';
        }

        if ($foreignKey->hasOption('clustered') && (bool) $foreignKey->getOption('clustered')) {
            $query .= ' CLUSTERED';
        }

        if ($foreignKey->hasOption('for_olap_workload') && (bool) $foreignKey->getOption('for_olap_workload')) {
            $query .= ' FOR OLAP WORKLOAD';
        }

        return $query;
    }

    /**
     * {@inheritdoc}
     */
    public function getAlterTableSQL(TableDiff $diff)
    {
        $sql          = [];
        $columnSql    = [];
        $commentsSQL  = [];
        $tableSql     = [];
        $alterClauses = [];

        foreach ($diff->addedColumns as $column) {
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
                continue;
            }

            $alterClauses[] = $this->getAlterTableAddColumnClause($column);

            $comment = $this->getColumnComment($column);

            if ($comment === null || $comment === '') {
                continue;
            }

            $commentsSQL[] = $this->getCommentOnColumnSQL(
                $diff->getName($this)->getQuotedName($this),
                $column->getQuotedName($this),
                $comment
            );
        }

        foreach ($diff->removedColumns as $column) {
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
                continue;
            }

            $alterClauses[] = $this->getAlterTableRemoveColumnClause($column);
        }

        foreach ($diff->changedColumns as $columnDiff) {
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
                continue;
            }

            $alterClause = $this->getAlterTableChangeColumnClause($columnDiff);

            if ($alterClause !== null) {
                $alterClauses[] = $alterClause;
            }

            if (! $columnDiff->hasChanged('comment')) {
                continue;
            }

            $column = $columnDiff->column;

            $commentsSQL[] = $this->getCommentOnColumnSQL(
                $diff->getName($this)->getQuotedName($this),
                $column->getQuotedName($this),
                $this->getColumnComment($column)
            );
        }

        foreach ($diff->renamedColumns as $oldColumnName => $column) {
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
                continue;
            }

            $sql[] = $this->getAlterTableClause($diff->getName($this)) . ' ' .
                $this->getAlterTableRenameColumnClause($oldColumnName, $column);
        }

        if (! $this->onSchemaAlterTable($diff, $tableSql)) {
            if (! empty($alterClauses)) {
                $sql[] = $this->getAlterTableClause($diff->getName($this)) . ' ' . implode(', ', $alterClauses);
            }

            $sql = array_merge($sql, $commentsSQL);

            $newName = $diff->getNewName();

            if ($newName !== false) {
                $sql[] = $this->getAlterTableClause($diff->getName($this)) . ' ' .
                    $this->getAlterTableRenameTableClause($newName);
            }

            $sql = array_merge(
                $this->getPreAlterTableIndexForeignKeySQL($diff),
                $sql,
                $this->getPostAlterTableIndexForeignKeySQL($diff)
            );
        }

        return array_merge($sql, $tableSql, $columnSql);
    }

    /**
     * Returns the SQL clause for creating a column in a table alteration.
     *
     * @param Column $column The column to add.
     *
     * @return string
     */
    protected function getAlterTableAddColumnClause(Column $column)
    {
        return 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray());
    }

    /**
     * Returns the SQL clause for altering a table.
     *
     * @param Identifier $tableName The quoted name of the table to alter.
     *
     * @return string
     */
    protected function getAlterTableClause(Identifier $tableName)
    {
        return 'ALTER TABLE ' . $tableName->getQuotedName($this);
    }

    /**
     * Returns the SQL clause for dropping a column in a table alteration.
     *
     * @param Column $column The column to drop.
     *
     * @return string
     */
    protected function getAlterTableRemoveColumnClause(Column $column)
    {
        return 'DROP ' . $column->getQuotedName($this);
    }

    /**
     * Returns the SQL clause for renaming a column in a table alteration.
     *
     * @param string $oldColumnName The quoted name of the column to rename.
     * @param Column $column        The column to rename to.
     *
     * @return string
     */
    protected function getAlterTableRenameColumnClause($oldColumnName, Column $column)
    {
        $oldColumnName = new Identifier($oldColumnName);

        return 'RENAME ' . $oldColumnName->getQuotedName($this) . ' TO ' . $column->getQuotedName($this);
    }

    /**
     * Returns the SQL clause for renaming a table in a table alteration.
     *
     * @param Identifier $newTableName The quoted name of the table to rename to.
     *
     * @return string
     */
    protected function getAlterTableRenameTableClause(Identifier $newTableName)
    {
        return 'RENAME ' . $newTableName->getQuotedName($this);
    }

    /**
     * Returns the SQL clause for altering a column in a table alteration.
     *
     * This method returns null in case that only the column comment has changed.
     * Changes in column comments have to be handled differently.
     *
     * @param ColumnDiff $columnDiff The diff of the column to alter.
     *
     * @return string|null
     */
    protected function getAlterTableChangeColumnClause(ColumnDiff $columnDiff)
    {
        $column = $columnDiff->column;

        // Do not return alter clause if only comment has changed.
        if (! ($columnDiff->hasChanged('comment') && count($columnDiff->changedProperties) === 1)) {
            $columnAlterationClause = 'ALTER ' .
                $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray());

            if ($columnDiff->hasChanged('default') && $column->getDefault() === null) {
                $columnAlterationClause .= ', ALTER ' . $column->getQuotedName($this) . ' DROP DEFAULT';
            }

            return $columnAlterationClause;
        }

        return null;
    }

    /**
     * {@inheritdoc}
     */
    public function getBigIntTypeDeclarationSQL(array $column)
    {
        $column['integer_type'] = 'BIGINT';

        return $this->_getCommonIntegerTypeDeclarationSQL($column);
    }

    /**
     * {@inheritdoc}
     */
    public function getBinaryDefaultLength()
    {
        return 1;
    }

    /**
     * {@inheritdoc}
     */
    public function getBinaryMaxLength()
    {
        return 32767;
    }

    /**
     * {@inheritdoc}
     */
    public function getBlobTypeDeclarationSQL(array $column)
    {
        return 'LONG BINARY';
    }

    /**
     * {@inheritdoc}
     *
     * BIT type columns require an explicit NULL declaration
     * in SQL Anywhere if they shall be nullable.
     * Otherwise by just omitting the NOT NULL clause,
     * SQL Anywhere will declare them NOT NULL nonetheless.
     */
    public function getBooleanTypeDeclarationSQL(array $column)
    {
        $nullClause = isset($column['notnull']) && (bool) $column['notnull'] === false ? ' NULL' : '';

        return 'BIT' . $nullClause;
    }

    /**
     * {@inheritdoc}
     */
    public function getClobTypeDeclarationSQL(array $column)
    {
        return 'TEXT';
    }

    /**
     * {@inheritdoc}
     */
    public function getCommentOnColumnSQL($tableName, $columnName, $comment)
    {
        $tableName  = new Identifier($tableName);
        $columnName = new Identifier($columnName);
        $comment    = $comment === null ? 'NULL' : $this->quoteStringLiteral($comment);

        return sprintf(
            'COMMENT ON COLUMN %s.%s IS %s',
            $tableName->getQuotedName($this),
            $columnName->getQuotedName($this),
            $comment
        );
    }

    /**
     * {@inheritdoc}
     */
    public function getConcatExpression()
    {
        return 'STRING(' . implode(', ', func_get_args()) . ')';
    }

    /**
     * {@inheritdoc}
     */
    public function getCreateConstraintSQL(Constraint $constraint, $table)
    {
        if ($constraint instanceof ForeignKeyConstraint) {
            return $this->getCreateForeignKeySQL($constraint, $table);
        }

        if ($table instanceof Table) {
            $table = $table->getQuotedName($this);
        }

        return 'ALTER TABLE ' . $table .
               ' ADD ' . $this->getTableConstraintDeclarationSQL($constraint, $constraint->getQuotedName($this));
    }

    /**
     * {@inheritdoc}
     */
    public function getCreateDatabaseSQL($name)
    {
        $name = new Identifier($name);

        return "CREATE DATABASE '" . $name->getName() . "'";
    }

    /**
     * {@inheritdoc}
     *
     * Appends SQL Anywhere specific flags if given.
     */
    public function getCreateIndexSQL(Index $index, $table)
    {
        return parent::getCreateIndexSQL($index, $table) . $this->getAdvancedIndexOptionsSQL($index);
    }

    /**
     * {@inheritdoc}
     */
    public function getCreatePrimaryKeySQL(Index $index, $table)
    {
        if ($table instanceof Table) {
            $table = $table->getQuotedName($this);
        }

        return 'ALTER TABLE ' . $table . ' ADD ' . $this->getPrimaryKeyDeclarationSQL($index);
    }

    /**
     * {@inheritdoc}
     */
    public function getCreateTemporaryTableSnippetSQL()
    {
        return 'CREATE ' . $this->getTemporaryTableSQL() . ' TABLE';
    }

    /**
     * {@inheritdoc}
     */
    public function getCreateViewSQL($name, $sql)
    {
        return 'CREATE VIEW ' . $name . ' AS ' . $sql;
    }

    /**
     * {@inheritdoc}
     */
    public function getCurrentDateSQL()
    {
        return 'CURRENT DATE';
    }

    /**
     * {@inheritdoc}
     */
    public function getCurrentTimeSQL()
    {
        return 'CURRENT TIME';
    }

    /**
     * {@inheritdoc}
     */
    public function getCurrentTimestampSQL()
    {
        return 'CURRENT TIMESTAMP';
    }

    /**
     * {@inheritdoc}
     */
    protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit)
    {
        $factorClause = '';

        if ($operator === '-') {
            $factorClause = '-1 * ';
        }

        return 'DATEADD(' . $unit . ', ' . $factorClause . $interval . ', ' . $date . ')';
    }

    /**
     * {@inheritdoc}
     */
    public function getDateDiffExpression($date1, $date2)
    {
        return 'DATEDIFF(day, ' . $date2 . ', ' . $date1 . ')';
    }

    /**
     * {@inheritdoc}
     */
    public function getDateTimeFormatString()
    {
        return 'Y-m-d H:i:s.u';
    }

    /**
     * {@inheritdoc}
     */
    public function getDateTimeTypeDeclarationSQL(array $column)
    {
        return 'DATETIME';
    }

    /**
     * {@inheritdoc}
     */
    public function getDateTimeTzFormatString()
    {
        return $this->getDateTimeFormatString();
    }

    /**
     * {@inheritdoc}
     */
    public function getDateTypeDeclarationSQL(array $column)
    {
        return 'DATE';
    }

    /**
     * {@inheritdoc}
     */
    public function getDefaultTransactionIsolationLevel()
    {
        return TransactionIsolationLevel::READ_UNCOMMITTED;
    }

    /**
     * {@inheritdoc}
     */
    public function getDropDatabaseSQL($name)
    {
        $name = new Identifier($name);

        return "DROP DATABASE '" . $name->getName() . "'";
    }

    /**
     * {@inheritdoc}
     */
    public function getDropIndexSQL($index, $table = null)
    {
        if ($index instanceof Index) {
            $index = $index->getQuotedName($this);
        }

        if (! is_string($index)) {
            throw new InvalidArgumentException(
                __METHOD__ . '() expects $index parameter to be string or ' . Index::class . '.'
            );
        }

        if (! isset($table)) {
            return 'DROP INDEX ' . $index;
        }

        if ($table instanceof Table) {
            $table = $table->getQuotedName($this);
        }

        if (! is_string($table)) {
            throw new InvalidArgumentException(
                __METHOD__ . '() expects $table parameter to be string or ' . Index::class . '.'
            );
        }

        return 'DROP INDEX ' . $table . '.' . $index;
    }

    /**
     * {@inheritdoc}
     */
    public function getDropViewSQL($name)
    {
        return 'DROP VIEW ' . $name;
    }

    /**
     * {@inheritdoc}
     */
    public function getForeignKeyBaseDeclarationSQL(ForeignKeyConstraint $foreignKey)
    {
        $sql              = '';
        $foreignKeyName   = $foreignKey->getName();
        $localColumns     = $foreignKey->getQuotedLocalColumns($this);
        $foreignColumns   = $foreignKey->getQuotedForeignColumns($this);
        $foreignTableName = $foreignKey->getQuotedForeignTableName($this);

        if (! empty($foreignKeyName)) {
            $sql .= 'CONSTRAINT ' . $foreignKey->getQuotedName($this) . ' ';
        }

        if (empty($localColumns)) {
            throw new InvalidArgumentException("Incomplete definition. 'local' required.");
        }

        if (empty($foreignColumns)) {
            throw new InvalidArgumentException("Incomplete definition. 'foreign' required.");
        }

        if (empty($foreignTableName)) {
            throw new InvalidArgumentException("Incomplete definition. 'foreignTable' required.");
        }

        if ($foreignKey->hasOption('notnull') && (bool) $foreignKey->getOption('notnull')) {
            $sql .= 'NOT NULL ';
        }

        return $sql .
            'FOREIGN KEY (' . $this->getIndexFieldDeclarationListSQL($localColumns) . ') ' .
            'REFERENCES ' . $foreignKey->getQuotedForeignTableName($this) .
            ' (' . $this->getIndexFieldDeclarationListSQL($foreignColumns) . ')';
    }

    /**
     * Returns foreign key MATCH clause for given type.
     *
     * @param int $type The foreign key match type
     *
     * @return string
     *
     * @throws InvalidArgumentException If unknown match type given.
     */
    public function getForeignKeyMatchClauseSQL($type)
    {
        switch ((int) $type) {
            case self::FOREIGN_KEY_MATCH_SIMPLE:
                return 'SIMPLE';

            case self::FOREIGN_KEY_MATCH_FULL:
                return 'FULL';

            case self::FOREIGN_KEY_MATCH_SIMPLE_UNIQUE:
                return 'UNIQUE SIMPLE';

            case self::FOREIGN_KEY_MATCH_FULL_UNIQUE:
                return 'UNIQUE FULL';

            default:
                throw new InvalidArgumentException('Invalid foreign key match type: ' . $type);
        }
    }

    /**
     * {@inheritdoc}
     */
    public function getForeignKeyReferentialActionSQL($action)
    {
        // NO ACTION is not supported, therefore falling back to RESTRICT.
        if (strtoupper($action) === 'NO ACTION') {
            return 'RESTRICT';
        }

        return parent::getForeignKeyReferentialActionSQL($action);
    }

    /**
     * {@inheritdoc}
     */
    public function getForUpdateSQL()
    {
        return '';
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Use application-generated UUIDs instead
     */
    public function getGuidExpression()
    {
        return 'NEWID()';
    }

    /**
     * {@inheritdoc}
     */
    public function getGuidTypeDeclarationSQL(array $column)
    {
        return 'UNIQUEIDENTIFIER';
    }

    /**
     * {@inheritdoc}
     */
    public function getIndexDeclarationSQL($name, Index $index)
    {
        // Index declaration in statements like CREATE TABLE is not supported.
        throw Exception::notSupported(__METHOD__);
    }

    /**
     * {@inheritdoc}
     */
    public function getIntegerTypeDeclarationSQL(array $column)
    {
        $column['integer_type'] = 'INT';

        return $this->_getCommonIntegerTypeDeclarationSQL($column);
    }

    /**
     * {@inheritdoc}
     */
    public function getListDatabasesSQL()
    {
        return 'SELECT db_name(number) AS name FROM sa_db_list()';
    }

    /**
     * {@inheritdoc}
     */
    public function getListTableColumnsSQL($table, $database = null)
    {
        $user = 'USER_NAME()';

        if (strpos($table, '.') !== false) {
            [$user, $table] = explode('.', $table);
            $user           = $this->quoteStringLiteral($user);
        }

        return sprintf(
            <<<'SQL'
SELECT    col.column_name,
          COALESCE(def.user_type_name, def.domain_name) AS 'type',
          def.declared_width AS 'length',
          def.scale,
          CHARINDEX('unsigned', def.domain_name) AS 'unsigned',
          IF col.nulls = 'Y' THEN 0 ELSE 1 ENDIF AS 'notnull',
          col."default",
          def.is_autoincrement AS 'autoincrement',
          rem.remarks AS 'comment'
FROM      sa_describe_query('SELECT * FROM "%s"') AS def
JOIN      SYS.SYSTABCOL AS col
ON        col.table_id = def.base_table_id AND col.column_id = def.base_column_id
LEFT JOIN SYS.SYSREMARK AS rem
ON        col.object_id = rem.object_id
WHERE     def.base_owner_name = %s
ORDER BY  def.base_column_id ASC
SQL
            ,
            $table,
            $user
        );
    }

    /**
     * {@inheritdoc}
     *
     * @todo Where is this used? Which information should be retrieved?
     */
    public function getListTableConstraintsSQL($table)
    {
        $user = '';

        if (strpos($table, '.') !== false) {
            [$user, $table] = explode('.', $table);
            $user           = $this->quoteStringLiteral($user);
            $table          = $this->quoteStringLiteral($table);
        } else {
            $table = $this->quoteStringLiteral($table);
        }

        return sprintf(
            <<<'SQL'
SELECT con.*
FROM   SYS.SYSCONSTRAINT AS con
JOIN   SYS.SYSTAB AS tab ON con.table_object_id = tab.object_id
WHERE  tab.table_name = %s
AND    tab.creator = USER_ID(%s)
SQL
            ,
            $table,
            $user
        );
    }

    /**
     * {@inheritdoc}
     */
    public function getListTableForeignKeysSQL($table)
    {
        $user = '';

        if (strpos($table, '.') !== false) {
            [$user, $table] = explode('.', $table);
            $user           = $this->quoteStringLiteral($user);
            $table          = $this->quoteStringLiteral($table);
        } else {
            $table = $this->quoteStringLiteral($table);
        }

        return sprintf(
            <<<'SQL'
SELECT    fcol.column_name AS local_column,
          ptbl.table_name AS foreign_table,
          pcol.column_name AS foreign_column,
          idx.index_name,
          IF fk.nulls = 'N'
              THEN 1
              ELSE NULL
          ENDIF AS notnull,
          CASE ut.referential_action
              WHEN 'C' THEN 'CASCADE'
              WHEN 'D' THEN 'SET DEFAULT'
              WHEN 'N' THEN 'SET NULL'
              WHEN 'R' THEN 'RESTRICT'
              ELSE NULL
          END AS  on_update,
          CASE dt.referential_action
              WHEN 'C' THEN 'CASCADE'
              WHEN 'D' THEN 'SET DEFAULT'
              WHEN 'N' THEN 'SET NULL'
              WHEN 'R' THEN 'RESTRICT'
              ELSE NULL
          END AS on_delete,
          IF fk.check_on_commit = 'Y'
              THEN 1
              ELSE NULL
          ENDIF AS check_on_commit, -- check_on_commit flag
          IF ftbl.clustered_index_id = idx.index_id
              THEN 1
              ELSE NULL
          ENDIF AS 'clustered', -- clustered flag
          IF fk.match_type = 0
              THEN NULL
              ELSE fk.match_type
          ENDIF AS 'match', -- match option
          IF pidx.max_key_distance = 1
              THEN 1
              ELSE NULL
          ENDIF AS for_olap_workload -- for_olap_workload flag
FROM      SYS.SYSFKEY AS fk
JOIN      SYS.SYSIDX AS idx
ON        fk.foreign_table_id = idx.table_id
AND       fk.foreign_index_id = idx.index_id
JOIN      SYS.SYSPHYSIDX pidx
ON        idx.table_id = pidx.table_id
AND       idx.phys_index_id = pidx.phys_index_id
JOIN      SYS.SYSTAB AS ptbl
ON        fk.primary_table_id = ptbl.table_id
JOIN      SYS.SYSTAB AS ftbl
ON        fk.foreign_table_id = ftbl.table_id
JOIN      SYS.SYSIDXCOL AS idxcol
ON        idx.table_id = idxcol.table_id
AND       idx.index_id = idxcol.index_id
JOIN      SYS.SYSTABCOL AS pcol
ON        ptbl.table_id = pcol.table_id
AND       idxcol.primary_column_id = pcol.column_id
JOIN      SYS.SYSTABCOL AS fcol
ON        ftbl.table_id = fcol.table_id
AND       idxcol.column_id = fcol.column_id
LEFT JOIN SYS.SYSTRIGGER ut
ON        fk.foreign_table_id = ut.foreign_table_id
AND       fk.foreign_index_id = ut.foreign_key_id
AND       ut.event = 'C'
LEFT JOIN SYS.SYSTRIGGER dt
ON        fk.foreign_table_id = dt.foreign_table_id
AND       fk.foreign_index_id = dt.foreign_key_id
AND       dt.event = 'D'
WHERE     ftbl.table_name = %s
AND       ftbl.creator = USER_ID(%s)
ORDER BY  fk.foreign_index_id ASC, idxcol.sequence ASC
SQL
            ,
            $table,
            $user
        );
    }

    /**
     * {@inheritdoc}
     */
    public function getListTableIndexesSQL($table, $database = null)
    {
        $user = '';

        if (strpos($table, '.') !== false) {
            [$user, $table] = explode('.', $table);
            $user           = $this->quoteStringLiteral($user);
            $table          = $this->quoteStringLiteral($table);
        } else {
            $table = $this->quoteStringLiteral($table);
        }

        return sprintf(
            <<<'SQL'
SELECT   idx.index_name AS key_name,
         IF idx.index_category = 1
             THEN 1
             ELSE 0
         ENDIF AS 'primary',
         col.column_name,
         IF idx."unique" IN(1, 2, 5)
             THEN 0
             ELSE 1
         ENDIF AS non_unique,
         IF tbl.clustered_index_id = idx.index_id
             THEN 1
             ELSE NULL
         ENDIF AS 'clustered', -- clustered flag
         IF idx."unique" = 5
             THEN 1
             ELSE NULL
         ENDIF AS with_nulls_not_distinct, -- with_nulls_not_distinct flag
         IF pidx.max_key_distance = 1
              THEN 1
              ELSE NULL
          ENDIF AS for_olap_workload -- for_olap_workload flag
FROM     SYS.SYSIDX AS idx
JOIN     SYS.SYSPHYSIDX pidx
ON       idx.table_id = pidx.table_id
AND      idx.phys_index_id = pidx.phys_index_id
JOIN     SYS.SYSIDXCOL AS idxcol
ON       idx.table_id = idxcol.table_id AND idx.index_id = idxcol.index_id
JOIN     SYS.SYSTABCOL AS col
ON       idxcol.table_id = col.table_id AND idxcol.column_id = col.column_id
JOIN     SYS.SYSTAB AS tbl
ON       idx.table_id = tbl.table_id
WHERE    tbl.table_name = %s
AND      tbl.creator = USER_ID(%s)
AND      idx.index_category != 2 -- exclude indexes implicitly created by foreign key constraints
ORDER BY idx.index_id ASC, idxcol.sequence ASC
SQL
            ,
            $table,
            $user
        );
    }

    /**
     * {@inheritdoc}
     */
    public function getListTablesSQL()
    {
        return "SELECT   tbl.table_name
                FROM     SYS.SYSTAB AS tbl
                JOIN     SYS.SYSUSER AS usr ON tbl.creator = usr.user_id
                JOIN     dbo.SYSOBJECTS AS obj ON tbl.object_id = obj.id
                WHERE    tbl.table_type IN(1, 3) -- 'BASE', 'GBL TEMP'
                AND      usr.user_name NOT IN('SYS', 'dbo', 'rs_systabgroup') -- exclude system users
                AND      obj.type = 'U' -- user created tables only
                ORDER BY tbl.table_name ASC";
    }

    /**
     * {@inheritdoc}
     *
     * @todo Where is this used? Which information should be retrieved?
     */
    public function getListUsersSQL()
    {
        return 'SELECT * FROM SYS.SYSUSER ORDER BY user_name ASC';
    }

    /**
     * {@inheritdoc}
     */
    public function getListViewsSQL($database)
    {
        return "SELECT   tbl.table_name, v.view_def
                FROM     SYS.SYSVIEW v
                JOIN     SYS.SYSTAB tbl ON v.view_object_id = tbl.object_id
                JOIN     SYS.SYSUSER usr ON tbl.creator = usr.user_id
                JOIN     dbo.SYSOBJECTS obj ON tbl.object_id = obj.id
                WHERE    usr.user_name NOT IN('SYS', 'dbo', 'rs_systabgroup') -- exclude system users
                ORDER BY tbl.table_name ASC";
    }

    /**
     * {@inheritdoc}
     */
    public function getLocateExpression($str, $substr, $startPos = false)
    {
        if ($startPos === false) {
            return 'LOCATE(' . $str . ', ' . $substr . ')';
        }

        return 'LOCATE(' . $str . ', ' . $substr . ', ' . $startPos . ')';
    }

    /**
     * {@inheritdoc}
     */
    public function getMaxIdentifierLength()
    {
        return 128;
    }

    /**
     * {@inheritdoc}
     */
    public function getMd5Expression($column)
    {
        return 'HASH(' . $column . ", 'MD5')";
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'sqlanywhere';
    }

    /**
     * Obtain DBMS specific SQL code portion needed to set a primary key
     * declaration to be used in statements like ALTER TABLE.
     *
     * @param Index  $index Index definition
     * @param string $name  Name of the primary key
     *
     * @return string DBMS specific SQL code portion needed to set a primary key
     *
     * @throws InvalidArgumentException If the given index is not a primary key.
     */
    public function getPrimaryKeyDeclarationSQL(Index $index, $name = null)
    {
        if (! $index->isPrimary()) {
            throw new InvalidArgumentException(
                'Can only create primary key declarations with getPrimaryKeyDeclarationSQL()'
            );
        }

        return $this->getTableConstraintDeclarationSQL($index, $name);
    }

    /**
     * {@inheritdoc}
     */
    public function getSetTransactionIsolationSQL($level)
    {
        return 'SET TEMPORARY OPTION isolation_level = ' . $this->_getTransactionIsolationLevelSQL($level);
    }

    /**
     * {@inheritdoc}
     */
    public function getSmallIntTypeDeclarationSQL(array $column)
    {
        $column['integer_type'] = 'SMALLINT';

        return $this->_getCommonIntegerTypeDeclarationSQL($column);
    }

    /**
     * Returns the SQL statement for starting an existing database.
     *
     * In SQL Anywhere you can start and stop databases on a
     * database server instance.
     * This is a required statement after having created a new database
     * as it has to be explicitly started to be usable.
     * SQL Anywhere does not automatically start a database after creation!
     *
     * @param string $database Name of the database to start.
     *
     * @return string
     */
    public function getStartDatabaseSQL($database)
    {
        $database = new Identifier($database);

        return "START DATABASE '" . $database->getName() . "' AUTOSTOP OFF";
    }

    /**
     * Returns the SQL statement for stopping a running database.
     *
     * In SQL Anywhere you can start and stop databases on a
     * database server instance.
     * This is a required statement before dropping an existing database
     * as it has to be explicitly stopped before it can be dropped.
     *
     * @param string $database Name of the database to stop.
     *
     * @return string
     */
    public function getStopDatabaseSQL($database)
    {
        $database = new Identifier($database);

        return 'STOP DATABASE "' . $database->getName() . '" UNCONDITIONALLY';
    }

    /**
     * {@inheritdoc}
     */
    public function getSubstringExpression($string, $start, $length = null)
    {
        if ($length === null) {
            return 'SUBSTRING(' . $string . ', ' . $start . ')';
        }

        return 'SUBSTRING(' . $string . ', ' . $start . ', ' . $length . ')';
    }

    /**
     * {@inheritdoc}
     */
    public function getTemporaryTableSQL()
    {
        return 'GLOBAL TEMPORARY';
    }

    /**
     * {@inheritdoc}
     */
    public function getTimeFormatString()
    {
        return 'H:i:s.u';
    }

    /**
     * {@inheritdoc}
     */
    public function getTimeTypeDeclarationSQL(array $column)
    {
        return 'TIME';
    }

    /**
     * {@inheritdoc}
     */
    public function getTrimExpression($str, $mode = TrimMode::UNSPECIFIED, $char = false)
    {
        if (! $char) {
            switch ($mode) {
                case TrimMode::LEADING:
                    return $this->getLtrimExpression($str);

                case TrimMode::TRAILING:
                    return $this->getRtrimExpression($str);

                default:
                    return 'TRIM(' . $str . ')';
            }
        }

        $pattern = "'%[^' + " . $char . " + ']%'";

        switch ($mode) {
            case TrimMode::LEADING:
                return 'SUBSTR(' . $str . ', PATINDEX(' . $pattern . ', ' . $str . '))';

            case TrimMode::TRAILING:
                return 'REVERSE(SUBSTR(REVERSE(' . $str . '), PATINDEX(' . $pattern . ', REVERSE(' . $str . '))))';

            default:
                return 'REVERSE(SUBSTR(REVERSE(SUBSTR(' . $str . ', PATINDEX(' . $pattern . ', ' . $str . '))), ' .
                    'PATINDEX(' . $pattern . ', ' .
                    'REVERSE(SUBSTR(' . $str . ', PATINDEX(' . $pattern . ', ' . $str . '))))))';
        }
    }

    /**
     * {@inheritdoc}
     */
    public function getTruncateTableSQL($tableName, $cascade = false)
    {
        $tableIdentifier = new Identifier($tableName);

        return 'TRUNCATE TABLE ' . $tableIdentifier->getQuotedName($this);
    }

    /**
     * {@inheritdoc}
     */
    public function getUniqueConstraintDeclarationSQL($name, Index $index)
    {
        if ($index->isPrimary()) {
            throw new InvalidArgumentException(
                'Cannot create primary key constraint declarations with getUniqueConstraintDeclarationSQL().'
            );
        }

        if (! $index->isUnique()) {
            throw new InvalidArgumentException(
                'Can only create unique constraint declarations, no common index declarations with ' .
                'getUniqueConstraintDeclarationSQL().'
            );
        }

        return $this->getTableConstraintDeclarationSQL($index, $name);
    }

    /**
     * {@inheritdoc}
     */
    public function getVarcharDefaultLength()
    {
        return 1;
    }

    /**
     * {@inheritdoc}
     */
    public function getVarcharMaxLength()
    {
        return 32767;
    }

    /**
     * {@inheritdoc}
     */
    public function hasNativeGuidType()
    {
        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function prefersIdentityColumns()
    {
        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function supportsCommentOnStatement()
    {
        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function supportsIdentityColumns()
    {
        return true;
    }

    /**
     * {@inheritdoc}
     */
    protected function _getCommonIntegerTypeDeclarationSQL(array $column)
    {
        $unsigned      = ! empty($column['unsigned']) ? 'UNSIGNED ' : '';
        $autoincrement = ! empty($column['autoincrement']) ? ' IDENTITY' : '';

        return $unsigned . $column['integer_type'] . $autoincrement;
    }

    /**
     * {@inheritdoc}
     */
    protected function _getCreateTableSQL($name, array $columns, array $options = [])
    {
        $columnListSql = $this->getColumnDeclarationListSQL($columns);
        $indexSql      = [];

        if (! empty($options['uniqueConstraints'])) {
            foreach ((array) $options['uniqueConstraints'] as $name => $definition) {
                $columnListSql .= ', ' . $this->getUniqueConstraintDeclarationSQL($name, $definition);
            }
        }

        if (! empty($options['indexes'])) {
            foreach ((array) $options['indexes'] as $index) {
                assert($index instanceof Index);
                $indexSql[] = $this->getCreateIndexSQL($index, $name);
            }
        }

        if (! empty($options['primary'])) {
            $flags = '';

            if (isset($options['primary_index']) && $options['primary_index']->hasFlag('clustered')) {
                $flags = ' CLUSTERED ';
            }

            $columnListSql .= ', PRIMARY KEY' . $flags
                . ' (' . implode(', ', array_unique(array_values((array) $options['primary']))) . ')';
        }

        if (! empty($options['foreignKeys'])) {
            foreach ((array) $options['foreignKeys'] as $definition) {
                $columnListSql .= ', ' . $this->getForeignKeyDeclarationSQL($definition);
            }
        }

        $query = 'CREATE TABLE ' . $name . ' (' . $columnListSql;
        $check = $this->getCheckDeclarationSQL($columns);

        if (! empty($check)) {
            $query .= ', ' . $check;
        }

        $query .= ')';

        return array_merge([$query], $indexSql);
    }

    /**
     * {@inheritdoc}
     */
    protected function _getTransactionIsolationLevelSQL($level)
    {
        switch ($level) {
            case TransactionIsolationLevel::READ_UNCOMMITTED:
                return '0';

            case TransactionIsolationLevel::READ_COMMITTED:
                return '1';

            case TransactionIsolationLevel::REPEATABLE_READ:
                return '2';

            case TransactionIsolationLevel::SERIALIZABLE:
                return '3';

            default:
                throw new InvalidArgumentException('Invalid isolation level:' . $level);
        }
    }

    /**
     * {@inheritdoc}
     */
    protected function doModifyLimitQuery($query, $limit, $offset)
    {
        $limitOffsetClause = $this->getTopClauseSQL($limit, $offset);

        if ($limitOffsetClause === '') {
            return $query;
        }

        if (! preg_match('/^\s*(SELECT\s+(DISTINCT\s+)?)(.*)/i', $query, $matches)) {
            return $query;
        }

        return $matches[1] . $limitOffsetClause . ' ' . $matches[3];
    }

    private function getTopClauseSQL(?int $limit, ?int $offset): string
    {
        if ($offset > 0) {
            return sprintf('TOP %s START AT %d', $limit ?? 'ALL', $offset + 1);
        }

        return $limit === null ? '' : 'TOP ' . $limit;
    }

    /**
     * Return the INDEX query section dealing with non-standard
     * SQL Anywhere options.
     *
     * @param Index $index Index definition
     *
     * @return string
     */
    protected function getAdvancedIndexOptionsSQL(Index $index)
    {
        $sql = '';

        if (! $index->isPrimary() && $index->hasFlag('for_olap_workload')) {
            $sql .= ' FOR OLAP WORKLOAD';
        }

        return $sql;
    }

    /**
     * {@inheritdoc}
     */
    protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed)
    {
        return $fixed
            ? 'BINARY(' . ($length ?: $this->getBinaryDefaultLength()) . ')'
            : 'VARBINARY(' . ($length ?: $this->getBinaryDefaultLength()) . ')';
    }

    /**
     * Returns the SQL snippet for creating a table constraint.
     *
     * @param Constraint  $constraint The table constraint to create the SQL snippet for.
     * @param string|null $name       The table constraint name to use if any.
     *
     * @return string
     *
     * @throws InvalidArgumentException If the given table constraint type is not supported by this method.
     */
    protected function getTableConstraintDeclarationSQL(Constraint $constraint, $name = null)
    {
        if ($constraint instanceof ForeignKeyConstraint) {
            return $this->getForeignKeyDeclarationSQL($constraint);
        }

        if (! $constraint instanceof Index) {
            throw new InvalidArgumentException('Unsupported constraint type: ' . get_class($constraint));
        }

        if (! $constraint->isPrimary() && ! $constraint->isUnique()) {
            throw new InvalidArgumentException(
                'Can only create primary, unique or foreign key constraint declarations, no common index declarations'
                    . ' with getTableConstraintDeclarationSQL().'
            );
        }

        $constraintColumns = $constraint->getQuotedColumns($this);

        if (empty($constraintColumns)) {
            throw new InvalidArgumentException("Incomplete definition. 'columns' required.");
        }

        $sql   = '';
        $flags = '';

        if (! empty($name)) {
            $name = new Identifier($name);
            $sql .= 'CONSTRAINT ' . $name->getQuotedName($this) . ' ';
        }

        if ($constraint->hasFlag('clustered')) {
            $flags = 'CLUSTERED ';
        }

        if ($constraint->isPrimary()) {
            return $sql . 'PRIMARY KEY ' . $flags
                . '(' . $this->getIndexFieldDeclarationListSQL($constraintColumns) . ')';
        }

        return $sql . 'UNIQUE ' . $flags . '(' . $this->getIndexFieldDeclarationListSQL($constraintColumns) . ')';
    }

    /**
     * {@inheritdoc}
     */
    protected function getCreateIndexSQLFlags(Index $index)
    {
        $type = '';
        if ($index->hasFlag('virtual')) {
            $type .= 'VIRTUAL ';
        }

        if ($index->isUnique()) {
            $type .= 'UNIQUE ';
        }

        if ($index->hasFlag('clustered')) {
            $type .= 'CLUSTERED ';
        }

        return $type;
    }

    /**
     * {@inheritdoc}
     */
    protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName)
    {
        return ['ALTER INDEX ' . $oldIndexName . ' ON ' . $tableName . ' RENAME TO ' . $index->getQuotedName($this)];
    }

    /**
     * {@inheritdoc}
     */
    protected function getReservedKeywordsClass()
    {
        return Keywords\SQLAnywhereKeywords::class;
    }

    /**
     * {@inheritdoc}
     */
    protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed)
    {
        return $fixed
            ? ($length ? 'CHAR(' . $length . ')' : 'CHAR(' . $this->getVarcharDefaultLength() . ')')
            : ($length ? 'VARCHAR(' . $length . ')' : 'VARCHAR(' . $this->getVarcharDefaultLength() . ')');
    }

    /**
     * {@inheritdoc}
     */
    protected function initializeDoctrineTypeMappings()
    {
        $this->doctrineTypeMapping = [
            'char' => 'string',
            'long nvarchar' => 'text',
            'long varchar' => 'text',
            'nchar' => 'string',
            'ntext' => 'text',
            'nvarchar' => 'string',
            'text' => 'text',
            'uniqueidentifierstr' => 'guid',
            'varchar' => 'string',
            'xml' => 'text',
            'bigint' => 'bigint',
            'unsigned bigint' => 'bigint',
            'bit' => 'boolean',
            'decimal' => 'decimal',
            'double' => 'float',
            'float' => 'float',
            'int' => 'integer',
            'integer' => 'integer',
            'unsigned int' => 'integer',
            'numeric' => 'decimal',
            'smallint' => 'smallint',
            'unsigned smallint' => 'smallint',
            'tinyint' => 'smallint',
            'unsigned tinyint' => 'smallint',
            'money' => 'decimal',
            'smallmoney' => 'decimal',
            'long varbit' => 'text',
            'varbit' => 'string',
            'date' => 'date',
            'datetime' => 'datetime',
            'smalldatetime' => 'datetime',
            'time' => 'time',
            'timestamp' => 'datetime',
            'binary' => 'binary',
            'image' => 'blob',
            'long binary' => 'blob',
            'uniqueidentifier' => 'guid',
            'varbinary' => 'binary',
        ];
    }
}
<?php

namespace Doctrine\DBAL;

use function sprintf;

/**
 * Doctrine\DBAL\ConnectionException
 *
 * @psalm-immutable
 */
class SQLParserUtilsException extends Exception
{
    /**
     * @param string $paramName
     *
     * @return SQLParserUtilsException
     */
    public static function missingParam($paramName)
    {
        return new self(
            sprintf('Value for :%1$s not found in params array. Params array key should be "%1$s"', $paramName)
        );
    }

    /**
     * @param string $typeName
     *
     * @return SQLParserUtilsException
     */
    public static function missingType($typeName)
    {
        return new self(
            sprintf('Value for :%1$s not found in types array. Types array key should be "%1$s"', $typeName)
        );
    }
}
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Abstraction;

use Doctrine\DBAL\Driver\Result as DriverResult;
use Doctrine\DBAL\Exception;
use Traversable;

/**
 * Abstraction-level result statement execution result. Provides additional methods on top
 * of the driver-level interface.
 *
 * @deprecated
 */
interface Result extends DriverResult
{
    /**
     * Returns an iterator over the result set rows represented as numeric arrays.
     *
     * @return Traversable<int,array<int,mixed>>
     *
     * @throws Exception
     */
    public function iterateNumeric(): Traversable;

    /**
     * Returns an iterator over the result set rows represented as associative arrays.
     *
     * @return Traversable<int,array<string,mixed>>
     *
     * @throws Exception
     */
    public function iterateAssociative(): Traversable;

    /**
     * Returns an iterator over the values of the first column of the result set.
     *
     * @return Traversable<int,mixed>
     *
     * @throws Exception
     */
    public function iterateColumn(): Traversable;
}
<?php

namespace Doctrine\DBAL;

use function str_replace;
use function strtoupper;
use function version_compare;

/**
 * Class to store and retrieve the version of Doctrine.
 *
 * @internal
 * @deprecated Refrain from checking the DBAL version at runtime.
 */
class Version
{
    /**
     * Current Doctrine Version.
     */
    public const VERSION = '2.13.9';

    /**
     * Compares a Doctrine version with the current one.
     *
     * @param string $version The Doctrine version to compare to.
     *
     * @return int -1 if older, 0 if it is the same, 1 if version passed as argument is newer.
     */
    public static function compare($version)
    {
        $version = str_replace(' ', '', strtoupper($version));

        return version_compare($version, self::VERSION);
    }
}
<?php

declare(strict_types=1);

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}.
 */
class EventArgs
{
    /**
     * Single instance of EventArgs.
     *
     * @var EventArgs|null
     */
    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).
     *
     * @link https://msdn.microsoft.com/en-us/library/system.eventargs.aspx
     * @see EventManager::dispatchEvent
     *
     * @return EventArgs
     */
    public static function getEmptyInstance()
    {
        if (! self::$_emptyEventArgsInstance) {
            self::$_emptyEventArgsInstance = new EventArgs();
        }

        return self::$_emptyEventArgsInstance;
    }
}
<?php

namespace Doctrine\Common;

use Doctrine\Deprecations\Deprecation;

use function spl_object_hash;

/**
 * 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.
 */
class EventManager
{
    /**
     * Map of registered listeners.
     * <event> => <listeners>
     *
     * @var array<string, object[]>
     */
    private $listeners = [];

    /**
     * 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|null $eventArgs The event arguments to pass to the event handlers/listeners.
     *                                  If not supplied, the single empty EventArgs instance is used.
     *
     * @return void
     */
    public function dispatchEvent($eventName, ?EventArgs $eventArgs = null)
    {
        if (! isset($this->listeners[$eventName])) {
            return;
        }

        $eventArgs = $eventArgs ?? EventArgs::getEmptyInstance();

        foreach ($this->listeners[$eventName] as $listener) {
            $listener->$eventName($eventArgs);
        }
    }

    /**
     * Gets the listeners of a specific event.
     *
     * @param string|null $event The name of the event.
     *
     * @return object[]|array<string, object[]> The event listeners for the specified event, or all event listeners.
     * @psalm-return ($event is null ? array<string, object[]> : object[])
     */
    public function getListeners($event = null)
    {
        if ($event === null) {
            Deprecation::trigger(
                'doctrine/event-manager',
                'https://github.com/doctrine/event-manager/pull/50',
                'Calling %s without an event name is deprecated. Call getAllListeners() instead.',
                __METHOD__
            );

            return $this->getAllListeners();
        }

        return $this->listeners[$event] ?? [];
    }

    /**
     * Gets all listeners keyed by event name.
     *
     * @return array<string, object[]> The event listeners for the specified event, or all event listeners.
     */
    public function getAllListeners(): array
    {
        return $this->listeners;
    }

    /**
     * Checks whether an event has any registered listeners.
     *
     * @param string $event
     *
     * @return bool TRUE if the specified event has any listeners, FALSE otherwise.
     */
    public function hasListeners($event)
    {
        return ! empty($this->listeners[$event]);
    }

    /**
     * Adds an event listener that listens on the specified events.
     *
     * @param string|string[] $events   The event(s) to listen on.
     * @param object          $listener The listener object.
     *
     * @return void
     */
    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|string[] $events
     * @param object          $listener
     *
     * @return void
     */
    public function removeEventListener($events, $listener)
    {
        // Picks the hash code related to that listener
        $hash = spl_object_hash($listener);

        foreach ((array) $events as $event) {
            unset($this->listeners[$event][$hash]);
        }
    }

    /**
     * Adds an EventSubscriber. The subscriber is asked for all the events it is
     * interested in and added as a listener for these events.
     *
     * @param EventSubscriber $subscriber The subscriber.
     *
     * @return void
     */
    public function addEventSubscriber(EventSubscriber $subscriber)
    {
        $this->addEventListener($subscriber->getSubscribedEvents(), $subscriber);
    }

    /**
     * Removes an EventSubscriber. The subscriber is asked for all the events it is
     * interested in and removed as a listener for these events.
     *
     * @param EventSubscriber $subscriber The subscriber.
     *
     * @return void
     */
    public function removeEventSubscriber(EventSubscriber $subscriber)
    {
        $this->removeEventListener($subscriber->getSubscribedEvents(), $subscriber);
    }
}
<?php

declare(strict_types=1);

namespace Doctrine\Common;

/**
 * An EventSubscriber knows what events it 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.
 */
interface EventSubscriber
{
    /**
     * Returns an array of events this subscriber wants to listen to.
     *
     * @return string[]
     */
    public function getSubscribedEvents();
}
<?php

declare(strict_types=1);

namespace Doctrine\Deprecations;

use Psr\Log\LoggerInterface;

use function array_key_exists;
use function array_reduce;
use function assert;
use function debug_backtrace;
use function sprintf;
use function str_replace;
use function strpos;
use function strrpos;
use function substr;
use function trigger_error;

use const DEBUG_BACKTRACE_IGNORE_ARGS;
use const DIRECTORY_SEPARATOR;
use const E_USER_DEPRECATED;

/**
 * Manages Deprecation logging in different ways.
 *
 * By default triggered exceptions are not logged.
 *
 * To enable different deprecation logging mechanisms you can call the
 * following methods:
 *
 *  - Minimal collection of deprecations via getTriggeredDeprecations()
 *    \Doctrine\Deprecations\Deprecation::enableTrackingDeprecations();
 *
 *  - Uses @trigger_error with E_USER_DEPRECATED
 *    \Doctrine\Deprecations\Deprecation::enableWithTriggerError();
 *
 *  - Sends deprecation messages via a PSR-3 logger
 *    \Doctrine\Deprecations\Deprecation::enableWithPsrLogger($logger);
 *
 * Packages that trigger deprecations should use the `trigger()` or
 * `triggerIfCalledFromOutside()` methods.
 */
class Deprecation
{
    private const TYPE_NONE               = 0;
    private const TYPE_TRACK_DEPRECATIONS = 1;
    private const TYPE_TRIGGER_ERROR      = 2;
    private const TYPE_PSR_LOGGER         = 4;

    /** @var int-mask-of<self::TYPE_*>|null */
    private static $type;

    /** @var LoggerInterface|null */
    private static $logger;

    /** @var array<string,bool> */
    private static $ignoredPackages = [];

    /** @var array<string,int> */
    private static $triggeredDeprecations = [];

    /** @var array<string,bool> */
    private static $ignoredLinks = [];

    /** @var bool */
    private static $deduplication = true;

    /**
     * Trigger a deprecation for the given package and identfier.
     *
     * The link should point to a Github issue or Wiki entry detailing the
     * deprecation. It is additionally used to de-duplicate the trigger of the
     * same deprecation during a request.
     *
     * @param float|int|string $args
     */
    public static function trigger(string $package, string $link, string $message, ...$args): void
    {
        $type = self::$type ?? self::getTypeFromEnv();

        if ($type === self::TYPE_NONE) {
            return;
        }

        if (isset(self::$ignoredLinks[$link])) {
            return;
        }

        if (array_key_exists($link, self::$triggeredDeprecations)) {
            self::$triggeredDeprecations[$link]++;
        } else {
            self::$triggeredDeprecations[$link] = 1;
        }

        if (self::$deduplication === true && self::$triggeredDeprecations[$link] > 1) {
            return;
        }

        if (isset(self::$ignoredPackages[$package])) {
            return;
        }

        $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);

        $message = sprintf($message, ...$args);

        self::delegateTriggerToBackend($message, $backtrace, $link, $package);
    }

    /**
     * Trigger a deprecation for the given package and identifier when called from outside.
     *
     * "Outside" means we assume that $package is currently installed as a
     * dependency and the caller is not a file in that package. When $package
     * is installed as a root package then deprecations triggered from the
     * tests folder are also considered "outside".
     *
     * This deprecation method assumes that you are using Composer to install
     * the dependency and are using the default /vendor/ folder and not a
     * Composer plugin to change the install location. The assumption is also
     * that $package is the exact composer packge name.
     *
     * Compared to {@link trigger()} this method causes some overhead when
     * deprecation tracking is enabled even during deduplication, because it
     * needs to call {@link debug_backtrace()}
     *
     * @param float|int|string $args
     */
    public static function triggerIfCalledFromOutside(string $package, string $link, string $message, ...$args): void
    {
        $type = self::$type ?? self::getTypeFromEnv();

        if ($type === self::TYPE_NONE) {
            return;
        }

        $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);

        // first check that the caller is not from a tests folder, in which case we always let deprecations pass
        if (isset($backtrace[1]['file'], $backtrace[0]['file']) && strpos($backtrace[1]['file'], DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR) === false) {
            $path = DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $package) . DIRECTORY_SEPARATOR;

            if (strpos($backtrace[0]['file'], $path) === false) {
                return;
            }

            if (strpos($backtrace[1]['file'], $path) !== false) {
                return;
            }
        }

        if (isset(self::$ignoredLinks[$link])) {
            return;
        }

        if (array_key_exists($link, self::$triggeredDeprecations)) {
            self::$triggeredDeprecations[$link]++;
        } else {
            self::$triggeredDeprecations[$link] = 1;
        }

        if (self::$deduplication === true && self::$triggeredDeprecations[$link] > 1) {
            return;
        }

        if (isset(self::$ignoredPackages[$package])) {
            return;
        }

        $message = sprintf($message, ...$args);

        self::delegateTriggerToBackend($message, $backtrace, $link, $package);
    }

    /** @param list<array{function: string, line?: int, file?: string, class?: class-string, type?: string, args?: mixed[], object?: object}> $backtrace */
    private static function delegateTriggerToBackend(string $message, array $backtrace, string $link, string $package): void
    {
        $type = self::$type ?? self::getTypeFromEnv();

        if (($type & self::TYPE_PSR_LOGGER) > 0) {
            $context = [
                'file' => $backtrace[0]['file'] ?? null,
                'line' => $backtrace[0]['line'] ?? null,
                'package' => $package,
                'link' => $link,
            ];

            assert(self::$logger !== null);

            self::$logger->notice($message, $context);
        }

        if (! (($type & self::TYPE_TRIGGER_ERROR) > 0)) {
            return;
        }

        $message .= sprintf(
            ' (%s:%d called by %s:%d, %s, package %s)',
            self::basename($backtrace[0]['file'] ?? 'native code'),
            $backtrace[0]['line'] ?? 0,
            self::basename($backtrace[1]['file'] ?? 'native code'),
            $backtrace[1]['line'] ?? 0,
            $link,
            $package
        );

        @trigger_error($message, E_USER_DEPRECATED);
    }

    /**
     * A non-local-aware version of PHPs basename function.
     */
    private static function basename(string $filename): string
    {
        $pos = strrpos($filename, DIRECTORY_SEPARATOR);

        if ($pos === false) {
            return $filename;
        }

        return substr($filename, $pos + 1);
    }

    public static function enableTrackingDeprecations(): void
    {
        self::$type  = self::$type ?? self::getTypeFromEnv();
        self::$type |= self::TYPE_TRACK_DEPRECATIONS;
    }

    public static function enableWithTriggerError(): void
    {
        self::$type  = self::$type ?? self::getTypeFromEnv();
        self::$type |= self::TYPE_TRIGGER_ERROR;
    }

    public static function enableWithPsrLogger(LoggerInterface $logger): void
    {
        self::$type   = self::$type ?? self::getTypeFromEnv();
        self::$type  |= self::TYPE_PSR_LOGGER;
        self::$logger = $logger;
    }

    public static function withoutDeduplication(): void
    {
        self::$deduplication = false;
    }

    public static function disable(): void
    {
        self::$type          = self::TYPE_NONE;
        self::$logger        = null;
        self::$deduplication = true;
        self::$ignoredLinks  = [];

        foreach (self::$triggeredDeprecations as $link => $count) {
            self::$triggeredDeprecations[$link] = 0;
        }
    }

    public static function ignorePackage(string $packageName): void
    {
        self::$ignoredPackages[$packageName] = true;
    }

    public static function ignoreDeprecations(string ...$links): void
    {
        foreach ($links as $link) {
            self::$ignoredLinks[$link] = true;
        }
    }

    public static function getUniqueTriggeredDeprecationsCount(): int
    {
        return array_reduce(self::$triggeredDeprecations, static function (int $carry, int $count) {
            return $carry + $count;
        }, 0);
    }

    /**
     * Returns each triggered deprecation link identifier and the amount of occurrences.
     *
     * @return array<string,int>
     */
    public static function getTriggeredDeprecations(): array
    {
        return self::$triggeredDeprecations;
    }

    /** @return int-mask-of<self::TYPE_*> */
    private static function getTypeFromEnv(): int
    {
        switch ($_SERVER['DOCTRINE_DEPRECATIONS'] ?? $_ENV['DOCTRINE_DEPRECATIONS'] ?? null) {
            case 'trigger':
                self::$type = self::TYPE_TRIGGER_ERROR;
                break;

            case 'track':
                self::$type = self::TYPE_TRACK_DEPRECATIONS;
                break;

            default:
                self::$type = self::TYPE_NONE;
                break;
        }

        return self::$type;
    }
}
<?php

declare(strict_types=1);

namespace Doctrine\Deprecations\PHPUnit;

use Doctrine\Deprecations\Deprecation;
use PHPUnit\Framework\Attributes\After;
use PHPUnit\Framework\Attributes\Before;

use function sprintf;

trait VerifyDeprecations
{
    /** @var array<string,int> */
    private $doctrineDeprecationsExpectations = [];

    /** @var array<string,int> */
    private $doctrineNoDeprecationsExpectations = [];

    public function expectDeprecationWithIdentifier(string $identifier): void
    {
        $this->doctrineDeprecationsExpectations[$identifier] = Deprecation::getTriggeredDeprecations()[$identifier] ?? 0;
    }

    public function expectNoDeprecationWithIdentifier(string $identifier): void
    {
        $this->doctrineNoDeprecationsExpectations[$identifier] = Deprecation::getTriggeredDeprecations()[$identifier] ?? 0;
    }

    /** @before */
    #[Before]
    public function enableDeprecationTracking(): void
    {
        Deprecation::enableTrackingDeprecations();
    }

    /** @after */
    #[After]
    public function verifyDeprecationsAreTriggered(): void
    {
        foreach ($this->doctrineDeprecationsExpectations as $identifier => $expectation) {
            $actualCount = Deprecation::getTriggeredDeprecations()[$identifier] ?? 0;

            $this->assertTrue(
                $actualCount > $expectation,
                sprintf(
                    "Expected deprecation with identifier '%s' was not triggered by code executed in test.",
                    $identifier
                )
            );
        }

        foreach ($this->doctrineNoDeprecationsExpectations as $identifier => $expectation) {
            $actualCount = Deprecation::getTriggeredDeprecations()[$identifier] ?? 0;

            $this->assertTrue(
                $actualCount === $expectation,
                sprintf(
                    "Deprecation with identifier '%s' was triggered by code executed in test, but expected not to.",
                    $identifier
                )
            );
        }
    }
}
<?php

namespace Doctrine\Common\Cache;

/**
 * Interface for cache that can be flushed.
 *
 * Intended to be used for partial clearing of a cache namespace. For a more
 * global "flushing", see {@see FlushableCache}.
 *
 * @link   www.doctrine-project.org
 */
interface ClearableCache
{
    /**
     * Deletes all cache entries in the current cache namespace.
     *
     * @return bool TRUE if the cache entries were successfully deleted, FALSE otherwise.
     */
    public function deleteAll();
}
<?php

namespace Doctrine\Common\Cache;

/**
 * Interface for cache drivers that allows to get many items at once.
 *
 * @deprecated
 *
 * @link   www.doctrine-project.org
 */
interface MultiGetCache
{
    /**
     * Returns an associative array of values for keys is found in cache.
     *
     * @param string[] $keys Array of keys to retrieve from cache
     *
     * @return mixed[] Array of retrieved values, indexed by the specified keys.
     *                 Values that couldn't be retrieved are not contained in this array.
     */
    public function fetchMultiple(array $keys);
}
<?php

namespace Doctrine\Common\Cache;

/**
 * Interface for cache drivers.
 *
 * @link   www.doctrine-project.org
 */
interface Cache
{
    public const STATS_HITS             = 'hits';
    public const STATS_MISSES           = 'misses';
    public const STATS_UPTIME           = 'uptime';
    public const STATS_MEMORY_USAGE     = 'memory_usage';
    public const STATS_MEMORY_AVAILABLE = 'memory_available';
    /**
     * Only for backward compatibility (may be removed in next major release)
     *
     * @deprecated
     */
    public const STATS_MEMORY_AVAILIABLE = 'memory_available';

    /**
     * Fetches an entry from the cache.
     *
     * @param string $id The id of the cache entry to fetch.
     *
     * @return mixed The cached data or FALSE, if no cache entry exists for the given id.
     */
    public function fetch($id);

    /**
     * Tests if an entry exists in the cache.
     *
     * @param string $id The cache id of the entry to check for.
     *
     * @return bool TRUE if a cache entry exists for the given cache id, FALSE otherwise.
     */
    public function contains($id);

    /**
     * Puts data into the cache.
     *
     * If a cache entry with the given id already exists, its data will be replaced.
     *
     * @param string $id       The cache id.
     * @param mixed  $data     The cache entry/data.
     * @param int    $lifeTime The lifetime in number of seconds for this cache entry.
     *                         If zero (the default), the entry never expires (although it may be deleted from the cache
     *                         to make place for other entries).
     *
     * @return bool TRUE if the entry was successfully stored in the cache, FALSE otherwise.
     */
    public function save($id, $data, $lifeTime = 0);

    /**
     * Deletes a cache entry.
     *
     * @param string $id The cache id.
     *
     * @return bool TRUE if the cache entry was successfully deleted, FALSE otherwise.
     *              Deleting a non-existing entry is considered successful.
     */
    public function delete($id);

    /**
     * Retrieves cached information from the data store.
     *
     * The server's statistics array has the following values:
     *
     * - <b>hits</b>
     * Number of keys that have been requested and found present.
     *
     * - <b>misses</b>
     * Number of items that have been requested and not found.
     *
     * - <b>uptime</b>
     * Time that the server is running.
     *
     * - <b>memory_usage</b>
     * Memory used by this server to store items.
     *
     * - <b>memory_available</b>
     * Memory allowed to use for storage.
     *
     * @return mixed[]|null An associative array with server's statistics if available, NULL otherwise.
     */
    public function getStats();
}
<?php

namespace Doctrine\Common\Cache;

use function array_combine;
use function array_key_exists;
use function array_map;
use function sprintf;

/**
 * Base class for cache provider implementations.
 */
abstract class CacheProvider implements Cache, FlushableCache, ClearableCache, MultiOperationCache
{
    public const DOCTRINE_NAMESPACE_CACHEKEY = 'DoctrineNamespaceCacheKey[%s]';

    /**
     * The namespace to prefix all cache ids with.
     *
     * @var string
     */
    private $namespace = '';

    /**
     * The namespace version.
     *
     * @var int|null
     */
    private $namespaceVersion;

    /**
     * Sets the namespace to prefix all cache ids with.
     *
     * @param string $namespace
     *
     * @return void
     */
    public function setNamespace($namespace)
    {
        $this->namespace        = (string) $namespace;
        $this->namespaceVersion = null;
    }

    /**
     * Retrieves the namespace that prefixes all cache ids.
     *
     * @return string
     */
    public function getNamespace()
    {
        return $this->namespace;
    }

    /**
     * {@inheritdoc}
     */
    public function fetch($id)
    {
        return $this->doFetch($this->getNamespacedId($id));
    }

    /**
     * {@inheritdoc}
     */
    public function fetchMultiple(array $keys)
    {
        if (empty($keys)) {
            return [];
        }

        // note: the array_combine() is in place to keep an association between our $keys and the $namespacedKeys
        $namespacedKeys = array_combine($keys, array_map([$this, 'getNamespacedId'], $keys));
        $items          = $this->doFetchMultiple($namespacedKeys);
        $foundItems     = [];

        // no internal array function supports this sort of mapping: needs to be iterative
        // this filters and combines keys in one pass
        foreach ($namespacedKeys as $requestedKey => $namespacedKey) {
            if (! isset($items[$namespacedKey]) && ! array_key_exists($namespacedKey, $items)) {
                continue;
            }

            $foundItems[$requestedKey] = $items[$namespacedKey];
        }

        return $foundItems;
    }

    /**
     * {@inheritdoc}
     */
    public function saveMultiple(array $keysAndValues, $lifetime = 0)
    {
        $namespacedKeysAndValues = [];
        foreach ($keysAndValues as $key => $value) {
            $namespacedKeysAndValues[$this->getNamespacedId($key)] = $value;
        }

        return $this->doSaveMultiple($namespacedKeysAndValues, $lifetime);
    }

    /**
     * {@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 deleteMultiple(array $keys)
    {
        return $this->doDeleteMultiple(array_map([$this, 'getNamespacedId'], $keys));
    }

    /**
     * {@inheritdoc}
     */
    public function delete($id)
    {
        return $this->doDelete($this->getNamespacedId($id));
    }

    /**
     * {@inheritdoc}
     */
    public function getStats()
    {
        return $this->doGetStats();
    }

    /**
     * {@inheritDoc}
     */
    public function flushAll()
    {
        return $this->doFlush();
    }

    /**
     * {@inheritDoc}
     */
    public function deleteAll()
    {
        $namespaceCacheKey = $this->getNamespaceCacheKey();
        $namespaceVersion  = $this->getNamespaceVersion() + 1;

        if ($this->doSave($namespaceCacheKey, $namespaceVersion)) {
            $this->namespaceVersion = $namespaceVersion;

            return true;
        }

        return false;
    }

    /**
     * Prefixes the passed id with the configured namespace value.
     *
     * @param string $id The id to namespace.
     *
     * @return string The namespaced id.
     */
    private function getNamespacedId(string $id): string
    {
        $namespaceVersion = $this->getNamespaceVersion();

        return sprintf('%s[%s][%s]', $this->namespace, $id, $namespaceVersion);
    }

    /**
     * Returns the namespace cache key.
     */
    private function getNamespaceCacheKey(): string
    {
        return sprintf(self::DOCTRINE_NAMESPACE_CACHEKEY, $this->namespace);
    }

    /**
     * Returns the namespace version.
     */
    private function getNamespaceVersion(): int
    {
        if ($this->namespaceVersion !== null) {
            return $this->namespaceVersion;
        }

        $namespaceCacheKey      = $this->getNamespaceCacheKey();
        $this->namespaceVersion = (int) $this->doFetch($namespaceCacheKey) ?: 1;

        return $this->namespaceVersion;
    }

    /**
     * Default implementation of doFetchMultiple. Each driver that supports multi-get should owerwrite it.
     *
     * @param string[] $keys Array of keys to retrieve from cache
     *
     * @return mixed[] Array of values retrieved for the given keys.
     */
    protected function doFetchMultiple(array $keys)
    {
        $returnValues = [];

        foreach ($keys as $key) {
            $item = $this->doFetch($key);
            if ($item === false && ! $this->doContains($key)) {
                continue;
            }

            $returnValues[$key] = $item;
        }

        return $returnValues;
    }

    /**
     * Fetches an entry from the cache.
     *
     * @param string $id The id of the cache entry to fetch.
     *
     * @return mixed|false The cached data or FALSE, if no cache entry exists for the given id.
     */
    abstract protected function doFetch($id);

    /**
     * Tests if an entry exists in the cache.
     *
     * @param string $id The cache id of the entry to check for.
     *
     * @return bool TRUE if a cache entry exists for the given cache id, FALSE otherwise.
     */
    abstract protected function doContains($id);

    /**
     * Default implementation of doSaveMultiple. Each driver that supports multi-put should override it.
     *
     * @param mixed[] $keysAndValues Array of keys and values to save in cache
     * @param int     $lifetime      The lifetime. If != 0, sets a specific lifetime for these
     *                               cache entries (0 => infinite lifeTime).
     *
     * @return bool TRUE if the operation was successful, FALSE if it wasn't.
     */
    protected function doSaveMultiple(array $keysAndValues, $lifetime = 0)
    {
        $success = true;

        foreach ($keysAndValues as $key => $value) {
            if ($this->doSave($key, $value, $lifetime)) {
                continue;
            }

            $success = false;
        }

        return $success;
    }

    /**
     * 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 bool TRUE if the entry was successfully stored in the cache, FALSE otherwise.
     */
    abstract protected function doSave($id, $data, $lifeTime = 0);

    /**
     * Default implementation of doDeleteMultiple. Each driver that supports multi-delete should override it.
     *
     * @param string[] $keys Array of keys to delete from cache
     *
     * @return bool TRUE if the operation was successful, FALSE if it wasn't
     */
    protected function doDeleteMultiple(array $keys)
    {
        $success = true;

        foreach ($keys as $key) {
            if ($this->doDelete($key)) {
                continue;
            }

            $success = false;
        }

        return $success;
    }

    /**
     * Deletes a cache entry.
     *
     * @param string $id The cache id.
     *
     * @return bool TRUE if the cache entry was successfully deleted, FALSE otherwise.
     */
    abstract protected function doDelete($id);

    /**
     * Flushes all cache entries.
     *
     * @return bool TRUE if the cache entries were successfully flushed, FALSE otherwise.
     */
    abstract protected function doFlush();

    /**
     * Retrieves cached information from the data store.
     *
     * @return mixed[]|null An associative array with server's statistics if available, NULL otherwise.
     */
    abstract protected function doGetStats();
}
<?php

namespace Doctrine\Common\Cache;

/**
 * Interface for cache drivers that allows to put many items at once.
 *
 * @deprecated
 *
 * @link   www.doctrine-project.org
 */
interface MultiPutCache
{
    /**
     * Returns a boolean value indicating if the operation succeeded.
     *
     * @param mixed[] $keysAndValues Array of keys and values to save in cache
     * @param int     $lifetime      The lifetime. If != 0, sets a specific lifetime for these
     *                               cache entries (0 => infinite lifeTime).
     *
     * @return bool TRUE if the operation was successful, FALSE if it wasn't.
     */
    public function saveMultiple(array $keysAndValues, $lifetime = 0);
}
<?php

namespace Doctrine\Common\Cache;

/**
 * Interface for cache that can be flushed.
 *
 * @link   www.doctrine-project.org
 */
interface FlushableCache
{
    /**
     * Flushes all cache entries, globally.
     *
     * @return bool TRUE if the cache entries were successfully flushed, FALSE otherwise.
     */
    public function flushAll();
}
<?php

namespace Doctrine\Common\Cache;

/**
 * Interface for cache drivers that allows to put many items at once.
 *
 * @deprecated
 *
 * @link   www.doctrine-project.org
 */
interface MultiDeleteCache
{
    /**
     * Deletes several cache entries.
     *
     * @param string[] $keys Array of keys to delete from cache
     *
     * @return bool TRUE if the operation was successful, FALSE if it wasn't.
     */
    public function deleteMultiple(array $keys);
}
<?php

namespace Doctrine\Common\Cache\Psr6;

use InvalidArgumentException;
use Psr\Cache\InvalidArgumentException as PsrInvalidArgumentException;

/**
 * @internal
 */
final class InvalidArgument extends InvalidArgumentException implements PsrInvalidArgumentException
{
}
<?php

namespace Doctrine\Common\Cache\Psr6;

use Doctrine\Common\Cache\Cache;
use Doctrine\Common\Cache\ClearableCache;
use Doctrine\Common\Cache\MultiDeleteCache;
use Doctrine\Common\Cache\MultiGetCache;
use Doctrine\Common\Cache\MultiPutCache;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Cache\DoctrineProvider as SymfonyDoctrineProvider;

use function array_key_exists;
use function assert;
use function count;
use function current;
use function get_class;
use function gettype;
use function is_object;
use function is_string;
use function microtime;
use function sprintf;
use function strpbrk;

use const PHP_VERSION_ID;

final class CacheAdapter implements CacheItemPoolInterface
{
    private const RESERVED_CHARACTERS = '{}()/\@:';

    /** @var Cache */
    private $cache;

    /** @var array<CacheItem|TypedCacheItem> */
    private $deferredItems = [];

    public static function wrap(Cache $cache): CacheItemPoolInterface
    {
        if ($cache instanceof DoctrineProvider && ! $cache->getNamespace()) {
            return $cache->getPool();
        }

        if ($cache instanceof SymfonyDoctrineProvider && ! $cache->getNamespace()) {
            $getPool = function () {
                // phpcs:ignore Squiz.Scope.StaticThisUsage.Found
                return $this->pool;
            };

            return $getPool->bindTo($cache, SymfonyDoctrineProvider::class)();
        }

        return new self($cache);
    }

    private function __construct(Cache $cache)
    {
        $this->cache = $cache;
    }

    /** @internal */
    public function getCache(): Cache
    {
        return $this->cache;
    }

    /**
     * {@inheritDoc}
     */
    public function getItem($key): CacheItemInterface
    {
        assert(self::validKey($key));

        if (isset($this->deferredItems[$key])) {
            $this->commit();
        }

        $value = $this->cache->fetch($key);

        if (PHP_VERSION_ID >= 80000) {
            if ($value !== false) {
                return new TypedCacheItem($key, $value, true);
            }

            return new TypedCacheItem($key, null, false);
        }

        if ($value !== false) {
            return new CacheItem($key, $value, true);
        }

        return new CacheItem($key, null, false);
    }

    /**
     * {@inheritDoc}
     */
    public function getItems(array $keys = []): array
    {
        if ($this->deferredItems) {
            $this->commit();
        }

        assert(self::validKeys($keys));

        $values = $this->doFetchMultiple($keys);
        $items  = [];

        if (PHP_VERSION_ID >= 80000) {
            foreach ($keys as $key) {
                if (array_key_exists($key, $values)) {
                    $items[$key] = new TypedCacheItem($key, $values[$key], true);
                } else {
                    $items[$key] = new TypedCacheItem($key, null, false);
                }
            }

            return $items;
        }

        foreach ($keys as $key) {
            if (array_key_exists($key, $values)) {
                $items[$key] = new CacheItem($key, $values[$key], true);
            } else {
                $items[$key] = new CacheItem($key, null, false);
            }
        }

        return $items;
    }

    /**
     * {@inheritDoc}
     */
    public function hasItem($key): bool
    {
        assert(self::validKey($key));

        if (isset($this->deferredItems[$key])) {
            $this->commit();
        }

        return $this->cache->contains($key);
    }

    public function clear(): bool
    {
        $this->deferredItems = [];

        if (! $this->cache instanceof ClearableCache) {
            return false;
        }

        return $this->cache->deleteAll();
    }

    /**
     * {@inheritDoc}
     */
    public function deleteItem($key): bool
    {
        assert(self::validKey($key));
        unset($this->deferredItems[$key]);

        return $this->cache->delete($key);
    }

    /**
     * {@inheritDoc}
     */
    public function deleteItems(array $keys): bool
    {
        foreach ($keys as $key) {
            assert(self::validKey($key));
            unset($this->deferredItems[$key]);
        }

        return $this->doDeleteMultiple($keys);
    }

    public function save(CacheItemInterface $item): bool
    {
        return $this->saveDeferred($item) && $this->commit();
    }

    public function saveDeferred(CacheItemInterface $item): bool
    {
        if (! $item instanceof CacheItem && ! $item instanceof TypedCacheItem) {
            return false;
        }

        $this->deferredItems[$item->getKey()] = $item;

        return true;
    }

    public function commit(): bool
    {
        if (! $this->deferredItems) {
            return true;
        }

        $now         = microtime(true);
        $itemsCount  = 0;
        $byLifetime  = [];
        $expiredKeys = [];

        foreach ($this->deferredItems as $key => $item) {
            $lifetime = ($item->getExpiry() ?? $now) - $now;

            if ($lifetime < 0) {
                $expiredKeys[] = $key;

                continue;
            }

            ++$itemsCount;
            $byLifetime[(int) $lifetime][$key] = $item->get();
        }

        $this->deferredItems = [];

        switch (count($expiredKeys)) {
            case 0:
                break;
            case 1:
                $this->cache->delete(current($expiredKeys));
                break;
            default:
                $this->doDeleteMultiple($expiredKeys);
                break;
        }

        if ($itemsCount === 1) {
            return $this->cache->save($key, $item->get(), (int) $lifetime);
        }

        $success = true;
        foreach ($byLifetime as $lifetime => $values) {
            $success = $this->doSaveMultiple($values, $lifetime) && $success;
        }

        return $success;
    }

    public function __destruct()
    {
        $this->commit();
    }

    /**
     * @param mixed $key
     */
    private static function validKey($key): bool
    {
        if (! is_string($key)) {
            throw new InvalidArgument(sprintf('Cache key must be string, "%s" given.', is_object($key) ? get_class($key) : gettype($key)));
        }

        if ($key === '') {
            throw new InvalidArgument('Cache key length must be greater than zero.');
        }

        if (strpbrk($key, self::RESERVED_CHARACTERS) !== false) {
            throw new InvalidArgument(sprintf('Cache key "%s" contains reserved characters "%s".', $key, self::RESERVED_CHARACTERS));
        }

        return true;
    }

    /**
     * @param mixed[] $keys
     */
    private static function validKeys(array $keys): bool
    {
        foreach ($keys as $key) {
            self::validKey($key);
        }

        return true;
    }

    /**
     * @param mixed[] $keys
     */
    private function doDeleteMultiple(array $keys): bool
    {
        if ($this->cache instanceof MultiDeleteCache) {
            return $this->cache->deleteMultiple($keys);
        }

        $success = true;
        foreach ($keys as $key) {
            $success = $this->cache->delete($key) && $success;
        }

        return $success;
    }

    /**
     * @param mixed[] $keys
     *
     * @return mixed[]
     */
    private function doFetchMultiple(array $keys): array
    {
        if ($this->cache instanceof MultiGetCache) {
            return $this->cache->fetchMultiple($keys);
        }

        $values = [];
        foreach ($keys as $key) {
            $value = $this->cache->fetch($key);
            if (! $value) {
                continue;
            }

            $values[$key] = $value;
        }

        return $values;
    }

    /**
     * @param mixed[] $keysAndValues
     */
    private function doSaveMultiple(array $keysAndValues, int $lifetime = 0): bool
    {
        if ($this->cache instanceof MultiPutCache) {
            return $this->cache->saveMultiple($keysAndValues, $lifetime);
        }

        $success = true;
        foreach ($keysAndValues as $key => $value) {
            $success = $this->cache->save($key, $value, $lifetime) && $success;
        }

        return $success;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Doctrine\Common\Cache\Psr6;

use Doctrine\Common\Cache\Cache;
use Doctrine\Common\Cache\CacheProvider;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Cache\Adapter\DoctrineAdapter as SymfonyDoctrineAdapter;
use Symfony\Contracts\Service\ResetInterface;

use function rawurlencode;

/**
 * This class was copied from the Symfony Framework, see the original copyright
 * notice above. The code is distributed subject to the license terms in
 * https://github.com/symfony/symfony/blob/ff0cf61278982539c49e467db9ab13cbd342f76d/LICENSE
 */
final class DoctrineProvider extends CacheProvider
{
    /** @var CacheItemPoolInterface */
    private $pool;

    public static function wrap(CacheItemPoolInterface $pool): Cache
    {
        if ($pool instanceof CacheAdapter) {
            return $pool->getCache();
        }

        if ($pool instanceof SymfonyDoctrineAdapter) {
            $getCache = function () {
                // phpcs:ignore Squiz.Scope.StaticThisUsage.Found
                return $this->provider;
            };

            return $getCache->bindTo($pool, SymfonyDoctrineAdapter::class)();
        }

        return new self($pool);
    }

    private function __construct(CacheItemPoolInterface $pool)
    {
        $this->pool = $pool;
    }

    /** @internal */
    public function getPool(): CacheItemPoolInterface
    {
        return $this->pool;
    }

    public function reset(): void
    {
        if ($this->pool instanceof ResetInterface) {
            $this->pool->reset();
        }

        $this->setNamespace($this->getNamespace());
    }

    /**
     * {@inheritdoc}
     */
    protected function doFetch($id)
    {
        $item = $this->pool->getItem(rawurlencode($id));

        return $item->isHit() ? $item->get() : false;
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    protected function doContains($id)
    {
        return $this->pool->hasItem(rawurlencode($id));
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    protected function doSave($id, $data, $lifeTime = 0)
    {
        $item = $this->pool->getItem(rawurlencode($id));

        if (0 < $lifeTime) {
            $item->expiresAfter($lifeTime);
        }

        return $this->pool->save($item->set($data));
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    protected function doDelete($id)
    {
        return $this->pool->deleteItem(rawurlencode($id));
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    protected function doFlush()
    {
        return $this->pool->clear();
    }

    /**
     * {@inheritdoc}
     *
     * @return array|null
     */
    protected function doGetStats()
    {
        return null;
    }
}
<?php

namespace Doctrine\Common\Cache\Psr6;

use DateInterval;
use DateTime;
use DateTimeInterface;
use Psr\Cache\CacheItemInterface;
use TypeError;

use function get_class;
use function gettype;
use function is_int;
use function is_object;
use function microtime;
use function sprintf;

final class CacheItem implements CacheItemInterface
{
    /** @var string */
    private $key;
    /** @var mixed */
    private $value;
    /** @var bool */
    private $isHit;
    /** @var float|null */
    private $expiry;

    /**
     * @internal
     *
     * @param mixed $data
     */
    public function __construct(string $key, $data, bool $isHit)
    {
        $this->key   = $key;
        $this->value = $data;
        $this->isHit = $isHit;
    }

    public function getKey(): string
    {
        return $this->key;
    }

    /**
     * {@inheritDoc}
     *
     * @return mixed
     */
    public function get()
    {
        return $this->value;
    }

    public function isHit(): bool
    {
        return $this->isHit;
    }

    /**
     * {@inheritDoc}
     */
    public function set($value): self
    {
        $this->value = $value;

        return $this;
    }

    /**
     * {@inheritDoc}
     */
    public function expiresAt($expiration): self
    {
        if ($expiration === null) {
            $this->expiry = null;
        } elseif ($expiration instanceof DateTimeInterface) {
            $this->expiry = (float) $expiration->format('U.u');
        } else {
            throw new TypeError(sprintf(
                'Expected $expiration to be an instance of DateTimeInterface or null, got %s',
                is_object($expiration) ? get_class($expiration) : gettype($expiration)
            ));
        }

        return $this;
    }

    /**
     * {@inheritDoc}
     */
    public function expiresAfter($time): self
    {
        if ($time === null) {
            $this->expiry = null;
        } elseif ($time instanceof DateInterval) {
            $this->expiry = microtime(true) + DateTime::createFromFormat('U', 0)->add($time)->format('U.u');
        } elseif (is_int($time)) {
            $this->expiry = $time + microtime(true);
        } else {
            throw new TypeError(sprintf(
                'Expected $time to be either an integer, an instance of DateInterval or null, got %s',
                is_object($time) ? get_class($time) : gettype($time)
            ));
        }

        return $this;
    }

    /**
     * @internal
     */
    public function getExpiry(): ?float
    {
        return $this->expiry;
    }
}
<?php

namespace Doctrine\Common\Cache\Psr6;

use DateInterval;
use DateTime;
use DateTimeInterface;
use Psr\Cache\CacheItemInterface;
use TypeError;

use function get_debug_type;
use function is_int;
use function microtime;
use function sprintf;

final class TypedCacheItem implements CacheItemInterface
{
    private ?float $expiry = null;

    /**
     * @internal
     */
    public function __construct(
        private string $key,
        private mixed $value,
        private bool $isHit,
    ) {
    }

    public function getKey(): string
    {
        return $this->key;
    }

    public function get(): mixed
    {
        return $this->value;
    }

    public function isHit(): bool
    {
        return $this->isHit;
    }

    public function set(mixed $value): static
    {
        $this->value = $value;

        return $this;
    }

    /**
     * {@inheritDoc}
     */
    public function expiresAt($expiration): static
    {
        if ($expiration === null) {
            $this->expiry = null;
        } elseif ($expiration instanceof DateTimeInterface) {
            $this->expiry = (float) $expiration->format('U.u');
        } else {
            throw new TypeError(sprintf(
                'Expected $expiration to be an instance of DateTimeInterface or null, got %s',
                get_debug_type($expiration)
            ));
        }

        return $this;
    }

    /**
     * {@inheritDoc}
     */
    public function expiresAfter($time): static
    {
        if ($time === null) {
            $this->expiry = null;
        } elseif ($time instanceof DateInterval) {
            $this->expiry = microtime(true) + DateTime::createFromFormat('U', 0)->add($time)->format('U.u');
        } elseif (is_int($time)) {
            $this->expiry = $time + microtime(true);
        } else {
            throw new TypeError(sprintf(
                'Expected $time to be either an integer, an instance of DateInterval or null, got %s',
                get_debug_type($time)
            ));
        }

        return $this;
    }

    /**
     * @internal
     */
    public function getExpiry(): ?float
    {
        return $this->expiry;
    }
}
<?php

namespace Doctrine\Common\Cache;

/**
 * Interface for cache drivers that supports multiple items manipulation.
 *
 * @link   www.doctrine-project.org
 */
interface MultiOperationCache extends MultiGetCache, MultiDeleteCache, MultiPutCache
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection;

/**
 * Represents a PHP type-hinted service reference.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
class TypedReference extends Reference
{
    private string $type;
    private ?string $name;
    private array $attributes;

    /**
     * @param string      $id              The service identifier
     * @param string      $type            The PHP type of the identified service
     * @param int         $invalidBehavior The behavior when the service does not exist
     * @param string|null $name            The name of the argument targeting the service
     * @param array       $attributes      The attributes to be used
     */
    public function __construct(string $id, string $type, int $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, ?string $name = null, array $attributes = [])
    {
        $this->name = $type === $id ? $name : null;
        parent::__construct($id, $invalidBehavior);
        $this->type = $type;
        $this->attributes = $attributes;
    }

    /**
     * @return string
     */
    public function getType()
    {
        return $this->type;
    }

    public function getName(): ?string
    {
        return $this->name;
    }

    public function getAttributes(): array
    {
        return $this->attributes;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection;

use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
use Symfony\Component\DependencyInjection\Argument\ServiceLocator as ArgumentServiceLocator;
use Symfony\Component\DependencyInjection\Exception\EnvNotFoundException;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\ParameterCircularReferenceException;
use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag;
use Symfony\Component\DependencyInjection\ParameterBag\FrozenParameterBag;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Contracts\Service\ResetInterface;

// Help opcache.preload discover always-needed symbols
class_exists(RewindableGenerator::class);
class_exists(ArgumentServiceLocator::class);

/**
 * Container is a dependency injection container.
 *
 * It gives access to object instances (services).
 * Services and parameters are simple key/pair stores.
 * The container can have five possible behaviors when a service
 * does not exist (or is not initialized for the last case):
 *
 *  * EXCEPTION_ON_INVALID_REFERENCE: Throws an exception at compilation time (the default)
 *  * NULL_ON_INVALID_REFERENCE:      Returns null
 *  * IGNORE_ON_INVALID_REFERENCE:    Ignores the wrapping command asking for the reference
 *                                    (for instance, ignore a setter if the service does not exist)
 *  * IGNORE_ON_UNINITIALIZED_REFERENCE: Ignores/returns null for uninitialized services or invalid references
 *  * RUNTIME_EXCEPTION_ON_INVALID_REFERENCE: Throws an exception at runtime
 *
 * @author Fabien Potencier <fabien@symfony.com>
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class Container implements ContainerInterface, ResetInterface
{
    protected $parameterBag;
    protected $services = [];
    protected $privates = [];
    protected $fileMap = [];
    protected $methodMap = [];
    protected $factories = [];
    protected $aliases = [];
    protected $loading = [];
    protected $resolving = [];
    protected $syntheticIds = [];

    private array $envCache = [];
    private bool $compiled = false;
    private \Closure $getEnv;

    private static \Closure $make;

    public function __construct(?ParameterBagInterface $parameterBag = null)
    {
        $this->parameterBag = $parameterBag ?? new EnvPlaceholderParameterBag();
    }

    /**
     * Compiles the container.
     *
     * This method does two things:
     *
     *  * Parameter values are resolved;
     *  * The parameter bag is frozen.
     *
     * @return void
     */
    public function compile()
    {
        $this->parameterBag->resolve();

        $this->parameterBag = new FrozenParameterBag(
            $this->parameterBag->all(),
            $this->parameterBag instanceof ParameterBag ? $this->parameterBag->allDeprecated() : []
        );

        $this->compiled = true;
    }

    /**
     * Returns true if the container is compiled.
     */
    public function isCompiled(): bool
    {
        return $this->compiled;
    }

    /**
     * Gets the service container parameter bag.
     */
    public function getParameterBag(): ParameterBagInterface
    {
        return $this->parameterBag;
    }

    /**
     * Gets a parameter.
     *
     * @return array|bool|string|int|float|\UnitEnum|null
     *
     * @throws ParameterNotFoundException if the parameter is not defined
     */
    public function getParameter(string $name)
    {
        return $this->parameterBag->get($name);
    }

    public function hasParameter(string $name): bool
    {
        return $this->parameterBag->has($name);
    }

    /**
     * @return void
     */
    public function setParameter(string $name, array|bool|string|int|float|\UnitEnum|null $value)
    {
        $this->parameterBag->set($name, $value);
    }

    /**
     * Sets a service.
     *
     * Setting a synthetic service to null resets it: has() returns false and get()
     * behaves in the same way as if the service was never created.
     *
     * @return void
     */
    public function set(string $id, ?object $service)
    {
        // Runs the internal initializer; used by the dumped container to include always-needed files
        if (isset($this->privates['service_container']) && $this->privates['service_container'] instanceof \Closure) {
            $initialize = $this->privates['service_container'];
            unset($this->privates['service_container']);
            $initialize($this);
        }

        if ('service_container' === $id) {
            throw new InvalidArgumentException('You cannot set service "service_container".');
        }

        if (!(isset($this->fileMap[$id]) || isset($this->methodMap[$id]))) {
            if (isset($this->syntheticIds[$id]) || !isset($this->getRemovedIds()[$id])) {
                // no-op
            } elseif (null === $service) {
                throw new InvalidArgumentException(\sprintf('The "%s" service is private, you cannot unset it.', $id));
            } else {
                throw new InvalidArgumentException(\sprintf('The "%s" service is private, you cannot replace it.', $id));
            }
        } elseif (isset($this->services[$id])) {
            throw new InvalidArgumentException(\sprintf('The "%s" service is already initialized, you cannot replace it.', $id));
        }

        if (isset($this->aliases[$id])) {
            unset($this->aliases[$id]);
        }

        if (null === $service) {
            unset($this->services[$id]);

            return;
        }

        $this->services[$id] = $service;
    }

    public function has(string $id): bool
    {
        if (isset($this->aliases[$id])) {
            $id = $this->aliases[$id];
        }
        if (isset($this->services[$id])) {
            return true;
        }
        if ('service_container' === $id) {
            return true;
        }

        return isset($this->fileMap[$id]) || isset($this->methodMap[$id]);
    }

    /**
     * Gets a service.
     *
     * @throws ServiceCircularReferenceException When a circular reference is detected
     * @throws ServiceNotFoundException          When the service is not defined
     *
     * @see Reference
     */
    public function get(string $id, int $invalidBehavior = self::EXCEPTION_ON_INVALID_REFERENCE): ?object
    {
        return $this->services[$id]
            ?? $this->services[$id = $this->aliases[$id] ?? $id]
            ?? ('service_container' === $id ? $this : ($this->factories[$id] ?? self::$make ??= self::make(...))($this, $id, $invalidBehavior));
    }

    /**
     * Creates a service.
     *
     * As a separate method to allow "get()" to use the really fast `??` operator.
     */
    private static function make(self $container, string $id, int $invalidBehavior): ?object
    {
        if (isset($container->loading[$id])) {
            throw new ServiceCircularReferenceException($id, array_merge(array_keys($container->loading), [$id]));
        }

        $container->loading[$id] = true;

        try {
            if (isset($container->fileMap[$id])) {
                return /* self::IGNORE_ON_UNINITIALIZED_REFERENCE */ 4 === $invalidBehavior ? null : $container->load($container->fileMap[$id]);
            } elseif (isset($container->methodMap[$id])) {
                return /* self::IGNORE_ON_UNINITIALIZED_REFERENCE */ 4 === $invalidBehavior ? null : $container->{$container->methodMap[$id]}($container);
            }
        } catch (\Exception $e) {
            unset($container->services[$id]);

            throw $e;
        } finally {
            unset($container->loading[$id]);
        }

        if (self::EXCEPTION_ON_INVALID_REFERENCE === $invalidBehavior) {
            if (!$id) {
                throw new ServiceNotFoundException($id);
            }
            if (isset($container->syntheticIds[$id])) {
                throw new ServiceNotFoundException($id, null, null, [], \sprintf('The "%s" service is synthetic, it needs to be set at boot time before it can be used.', $id));
            }
            if (isset($container->getRemovedIds()[$id])) {
                throw new ServiceNotFoundException($id, null, null, [], \sprintf('The "%s" service or alias has been removed or inlined when the container was compiled. You should either make it public, or stop using the container directly and use dependency injection instead.', $id));
            }

            $alternatives = [];
            foreach ($container->getServiceIds() as $knownId) {
                if ('' === $knownId || '.' === $knownId[0]) {
                    continue;
                }
                $lev = levenshtein($id, $knownId);
                if ($lev <= \strlen($id) / 3 || str_contains($knownId, $id)) {
                    $alternatives[] = $knownId;
                }
            }

            throw new ServiceNotFoundException($id, null, null, $alternatives);
        }

        return null;
    }

    /**
     * Returns true if the given service has actually been initialized.
     */
    public function initialized(string $id): bool
    {
        if (isset($this->aliases[$id])) {
            $id = $this->aliases[$id];
        }

        if ('service_container' === $id) {
            return false;
        }

        return isset($this->services[$id]);
    }

    /**
     * @return void
     */
    public function reset()
    {
        $services = $this->services + $this->privates;

        foreach ($services as $service) {
            try {
                if ($service instanceof ResetInterface) {
                    $service->reset();
                }
            } catch (\Throwable) {
                continue;
            }
        }

        $this->services = $this->factories = $this->privates = [];
    }

    /**
     * Gets all service ids.
     *
     * @return string[]
     */
    public function getServiceIds(): array
    {
        return array_map('strval', array_unique(array_merge(['service_container'], array_keys($this->fileMap), array_keys($this->methodMap), array_keys($this->aliases), array_keys($this->services))));
    }

    /**
     * Gets service ids that existed at compile time.
     */
    public function getRemovedIds(): array
    {
        return [];
    }

    /**
     * Camelizes a string.
     */
    public static function camelize(string $id): string
    {
        return strtr(ucwords(strtr($id, ['_' => ' ', '.' => '_ ', '\\' => '_ '])), [' ' => '']);
    }

    /**
     * A string to underscore.
     */
    public static function underscore(string $id): string
    {
        return strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], ['\\1_\\2', '\\1_\\2'], str_replace('_', '.', $id)));
    }

    /**
     * Creates a service by requiring its factory file.
     *
     * @return mixed
     */
    protected function load(string $file)
    {
        return require $file;
    }

    /**
     * Fetches a variable from the environment.
     *
     * @throws EnvNotFoundException When the environment variable is not found and has no default value
     */
    protected function getEnv(string $name): mixed
    {
        if (isset($this->resolving[$envName = "env($name)"])) {
            throw new ParameterCircularReferenceException(array_keys($this->resolving));
        }
        if (isset($this->envCache[$name]) || \array_key_exists($name, $this->envCache)) {
            return $this->envCache[$name];
        }
        if (!$this->has($id = 'container.env_var_processors_locator')) {
            $this->set($id, new ServiceLocator([]));
        }
        $this->getEnv ??= $this->getEnv(...);
        $processors = $this->get($id);

        if (false !== $i = strpos($name, ':')) {
            $prefix = substr($name, 0, $i);
            $localName = substr($name, 1 + $i);
        } else {
            $prefix = 'string';
            $localName = $name;
        }

        $processor = $processors->has($prefix) ? $processors->get($prefix) : new EnvVarProcessor($this);
        if (false === $i) {
            $prefix = '';
        }

        $this->resolving[$envName] = true;
        try {
            return $this->envCache[$name] = $processor->getEnv($prefix, $localName, $this->getEnv);
        } finally {
            unset($this->resolving[$envName]);
        }
    }

    /**
     * @internal
     */
    final protected function getService(string|false $registry, string $id, ?string $method, string|bool $load): mixed
    {
        if ('service_container' === $id) {
            return $this;
        }
        if (\is_string($load)) {
            throw new RuntimeException($load);
        }
        if (null === $method) {
            return false !== $registry ? $this->{$registry}[$id] ?? null : null;
        }
        if (false !== $registry) {
            return $this->{$registry}[$id] ??= $load ? $this->load($method) : $this->{$method}($this);
        }
        if (!$load) {
            return $this->{$method}($this);
        }

        return ($factory = $this->factories[$id] ?? $this->factories['service_container'][$id] ?? null) ? $factory($this) : $this->load($method);
    }

    private function __clone()
    {
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Exception;

/**
 * Base LogicException for Dependency Injection component.
 */
class LogicException extends \LogicException implements ExceptionInterface
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Exception;

use Psr\Container\NotFoundExceptionInterface;

/**
 * This exception is thrown when a non-existent parameter is used.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class ParameterNotFoundException extends InvalidArgumentException implements NotFoundExceptionInterface
{
    private string $key;
    private ?string $sourceId;
    private ?string $sourceKey;
    private array $alternatives;
    private ?string $nonNestedAlternative;

    /**
     * @param string          $key                  The requested parameter key
     * @param string|null     $sourceId             The service id that references the non-existent parameter
     * @param string|null     $sourceKey            The parameter key that references the non-existent parameter
     * @param \Throwable|null $previous             The previous exception
     * @param string[]        $alternatives         Some parameter name alternatives
     * @param string|null     $nonNestedAlternative The alternative parameter name when the user expected dot notation for nested parameters
     */
    public function __construct(string $key, ?string $sourceId = null, ?string $sourceKey = null, ?\Throwable $previous = null, array $alternatives = [], ?string $nonNestedAlternative = null)
    {
        $this->key = $key;
        $this->sourceId = $sourceId;
        $this->sourceKey = $sourceKey;
        $this->alternatives = $alternatives;
        $this->nonNestedAlternative = $nonNestedAlternative;

        parent::__construct('', 0, $previous);

        $this->updateRepr();
    }

    /**
     * @return void
     */
    public function updateRepr()
    {
        if (null !== $this->sourceId) {
            $this->message = \sprintf('The service "%s" has a dependency on a non-existent parameter "%s".', $this->sourceId, $this->key);
        } elseif (null !== $this->sourceKey) {
            $this->message = \sprintf('The parameter "%s" has a dependency on a non-existent parameter "%s".', $this->sourceKey, $this->key);
        } elseif ('.' === ($this->key[0] ?? '')) {
            $this->message = \sprintf('Parameter "%s" not found. It was probably deleted during the compilation of the container.', $this->key);
        } else {
            $this->message = \sprintf('You have requested a non-existent parameter "%s".', $this->key);
        }

        if ($this->alternatives) {
            if (1 == \count($this->alternatives)) {
                $this->message .= ' Did you mean this: "';
            } else {
                $this->message .= ' Did you mean one of these: "';
            }
            $this->message .= implode('", "', $this->alternatives).'"?';
        } elseif (null !== $this->nonNestedAlternative) {
            $this->message .= ' You cannot access nested array items, do you want to inject "'.$this->nonNestedAlternative.'" instead?';
        }
    }

    /**
     * @return string
     */
    public function getKey()
    {
        return $this->key;
    }

    /**
     * @return string|null
     */
    public function getSourceId()
    {
        return $this->sourceId;
    }

    /**
     * @return string|null
     */
    public function getSourceKey()
    {
        return $this->sourceKey;
    }

    /**
     * @return void
     */
    public function setSourceId(?string $sourceId)
    {
        $this->sourceId = $sourceId;

        $this->updateRepr();
    }

    /**
     * @return void
     */
    public function setSourceKey(?string $sourceKey)
    {
        $this->sourceKey = $sourceKey;

        $this->updateRepr();
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Exception;

/**
 * Thrown when trying to inject a parameter into a constructor/method with an incompatible type.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 * @author Julien Maulny <jmaulny@darkmira.fr>
 */
class InvalidParameterTypeException extends InvalidArgumentException
{
    public function __construct(string $serviceId, string $type, \ReflectionParameter $parameter)
    {
        $acceptedType = $parameter->getType();
        $acceptedType = $acceptedType instanceof \ReflectionNamedType ? $acceptedType->getName() : (string) $acceptedType;
        $this->code = $type;

        $function = $parameter->getDeclaringFunction();
        $functionName = $function instanceof \ReflectionMethod
            ? \sprintf('%s::%s', $function->getDeclaringClass()->getName(), $function->getName())
            : $function->getName();

        parent::__construct(\sprintf('Invalid definition for service "%s": argument %d of "%s()" accepts "%s", "%s" passed.', $serviceId, 1 + $parameter->getPosition(), $functionName, $acceptedType, $type));
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Exception;

/**
 * This exception wraps exceptions whose messages contain a reference to an env parameter.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
class EnvParameterException extends InvalidArgumentException
{
    public function __construct(array $envs, ?\Throwable $previous = null, string $message = 'Incompatible use of dynamic environment variables "%s" found in parameters.')
    {
        parent::__construct(\sprintf($message, implode('", "', $envs)), 0, $previous);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Exception;

/**
 * Base BadMethodCallException for Dependency Injection component.
 */
class BadMethodCallException extends \BadMethodCallException implements ExceptionInterface
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Exception;

/**
 * This exception is thrown when a circular reference is detected.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class ServiceCircularReferenceException extends RuntimeException
{
    private string $serviceId;
    private array $path;

    public function __construct(string $serviceId, array $path, ?\Throwable $previous = null)
    {
        parent::__construct(\sprintf('Circular reference detected for service "%s", path: "%s".', $serviceId, implode(' -> ', $path)), 0, $previous);

        $this->serviceId = $serviceId;
        $this->path = $path;
    }

    /**
     * @return string
     */
    public function getServiceId()
    {
        return $this->serviceId;
    }

    /**
     * @return array
     */
    public function getPath()
    {
        return $this->path;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Exception;

/**
 * Base RuntimeException for Dependency Injection component.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class RuntimeException extends \RuntimeException implements ExceptionInterface
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Exception;

/**
 * Thrown when a definition cannot be autowired.
 */
class AutowiringFailedException extends RuntimeException
{
    private string $serviceId;
    private ?\Closure $messageCallback = null;

    public function __construct(string $serviceId, string|\Closure $message = '', int $code = 0, ?\Throwable $previous = null)
    {
        $this->serviceId = $serviceId;

        if ($message instanceof \Closure && \function_exists('xdebug_is_enabled') && xdebug_is_enabled()) {
            $message = $message();
        }

        if (!$message instanceof \Closure) {
            parent::__construct($message, $code, $previous);

            return;
        }

        $this->messageCallback = $message;
        parent::__construct('', $code, $previous);

        $this->message = new class($this->message, $this->messageCallback) {
            private string|self $message;
            private ?\Closure $messageCallback;

            public function __construct(&$message, &$messageCallback)
            {
                $this->message = &$message;
                $this->messageCallback = &$messageCallback;
            }

            public function __toString(): string
            {
                $messageCallback = $this->messageCallback;
                $this->messageCallback = null;

                try {
                    return $this->message = $messageCallback();
                } catch (\Throwable $e) {
                    return $this->message = $e->getMessage();
                }
            }
        };
    }

    public function getMessageCallback(): ?\Closure
    {
        return $this->messageCallback;
    }

    /**
     * @return string
     */
    public function getServiceId()
    {
        return $this->serviceId;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Exception;

use Psr\Container\ContainerExceptionInterface;

/**
 * Base ExceptionInterface for Dependency Injection component.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 * @author Bulat Shakirzyanov <bulat@theopenskyproject.com>
 */
interface ExceptionInterface extends ContainerExceptionInterface, \Throwable
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Exception;

use Psr\Container\NotFoundExceptionInterface;

/**
 * This exception is thrown when a non-existent service is requested.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class ServiceNotFoundException extends InvalidArgumentException implements NotFoundExceptionInterface
{
    private string $id;
    private ?string $sourceId;
    private array $alternatives;

    public function __construct(string $id, ?string $sourceId = null, ?\Throwable $previous = null, array $alternatives = [], ?string $msg = null)
    {
        if (null !== $msg) {
            // no-op
        } elseif (null === $sourceId) {
            $msg = \sprintf('You have requested a non-existent service "%s".', $id);
        } else {
            $msg = \sprintf('The service "%s" has a dependency on a non-existent service "%s".', $sourceId, $id);
        }

        if ($alternatives) {
            if (1 == \count($alternatives)) {
                $msg .= ' Did you mean this: "';
            } else {
                $msg .= ' Did you mean one of these: "';
            }
            $msg .= implode('", "', $alternatives).'"?';
        }

        parent::__construct($msg, 0, $previous);

        $this->id = $id;
        $this->sourceId = $sourceId;
        $this->alternatives = $alternatives;
    }

    /**
     * @return string
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * @return string|null
     */
    public function getSourceId()
    {
        return $this->sourceId;
    }

    /**
     * @return array
     */
    public function getAlternatives()
    {
        return $this->alternatives;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Exception;

/**
 * This exception is thrown when an environment variable is not found.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
class EnvNotFoundException extends InvalidArgumentException
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Exception;

/**
 * Base OutOfBoundsException for Dependency Injection component.
 */
class OutOfBoundsException extends \OutOfBoundsException implements ExceptionInterface
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Exception;

/**
 * Base InvalidArgumentException for Dependency Injection component.
 *
 * @author Bulat Shakirzyanov <bulat@theopenskyproject.com>
 */
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Exception;

/**
 * This exception is thrown when a circular reference in a parameter is detected.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class ParameterCircularReferenceException extends RuntimeException
{
    private array $parameters;

    public function __construct(array $parameters, ?\Throwable $previous = null)
    {
        parent::__construct(\sprintf('Circular reference detected for parameter "%s" ("%s" > "%s").', $parameters[0], implode('" > "', $parameters), $parameters[0]), 0, $previous);

        $this->parameters = $parameters;
    }

    /**
     * @return array
     */
    public function getParameters()
    {
        return $this->parameters;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection;

/**
 * TaggedContainerInterface is the interface implemented when a container knows how to deals with tags.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
interface TaggedContainerInterface extends ContainerInterface
{
    /**
     * Returns service ids for a given tag.
     *
     * @param string $name The tag name
     */
    public function findTaggedServiceIds(string $name): array;
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection;

use Psr\Container\ContainerInterface;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;

/**
 * Turns public and "container.reversible" services back to their ids.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
final class ReverseContainer
{
    private Container $serviceContainer;
    private ContainerInterface $reversibleLocator;
    private string $tagName;
    private \Closure $getServiceId;

    public function __construct(Container $serviceContainer, ContainerInterface $reversibleLocator, string $tagName = 'container.reversible')
    {
        $this->serviceContainer = $serviceContainer;
        $this->reversibleLocator = $reversibleLocator;
        $this->tagName = $tagName;
        $this->getServiceId = \Closure::bind(fn (object $service): ?string => array_search($service, $this->services, true) ?: array_search($service, $this->privates, true) ?: null, $serviceContainer, Container::class);
    }

    /**
     * Returns the id of the passed object when it exists as a service.
     *
     * To be reversible, services need to be either public or be tagged with "container.reversible".
     */
    public function getId(object $service): ?string
    {
        if ($this->serviceContainer === $service) {
            return 'service_container';
        }

        if (null === $id = ($this->getServiceId)($service)) {
            return null;
        }

        if ($this->serviceContainer->has($id) || $this->reversibleLocator->has($id)) {
            return $id;
        }

        return null;
    }

    /**
     * @throws ServiceNotFoundException When the service is not reversible
     */
    public function getService(string $id): object
    {
        if ($this->reversibleLocator->has($id)) {
            return $this->reversibleLocator->get($id);
        }

        if (isset($this->serviceContainer->getRemovedIds()[$id])) {
            throw new ServiceNotFoundException($id, null, null, [], \sprintf('The "%s" service is private and cannot be accessed by reference. You should either make it public, or tag it as "%s".', $id, $this->tagName));
        }

        return $this->serviceContainer->get($id);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Dumper;

use Symfony\Component\DependencyInjection\ContainerBuilder;

/**
 * Dumper is the abstract class for all built-in dumpers.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
abstract class Dumper implements DumperInterface
{
    protected $container;

    public function __construct(ContainerBuilder $container)
    {
        $this->container = $container;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Dumper;

use Composer\Autoload\ClassLoader;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\DependencyInjection\Argument\AbstractArgument;
use Symfony\Component\DependencyInjection\Argument\ArgumentInterface;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\LazyClosure;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceLocator;
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\DependencyInjection\Compiler\AnalyzeServiceReferencesPass;
use Symfony\Component\DependencyInjection\Compiler\CheckCircularReferencesPass;
use Symfony\Component\DependencyInjection\Compiler\ServiceReferenceGraphNode;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\EnvParameterException;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\ExpressionLanguage;
use Symfony\Component\DependencyInjection\LazyProxy\PhpDumper\DumperInterface;
use Symfony\Component\DependencyInjection\LazyProxy\PhpDumper\LazyServiceDumper;
use Symfony\Component\DependencyInjection\LazyProxy\PhpDumper\NullDumper;
use Symfony\Component\DependencyInjection\Parameter;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ServiceLocator as BaseServiceLocator;
use Symfony\Component\DependencyInjection\TypedReference;
use Symfony\Component\DependencyInjection\Variable;
use Symfony\Component\ErrorHandler\DebugClassLoader;
use Symfony\Component\ExpressionLanguage\Expression;

/**
 * PhpDumper dumps a service container as a PHP class.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class PhpDumper extends Dumper
{
    /**
     * Characters that might appear in the generated variable name as first character.
     */
    public const FIRST_CHARS = 'abcdefghijklmnopqrstuvwxyz';

    /**
     * Characters that might appear in the generated variable name as any but the first character.
     */
    public const NON_FIRST_CHARS = 'abcdefghijklmnopqrstuvwxyz0123456789_';

    /** @var \SplObjectStorage<Definition, Variable>|null */
    private ?\SplObjectStorage $definitionVariables = null;
    private ?array $referenceVariables = null;
    private int $variableCount;
    private ?\SplObjectStorage $inlinedDefinitions = null;
    private ?array $serviceCalls = null;
    private array $reservedVariables = ['instance', 'class', 'this', 'container'];
    private ExpressionLanguage $expressionLanguage;
    private ?string $targetDirRegex = null;
    private int $targetDirMaxMatches;
    private string $docStar;
    private array $serviceIdToMethodNameMap;
    private array $usedMethodNames;
    private string $namespace;
    private bool $asFiles;
    private string $hotPathTag;
    private array $preloadTags;
    private bool $inlineFactories;
    private bool $inlineRequires;
    private array $inlinedRequires = [];
    private array $circularReferences = [];
    private array $singleUsePrivateIds = [];
    private array $preload = [];
    private bool $addGetService = false;
    private array $locatedIds = [];
    private string $serviceLocatorTag;
    private array $exportedVariables = [];
    private array $dynamicParameters = [];
    private string $baseClass;
    private string $class;
    private DumperInterface $proxyDumper;
    private bool $hasProxyDumper = true;

    public function __construct(ContainerBuilder $container)
    {
        if (!$container->isCompiled()) {
            throw new LogicException('Cannot dump an uncompiled container.');
        }

        parent::__construct($container);
    }

    /**
     * Sets the dumper to be used when dumping proxies in the generated container.
     *
     * @return void
     */
    public function setProxyDumper(DumperInterface $proxyDumper)
    {
        $this->proxyDumper = $proxyDumper;
        $this->hasProxyDumper = !$proxyDumper instanceof NullDumper;
    }

    /**
     * Dumps the service container as a PHP class.
     *
     * Available options:
     *
     *  * class:      The class name
     *  * base_class: The base class name
     *  * namespace:  The class namespace
     *  * as_files:   To split the container in several files
     *
     * @return string|array A PHP class representing the service container or an array of PHP files if the "as_files" option is set
     *
     * @throws EnvParameterException When an env var exists but has not been dumped
     */
    public function dump(array $options = []): string|array
    {
        $this->locatedIds = [];
        $this->targetDirRegex = null;
        $this->inlinedRequires = [];
        $this->exportedVariables = [];
        $this->dynamicParameters = [];
        $options = array_merge([
            'class' => 'ProjectServiceContainer',
            'base_class' => 'Container',
            'namespace' => '',
            'as_files' => false,
            'debug' => true,
            'hot_path_tag' => 'container.hot_path',
            'preload_tags' => ['container.preload', 'container.no_preload'],
            'inline_factories_parameter' => 'container.dumper.inline_factories', // @deprecated since Symfony 6.3
            'inline_class_loader_parameter' => 'container.dumper.inline_class_loader', // @deprecated since Symfony 6.3
            'inline_factories' => null,
            'inline_class_loader' => null,
            'preload_classes' => [],
            'service_locator_tag' => 'container.service_locator',
            'build_time' => time(),
        ], $options);

        $this->addGetService = false;
        $this->namespace = $options['namespace'];
        $this->asFiles = $options['as_files'];
        $this->hotPathTag = $options['hot_path_tag'];
        $this->preloadTags = $options['preload_tags'];

        $this->inlineFactories = false;
        if (isset($options['inline_factories'])) {
            $this->inlineFactories = $this->asFiles && $options['inline_factories'];
        } elseif (!$options['inline_factories_parameter']) {
            trigger_deprecation('symfony/dependency-injection', '6.3', 'Option "inline_factories_parameter" passed to "%s()" is deprecated, use option "inline_factories" instead.', __METHOD__);
        } elseif ($this->container->hasParameter($options['inline_factories_parameter'])) {
            trigger_deprecation('symfony/dependency-injection', '6.3', 'Option "inline_factories_parameter" passed to "%s()" is deprecated, use option "inline_factories" instead.', __METHOD__);
            $this->inlineFactories = $this->asFiles && $this->container->getParameter($options['inline_factories_parameter']);
        }

        $this->inlineRequires = $options['debug'];
        if (isset($options['inline_class_loader'])) {
            $this->inlineRequires = $options['inline_class_loader'];
        } elseif (!$options['inline_class_loader_parameter']) {
            trigger_deprecation('symfony/dependency-injection', '6.3', 'Option "inline_class_loader_parameter" passed to "%s()" is deprecated, use option "inline_class_loader" instead.', __METHOD__);
            $this->inlineRequires = false;
        } elseif ($this->container->hasParameter($options['inline_class_loader_parameter'])) {
            trigger_deprecation('symfony/dependency-injection', '6.3', 'Option "inline_class_loader_parameter" passed to "%s()" is deprecated, use option "inline_class_loader" instead.', __METHOD__);
            $this->inlineRequires = $this->container->getParameter($options['inline_class_loader_parameter']);
        }

        $this->serviceLocatorTag = $options['service_locator_tag'];
        $this->class = $options['class'];

        if (!str_starts_with($baseClass = $options['base_class'], '\\') && 'Container' !== $baseClass) {
            $baseClass = \sprintf('%s\%s', $options['namespace'] ? '\\'.$options['namespace'] : '', $baseClass);
            $this->baseClass = $baseClass;
        } elseif ('Container' === $baseClass) {
            $this->baseClass = Container::class;
        } else {
            $this->baseClass = $baseClass;
        }

        $this->initializeMethodNamesMap('Container' === $baseClass ? Container::class : $baseClass);

        if (!$this->hasProxyDumper) {
            (new AnalyzeServiceReferencesPass(true, false))->process($this->container);
            (new CheckCircularReferencesPass())->process($this->container);
        }

        $this->analyzeReferences();
        $this->docStar = $options['debug'] ? '*' : '';

        if (!empty($options['file']) && is_dir($dir = \dirname($options['file']))) {
            // Build a regexp where the first root dirs are mandatory,
            // but every other sub-dir is optional up to the full path in $dir
            // Mandate at least 1 root dir and not more than 5 optional dirs.

            $dir = explode(\DIRECTORY_SEPARATOR, realpath($dir));
            $i = \count($dir);

            if (2 + (int) ('\\' === \DIRECTORY_SEPARATOR) <= $i) {
                $regex = '';
                $lastOptionalDir = $i > 8 ? $i - 5 : (2 + (int) ('\\' === \DIRECTORY_SEPARATOR));
                $this->targetDirMaxMatches = $i - $lastOptionalDir;

                while (--$i >= $lastOptionalDir) {
                    $regex = \sprintf('(%s%s)?', preg_quote(\DIRECTORY_SEPARATOR.$dir[$i], '#'), $regex);
                }

                do {
                    $regex = preg_quote(\DIRECTORY_SEPARATOR.$dir[$i], '#').$regex;
                } while (0 < --$i);

                $this->targetDirRegex = '#(^|file://|[:;, \|\r\n])'.preg_quote($dir[0], '#').$regex.'#';
            }
        }

        $proxyClasses = $this->inlineFactories ? $this->generateProxyClasses() : null;

        if ($options['preload_classes']) {
            $this->preload = array_combine($options['preload_classes'], $options['preload_classes']);
        }

        $code = $this->addDefaultParametersMethod();
        $code =
            $this->startClass($options['class'], $baseClass, $this->inlineFactories && $proxyClasses).
            $this->addServices($services).
            $this->addDeprecatedAliases().
            $code
        ;

        $proxyClasses ??= $this->generateProxyClasses();

        if ($this->addGetService) {
            $code = preg_replace(
                "/\r?\n\r?\n    public function __construct.+?\\{\r?\n/s",
                "\n    protected \Closure \$getService;$0",
                $code,
                1
            );
        }

        if ($this->asFiles) {
            $fileTemplate = <<<EOF
                <?php

                use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
                use Symfony\Component\DependencyInjection\ContainerInterface;
                use Symfony\Component\DependencyInjection\Exception\RuntimeException;

                /*{$this->docStar}
                 * @internal This class has been auto-generated by the Symfony Dependency Injection Component.
                 */
                class %s extends {$options['class']}
                {%s}

                EOF;
            $files = [];
            $preloadedFiles = [];
            $ids = $this->container->getRemovedIds();
            foreach ($this->container->getDefinitions() as $id => $definition) {
                if (!$definition->isPublic()) {
                    $ids[$id] = true;
                }
            }
            if ($ids = array_keys($ids)) {
                sort($ids);
                $c = "<?php\n\nreturn [\n";
                foreach ($ids as $id) {
                    $c .= '    '.$this->doExport($id)." => true,\n";
                }
                $files['removed-ids.php'] = $c."];\n";
            }

            if (!$this->inlineFactories) {
                foreach ($this->generateServiceFiles($services) as $file => [$c, $preload]) {
                    $files[$file] = \sprintf($fileTemplate, substr($file, 0, -4), $c);

                    if ($preload) {
                        $preloadedFiles[$file] = $file;
                    }
                }
                foreach ($proxyClasses as $file => $c) {
                    $files[$file] = "<?php\n".$c;
                    $preloadedFiles[$file] = $file;
                }
            }

            $code .= $this->endClass();

            if ($this->inlineFactories && $proxyClasses) {
                $files['proxy-classes.php'] = "<?php\n\n";

                foreach ($proxyClasses as $c) {
                    $files['proxy-classes.php'] .= $c;
                }
            }

            $files[$options['class'].'.php'] = $code;
            $hash = ucfirst(strtr(ContainerBuilder::hash($files), '._', 'xx'));
            $code = [];

            foreach ($files as $file => $c) {
                $code["Container{$hash}/{$file}"] = substr_replace($c, "<?php\n\nnamespace Container{$hash};\n", 0, 6);

                if (isset($preloadedFiles[$file])) {
                    $preloadedFiles[$file] = "Container{$hash}/{$file}";
                }
            }
            $namespaceLine = $this->namespace ? "\nnamespace {$this->namespace};\n" : '';
            $time = $options['build_time'];
            $id = hash('crc32', $hash.$time);
            $this->asFiles = false;

            if ($this->preload && null !== $autoloadFile = $this->getAutoloadFile()) {
                $autoloadFile = trim($this->export($autoloadFile), '()\\');

                $preloadedFiles = array_reverse($preloadedFiles);
                if ('' !== $preloadedFiles = implode("';\nrequire __DIR__.'/", $preloadedFiles)) {
                    $preloadedFiles = "require __DIR__.'/$preloadedFiles';\n";
                }

                $code[$options['class'].'.preload.php'] = <<<EOF
                    <?php

                    // This file has been auto-generated by the Symfony Dependency Injection Component
                    // You can reference it in the "opcache.preload" php.ini setting on PHP >= 7.4 when preloading is desired

                    use Symfony\Component\DependencyInjection\Dumper\Preloader;

                    if (in_array(PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) {
                        return;
                    }

                    require $autoloadFile;
                    (require __DIR__.'/{$options['class']}.php')->set(\\Container{$hash}\\{$options['class']}::class, null);
                    $preloadedFiles
                    \$classes = [];

                    EOF;

                foreach ($this->preload as $class) {
                    if (!$class || str_contains($class, '$') || \in_array($class, ['int', 'float', 'string', 'bool', 'resource', 'object', 'array', 'null', 'callable', 'iterable', 'mixed', 'void', 'never'], true)) {
                        continue;
                    }
                    if (!(class_exists($class, false) || interface_exists($class, false) || trait_exists($class, false)) || (new \ReflectionClass($class))->isUserDefined()) {
                        $code[$options['class'].'.preload.php'] .= \sprintf("\$classes[] = '%s';\n", $class);
                    }
                }

                $code[$options['class'].'.preload.php'] .= <<<'EOF'

                    $preloaded = Preloader::preload($classes);

                    EOF;
            }

            $code[$options['class'].'.php'] = <<<EOF
                <?php
                {$namespaceLine}
                // This file has been auto-generated by the Symfony Dependency Injection Component for internal use.

                if (\\class_exists(\\Container{$hash}\\{$options['class']}::class, false)) {
                    // no-op
                } elseif (!include __DIR__.'/Container{$hash}/{$options['class']}.php') {
                    touch(__DIR__.'/Container{$hash}.legacy');

                    return;
                }

                if (!\\class_exists({$options['class']}::class, false)) {
                    \\class_alias(\\Container{$hash}\\{$options['class']}::class, {$options['class']}::class, false);
                }

                return new \\Container{$hash}\\{$options['class']}([
                    'container.build_hash' => '$hash',
                    'container.build_id' => '$id',
                    'container.build_time' => $time,
                    'container.runtime_mode' => \\in_array(\\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true) ? 'web=0' : 'web=1',
                ], __DIR__.\\DIRECTORY_SEPARATOR.'Container{$hash}');

                EOF;
        } else {
            $code .= $this->endClass();
            foreach ($proxyClasses as $c) {
                $code .= $c;
            }
        }

        $this->targetDirRegex = null;
        $this->inlinedRequires = [];
        $this->circularReferences = [];
        $this->locatedIds = [];
        $this->exportedVariables = [];
        $this->dynamicParameters = [];
        $this->preload = [];

        $unusedEnvs = [];
        foreach ($this->container->getEnvCounters() as $env => $use) {
            if (!$use) {
                $unusedEnvs[] = $env;
            }
        }
        if ($unusedEnvs) {
            throw new EnvParameterException($unusedEnvs, null, 'Environment variables "%s" are never used. Please, check your container\'s configuration.');
        }

        return $code;
    }

    /**
     * Retrieves the currently set proxy dumper or instantiates one.
     */
    private function getProxyDumper(): DumperInterface
    {
        return $this->proxyDumper ??= new LazyServiceDumper($this->class);
    }

    private function analyzeReferences(): void
    {
        (new AnalyzeServiceReferencesPass(false, $this->hasProxyDumper))->process($this->container);
        $checkedNodes = [];
        $this->circularReferences = [];
        $this->singleUsePrivateIds = [];
        foreach ($this->container->getCompiler()->getServiceReferenceGraph()->getNodes() as $id => $node) {
            if (!$node->getValue() instanceof Definition) {
                continue;
            }

            if ($this->isSingleUsePrivateNode($node)) {
                $this->singleUsePrivateIds[$id] = $id;
            }

            $this->collectCircularReferences($id, $node->getOutEdges(), $checkedNodes);
        }

        $this->container->getCompiler()->getServiceReferenceGraph()->clear();
        $this->singleUsePrivateIds = array_diff_key($this->singleUsePrivateIds, $this->circularReferences);
    }

    private function collectCircularReferences(string $sourceId, array $edges, array &$checkedNodes, array &$loops = [], array $path = [], bool $byConstructor = true): void
    {
        $path[$sourceId] = $byConstructor;
        $checkedNodes[$sourceId] = true;
        foreach ($edges as $edge) {
            $node = $edge->getDestNode();
            $id = $node->getId();
            if (($sourceId === $id && !$edge->isLazy()) || !$node->getValue() instanceof Definition || $edge->isWeak()) {
                continue;
            }

            if (isset($path[$id])) {
                $loop = null;
                $loopByConstructor = $edge->isReferencedByConstructor() && !$edge->isLazy();
                $pathInLoop = [$id, []];
                foreach ($path as $k => $pathByConstructor) {
                    if (null !== $loop) {
                        $loop[] = $k;
                        $pathInLoop[1][$k] = $pathByConstructor;
                        $loops[$k][] = &$pathInLoop;
                        $loopByConstructor = $loopByConstructor && $pathByConstructor;
                    } elseif ($k === $id) {
                        $loop = [];
                    }
                }
                $this->addCircularReferences($id, $loop, $loopByConstructor);
            } elseif (!isset($checkedNodes[$id])) {
                $this->collectCircularReferences($id, $node->getOutEdges(), $checkedNodes, $loops, $path, $edge->isReferencedByConstructor() && !$edge->isLazy());
            } elseif (isset($loops[$id])) {
                // we already had detected loops for this edge
                // let's check if we have a common ancestor in one of the detected loops
                foreach ($loops[$id] as [$first, $loopPath]) {
                    if (!isset($path[$first])) {
                        continue;
                    }
                    // We have a common ancestor, let's fill the current path
                    $fillPath = null;
                    foreach ($loopPath as $k => $pathByConstructor) {
                        if (null !== $fillPath) {
                            $fillPath[$k] = $pathByConstructor;
                        } elseif ($k === $id) {
                            $fillPath = $path;
                            $fillPath[$k] = $pathByConstructor;
                        }
                    }

                    // we can now build the loop
                    $loop = null;
                    $loopByConstructor = $edge->isReferencedByConstructor() && !$edge->isLazy();
                    foreach ($fillPath as $k => $pathByConstructor) {
                        if (null !== $loop) {
                            $loop[] = $k;
                            $loopByConstructor = $loopByConstructor && $pathByConstructor;
                        } elseif ($k === $first) {
                            $loop = [];
                        }
                    }
                    $this->addCircularReferences($first, $loop, $loopByConstructor);
                    break;
                }
            }
        }
        unset($path[$sourceId]);
    }

    private function addCircularReferences(string $sourceId, array $currentPath, bool $byConstructor): void
    {
        $currentId = $sourceId;
        $currentPath = array_reverse($currentPath);
        $currentPath[] = $currentId;
        foreach ($currentPath as $parentId) {
            if (empty($this->circularReferences[$parentId][$currentId])) {
                $this->circularReferences[$parentId][$currentId] = $byConstructor;
            }

            $currentId = $parentId;
        }
    }

    private function collectLineage(string $class, array &$lineage): void
    {
        if (isset($lineage[$class])) {
            return;
        }
        if (!$r = $this->container->getReflectionClass($class, false)) {
            return;
        }
        if (is_a($class, $this->baseClass, true)) {
            return;
        }
        $file = $r->getFileName();
        if ($file && str_ends_with($file, ') : eval()\'d code')) {
            $file = substr($file, 0, strrpos($file, '(', -17));
        }
        if (!$file || $this->doExport($file) === $exportedFile = $this->export($file)) {
            return;
        }

        $lineage[$class] = substr($exportedFile, 1, -1);

        if ($parent = $r->getParentClass()) {
            $this->collectLineage($parent->name, $lineage);
        }

        foreach ($r->getInterfaces() as $parent) {
            $this->collectLineage($parent->name, $lineage);
        }

        foreach ($r->getTraits() as $parent) {
            $this->collectLineage($parent->name, $lineage);
        }

        unset($lineage[$class]);
        $lineage[$class] = substr($exportedFile, 1, -1);
    }

    private function generateProxyClasses(): array
    {
        $proxyClasses = [];
        $alreadyGenerated = [];
        $definitions = $this->container->getDefinitions();
        $strip = '' === $this->docStar;
        $proxyDumper = $this->getProxyDumper();
        ksort($definitions);
        foreach ($definitions as $id => $definition) {
            if (!$definition = $this->isProxyCandidate($definition, $asGhostObject, $id)) {
                continue;
            }
            if (isset($alreadyGenerated[$asGhostObject][$class = $definition->getClass()])) {
                continue;
            }
            $alreadyGenerated[$asGhostObject][$class] = true;

            if ($this->container->isTrackingResources()) {
                foreach (array_column($definition->getTag('proxy'), 'interface') ?: [$class] as $r) {
                    if (!$r = $this->container->getReflectionClass($r)) {
                        continue;
                    }
                    do {
                        if ($file = $r->getFileName()) {
                            if (str_ends_with($file, ') : eval()\'d code')) {
                                $file = substr($file, 0, strrpos($file, '(', -17));
                            }
                            if (is_file($file)) {
                                $this->container->addResource(new FileResource($file));
                            }
                        }
                        $r = $r->getParentClass() ?: null;
                    } while ($r?->isUserDefined());
                }
            }

            if ("\n" === $proxyCode = "\n".$proxyDumper->getProxyCode($definition, $id)) {
                continue;
            }

            if ($this->inlineRequires) {
                $lineage = [];
                $this->collectLineage($class, $lineage);

                $code = '';
                foreach (array_diff_key(array_flip($lineage), $this->inlinedRequires) as $file => $class) {
                    if ($this->inlineFactories) {
                        $this->inlinedRequires[$file] = true;
                    }
                    $code .= \sprintf("include_once %s;\n", $file);
                }

                $proxyCode = $code.$proxyCode;
            }

            if ($strip) {
                $proxyCode = "<?php\n".$proxyCode;
                $proxyCode = substr(self::stripComments($proxyCode), 5);
            }

            $proxyClass = $this->inlineRequires ? substr($proxyCode, \strlen($code)) : $proxyCode;
            $i = strpos($proxyClass, 'class');
            $proxyClass = substr($proxyClass, 6 + $i, strpos($proxyClass, ' ', 7 + $i) - $i - 6);

            if ($this->asFiles || $this->namespace) {
                $proxyCode .= "\nif (!\\class_exists('$proxyClass', false)) {\n    \\class_alias(__NAMESPACE__.'\\\\$proxyClass', '$proxyClass', false);\n}\n";
            }

            $proxyClasses[$proxyClass.'.php'] = $proxyCode;
        }

        return $proxyClasses;
    }

    private function addServiceInclude(string $cId, Definition $definition, bool $isProxyCandidate): string
    {
        $code = '';

        if ($this->inlineRequires && (!$this->isHotPath($definition) || $isProxyCandidate)) {
            $lineage = [];
            foreach ($this->inlinedDefinitions as $def) {
                if (!$def->isDeprecated()) {
                    foreach ($this->getClasses($def, $cId) as $class) {
                        $this->collectLineage($class, $lineage);
                    }
                }
            }

            foreach ($this->serviceCalls as $id => [$callCount, $behavior]) {
                if ('service_container' !== $id && $id !== $cId
                    && ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE !== $behavior
                    && $this->container->has($id)
                    && $this->isTrivialInstance($def = $this->container->findDefinition($id))
                ) {
                    foreach ($this->getClasses($def, $cId) as $class) {
                        $this->collectLineage($class, $lineage);
                    }
                }
            }

            foreach (array_diff_key(array_flip($lineage), $this->inlinedRequires) as $file => $class) {
                $code .= \sprintf("        include_once %s;\n", $file);
            }
        }

        foreach ($this->inlinedDefinitions as $def) {
            if ($file = $def->getFile()) {
                $file = $this->dumpValue($file);
                $file = '(' === $file[0] ? substr($file, 1, -1) : $file;
                $code .= \sprintf("        include_once %s;\n", $file);
            }
        }

        if ('' !== $code) {
            $code .= "\n";
        }

        return $code;
    }

    /**
     * @throws InvalidArgumentException
     * @throws RuntimeException
     */
    private function addServiceInstance(string $id, Definition $definition, bool $isSimpleInstance): string
    {
        $class = $this->dumpValue($definition->getClass());

        if (str_starts_with($class, "'") && !str_contains($class, '$') && !preg_match('/^\'(?:\\\{2})?[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*(?:\\\{2}[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)*\'$/', $class)) {
            throw new InvalidArgumentException(\sprintf('"%s" is not a valid class name for the "%s" service.', $class, $id));
        }

        $asGhostObject = false;
        $isProxyCandidate = $this->isProxyCandidate($definition, $asGhostObject, $id);

        $lastWitherIndex = null;
        foreach ($definition->getMethodCalls() as $k => $call) {
            if ($call[2] ?? false) {
                $lastWitherIndex = $k;
            }
        }

        $shouldShareInline = !$isProxyCandidate && $definition->isShared() && !isset($this->singleUsePrivateIds[$id]) && null === $lastWitherIndex;
        $serviceAccessor = \sprintf('$container->%s[%s]', $this->container->getDefinition($id)->isPublic() ? 'services' : 'privates', $this->doExport($id));
        $return = match (true) {
            $shouldShareInline && !isset($this->circularReferences[$id]) && $isSimpleInstance => 'return '.$serviceAccessor.' = ',
            $shouldShareInline && !isset($this->circularReferences[$id]) => $serviceAccessor.' = $instance = ',
            $shouldShareInline || !$isSimpleInstance => '$instance = ',
            default => 'return ',
        };

        $code = $this->addNewInstance($definition, '        '.$return, $id, $asGhostObject);

        if ($shouldShareInline && isset($this->circularReferences[$id])) {
            $code .= \sprintf(
                "\n        if (isset(%s)) {\n            return %1\$s;\n        }\n\n        %s%1\$s = \$instance;\n",
                $serviceAccessor,
                $isSimpleInstance ? 'return ' : ''
            );
        }

        return $code;
    }

    private function isTrivialInstance(Definition $definition): bool
    {
        if ($definition->hasErrors()) {
            return true;
        }
        if ($definition->isSynthetic() || $definition->getFile() || $definition->getMethodCalls() || $definition->getProperties() || $definition->getConfigurator()) {
            return false;
        }
        if ($definition->isDeprecated() || $definition->isLazy() || $definition->getFactory() || 3 < \count($definition->getArguments())) {
            return false;
        }

        foreach ($definition->getArguments() as $arg) {
            if (!$arg || $arg instanceof Parameter) {
                continue;
            }
            if (\is_array($arg) && 3 >= \count($arg)) {
                foreach ($arg as $k => $v) {
                    if ($this->dumpValue($k) !== $this->dumpValue($k, false)) {
                        return false;
                    }
                    if (!$v || $v instanceof Parameter) {
                        continue;
                    }
                    if ($v instanceof Reference && $this->container->has($id = (string) $v) && $this->container->findDefinition($id)->isSynthetic()) {
                        continue;
                    }
                    if (!\is_scalar($v) || $this->dumpValue($v) !== $this->dumpValue($v, false)) {
                        return false;
                    }
                }
            } elseif ($arg instanceof Reference && $this->container->has($id = (string) $arg) && $this->container->findDefinition($id)->isSynthetic()) {
                continue;
            } elseif (!\is_scalar($arg) || $this->dumpValue($arg) !== $this->dumpValue($arg, false)) {
                return false;
            }
        }

        return true;
    }

    private function addServiceMethodCalls(Definition $definition, string $variableName, ?string $sharedNonLazyId): string
    {
        $lastWitherIndex = null;
        foreach ($definition->getMethodCalls() as $k => $call) {
            if ($call[2] ?? false) {
                $lastWitherIndex = $k;
            }
        }

        $calls = '';
        foreach ($definition->getMethodCalls() as $k => $call) {
            $arguments = [];
            foreach ($call[1] as $i => $value) {
                $arguments[] = (\is_string($i) ? $i.': ' : '').$this->dumpValue($value);
            }

            $witherAssignation = '';

            if ($call[2] ?? false) {
                if (null !== $sharedNonLazyId && $lastWitherIndex === $k && 'instance' === $variableName) {
                    $witherAssignation = \sprintf('$container->%s[\'%s\'] = ', $definition->isPublic() ? 'services' : 'privates', $sharedNonLazyId);
                }
                $witherAssignation .= \sprintf('$%s = ', $variableName);
            }

            $calls .= $this->wrapServiceConditionals($call[1], \sprintf("        %s\$%s->%s(%s);\n", $witherAssignation, $variableName, $call[0], implode(', ', $arguments)));
        }

        return $calls;
    }

    private function addServiceProperties(Definition $definition, string $variableName = 'instance'): string
    {
        $code = '';
        foreach ($definition->getProperties() as $name => $value) {
            $code .= \sprintf("        \$%s->%s = %s;\n", $variableName, $name, $this->dumpValue($value));
        }

        return $code;
    }

    private function addServiceConfigurator(Definition $definition, string $variableName = 'instance'): string
    {
        if (!$callable = $definition->getConfigurator()) {
            return '';
        }

        if (\is_array($callable)) {
            if ($callable[0] instanceof Reference
                || ($callable[0] instanceof Definition && $this->definitionVariables->offsetExists($callable[0]))
            ) {
                return \sprintf("        %s->%s(\$%s);\n", $this->dumpValue($callable[0]), $callable[1], $variableName);
            }

            $class = $this->dumpValue($callable[0]);
            // If the class is a string we can optimize away
            if (str_starts_with($class, "'") && !str_contains($class, '$')) {
                return \sprintf("        %s::%s(\$%s);\n", $this->dumpLiteralClass($class), $callable[1], $variableName);
            }

            if (str_starts_with($class, 'new ')) {
                return \sprintf("        (%s)->%s(\$%s);\n", $this->dumpValue($callable[0]), $callable[1], $variableName);
            }

            return \sprintf("        [%s, '%s'](\$%s);\n", $this->dumpValue($callable[0]), $callable[1], $variableName);
        }

        return \sprintf("        %s(\$%s);\n", $callable, $variableName);
    }

    private function addService(string $id, Definition $definition): array
    {
        $this->definitionVariables = new \SplObjectStorage();
        $this->referenceVariables = [];
        $this->variableCount = 0;
        $this->referenceVariables[$id] = new Variable('instance');

        $return = [];

        if ($class = $definition->getClass()) {
            $class = $class instanceof Parameter ? '%'.$class.'%' : $this->container->resolveEnvPlaceholders($class);
            $return[] = \sprintf(str_starts_with($class, '%') ? '@return object A %1$s instance' : '@return \%s', ltrim($class, '\\'));
        } elseif ($factory = $definition->getFactory()) {
            if (\is_string($factory) && !str_starts_with($factory, '@=')) {
                $return[] = \sprintf('@return object An instance returned by %s()', $factory);
            } elseif (\is_array($factory) && (\is_string($factory[0]) || $factory[0] instanceof Definition || $factory[0] instanceof Reference)) {
                $class = $factory[0] instanceof Definition ? $factory[0]->getClass() : (string) $factory[0];
                $class = $class instanceof Parameter ? '%'.$class.'%' : $this->container->resolveEnvPlaceholders($class);
                $return[] = \sprintf('@return object An instance returned by %s::%s()', $class, $factory[1]);
            }
        }

        if ($definition->isDeprecated()) {
            if ($return && str_starts_with($return[\count($return) - 1], '@return')) {
                $return[] = '';
            }

            $deprecation = $definition->getDeprecation($id);
            $return[] = \sprintf('@deprecated %s', ($deprecation['package'] || $deprecation['version'] ? "Since {$deprecation['package']} {$deprecation['version']}: " : '').$deprecation['message']);
        }

        $return = str_replace("\n     * \n", "\n     *\n", implode("\n     * ", $return));
        $return = $this->container->resolveEnvPlaceholders($return);

        $shared = $definition->isShared() ? ' shared' : '';
        $public = $definition->isPublic() ? 'public' : 'private';
        $autowired = $definition->isAutowired() ? ' autowired' : '';
        $asFile = $this->asFiles && !$this->inlineFactories && !$this->isHotPath($definition);
        $methodName = $this->generateMethodName($id);

        if ($asFile || $definition->isLazy()) {
            $lazyInitialization = ', $lazyLoad = true';
        } else {
            $lazyInitialization = '';
        }

        $code = <<<EOF

                /*{$this->docStar}
                 * Gets the $public '$id'$shared$autowired service.
                 *
                 * $return
            EOF;
        $code = str_replace('*/', ' ', $code).<<<EOF

                 */
                protected static function {$methodName}(\$container$lazyInitialization)
                {

            EOF;

        if ($asFile) {
            $file = $methodName.'.php';
            $code = str_replace("protected static function {$methodName}(", 'public static function do(', $code);
        } else {
            $file = null;
        }

        if ($definition->hasErrors() && $e = $definition->getErrors()) {
            $code .= \sprintf("        throw new RuntimeException(%s);\n", $this->export(reset($e)));
        } else {
            $this->serviceCalls = [];
            $this->inlinedDefinitions = $this->getDefinitionsFromArguments([$definition], null, $this->serviceCalls);

            if ($definition->isDeprecated()) {
                $deprecation = $definition->getDeprecation($id);
                $code .= \sprintf("        trigger_deprecation(%s, %s, %s);\n\n", $this->export($deprecation['package']), $this->export($deprecation['version']), $this->export($deprecation['message']));
            } elseif ($definition->hasTag($this->hotPathTag) || !$definition->hasTag($this->preloadTags[1])) {
                foreach ($this->inlinedDefinitions as $def) {
                    foreach ($this->getClasses($def, $id) as $class) {
                        $this->preload[$class] = $class;
                    }
                }
            }

            if (!$definition->isShared()) {
                $factory = \sprintf('$container->factories%s[%s]', $definition->isPublic() ? '' : "['service_container']", $this->doExport($id));
            }

            $asGhostObject = false;
            if ($isProxyCandidate = $this->isProxyCandidate($definition, $asGhostObject, $id)) {
                $definition = $isProxyCandidate;

                if (!$definition->isShared()) {
                    $code .= \sprintf('        %s ??= ', $factory);

                    if ($definition->isPublic()) {
                        $code .= \sprintf("fn () => self::%s(\$container);\n\n", $asFile ? 'do' : $methodName);
                    } else {
                        $code .= \sprintf("self::%s(...);\n\n", $asFile ? 'do' : $methodName);
                    }
                }
                $lazyLoad = $asGhostObject ? '$proxy' : 'false';

                $factoryCode = $asFile ? \sprintf('self::do($container, %s)', $lazyLoad) : \sprintf('self::%s($container, %s)', $methodName, $lazyLoad);
                $code .= $this->getProxyDumper()->getProxyFactoryCode($definition, $id, $factoryCode);
            }

            $c = $this->addServiceInclude($id, $definition, null !== $isProxyCandidate);

            if ('' !== $c && $isProxyCandidate && !$definition->isShared()) {
                $c = implode("\n", array_map(static fn ($line) => $line ? '    '.$line : $line, explode("\n", $c)));
                $code .= "        static \$include = true;\n\n";
                $code .= "        if (\$include) {\n";
                $code .= $c;
                $code .= "            \$include = false;\n";
                $code .= "        }\n\n";
            } else {
                $code .= $c;
            }

            $c = $this->addInlineService($id, $definition);

            if (!$isProxyCandidate && !$definition->isShared()) {
                $c = implode("\n", array_map(static fn ($line) => $line ? '    '.$line : $line, explode("\n", $c)));
                $lazyloadInitialization = $definition->isLazy() ? ', $lazyLoad = true' : '';

                $c = \sprintf("        %s = function (\$container%s) {\n%s        };\n\n        return %1\$s(\$container);\n", $factory, $lazyloadInitialization, $c);
            }

            $code .= $c;
        }

        $code .= "    }\n";

        $this->definitionVariables = $this->inlinedDefinitions = null;
        $this->referenceVariables = $this->serviceCalls = null;

        return [$file, $code];
    }

    private function addInlineVariables(string $id, Definition $definition, array $arguments, bool $forConstructor): string
    {
        $code = '';

        foreach ($arguments as $argument) {
            if (\is_array($argument)) {
                $code .= $this->addInlineVariables($id, $definition, $argument, $forConstructor);
            } elseif ($argument instanceof Reference) {
                $code .= $this->addInlineReference($id, $definition, $argument, $forConstructor);
            } elseif ($argument instanceof Definition) {
                $code .= $this->addInlineService($id, $definition, $argument, $forConstructor);
            }
        }

        return $code;
    }

    private function addInlineReference(string $id, Definition $definition, string $targetId, bool $forConstructor): string
    {
        while ($this->container->hasAlias($targetId)) {
            $targetId = (string) $this->container->getAlias($targetId);
        }

        [$callCount, $behavior] = $this->serviceCalls[$targetId];

        if ($id === $targetId) {
            return $this->addInlineService($id, $definition, $definition);
        }

        if ('service_container' === $targetId || isset($this->referenceVariables[$targetId])) {
            return '';
        }

        if ($this->container->hasDefinition($targetId) && ($def = $this->container->getDefinition($targetId)) && !$def->isShared()) {
            return '';
        }

        $hasSelfRef = isset($this->circularReferences[$id][$targetId]) && !isset($this->definitionVariables[$definition]) && !($this->hasProxyDumper && $definition->isLazy());

        if ($hasSelfRef && !$forConstructor && !$forConstructor = !$this->circularReferences[$id][$targetId]) {
            $code = $this->addInlineService($id, $definition, $definition);
        } else {
            $code = '';
        }

        if (isset($this->referenceVariables[$targetId]) || (2 > $callCount && (!$hasSelfRef || !$forConstructor))) {
            return $code;
        }

        $name = $this->getNextVariableName();
        $this->referenceVariables[$targetId] = new Variable($name);

        $reference = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE !== $behavior ? new Reference($targetId, $behavior) : null;
        $code .= \sprintf("        \$%s = %s;\n", $name, $this->getServiceCall($targetId, $reference));

        if (!$hasSelfRef || !$forConstructor) {
            return $code;
        }

        $code .= \sprintf(<<<'EOTXT'

                    if (isset($container->%s[%s])) {
                        return $container->%1$s[%2$s];
                    }

            EOTXT,
            $this->container->getDefinition($id)->isPublic() ? 'services' : 'privates',
            $this->doExport($id)
        );

        return $code;
    }

    private function addInlineService(string $id, Definition $definition, ?Definition $inlineDef = null, bool $forConstructor = true): string
    {
        $code = '';

        if ($isSimpleInstance = $isRootInstance = null === $inlineDef) {
            foreach ($this->serviceCalls as $targetId => [, , $byConstructor]) {
                if ($byConstructor && isset($this->circularReferences[$id][$targetId]) && !$this->circularReferences[$id][$targetId] && !($this->hasProxyDumper && $definition->isLazy())) {
                    $code .= $this->addInlineReference($id, $definition, $targetId, $forConstructor);
                }
            }
        }

        if (isset($this->definitionVariables[$inlineDef ??= $definition])) {
            return $code;
        }

        $arguments = [$inlineDef->getArguments(), $inlineDef->getFactory()];

        $code .= $this->addInlineVariables($id, $definition, $arguments, $forConstructor);

        if ($arguments = array_filter([$inlineDef->getProperties(), $inlineDef->getMethodCalls(), $inlineDef->getConfigurator()])) {
            $isSimpleInstance = false;
        } elseif ($definition !== $inlineDef && 2 > $this->inlinedDefinitions[$inlineDef]) {
            return $code;
        }

        $asGhostObject = false;
        $isProxyCandidate = $this->isProxyCandidate($inlineDef, $asGhostObject, $id);

        if (isset($this->definitionVariables[$inlineDef])) {
            $isSimpleInstance = false;
        } else {
            $name = $definition === $inlineDef ? 'instance' : $this->getNextVariableName();
            $this->definitionVariables[$inlineDef] = new Variable($name);
            $code .= '' !== $code ? "\n" : '';

            if ('instance' === $name) {
                $code .= $this->addServiceInstance($id, $definition, $isSimpleInstance);
            } else {
                $code .= $this->addNewInstance($inlineDef, '        $'.$name.' = ', $id);
            }

            if ('' !== $inline = $this->addInlineVariables($id, $definition, $arguments, false)) {
                $code .= "\n".$inline."\n";
            } elseif ($arguments && 'instance' === $name) {
                $code .= "\n";
            }

            $code .= $this->addServiceProperties($inlineDef, $name);
            $code .= $this->addServiceMethodCalls($inlineDef, $name, !$isProxyCandidate && $inlineDef->isShared() && !isset($this->singleUsePrivateIds[$id]) ? $id : null);
            $code .= $this->addServiceConfigurator($inlineDef, $name);
        }

        if (!$isRootInstance || $isSimpleInstance) {
            return $code;
        }

        return $code."\n        return \$instance;\n";
    }

    private function addServices(?array &$services = null): string
    {
        $publicServices = $privateServices = '';
        $definitions = $this->container->getDefinitions();
        ksort($definitions);
        foreach ($definitions as $id => $definition) {
            if (!$definition->isSynthetic()) {
                $services[$id] = $this->addService($id, $definition);
            } elseif ($definition->hasTag($this->hotPathTag) || !$definition->hasTag($this->preloadTags[1])) {
                $services[$id] = null;

                foreach ($this->getClasses($definition, $id) as $class) {
                    $this->preload[$class] = $class;
                }
            }
        }

        foreach ($definitions as $id => $definition) {
            if (!([$file, $code] = $services[$id]) || null !== $file) {
                continue;
            }
            if ($definition->isPublic()) {
                $publicServices .= $code;
            } elseif (!$this->isTrivialInstance($definition) || isset($this->locatedIds[$id])) {
                $privateServices .= $code;
            }
        }

        return $publicServices.$privateServices;
    }

    private function generateServiceFiles(array $services): iterable
    {
        $definitions = $this->container->getDefinitions();
        ksort($definitions);
        foreach ($definitions as $id => $definition) {
            if (([$file, $code] = $services[$id]) && null !== $file && ($definition->isPublic() || !$this->isTrivialInstance($definition) || isset($this->locatedIds[$id]))) {
                yield $file => [$code, $definition->hasTag($this->hotPathTag) || !$definition->hasTag($this->preloadTags[1]) && !$definition->isDeprecated() && !$definition->hasErrors()];
            }
        }
    }

    private function addNewInstance(Definition $definition, string $return = '', ?string $id = null, bool $asGhostObject = false): string
    {
        $tail = $return ? str_repeat(')', substr_count($return, '(') - substr_count($return, ')')).";\n" : '';

        if (BaseServiceLocator::class === $definition->getClass() && $definition->hasTag($this->serviceLocatorTag)) {
            $arguments = [];
            foreach ($definition->getArgument(0) as $k => $argument) {
                $arguments[$k] = $argument->getValues()[0];
            }

            return $return.$this->dumpValue(new ServiceLocatorArgument($arguments)).$tail;
        }

        $arguments = [];
        foreach ($definition->getArguments() as $i => $value) {
            $arguments[] = (\is_string($i) ? $i.': ' : '').$this->dumpValue($value);
        }

        if ($callable = $definition->getFactory()) {
            if ('current' === $callable && [0] === array_keys($definition->getArguments()) && \is_array($value) && [0] === array_keys($value)) {
                return $return.$this->dumpValue($value[0]).$tail;
            }

            if (['Closure', 'fromCallable'] === $callable) {
                $callable = $definition->getArgument(0);
                if ($callable instanceof ServiceClosureArgument) {
                    return $return.$this->dumpValue($callable).$tail;
                }

                $arguments = ['...'];

                if ($callable instanceof Reference || $callable instanceof Definition) {
                    $callable = [$callable, '__invoke'];
                }
            }

            if (\is_string($callable) && str_starts_with($callable, '@=')) {
                return $return.\sprintf('(($args = %s) ? (%s) : null)',
                    $this->dumpValue(new ServiceLocatorArgument($definition->getArguments())),
                    $this->getExpressionLanguage()->compile(substr($callable, 2), ['container' => 'container', 'args' => 'args'])
                ).$tail;
            }

            if (!\is_array($callable)) {
                return $return.\sprintf('%s(%s)', $this->dumpLiteralClass($this->dumpValue($callable)), $arguments ? implode(', ', $arguments) : '').$tail;
            }

            if (!preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $callable[1])) {
                throw new RuntimeException(\sprintf('Cannot dump definition because of invalid factory method (%s).', $callable[1] ?: 'n/a'));
            }

            if (['...'] === $arguments && ('Closure' !== ($class = $definition->getClass() ?: 'Closure') || $definition->isLazy() && (
                $callable[0] instanceof Reference
                || ($callable[0] instanceof Definition && !$this->definitionVariables->offsetExists($callable[0]))
            ))) {
                $initializer = 'fn () => '.$this->dumpValue($callable[0]);
                $this->preload[LazyClosure::class] = LazyClosure::class;

                return $return.LazyClosure::getCode($initializer, $callable, $class, $this->container, $id).$tail;
            }

            if ($callable[0] instanceof Reference
                || ($callable[0] instanceof Definition && $this->definitionVariables->offsetExists($callable[0]))
            ) {
                return $return.\sprintf('%s->%s(%s)', $this->dumpValue($callable[0]), $callable[1], $arguments ? implode(', ', $arguments) : '').$tail;
            }

            $class = $this->dumpValue($callable[0]);
            // If the class is a string we can optimize away
            if (str_starts_with($class, "'") && !str_contains($class, '$')) {
                if ("''" === $class) {
                    throw new RuntimeException(\sprintf('Cannot dump definition: "%s" service is defined to be created by a factory but is missing the service reference, did you forget to define the factory service id or class?', $id ? 'The "'.$id.'"' : 'inline'));
                }

                return $return.\sprintf('%s::%s(%s)', $this->dumpLiteralClass($class), $callable[1], $arguments ? implode(', ', $arguments) : '').$tail;
            }

            if (str_starts_with($class, 'new ')) {
                return $return.\sprintf('(%s)->%s(%s)', $class, $callable[1], $arguments ? implode(', ', $arguments) : '').$tail;
            }

            return $return.\sprintf("[%s, '%s'](%s)", $class, $callable[1], $arguments ? implode(', ', $arguments) : '').$tail;
        }

        if (null === $class = $definition->getClass()) {
            throw new RuntimeException('Cannot dump definitions which have no class nor factory.');
        }

        if (!$asGhostObject) {
            return $return.\sprintf('new %s(%s)', $this->dumpLiteralClass($this->dumpValue($class)), implode(', ', $arguments)).$tail;
        }

        if (!method_exists($this->container->getParameterBag()->resolveValue($class), '__construct')) {
            return $return.'$lazyLoad'.$tail;
        }

        return $return.\sprintf('($lazyLoad->__construct(%s) && false ?: $lazyLoad)', implode(', ', $arguments)).$tail;
    }

    private function startClass(string $class, string $baseClass, bool $hasProxyClasses): string
    {
        $namespaceLine = !$this->asFiles && $this->namespace ? "\nnamespace {$this->namespace};\n" : '';

        $code = <<<EOF
            <?php
            $namespaceLine
            use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
            use Symfony\Component\DependencyInjection\ContainerInterface;
            use Symfony\Component\DependencyInjection\Container;
            use Symfony\Component\DependencyInjection\Exception\LogicException;
            use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException;
            use Symfony\Component\DependencyInjection\Exception\RuntimeException;
            use Symfony\Component\DependencyInjection\ParameterBag\FrozenParameterBag;
            use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;

            /*{$this->docStar}
             * @internal This class has been auto-generated by the Symfony Dependency Injection Component.
             */
            class $class extends $baseClass
            {
                private const DEPRECATED_PARAMETERS = [];

                protected \$parameters = [];

                public function __construct()
                {

            EOF;
        $code = str_replace("    private const DEPRECATED_PARAMETERS = [];\n\n", $this->addDeprecatedParameters(), $code);
        if ($this->asFiles) {
            $code = str_replace('__construct()', '__construct(private array $buildParameters = [], protected string $containerDir = __DIR__)', $code);

            if (null !== $this->targetDirRegex) {
                $code = str_replace('$parameters = []', "\$targetDir;\n    protected \$parameters = []", $code);
                $code .= '        $this->targetDir = \\dirname($containerDir);'."\n";
            }
        }

        if ($this->needsUnsetParameterBag()) {
            $code .= "        parent::__construct();\n";
            $code .= "        unset(\$this->parameterBag);\n\n";
        }

        if ($this->container->getParameterBag()->all()) {
            $code .= "        \$this->parameters = \$this->getDefaultParameters();\n\n";
        }
        $code .= "        \$this->services = \$this->privates = [];\n";

        $code .= $this->addSyntheticIds();
        $code .= $this->addMethodMap();
        $code .= $this->asFiles && !$this->inlineFactories ? $this->addFileMap() : '';
        $code .= $this->addAliases();
        $code .= $this->addInlineRequires($hasProxyClasses);
        $code .= <<<EOF
                }

                public function compile(): void
                {
                    throw new LogicException('You cannot compile a dumped container that was already compiled.');
                }

                public function isCompiled(): bool
                {
                    return true;
                }

            EOF;
        $code .= $this->addRemovedIds();

        if ($this->asFiles && !$this->inlineFactories) {
            $code .= <<<'EOF'

                    protected function load($file, $lazyLoad = true): mixed
                    {
                        if (class_exists($class = __NAMESPACE__.'\\'.$file, false)) {
                            return $class::do($this, $lazyLoad);
                        }

                        if ('.' === $file[-4]) {
                            $class = substr($class, 0, -4);
                        } else {
                            $file .= '.php';
                        }

                        $service = require $this->containerDir.\DIRECTORY_SEPARATOR.$file;

                        return class_exists($class, false) ? $class::do($this, $lazyLoad) : $service;
                    }

                EOF;
        }

        foreach ($this->container->getDefinitions() as $definition) {
            if (!$definition->isLazy() || !$this->hasProxyDumper) {
                continue;
            }

            if ($this->asFiles && !$this->inlineFactories) {
                $proxyLoader = "class_exists(\$class, false) || require __DIR__.'/'.\$class.'.php';\n\n        ";
            } else {
                $proxyLoader = '';
            }

            $code .= <<<EOF

                    protected function createProxy(\$class, \Closure \$factory)
                    {
                        {$proxyLoader}return \$factory();
                    }

                EOF;
            break;
        }

        return $code;
    }

    private function addSyntheticIds(): string
    {
        $code = '';
        $definitions = $this->container->getDefinitions();
        ksort($definitions);
        foreach ($definitions as $id => $definition) {
            if ($definition->isSynthetic() && 'service_container' !== $id) {
                $code .= '            '.$this->doExport($id)." => true,\n";
            }
        }

        return $code ? "        \$this->syntheticIds = [\n{$code}        ];\n" : '';
    }

    private function addRemovedIds(): string
    {
        $ids = $this->container->getRemovedIds();
        foreach ($this->container->getDefinitions() as $id => $definition) {
            if (!$definition->isPublic()) {
                $ids[$id] = true;
            }
        }
        if (!$ids) {
            return '';
        }
        if ($this->asFiles) {
            $code = "require \$this->containerDir.\\DIRECTORY_SEPARATOR.'removed-ids.php'";
        } else {
            $code = '';
            $ids = array_keys($ids);
            sort($ids);
            foreach ($ids as $id) {
                if (preg_match(ContainerBuilder::ANONYMOUS_ID_REGEXP, $id)) {
                    continue;
                }
                $code .= '            '.$this->doExport($id)." => true,\n";
            }

            $code = "[\n{$code}        ]";
        }

        return <<<EOF

                public function getRemovedIds(): array
                {
                    return {$code};
                }

            EOF;
    }

    private function addDeprecatedParameters(): string
    {
        if (!($bag = $this->container->getParameterBag()) instanceof ParameterBag) {
            return '';
        }

        if (!$deprecated = $bag->allDeprecated()) {
            return '';
        }
        $code = '';
        ksort($deprecated);
        foreach ($deprecated as $param => $deprecation) {
            $code .= '        '.$this->doExport($param).' => ['.implode(', ', array_map($this->doExport(...), $deprecation))."],\n";
        }

        return "    private const DEPRECATED_PARAMETERS = [\n{$code}    ];\n\n";
    }

    private function addMethodMap(): string
    {
        $code = '';
        $definitions = $this->container->getDefinitions();
        ksort($definitions);
        foreach ($definitions as $id => $definition) {
            if (!$definition->isSynthetic() && $definition->isPublic() && (!$this->asFiles || $this->inlineFactories || $this->isHotPath($definition))) {
                $code .= '            '.$this->doExport($id).' => '.$this->doExport($this->generateMethodName($id)).",\n";
            }
        }

        $aliases = $this->container->getAliases();
        foreach ($aliases as $alias => $id) {
            if (!$id->isDeprecated()) {
                continue;
            }
            $code .= '            '.$this->doExport($alias).' => '.$this->doExport($this->generateMethodName($alias)).",\n";
        }

        return $code ? "        \$this->methodMap = [\n{$code}        ];\n" : '';
    }

    private function addFileMap(): string
    {
        $code = '';
        $definitions = $this->container->getDefinitions();
        ksort($definitions);
        foreach ($definitions as $id => $definition) {
            if (!$definition->isSynthetic() && $definition->isPublic() && !$this->isHotPath($definition)) {
                $code .= \sprintf("            %s => '%s',\n", $this->doExport($id), $this->generateMethodName($id));
            }
        }

        return $code ? "        \$this->fileMap = [\n{$code}        ];\n" : '';
    }

    private function addAliases(): string
    {
        if (!$aliases = $this->container->getAliases()) {
            return "\n        \$this->aliases = [];\n";
        }

        $code = "        \$this->aliases = [\n";
        ksort($aliases);
        foreach ($aliases as $alias => $id) {
            if ($id->isDeprecated()) {
                continue;
            }

            $id = (string) $id;
            while (isset($aliases[$id])) {
                $id = (string) $aliases[$id];
            }
            $code .= '            '.$this->doExport($alias).' => '.$this->doExport($id).",\n";
        }

        return $code."        ];\n";
    }

    private function addDeprecatedAliases(): string
    {
        $code = '';
        $aliases = $this->container->getAliases();
        foreach ($aliases as $alias => $definition) {
            if (!$definition->isDeprecated()) {
                continue;
            }
            $public = $definition->isPublic() ? 'public' : 'private';
            $id = (string) $definition;
            $methodNameAlias = $this->generateMethodName($alias);
            $idExported = $this->export($id);
            $deprecation = $definition->getDeprecation($alias);
            $packageExported = $this->export($deprecation['package']);
            $versionExported = $this->export($deprecation['version']);
            $messageExported = $this->export($deprecation['message']);
            $code .= <<<EOF

                    /*{$this->docStar}
                     * Gets the $public '$alias' alias.
                     *
                     * @return object The "$id" service.
                     */
                    protected static function {$methodNameAlias}(\$container)
                    {
                        trigger_deprecation($packageExported, $versionExported, $messageExported);

                        return \$container->get($idExported);
                    }

                EOF;
        }

        return $code;
    }

    private function addInlineRequires(bool $hasProxyClasses): string
    {
        $lineage = [];
        $hotPathServices = $this->hotPathTag && $this->inlineRequires ? $this->container->findTaggedServiceIds($this->hotPathTag) : [];

        foreach ($hotPathServices as $id => $tags) {
            $definition = $this->container->getDefinition($id);

            if ($definition->isLazy() && $this->hasProxyDumper) {
                continue;
            }

            $inlinedDefinitions = $this->getDefinitionsFromArguments([$definition]);

            foreach ($inlinedDefinitions as $def) {
                foreach ($this->getClasses($def, $id) as $class) {
                    $this->collectLineage($class, $lineage);
                }
            }
        }

        $code = '';

        foreach ($lineage as $file) {
            if (!isset($this->inlinedRequires[$file])) {
                $this->inlinedRequires[$file] = true;
                $code .= \sprintf("\n            include_once %s;", $file);
            }
        }

        if ($hasProxyClasses) {
            $code .= "\n            include_once __DIR__.'/proxy-classes.php';";
        }

        return $code ? \sprintf("\n        \$this->privates['service_container'] = static function (\$container) {%s\n        };\n", $code) : '';
    }

    private function needsUnsetParameterBag(): bool
    {
        if (Container::class === $this->baseClass) {
            return false;
        }
        $r = $this->container->getReflectionClass($this->baseClass, false);

        return null !== $r
            && (null !== $constructor = $r->getConstructor())
            && 0 === $constructor->getNumberOfRequiredParameters()
            && Container::class !== $constructor->getDeclaringClass()->name;
    }

    private function addDefaultParametersMethod(): string
    {
        if (!$this->container->getParameterBag()->all() && !$this->needsUnsetParameterBag()) {
            return '';
        }

        $php = [];
        $dynamicPhp = [];

        foreach ($this->container->getParameterBag()->all() as $key => $value) {
            if ($key !== $resolvedKey = $this->container->resolveEnvPlaceholders($key)) {
                throw new InvalidArgumentException(\sprintf('Parameter name cannot use env parameters: "%s".', $resolvedKey));
            }
            $hasEnum = false;
            $export = $this->exportParameters([$value], '', 12, $hasEnum);
            $export = explode('0 => ', substr(rtrim($export, " ]\n"), 2, -1), 2);

            if ($hasEnum || preg_match("/\\\$container->(?:getEnv\('(?:[-.\w\\\\]*+:)*+\w*+'\)|targetDir\.'')/", $export[1])) {
                $dynamicPhp[$key] = \sprintf('%s%s => %s,', $export[0], $this->export($key), $export[1]);
                $this->dynamicParameters[$key] = true;
            } else {
                $php[] = \sprintf('%s%s => %s,', $export[0], $this->export($key), $export[1]);
            }
        }
        $parameters = \sprintf("[\n%s\n%s]", implode("\n", $php), str_repeat(' ', 8));

        $code = <<<'EOF'

                public function getParameter(string $name): array|bool|string|int|float|\UnitEnum|null
                {
                    if (isset(self::DEPRECATED_PARAMETERS[$name])) {
                        trigger_deprecation(...self::DEPRECATED_PARAMETERS[$name]);
                    }

                    if (\array_key_exists($name, $this->buildParameters)) {
                        return $this->buildParameters[$name];
                    }

                    if (isset($this->loadedDynamicParameters[$name])) {
                        return $this->loadedDynamicParameters[$name] ? $this->dynamicParameters[$name] : $this->getDynamicParameter($name);
                    }

                    if (!\array_key_exists($name, $this->parameters) || '.' === ($name[0] ?? '')) {
                        throw new ParameterNotFoundException($name);
                    }

                    return $this->parameters[$name];
                }

                public function hasParameter(string $name): bool
                {
                    if (\array_key_exists($name, $this->buildParameters)) {
                        return true;
                    }

                    return \array_key_exists($name, $this->parameters) || isset($this->loadedDynamicParameters[$name]);
                }

                public function setParameter(string $name, $value): void
                {
                    throw new LogicException('Impossible to call set() on a frozen ParameterBag.');
                }

                public function getParameterBag(): ParameterBagInterface
                {
                    if (!isset($this->parameterBag)) {
                        $parameters = $this->parameters;
                        foreach ($this->loadedDynamicParameters as $name => $loaded) {
                            $parameters[$name] = $loaded ? $this->dynamicParameters[$name] : $this->getDynamicParameter($name);
                        }
                        foreach ($this->buildParameters as $name => $value) {
                            $parameters[$name] = $value;
                        }
                        $this->parameterBag = new FrozenParameterBag($parameters, self::DEPRECATED_PARAMETERS);
                    }

                    return $this->parameterBag;
                }

            EOF;

        if (!$this->asFiles) {
            $code = preg_replace('/^.*buildParameters.*\n.*\n.*\n\n?/m', '', $code);
        }

        if (!($bag = $this->container->getParameterBag()) instanceof ParameterBag || !$bag->allDeprecated()) {
            $code = preg_replace("/\n.*DEPRECATED_PARAMETERS.*\n.*\n.*\n/m", '', $code, 1);
            $code = str_replace(', self::DEPRECATED_PARAMETERS', '', $code);
        }

        if ($dynamicPhp) {
            $loadedDynamicParameters = $this->exportParameters(array_combine(array_keys($dynamicPhp), array_fill(0, \count($dynamicPhp), false)), '', 8);
            $getDynamicParameter = <<<'EOF'
                        $container = $this;
                        $value = match ($name) {
                %s
                            default => throw new ParameterNotFoundException($name),
                        };
                        $this->loadedDynamicParameters[$name] = true;

                        return $this->dynamicParameters[$name] = $value;
                EOF;
            $getDynamicParameter = \sprintf($getDynamicParameter, implode("\n", $dynamicPhp));
        } else {
            $loadedDynamicParameters = '[]';
            $getDynamicParameter = str_repeat(' ', 8).'throw new ParameterNotFoundException($name);';
        }

        $code .= <<<EOF

                private \$loadedDynamicParameters = {$loadedDynamicParameters};
                private \$dynamicParameters = [];

                private function getDynamicParameter(string \$name)
                {
            {$getDynamicParameter}
                }

                protected function getDefaultParameters(): array
                {
                    return $parameters;
                }

            EOF;

        return $code;
    }

    /**
     * @throws InvalidArgumentException
     */
    private function exportParameters(array $parameters, string $path = '', int $indent = 12, bool &$hasEnum = false): string
    {
        $php = [];
        foreach ($parameters as $key => $value) {
            if (\is_array($value)) {
                $value = $this->exportParameters($value, $path.'/'.$key, $indent + 4, $hasEnum);
            } elseif ($value instanceof ArgumentInterface) {
                throw new InvalidArgumentException(\sprintf('You cannot dump a container with parameters that contain special arguments. "%s" found in "%s".', get_debug_type($value), $path.'/'.$key));
            } elseif ($value instanceof Variable) {
                throw new InvalidArgumentException(\sprintf('You cannot dump a container with parameters that contain variable references. Variable "%s" found in "%s".', $value, $path.'/'.$key));
            } elseif ($value instanceof Definition) {
                throw new InvalidArgumentException(\sprintf('You cannot dump a container with parameters that contain service definitions. Definition for "%s" found in "%s".', $value->getClass(), $path.'/'.$key));
            } elseif ($value instanceof Reference) {
                throw new InvalidArgumentException(\sprintf('You cannot dump a container with parameters that contain references to other services (reference to service "%s" found in "%s").', $value, $path.'/'.$key));
            } elseif ($value instanceof Expression) {
                throw new InvalidArgumentException(\sprintf('You cannot dump a container with parameters that contain expressions. Expression "%s" found in "%s".', $value, $path.'/'.$key));
            } elseif ($value instanceof \UnitEnum) {
                $hasEnum = true;
                $value = \sprintf('\%s::%s', $value::class, $value->name);
            } else {
                $value = $this->export($value);
            }

            $php[] = \sprintf('%s%s => %s,', str_repeat(' ', $indent), $this->export($key), $value);
        }

        return \sprintf("[\n%s\n%s]", implode("\n", $php), str_repeat(' ', $indent - 4));
    }

    private function endClass(): string
    {
        return <<<'EOF'
            }

            EOF;
    }

    private function wrapServiceConditionals(mixed $value, string $code): string
    {
        if (!$condition = $this->getServiceConditionals($value)) {
            return $code;
        }

        // re-indent the wrapped code
        $code = implode("\n", array_map(static fn ($line) => $line ? '    '.$line : $line, explode("\n", $code)));

        return \sprintf("        if (%s) {\n%s        }\n", $condition, $code);
    }

    private function getServiceConditionals(mixed $value): string
    {
        $conditions = [];
        foreach (ContainerBuilder::getInitializedConditionals($value) as $service) {
            if (!$this->container->hasDefinition($service)) {
                return 'false';
            }
            $conditions[] = \sprintf('isset($container->%s[%s])', $this->container->getDefinition($service)->isPublic() ? 'services' : 'privates', $this->doExport($service));
        }
        foreach (ContainerBuilder::getServiceConditionals($value) as $service) {
            if ($this->container->hasDefinition($service) && !$this->container->getDefinition($service)->isPublic()) {
                continue;
            }

            $conditions[] = \sprintf('$container->has(%s)', $this->doExport($service));
        }

        if (!$conditions) {
            return '';
        }

        return implode(' && ', $conditions);
    }

    private function getDefinitionsFromArguments(array $arguments, ?\SplObjectStorage $definitions = null, array &$calls = [], ?bool $byConstructor = null): \SplObjectStorage
    {
        $definitions ??= new \SplObjectStorage();

        foreach ($arguments as $argument) {
            if (\is_array($argument)) {
                $this->getDefinitionsFromArguments($argument, $definitions, $calls, $byConstructor);
            } elseif ($argument instanceof Reference) {
                $id = (string) $argument;

                while ($this->container->hasAlias($id)) {
                    $id = (string) $this->container->getAlias($id);
                }

                if (!isset($calls[$id])) {
                    $calls[$id] = [0, $argument->getInvalidBehavior(), $byConstructor];
                } else {
                    $calls[$id][1] = min($calls[$id][1], $argument->getInvalidBehavior());
                }

                ++$calls[$id][0];
            } elseif (!$argument instanceof Definition) {
                // no-op
            } elseif (isset($definitions[$argument])) {
                $definitions[$argument] = 1 + $definitions[$argument];
            } else {
                $definitions[$argument] = 1;
                $arguments = [$argument->getArguments(), $argument->getFactory()];
                $this->getDefinitionsFromArguments($arguments, $definitions, $calls, null === $byConstructor || $byConstructor);
                $arguments = [$argument->getProperties(), $argument->getMethodCalls(), $argument->getConfigurator()];
                $this->getDefinitionsFromArguments($arguments, $definitions, $calls, null !== $byConstructor && $byConstructor);
            }
        }

        return $definitions;
    }

    /**
     * @throws RuntimeException
     */
    private function dumpValue(mixed $value, bool $interpolate = true): string
    {
        if (\is_array($value)) {
            if ($value && $interpolate && false !== $param = array_search($value, $this->container->getParameterBag()->all(), true)) {
                return $this->dumpValue("%$param%");
            }
            $isList = array_is_list($value);
            $code = [];
            foreach ($value as $k => $v) {
                $code[] = $isList ? $this->dumpValue($v, $interpolate) : \sprintf('%s => %s', $this->dumpValue($k, $interpolate), $this->dumpValue($v, $interpolate));
            }

            return \sprintf('[%s]', implode(', ', $code));
        } elseif ($value instanceof ArgumentInterface) {
            $scope = [$this->definitionVariables, $this->referenceVariables];
            $this->definitionVariables = $this->referenceVariables = null;

            try {
                if ($value instanceof ServiceClosureArgument) {
                    $value = $value->getValues()[0];
                    $code = $this->dumpValue($value, $interpolate);

                    $returnedType = '';
                    if ($value instanceof TypedReference) {
                        $type = $value->getType();
                        $nullable = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE >= $value->getInvalidBehavior() ? '' : '?';

                        if ('?' === ($type[0] ?? '')) {
                            $type = substr($type, 1);
                            $nullable = '?';
                        }

                        $returnedType = \sprintf(': %s\%s', $nullable, str_replace(['|', '&'], ['|\\', '&\\'], $type));
                    }

                    $attribute = '';
                    if ($value instanceof Reference) {
                        $attribute = 'name: '.$this->dumpValue((string) $value, $interpolate);

                        if ($this->container->hasDefinition($value) && ($class = $this->container->findDefinition($value)->getClass()) && $class !== (string) $value) {
                            $attribute .= ', class: '.$this->dumpValue($class, $interpolate);
                        }

                        $attribute = \sprintf('#[\Closure(%s)] ', $attribute);
                    }

                    return \sprintf('%sfn ()%s => %s', $attribute, $returnedType, $code);
                }

                if ($value instanceof IteratorArgument) {
                    if (!$values = $value->getValues()) {
                        return 'new RewindableGenerator(fn () => new \EmptyIterator(), 0)';
                    }

                    $code = [];
                    $code[] = 'new RewindableGenerator(function () use ($container) {';

                    $operands = [0];
                    foreach ($values as $k => $v) {
                        ($c = $this->getServiceConditionals($v)) ? $operands[] = "(int) ($c)" : ++$operands[0];
                        $v = $this->wrapServiceConditionals($v, \sprintf("        yield %s => %s;\n", $this->dumpValue($k, $interpolate), $this->dumpValue($v, $interpolate)));
                        foreach (explode("\n", $v) as $v) {
                            if ($v) {
                                $code[] = '    '.$v;
                            }
                        }
                    }

                    $code[] = \sprintf('        }, %s)', \count($operands) > 1 ? 'fn () => '.implode(' + ', $operands) : $operands[0]);

                    return implode("\n", $code);
                }

                if ($value instanceof ServiceLocatorArgument) {
                    $serviceMap = '';
                    $serviceTypes = '';
                    foreach ($value->getValues() as $k => $v) {
                        if (!$v instanceof Reference) {
                            $serviceMap .= \sprintf("\n            %s => [%s],", $this->export($k), $this->dumpValue($v));
                            $serviceTypes .= \sprintf("\n            %s => '?',", $this->export($k));
                            continue;
                        }
                        $id = (string) $v;
                        while ($this->container->hasAlias($id)) {
                            $id = (string) $this->container->getAlias($id);
                        }
                        $definition = $this->container->getDefinition($id);
                        $load = !($definition->hasErrors() && $e = $definition->getErrors()) ? $this->asFiles && !$this->inlineFactories && !$this->isHotPath($definition) : reset($e);
                        $serviceMap .= \sprintf("\n            %s => [%s, %s, %s, %s],",
                            $this->export($k),
                            $this->export($definition->isShared() ? ($definition->isPublic() ? 'services' : 'privates') : false),
                            $this->doExport($id),
                            $this->export(ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE !== $v->getInvalidBehavior() && !\is_string($load) ? $this->generateMethodName($id) : null),
                            $this->export($load)
                        );
                        $serviceTypes .= \sprintf("\n            %s => %s,", $this->export($k), $this->export($v instanceof TypedReference ? $v->getType() : '?'));
                        $this->locatedIds[$id] = true;
                    }
                    $this->addGetService = true;

                    return \sprintf('new \%s($container->getService ??= $container->getService(...), [%s%s], [%s%s])', ServiceLocator::class, $serviceMap, $serviceMap ? "\n        " : '', $serviceTypes, $serviceTypes ? "\n        " : '');
                }
            } finally {
                [$this->definitionVariables, $this->referenceVariables] = $scope;
            }
        } elseif ($value instanceof Definition) {
            if ($value->hasErrors() && $e = $value->getErrors()) {
                return \sprintf('throw new RuntimeException(%s)', $this->export(reset($e)));
            }
            if ($this->definitionVariables?->offsetExists($value)) {
                return $this->dumpValue($this->definitionVariables[$value], $interpolate);
            }
            if ($value->getMethodCalls()) {
                throw new RuntimeException('Cannot dump definitions which have method calls.');
            }
            if ($value->getProperties()) {
                throw new RuntimeException('Cannot dump definitions which have properties.');
            }
            if (null !== $value->getConfigurator()) {
                throw new RuntimeException('Cannot dump definitions which have a configurator.');
            }

            return $this->addNewInstance($value);
        } elseif ($value instanceof Variable) {
            return '$'.$value;
        } elseif ($value instanceof Reference) {
            $id = (string) $value;

            while ($this->container->hasAlias($id)) {
                $id = (string) $this->container->getAlias($id);
            }

            if (null !== $this->referenceVariables && isset($this->referenceVariables[$id])) {
                return $this->dumpValue($this->referenceVariables[$id], $interpolate);
            }

            return $this->getServiceCall($id, $value);
        } elseif ($value instanceof Expression) {
            return $this->getExpressionLanguage()->compile((string) $value, ['container' => 'container']);
        } elseif ($value instanceof Parameter) {
            return $this->dumpParameter($value);
        } elseif (true === $interpolate && \is_string($value)) {
            if (preg_match('/^%([^%]+)%$/', $value, $match)) {
                // we do this to deal with non string values (Boolean, integer, ...)
                // the preg_replace_callback converts them to strings
                return $this->dumpParameter($match[1]);
            }
            $replaceParameters = fn ($match) => "'.".$this->dumpParameter($match[2]).".'";

            $code = str_replace('%%', '%', preg_replace_callback('/(?<!%)(%)([^%]+)\1/', $replaceParameters, $this->export($value)));

            return $code;
        } elseif ($value instanceof \UnitEnum) {
            return \sprintf('\%s::%s', $value::class, $value->name);
        } elseif ($value instanceof AbstractArgument) {
            throw new RuntimeException($value->getTextWithContext());
        } elseif (\is_object($value) || \is_resource($value)) {
            throw new RuntimeException(\sprintf('Unable to dump a service container if a parameter is an object or a resource, got "%s".', get_debug_type($value)));
        }

        return $this->export($value);
    }

    /**
     * Dumps a string to a literal (aka PHP Code) class value.
     *
     * @throws RuntimeException
     */
    private function dumpLiteralClass(string $class): string
    {
        if (str_contains($class, '$')) {
            return \sprintf('${($_ = %s) && false ?: "_"}', $class);
        }
        if (!str_starts_with($class, "'") || !preg_match('/^\'(?:\\\{2})?[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*(?:\\\{2}[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)*\'$/', $class)) {
            throw new RuntimeException(\sprintf('Cannot dump definition because of invalid class name (%s).', $class ?: 'n/a'));
        }

        $class = substr(str_replace('\\\\', '\\', $class), 1, -1);

        return str_starts_with($class, '\\') ? $class : '\\'.$class;
    }

    private function dumpParameter(string $name): string
    {
        if (!$this->container->hasParameter($name) || ($this->dynamicParameters[$name] ?? false)) {
            return \sprintf('$container->getParameter(%s)', $this->doExport($name));
        }

        $value = $this->container->getParameter($name);
        $dumpedValue = $this->dumpValue($value, false);

        if (!$value || !\is_array($value)) {
            return $dumpedValue;
        }

        return \sprintf('$container->parameters[%s]', $this->doExport($name));
    }

    private function getServiceCall(string $id, ?Reference $reference = null): string
    {
        while ($this->container->hasAlias($id)) {
            $id = (string) $this->container->getAlias($id);
        }

        if ('service_container' === $id) {
            return '$container';
        }

        if ($this->container->hasDefinition($id) && $definition = $this->container->getDefinition($id)) {
            if ($definition->isSynthetic()) {
                $code = \sprintf('$container->get(%s%s)', $this->doExport($id), null !== $reference ? ', '.$reference->getInvalidBehavior() : '');
            } elseif (null !== $reference && ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE === $reference->getInvalidBehavior()) {
                $code = 'null';
                if (!$definition->isShared()) {
                    return $code;
                }
            } elseif ($this->isTrivialInstance($definition)) {
                if ($definition->hasErrors() && $e = $definition->getErrors()) {
                    return \sprintf('throw new RuntimeException(%s)', $this->export(reset($e)));
                }
                $code = $this->addNewInstance($definition, '', $id);
                if ($definition->isShared() && !isset($this->singleUsePrivateIds[$id])) {
                    return \sprintf('($container->%s[%s] ??= %s)', $definition->isPublic() ? 'services' : 'privates', $this->doExport($id), $code);
                }
                $code = "($code)";
            } else {
                $code = $this->asFiles && !$this->inlineFactories && !$this->isHotPath($definition) ? "\$container->load('%s')" : 'self::%s($container)';
                $code = \sprintf($code, $this->generateMethodName($id));

                if (!$definition->isShared()) {
                    $factory = \sprintf('$container->factories%s[%s]', $definition->isPublic() ? '' : "['service_container']", $this->doExport($id));
                    $code = \sprintf('(isset(%s) ? %1$s($container) : %s)', $factory, $code);
                }
            }
            if ($definition->isShared() && !isset($this->singleUsePrivateIds[$id])) {
                $code = \sprintf('($container->%s[%s] ?? %s)', $definition->isPublic() ? 'services' : 'privates', $this->doExport($id), $code);
            }

            return $code;
        }
        if (null !== $reference && ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE === $reference->getInvalidBehavior()) {
            return 'null';
        }
        if (null !== $reference && ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE < $reference->getInvalidBehavior()) {
            $code = \sprintf('$container->get(%s, ContainerInterface::NULL_ON_INVALID_REFERENCE)', $this->doExport($id));
        } else {
            $code = \sprintf('$container->get(%s)', $this->doExport($id));
        }

        return \sprintf('($container->services[%s] ?? %s)', $this->doExport($id), $code);
    }

    /**
     * Initializes the method names map to avoid conflicts with the Container methods.
     */
    private function initializeMethodNamesMap(string $class): void
    {
        $this->serviceIdToMethodNameMap = [];
        $this->usedMethodNames = [];

        if ($reflectionClass = $this->container->getReflectionClass($class)) {
            foreach ($reflectionClass->getMethods() as $method) {
                $this->usedMethodNames[strtolower($method->getName())] = true;
            }
        }
    }

    /**
     * @throws InvalidArgumentException
     */
    private function generateMethodName(string $id): string
    {
        if (isset($this->serviceIdToMethodNameMap[$id])) {
            return $this->serviceIdToMethodNameMap[$id];
        }

        $i = strrpos($id, '\\');
        $name = Container::camelize(false !== $i && isset($id[1 + $i]) ? substr($id, 1 + $i) : $id);
        $name = preg_replace('/[^a-zA-Z0-9_\x7f-\xff]/', '', $name);
        $methodName = 'get'.$name.'Service';
        $suffix = 1;

        while (isset($this->usedMethodNames[strtolower($methodName)])) {
            ++$suffix;
            $methodName = 'get'.$name.$suffix.'Service';
        }

        $this->serviceIdToMethodNameMap[$id] = $methodName;
        $this->usedMethodNames[strtolower($methodName)] = true;

        return $methodName;
    }

    private function getNextVariableName(): string
    {
        $firstChars = self::FIRST_CHARS;
        $firstCharsLength = \strlen($firstChars);
        $nonFirstChars = self::NON_FIRST_CHARS;
        $nonFirstCharsLength = \strlen($nonFirstChars);

        while (true) {
            $name = '';
            $i = $this->variableCount;

            if ('' === $name) {
                $name .= $firstChars[$i % $firstCharsLength];
                $i = (int) ($i / $firstCharsLength);
            }

            while ($i > 0) {
                --$i;
                $name .= $nonFirstChars[$i % $nonFirstCharsLength];
                $i = (int) ($i / $nonFirstCharsLength);
            }

            ++$this->variableCount;

            // check that the name is not reserved
            if (\in_array($name, $this->reservedVariables, true)) {
                continue;
            }

            return $name;
        }
    }

    private function getExpressionLanguage(): ExpressionLanguage
    {
        if (!isset($this->expressionLanguage)) {
            if (!class_exists(\Symfony\Component\ExpressionLanguage\ExpressionLanguage::class)) {
                throw new LogicException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed. Try running "composer require symfony/expression-language".');
            }
            $providers = $this->container->getExpressionLanguageProviders();
            $this->expressionLanguage = new ExpressionLanguage(null, $providers, function ($arg) {
                $id = '""' === substr_replace($arg, '', 1, -1) ? stripcslashes(substr($arg, 1, -1)) : null;

                if (null !== $id && ($this->container->hasAlias($id) || $this->container->hasDefinition($id))) {
                    return $this->getServiceCall($id);
                }

                return \sprintf('$container->get(%s)', $arg);
            });

            if ($this->container->isTrackingResources()) {
                foreach ($providers as $provider) {
                    $this->container->addObjectResource($provider);
                }
            }
        }

        return $this->expressionLanguage;
    }

    private function isHotPath(Definition $definition): bool
    {
        return $this->hotPathTag && $definition->hasTag($this->hotPathTag) && !$definition->isDeprecated();
    }

    private function isSingleUsePrivateNode(ServiceReferenceGraphNode $node): bool
    {
        if ($node->getValue()->isPublic()) {
            return false;
        }
        $ids = [];
        foreach ($node->getInEdges() as $edge) {
            if (!$value = $edge->getSourceNode()->getValue()) {
                continue;
            }
            if ($edge->isLazy() || !$value instanceof Definition || !$value->isShared() || $edge->isFromMultiUseArgument()) {
                return false;
            }

            // When the source node is a proxy or ghost, it will construct its references only when the node itself is initialized.
            // Since the node can be cloned before being fully initialized, we do not know how often its references are used.
            if ($this->getProxyDumper()->isProxyCandidate($value)) {
                return false;
            }
            $ids[$edge->getSourceNode()->getId()] = true;
        }

        return 1 === \count($ids);
    }

    private function export(mixed $value): mixed
    {
        if (null !== $this->targetDirRegex && \is_string($value) && preg_match($this->targetDirRegex, $value, $matches, \PREG_OFFSET_CAPTURE)) {
            $suffix = $matches[0][1] + \strlen($matches[0][0]);
            $matches[0][1] += \strlen($matches[1][0]);
            $prefix = $matches[0][1] ? $this->doExport(substr($value, 0, $matches[0][1]), true).'.' : '';

            if ('\\' === \DIRECTORY_SEPARATOR && isset($value[$suffix])) {
                $cookie = '\\'.random_int(100000, \PHP_INT_MAX);
                $suffix = '.'.$this->doExport(str_replace('\\', $cookie, substr($value, $suffix)), true);
                $suffix = str_replace('\\'.$cookie, "'.\\DIRECTORY_SEPARATOR.'", $suffix);
            } else {
                $suffix = isset($value[$suffix]) ? '.'.$this->doExport(substr($value, $suffix), true) : '';
            }

            $dirname = $this->asFiles ? '$container->containerDir' : '__DIR__';
            $offset = 2 + $this->targetDirMaxMatches - \count($matches);

            if (0 < $offset) {
                $dirname = \sprintf('\dirname(__DIR__, %d)', $offset + (int) $this->asFiles);
            } elseif ($this->asFiles) {
                $dirname = "\$container->targetDir.''"; // empty string concatenation on purpose
            }

            if ($prefix || $suffix) {
                return \sprintf('(%s%s%s)', $prefix, $dirname, $suffix);
            }

            return $dirname;
        }

        return $this->doExport($value, true);
    }

    private function doExport(mixed $value, bool $resolveEnv = false): mixed
    {
        $shouldCacheValue = $resolveEnv && \is_string($value);
        if ($shouldCacheValue && isset($this->exportedVariables[$value])) {
            return $this->exportedVariables[$value];
        }
        if (\is_string($value) && str_contains($value, "\n")) {
            $cleanParts = explode("\n", $value);
            $cleanParts = array_map(static fn ($part) => var_export($part, true), $cleanParts);
            $export = implode('."\n".', $cleanParts);
        } else {
            $export = var_export($value, true);
        }

        if ($resolveEnv && "'" === $export[0] && $export !== $resolvedExport = $this->container->resolveEnvPlaceholders($export, "'.\$container->getEnv('string:%s').'")) {
            $export = $resolvedExport;
            if (str_ends_with($export, ".''")) {
                $export = substr($export, 0, -3);
                if ("'" === $export[1]) {
                    $export = substr_replace($export, '', 23, 7);
                }
            }
            if ("'" === $export[1]) {
                $export = substr($export, 3);
            }
        }

        if ($shouldCacheValue) {
            $this->exportedVariables[$value] = $export;
        }

        return $export;
    }

    private function getAutoloadFile(): ?string
    {
        $file = null;

        foreach (spl_autoload_functions() as $autoloader) {
            if (!\is_array($autoloader)) {
                continue;
            }

            if ($autoloader[0] instanceof DebugClassLoader) {
                $autoloader = $autoloader[0]->getClassLoader();
            }

            if (!\is_array($autoloader) || !$autoloader[0] instanceof ClassLoader || !$autoloader[0]->findFile(__CLASS__)) {
                continue;
            }

            foreach (get_declared_classes() as $class) {
                if (str_starts_with($class, 'ComposerAutoloaderInit') && $class::getLoader() === $autoloader[0]) {
                    $file = \dirname((new \ReflectionClass($class))->getFileName(), 2).'/autoload.php';

                    if (null !== $this->targetDirRegex && preg_match($this->targetDirRegex.'A', $file)) {
                        return $file;
                    }
                }
            }
        }

        return $file;
    }

    private function getClasses(Definition $definition, string $id): array
    {
        $classes = [];

        while ($definition instanceof Definition) {
            foreach ($definition->getTag($this->preloadTags[0]) as $tag) {
                if (!isset($tag['class'])) {
                    throw new InvalidArgumentException(\sprintf('Missing attribute "class" on tag "%s" for service "%s".', $this->preloadTags[0], $id));
                }

                $classes[] = trim($tag['class'], '\\');
            }

            if ($class = $definition->getClass()) {
                $classes[] = trim($class, '\\');
            }
            $factory = $definition->getFactory();

            if (\is_string($factory) && !str_starts_with($factory, '@=') && str_contains($factory, '::')) {
                $factory = explode('::', $factory);
            }

            if (!\is_array($factory)) {
                $definition = $factory;
                continue;
            }

            $definition = $factory[0] ?? null;

            if (\is_string($definition)) {
                $classes[] = trim($factory[0], '\\');
            }
        }

        return $classes;
    }

    private function isProxyCandidate(Definition $definition, ?bool &$asGhostObject, string $id): ?Definition
    {
        $asGhostObject = false;

        if (['Closure', 'fromCallable'] === $definition->getFactory()) {
            return null;
        }

        if (!$definition->isLazy() || !$this->hasProxyDumper) {
            return null;
        }

        return $this->getProxyDumper()->isProxyCandidate($definition, $asGhostObject, $id) ? $definition : null;
    }

    /**
     * Removes comments from a PHP source string.
     *
     * We don't use the PHP php_strip_whitespace() function
     * as we want the content to be readable and well-formatted.
     */
    private static function stripComments(string $source): string
    {
        if (!\function_exists('token_get_all')) {
            return $source;
        }

        $rawChunk = '';
        $output = '';
        $tokens = token_get_all($source);
        $ignoreSpace = false;
        for ($i = 0; isset($tokens[$i]); ++$i) {
            $token = $tokens[$i];
            if (!isset($token[1]) || 'b"' === $token) {
                $rawChunk .= $token;
            } elseif (\T_START_HEREDOC === $token[0]) {
                $output .= $rawChunk.$token[1];
                do {
                    $token = $tokens[++$i];
                    $output .= isset($token[1]) && 'b"' !== $token ? $token[1] : $token;
                } while (\T_END_HEREDOC !== $token[0]);
                $rawChunk = '';
            } elseif (\T_WHITESPACE === $token[0]) {
                if ($ignoreSpace) {
                    $ignoreSpace = false;

                    continue;
                }

                // replace multiple new lines with a single newline
                $rawChunk .= preg_replace(['/\n{2,}/S'], "\n", $token[1]);
            } elseif (\in_array($token[0], [\T_COMMENT, \T_DOC_COMMENT])) {
                if (!\in_array($rawChunk[\strlen($rawChunk) - 1], [' ', "\n", "\r", "\t"], true)) {
                    $rawChunk .= ' ';
                }
                $ignoreSpace = true;
            } else {
                $rawChunk .= $token[1];

                // The PHP-open tag already has a new-line
                if (\T_OPEN_TAG === $token[0]) {
                    $ignoreSpace = true;
                } else {
                    $ignoreSpace = false;
                }
            }
        }

        $output .= $rawChunk;

        unset($tokens, $rawChunk);
        gc_mem_caches();

        return $output;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Dumper;

/**
 * DumperInterface is the interface implemented by service container dumper classes.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
interface DumperInterface
{
    /**
     * Dumps the service container.
     */
    public function dump(array $options = []): string|array;
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Dumper;

use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\Argument\AbstractArgument;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\Parameter;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\ExpressionLanguage\Expression;

/**
 * XmlDumper dumps a service container as an XML string.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 * @author Martin Hasoň <martin.hason@gmail.com>
 */
class XmlDumper extends Dumper
{
    private \DOMDocument $document;

    /**
     * Dumps the service container as an XML string.
     */
    public function dump(array $options = []): string
    {
        $this->document = new \DOMDocument('1.0', 'utf-8');
        $this->document->formatOutput = true;

        $container = $this->document->createElementNS('http://symfony.com/schema/dic/services', 'container');
        $container->setAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance');
        $container->setAttribute('xsi:schemaLocation', 'http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd');

        $this->addParameters($container);
        $this->addServices($container);

        $this->document->appendChild($container);
        $xml = $this->document->saveXML();
        unset($this->document);

        return $this->container->resolveEnvPlaceholders($xml);
    }

    private function addParameters(\DOMElement $parent): void
    {
        $data = $this->container->getParameterBag()->all();
        if (!$data) {
            return;
        }

        if ($this->container->isCompiled()) {
            $data = $this->escape($data);
        }

        $parameters = $this->document->createElement('parameters');
        $parent->appendChild($parameters);
        $this->convertParameters($data, 'parameter', $parameters);
    }

    private function addMethodCalls(array $methodcalls, \DOMElement $parent): void
    {
        foreach ($methodcalls as $methodcall) {
            $call = $this->document->createElement('call');
            $call->setAttribute('method', $methodcall[0]);
            if (\count($methodcall[1])) {
                $this->convertParameters($methodcall[1], 'argument', $call);
            }
            if ($methodcall[2] ?? false) {
                $call->setAttribute('returns-clone', 'true');
            }
            $parent->appendChild($call);
        }
    }

    private function addService(Definition $definition, ?string $id, \DOMElement $parent): void
    {
        $service = $this->document->createElement('service');
        if (null !== $id) {
            $service->setAttribute('id', $id);
        }
        if ($class = $definition->getClass()) {
            if (str_starts_with($class, '\\')) {
                $class = substr($class, 1);
            }

            $service->setAttribute('class', $class);
        }
        if (!$definition->isShared()) {
            $service->setAttribute('shared', 'false');
        }
        if ($definition->isPublic()) {
            $service->setAttribute('public', 'true');
        }
        if ($definition->isSynthetic()) {
            $service->setAttribute('synthetic', 'true');
        }
        if ($definition->isLazy()) {
            $service->setAttribute('lazy', 'true');
        }
        if (null !== $decoratedService = $definition->getDecoratedService()) {
            [$decorated, $renamedId, $priority] = $decoratedService;
            $service->setAttribute('decorates', $decorated);

            $decorationOnInvalid = $decoratedService[3] ?? ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE;
            if (\in_array($decorationOnInvalid, [ContainerInterface::IGNORE_ON_INVALID_REFERENCE, ContainerInterface::NULL_ON_INVALID_REFERENCE], true)) {
                $invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE === $decorationOnInvalid ? 'null' : 'ignore';
                $service->setAttribute('decoration-on-invalid', $invalidBehavior);
            }
            if (null !== $renamedId) {
                $service->setAttribute('decoration-inner-name', $renamedId);
            }
            if (0 !== $priority) {
                $service->setAttribute('decoration-priority', $priority);
            }
        }

        $tags = $definition->getTags();
        $tags['container.error'] = array_map(static fn ($e) => ['message' => $e], $definition->getErrors());
        foreach ($tags as $name => $tags) {
            foreach ($tags as $attributes) {
                $tag = $this->document->createElement('tag');

                // Check if we have recursive attributes
                if (array_filter($attributes, \is_array(...))) {
                    $tag->setAttribute('name', $name);
                    $this->addTagRecursiveAttributes($tag, $attributes);
                } else {
                    if (!\array_key_exists('name', $attributes)) {
                        $tag->setAttribute('name', $name);
                    } else {
                        $tag->appendChild($this->document->createTextNode($name));
                    }
                    foreach ($attributes as $key => $value) {
                        $tag->setAttribute($key, $value ?? '');
                    }
                }
                $service->appendChild($tag);
            }
        }

        if ($definition->getFile()) {
            $file = $this->document->createElement('file');
            $file->appendChild($this->document->createTextNode($definition->getFile()));
            $service->appendChild($file);
        }

        if ($parameters = $definition->getArguments()) {
            $this->convertParameters($parameters, 'argument', $service);
        }

        if ($parameters = $definition->getProperties()) {
            $this->convertParameters($parameters, 'property', $service, 'name');
        }

        $this->addMethodCalls($definition->getMethodCalls(), $service);

        if ($callable = $definition->getFactory()) {
            if (\is_array($callable) && ['Closure', 'fromCallable'] !== $callable && $definition->getClass() === $callable[0]) {
                $service->setAttribute('constructor', $callable[1]);
            } else {
                $factory = $this->document->createElement('factory');

                if (\is_array($callable) && $callable[0] instanceof Definition) {
                    $this->addService($callable[0], null, $factory);
                    $factory->setAttribute('method', $callable[1]);
                } elseif (\is_array($callable)) {
                    if (null !== $callable[0]) {
                        $factory->setAttribute($callable[0] instanceof Reference ? 'service' : 'class', $callable[0]);
                    }
                    $factory->setAttribute('method', $callable[1]);
                } else {
                    $factory->setAttribute('function', $callable);
                }
                $service->appendChild($factory);
            }
        }

        if ($definition->isDeprecated()) {
            $deprecation = $definition->getDeprecation('%service_id%');
            $deprecated = $this->document->createElement('deprecated');
            $deprecated->appendChild($this->document->createTextNode($definition->getDeprecation('%service_id%')['message']));
            $deprecated->setAttribute('package', $deprecation['package']);
            $deprecated->setAttribute('version', $deprecation['version']);

            $service->appendChild($deprecated);
        }

        if ($definition->isAutowired()) {
            $service->setAttribute('autowire', 'true');
        }

        if ($definition->isAutoconfigured()) {
            $service->setAttribute('autoconfigure', 'true');
        }

        if ($definition->isAbstract()) {
            $service->setAttribute('abstract', 'true');
        }

        if ($callable = $definition->getConfigurator()) {
            $configurator = $this->document->createElement('configurator');

            if (\is_array($callable) && $callable[0] instanceof Definition) {
                $this->addService($callable[0], null, $configurator);
                $configurator->setAttribute('method', $callable[1]);
            } elseif (\is_array($callable)) {
                $configurator->setAttribute($callable[0] instanceof Reference ? 'service' : 'class', $callable[0]);
                $configurator->setAttribute('method', $callable[1]);
            } else {
                $configurator->setAttribute('function', $callable);
            }
            $service->appendChild($configurator);
        }

        $parent->appendChild($service);
    }

    private function addServiceAlias(string $alias, Alias $id, \DOMElement $parent): void
    {
        $service = $this->document->createElement('service');
        $service->setAttribute('id', $alias);
        $service->setAttribute('alias', $id);
        if ($id->isPublic()) {
            $service->setAttribute('public', 'true');
        }

        if ($id->isDeprecated()) {
            $deprecation = $id->getDeprecation('%alias_id%');
            $deprecated = $this->document->createElement('deprecated');
            $deprecated->appendChild($this->document->createTextNode($deprecation['message']));
            $deprecated->setAttribute('package', $deprecation['package']);
            $deprecated->setAttribute('version', $deprecation['version']);

            $service->appendChild($deprecated);
        }

        $parent->appendChild($service);
    }

    private function addServices(\DOMElement $parent): void
    {
        $definitions = $this->container->getDefinitions();
        if (!$definitions) {
            return;
        }

        $services = $this->document->createElement('services');
        foreach ($definitions as $id => $definition) {
            $this->addService($definition, $id, $services);
        }

        $aliases = $this->container->getAliases();
        foreach ($aliases as $alias => $id) {
            while (isset($aliases[(string) $id])) {
                $id = $aliases[(string) $id];
            }
            $this->addServiceAlias($alias, $id, $services);
        }
        $parent->appendChild($services);
    }

    private function addTagRecursiveAttributes(\DOMElement $parent, array $attributes): void
    {
        foreach ($attributes as $name => $value) {
            $attribute = $this->document->createElement('attribute');
            $attribute->setAttribute('name', $name);

            if (\is_array($value)) {
                $this->addTagRecursiveAttributes($attribute, $value);
            } else {
                $attribute->appendChild($this->document->createTextNode($value));
            }

            $parent->appendChild($attribute);
        }
    }

    private function convertParameters(array $parameters, string $type, \DOMElement $parent, string $keyAttribute = 'key'): void
    {
        $withKeys = !array_is_list($parameters);
        foreach ($parameters as $key => $value) {
            $element = $this->document->createElement($type);
            if ($withKeys) {
                $element->setAttribute($keyAttribute, $key);
            }

            if (\is_array($tag = $value)) {
                $element->setAttribute('type', 'collection');
                $this->convertParameters($value, $type, $element, 'key');
            } elseif ($value instanceof TaggedIteratorArgument || ($value instanceof ServiceLocatorArgument && $tag = $value->getTaggedIteratorArgument())) {
                $element->setAttribute('type', $value instanceof TaggedIteratorArgument ? 'tagged_iterator' : 'tagged_locator');
                $element->setAttribute('tag', $tag->getTag());

                if (null !== $tag->getIndexAttribute()) {
                    $element->setAttribute('index-by', $tag->getIndexAttribute());

                    if (null !== $tag->getDefaultIndexMethod()) {
                        $element->setAttribute('default-index-method', $tag->getDefaultIndexMethod());
                    }
                    if (null !== $tag->getDefaultPriorityMethod()) {
                        $element->setAttribute('default-priority-method', $tag->getDefaultPriorityMethod());
                    }
                }
                if ($excludes = $tag->getExclude()) {
                    if (1 === \count($excludes)) {
                        $element->setAttribute('exclude', $excludes[0]);
                    } else {
                        foreach ($excludes as $exclude) {
                            $element->appendChild($this->document->createElement('exclude', $exclude));
                        }
                    }
                }
                if (!$tag->excludeSelf()) {
                    $element->setAttribute('exclude-self', 'false');
                }
            } elseif ($value instanceof IteratorArgument) {
                $element->setAttribute('type', 'iterator');
                $this->convertParameters($value->getValues(), $type, $element, 'key');
            } elseif ($value instanceof ServiceLocatorArgument) {
                $element->setAttribute('type', 'service_locator');
                $this->convertParameters($value->getValues(), $type, $element, 'key');
            } elseif ($value instanceof ServiceClosureArgument && !$value->getValues()[0] instanceof Reference) {
                $element->setAttribute('type', 'service_closure');
                $this->convertParameters($value->getValues(), $type, $element, 'key');
            } elseif ($value instanceof Reference || $value instanceof ServiceClosureArgument) {
                $element->setAttribute('type', 'service');
                if ($value instanceof ServiceClosureArgument) {
                    $element->setAttribute('type', 'service_closure');
                    $value = $value->getValues()[0];
                }
                $element->setAttribute('id', (string) $value);
                $behavior = $value->getInvalidBehavior();
                if (ContainerInterface::NULL_ON_INVALID_REFERENCE == $behavior) {
                    $element->setAttribute('on-invalid', 'null');
                } elseif (ContainerInterface::IGNORE_ON_INVALID_REFERENCE == $behavior) {
                    $element->setAttribute('on-invalid', 'ignore');
                } elseif (ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE == $behavior) {
                    $element->setAttribute('on-invalid', 'ignore_uninitialized');
                }
            } elseif ($value instanceof Definition) {
                $element->setAttribute('type', 'service');
                $this->addService($value, null, $element);
            } elseif ($value instanceof Expression) {
                $element->setAttribute('type', 'expression');
                $text = $this->document->createTextNode(self::phpToXml((string) $value));
                $element->appendChild($text);
            } elseif (\is_string($value) && !preg_match('/^[^\x00-\x08\x0B\x0C\x0E-\x1F\x7F]*+$/u', $value)) {
                $element->setAttribute('type', 'binary');
                $text = $this->document->createTextNode(self::phpToXml(base64_encode($value)));
                $element->appendChild($text);
            } elseif ($value instanceof \UnitEnum) {
                $element->setAttribute('type', 'constant');
                $element->appendChild($this->document->createTextNode(self::phpToXml($value)));
            } elseif ($value instanceof AbstractArgument) {
                $element->setAttribute('type', 'abstract');
                $text = $this->document->createTextNode(self::phpToXml($value->getText()));
                $element->appendChild($text);
            } else {
                if (\in_array($value, ['null', 'true', 'false'], true)) {
                    $element->setAttribute('type', 'string');
                }

                if (\is_string($value) && (is_numeric($value) || preg_match('/^0b[01]*$/', $value) || preg_match('/^0x[0-9a-f]++$/i', $value))) {
                    $element->setAttribute('type', 'string');
                }

                $text = $this->document->createTextNode(self::phpToXml($value));
                $element->appendChild($text);
            }
            $parent->appendChild($element);
        }
    }

    /**
     * Escapes arguments.
     */
    private function escape(array $arguments): array
    {
        $args = [];
        foreach ($arguments as $k => $v) {
            if (\is_array($v)) {
                $args[$k] = $this->escape($v);
            } elseif (\is_string($v)) {
                $args[$k] = str_replace('%', '%%', $v);
            } else {
                $args[$k] = $v;
            }
        }

        return $args;
    }

    /**
     * Converts php types to xml types.
     *
     * @throws RuntimeException When trying to dump object or resource
     */
    public static function phpToXml(mixed $value): string
    {
        switch (true) {
            case null === $value:
                return 'null';
            case true === $value:
                return 'true';
            case false === $value:
                return 'false';
            case $value instanceof Parameter:
                return '%'.$value.'%';
            case $value instanceof \UnitEnum:
                return \sprintf('%s::%s', $value::class, $value->name);
            case \is_object($value) || \is_resource($value):
                throw new RuntimeException(\sprintf('Unable to dump a service container if a parameter is an object or a resource, got "%s".', get_debug_type($value)));
            default:
                return (string) $value;
        }
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Dumper;

use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\Argument\AbstractArgument;
use Symfony\Component\DependencyInjection\Argument\ArgumentInterface;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\Parameter;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\ExpressionLanguage\Expression;
use Symfony\Component\Yaml\Dumper as YmlDumper;
use Symfony\Component\Yaml\Parser;
use Symfony\Component\Yaml\Tag\TaggedValue;
use Symfony\Component\Yaml\Yaml;

/**
 * YamlDumper dumps a service container as a YAML string.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class YamlDumper extends Dumper
{
    private YmlDumper $dumper;

    /**
     * Dumps the service container as an YAML string.
     */
    public function dump(array $options = []): string
    {
        if (!class_exists(YmlDumper::class)) {
            throw new LogicException('Unable to dump the container as the Symfony Yaml Component is not installed. Try running "composer require symfony/yaml".');
        }

        $this->dumper ??= new YmlDumper();

        return $this->addParameters()."\n".$this->addServices();
    }

    private function addService(string $id, Definition $definition): string
    {
        $code = "    {$this->dumper->dump($id)}:\n";
        if ($class = $definition->getClass()) {
            if (str_starts_with($class, '\\')) {
                $class = substr($class, 1);
            }

            $code .= \sprintf("        class: %s\n", $this->dumper->dump($this->container->resolveEnvPlaceholders($class)));
        }

        if (!$definition->isPrivate()) {
            $code .= \sprintf("        public: %s\n", $definition->isPublic() ? 'true' : 'false');
        }

        $tagsCode = '';
        $tags = $definition->getTags();
        $tags['container.error'] = array_map(static fn ($e) => ['message' => $e], $definition->getErrors());
        foreach ($tags as $name => $tags) {
            foreach ($tags as $attributes) {
                $att = [];
                foreach ($attributes as $key => $value) {
                    $att[] = \sprintf('%s: %s', $this->dumper->dump($key), $this->dumper->dump($value));
                }
                $att = $att ? ': { '.implode(', ', $att).' }' : '';

                $tagsCode .= \sprintf("            - %s%s\n", $this->dumper->dump($name), $att);
            }
        }
        if ($tagsCode) {
            $code .= "        tags:\n".$tagsCode;
        }

        if ($definition->getFile()) {
            $code .= \sprintf("        file: %s\n", $this->dumper->dump($this->container->resolveEnvPlaceholders($definition->getFile())));
        }

        if ($definition->isSynthetic()) {
            $code .= "        synthetic: true\n";
        }

        if ($definition->isDeprecated()) {
            $code .= "        deprecated:\n";
            foreach ($definition->getDeprecation('%service_id%') as $key => $value) {
                if ('' !== $value) {
                    $code .= \sprintf("            %s: %s\n", $key, $this->dumper->dump($value));
                }
            }
        }

        if ($definition->isAutowired()) {
            $code .= "        autowire: true\n";
        }

        if ($definition->isAutoconfigured()) {
            $code .= "        autoconfigure: true\n";
        }

        if ($definition->isAbstract()) {
            $code .= "        abstract: true\n";
        }

        if ($definition->isLazy()) {
            $code .= "        lazy: true\n";
        }

        if ($definition->getArguments()) {
            $code .= \sprintf("        arguments: %s\n", $this->dumper->dump($this->dumpValue($definition->getArguments()), 0));
        }

        if ($definition->getProperties()) {
            $code .= \sprintf("        properties: %s\n", $this->dumper->dump($this->dumpValue($definition->getProperties()), 0));
        }

        if ($definition->getMethodCalls()) {
            $code .= \sprintf("        calls:\n%s\n", $this->dumper->dump($this->dumpValue($definition->getMethodCalls()), 1, 12));
        }

        if (!$definition->isShared()) {
            $code .= "        shared: false\n";
        }

        if (null !== $decoratedService = $definition->getDecoratedService()) {
            [$decorated, $renamedId, $priority] = $decoratedService;
            $code .= \sprintf("        decorates: %s\n", $decorated);
            if (null !== $renamedId) {
                $code .= \sprintf("        decoration_inner_name: %s\n", $renamedId);
            }
            if (0 !== $priority) {
                $code .= \sprintf("        decoration_priority: %s\n", $priority);
            }

            $decorationOnInvalid = $decoratedService[3] ?? ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE;
            if (\in_array($decorationOnInvalid, [ContainerInterface::IGNORE_ON_INVALID_REFERENCE, ContainerInterface::NULL_ON_INVALID_REFERENCE])) {
                $invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE === $decorationOnInvalid ? 'null' : 'ignore';
                $code .= \sprintf("        decoration_on_invalid: %s\n", $invalidBehavior);
            }
        }

        if ($callable = $definition->getFactory()) {
            if (\is_array($callable) && ['Closure', 'fromCallable'] !== $callable && $definition->getClass() === $callable[0]) {
                $code .= \sprintf("        constructor: %s\n", $callable[1]);
            } else {
                $code .= \sprintf("        factory: %s\n", $this->dumper->dump($this->dumpCallable($callable), 0));
            }
        }

        if ($callable = $definition->getConfigurator()) {
            $code .= \sprintf("        configurator: %s\n", $this->dumper->dump($this->dumpCallable($callable), 0));
        }

        return $code;
    }

    private function addServiceAlias(string $alias, Alias $id): string
    {
        $deprecated = '';

        if ($id->isDeprecated()) {
            $deprecated = "        deprecated:\n";

            foreach ($id->getDeprecation('%alias_id%') as $key => $value) {
                if ('' !== $value) {
                    $deprecated .= \sprintf("            %s: %s\n", $key, $value);
                }
            }
        }

        if (!$id->isDeprecated() && $id->isPrivate()) {
            return \sprintf("    %s: '@%s'\n", $alias, $id);
        }

        if ($id->isPublic()) {
            $deprecated = "        public: true\n".$deprecated;
        }

        return \sprintf("    %s:\n        alias: %s\n%s", $alias, $id, $deprecated);
    }

    private function addServices(): string
    {
        if (!$this->container->getDefinitions()) {
            return '';
        }

        $code = "services:\n";
        foreach ($this->container->getDefinitions() as $id => $definition) {
            $code .= $this->addService($id, $definition);
        }

        $aliases = $this->container->getAliases();
        foreach ($aliases as $alias => $id) {
            while (isset($aliases[(string) $id])) {
                $id = $aliases[(string) $id];
            }
            $code .= $this->addServiceAlias($alias, $id);
        }

        return $code;
    }

    private function addParameters(): string
    {
        if (!$this->container->getParameterBag()->all()) {
            return '';
        }

        $parameters = $this->prepareParameters($this->container->getParameterBag()->all(), $this->container->isCompiled());

        return $this->dumper->dump(['parameters' => $parameters], 2);
    }

    /**
     * Dumps callable to YAML format.
     */
    private function dumpCallable(mixed $callable): mixed
    {
        if (\is_array($callable)) {
            if ($callable[0] instanceof Reference) {
                $callable = [$this->getServiceCall((string) $callable[0], $callable[0]), $callable[1]];
            } else {
                $callable = [$callable[0], $callable[1]];
            }
        }

        return $this->container->resolveEnvPlaceholders($callable);
    }

    /**
     * Dumps the value to YAML format.
     *
     * @throws RuntimeException When trying to dump object or resource
     */
    private function dumpValue(mixed $value): mixed
    {
        if ($value instanceof ServiceClosureArgument) {
            $value = $value->getValues()[0];

            return new TaggedValue('service_closure', $this->dumpValue($value));
        }
        if ($value instanceof ArgumentInterface) {
            $tag = $value;

            if ($value instanceof TaggedIteratorArgument || ($value instanceof ServiceLocatorArgument && $tag = $value->getTaggedIteratorArgument())) {
                if (null === $tag->getIndexAttribute()) {
                    $content = $tag->getTag();
                } else {
                    $content = [
                        'tag' => $tag->getTag(),
                        'index_by' => $tag->getIndexAttribute(),
                    ];

                    if (null !== $tag->getDefaultIndexMethod()) {
                        $content['default_index_method'] = $tag->getDefaultIndexMethod();
                    }
                    if (null !== $tag->getDefaultPriorityMethod()) {
                        $content['default_priority_method'] = $tag->getDefaultPriorityMethod();
                    }
                }
                if ($excludes = $tag->getExclude()) {
                    if (!\is_array($content)) {
                        $content = ['tag' => $content];
                    }
                    $content['exclude'] = 1 === \count($excludes) ? $excludes[0] : $excludes;
                }
                if (!$tag->excludeSelf()) {
                    $content['exclude_self'] = false;
                }

                return new TaggedValue($value instanceof TaggedIteratorArgument ? 'tagged_iterator' : 'tagged_locator', $content);
            }

            if ($value instanceof IteratorArgument) {
                $tag = 'iterator';
            } elseif ($value instanceof ServiceLocatorArgument) {
                $tag = 'service_locator';
            } else {
                throw new RuntimeException(\sprintf('Unspecified Yaml tag for type "%s".', get_debug_type($value)));
            }

            return new TaggedValue($tag, $this->dumpValue($value->getValues()));
        }

        if (\is_array($value)) {
            $code = [];
            foreach ($value as $k => $v) {
                $code[$this->container->resolveEnvPlaceholders($k)] = $this->dumpValue($v);
            }

            return $code;
        } elseif ($value instanceof Reference) {
            return $this->getServiceCall((string) $value, $value);
        } elseif ($value instanceof Parameter) {
            return $this->getParameterCall((string) $value);
        } elseif ($value instanceof Expression) {
            return $this->getExpressionCall((string) $value);
        } elseif ($value instanceof Definition) {
            return new TaggedValue('service', (new Parser())->parse("_:\n".$this->addService('_', $value), Yaml::PARSE_CUSTOM_TAGS)['_']['_']);
        } elseif ($value instanceof \UnitEnum) {
            return new TaggedValue('php/const', \sprintf('%s::%s', $value::class, $value->name));
        } elseif ($value instanceof AbstractArgument) {
            return new TaggedValue('abstract', $value->getText());
        } elseif (\is_object($value) || \is_resource($value)) {
            throw new RuntimeException(\sprintf('Unable to dump a service container if a parameter is an object or a resource, got "%s".', get_debug_type($value)));
        }

        return $this->container->resolveEnvPlaceholders($value);
    }

    private function getServiceCall(string $id, ?Reference $reference = null): string
    {
        if (null !== $reference) {
            switch ($reference->getInvalidBehavior()) {
                case ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE: break;
                case ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE: break;
                case ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE: return \sprintf('@!%s', $id);
                default: return \sprintf('@?%s', $id);
            }
        }

        return \sprintf('@%s', $id);
    }

    private function getParameterCall(string $id): string
    {
        return \sprintf('%%%s%%', $id);
    }

    private function getExpressionCall(string $expression): string
    {
        return \sprintf('@=%s', $expression);
    }

    private function prepareParameters(array $parameters, bool $escape = true): array
    {
        $filtered = [];
        foreach ($parameters as $key => $value) {
            if (\is_array($value)) {
                $value = $this->prepareParameters($value, $escape);
            } elseif ($value instanceof Reference || \is_string($value) && str_starts_with($value, '@')) {
                $value = '@'.$value;
            }

            $filtered[$key] = $value;
        }

        return $escape ? $this->container->resolveEnvPlaceholders($this->escape($filtered)) : $filtered;
    }

    private function escape(array $arguments): array
    {
        $args = [];
        foreach ($arguments as $k => $v) {
            if (\is_array($v)) {
                $args[$k] = $this->escape($v);
            } elseif (\is_string($v)) {
                $args[$k] = str_replace('%', '%%', $v);
            } else {
                $args[$k] = $v;
            }
        }

        return $args;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Dumper;

use Symfony\Component\DependencyInjection\Argument\ArgumentInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException;
use Symfony\Component\DependencyInjection\Parameter;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
use Symfony\Component\DependencyInjection\Reference;

/**
 * GraphvizDumper dumps a service container as a graphviz file.
 *
 * You can convert the generated dot file with the dot utility (http://www.graphviz.org/):
 *
 *   dot -Tpng container.dot > foo.png
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class GraphvizDumper extends Dumper
{
    private array $nodes;
    private array $edges;
    // All values should be strings
    private array $options = [
        'graph' => ['ratio' => 'compress'],
        'node' => ['fontsize' => '11', 'fontname' => 'Arial', 'shape' => 'record'],
        'edge' => ['fontsize' => '9', 'fontname' => 'Arial', 'color' => 'grey', 'arrowhead' => 'open', 'arrowsize' => '0.5'],
        'node.instance' => ['fillcolor' => '#9999ff', 'style' => 'filled'],
        'node.definition' => ['fillcolor' => '#eeeeee'],
        'node.missing' => ['fillcolor' => '#ff9999', 'style' => 'filled'],
    ];

    /**
     * Dumps the service container as a graphviz graph.
     *
     * Available options:
     *
     *  * graph: The default options for the whole graph
     *  * node: The default options for nodes
     *  * edge: The default options for edges
     *  * node.instance: The default options for services that are defined directly by object instances
     *  * node.definition: The default options for services that are defined via service definition instances
     *  * node.missing: The default options for missing services
     */
    public function dump(array $options = []): string
    {
        foreach (['graph', 'node', 'edge', 'node.instance', 'node.definition', 'node.missing'] as $key) {
            if (isset($options[$key])) {
                $this->options[$key] = array_merge($this->options[$key], $options[$key]);
            }
        }

        $this->nodes = $this->findNodes();

        $this->edges = [];
        foreach ($this->container->getDefinitions() as $id => $definition) {
            $this->edges[$id] = array_merge(
                $this->findEdges($id, $definition->getArguments(), true, ''),
                $this->findEdges($id, $definition->getProperties(), false, '')
            );

            foreach ($definition->getMethodCalls() as $call) {
                $this->edges[$id] = array_merge(
                    $this->edges[$id],
                    $this->findEdges($id, $call[1], false, $call[0].'()')
                );
            }
        }

        return $this->container->resolveEnvPlaceholders($this->startDot().$this->addNodes().$this->addEdges().$this->endDot(), '__ENV_%s__');
    }

    private function addNodes(): string
    {
        $code = '';
        foreach ($this->nodes as $id => $node) {
            $aliases = $this->getAliases($id);

            $code .= \sprintf("  node_%s [label=\"%s\\n%s\\n\", shape=%s%s];\n", $this->dotize($id), $id.($aliases ? ' ('.implode(', ', $aliases).')' : ''), $node['class'], $this->options['node']['shape'], $this->addAttributes($node['attributes']));
        }

        return $code;
    }

    private function addEdges(): string
    {
        $code = '';
        foreach ($this->edges as $id => $edges) {
            foreach ($edges as $edge) {
                $code .= \sprintf("  node_%s -> node_%s [label=\"%s\" style=\"%s\"%s];\n", $this->dotize($id), $this->dotize($edge['to']), $edge['name'], $edge['required'] ? 'filled' : 'dashed', $edge['lazy'] ? ' color="#9999ff"' : '');
            }
        }

        return $code;
    }

    /**
     * Finds all edges belonging to a specific service id.
     */
    private function findEdges(string $id, array $arguments, bool $required, string $name, bool $lazy = false): array
    {
        $edges = [];
        foreach ($arguments as $argument) {
            if ($argument instanceof Parameter) {
                $argument = $this->container->hasParameter($argument) ? $this->container->getParameter($argument) : null;
            } elseif (\is_string($argument) && preg_match('/^%([^%]+)%$/', $argument, $match)) {
                $argument = $this->container->hasParameter($match[1]) ? $this->container->getParameter($match[1]) : null;
            }

            if ($argument instanceof Reference) {
                $lazyEdge = $lazy;

                if (!$this->container->has((string) $argument)) {
                    $this->nodes[(string) $argument] = ['name' => $name, 'required' => $required, 'class' => '', 'attributes' => $this->options['node.missing']];
                } elseif ('service_container' !== (string) $argument) {
                    $lazyEdge = $lazy || $this->container->getDefinition((string) $argument)->isLazy();
                }

                $edges[] = [['name' => $name, 'required' => $required, 'to' => $argument, 'lazy' => $lazyEdge]];
            } elseif ($argument instanceof ArgumentInterface) {
                $edges[] = $this->findEdges($id, $argument->getValues(), $required, $name, true);
            } elseif ($argument instanceof Definition) {
                $edges[] = $this->findEdges($id, $argument->getArguments(), $required, '');
                $edges[] = $this->findEdges($id, $argument->getProperties(), false, '');

                foreach ($argument->getMethodCalls() as $call) {
                    $edges[] = $this->findEdges($id, $call[1], false, $call[0].'()');
                }
            } elseif (\is_array($argument)) {
                $edges[] = $this->findEdges($id, $argument, $required, $name, $lazy);
            }
        }

        return array_merge([], ...$edges);
    }

    private function findNodes(): array
    {
        $nodes = [];

        $container = $this->cloneContainer();

        foreach ($container->getDefinitions() as $id => $definition) {
            $class = $definition->getClass();

            if (str_starts_with($class, '\\')) {
                $class = substr($class, 1);
            }

            try {
                $class = $this->container->getParameterBag()->resolveValue($class);
            } catch (ParameterNotFoundException) {
            }

            $nodes[$id] = ['class' => str_replace('\\', '\\\\', $class), 'attributes' => array_merge($this->options['node.definition'], ['style' => $definition->isShared() ? 'filled' : 'dotted'])];
            $container->setDefinition($id, new Definition('stdClass'));
        }

        foreach ($container->getServiceIds() as $id) {
            if (\array_key_exists($id, $container->getAliases())) {
                continue;
            }

            if (!$container->hasDefinition($id)) {
                $nodes[$id] = ['class' => str_replace('\\', '\\\\', $container->get($id)::class), 'attributes' => $this->options['node.instance']];
            }
        }

        return $nodes;
    }

    private function cloneContainer(): ContainerBuilder
    {
        $parameterBag = new ParameterBag($this->container->getParameterBag()->all());

        $container = new ContainerBuilder($parameterBag);
        $container->setDefinitions($this->container->getDefinitions());
        $container->setAliases($this->container->getAliases());
        $container->setResources($this->container->getResources());
        foreach ($this->container->getExtensions() as $extension) {
            $container->registerExtension($extension);
        }

        return $container;
    }

    private function startDot(): string
    {
        return \sprintf("digraph sc {\n  %s\n  node [%s];\n  edge [%s];\n\n",
            $this->addOptions($this->options['graph']),
            $this->addOptions($this->options['node']),
            $this->addOptions($this->options['edge'])
        );
    }

    private function endDot(): string
    {
        return "}\n";
    }

    private function addAttributes(array $attributes): string
    {
        $code = [];
        foreach ($attributes as $k => $v) {
            $code[] = \sprintf('%s="%s"', $k, $v);
        }

        return $code ? ', '.implode(', ', $code) : '';
    }

    private function addOptions(array $options): string
    {
        $code = [];
        foreach ($options as $k => $v) {
            $code[] = \sprintf('%s="%s"', $k, $v);
        }

        return implode(' ', $code);
    }

    private function dotize(string $id): string
    {
        return preg_replace('/\W/i', '_', $id);
    }

    private function getAliases(string $id): array
    {
        $aliases = [];
        foreach ($this->container->getAliases() as $alias => $origin) {
            if ($id == $origin) {
                $aliases[] = $alias;
            }
        }

        return $aliases;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Dumper;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 */
final class Preloader
{
    public static function append(string $file, array $list): void
    {
        if (!file_exists($file)) {
            throw new \LogicException(\sprintf('File "%s" does not exist.', $file));
        }

        $cacheDir = \dirname($file);
        $classes = [];

        foreach ($list as $item) {
            if (str_starts_with($item, $cacheDir)) {
                file_put_contents($file, \sprintf("require_once __DIR__.%s;\n", var_export(strtr(substr($item, \strlen($cacheDir)), \DIRECTORY_SEPARATOR, '/'), true)), \FILE_APPEND);
                continue;
            }

            $classes[] = \sprintf("\$classes[] = %s;\n", var_export($item, true));
        }

        file_put_contents($file, \sprintf("\n\$classes = [];\n%s\$preloaded = Preloader::preload(\$classes, \$preloaded);\n", implode('', $classes)), \FILE_APPEND);
    }

    public static function preload(array $classes, array $preloaded = []): array
    {
        set_error_handler(static function ($t, $m, $f, $l) {
            if (error_reporting() & $t) {
                if (__FILE__ !== $f) {
                    throw new \ErrorException($m, 0, $t, $f, $l);
                }

                throw new \ReflectionException($m);
            }
        });

        $prev = [];

        try {
            while ($prev !== $classes) {
                $prev = $classes;
                foreach ($classes as $c) {
                    if (!isset($preloaded[$c])) {
                        self::doPreload($c, $preloaded);
                    }
                }
                $classes = array_merge(get_declared_classes(), get_declared_interfaces(), get_declared_traits());
            }
        } finally {
            restore_error_handler();
        }

        return $preloaded;
    }

    private static function doPreload(string $class, array &$preloaded): void
    {
        if (isset($preloaded[$class]) || \in_array($class, ['self', 'static', 'parent'], true)) {
            return;
        }

        $preloaded[$class] = true;

        try {
            if (!class_exists($class) && !interface_exists($class, false) && !trait_exists($class, false)) {
                return;
            }

            $r = new \ReflectionClass($class);

            if ($r->isInternal()) {
                return;
            }

            $r->getConstants();
            $r->getDefaultProperties();

            foreach ($r->getProperties(\ReflectionProperty::IS_PUBLIC) as $p) {
                self::preloadType($p->getType(), $preloaded);
            }

            foreach ($r->getMethods(\ReflectionMethod::IS_PUBLIC) as $m) {
                foreach ($m->getParameters() as $p) {
                    if ($p->isDefaultValueAvailable() && $p->isDefaultValueConstant()) {
                        $c = $p->getDefaultValueConstantName();

                        if ($i = strpos($c, '::')) {
                            self::doPreload(substr($c, 0, $i), $preloaded);
                        }
                    }

                    self::preloadType($p->getType(), $preloaded);
                }

                self::preloadType($m->getReturnType(), $preloaded);
            }
        } catch (\Throwable) {
            // ignore missing classes
        }
    }

    private static function preloadType(?\ReflectionType $t, array &$preloaded): void
    {
        if (!$t) {
            return;
        }

        foreach (($t instanceof \ReflectionUnionType || $t instanceof \ReflectionIntersectionType) ? $t->getTypes() : [$t] as $t) {
            if (!$t->isBuiltin()) {
                self::doPreload($t instanceof \ReflectionNamedType ? $t->getName() : $t, $preloaded);
            }
        }
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Loader;

use Symfony\Component\Config\Exception\FileLocatorFileNotFoundException;
use Symfony\Component\Config\Exception\LoaderLoadException;
use Symfony\Component\Config\FileLocatorInterface;
use Symfony\Component\Config\Loader\FileLoader as BaseFileLoader;
use Symfony\Component\Config\Loader\Loader;
use Symfony\Component\Config\Resource\GlobResource;
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\Attribute\AsAlias;
use Symfony\Component\DependencyInjection\Attribute\Exclude;
use Symfony\Component\DependencyInjection\Attribute\When;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Compiler\RegisterAutoconfigureAttributesPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\LogicException;

/**
 * FileLoader is the abstract class used by all built-in loaders that are file based.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
abstract class FileLoader extends BaseFileLoader
{
    public const ANONYMOUS_ID_REGEXP = ContainerBuilder::ANONYMOUS_ID_REGEXP;

    protected $container;
    protected $isLoadingInstanceof = false;
    protected $instanceof = [];
    protected $interfaces = [];
    protected $singlyImplemented = [];
    /** @var array<string, Alias> */
    protected $aliases = [];
    protected $autoRegisterAliasesForSinglyImplementedInterfaces = true;

    public function __construct(ContainerBuilder $container, FileLocatorInterface $locator, ?string $env = null)
    {
        $this->container = $container;

        parent::__construct($locator, $env);
    }

    /**
     * @param bool|string $ignoreErrors Whether errors should be ignored; pass "not_found" to ignore only when the loaded resource is not found
     */
    public function import(mixed $resource, ?string $type = null, bool|string $ignoreErrors = false, ?string $sourceResource = null, $exclude = null): mixed
    {
        $args = \func_get_args();

        if ($ignoreNotFound = 'not_found' === $ignoreErrors) {
            $args[2] = false;
        } elseif (!\is_bool($ignoreErrors)) {
            throw new \TypeError(\sprintf('Invalid argument $ignoreErrors provided to "%s::import()": boolean or "not_found" expected, "%s" given.', static::class, get_debug_type($ignoreErrors)));
        }

        try {
            return parent::import(...$args);
        } catch (LoaderLoadException $e) {
            if (!$ignoreNotFound || !($prev = $e->getPrevious()) instanceof FileLocatorFileNotFoundException) {
                throw $e;
            }

            foreach ($prev->getTrace() as $frame) {
                if ('import' === ($frame['function'] ?? null) && is_a($frame['class'] ?? '', Loader::class, true)) {
                    break;
                }
            }

            if (__FILE__ !== $frame['file']) {
                throw $e;
            }
        }

        return null;
    }

    /**
     * Registers a set of classes as services using PSR-4 for discovery.
     *
     * @param Definition           $prototype A definition to use as template
     * @param string               $namespace The namespace prefix of classes in the scanned directory
     * @param string               $resource  The directory to look for classes, glob-patterns allowed
     * @param string|string[]|null $exclude   A globbed path of files to exclude or an array of globbed paths of files to exclude
     * @param string|null          $source    The path to the file that defines the auto-discovery rule
     *
     * @return void
     */
    public function registerClasses(Definition $prototype, string $namespace, string $resource, string|array|null $exclude = null/* , string $source = null */)
    {
        if (!str_ends_with($namespace, '\\')) {
            throw new InvalidArgumentException(\sprintf('Namespace prefix must end with a "\\": "%s".', $namespace));
        }
        if (!preg_match('/^(?:[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+\\\\)++$/', $namespace)) {
            throw new InvalidArgumentException(\sprintf('Namespace is not a valid PSR-4 prefix: "%s".', $namespace));
        }
        // This can happen with YAML files
        if (\is_array($exclude) && \in_array(null, $exclude, true)) {
            throw new InvalidArgumentException('The exclude list must not contain a "null" value.');
        }
        // This can happen with XML files
        if (\is_array($exclude) && \in_array('', $exclude, true)) {
            throw new InvalidArgumentException('The exclude list must not contain an empty value.');
        }

        $source = \func_num_args() > 4 ? func_get_arg(4) : null;
        $autoconfigureAttributes = new RegisterAutoconfigureAttributesPass();
        $autoconfigureAttributes = $autoconfigureAttributes->accept($prototype) ? $autoconfigureAttributes : null;
        $classes = $this->findClasses($namespace, $resource, (array) $exclude, $autoconfigureAttributes, $source);

        $getPrototype = static fn () => clone $prototype;
        $serialized = serialize($prototype);

        // avoid deep cloning if no definitions are nested
        if (strpos($serialized, 'O:48:"Symfony\Component\DependencyInjection\Definition"', 55)
            || strpos($serialized, 'O:53:"Symfony\Component\DependencyInjection\ChildDefinition"', 55)
        ) {
            // prepare for deep cloning
            foreach (['Arguments', 'Properties', 'MethodCalls', 'Configurator', 'Factory', 'Bindings'] as $key) {
                $serialized = serialize($prototype->{'get'.$key}());

                if (strpos($serialized, 'O:48:"Symfony\Component\DependencyInjection\Definition"')
                    || strpos($serialized, 'O:53:"Symfony\Component\DependencyInjection\ChildDefinition"')
                ) {
                    $getPrototype = static fn () => $getPrototype()->{'set'.$key}(unserialize($serialized, ['allowed_classes' => true]));
                }
            }
        }
        unset($serialized);

        foreach ($classes as $class => $errorMessage) {
            if (null === $errorMessage && $autoconfigureAttributes) {
                $r = $this->container->getReflectionClass($class);
                if ($r->getAttributes(Exclude::class)[0] ?? null) {
                    $this->addContainerExcludedTag($class, $source);
                    continue;
                }
                if ($this->env) {
                    $attribute = null;
                    foreach ($r->getAttributes(When::class, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
                        if ($this->env === $attribute->newInstance()->env) {
                            $attribute = null;
                            break;
                        }
                    }
                    if (null !== $attribute) {
                        $this->addContainerExcludedTag($class, $source);
                        continue;
                    }
                }
            }

            if (interface_exists($class, false)) {
                $this->interfaces[] = $class;
            } else {
                $this->setDefinition($class, $definition = $getPrototype());
                $definition->setClass($class);
                if (null !== $errorMessage) {
                    $definition->addError($errorMessage);

                    continue;
                }

                $interfaces = [];
                foreach (class_implements($class, false) as $interface) {
                    $this->singlyImplemented[$interface] = ($this->singlyImplemented[$interface] ?? $class) !== $class ? false : $class;
                    $interfaces[] = $interface;
                }

                if (!$autoconfigureAttributes) {
                    continue;
                }
                $r = $this->container->getReflectionClass($class);
                $defaultAlias = 1 === \count($interfaces) ? $interfaces[0] : null;
                foreach ($r->getAttributes(AsAlias::class) as $attr) {
                    /** @var AsAlias $attribute */
                    $attribute = $attr->newInstance();
                    $alias = $attribute->id ?? $defaultAlias;
                    $public = $attribute->public;
                    if (null === $alias) {
                        throw new LogicException(\sprintf('Alias cannot be automatically determined for class "%s". If you have used the #[AsAlias] attribute with a class implementing multiple interfaces, add the interface you want to alias to the first parameter of #[AsAlias].', $class));
                    }
                    if (isset($this->aliases[$alias])) {
                        throw new LogicException(\sprintf('The "%s" alias has already been defined with the #[AsAlias] attribute in "%s".', $alias, $this->aliases[$alias]));
                    }
                    $this->aliases[$alias] = new Alias($class, $public);
                }
            }
        }

        foreach ($this->aliases as $alias => $aliasDefinition) {
            $this->container->setAlias($alias, $aliasDefinition);
        }

        if ($this->autoRegisterAliasesForSinglyImplementedInterfaces) {
            $this->registerAliasesForSinglyImplementedInterfaces();
        }
    }

    /**
     * @return void
     */
    public function registerAliasesForSinglyImplementedInterfaces()
    {
        foreach ($this->interfaces as $interface) {
            if (!empty($this->singlyImplemented[$interface]) && !isset($this->aliases[$interface]) && !$this->container->has($interface)) {
                $this->container->setAlias($interface, $this->singlyImplemented[$interface]);
            }
        }

        $this->interfaces = $this->singlyImplemented = $this->aliases = [];
    }

    /**
     * Registers a definition in the container with its instanceof-conditionals.
     *
     * @return void
     */
    protected function setDefinition(string $id, Definition $definition)
    {
        $this->container->removeBindings($id);

        foreach ($definition->getTag('container.error') as $error) {
            if (isset($error['message'])) {
                $definition->addError($error['message']);
            }
        }

        if ($this->isLoadingInstanceof) {
            if (!$definition instanceof ChildDefinition) {
                throw new InvalidArgumentException(\sprintf('Invalid type definition "%s": ChildDefinition expected, "%s" given.', $id, get_debug_type($definition)));
            }
            $this->instanceof[$id] = $definition;
        } else {
            $this->container->setDefinition($id, $definition->setInstanceofConditionals($this->instanceof));
        }
    }

    private function findClasses(string $namespace, string $pattern, array $excludePatterns, ?RegisterAutoconfigureAttributesPass $autoconfigureAttributes, ?string $source): array
    {
        $parameterBag = $this->container->getParameterBag();

        $excludePaths = [];
        $excludePrefix = null;
        $excludePatterns = $parameterBag->unescapeValue($parameterBag->resolveValue($excludePatterns));
        foreach ($excludePatterns as $excludePattern) {
            foreach ($this->glob($excludePattern, true, $resource, true, true) as $path => $info) {
                $excludePrefix ??= $resource->getPrefix();

                // normalize Windows slashes and remove trailing slashes
                $excludePaths[rtrim(str_replace('\\', '/', $path), '/')] = true;
            }
        }

        $pattern = $parameterBag->unescapeValue($parameterBag->resolveValue($pattern));
        $classes = [];
        $prefixLen = null;
        foreach ($this->glob($pattern, true, $resource, false, false, $excludePaths) as $path => $info) {
            if (null === $prefixLen) {
                $prefixLen = \strlen($resource->getPrefix());

                if ($excludePrefix && !str_starts_with($excludePrefix, $resource->getPrefix())) {
                    throw new InvalidArgumentException(\sprintf('Invalid "exclude" pattern when importing classes for "%s": make sure your "exclude" pattern (%s) is a subset of the "resource" pattern (%s).', $namespace, $excludePattern, $pattern));
                }
            }

            if (isset($excludePaths[str_replace('\\', '/', $path)])) {
                continue;
            }

            if (!str_ends_with($path, '.php')) {
                continue;
            }
            $class = $namespace.ltrim(str_replace('/', '\\', substr($path, $prefixLen, -4)), '\\');

            if (!preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\\\\[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+)*+$/', $class)) {
                continue;
            }

            try {
                $r = $this->container->getReflectionClass($class);
            } catch (\ReflectionException $e) {
                $classes[$class] = $e->getMessage();
                continue;
            }
            // check to make sure the expected class exists
            if (!$r) {
                throw new InvalidArgumentException(\sprintf('Expected to find class "%s" in file "%s" while importing services from resource "%s", but it was not found! Check the namespace prefix used with the resource.', $class, $path, $pattern));
            }

            if ($r->isInstantiable() || $r->isInterface()) {
                $classes[$class] = null;
            }

            if ($autoconfigureAttributes && !$r->isInstantiable()) {
                $autoconfigureAttributes->processClass($this->container, $r);
            }
        }

        // track only for new & removed files
        if ($resource instanceof GlobResource) {
            $this->container->addResource($resource);
        } else {
            foreach ($resource as $path) {
                $this->container->fileExists($path, false);
            }
        }

        if (null !== $prefixLen) {
            foreach ($excludePaths as $path => $_) {
                $class = $namespace.ltrim(str_replace('/', '\\', substr($path, $prefixLen, str_ends_with($path, '.php') ? -4 : null)), '\\');
                $this->addContainerExcludedTag($class, $source);
            }
        }

        return $classes;
    }

    private function addContainerExcludedTag(string $class, ?string $source): void
    {
        if ($this->container->has($class)) {
            return;
        }

        static $attributes = [];

        if (null !== $source && !isset($attributes[$source])) {
            $attributes[$source] = ['source' => \sprintf('in "%s/%s"', basename(\dirname($source)), basename($source))];
        }

        $this->container->register($class, $class)
            ->setAbstract(true)
            ->addTag('container.excluded', null !== $source ? $attributes[$source] : []);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Loader;

use Symfony\Component\Config\Loader\Loader;
use Symfony\Component\DependencyInjection\ContainerBuilder;

/**
 * ClosureLoader loads service definitions from a PHP closure.
 *
 * The Closure has access to the container as its first argument.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class ClosureLoader extends Loader
{
    private ContainerBuilder $container;

    public function __construct(ContainerBuilder $container, ?string $env = null)
    {
        $this->container = $container;
        parent::__construct($env);
    }

    public function load(mixed $resource, ?string $type = null): mixed
    {
        return $resource($this->container, $this->env);
    }

    public function supports(mixed $resource, ?string $type = null): bool
    {
        return $resource instanceof \Closure;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Loader;

/**
 * GlobFileLoader loads files from a glob pattern.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
class GlobFileLoader extends FileLoader
{
    public function load(mixed $resource, ?string $type = null): mixed
    {
        foreach ($this->glob($resource, false, $globResource) as $path => $info) {
            $this->import($path);
        }

        $this->container->addResource($globResource);

        return null;
    }

    public function supports(mixed $resource, ?string $type = null): bool
    {
        return 'glob' === $type;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Loader;

/**
 * DirectoryLoader is a recursive loader to go through directories.
 *
 * @author Sebastien Lavoie <seb@wemakecustom.com>
 */
class DirectoryLoader extends FileLoader
{
    public function load(mixed $file, ?string $type = null): mixed
    {
        $file = rtrim($file, '/');
        $path = $this->locator->locate($file);
        $this->container->fileExists($path, false);

        foreach (scandir($path) as $dir) {
            if ('.' !== $dir[0]) {
                if (is_dir($path.'/'.$dir)) {
                    $dir .= '/'; // append / to allow recursion
                }

                $this->setCurrentDir($path);

                $this->import($dir, null, false, $path);
            }
        }

        return null;
    }

    public function supports(mixed $resource, ?string $type = null): bool
    {
        if ('directory' === $type) {
            return true;
        }

        return null === $type && \is_string($resource) && str_ends_with($resource, '/');
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Loader;

use Symfony\Component\Config\Builder\ConfigBuilderGenerator;
use Symfony\Component\Config\Builder\ConfigBuilderGeneratorInterface;
use Symfony\Component\Config\Builder\ConfigBuilderInterface;
use Symfony\Component\Config\FileLocatorInterface;
use Symfony\Component\DependencyInjection\Attribute\When;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Extension\ConfigurationExtensionInterface;
use Symfony\Component\DependencyInjection\Extension\ExtensionInterface;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

/**
 * PhpFileLoader loads service definitions from a PHP file.
 *
 * The PHP file is required and the $container variable can be
 * used within the file to change the container.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class PhpFileLoader extends FileLoader
{
    protected $autoRegisterAliasesForSinglyImplementedInterfaces = false;
    private ?ConfigBuilderGeneratorInterface $generator;

    public function __construct(ContainerBuilder $container, FileLocatorInterface $locator, ?string $env = null, ?ConfigBuilderGeneratorInterface $generator = null)
    {
        parent::__construct($container, $locator, $env);
        $this->generator = $generator;
    }

    public function load(mixed $resource, ?string $type = null): mixed
    {
        // the container and loader variables are exposed to the included file below
        $container = $this->container;
        $loader = $this;

        $path = $this->locator->locate($resource);
        $this->setCurrentDir(\dirname($path));
        $this->container->fileExists($path);

        // the closure forbids access to the private scope in the included file
        $load = \Closure::bind(function ($path, $env) use ($container, $loader, $resource, $type) {
            return include $path;
        }, $this, ProtectedPhpFileLoader::class);

        $instanceof = $this->instanceof;
        $this->instanceof = [];

        try {
            $callback = $load($path, $this->env);

            if (\is_object($callback) && \is_callable($callback)) {
                $this->executeCallback($callback, new ContainerConfigurator($this->container, $this, $this->instanceof, $path, $resource, $this->env), $path);
            }
        } finally {
            $this->instanceof = $instanceof;
            $this->registerAliasesForSinglyImplementedInterfaces();
        }

        return null;
    }

    public function supports(mixed $resource, ?string $type = null): bool
    {
        if (!\is_string($resource)) {
            return false;
        }

        if (null === $type && 'php' === pathinfo($resource, \PATHINFO_EXTENSION)) {
            return true;
        }

        return 'php' === $type;
    }

    /**
     * Resolve the parameters to the $callback and execute it.
     */
    private function executeCallback(callable $callback, ContainerConfigurator $containerConfigurator, string $path): void
    {
        $callback = $callback(...);
        $arguments = [];
        $configBuilders = [];
        $r = new \ReflectionFunction($callback);

        $attribute = null;
        foreach ($r->getAttributes(When::class, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
            if ($this->env === $attribute->newInstance()->env) {
                $attribute = null;
                break;
            }
        }
        if (null !== $attribute) {
            return;
        }

        foreach ($r->getParameters() as $parameter) {
            $reflectionType = $parameter->getType();
            if (!$reflectionType instanceof \ReflectionNamedType) {
                throw new \InvalidArgumentException(\sprintf('Could not resolve argument "$%s" for "%s". You must typehint it (for example with "%s" or "%s").', $parameter->getName(), $path, ContainerConfigurator::class, ContainerBuilder::class));
            }
            $type = $reflectionType->getName();

            switch ($type) {
                case ContainerConfigurator::class:
                    $arguments[] = $containerConfigurator;
                    break;
                case ContainerBuilder::class:
                    $arguments[] = $this->container;
                    break;
                case FileLoader::class:
                case self::class:
                    $arguments[] = $this;
                    break;
                case 'string':
                    if (null !== $this->env && 'env' === $parameter->getName()) {
                        $arguments[] = $this->env;
                        break;
                    }
                    // no break
                default:
                    try {
                        $configBuilder = $this->configBuilder($type);
                    } catch (InvalidArgumentException|\LogicException $e) {
                        throw new \InvalidArgumentException(\sprintf('Could not resolve argument "%s" for "%s".', $type.' $'.$parameter->getName(), $path), 0, $e);
                    }
                    $configBuilders[] = $configBuilder;
                    $arguments[] = $configBuilder;
            }
        }

        // Force load ContainerConfigurator to make env(), param() etc available.
        class_exists(ContainerConfigurator::class);

        $callback(...$arguments);

        /** @var ConfigBuilderInterface $configBuilder */
        foreach ($configBuilders as $configBuilder) {
            $containerConfigurator->extension($configBuilder->getExtensionAlias(), $configBuilder->toArray());
        }
    }

    /**
     * @param string $namespace FQCN string for a class implementing ConfigBuilderInterface
     */
    private function configBuilder(string $namespace): ConfigBuilderInterface
    {
        if (!class_exists(ConfigBuilderGenerator::class)) {
            throw new \LogicException('You cannot use the config builder as the Config component is not installed. Try running "composer require symfony/config".');
        }

        if (null === $this->generator) {
            throw new \LogicException('You cannot use the ConfigBuilders without providing a class implementing ConfigBuilderGeneratorInterface.');
        }

        // If class exists and implements ConfigBuilderInterface
        if (class_exists($namespace) && is_subclass_of($namespace, ConfigBuilderInterface::class)) {
            return new $namespace();
        }

        // If it does not start with Symfony\Config\ we don't know how to handle this
        if (!str_starts_with($namespace, 'Symfony\\Config\\')) {
            throw new InvalidArgumentException(\sprintf('Could not find or generate class "%s".', $namespace));
        }

        // Try to get the extension alias
        $alias = Container::underscore(substr($namespace, 15, -6));

        if (str_contains($alias, '\\')) {
            throw new InvalidArgumentException('You can only use "root" ConfigBuilders from "Symfony\\Config\\" namespace. Nested classes like "Symfony\\Config\\Framework\\CacheConfig" cannot be used.');
        }

        if (!$this->container->hasExtension($alias)) {
            $extensions = array_filter(array_map(static fn (ExtensionInterface $ext) => $ext->getAlias(), $this->container->getExtensions()));
            throw new InvalidArgumentException(\sprintf('There is no extension able to load the configuration for "%s". Looked for namespace "%s", found "%s".', $namespace, $alias, $extensions ? implode('", "', $extensions) : 'none'));
        }

        $extension = $this->container->getExtension($alias);
        if (!$extension instanceof ConfigurationExtensionInterface) {
            throw new \LogicException(\sprintf('You cannot use the config builder for "%s" because the extension does not implement "%s".', $namespace, ConfigurationExtensionInterface::class));
        }

        $configuration = $extension->getConfiguration([], $this->container);
        $loader = $this->generator->build($configuration);

        return $loader();
    }
}

/**
 * @internal
 */
final class ProtectedPhpFileLoader extends PhpFileLoader
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Loader;

use Symfony\Component\Config\Util\XmlUtils;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;

/**
 * IniFileLoader loads parameters from INI files.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class IniFileLoader extends FileLoader
{
    public function load(mixed $resource, ?string $type = null): mixed
    {
        $path = $this->locator->locate($resource);

        $this->container->fileExists($path);

        // first pass to catch parsing errors
        $result = parse_ini_file($path, true);
        if (false === $result || [] === $result) {
            throw new InvalidArgumentException(\sprintf('The "%s" file is not valid.', $resource));
        }

        // real raw parsing
        $result = parse_ini_file($path, true, \INI_SCANNER_RAW);

        if (isset($result['parameters']) && \is_array($result['parameters'])) {
            foreach ($result['parameters'] as $key => $value) {
                if (\is_array($value)) {
                    $this->container->setParameter($key, array_map($this->phpize(...), $value));
                } else {
                    $this->container->setParameter($key, $this->phpize($value));
                }
            }
        }

        if ($this->env && \is_array($result['parameters@'.$this->env] ?? null)) {
            foreach ($result['parameters@'.$this->env] as $key => $value) {
                $this->container->setParameter($key, $this->phpize($value));
            }
        }

        return null;
    }

    public function supports(mixed $resource, ?string $type = null): bool
    {
        if (!\is_string($resource)) {
            return false;
        }

        if (null === $type && 'ini' === pathinfo($resource, \PATHINFO_EXTENSION)) {
            return true;
        }

        return 'ini' === $type;
    }

    /**
     * Note that the following features are not supported:
     *  * strings with escaped quotes are not supported "foo\"bar";
     *  * string concatenation ("foo" "bar").
     */
    private function phpize(string $value): mixed
    {
        // trim on the right as comments removal keep whitespaces
        if ($value !== $v = rtrim($value)) {
            $value = '""' === substr_replace($v, '', 1, -1) ? substr($v, 1, -1) : $v;
        }
        $lowercaseValue = strtolower($value);

        return match (true) {
            \defined($value) => \constant($value),
            'yes' === $lowercaseValue,
            'on' === $lowercaseValue => true,
            'no' === $lowercaseValue,
            'off' === $lowercaseValue,
            'none' === $lowercaseValue => false,
            isset($value[1]) && (
                ("'" === $value[0] && "'" === $value[\strlen($value) - 1])
                || ('"' === $value[0] && '"' === $value[\strlen($value) - 1])
            ) => substr($value, 1, -1), // quoted string
            default => XmlUtils::phpize($value),
        };
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Loader;

use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\Argument\AbstractArgument;
use Symfony\Component\DependencyInjection\Argument\BoundArgument;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\Extension\ExtensionInterface;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\ExpressionLanguage\Expression;
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Parser as YamlParser;
use Symfony\Component\Yaml\Tag\TaggedValue;
use Symfony\Component\Yaml\Yaml;

/**
 * YamlFileLoader loads YAML files service definitions.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class YamlFileLoader extends FileLoader
{
    private const SERVICE_KEYWORDS = [
        'alias' => 'alias',
        'parent' => 'parent',
        'class' => 'class',
        'shared' => 'shared',
        'synthetic' => 'synthetic',
        'lazy' => 'lazy',
        'public' => 'public',
        'abstract' => 'abstract',
        'deprecated' => 'deprecated',
        'factory' => 'factory',
        'file' => 'file',
        'arguments' => 'arguments',
        'properties' => 'properties',
        'configurator' => 'configurator',
        'calls' => 'calls',
        'tags' => 'tags',
        'decorates' => 'decorates',
        'decoration_inner_name' => 'decoration_inner_name',
        'decoration_priority' => 'decoration_priority',
        'decoration_on_invalid' => 'decoration_on_invalid',
        'autowire' => 'autowire',
        'autoconfigure' => 'autoconfigure',
        'bind' => 'bind',
        'constructor' => 'constructor',
    ];

    private const PROTOTYPE_KEYWORDS = [
        'resource' => 'resource',
        'namespace' => 'namespace',
        'exclude' => 'exclude',
        'parent' => 'parent',
        'shared' => 'shared',
        'lazy' => 'lazy',
        'public' => 'public',
        'abstract' => 'abstract',
        'deprecated' => 'deprecated',
        'factory' => 'factory',
        'arguments' => 'arguments',
        'properties' => 'properties',
        'configurator' => 'configurator',
        'calls' => 'calls',
        'tags' => 'tags',
        'autowire' => 'autowire',
        'autoconfigure' => 'autoconfigure',
        'bind' => 'bind',
        'constructor' => 'constructor',
    ];

    private const INSTANCEOF_KEYWORDS = [
        'shared' => 'shared',
        'lazy' => 'lazy',
        'public' => 'public',
        'properties' => 'properties',
        'configurator' => 'configurator',
        'calls' => 'calls',
        'tags' => 'tags',
        'autowire' => 'autowire',
        'bind' => 'bind',
        'constructor' => 'constructor',
    ];

    private const DEFAULTS_KEYWORDS = [
        'public' => 'public',
        'tags' => 'tags',
        'autowire' => 'autowire',
        'autoconfigure' => 'autoconfigure',
        'bind' => 'bind',
    ];

    private YamlParser $yamlParser;

    private int $anonymousServicesCount;
    private string $anonymousServicesSuffix;

    protected $autoRegisterAliasesForSinglyImplementedInterfaces = false;

    public function load(mixed $resource, ?string $type = null): mixed
    {
        $path = $this->locator->locate($resource);

        $content = $this->loadFile($path);

        $this->container->fileExists($path);

        // empty file
        if (null === $content) {
            return null;
        }

        $this->loadContent($content, $path);

        // per-env configuration
        if ($this->env && isset($content['when@'.$this->env])) {
            if (!\is_array($content['when@'.$this->env])) {
                throw new InvalidArgumentException(\sprintf('The "when@%s" key should contain an array in "%s". Check your YAML syntax.', $this->env, $path));
            }

            $env = $this->env;
            $this->env = null;
            try {
                $this->loadContent($content['when@'.$env], $path);
            } finally {
                $this->env = $env;
            }
        }

        return null;
    }

    private function loadContent(array $content, string $path): void
    {
        // imports
        $this->parseImports($content, $path);

        // parameters
        if (isset($content['parameters'])) {
            if (!\is_array($content['parameters'])) {
                throw new InvalidArgumentException(\sprintf('The "parameters" key should contain an array in "%s". Check your YAML syntax.', $path));
            }

            foreach ($content['parameters'] as $key => $value) {
                $this->container->setParameter($key, $this->resolveServices($value, $path, true));
            }
        }

        // extensions
        $this->loadFromExtensions($content);

        // services
        $this->anonymousServicesCount = 0;
        $this->anonymousServicesSuffix = '~'.ContainerBuilder::hash($path);
        $this->setCurrentDir(\dirname($path));
        try {
            $this->parseDefinitions($content, $path);
        } finally {
            $this->instanceof = [];
            $this->registerAliasesForSinglyImplementedInterfaces();
        }
    }

    public function supports(mixed $resource, ?string $type = null): bool
    {
        if (!\is_string($resource)) {
            return false;
        }

        if (null === $type && \in_array(pathinfo($resource, \PATHINFO_EXTENSION), ['yaml', 'yml'], true)) {
            return true;
        }

        return \in_array($type, ['yaml', 'yml'], true);
    }

    private function parseImports(array $content, string $file): void
    {
        if (!isset($content['imports'])) {
            return;
        }

        if (!\is_array($content['imports'])) {
            throw new InvalidArgumentException(\sprintf('The "imports" key should contain an array in "%s". Check your YAML syntax.', $file));
        }

        $defaultDirectory = \dirname($file);
        foreach ($content['imports'] as $import) {
            if (!\is_array($import)) {
                $import = ['resource' => $import];
            }
            if (!isset($import['resource'])) {
                throw new InvalidArgumentException(\sprintf('An import should provide a resource in "%s". Check your YAML syntax.', $file));
            }

            $this->setCurrentDir($defaultDirectory);
            $this->import($import['resource'], $import['type'] ?? null, $import['ignore_errors'] ?? false, $file);
        }
    }

    private function parseDefinitions(array $content, string $file, bool $trackBindings = true): void
    {
        if (!isset($content['services'])) {
            return;
        }

        if (!\is_array($content['services'])) {
            throw new InvalidArgumentException(\sprintf('The "services" key should contain an array in "%s". Check your YAML syntax.', $file));
        }

        if (\array_key_exists('_instanceof', $content['services'])) {
            $instanceof = $content['services']['_instanceof'];
            unset($content['services']['_instanceof']);

            if (!\is_array($instanceof)) {
                throw new InvalidArgumentException(\sprintf('Service "_instanceof" key must be an array, "%s" given in "%s".', get_debug_type($instanceof), $file));
            }
            $this->instanceof = [];
            $this->isLoadingInstanceof = true;
            foreach ($instanceof as $id => $service) {
                if (!$service || !\is_array($service)) {
                    throw new InvalidArgumentException(\sprintf('Type definition "%s" must be a non-empty array within "_instanceof" in "%s". Check your YAML syntax.', $id, $file));
                }
                if (\is_string($service) && str_starts_with($service, '@')) {
                    throw new InvalidArgumentException(\sprintf('Type definition "%s" cannot be an alias within "_instanceof" in "%s". Check your YAML syntax.', $id, $file));
                }
                $this->parseDefinition($id, $service, $file, [], false, $trackBindings);
            }
        }

        $this->isLoadingInstanceof = false;
        $defaults = $this->parseDefaults($content, $file);
        foreach ($content['services'] as $id => $service) {
            $this->parseDefinition($id, $service, $file, $defaults, false, $trackBindings);
        }
    }

    /**
     * @throws InvalidArgumentException
     */
    private function parseDefaults(array &$content, string $file): array
    {
        if (!\array_key_exists('_defaults', $content['services'])) {
            return [];
        }
        $defaults = $content['services']['_defaults'];
        unset($content['services']['_defaults']);

        if (!\is_array($defaults)) {
            throw new InvalidArgumentException(\sprintf('Service "_defaults" key must be an array, "%s" given in "%s".', get_debug_type($defaults), $file));
        }

        foreach ($defaults as $key => $default) {
            if (!isset(self::DEFAULTS_KEYWORDS[$key])) {
                throw new InvalidArgumentException(\sprintf('The configuration key "%s" cannot be used to define a default value in "%s". Allowed keys are "%s".', $key, $file, implode('", "', self::DEFAULTS_KEYWORDS)));
            }
        }

        if (isset($defaults['tags'])) {
            if (!\is_array($tags = $defaults['tags'])) {
                throw new InvalidArgumentException(\sprintf('Parameter "tags" in "_defaults" must be an array in "%s". Check your YAML syntax.', $file));
            }

            foreach ($tags as $tag) {
                if (!\is_array($tag)) {
                    $tag = ['name' => $tag];
                }

                if (1 === \count($tag) && \is_array(current($tag))) {
                    $name = key($tag);
                    $tag = current($tag);
                } else {
                    if (!isset($tag['name'])) {
                        throw new InvalidArgumentException(\sprintf('A "tags" entry in "_defaults" is missing a "name" key in "%s".', $file));
                    }
                    $name = $tag['name'];
                    unset($tag['name']);
                }

                if (!\is_string($name) || '' === $name) {
                    throw new InvalidArgumentException(\sprintf('The tag name in "_defaults" must be a non-empty string in "%s".', $file));
                }

                $this->validateAttributes(\sprintf('Tag "%s", attribute "%s" in "_defaults" must be of a scalar-type in "%s". Check your YAML syntax.', $name, '%s', $file), $tag);
            }
        }

        if (isset($defaults['bind'])) {
            if (!\is_array($defaults['bind'])) {
                throw new InvalidArgumentException(\sprintf('Parameter "bind" in "_defaults" must be an array in "%s". Check your YAML syntax.', $file));
            }

            foreach ($this->resolveServices($defaults['bind'], $file) as $argument => $value) {
                $defaults['bind'][$argument] = new BoundArgument($value, true, BoundArgument::DEFAULTS_BINDING, $file);
            }
        }

        return $defaults;
    }

    private function isUsingShortSyntax(array $service): bool
    {
        foreach ($service as $key => $value) {
            if (\is_string($key) && ('' === $key || ('$' !== $key[0] && !str_contains($key, '\\')))) {
                return false;
            }
        }

        return true;
    }

    /**
     * @throws InvalidArgumentException When tags are invalid
     */
    private function parseDefinition(string $id, array|string|null $service, string $file, array $defaults, bool $return = false, bool $trackBindings = true): Definition|Alias|null
    {
        if (preg_match('/^_[a-zA-Z0-9_]*$/', $id)) {
            throw new InvalidArgumentException(\sprintf('Service names that start with an underscore are reserved. Rename the "%s" service or define it in XML instead.', $id));
        }

        if (\is_string($service) && str_starts_with($service, '@')) {
            $alias = new Alias(substr($service, 1));

            if (isset($defaults['public'])) {
                $alias->setPublic($defaults['public']);
            }

            return $return ? $alias : $this->container->setAlias($id, $alias);
        }

        if (\is_array($service) && $this->isUsingShortSyntax($service)) {
            $service = ['arguments' => $service];
        }

        if (!\is_array($service ??= [])) {
            throw new InvalidArgumentException(\sprintf('A service definition must be an array or a string starting with "@" but "%s" found for service "%s" in "%s". Check your YAML syntax.', get_debug_type($service), $id, $file));
        }

        if (isset($service['stack'])) {
            if (!\is_array($service['stack'])) {
                throw new InvalidArgumentException(\sprintf('A stack must be an array of definitions, "%s" given for service "%s" in "%s". Check your YAML syntax.', get_debug_type($service), $id, $file));
            }

            $stack = [];

            foreach ($service['stack'] as $k => $frame) {
                if (\is_array($frame) && 1 === \count($frame) && !isset(self::SERVICE_KEYWORDS[key($frame)])) {
                    $frame = [
                        'class' => key($frame),
                        'arguments' => current($frame),
                    ];
                }

                if (\is_array($frame) && isset($frame['stack'])) {
                    throw new InvalidArgumentException(\sprintf('Service stack "%s" cannot contain another stack in "%s".', $id, $file));
                }

                $definition = $this->parseDefinition($id.'" at index "'.$k, $frame, $file, $defaults, true);

                if ($definition instanceof Definition) {
                    $definition->setInstanceofConditionals($this->instanceof);
                }

                $stack[$k] = $definition;
            }

            if ($diff = array_diff(array_keys($service), ['stack', 'public', 'deprecated'])) {
                throw new InvalidArgumentException(\sprintf('Invalid attribute "%s"; supported ones are "public" and "deprecated" for service "%s" in "%s". Check your YAML syntax.', implode('", "', $diff), $id, $file));
            }

            $service = [
                'parent' => '',
                'arguments' => $stack,
                'tags' => ['container.stack'],
                'public' => $service['public'] ?? null,
                'deprecated' => $service['deprecated'] ?? null,
            ];
        }

        $definition = isset($service[0]) && $service[0] instanceof Definition ? array_shift($service) : null;
        $return = null === $definition ? $return : true;

        if (isset($service['from_callable'])) {
            foreach (['alias', 'parent', 'synthetic', 'factory', 'file', 'arguments', 'properties', 'configurator', 'calls'] as $key) {
                if (isset($service['factory'])) {
                    throw new InvalidArgumentException(\sprintf('The configuration key "%s" is unsupported for the service "%s" when using "from_callable" in "%s".', $key, $id, $file));
                }
            }

            if ('Closure' !== $service['class'] ??= 'Closure') {
                $service['lazy'] = true;
            }

            $service['factory'] = ['Closure', 'fromCallable'];
            $service['arguments'] = [$service['from_callable']];
            unset($service['from_callable']);
        }

        $this->checkDefinition($id, $service, $file);

        if (isset($service['alias'])) {
            $alias = new Alias($service['alias']);

            if (isset($service['public'])) {
                $alias->setPublic($service['public']);
            } elseif (isset($defaults['public'])) {
                $alias->setPublic($defaults['public']);
            }

            foreach ($service as $key => $value) {
                if (!\in_array($key, ['alias', 'public', 'deprecated'])) {
                    throw new InvalidArgumentException(\sprintf('The configuration key "%s" is unsupported for the service "%s" which is defined as an alias in "%s". Allowed configuration keys for service aliases are "alias", "public" and "deprecated".', $key, $id, $file));
                }

                if ('deprecated' === $key) {
                    $deprecation = \is_array($value) ? $value : ['message' => $value];

                    if (!isset($deprecation['package'])) {
                        throw new InvalidArgumentException(\sprintf('Missing attribute "package" of the "deprecated" option in "%s".', $file));
                    }

                    if (!isset($deprecation['version'])) {
                        throw new InvalidArgumentException(\sprintf('Missing attribute "version" of the "deprecated" option in "%s".', $file));
                    }

                    $alias->setDeprecated($deprecation['package'] ?? '', $deprecation['version'] ?? '', $deprecation['message'] ?? '');
                }
            }

            return $return ? $alias : $this->container->setAlias($id, $alias);
        }

        $changes = [];
        if (null !== $definition) {
            $changes = $definition->getChanges();
        } elseif ($this->isLoadingInstanceof) {
            $definition = new ChildDefinition('');
        } elseif (isset($service['parent'])) {
            if ('' !== $service['parent'] && '@' === $service['parent'][0]) {
                throw new InvalidArgumentException(\sprintf('The value of the "parent" option for the "%s" service must be the id of the service without the "@" prefix (replace "%s" with "%s").', $id, $service['parent'], substr($service['parent'], 1)));
            }

            $definition = new ChildDefinition($service['parent']);
        } else {
            $definition = new Definition();
        }

        if (isset($defaults['public'])) {
            $definition->setPublic($defaults['public']);
        }
        if (isset($defaults['autowire'])) {
            $definition->setAutowired($defaults['autowire']);
        }
        if (isset($defaults['autoconfigure'])) {
            $definition->setAutoconfigured($defaults['autoconfigure']);
        }

        $definition->setChanges($changes);

        if (isset($service['class'])) {
            $definition->setClass($service['class']);
        }

        if (isset($service['shared'])) {
            $definition->setShared($service['shared']);
        }

        if (isset($service['synthetic'])) {
            $definition->setSynthetic($service['synthetic']);
        }

        if (isset($service['lazy'])) {
            $definition->setLazy((bool) $service['lazy']);
            if (\is_string($service['lazy'])) {
                $definition->addTag('proxy', ['interface' => $service['lazy']]);
            }
        }

        if (isset($service['public'])) {
            $definition->setPublic($service['public']);
        }

        if (isset($service['abstract'])) {
            $definition->setAbstract($service['abstract']);
        }

        if (isset($service['deprecated'])) {
            $deprecation = \is_array($service['deprecated']) ? $service['deprecated'] : ['message' => $service['deprecated']];

            if (!isset($deprecation['package'])) {
                throw new InvalidArgumentException(\sprintf('Missing attribute "package" of the "deprecated" option in "%s".', $file));
            }

            if (!isset($deprecation['version'])) {
                throw new InvalidArgumentException(\sprintf('Missing attribute "version" of the "deprecated" option in "%s".', $file));
            }

            $definition->setDeprecated($deprecation['package'] ?? '', $deprecation['version'] ?? '', $deprecation['message'] ?? '');
        }

        if (isset($service['factory'])) {
            $definition->setFactory($this->parseCallable($service['factory'], 'factory', $id, $file));
        }

        if (isset($service['constructor'])) {
            if (null !== $definition->getFactory()) {
                throw new LogicException(\sprintf('The "%s" service cannot declare a factory as well as a constructor.', $id));
            }

            $definition->setFactory([null, $service['constructor']]);
        }

        if (isset($service['file'])) {
            $definition->setFile($service['file']);
        }

        if (isset($service['arguments'])) {
            $definition->setArguments($this->resolveServices($service['arguments'], $file));
        }

        if (isset($service['properties'])) {
            $definition->setProperties($this->resolveServices($service['properties'], $file));
        }

        if (isset($service['configurator'])) {
            $definition->setConfigurator($this->parseCallable($service['configurator'], 'configurator', $id, $file));
        }

        if (isset($service['calls'])) {
            if (!\is_array($service['calls'])) {
                throw new InvalidArgumentException(\sprintf('Parameter "calls" must be an array for service "%s" in "%s". Check your YAML syntax.', $id, $file));
            }

            foreach ($service['calls'] as $k => $call) {
                if (!\is_array($call) && (!\is_string($k) || !$call instanceof TaggedValue)) {
                    throw new InvalidArgumentException(\sprintf('Invalid method call for service "%s": expected map or array, "%s" given in "%s".', $id, $call instanceof TaggedValue ? '!'.$call->getTag() : get_debug_type($call), $file));
                }

                if (\is_string($k)) {
                    throw new InvalidArgumentException(\sprintf('Invalid method call for service "%s", did you forget a leading dash before "%s: ..." in "%s"?', $id, $k, $file));
                }

                if (isset($call['method']) && \is_string($call['method'])) {
                    $method = $call['method'];
                    $args = $call['arguments'] ?? [];
                    $returnsClone = $call['returns_clone'] ?? false;
                } else {
                    if (1 === \count($call) && \is_string(key($call))) {
                        $method = key($call);
                        $args = $call[$method];

                        if ($args instanceof TaggedValue) {
                            if ('returns_clone' !== $args->getTag()) {
                                throw new InvalidArgumentException(\sprintf('Unsupported tag "!%s", did you mean "!returns_clone" for service "%s" in "%s"?', $args->getTag(), $id, $file));
                            }

                            $returnsClone = true;
                            $args = $args->getValue();
                        } else {
                            $returnsClone = false;
                        }
                    } elseif (empty($call[0])) {
                        throw new InvalidArgumentException(\sprintf('Invalid call for service "%s": the method must be defined as the first index of an array or as the only key of a map in "%s".', $id, $file));
                    } else {
                        $method = $call[0];
                        $args = $call[1] ?? [];
                        $returnsClone = $call[2] ?? false;
                    }
                }

                if (!\is_array($args)) {
                    throw new InvalidArgumentException(\sprintf('The second parameter for function call "%s" must be an array of its arguments for service "%s" in "%s". Check your YAML syntax.', $method, $id, $file));
                }

                $args = $this->resolveServices($args, $file);
                $definition->addMethodCall($method, $args, $returnsClone);
            }
        }

        $tags = $service['tags'] ?? [];
        if (!\is_array($tags)) {
            throw new InvalidArgumentException(\sprintf('Parameter "tags" must be an array for service "%s" in "%s". Check your YAML syntax.', $id, $file));
        }

        if (isset($defaults['tags'])) {
            $tags = array_merge($tags, $defaults['tags']);
        }

        foreach ($tags as $tag) {
            if (!\is_array($tag)) {
                $tag = ['name' => $tag];
            }

            if (1 === \count($tag) && \is_array(current($tag))) {
                $name = key($tag);
                $tag = current($tag);
            } else {
                if (!isset($tag['name'])) {
                    throw new InvalidArgumentException(\sprintf('A "tags" entry is missing a "name" key for service "%s" in "%s".', $id, $file));
                }
                $name = $tag['name'];
                unset($tag['name']);
            }

            if (!\is_string($name) || '' === $name) {
                throw new InvalidArgumentException(\sprintf('The tag name for service "%s" in "%s" must be a non-empty string.', $id, $file));
            }

            $this->validateAttributes(\sprintf('A "tags" attribute must be of a scalar-type for service "%s", tag "%s", attribute "%s" in "%s". Check your YAML syntax.', $id, $name, '%s', $file), $tag);

            $definition->addTag($name, $tag);
        }

        if (null !== $decorates = $service['decorates'] ?? null) {
            if ('' !== $decorates && '@' === $decorates[0]) {
                throw new InvalidArgumentException(\sprintf('The value of the "decorates" option for the "%s" service must be the id of the service without the "@" prefix (replace "%s" with "%s").', $id, $service['decorates'], substr($decorates, 1)));
            }

            $decorationOnInvalid = \array_key_exists('decoration_on_invalid', $service) ? $service['decoration_on_invalid'] : 'exception';
            if ('exception' === $decorationOnInvalid) {
                $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE;
            } elseif ('ignore' === $decorationOnInvalid) {
                $invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE;
            } elseif (null === $decorationOnInvalid) {
                $invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE;
            } elseif ('null' === $decorationOnInvalid) {
                throw new InvalidArgumentException(\sprintf('Invalid value "%s" for attribute "decoration_on_invalid" on service "%s". Did you mean null (without quotes) in "%s"?', $decorationOnInvalid, $id, $file));
            } else {
                throw new InvalidArgumentException(\sprintf('Invalid value "%s" for attribute "decoration_on_invalid" on service "%s". Did you mean "exception", "ignore" or null in "%s"?', $decorationOnInvalid, $id, $file));
            }

            $renameId = $service['decoration_inner_name'] ?? null;
            $priority = $service['decoration_priority'] ?? 0;

            $definition->setDecoratedService($decorates, $renameId, $priority, $invalidBehavior);
        }

        if (isset($service['autowire'])) {
            $definition->setAutowired($service['autowire']);
        }

        if (isset($defaults['bind']) || isset($service['bind'])) {
            // deep clone, to avoid multiple process of the same instance in the passes
            $bindings = $definition->getBindings();
            $bindings += isset($defaults['bind']) ? unserialize(serialize($defaults['bind']), ['allowed_classes' => true]) : [];

            if (isset($service['bind'])) {
                if (!\is_array($service['bind'])) {
                    throw new InvalidArgumentException(\sprintf('Parameter "bind" must be an array for service "%s" in "%s". Check your YAML syntax.', $id, $file));
                }

                $bindings = array_merge($bindings, $this->resolveServices($service['bind'], $file));
                $bindingType = $this->isLoadingInstanceof ? BoundArgument::INSTANCEOF_BINDING : BoundArgument::SERVICE_BINDING;
                foreach ($bindings as $argument => $value) {
                    if (!$value instanceof BoundArgument) {
                        $bindings[$argument] = new BoundArgument($value, $trackBindings, $bindingType, $file);
                    }
                }
            }

            $definition->setBindings($bindings);
        }

        if (isset($service['autoconfigure'])) {
            $definition->setAutoconfigured($service['autoconfigure']);
        }

        if (\array_key_exists('namespace', $service) && !\array_key_exists('resource', $service)) {
            throw new InvalidArgumentException(\sprintf('A "resource" attribute must be set when the "namespace" attribute is set for service "%s" in "%s". Check your YAML syntax.', $id, $file));
        }

        if ($return) {
            if (\array_key_exists('resource', $service)) {
                throw new InvalidArgumentException(\sprintf('Invalid "resource" attribute found for service "%s" in "%s". Check your YAML syntax.', $id, $file));
            }

            return $definition;
        }

        if (\array_key_exists('resource', $service)) {
            if (!\is_string($service['resource'])) {
                throw new InvalidArgumentException(\sprintf('A "resource" attribute must be of type string for service "%s" in "%s". Check your YAML syntax.', $id, $file));
            }
            $exclude = $service['exclude'] ?? null;
            $namespace = $service['namespace'] ?? $id;
            $this->registerClasses($definition, $namespace, $service['resource'], $exclude, $file);
        } else {
            $this->setDefinition($id, $definition);
        }

        return null;
    }

    /**
     * @throws InvalidArgumentException When errors occur
     */
    private function parseCallable(mixed $callable, string $parameter, string $id, string $file): string|array|Reference
    {
        if (\is_string($callable)) {
            if (str_starts_with($callable, '@=')) {
                if ('factory' !== $parameter) {
                    throw new InvalidArgumentException(\sprintf('Using expressions in "%s" for the "%s" service is not supported in "%s".', $parameter, $id, $file));
                }
                if (!class_exists(Expression::class)) {
                    throw new \LogicException('The "@=" expression syntax cannot be used without the ExpressionLanguage component. Try running "composer require symfony/expression-language".');
                }

                return $callable;
            }

            if ('' !== $callable && '@' === $callable[0]) {
                if (!str_contains($callable, ':')) {
                    return [$this->resolveServices($callable, $file), '__invoke'];
                }

                throw new InvalidArgumentException(\sprintf('The value of the "%s" option for the "%s" service must be the id of the service without the "@" prefix (replace "%s" with "%s" in "%s").', $parameter, $id, $callable, substr($callable, 1), $file));
            }

            return $callable;
        }

        if (\is_array($callable)) {
            if (isset($callable[0]) && isset($callable[1])) {
                return [$this->resolveServices($callable[0], $file), $callable[1]];
            }

            if ('factory' === $parameter && isset($callable[1]) && null === $callable[0]) {
                return $callable;
            }

            throw new InvalidArgumentException(\sprintf('Parameter "%s" must contain an array with two elements for service "%s" in "%s". Check your YAML syntax.', $parameter, $id, $file));
        }

        throw new InvalidArgumentException(\sprintf('Parameter "%s" must be a string or an array for service "%s" in "%s". Check your YAML syntax.', $parameter, $id, $file));
    }

    /**
     * Loads a YAML file.
     *
     * @throws InvalidArgumentException when the given file is not a local file or when it does not exist
     */
    protected function loadFile(string $file): ?array
    {
        if (!class_exists(YamlParser::class)) {
            throw new RuntimeException('Unable to load YAML config files as the Symfony Yaml Component is not installed. Try running "composer require symfony/yaml".');
        }

        if (!stream_is_local($file)) {
            throw new InvalidArgumentException(\sprintf('This is not a local file "%s".', $file));
        }

        if (!is_file($file)) {
            throw new InvalidArgumentException(\sprintf('The file "%s" does not exist.', $file));
        }

        $this->yamlParser ??= new YamlParser();

        try {
            $configuration = $this->yamlParser->parseFile($file, Yaml::PARSE_CONSTANT | Yaml::PARSE_CUSTOM_TAGS);
        } catch (ParseException $e) {
            throw new InvalidArgumentException(\sprintf('The file "%s" does not contain valid YAML: ', $file).$e->getMessage(), 0, $e);
        }

        return $this->validate($configuration, $file);
    }

    /**
     * Validates a YAML file.
     *
     * @throws InvalidArgumentException When service file is not valid
     */
    private function validate(mixed $content, string $file): ?array
    {
        if (null === $content) {
            return $content;
        }

        if (!\is_array($content)) {
            throw new InvalidArgumentException(\sprintf('The service file "%s" is not valid. It should contain an array. Check your YAML syntax.', $file));
        }

        foreach ($content as $namespace => $data) {
            if (\in_array($namespace, ['imports', 'parameters', 'services']) || str_starts_with($namespace, 'when@')) {
                continue;
            }

            if (!$this->container->hasExtension($namespace)) {
                $extensionNamespaces = array_filter(array_map(static fn (ExtensionInterface $ext) => $ext->getAlias(), $this->container->getExtensions()));
                throw new InvalidArgumentException(\sprintf('There is no extension able to load the configuration for "%s" (in "%s"). Looked for namespace "%s", found "%s".', $namespace, $file, $namespace, $extensionNamespaces ? \sprintf('"%s"', implode('", "', $extensionNamespaces)) : 'none'));
            }
        }

        return $content;
    }

    private function resolveServices(mixed $value, string $file, bool $isParameter = false): mixed
    {
        if ($value instanceof TaggedValue) {
            $argument = $value->getValue();

            if ('closure' === $value->getTag()) {
                $argument = $this->resolveServices($argument, $file, $isParameter);

                return (new Definition('Closure'))
                    ->setFactory(['Closure', 'fromCallable'])
                    ->addArgument($argument);
            }
            if ('iterator' === $value->getTag()) {
                if (!\is_array($argument)) {
                    throw new InvalidArgumentException(\sprintf('"!iterator" tag only accepts sequences in "%s".', $file));
                }
                $argument = $this->resolveServices($argument, $file, $isParameter);

                return new IteratorArgument($argument);
            }
            if ('service_closure' === $value->getTag()) {
                $argument = $this->resolveServices($argument, $file, $isParameter);

                return new ServiceClosureArgument($argument);
            }
            if ('service_locator' === $value->getTag()) {
                if (!\is_array($argument)) {
                    throw new InvalidArgumentException(\sprintf('"!service_locator" tag only accepts maps in "%s".', $file));
                }

                $argument = $this->resolveServices($argument, $file, $isParameter);

                if (isset($argument[0])) {
                    trigger_deprecation('symfony/dependency-injection', '6.3', 'Using integers as keys in a "!service_locator" tag is deprecated. The keys will default to the IDs of the original services in 7.0.');
                }

                return new ServiceLocatorArgument($argument);
            }
            if (\in_array($value->getTag(), ['tagged', 'tagged_iterator', 'tagged_locator'], true)) {
                $forLocator = 'tagged_locator' === $value->getTag();

                if (\is_array($argument) && isset($argument['tag']) && $argument['tag']) {
                    if ($diff = array_diff(array_keys($argument), $supportedKeys = ['tag', 'index_by', 'default_index_method', 'default_priority_method', 'exclude', 'exclude_self'])) {
                        throw new InvalidArgumentException(\sprintf('"!%s" tag contains unsupported key "%s"; supported ones are "%s".', $value->getTag(), implode('", "', $diff), implode('", "', $supportedKeys)));
                    }

                    $argument = new TaggedIteratorArgument($argument['tag'], $argument['index_by'] ?? null, $argument['default_index_method'] ?? null, $forLocator, $argument['default_priority_method'] ?? null, (array) ($argument['exclude'] ?? null), $argument['exclude_self'] ?? true);
                } elseif (\is_string($argument) && $argument) {
                    $argument = new TaggedIteratorArgument($argument, null, null, $forLocator);
                } else {
                    throw new InvalidArgumentException(\sprintf('"!%s" tags only accept a non empty string or an array with a key "tag" in "%s".', $value->getTag(), $file));
                }

                if ($forLocator) {
                    $argument = new ServiceLocatorArgument($argument);
                }

                return $argument;
            }
            if ('service' === $value->getTag()) {
                if ($isParameter) {
                    throw new InvalidArgumentException(\sprintf('Using an anonymous service in a parameter is not allowed in "%s".', $file));
                }

                $isLoadingInstanceof = $this->isLoadingInstanceof;
                $this->isLoadingInstanceof = false;
                $instanceof = $this->instanceof;
                $this->instanceof = [];

                $id = \sprintf('.%d_%s', ++$this->anonymousServicesCount, preg_replace('/^.*\\\\/', '', $argument['class'] ?? '').$this->anonymousServicesSuffix);
                $this->parseDefinition($id, $argument, $file, []);

                if (!$this->container->hasDefinition($id)) {
                    throw new InvalidArgumentException(\sprintf('Creating an alias using the tag "!service" is not allowed in "%s".', $file));
                }

                $this->container->getDefinition($id);

                $this->isLoadingInstanceof = $isLoadingInstanceof;
                $this->instanceof = $instanceof;

                return new Reference($id);
            }
            if ('abstract' === $value->getTag()) {
                return new AbstractArgument($value->getValue());
            }

            throw new InvalidArgumentException(\sprintf('Unsupported tag "!%s".', $value->getTag()));
        }

        if (\is_array($value)) {
            foreach ($value as $k => $v) {
                $value[$k] = $this->resolveServices($v, $file, $isParameter);
            }
        } elseif (\is_string($value) && str_starts_with($value, '@=')) {
            if ($isParameter) {
                throw new InvalidArgumentException(\sprintf('Using expressions in parameters is not allowed in "%s".', $file));
            }

            if (!class_exists(Expression::class)) {
                throw new \LogicException('The "@=" expression syntax cannot be used without the ExpressionLanguage component. Try running "composer require symfony/expression-language".');
            }

            return new Expression(substr($value, 2));
        } elseif (\is_string($value) && str_starts_with($value, '@')) {
            if (str_starts_with($value, '@@')) {
                $value = substr($value, 1);
                $invalidBehavior = null;
            } elseif (str_starts_with($value, '@!')) {
                $value = substr($value, 2);
                $invalidBehavior = ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE;
            } elseif (str_starts_with($value, '@?')) {
                $value = substr($value, 2);
                $invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE;
            } else {
                $value = substr($value, 1);
                $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE;
            }

            if (null !== $invalidBehavior) {
                $value = new Reference($value, $invalidBehavior);
            }
        }

        return $value;
    }

    private function loadFromExtensions(array $content): void
    {
        foreach ($content as $namespace => $values) {
            if (\in_array($namespace, ['imports', 'parameters', 'services']) || str_starts_with($namespace, 'when@')) {
                continue;
            }

            if (!\is_array($values) && null !== $values) {
                $values = [];
            }

            $this->container->loadFromExtension($namespace, $values);
        }
    }

    private function checkDefinition(string $id, array $definition, string $file): void
    {
        if ($this->isLoadingInstanceof) {
            $keywords = self::INSTANCEOF_KEYWORDS;
        } elseif (isset($definition['resource']) || isset($definition['namespace'])) {
            $keywords = self::PROTOTYPE_KEYWORDS;
        } else {
            $keywords = self::SERVICE_KEYWORDS;
        }

        foreach ($definition as $key => $value) {
            if (!isset($keywords[$key])) {
                throw new InvalidArgumentException(\sprintf('The configuration key "%s" is unsupported for definition "%s" in "%s". Allowed configuration keys are "%s".', $key, $id, $file, implode('", "', $keywords)));
            }
        }
    }

    private function validateAttributes(string $message, array $attributes, array $path = []): void
    {
        foreach ($attributes as $name => $value) {
            if (\is_array($value)) {
                $this->validateAttributes($message, $value, [...$path, $name]);
            } elseif (!\is_scalar($value ?? '')) {
                $name = implode('.', [...$path, $name]);
                throw new InvalidArgumentException(\sprintf($message, $name));
            }
        }
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Loader;

use Symfony\Component\Config\Util\XmlUtils;
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\Argument\AbstractArgument;
use Symfony\Component\DependencyInjection\Argument\BoundArgument;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\Extension\ExtensionInterface;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\ExpressionLanguage\Expression;

/**
 * XmlFileLoader loads XML files service definitions.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class XmlFileLoader extends FileLoader
{
    public const NS = 'http://symfony.com/schema/dic/services';

    protected $autoRegisterAliasesForSinglyImplementedInterfaces = false;

    public function load(mixed $resource, ?string $type = null): mixed
    {
        $path = $this->locator->locate($resource);

        $xml = $this->parseFileToDOM($path);

        $this->container->fileExists($path);

        $this->loadXml($xml, $path);

        if ($this->env) {
            $xpath = new \DOMXPath($xml);
            $xpath->registerNamespace('container', self::NS);
            foreach ($xpath->query(\sprintf('//container:when[@env="%s"]', $this->env)) ?: [] as $root) {
                $env = $this->env;
                $this->env = null;
                try {
                    $this->loadXml($xml, $path, $root);
                } finally {
                    $this->env = $env;
                }
            }
        }

        return null;
    }

    private function loadXml(\DOMDocument $xml, string $path, ?\DOMNode $root = null): void
    {
        $defaults = $this->getServiceDefaults($xml, $path, $root);

        // anonymous services
        $this->processAnonymousServices($xml, $path, $root);

        // imports
        $this->parseImports($xml, $path, $root);

        // parameters
        $this->parseParameters($xml, $path, $root);

        // extensions
        $this->loadFromExtensions($xml, $root);

        // services
        try {
            $this->parseDefinitions($xml, $path, $defaults, $root);
        } finally {
            $this->instanceof = [];
            $this->registerAliasesForSinglyImplementedInterfaces();
        }
    }

    public function supports(mixed $resource, ?string $type = null): bool
    {
        if (!\is_string($resource)) {
            return false;
        }

        if (null === $type && 'xml' === pathinfo($resource, \PATHINFO_EXTENSION)) {
            return true;
        }

        return 'xml' === $type;
    }

    private function parseParameters(\DOMDocument $xml, string $file, ?\DOMNode $root = null): void
    {
        if ($parameters = $this->getChildren($root ?? $xml->documentElement, 'parameters')) {
            $this->container->getParameterBag()->add($this->getArgumentsAsPhp($parameters[0], 'parameter', $file));
        }
    }

    private function parseImports(\DOMDocument $xml, string $file, ?\DOMNode $root = null): void
    {
        $xpath = new \DOMXPath($xml);
        $xpath->registerNamespace('container', self::NS);

        if (false === $imports = $xpath->query('./container:imports/container:import', $root)) {
            return;
        }

        $defaultDirectory = \dirname($file);
        foreach ($imports as $import) {
            $this->setCurrentDir($defaultDirectory);
            $this->import($import->getAttribute('resource'), XmlUtils::phpize($import->getAttribute('type')) ?: null, XmlUtils::phpize($import->getAttribute('ignore-errors')) ?: false, $file);
        }
    }

    private function parseDefinitions(\DOMDocument $xml, string $file, Definition $defaults, ?\DOMNode $root = null): void
    {
        $xpath = new \DOMXPath($xml);
        $xpath->registerNamespace('container', self::NS);

        if (false === $services = $xpath->query('./container:services/container:service|./container:services/container:prototype|./container:services/container:stack', $root)) {
            return;
        }
        $this->setCurrentDir(\dirname($file));

        $this->instanceof = [];
        $this->isLoadingInstanceof = true;
        $instanceof = $xpath->query('./container:services/container:instanceof', $root);
        foreach ($instanceof as $service) {
            $this->setDefinition((string) $service->getAttribute('id'), $this->parseDefinition($service, $file, new Definition()));
        }

        $this->isLoadingInstanceof = false;
        foreach ($services as $service) {
            if ('stack' === $service->tagName) {
                $service->setAttribute('parent', '-');
                $definition = $this->parseDefinition($service, $file, $defaults)
                    ->setTags(array_merge_recursive(['container.stack' => [[]]], $defaults->getTags()))
                ;
                $this->setDefinition($id = (string) $service->getAttribute('id'), $definition);
                $stack = [];

                foreach ($this->getChildren($service, 'service') as $k => $frame) {
                    $k = $frame->getAttribute('id') ?: $k;
                    $frame->setAttribute('id', $id.'" at index "'.$k);

                    if ($alias = $frame->getAttribute('alias')) {
                        $this->validateAlias($frame, $file);
                        $stack[$k] = new Reference($alias);
                    } else {
                        $stack[$k] = $this->parseDefinition($frame, $file, $defaults)
                            ->setInstanceofConditionals($this->instanceof);
                    }
                }

                $definition->setArguments($stack);
            } elseif (null !== $definition = $this->parseDefinition($service, $file, $defaults)) {
                if ('prototype' === $service->tagName) {
                    $excludes = array_column($this->getChildren($service, 'exclude'), 'nodeValue');
                    if ($service->hasAttribute('exclude')) {
                        if (\count($excludes) > 0) {
                            throw new InvalidArgumentException('You cannot use both the attribute "exclude" and <exclude> tags at the same time.');
                        }
                        $excludes = [$service->getAttribute('exclude')];
                    }
                    $this->registerClasses($definition, (string) $service->getAttribute('namespace'), (string) $service->getAttribute('resource'), $excludes, $file);
                } else {
                    $this->setDefinition((string) $service->getAttribute('id'), $definition);
                }
            }
        }
    }

    private function getServiceDefaults(\DOMDocument $xml, string $file, ?\DOMNode $root = null): Definition
    {
        $xpath = new \DOMXPath($xml);
        $xpath->registerNamespace('container', self::NS);

        if (null === $defaultsNode = $xpath->query('./container:services/container:defaults', $root)->item(0)) {
            return new Definition();
        }

        $defaultsNode->setAttribute('id', '<defaults>');

        return $this->parseDefinition($defaultsNode, $file, new Definition());
    }

    /**
     * Parses an individual Definition.
     */
    private function parseDefinition(\DOMElement $service, string $file, Definition $defaults): ?Definition
    {
        if ($alias = $service->getAttribute('alias')) {
            $this->validateAlias($service, $file);

            $this->container->setAlias($service->getAttribute('id'), $alias = new Alias($alias));
            if ($publicAttr = $service->getAttribute('public')) {
                $alias->setPublic(XmlUtils::phpize($publicAttr));
            } elseif ($defaults->getChanges()['public'] ?? false) {
                $alias->setPublic($defaults->isPublic());
            }

            if ($deprecated = $this->getChildren($service, 'deprecated')) {
                $message = $deprecated[0]->nodeValue ?: '';
                $package = $deprecated[0]->getAttribute('package') ?: '';
                $version = $deprecated[0]->getAttribute('version') ?: '';

                if (!$deprecated[0]->hasAttribute('package')) {
                    throw new InvalidArgumentException(\sprintf('Missing attribute "package" at node "deprecated" in "%s".', $file));
                }

                if (!$deprecated[0]->hasAttribute('version')) {
                    throw new InvalidArgumentException(\sprintf('Missing attribute "version" at node "deprecated" in "%s".', $file));
                }

                $alias->setDeprecated($package, $version, $message);
            }

            return null;
        }

        if ($this->isLoadingInstanceof) {
            $definition = new ChildDefinition('');
        } elseif ($parent = $service->getAttribute('parent')) {
            $definition = new ChildDefinition($parent);
        } else {
            $definition = new Definition();
        }

        if ($defaults->getChanges()['public'] ?? false) {
            $definition->setPublic($defaults->isPublic());
        }
        $definition->setAutowired($defaults->isAutowired());
        $definition->setAutoconfigured($defaults->isAutoconfigured());
        $definition->setChanges([]);

        foreach (['class', 'public', 'shared', 'synthetic', 'abstract'] as $key) {
            if ($value = $service->getAttribute($key)) {
                $method = 'set'.$key;
                $definition->$method(XmlUtils::phpize($value));
            }
        }

        if ($value = $service->getAttribute('lazy')) {
            $definition->setLazy((bool) $value = XmlUtils::phpize($value));
            if (\is_string($value)) {
                $definition->addTag('proxy', ['interface' => $value]);
            }
        }

        if ($value = $service->getAttribute('autowire')) {
            $definition->setAutowired(XmlUtils::phpize($value));
        }

        if ($value = $service->getAttribute('autoconfigure')) {
            $definition->setAutoconfigured(XmlUtils::phpize($value));
        }

        if ($files = $this->getChildren($service, 'file')) {
            $definition->setFile($files[0]->nodeValue);
        }

        if ($deprecated = $this->getChildren($service, 'deprecated')) {
            $message = $deprecated[0]->nodeValue ?: '';
            $package = $deprecated[0]->getAttribute('package') ?: '';
            $version = $deprecated[0]->getAttribute('version') ?: '';

            if (!$deprecated[0]->hasAttribute('package')) {
                throw new InvalidArgumentException(\sprintf('Missing attribute "package" at node "deprecated" in "%s".', $file));
            }

            if (!$deprecated[0]->hasAttribute('version')) {
                throw new InvalidArgumentException(\sprintf('Missing attribute "version" at node "deprecated" in "%s".', $file));
            }

            $definition->setDeprecated($package, $version, $message);
        }

        $definition->setArguments($this->getArgumentsAsPhp($service, 'argument', $file, $definition instanceof ChildDefinition));
        $definition->setProperties($this->getArgumentsAsPhp($service, 'property', $file));

        if ($factories = $this->getChildren($service, 'factory')) {
            $factory = $factories[0];
            if ($function = $factory->getAttribute('function')) {
                $definition->setFactory($function);
            } elseif ($expression = $factory->getAttribute('expression')) {
                if (!class_exists(Expression::class)) {
                    throw new \LogicException('The "expression" attribute cannot be used on factories without the ExpressionLanguage component. Try running "composer require symfony/expression-language".');
                }
                $definition->setFactory('@='.$expression);
            } else {
                if ($childService = $factory->getAttribute('service')) {
                    $class = new Reference($childService, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE);
                } else {
                    $class = $factory->hasAttribute('class') ? $factory->getAttribute('class') : null;
                }

                $definition->setFactory([$class, $factory->getAttribute('method') ?: '__invoke']);
            }
        }

        if ($constructor = $service->getAttribute('constructor')) {
            if (null !== $definition->getFactory()) {
                throw new LogicException(\sprintf('The "%s" service cannot declare a factory as well as a constructor.', $service->getAttribute('id')));
            }

            $definition->setFactory([null, $constructor]);
        }

        if ($configurators = $this->getChildren($service, 'configurator')) {
            $configurator = $configurators[0];
            if ($function = $configurator->getAttribute('function')) {
                $definition->setConfigurator($function);
            } else {
                if ($childService = $configurator->getAttribute('service')) {
                    $class = new Reference($childService, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE);
                } else {
                    $class = $configurator->getAttribute('class');
                }

                $definition->setConfigurator([$class, $configurator->getAttribute('method') ?: '__invoke']);
            }
        }

        foreach ($this->getChildren($service, 'call') as $call) {
            $definition->addMethodCall(
                $call->getAttribute('method'),
                $this->getArgumentsAsPhp($call, 'argument', $file),
                XmlUtils::phpize($call->getAttribute('returns-clone')) ?: false
            );
        }

        $tags = $this->getChildren($service, 'tag');

        foreach ($tags as $tag) {
            $tagNameComesFromAttribute = $tag->childElementCount || '' === $tag->nodeValue;
            if ('' === $tagName = $tagNameComesFromAttribute ? $tag->getAttribute('name') : $tag->nodeValue) {
                throw new InvalidArgumentException(\sprintf('The tag name for service "%s" in "%s" must be a non-empty string.', (string) $service->getAttribute('id'), $file));
            }

            $parameters = $this->getTagAttributes($tag, \sprintf('The attribute name of tag "%s" for service "%s" in %s must be a non-empty string.', $tagName, (string) $service->getAttribute('id'), $file));
            foreach ($tag->attributes as $name => $node) {
                if ($tagNameComesFromAttribute && 'name' === $name) {
                    continue;
                }

                if (str_contains($name, '-') && !str_contains($name, '_') && !\array_key_exists($normalizedName = str_replace('-', '_', $name), $parameters)) {
                    $parameters[$normalizedName] = XmlUtils::phpize($node->nodeValue);
                }
                // keep not normalized key
                $parameters[$name] = XmlUtils::phpize($node->nodeValue);
            }

            $definition->addTag($tagName, $parameters);
        }

        $definition->setTags(array_merge_recursive($definition->getTags(), $defaults->getTags()));

        $bindings = $this->getArgumentsAsPhp($service, 'bind', $file);
        $bindingType = $this->isLoadingInstanceof ? BoundArgument::INSTANCEOF_BINDING : BoundArgument::SERVICE_BINDING;
        foreach ($bindings as $argument => $value) {
            $bindings[$argument] = new BoundArgument($value, true, $bindingType, $file);
        }

        // deep clone, to avoid multiple process of the same instance in the passes
        $bindings = array_merge(unserialize(serialize($defaults->getBindings()), ['allowed_classes' => true]), $bindings);

        if ($bindings) {
            $definition->setBindings($bindings);
        }

        if ($decorates = $service->getAttribute('decorates')) {
            $decorationOnInvalid = $service->getAttribute('decoration-on-invalid') ?: 'exception';
            if ('exception' === $decorationOnInvalid) {
                $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE;
            } elseif ('ignore' === $decorationOnInvalid) {
                $invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE;
            } elseif ('null' === $decorationOnInvalid) {
                $invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE;
            } else {
                throw new InvalidArgumentException(\sprintf('Invalid value "%s" for attribute "decoration-on-invalid" on service "%s". Did you mean "exception", "ignore" or "null" in "%s"?', $decorationOnInvalid, $service->getAttribute('id'), $file));
            }

            $renameId = $service->hasAttribute('decoration-inner-name') ? $service->getAttribute('decoration-inner-name') : null;
            $priority = $service->hasAttribute('decoration-priority') ? $service->getAttribute('decoration-priority') : 0;

            $definition->setDecoratedService($decorates, $renameId, $priority, $invalidBehavior);
        }

        if ($callable = $this->getChildren($service, 'from-callable')) {
            if ($definition instanceof ChildDefinition) {
                throw new InvalidArgumentException(\sprintf('Attribute "parent" is unsupported when using "<from-callable>" on service "%s".', (string) $service->getAttribute('id')));
            }

            foreach ([
                'Attribute "synthetic"' => 'isSynthetic',
                'Attribute "file"' => 'getFile',
                'Tag "<factory>"' => 'getFactory',
                'Tag "<argument>"' => 'getArguments',
                'Tag "<property>"' => 'getProperties',
                'Tag "<configurator>"' => 'getConfigurator',
                'Tag "<call>"' => 'getMethodCalls',
            ] as $key => $method) {
                if ($definition->$method()) {
                    throw new InvalidArgumentException($key.\sprintf(' is unsupported when using "<from-callable>" on service "%s".', (string) $service->getAttribute('id')));
                }
            }

            $definition->setFactory(['Closure', 'fromCallable']);

            if ('Closure' !== ($definition->getClass() ?? 'Closure')) {
                $definition->setLazy(true);
            } else {
                $definition->setClass('Closure');
            }

            $callable = $callable[0];
            if ($function = $callable->getAttribute('function')) {
                $definition->setArguments([$function]);
            } elseif ($expression = $callable->getAttribute('expression')) {
                if (!class_exists(Expression::class)) {
                    throw new \LogicException('The "expression" attribute cannot be used without the ExpressionLanguage component. Try running "composer require symfony/expression-language".');
                }
                $definition->setArguments(['@='.$expression]);
            } else {
                if ($childService = $callable->getAttribute('service')) {
                    $class = new Reference($childService, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE);
                } else {
                    $class = $callable->hasAttribute('class') ? $callable->getAttribute('class') : null;
                }

                $definition->setArguments([[$class, $callable->getAttribute('method') ?: '__invoke']]);
            }
        }

        return $definition;
    }

    /**
     * Parses an XML file to a \DOMDocument.
     *
     * @throws InvalidArgumentException When loading of XML file returns error
     */
    private function parseFileToDOM(string $file): \DOMDocument
    {
        try {
            $dom = XmlUtils::loadFile($file, $this->validateSchema(...));
        } catch (\InvalidArgumentException $e) {
            $invalidSecurityElements = [];
            $errors = explode("\n", $e->getMessage());
            foreach ($errors as $i => $error) {
                if (preg_match("#^\[ERROR 1871] Element '\{http://symfony\.com/schema/dic/security}([^']+)'#", $error, $matches)) {
                    $invalidSecurityElements[$i] = $matches[1];
                }
            }
            if ($invalidSecurityElements) {
                $dom = XmlUtils::loadFile($file);

                foreach ($invalidSecurityElements as $errorIndex => $tagName) {
                    foreach ($dom->getElementsByTagNameNS('http://symfony.com/schema/dic/security', $tagName) as $element) {
                        if (!$parent = $element->parentNode) {
                            continue;
                        }
                        if ('http://symfony.com/schema/dic/security' !== $parent->namespaceURI) {
                            continue;
                        }
                        if ('provider' === $parent->localName || 'firewall' === $parent->localName) {
                            unset($errors[$errorIndex]);
                        }
                    }
                }
            }
            if ($errors) {
                throw new InvalidArgumentException(\sprintf('Unable to parse file "%s": ', $file).implode("\n", $errors), $e->getCode(), $e);
            }
        }

        $this->validateExtensions($dom, $file);

        return $dom;
    }

    /**
     * Processes anonymous services.
     */
    private function processAnonymousServices(\DOMDocument $xml, string $file, ?\DOMNode $root = null): void
    {
        $definitions = [];
        $count = 0;
        $suffix = '~'.ContainerBuilder::hash($file);

        $xpath = new \DOMXPath($xml);
        $xpath->registerNamespace('container', self::NS);

        // anonymous services as arguments/properties
        if (false !== $nodes = $xpath->query('.//container:argument[@type="service"][not(@id)]|.//container:property[@type="service"][not(@id)]|.//container:bind[not(@id)]|.//container:factory[not(@service)]|.//container:configurator[not(@service)]', $root)) {
            foreach ($nodes as $node) {
                if ($services = $this->getChildren($node, 'service')) {
                    // give it a unique name
                    $id = \sprintf('.%d_%s', ++$count, preg_replace('/^.*\\\\/', '', $services[0]->getAttribute('class')).$suffix);
                    $node->setAttribute('id', $id);
                    $node->setAttribute('service', $id);

                    $definitions[$id] = [$services[0], $file];
                    $services[0]->setAttribute('id', $id);

                    // anonymous services are always private
                    // we could not use the constant false here, because of XML parsing
                    $services[0]->setAttribute('public', 'false');
                }
            }
        }

        // anonymous services "in the wild"
        if (false !== $nodes = $xpath->query('.//container:services/container:service[not(@id)]', $root)) {
            foreach ($nodes as $node) {
                throw new InvalidArgumentException(\sprintf('Top-level services must have "id" attribute, none found in "%s" at line %d.', $file, $node->getLineNo()));
            }
        }

        // resolve definitions
        uksort($definitions, 'strnatcmp');
        foreach (array_reverse($definitions) as $id => [$domElement, $file]) {
            if (null !== $definition = $this->parseDefinition($domElement, $file, new Definition())) {
                $this->setDefinition($id, $definition);
            }
        }
    }

    private function getArgumentsAsPhp(\DOMElement $node, string $name, string $file, bool $isChildDefinition = false): array
    {
        $arguments = [];
        foreach ($this->getChildren($node, $name) as $arg) {
            if ($arg->hasAttribute('name')) {
                $arg->setAttribute('key', $arg->getAttribute('name'));
            }

            // this is used by ChildDefinition to overwrite a specific
            // argument of the parent definition
            if ($arg->hasAttribute('index')) {
                $key = ($isChildDefinition ? 'index_' : '').$arg->getAttribute('index');
            } elseif (!$arg->hasAttribute('key')) {
                // Append an empty argument, then fetch its key to overwrite it later
                $arguments[] = null;
                $keys = array_keys($arguments);
                $key = array_pop($keys);
            } else {
                $key = $arg->getAttribute('key');
            }

            $trim = $arg->hasAttribute('trim') && XmlUtils::phpize($arg->getAttribute('trim'));
            $onInvalid = $arg->getAttribute('on-invalid');
            $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE;
            if ('ignore' == $onInvalid) {
                $invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE;
            } elseif ('ignore_uninitialized' == $onInvalid) {
                $invalidBehavior = ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE;
            } elseif ('null' == $onInvalid) {
                $invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE;
            }

            switch ($type = $arg->getAttribute('type')) {
                case 'service':
                    if ('' === $arg->getAttribute('id')) {
                        throw new InvalidArgumentException(\sprintf('Tag "<%s>" with type="service" has no or empty "id" attribute in "%s".', $name, $file));
                    }

                    $arguments[$key] = new Reference($arg->getAttribute('id'), $invalidBehavior);
                    break;
                case 'expression':
                    if (!class_exists(Expression::class)) {
                        throw new \LogicException('The type="expression" attribute cannot be used without the ExpressionLanguage component. Try running "composer require symfony/expression-language".');
                    }

                    $arguments[$key] = new Expression($arg->nodeValue);
                    break;
                case 'collection':
                    $arguments[$key] = $this->getArgumentsAsPhp($arg, $name, $file);
                    break;
                case 'iterator':
                    $arg = $this->getArgumentsAsPhp($arg, $name, $file);
                    $arguments[$key] = new IteratorArgument($arg);
                    break;
                case 'closure':
                case 'service_closure':
                    if ('' !== $arg->getAttribute('id')) {
                        $arg = new Reference($arg->getAttribute('id'), $invalidBehavior);
                    } else {
                        $arg = $this->getArgumentsAsPhp($arg, $name, $file);
                    }
                    $arguments[$key] = match ($type) {
                        'service_closure' => new ServiceClosureArgument($arg),
                        'closure' => (new Definition('Closure'))
                            ->setFactory(['Closure', 'fromCallable'])
                            ->addArgument($arg),
                    };
                    break;
                case 'service_locator':
                    $arg = $this->getArgumentsAsPhp($arg, $name, $file);

                    if (isset($arg[0])) {
                        trigger_deprecation('symfony/dependency-injection', '6.3', 'Skipping "key" argument or using integers as values in a "service_locator" tag is deprecated. The keys will default to the IDs of the original services in 7.0.');
                    }

                    $arguments[$key] = new ServiceLocatorArgument($arg);
                    break;
                case 'tagged':
                case 'tagged_iterator':
                case 'tagged_locator':
                    $forLocator = 'tagged_locator' === $type;

                    if (!$arg->getAttribute('tag')) {
                        throw new InvalidArgumentException(\sprintf('Tag "<%s>" with type="%s" has no or empty "tag" attribute in "%s".', $name, $type, $file));
                    }

                    $excludes = array_column($this->getChildren($arg, 'exclude'), 'nodeValue');
                    if ($arg->hasAttribute('exclude')) {
                        if (\count($excludes) > 0) {
                            throw new InvalidArgumentException('You cannot use both the attribute "exclude" and <exclude> tags at the same time.');
                        }
                        $excludes = [$arg->getAttribute('exclude')];
                    }

                    $arguments[$key] = new TaggedIteratorArgument($arg->getAttribute('tag'), $arg->getAttribute('index-by') ?: null, $arg->getAttribute('default-index-method') ?: null, $forLocator, $arg->getAttribute('default-priority-method') ?: null, $excludes, !$arg->hasAttribute('exclude-self') || XmlUtils::phpize($arg->getAttribute('exclude-self')));

                    if ($forLocator) {
                        $arguments[$key] = new ServiceLocatorArgument($arguments[$key]);
                    }
                    break;
                case 'binary':
                    if (false === $value = base64_decode($arg->nodeValue)) {
                        throw new InvalidArgumentException(\sprintf('Tag "<%s>" with type="binary" is not a valid base64 encoded string.', $name));
                    }
                    $arguments[$key] = $value;
                    break;
                case 'abstract':
                    $arguments[$key] = new AbstractArgument($arg->nodeValue);
                    break;
                case 'string':
                    $arguments[$key] = $trim ? trim($arg->nodeValue) : $arg->nodeValue;
                    break;
                case 'constant':
                    $arguments[$key] = \constant(trim($arg->nodeValue));
                    break;
                default:
                    $arguments[$key] = XmlUtils::phpize($trim ? trim($arg->nodeValue) : $arg->nodeValue);
            }
        }

        return $arguments;
    }

    /**
     * Get child elements by name.
     *
     * @return \DOMElement[]
     */
    private function getChildren(\DOMNode $node, string $name): array
    {
        $children = [];
        foreach ($node->childNodes as $child) {
            if ($child instanceof \DOMElement && $child->localName === $name && self::NS === $child->namespaceURI) {
                $children[] = $child;
            }
        }

        return $children;
    }

    private function getTagAttributes(\DOMNode $node, string $missingName): array
    {
        $parameters = [];
        $children = $this->getChildren($node, 'attribute');

        foreach ($children as $childNode) {
            if ('' === $name = $childNode->getAttribute('name')) {
                throw new InvalidArgumentException($missingName);
            }

            if ($this->getChildren($childNode, 'attribute')) {
                $parameters[$name] = $this->getTagAttributes($childNode, $missingName);
            } else {
                if (str_contains($name, '-') && !str_contains($name, '_') && !\array_key_exists($normalizedName = str_replace('-', '_', $name), $parameters)) {
                    $parameters[$normalizedName] = XmlUtils::phpize($childNode->nodeValue);
                }
                // keep not normalized key
                $parameters[$name] = XmlUtils::phpize($childNode->nodeValue);
            }
        }

        return $parameters;
    }

    /**
     * Validates a documents XML schema.
     *
     * @throws RuntimeException When extension references a non-existent XSD file
     */
    public function validateSchema(\DOMDocument $dom): bool
    {
        $schemaLocations = ['http://symfony.com/schema/dic/services' => str_replace('\\', '/', __DIR__.'/schema/dic/services/services-1.0.xsd')];

        if ($element = $dom->documentElement->getAttributeNS('http://www.w3.org/2001/XMLSchema-instance', 'schemaLocation')) {
            $items = preg_split('/\s+/', $element);
            for ($i = 0, $nb = \count($items); $i < $nb; $i += 2) {
                if (!$this->container->hasExtension($items[$i])) {
                    continue;
                }

                if (($extension = $this->container->getExtension($items[$i])) && false !== $extension->getXsdValidationBasePath()) {
                    $ns = $extension->getNamespace();
                    $path = str_replace([$ns, str_replace('http://', 'https://', $ns)], str_replace('\\', '/', $extension->getXsdValidationBasePath()).'/', $items[$i + 1]);

                    if (!is_file($path)) {
                        throw new RuntimeException(\sprintf('Extension "%s" references a non-existent XSD file "%s".', get_debug_type($extension), $path));
                    }

                    $schemaLocations[$items[$i]] = $path;
                }
            }
        }

        $tmpfiles = [];
        $imports = '';
        foreach ($schemaLocations as $namespace => $location) {
            if (0 === stripos($location, 'phar://')) {
                if ($tmpfile = tempnam(sys_get_temp_dir(), 'symfony')) {
                    copy($location, $tmpfile);
                    $tmpfiles[] = $tmpfile;
                    $location = self::getFileUrl($tmpfile);
                } else {
                    $parts = explode('/', '\\' === \DIRECTORY_SEPARATOR ? str_replace('\\', '/', $location) : $location);
                    array_shift($parts);
                    $drive = '\\' === \DIRECTORY_SEPARATOR ? array_shift($parts).'/' : '';
                    $location = 'phar:///'.$drive.implode('/', array_map('rawurlencode', $parts));
                }
            } elseif ('\\' === \DIRECTORY_SEPARATOR && str_starts_with($location, '\\\\')) {
                $parts = explode('/', $location);
                $drive = array_shift($parts).'/';
                $location = $drive.implode('/', array_map('rawurlencode', $parts));
            } else {
                $location = self::getFileUrl($location);
            }

            $imports .= \sprintf('  <xsd:import namespace="%s" schemaLocation="%s" />'."\n", $namespace, $location);
        }

        $source = <<<EOF
            <?xml version="1.0" encoding="utf-8" ?>
            <xsd:schema xmlns="http://symfony.com/schema"
                xmlns:xsd="http://www.w3.org/2001/XMLSchema"
                targetNamespace="http://symfony.com/schema"
                elementFormDefault="qualified">

                <xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
            $imports
            </xsd:schema>
            EOF
        ;

        if ($this->shouldEnableEntityLoader()) {
            $disableEntities = libxml_disable_entity_loader(false);
            $valid = @$dom->schemaValidateSource($source);
            libxml_disable_entity_loader($disableEntities);
        } else {
            $valid = @$dom->schemaValidateSource($source);
        }
        foreach ($tmpfiles as $tmpfile) {
            @unlink($tmpfile);
        }

        return $valid;
    }

    private function shouldEnableEntityLoader(): bool
    {
        static $dom, $schema;
        if (null === $dom) {
            $dom = new \DOMDocument();
            $dom->loadXML('<?xml version="1.0"?><test/>');

            $tmpfile = tempnam(sys_get_temp_dir(), 'symfony');
            register_shutdown_function(static function () use ($tmpfile) {
                @unlink($tmpfile);
            });
            $schema = '<?xml version="1.0" encoding="utf-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <xsd:include schemaLocation="'.self::getFileUrl($tmpfile).'" />
</xsd:schema>';
            file_put_contents($tmpfile, '<?xml version="1.0" encoding="utf-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <xsd:element name="test" type="testType" />
  <xsd:complexType name="testType"/>
</xsd:schema>');
        }

        return !@$dom->schemaValidateSource($schema);
    }

    private static function getFileUrl(string $path): string
    {
        if ('\\' === \DIRECTORY_SEPARATOR) {
            $parts = explode('/', str_replace('\\', '/', $path));
            $drive = array_shift($parts).'/';
        } else {
            $parts = explode('/', $path);
            $drive = '';
        }

        return 'file:///'.$drive.implode('/', array_map('rawurlencode', $parts));
    }

    private function validateAlias(\DOMElement $alias, string $file): void
    {
        foreach ($alias->attributes as $name => $node) {
            if (!\in_array($name, ['alias', 'id', 'public'])) {
                throw new InvalidArgumentException(\sprintf('Invalid attribute "%s" defined for alias "%s" in "%s".', $name, $alias->getAttribute('id'), $file));
            }
        }

        foreach ($alias->childNodes as $child) {
            if (!$child instanceof \DOMElement || self::NS !== $child->namespaceURI) {
                continue;
            }
            if ('deprecated' !== $child->localName) {
                throw new InvalidArgumentException(\sprintf('Invalid child element "%s" defined for alias "%s" in "%s".', $child->localName, $alias->getAttribute('id'), $file));
            }
        }
    }

    /**
     * Validates an extension.
     *
     * @throws InvalidArgumentException When no extension is found corresponding to a tag
     */
    private function validateExtensions(\DOMDocument $dom, string $file): void
    {
        foreach ($dom->documentElement->childNodes as $node) {
            if (!$node instanceof \DOMElement || 'http://symfony.com/schema/dic/services' === $node->namespaceURI) {
                continue;
            }

            // can it be handled by an extension?
            if (!$this->container->hasExtension($node->namespaceURI)) {
                $extensionNamespaces = array_filter(array_map(static fn (ExtensionInterface $ext) => $ext->getNamespace(), $this->container->getExtensions()));
                throw new InvalidArgumentException(\sprintf('There is no extension able to load the configuration for "%s" (in "%s"). Looked for namespace "%s", found "%s".', $node->tagName, $file, $node->namespaceURI, $extensionNamespaces ? implode('", "', $extensionNamespaces) : 'none'));
            }
        }
    }

    /**
     * Loads from an extension.
     */
    private function loadFromExtensions(\DOMDocument $xml): void
    {
        foreach ($xml->documentElement->childNodes as $node) {
            if (!$node instanceof \DOMElement || self::NS === $node->namespaceURI) {
                continue;
            }

            $values = static::convertDomElementToArray($node);
            if (!\is_array($values)) {
                $values = [];
            }

            $this->container->loadFromExtension($node->namespaceURI, $values);
        }
    }

    /**
     * Converts a \DOMElement object to a PHP array.
     *
     * The following rules applies during the conversion:
     *
     *  * Each tag is converted to a key value or an array
     *    if there is more than one "value"
     *
     *  * The content of a tag is set under a "value" key (<foo>bar</foo>)
     *    if the tag also has some nested tags
     *
     *  * The attributes are converted to keys (<foo foo="bar"/>)
     *
     *  * The nested-tags are converted to keys (<foo><foo>bar</foo></foo>)
     *
     * @param \DOMElement $element A \DOMElement instance
     */
    public static function convertDomElementToArray(\DOMElement $element): mixed
    {
        return XmlUtils::convertDomElementToArray($element, false);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Loader\Configurator\Traits;

use Symfony\Component\DependencyInjection\Argument\BoundArgument;
use Symfony\Component\DependencyInjection\Loader\Configurator\DefaultsConfigurator;
use Symfony\Component\DependencyInjection\Loader\Configurator\InstanceofConfigurator;

trait BindTrait
{
    /**
     * Sets bindings.
     *
     * Bindings map $named or FQCN arguments to values that should be
     * injected in the matching parameters (of the constructor, of methods
     * called and of controller actions).
     *
     * @param string $nameOrFqcn A parameter name with its "$" prefix, or an FQCN
     * @param mixed  $valueOrRef The value or reference to bind
     *
     * @return $this
     */
    final public function bind(string $nameOrFqcn, mixed $valueOrRef): static
    {
        $valueOrRef = static::processValue($valueOrRef, true);
        $bindings = $this->definition->getBindings();
        $type = $this instanceof DefaultsConfigurator ? BoundArgument::DEFAULTS_BINDING : ($this instanceof InstanceofConfigurator ? BoundArgument::INSTANCEOF_BINDING : BoundArgument::SERVICE_BINDING);
        $bindings[$nameOrFqcn] = new BoundArgument($valueOrRef, true, $type, $this->path ?? null);
        $this->definition->setBindings($bindings);

        return $this;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Loader\Configurator\Traits;

trait ShareTrait
{
    /**
     * Sets if the service must be shared or not.
     *
     * @return $this
     */
    final public function share(bool $shared = true): static
    {
        $this->definition->setShared($shared);

        return $this;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Loader\Configurator\Traits;

use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;

trait DecorateTrait
{
    /**
     * Sets the service that this service is decorating.
     *
     * @param string|null $id The decorated service id, use null to remove decoration
     *
     * @return $this
     *
     * @throws InvalidArgumentException in case the decorated service id and the new decorated service id are equals
     */
    final public function decorate(?string $id, ?string $renamedId = null, int $priority = 0, int $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE): static
    {
        $this->definition->setDecoratedService($id, $renamedId, $priority, $invalidBehavior);

        return $this;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Loader\Configurator\Traits;

trait ClassTrait
{
    /**
     * Sets the service class.
     *
     * @return $this
     */
    final public function class(?string $class): static
    {
        $this->definition->setClass($class);

        return $this;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Loader\Configurator\Traits;

use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;

trait ParentTrait
{
    /**
     * Sets the Definition to inherit from.
     *
     * @return $this
     *
     * @throws InvalidArgumentException when parent cannot be set
     */
    final public function parent(string $parent): static
    {
        if (!$this->allowParent) {
            throw new InvalidArgumentException(\sprintf('A parent cannot be defined when either "_instanceof" or "_defaults" are also defined for service prototype "%s".', $this->id));
        }

        if ($this->definition instanceof ChildDefinition) {
            $this->definition->setParent($parent);
        } else {
            // cast Definition to ChildDefinition
            $definition = serialize($this->definition);
            $definition = substr_replace($definition, '53', 2, 2);
            $definition = substr_replace($definition, 'Child', 44, 0);
            $definition = unserialize($definition, ['allowed_classes' => true]);

            $this->definition = $definition->setParent($parent);
        }

        return $this;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Loader\Configurator\Traits;

use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;

trait CallTrait
{
    /**
     * Adds a method to call after service initialization.
     *
     * @param string $method       The method name to call
     * @param array  $arguments    An array of arguments to pass to the method call
     * @param bool   $returnsClone Whether the call returns the service instance or not
     *
     * @return $this
     *
     * @throws InvalidArgumentException on empty $method param
     */
    final public function call(string $method, array $arguments = [], bool $returnsClone = false): static
    {
        $this->definition->addMethodCall($method, static::processValue($arguments, true), $returnsClone);

        return $this;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Loader\Configurator\Traits;

trait ArgumentTrait
{
    /**
     * Sets the arguments to pass to the service constructor/factory method.
     *
     * @return $this
     */
    final public function args(array $arguments): static
    {
        $this->definition->setArguments(static::processValue($arguments, true));

        return $this;
    }

    /**
     * Sets one argument to pass to the service constructor/factory method.
     *
     * @return $this
     */
    final public function arg(string|int $key, mixed $value): static
    {
        $this->definition->setArgument($key, static::processValue($value, true));

        return $this;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Loader\Configurator\Traits;

trait AbstractTrait
{
    /**
     * Whether this definition is abstract, that means it merely serves as a
     * template for other definitions.
     *
     * @return $this
     */
    final public function abstract(bool $abstract = true): static
    {
        $this->definition->setAbstract($abstract);

        return $this;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Loader\Configurator\Traits;

use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;

trait TagTrait
{
    /**
     * Adds a tag for this definition.
     *
     * @return $this
     */
    final public function tag(string $name, array $attributes = []): static
    {
        if ('' === $name) {
            throw new InvalidArgumentException(\sprintf('The tag name for service "%s" must be a non-empty string.', $this->id));
        }

        $this->validateAttributes($name, $attributes);

        $this->definition->addTag($name, $attributes);

        return $this;
    }

    private function validateAttributes(string $tag, array $attributes, array $path = []): void
    {
        foreach ($attributes as $name => $value) {
            if (\is_array($value)) {
                $this->validateAttributes($tag, $value, [...$path, $name]);
            } elseif (!\is_scalar($value ?? '')) {
                $name = implode('.', [...$path, $name]);
                throw new InvalidArgumentException(\sprintf('A tag attribute must be of a scalar-type or an array of scalar-types for service "%s", tag "%s", attribute "%s".', $this->id, $tag, $name));
            }
        }
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Loader\Configurator\Traits;

trait PropertyTrait
{
    /**
     * Sets a specific property.
     *
     * @return $this
     */
    final public function property(string $name, mixed $value): static
    {
        $this->definition->setProperty($name, static::processValue($value, true));

        return $this;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Loader\Configurator\Traits;

trait SyntheticTrait
{
    /**
     * Sets whether this definition is synthetic, that is not constructed by the
     * container, but dynamically injected.
     *
     * @return $this
     */
    final public function synthetic(bool $synthetic = true): static
    {
        $this->definition->setSynthetic($synthetic);

        return $this;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Loader\Configurator\Traits;

use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Loader\Configurator\FromCallableConfigurator;
use Symfony\Component\DependencyInjection\Loader\Configurator\ReferenceConfigurator;
use Symfony\Component\ExpressionLanguage\Expression;

trait FromCallableTrait
{
    final public function fromCallable(string|array|ReferenceConfigurator|Expression $callable): FromCallableConfigurator
    {
        if ($this->definition instanceof ChildDefinition) {
            throw new InvalidArgumentException('The configuration key "parent" is unsupported when using "fromCallable()".');
        }

        foreach ([
            'synthetic' => 'isSynthetic',
            'factory' => 'getFactory',
            'file' => 'getFile',
            'arguments' => 'getArguments',
            'properties' => 'getProperties',
            'configurator' => 'getConfigurator',
            'calls' => 'getMethodCalls',
        ] as $key => $method) {
            if ($this->definition->$method()) {
                throw new InvalidArgumentException(\sprintf('The configuration key "%s" is unsupported when using "fromCallable()".', $key));
            }
        }

        $this->definition->setFactory(['Closure', 'fromCallable']);

        if (\is_string($callable) && 1 === substr_count($callable, ':')) {
            $parts = explode(':', $callable);

            throw new InvalidArgumentException(\sprintf('Invalid callable "%s": the "service:method" notation is not available when using PHP-based DI configuration. Use "[service(\'%s\'), \'%s\']" instead.', $callable, $parts[0], $parts[1]));
        }

        if ($callable instanceof Expression) {
            $callable = '@='.$callable;
        }

        $this->definition->setArguments([static::processValue($callable, true)]);

        if ('Closure' !== ($this->definition->getClass() ?? 'Closure')) {
            $this->definition->setLazy(true);
        } else {
            $this->definition->setClass('Closure');
        }

        return new FromCallableConfigurator($this, $this->definition);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Loader\Configurator\Traits;

use Symfony\Component\DependencyInjection\Loader\Configurator\ReferenceConfigurator;

trait ConfiguratorTrait
{
    /**
     * Sets a configurator to call after the service is fully initialized.
     *
     * @return $this
     */
    final public function configurator(string|array|ReferenceConfigurator $configurator): static
    {
        $this->definition->setConfigurator(static::processValue($configurator, true));

        return $this;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Loader\Configurator\Traits;

trait LazyTrait
{
    /**
     * Sets the lazy flag of this service.
     *
     * @param bool|string $lazy A FQCN to derivate the lazy proxy from or `true` to make it extend from the definition's class
     *
     * @return $this
     */
    final public function lazy(bool|string $lazy = true): static
    {
        $this->definition->setLazy((bool) $lazy);
        if (\is_string($lazy)) {
            $this->definition->addTag('proxy', ['interface' => $lazy]);
        }

        return $this;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Loader\Configurator\Traits;

use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;

trait DeprecateTrait
{
    /**
     * Whether this definition is deprecated, that means it should not be called anymore.
     *
     * @param string $package The name of the composer package that is triggering the deprecation
     * @param string $version The version of the package that introduced the deprecation
     * @param string $message The deprecation message to use
     *
     * @return $this
     *
     * @throws InvalidArgumentException when the message template is invalid
     */
    final public function deprecate(string $package, string $version, string $message): static
    {
        $this->definition->setDeprecated($package, $version, $message);

        return $this;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Loader\Configurator\Traits;

use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Loader\Configurator\ReferenceConfigurator;
use Symfony\Component\ExpressionLanguage\Expression;

trait FactoryTrait
{
    /**
     * Sets a factory.
     *
     * @return $this
     */
    final public function factory(string|array|ReferenceConfigurator|Expression $factory): static
    {
        if (\is_string($factory) && 1 === substr_count($factory, ':')) {
            $factoryParts = explode(':', $factory);

            throw new InvalidArgumentException(\sprintf('Invalid factory "%s": the "service:method" notation is not available when using PHP-based DI configuration. Use "[service(\'%s\'), \'%s\']" instead.', $factory, $factoryParts[0], $factoryParts[1]));
        }

        if ($factory instanceof Expression) {
            $factory = '@='.$factory;
        }

        $this->definition->setFactory(static::processValue($factory, true));

        return $this;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Loader\Configurator\Traits;

trait PublicTrait
{
    /**
     * @return $this
     */
    final public function public(): static
    {
        $this->definition->setPublic(true);

        return $this;
    }

    /**
     * @return $this
     */
    final public function private(): static
    {
        $this->definition->setPublic(false);

        return $this;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Loader\Configurator\Traits;

use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;

trait AutoconfigureTrait
{
    /**
     * Sets whether or not instanceof conditionals should be prepended with a global set.
     *
     * @return $this
     *
     * @throws InvalidArgumentException when a parent is already set
     */
    final public function autoconfigure(bool $autoconfigured = true): static
    {
        $this->definition->setAutoconfigured($autoconfigured);

        return $this;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Loader\Configurator\Traits;

trait AutowireTrait
{
    /**
     * Enables/disables autowiring.
     *
     * @return $this
     */
    final public function autowire(bool $autowired = true): static
    {
        $this->definition->setAutowired($autowired);

        return $this;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Loader\Configurator\Traits;

trait FileTrait
{
    /**
     * Sets a file to require before creating the service.
     *
     * @return $this
     */
    final public function file(string $file): static
    {
        $this->definition->setFile($file);

        return $this;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Loader\Configurator\Traits;

trait ConstructorTrait
{
    /**
     * Sets a static constructor.
     *
     * @return $this
     */
    final public function constructor(string $constructor): static
    {
        $this->definition->setFactory([null, $constructor]);

        return $this;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\ExpressionLanguage\Expression;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 */
class ParametersConfigurator extends AbstractConfigurator
{
    public const FACTORY = 'parameters';

    private ContainerBuilder $container;

    public function __construct(ContainerBuilder $container)
    {
        $this->container = $container;
    }

    /**
     * @return $this
     */
    final public function set(string $name, mixed $value): static
    {
        if ($value instanceof Expression) {
            throw new InvalidArgumentException(\sprintf('Using an expression in parameter "%s" is not allowed.', $name));
        }

        $this->container->setParameter($name, static::processValue($value, true));

        return $this;
    }

    /**
     * @return $this
     */
    final public function __invoke(string $name, mixed $value): static
    {
        return $this->set($name, $value);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 */
class PrototypeConfigurator extends AbstractServiceConfigurator
{
    use Traits\AbstractTrait;
    use Traits\ArgumentTrait;
    use Traits\AutoconfigureTrait;
    use Traits\AutowireTrait;
    use Traits\BindTrait;
    use Traits\CallTrait;
    use Traits\ConfiguratorTrait;
    use Traits\ConstructorTrait;
    use Traits\DeprecateTrait;
    use Traits\FactoryTrait;
    use Traits\LazyTrait;
    use Traits\ParentTrait;
    use Traits\PropertyTrait;
    use Traits\PublicTrait;
    use Traits\ShareTrait;
    use Traits\TagTrait;

    public const FACTORY = 'load';

    private PhpFileLoader $loader;
    private string $resource;
    private ?array $excludes = null;
    private bool $allowParent;
    private ?string $path;

    public function __construct(ServicesConfigurator $parent, PhpFileLoader $loader, Definition $defaults, string $namespace, string $resource, bool $allowParent, ?string $path = null)
    {
        $definition = new Definition();
        if (!$defaults->isPublic() || !$defaults->isPrivate()) {
            $definition->setPublic($defaults->isPublic());
        }
        $definition->setAutowired($defaults->isAutowired());
        $definition->setAutoconfigured($defaults->isAutoconfigured());
        // deep clone, to avoid multiple process of the same instance in the passes
        $definition->setBindings(unserialize(serialize($defaults->getBindings()), ['allowed_classes' => true]));
        $definition->setChanges([]);

        $this->loader = $loader;
        $this->resource = $resource;
        $this->allowParent = $allowParent;
        $this->path = $path;

        parent::__construct($parent, $definition, $namespace, $defaults->getTags());
    }

    public function __destruct()
    {
        parent::__destruct();

        if (isset($this->loader)) {
            $this->loader->registerClasses($this->definition, $this->id, $this->resource, $this->excludes, $this->path);
        }
        unset($this->loader);
    }

    /**
     * Excludes files from registration using glob patterns.
     *
     * @param string[]|string $excludes
     *
     * @return $this
     */
    final public function exclude(array|string $excludes): static
    {
        $this->excludes = (array) $excludes;

        return $this;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use Symfony\Component\Config\Loader\ParamConfigurator;
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\Argument\AbstractArgument;
use Symfony\Component\DependencyInjection\Argument\ArgumentInterface;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Parameter;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\ExpressionLanguage\Expression;

abstract class AbstractConfigurator
{
    public const FACTORY = 'unknown';

    /**
     * @var \Closure(mixed, bool):mixed|null
     */
    public static $valuePreProcessor;

    /** @internal */
    protected Definition|Alias|null $definition = null;

    /**
     * @return mixed
     */
    public function __call(string $method, array $args)
    {
        if (method_exists($this, 'set'.$method)) {
            return $this->{'set'.$method}(...$args);
        }

        throw new \BadMethodCallException(\sprintf('Call to undefined method "%s::%s()".', static::class, $method));
    }

    public function __serialize(): array
    {
        throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
    }

    public function __unserialize(array $data): void
    {
        throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
    }

    /**
     * Checks that a value is valid, optionally replacing Definition and Reference configurators by their configure value.
     *
     * @param bool $allowServices whether Definition and Reference are allowed; by default, only scalars, arrays and enum are
     *
     * @return mixed the value, optionally cast to a Definition/Reference
     */
    public static function processValue(mixed $value, bool $allowServices = false): mixed
    {
        if (\is_array($value)) {
            foreach ($value as $k => $v) {
                $value[$k] = static::processValue($v, $allowServices);
            }

            return self::$valuePreProcessor ? (self::$valuePreProcessor)($value, $allowServices) : $value;
        }

        if (self::$valuePreProcessor) {
            $value = (self::$valuePreProcessor)($value, $allowServices);
        }

        if ($value instanceof ParamConfigurator) {
            return (string) $value;
        }

        if (\is_scalar($value ?? '') || $value instanceof \UnitEnum) {
            return $value;
        }

        if (!$allowServices) {
            throw new InvalidArgumentException(\sprintf('Cannot use values of type "%s" in service configuration files.', get_debug_type($value)));
        }

        if ($value instanceof ReferenceConfigurator) {
            $reference = new Reference($value->id, $value->invalidBehavior);

            return $value instanceof ClosureReferenceConfigurator ? new ServiceClosureArgument($reference) : $reference;
        }

        if ($value instanceof InlineServiceConfigurator) {
            $def = $value->definition;
            $value->definition = null;

            return $def;
        }

        if ($value instanceof self) {
            throw new InvalidArgumentException(\sprintf('"%s()" can be used only at the root of service configuration files.', $value::FACTORY));
        }

        switch (true) {
            case $value instanceof ArgumentInterface:
            case $value instanceof Definition:
            case $value instanceof Expression:
            case $value instanceof Parameter:
            case $value instanceof AbstractArgument:
            case $value instanceof Reference:
                return $value;
        }

        throw new InvalidArgumentException(\sprintf('Cannot use values of type "%s" in service configuration files.', get_debug_type($value)));
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use Symfony\Component\DependencyInjection\Definition;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 */
class InstanceofConfigurator extends AbstractServiceConfigurator
{
    use Traits\AutowireTrait;
    use Traits\BindTrait;
    use Traits\CallTrait;
    use Traits\ConfiguratorTrait;
    use Traits\ConstructorTrait;
    use Traits\LazyTrait;
    use Traits\PropertyTrait;
    use Traits\PublicTrait;
    use Traits\ShareTrait;
    use Traits\TagTrait;

    public const FACTORY = 'instanceof';

    private ?string $path;

    public function __construct(ServicesConfigurator $parent, Definition $definition, string $id, ?string $path = null)
    {
        parent::__construct($parent, $definition, $id, []);

        $this->path = $path;
    }

    /**
     * Defines an instanceof-conditional to be applied to following service definitions.
     */
    final public function instanceof(string $fqcn): self
    {
        return $this->parent->instanceof($fqcn);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use Symfony\Component\DependencyInjection\Definition;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 */
class InlineServiceConfigurator extends AbstractConfigurator
{
    use Traits\ArgumentTrait;
    use Traits\AutowireTrait;
    use Traits\BindTrait;
    use Traits\CallTrait;
    use Traits\ConfiguratorTrait;
    use Traits\ConstructorTrait;
    use Traits\FactoryTrait;
    use Traits\FileTrait;
    use Traits\LazyTrait;
    use Traits\ParentTrait;
    use Traits\PropertyTrait;
    use Traits\TagTrait;

    public const FACTORY = 'service';

    private string $id = '[inline]';
    private bool $allowParent = true;
    private ?string $path = null;

    public function __construct(Definition $definition)
    {
        $this->definition = $definition;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 */
class ServicesConfigurator extends AbstractConfigurator
{
    public const FACTORY = 'services';

    private Definition $defaults;
    private ContainerBuilder $container;
    private PhpFileLoader $loader;
    private array $instanceof;
    private ?string $path;
    private string $anonymousHash;
    private int $anonymousCount;

    public function __construct(ContainerBuilder $container, PhpFileLoader $loader, array &$instanceof, ?string $path = null, int &$anonymousCount = 0)
    {
        $this->defaults = new Definition();
        $this->container = $container;
        $this->loader = $loader;
        $this->instanceof = &$instanceof;
        $this->path = $path;
        $this->anonymousHash = ContainerBuilder::hash($path ?: mt_rand());
        $this->anonymousCount = &$anonymousCount;
        $instanceof = [];
    }

    /**
     * Defines a set of defaults for following service definitions.
     */
    final public function defaults(): DefaultsConfigurator
    {
        return new DefaultsConfigurator($this, $this->defaults = new Definition(), $this->path);
    }

    /**
     * Defines an instanceof-conditional to be applied to following service definitions.
     */
    final public function instanceof(string $fqcn): InstanceofConfigurator
    {
        $this->instanceof[$fqcn] = $definition = new ChildDefinition('');

        return new InstanceofConfigurator($this, $definition, $fqcn, $this->path);
    }

    /**
     * Registers a service.
     *
     * @param string|null $id    The service id, or null to create an anonymous service
     * @param string|null $class The class of the service, or null when $id is also the class name
     */
    final public function set(?string $id, ?string $class = null): ServiceConfigurator
    {
        $defaults = $this->defaults;
        $definition = new Definition();

        if (null === $id) {
            if (!$class) {
                throw new \LogicException('Anonymous services must have a class name.');
            }

            $id = \sprintf('.%d_%s', ++$this->anonymousCount, preg_replace('/^.*\\\\/', '', $class).'~'.$this->anonymousHash);
        } elseif (!$defaults->isPublic() || !$defaults->isPrivate()) {
            $definition->setPublic($defaults->isPublic() && !$defaults->isPrivate());
        }

        $definition->setAutowired($defaults->isAutowired());
        $definition->setAutoconfigured($defaults->isAutoconfigured());
        // deep clone, to avoid multiple process of the same instance in the passes
        $definition->setBindings(unserialize(serialize($defaults->getBindings()), ['allowed_classes' => true]));
        $definition->setChanges([]);

        $configurator = new ServiceConfigurator($this->container, $this->instanceof, true, $this, $definition, $id, $defaults->getTags(), $this->path);

        return null !== $class ? $configurator->class($class) : $configurator;
    }

    /**
     * Removes an already defined service definition or alias.
     *
     * @return $this
     */
    final public function remove(string $id): static
    {
        $this->container->removeDefinition($id);
        $this->container->removeAlias($id);

        return $this;
    }

    /**
     * Creates an alias.
     */
    final public function alias(string $id, string $referencedId): AliasConfigurator
    {
        $ref = static::processValue($referencedId, true);
        $alias = new Alias((string) $ref);
        if (!$this->defaults->isPublic() || !$this->defaults->isPrivate()) {
            $alias->setPublic($this->defaults->isPublic());
        }
        $this->container->setAlias($id, $alias);

        return new AliasConfigurator($this, $alias);
    }

    /**
     * Registers a PSR-4 namespace using a glob pattern.
     */
    final public function load(string $namespace, string $resource): PrototypeConfigurator
    {
        return new PrototypeConfigurator($this, $this->loader, $this->defaults, $namespace, $resource, true, $this->path);
    }

    /**
     * Gets an already defined service definition.
     *
     * @throws ServiceNotFoundException if the service definition does not exist
     */
    final public function get(string $id): ServiceConfigurator
    {
        $definition = $this->container->getDefinition($id);

        return new ServiceConfigurator($this->container, $definition->getInstanceofConditionals(), true, $this, $definition, $id, []);
    }

    /**
     * Registers a stack of decorator services.
     *
     * @param InlineServiceConfigurator[]|ReferenceConfigurator[] $services
     */
    final public function stack(string $id, array $services): AliasConfigurator
    {
        foreach ($services as $i => $service) {
            if ($service instanceof InlineServiceConfigurator) {
                $definition = $service->definition->setInstanceofConditionals($this->instanceof);

                $changes = $definition->getChanges();
                $definition->setAutowired((isset($changes['autowired']) ? $definition : $this->defaults)->isAutowired());
                $definition->setAutoconfigured((isset($changes['autoconfigured']) ? $definition : $this->defaults)->isAutoconfigured());
                $definition->setBindings(array_merge($this->defaults->getBindings(), $definition->getBindings()));
                $definition->setChanges($changes);

                $services[$i] = $definition;
            } elseif (!$service instanceof ReferenceConfigurator) {
                throw new InvalidArgumentException(\sprintf('"%s()" expects a list of definitions as returned by "%s()" or "%s()", "%s" given at index "%s" for service "%s".', __METHOD__, InlineServiceConfigurator::FACTORY, ReferenceConfigurator::FACTORY, $service instanceof AbstractConfigurator ? $service::FACTORY.'()' : get_debug_type($service), $i, $id));
            }
        }

        $alias = $this->alias($id, '');
        $alias->definition = $this->set($id)
            ->parent('')
            ->args($services)
            ->tag('container.stack')
            ->definition;

        return $alias;
    }

    /**
     * Registers a service.
     */
    final public function __invoke(string $id, ?string $class = null): ServiceConfigurator
    {
        return $this->set($id, $class);
    }

    public function __destruct()
    {
        $this->loader->registerAliasesForSinglyImplementedInterfaces();
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use Symfony\Component\DependencyInjection\Definition;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 */
class FromCallableConfigurator extends AbstractServiceConfigurator
{
    use Traits\AbstractTrait;
    use Traits\AutoconfigureTrait;
    use Traits\AutowireTrait;
    use Traits\BindTrait;
    use Traits\DecorateTrait;
    use Traits\DeprecateTrait;
    use Traits\LazyTrait;
    use Traits\PublicTrait;
    use Traits\ShareTrait;
    use Traits\TagTrait;

    public const FACTORY = 'services';

    private ServiceConfigurator $serviceConfigurator;

    public function __construct(ServiceConfigurator $serviceConfigurator, Definition $definition)
    {
        $this->serviceConfigurator = $serviceConfigurator;

        parent::__construct($serviceConfigurator->parent, $definition, $serviceConfigurator->id);
    }

    public function __destruct()
    {
        $this->serviceConfigurator->__destruct();
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 */
class ReferenceConfigurator extends AbstractConfigurator
{
    /** @internal */
    protected string $id;

    /** @internal */
    protected int $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE;

    public function __construct(string $id)
    {
        $this->id = $id;
    }

    /**
     * @return $this
     */
    final public function ignoreOnInvalid(): static
    {
        $this->invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE;

        return $this;
    }

    /**
     * @return $this
     */
    final public function nullOnInvalid(): static
    {
        $this->invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE;

        return $this;
    }

    /**
     * @return $this
     */
    final public function ignoreOnUninitialized(): static
    {
        $this->invalidBehavior = ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE;

        return $this;
    }

    public function __toString(): string
    {
        return $this->id;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use Symfony\Component\Config\Loader\ParamConfigurator;
use Symfony\Component\DependencyInjection\Argument\AbstractArgument;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Extension\ExtensionInterface;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
use Symfony\Component\ExpressionLanguage\Expression;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 */
class ContainerConfigurator extends AbstractConfigurator
{
    public const FACTORY = 'container';

    private ContainerBuilder $container;
    private PhpFileLoader $loader;
    private array $instanceof;
    private string $path;
    private string $file;
    private int $anonymousCount = 0;
    private ?string $env;

    public function __construct(ContainerBuilder $container, PhpFileLoader $loader, array &$instanceof, string $path, string $file, ?string $env = null)
    {
        $this->container = $container;
        $this->loader = $loader;
        $this->instanceof = &$instanceof;
        $this->path = $path;
        $this->file = $file;
        $this->env = $env;
    }

    final public function extension(string $namespace, array $config): void
    {
        if (!$this->container->hasExtension($namespace)) {
            $extensions = array_filter(array_map(static fn (ExtensionInterface $ext) => $ext->getAlias(), $this->container->getExtensions()));
            throw new InvalidArgumentException(\sprintf('There is no extension able to load the configuration for "%s" (in "%s"). Looked for namespace "%s", found "%s".', $namespace, $this->file, $namespace, $extensions ? implode('", "', $extensions) : 'none'));
        }

        $this->container->loadFromExtension($namespace, static::processValue($config));
    }

    final public function import(string $resource, ?string $type = null, bool|string $ignoreErrors = false): void
    {
        $this->loader->setCurrentDir(\dirname($this->path));
        $this->loader->import($resource, $type, $ignoreErrors, $this->file);
    }

    final public function parameters(): ParametersConfigurator
    {
        return new ParametersConfigurator($this->container);
    }

    final public function services(): ServicesConfigurator
    {
        return new ServicesConfigurator($this->container, $this->loader, $this->instanceof, $this->path, $this->anonymousCount);
    }

    /**
     * Get the current environment to be able to write conditional configuration.
     */
    final public function env(): ?string
    {
        return $this->env;
    }

    final public function withPath(string $path): static
    {
        $clone = clone $this;
        $clone->path = $clone->file = $path;
        $clone->loader->setCurrentDir(\dirname($path));

        return $clone;
    }
}

/**
 * Creates a parameter.
 */
function param(string $name): ParamConfigurator
{
    return new ParamConfigurator($name);
}

/**
 * Creates a reference to a service.
 */
function service(string $serviceId): ReferenceConfigurator
{
    return new ReferenceConfigurator($serviceId);
}

/**
 * Creates an inline service.
 */
function inline_service(?string $class = null): InlineServiceConfigurator
{
    return new InlineServiceConfigurator(new Definition($class));
}

/**
 * Creates a service locator.
 *
 * @param array<ReferenceConfigurator|InlineServiceConfigurator> $values
 */
function service_locator(array $values): ServiceLocatorArgument
{
    $values = AbstractConfigurator::processValue($values, true);

    if (isset($values[0])) {
        trigger_deprecation('symfony/dependency-injection', '6.3', 'Using integers as keys in a "service_locator()" argument is deprecated. The keys will default to the IDs of the original services in 7.0.');
    }

    return new ServiceLocatorArgument($values);
}

/**
 * Creates a lazy iterator.
 *
 * @param ReferenceConfigurator[] $values
 */
function iterator(array $values): IteratorArgument
{
    return new IteratorArgument(AbstractConfigurator::processValue($values, true));
}

/**
 * Creates a lazy iterator by tag name.
 */
function tagged_iterator(string $tag, ?string $indexAttribute = null, ?string $defaultIndexMethod = null, ?string $defaultPriorityMethod = null, string|array $exclude = [], bool $excludeSelf = true): TaggedIteratorArgument
{
    return new TaggedIteratorArgument($tag, $indexAttribute, $defaultIndexMethod, false, $defaultPriorityMethod, (array) $exclude, $excludeSelf);
}

/**
 * Creates a service locator by tag name.
 */
function tagged_locator(string $tag, ?string $indexAttribute = null, ?string $defaultIndexMethod = null, ?string $defaultPriorityMethod = null, string|array $exclude = [], bool $excludeSelf = true): ServiceLocatorArgument
{
    return new ServiceLocatorArgument(new TaggedIteratorArgument($tag, $indexAttribute, $defaultIndexMethod, true, $defaultPriorityMethod, (array) $exclude, $excludeSelf));
}

/**
 * Creates an expression.
 */
function expr(string $expression): Expression
{
    return new Expression($expression);
}

/**
 * Creates an abstract argument.
 */
function abstract_arg(string $description): AbstractArgument
{
    return new AbstractArgument($description);
}

/**
 * Creates an environment variable reference.
 */
function env(string $name): EnvConfigurator
{
    return new EnvConfigurator($name);
}

/**
 * Creates a closure service reference.
 */
function service_closure(string $serviceId): ClosureReferenceConfigurator
{
    return new ClosureReferenceConfigurator($serviceId);
}

/**
 * Creates a closure.
 */
function closure(string|array|ReferenceConfigurator|Expression $callable): InlineServiceConfigurator
{
    return (new InlineServiceConfigurator(new Definition('Closure')))
        ->factory(['Closure', 'fromCallable'])
        ->args([$callable]);
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 */
class DefaultsConfigurator extends AbstractServiceConfigurator
{
    use Traits\AutoconfigureTrait;
    use Traits\AutowireTrait;
    use Traits\BindTrait;
    use Traits\PublicTrait;

    public const FACTORY = 'defaults';

    private ?string $path;

    public function __construct(ServicesConfigurator $parent, Definition $definition, ?string $path = null)
    {
        parent::__construct($parent, $definition, null, []);

        $this->path = $path;
    }

    /**
     * Adds a tag for this definition.
     *
     * @return $this
     *
     * @throws InvalidArgumentException when an invalid tag name or attribute is provided
     */
    final public function tag(string $name, array $attributes = []): static
    {
        if ('' === $name) {
            throw new InvalidArgumentException('The tag name in "_defaults" must be a non-empty string.');
        }

        $this->validateAttributes($name, $attributes);

        $this->definition->addTag($name, $attributes);

        return $this;
    }

    /**
     * Defines an instanceof-conditional to be applied to following service definitions.
     */
    final public function instanceof(string $fqcn): InstanceofConfigurator
    {
        return $this->parent->instanceof($fqcn);
    }

    private function validateAttributes(string $tag, array $attributes, array $path = []): void
    {
        foreach ($attributes as $name => $value) {
            if (\is_array($value)) {
                $this->validateAttributes($tag, $value, [...$path, $name]);
            } elseif (!\is_scalar($value ?? '')) {
                $name = implode('.', [...$path, $name]);
                throw new InvalidArgumentException(\sprintf('Tag "%s", attribute "%s" in "_defaults" must be of a scalar-type or an array of scalar-type.', $tag, $name));
            }
        }
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use Symfony\Component\Config\Loader\ParamConfigurator;

class EnvConfigurator extends ParamConfigurator
{
    /**
     * @var string[]
     */
    private array $stack;

    public function __construct(string $name)
    {
        $this->stack = explode(':', $name);
    }

    public function __toString(): string
    {
        return '%env('.implode(':', $this->stack).')%';
    }

    /**
     * @return $this
     */
    public function __call(string $name, array $arguments): static
    {
        $processor = strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], '\1_\2', $name));

        $this->custom($processor, ...$arguments);

        return $this;
    }

    /**
     * @return $this
     */
    public function custom(string $processor, ...$args): static
    {
        array_unshift($this->stack, $processor, ...$args);

        return $this;
    }

    /**
     * @return $this
     */
    public function base64(): static
    {
        array_unshift($this->stack, 'base64');

        return $this;
    }

    /**
     * @return $this
     */
    public function bool(): static
    {
        array_unshift($this->stack, 'bool');

        return $this;
    }

    /**
     * @return $this
     */
    public function not(): static
    {
        array_unshift($this->stack, 'not');

        return $this;
    }

    /**
     * @return $this
     */
    public function const(): static
    {
        array_unshift($this->stack, 'const');

        return $this;
    }

    /**
     * @return $this
     */
    public function csv(): static
    {
        array_unshift($this->stack, 'csv');

        return $this;
    }

    /**
     * @return $this
     */
    public function file(): static
    {
        array_unshift($this->stack, 'file');

        return $this;
    }

    /**
     * @return $this
     */
    public function float(): static
    {
        array_unshift($this->stack, 'float');

        return $this;
    }

    /**
     * @return $this
     */
    public function int(): static
    {
        array_unshift($this->stack, 'int');

        return $this;
    }

    /**
     * @return $this
     */
    public function json(): static
    {
        array_unshift($this->stack, 'json');

        return $this;
    }

    /**
     * @return $this
     */
    public function key(string $key): static
    {
        array_unshift($this->stack, 'key', $key);

        return $this;
    }

    /**
     * @return $this
     */
    public function url(): static
    {
        array_unshift($this->stack, 'url');

        return $this;
    }

    /**
     * @return $this
     */
    public function queryString(): static
    {
        array_unshift($this->stack, 'query_string');

        return $this;
    }

    /**
     * @return $this
     */
    public function resolve(): static
    {
        array_unshift($this->stack, 'resolve');

        return $this;
    }

    /**
     * @return $this
     */
    public function default(string $fallbackParam): static
    {
        array_unshift($this->stack, 'default', $fallbackParam);

        return $this;
    }

    /**
     * @return $this
     */
    public function string(): static
    {
        array_unshift($this->stack, 'string');

        return $this;
    }

    /**
     * @return $this
     */
    public function trim(): static
    {
        array_unshift($this->stack, 'trim');

        return $this;
    }

    /**
     * @return $this
     */
    public function require(): static
    {
        array_unshift($this->stack, 'require');

        return $this;
    }

    /**
     * @param class-string<\BackedEnum> $backedEnumClassName
     *
     * @return $this
     */
    public function enum(string $backedEnumClassName): static
    {
        array_unshift($this->stack, 'enum', $backedEnumClassName);

        return $this;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Loader\Configurator;

class ClosureReferenceConfigurator extends ReferenceConfigurator
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;

abstract class AbstractServiceConfigurator extends AbstractConfigurator
{
    protected $parent;
    protected $id;
    private array $defaultTags = [];

    public function __construct(ServicesConfigurator $parent, Definition $definition, ?string $id = null, array $defaultTags = [])
    {
        $this->parent = $parent;
        $this->definition = $definition;
        $this->id = $id;
        $this->defaultTags = $defaultTags;
    }

    public function __destruct()
    {
        // default tags should be added last
        foreach ($this->defaultTags as $name => $attributes) {
            foreach ($attributes as $attribute) {
                $this->definition->addTag($name, $attribute);
            }
        }
        $this->defaultTags = [];
    }

    /**
     * Registers a service.
     */
    final public function set(?string $id, ?string $class = null): ServiceConfigurator
    {
        $this->__destruct();

        return $this->parent->set($id, $class);
    }

    /**
     * Creates an alias.
     */
    final public function alias(string $id, string $referencedId): AliasConfigurator
    {
        $this->__destruct();

        return $this->parent->alias($id, $referencedId);
    }

    /**
     * Registers a PSR-4 namespace using a glob pattern.
     */
    final public function load(string $namespace, string $resource): PrototypeConfigurator
    {
        $this->__destruct();

        return $this->parent->load($namespace, $resource);
    }

    /**
     * Gets an already defined service definition.
     *
     * @throws ServiceNotFoundException if the service definition does not exist
     */
    final public function get(string $id): ServiceConfigurator
    {
        $this->__destruct();

        return $this->parent->get($id);
    }

    /**
     * Removes an already defined service definition or alias.
     */
    final public function remove(string $id): ServicesConfigurator
    {
        $this->__destruct();

        return $this->parent->remove($id);
    }

    /**
     * Registers a stack of decorator services.
     *
     * @param InlineServiceConfigurator[]|ReferenceConfigurator[] $services
     */
    final public function stack(string $id, array $services): AliasConfigurator
    {
        $this->__destruct();

        return $this->parent->stack($id, $services);
    }

    /**
     * Registers a service.
     */
    final public function __invoke(string $id, ?string $class = null): ServiceConfigurator
    {
        $this->__destruct();

        return $this->parent->set($id, $class);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use Symfony\Component\DependencyInjection\Alias;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 */
class AliasConfigurator extends AbstractServiceConfigurator
{
    use Traits\DeprecateTrait;
    use Traits\PublicTrait;

    public const FACTORY = 'alias';

    public function __construct(ServicesConfigurator $parent, Alias $alias)
    {
        $this->parent = $parent;
        $this->definition = $alias;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 */
class ServiceConfigurator extends AbstractServiceConfigurator
{
    use Traits\AbstractTrait;
    use Traits\ArgumentTrait;
    use Traits\AutoconfigureTrait;
    use Traits\AutowireTrait;
    use Traits\BindTrait;
    use Traits\CallTrait;
    use Traits\ClassTrait;
    use Traits\ConfiguratorTrait;
    use Traits\ConstructorTrait;
    use Traits\DecorateTrait;
    use Traits\DeprecateTrait;
    use Traits\FactoryTrait;
    use Traits\FileTrait;
    use Traits\FromCallableTrait;
    use Traits\LazyTrait;
    use Traits\ParentTrait;
    use Traits\PropertyTrait;
    use Traits\PublicTrait;
    use Traits\ShareTrait;
    use Traits\SyntheticTrait;
    use Traits\TagTrait;

    public const FACTORY = 'services';

    private ContainerBuilder $container;
    private array $instanceof;
    private bool $allowParent;
    private ?string $path;
    private bool $destructed = false;

    public function __construct(ContainerBuilder $container, array $instanceof, bool $allowParent, ServicesConfigurator $parent, Definition $definition, ?string $id, array $defaultTags, ?string $path = null)
    {
        $this->container = $container;
        $this->instanceof = $instanceof;
        $this->allowParent = $allowParent;
        $this->path = $path;

        parent::__construct($parent, $definition, $id, $defaultTags);
    }

    public function __destruct()
    {
        if ($this->destructed) {
            return;
        }
        $this->destructed = true;

        parent::__destruct();

        $this->container->removeBindings($this->id);
        $this->container->setDefinition($this->id, $this->definition->setInstanceofConditionals($this->instanceof));
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection;

use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\ExpressionLanguage\ExpressionFunction;
use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface;

/**
 * Define some ExpressionLanguage functions.
 *
 * To get a service, use service('request').
 * To get a parameter, use parameter('kernel.debug').
 * To get an env variable, use env('SOME_VARIABLE').
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class ExpressionLanguageProvider implements ExpressionFunctionProviderInterface
{
    private ?\Closure $serviceCompiler;

    private ?\Closure $getEnv;

    public function __construct(?callable $serviceCompiler = null, ?\Closure $getEnv = null)
    {
        $this->serviceCompiler = null === $serviceCompiler ? null : $serviceCompiler(...);
        $this->getEnv = $getEnv;
    }

    public function getFunctions(): array
    {
        return [
            new ExpressionFunction('service', $this->serviceCompiler ?? static fn ($arg) => \sprintf('$container->get(%s)', $arg), static fn (array $variables, $value) => $variables['container']->get($value)),

            new ExpressionFunction('parameter', static fn ($arg) => \sprintf('$container->getParameter(%s)', $arg), static fn (array $variables, $value) => $variables['container']->getParameter($value)),

            new ExpressionFunction('env', static fn ($arg) => \sprintf('$container->getEnv(%s)', $arg), function (array $variables, $value) {
                if (!$this->getEnv) {
                    throw new LogicException('You need to pass a getEnv closure to the expression language provider to use the "env" function.');
                }

                return ($this->getEnv)($value);
            }),

            new ExpressionFunction('arg', static fn ($arg) => \sprintf('$args?->get(%s)', $arg), static fn (array $variables, $value) => $variables['args']?->get($value)),
        ];
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Argument;

/**
 * @internal
 */
class RewindableGenerator implements \IteratorAggregate, \Countable
{
    private \Closure $generator;
    private \Closure|int $count;

    public function __construct(callable $generator, int|callable $count)
    {
        $this->generator = $generator(...);
        $this->count = \is_int($count) ? $count : $count(...);
    }

    public function getIterator(): \Traversable
    {
        $g = $this->generator;

        return $g();
    }

    public function count(): int
    {
        if (!\is_int($count = $this->count)) {
            $this->count = $count();
        }

        return $this->count;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Argument;

/**
 * Represents a closure acting as a service locator.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
class ServiceLocatorArgument implements ArgumentInterface
{
    private array $values;
    private ?TaggedIteratorArgument $taggedIteratorArgument = null;

    public function __construct(array|TaggedIteratorArgument $values = [])
    {
        if ($values instanceof TaggedIteratorArgument) {
            $this->taggedIteratorArgument = $values;
            $values = [];
        }

        $this->setValues($values);
    }

    public function getTaggedIteratorArgument(): ?TaggedIteratorArgument
    {
        return $this->taggedIteratorArgument;
    }

    public function getValues(): array
    {
        return $this->values;
    }

    /**
     * @return void
     */
    public function setValues(array $values)
    {
        $this->values = $values;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Argument;

/**
 * Represents a collection of values to lazily iterate over.
 *
 * @author Titouan Galopin <galopintitouan@gmail.com>
 */
class IteratorArgument implements ArgumentInterface
{
    private array $values;

    public function __construct(array $values)
    {
        $this->setValues($values);
    }

    public function getValues(): array
    {
        return $this->values;
    }

    /**
     * @return void
     */
    public function setValues(array $values)
    {
        $this->values = $values;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Argument;

trigger_deprecation('symfony/dependency-injection', '6.1', '"%s" is deprecated.', ReferenceSetArgumentTrait::class);

use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Reference;

/**
 * @author Titouan Galopin <galopintitouan@gmail.com>
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @deprecated since Symfony 6.1
 */
trait ReferenceSetArgumentTrait
{
    private array $values;

    /**
     * @param Reference[] $values
     */
    public function __construct(array $values)
    {
        $this->setValues($values);
    }

    /**
     * @return Reference[]
     */
    public function getValues(): array
    {
        return $this->values;
    }

    /**
     * @param Reference[] $values The service references to put in the set
     *
     * @return void
     */
    public function setValues(array $values)
    {
        foreach ($values as $k => $v) {
            if (null !== $v && !$v instanceof Reference) {
                throw new InvalidArgumentException(\sprintf('A "%s" must hold only Reference instances, "%s" given.', __CLASS__, get_debug_type($v)));
            }
        }

        $this->values = $values;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Argument;

use Symfony\Component\DependencyInjection\ServiceLocator as BaseServiceLocator;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @internal
 */
class ServiceLocator extends BaseServiceLocator
{
    private \Closure $factory;
    private array $serviceMap;
    private ?array $serviceTypes;

    public function __construct(\Closure $factory, array $serviceMap, ?array $serviceTypes = null)
    {
        $this->factory = $factory;
        $this->serviceMap = $serviceMap;
        $this->serviceTypes = $serviceTypes;
        parent::__construct($serviceMap);
    }

    public function get(string $id): mixed
    {
        return match (\count($this->serviceMap[$id] ?? [])) {
            0 => parent::get($id),
            1 => $this->serviceMap[$id][0],
            default => ($this->factory)(...$this->serviceMap[$id]),
        };
    }

    public function getProvidedServices(): array
    {
        return $this->serviceTypes ??= array_map(static fn () => '?', $this->serviceMap);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Argument;

/**
 * @author Guilhem Niot <guilhem.niot@gmail.com>
 */
final class BoundArgument implements ArgumentInterface
{
    public const SERVICE_BINDING = 0;
    public const DEFAULTS_BINDING = 1;
    public const INSTANCEOF_BINDING = 2;

    private static int $sequence = 0;

    private mixed $value;
    private ?int $identifier = null;
    private ?bool $used = null;
    private int $type;
    private ?string $file;

    public function __construct(mixed $value, bool $trackUsage = true, int $type = 0, ?string $file = null)
    {
        $this->value = $value;
        if ($trackUsage) {
            $this->identifier = ++self::$sequence;
        } else {
            $this->used = true;
        }
        $this->type = $type;
        $this->file = $file;
    }

    public function getValues(): array
    {
        return [$this->value, $this->identifier, $this->used, $this->type, $this->file];
    }

    public function setValues(array $values): void
    {
        if (5 === \count($values)) {
            [$this->value, $this->identifier, $this->used, $this->type, $this->file] = $values;
        } else {
            [$this->value, $this->identifier, $this->used] = $values;
        }
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Argument;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\VarExporter\ProxyHelper;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @internal
 */
class LazyClosure
{
    public readonly object $service;

    public function __construct(
        private \Closure $initializer,
    ) {
        unset($this->service);
    }

    public function __get(mixed $name): mixed
    {
        if ('service' !== $name) {
            throw new InvalidArgumentException(\sprintf('Cannot read property "%s" from a lazy closure.', $name));
        }

        if (isset($this->initializer)) {
            if (\is_string($service = ($this->initializer)())) {
                $service = (new \ReflectionClass($service))->newInstanceWithoutConstructor();
            }
            $this->service = $service;
            unset($this->initializer);
        }

        return $this->service;
    }

    public static function getCode(string $initializer, array $callable, string $class, ContainerBuilder $container, ?string $id): string
    {
        $method = $callable[1];

        if ($asClosure = 'Closure' === $class) {
            $class = ($callable[0] instanceof Reference ? $container->findDefinition($callable[0]) : $callable[0])->getClass();
        }

        $r = $container->getReflectionClass($class);

        if (null !== $id) {
            $id = \sprintf(' for service "%s"', $id);
        }

        if (!$asClosure) {
            $id = str_replace('%', '%%', (string) $id);

            if (!$r || !$r->isInterface()) {
                throw new RuntimeException(\sprintf("Cannot create adapter{$id} because \"%s\" is not an interface.", $class));
            }
            if (1 !== \count($method = $r->getMethods())) {
                throw new RuntimeException(\sprintf("Cannot create adapter{$id} because interface \"%s\" doesn't have exactly one method.", $class));
            }
            $method = $method[0]->name;
        } elseif (!$r || !$r->hasMethod($method)) {
            throw new RuntimeException("Cannot create lazy closure{$id} because its corresponding callable is invalid.");
        }

        $methodReflector = $r->getMethod($method);
        $code = ProxyHelper::exportSignature($methodReflector, true, $args);

        if ($asClosure) {
            $code = ' { '.preg_replace('/: static$/', ': \\'.$r->name, $code);
        } else {
            $code = ' implements \\'.$r->name.' { '.$code;
        }

        $code = 'new class('.$initializer.') extends \\'.self::class
            .$code.' { '.($methodReflector->hasReturnType() && 'void' === (string) $methodReflector->getReturnType() ? '' : 'return ').'$this->service->'.$callable[1].'('.$args.'); } '
            .'}';

        return $asClosure ? '('.$code.')->'.$method.'(...)' : $code;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Argument;

/**
 * Represents an abstract service argument, which have to be set by a compiler pass or a DI extension.
 */
final class AbstractArgument
{
    private string $text;
    private string $context = '';

    public function __construct(string $text = '')
    {
        $this->text = trim($text, '. ');
    }

    public function setContext(string $context): void
    {
        $this->context = $context.' is abstract'.('' === $this->text ? '' : ': ');
    }

    public function getText(): string
    {
        return $this->text;
    }

    public function getTextWithContext(): string
    {
        return $this->context.$this->text.'.';
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Argument;

use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;

/**
 * Represents a service wrapped in a memoizing closure.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
class ServiceClosureArgument implements ArgumentInterface
{
    private array $values;

    public function __construct(mixed $value)
    {
        $this->values = [$value];
    }

    public function getValues(): array
    {
        return $this->values;
    }

    /**
     * @return void
     */
    public function setValues(array $values)
    {
        if ([0] !== array_keys($values)) {
            throw new InvalidArgumentException('A ServiceClosureArgument must hold one and only one value.');
        }

        $this->values = $values;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Argument;

/**
 * Represents a collection of services found by tag name to lazily iterate over.
 *
 * @author Roland Franssen <franssen.roland@gmail.com>
 */
class TaggedIteratorArgument extends IteratorArgument
{
    private string $tag;
    private mixed $indexAttribute;
    private ?string $defaultIndexMethod;
    private ?string $defaultPriorityMethod;
    private bool $needsIndexes;
    private array $exclude;
    private bool $excludeSelf = true;

    /**
     * @param string      $tag                   The name of the tag identifying the target services
     * @param string|null $indexAttribute        The name of the attribute that defines the key referencing each service in the tagged collection
     * @param string|null $defaultIndexMethod    The static method that should be called to get each service's key when their tag doesn't define the previous attribute
     * @param bool        $needsIndexes          Whether indexes are required and should be generated when computing the map
     * @param string|null $defaultPriorityMethod The static method that should be called to get each service's priority when their tag doesn't define the "priority" attribute
     * @param array       $exclude               Services to exclude from the iterator
     * @param bool        $excludeSelf           Whether to automatically exclude the referencing service from the iterator
     */
    public function __construct(string $tag, ?string $indexAttribute = null, ?string $defaultIndexMethod = null, bool $needsIndexes = false, ?string $defaultPriorityMethod = null, array $exclude = [], bool $excludeSelf = true)
    {
        parent::__construct([]);

        if (null === $indexAttribute && $needsIndexes) {
            $indexAttribute = preg_match('/[^.]++$/', $tag, $m) ? $m[0] : $tag;
        }

        $this->tag = $tag;
        $this->indexAttribute = $indexAttribute;
        $this->defaultIndexMethod = $defaultIndexMethod ?: ($indexAttribute ? 'getDefault'.str_replace(' ', '', ucwords(preg_replace('/[^a-zA-Z0-9\x7f-\xff]++/', ' ', $indexAttribute))).'Name' : null);
        $this->needsIndexes = $needsIndexes;
        $this->defaultPriorityMethod = $defaultPriorityMethod ?: ($indexAttribute ? 'getDefault'.str_replace(' ', '', ucwords(preg_replace('/[^a-zA-Z0-9\x7f-\xff]++/', ' ', $indexAttribute))).'Priority' : null);
        $this->exclude = $exclude;
        $this->excludeSelf = $excludeSelf;
    }

    /**
     * @return string
     */
    public function getTag()
    {
        return $this->tag;
    }

    public function getIndexAttribute(): ?string
    {
        return $this->indexAttribute;
    }

    public function getDefaultIndexMethod(): ?string
    {
        return $this->defaultIndexMethod;
    }

    public function needsIndexes(): bool
    {
        return $this->needsIndexes;
    }

    public function getDefaultPriorityMethod(): ?string
    {
        return $this->defaultPriorityMethod;
    }

    public function getExclude(): array
    {
        return $this->exclude;
    }

    public function excludeSelf(): bool
    {
        return $this->excludeSelf;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Argument;

/**
 * Represents a complex argument containing nested values.
 *
 * @author Titouan Galopin <galopintitouan@gmail.com>
 */
interface ArgumentInterface
{
    public function getValues(): array;

    /**
     * @return void
     */
    public function setValues(array $values);
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection;

use Symfony\Component\DependencyInjection\Exception\EnvNotFoundException;
use Symfony\Component\DependencyInjection\Exception\ParameterCircularReferenceException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 */
class EnvVarProcessor implements EnvVarProcessorInterface
{
    private ContainerInterface $container;
    /** @var \Traversable<EnvVarLoaderInterface> */
    private \Traversable $loaders;
    private array $loadedVars = [];

    /**
     * @param \Traversable<EnvVarLoaderInterface>|null $loaders
     */
    public function __construct(ContainerInterface $container, ?\Traversable $loaders = null)
    {
        $this->container = $container;
        $this->loaders = $loaders ?? new \ArrayIterator();
    }

    public static function getProvidedTypes(): array
    {
        return [
            'base64' => 'string',
            'bool' => 'bool',
            'not' => 'bool',
            'const' => 'bool|int|float|string|array',
            'csv' => 'array',
            'file' => 'string',
            'float' => 'float',
            'int' => 'int',
            'json' => 'array',
            'key' => 'bool|int|float|string|array',
            'url' => 'array',
            'query_string' => 'array',
            'resolve' => 'string',
            'default' => 'bool|int|float|string|array',
            'string' => 'string',
            'trim' => 'string',
            'require' => 'bool|int|float|string|array',
            'enum' => \BackedEnum::class,
            'shuffle' => 'array',
            'defined' => 'bool',
        ];
    }

    public function getEnv(string $prefix, string $name, \Closure $getEnv): mixed
    {
        $i = strpos($name, ':');

        if ('key' === $prefix) {
            if (false === $i) {
                throw new RuntimeException(\sprintf('Invalid env "key:%s": a key specifier should be provided.', $name));
            }

            $next = substr($name, $i + 1);
            $key = substr($name, 0, $i);
            $array = $getEnv($next);

            if (!\is_array($array)) {
                throw new RuntimeException(\sprintf('Resolved value of "%s" did not result in an array value.', $next));
            }

            if (!isset($array[$key]) && !\array_key_exists($key, $array)) {
                throw new EnvNotFoundException(\sprintf('Key "%s" not found in %s (resolved from "%s").', $key, json_encode($array), $next));
            }

            return $array[$key];
        }

        if ('enum' === $prefix) {
            if (false === $i) {
                throw new RuntimeException(\sprintf('Invalid env "enum:%s": a "%s" class-string should be provided.', $name, \BackedEnum::class));
            }

            $next = substr($name, $i + 1);
            $backedEnumClassName = substr($name, 0, $i);
            $backedEnumValue = $getEnv($next);

            if (!\is_string($backedEnumValue) && !\is_int($backedEnumValue)) {
                throw new RuntimeException(\sprintf('Resolved value of "%s" did not result in a string or int value.', $next));
            }

            if (!is_subclass_of($backedEnumClassName, \BackedEnum::class)) {
                throw new RuntimeException(\sprintf('"%s" is not a "%s".', $backedEnumClassName, \BackedEnum::class));
            }

            return $backedEnumClassName::tryFrom($backedEnumValue) ?? throw new RuntimeException(\sprintf('Enum value "%s" is not backed by "%s".', $backedEnumValue, $backedEnumClassName));
        }

        if ('defined' === $prefix) {
            try {
                return '' !== ($getEnv($name) ?? '');
            } catch (EnvNotFoundException) {
                return false;
            }
        }

        if ('default' === $prefix) {
            if (false === $i) {
                throw new RuntimeException(\sprintf('Invalid env "default:%s": a fallback parameter should be provided.', $name));
            }

            $next = substr($name, $i + 1);
            $default = substr($name, 0, $i);

            if ('' !== $default && !$this->container->hasParameter($default)) {
                throw new RuntimeException(\sprintf('Invalid env fallback in "default:%s": parameter "%s" not found.', $name, $default));
            }

            try {
                $env = $getEnv($next);

                if ('' !== $env && null !== $env) {
                    return $env;
                }
            } catch (EnvNotFoundException) {
                // no-op
            }

            return '' === $default ? null : $this->container->getParameter($default);
        }

        if ('file' === $prefix || 'require' === $prefix) {
            if (!\is_scalar($file = $getEnv($name))) {
                throw new RuntimeException(\sprintf('Invalid file name: env var "%s" is non-scalar.', $name));
            }
            if (!is_file($file)) {
                throw new EnvNotFoundException(\sprintf('File "%s" not found (resolved from "%s").', $file, $name));
            }

            if ('file' === $prefix) {
                return file_get_contents($file);
            }

            return require $file;
        }

        $returnNull = false;
        if ('' === $prefix) {
            if ('' === $name) {
                return null;
            }
            $returnNull = true;
            $prefix = 'string';
        }

        if (false !== $i || 'string' !== $prefix) {
            $env = $getEnv($name);
        } elseif ('' === ($env = $_ENV[$name] ?? (str_starts_with($name, 'HTTP_') ? null : ($_SERVER[$name] ?? null)))
            || (false !== $env && false === $env ??= getenv($name) ?? false) // null is a possible value because of thread safety issues
        ) {
            foreach ($this->loadedVars as $i => $vars) {
                if (false === $env = $vars[$name] ?? $env) {
                    continue;
                }
                if ($env instanceof \Stringable) {
                    $this->loadedVars[$i][$name] = $env = (string) $env;
                }
                if ('' !== ($env ?? '')) {
                    break;
                }
            }

            if (false === $env || '' === $env) {
                $loaders = $this->loaders;
                $this->loaders = new \ArrayIterator();

                try {
                    $i = 0;
                    $ended = true;
                    $count = $loaders instanceof \Countable ? $loaders->count() : 0;
                    foreach ($loaders as $loader) {
                        if (\count($this->loadedVars) > $i++) {
                            continue;
                        }
                        $this->loadedVars[] = $vars = $loader->loadEnvVars();
                        if (false === $env = $vars[$name] ?? $env) {
                            continue;
                        }
                        if ($env instanceof \Stringable) {
                            $this->loadedVars[array_key_last($this->loadedVars)][$name] = $env = (string) $env;
                        }
                        if ('' !== ($env ?? '')) {
                            $ended = false;
                            break;
                        }
                    }
                    if ($ended || $count === $i) {
                        $loaders = $this->loaders;
                    }
                } catch (ParameterCircularReferenceException) {
                    // skip loaders that need an env var that is not defined
                } finally {
                    $this->loaders = $loaders;
                }
            }

            if (false === $env) {
                if (!$this->container->hasParameter("env($name)")) {
                    throw new EnvNotFoundException(\sprintf('Environment variable not found: "%s".', $name));
                }

                $env = $this->container->getParameter("env($name)");
            }
        }

        if (null === $env) {
            if ($returnNull) {
                return null;
            }

            if (!isset($this->getProvidedTypes()[$prefix])) {
                throw new RuntimeException(\sprintf('Unsupported env var prefix "%s".', $prefix));
            }

            if (!\in_array($prefix, ['string', 'bool', 'not', 'int', 'float'], true)) {
                return null;
            }
        }

        if ('shuffle' === $prefix) {
            \is_array($env) ? shuffle($env) : throw new RuntimeException(\sprintf('Env var "%s" cannot be shuffled, expected array, got "%s".', $name, get_debug_type($env)));

            return $env;
        }

        if (null !== $env && !\is_scalar($env)) {
            throw new RuntimeException(\sprintf('Non-scalar env var "%s" cannot be cast to "%s".', $name, $prefix));
        }

        if ('string' === $prefix) {
            return (string) $env;
        }

        if (\in_array($prefix, ['bool', 'not'], true)) {
            $env = (bool) (filter_var($env, \FILTER_VALIDATE_BOOL) ?: filter_var($env, \FILTER_VALIDATE_INT) ?: filter_var($env, \FILTER_VALIDATE_FLOAT));

            return 'not' === $prefix xor $env;
        }

        if ('int' === $prefix) {
            if (null !== $env && false === $env = filter_var($env, \FILTER_VALIDATE_INT) ?: filter_var($env, \FILTER_VALIDATE_FLOAT)) {
                throw new RuntimeException(\sprintf('Non-numeric env var "%s" cannot be cast to int.', $name));
            }

            return (int) $env;
        }

        if ('float' === $prefix) {
            if (null !== $env && false === $env = filter_var($env, \FILTER_VALIDATE_FLOAT)) {
                throw new RuntimeException(\sprintf('Non-numeric env var "%s" cannot be cast to float.', $name));
            }

            return (float) $env;
        }

        if ('const' === $prefix) {
            if (!\defined($env)) {
                throw new RuntimeException(\sprintf('Env var "%s" maps to undefined constant "%s".', $name, $env));
            }

            return \constant($env);
        }

        if ('base64' === $prefix) {
            return base64_decode(strtr($env, '-_', '+/'));
        }

        if ('json' === $prefix) {
            $env = json_decode($env, true);

            if (\JSON_ERROR_NONE !== json_last_error()) {
                throw new RuntimeException(\sprintf('Invalid JSON in env var "%s": ', $name).json_last_error_msg());
            }

            if (null !== $env && !\is_array($env)) {
                throw new RuntimeException(\sprintf('Invalid JSON env var "%s": array or null expected, "%s" given.', $name, get_debug_type($env)));
            }

            return $env;
        }

        if ('url' === $prefix) {
            $params = parse_url($env);

            if (false === $params) {
                throw new RuntimeException(\sprintf('Invalid URL in env var "%s".', $name));
            }
            if (!isset($params['scheme'], $params['host'])) {
                throw new RuntimeException(\sprintf('Invalid URL in env var "%s": scheme and host expected.', $name));
            }
            $params += [
                'port' => null,
                'user' => null,
                'pass' => null,
                'path' => null,
                'query' => null,
                'fragment' => null,
            ];

            $params['user'] = null !== $params['user'] ? rawurldecode($params['user']) : null;
            $params['pass'] = null !== $params['pass'] ? rawurldecode($params['pass']) : null;

            // remove the '/' separator
            $params['path'] = '/' === ($params['path'] ?? '/') ? '' : substr($params['path'], 1);

            return $params;
        }

        if ('query_string' === $prefix) {
            $queryString = parse_url($env, \PHP_URL_QUERY) ?: (parse_url($env, \PHP_URL_SCHEME) ? '' : $env);
            parse_str($queryString, $result);

            return $result;
        }

        if ('resolve' === $prefix) {
            return preg_replace_callback('/%%|%([^%\s]+)%/', function ($match) use ($name, $getEnv) {
                if (!isset($match[1])) {
                    return '%';
                }

                if (str_starts_with($match[1], 'env(') && str_ends_with($match[1], ')') && 'env()' !== $match[1]) {
                    $value = $getEnv(substr($match[1], 4, -1));
                } else {
                    $value = $this->container->getParameter($match[1]);
                }

                if (!\is_scalar($value)) {
                    throw new RuntimeException(\sprintf('Parameter "%s" found when resolving env var "%s" must be scalar, "%s" given.', $match[1], $name, get_debug_type($value)));
                }

                return $value;
            }, $env);
        }

        if ('csv' === $prefix) {
            return '' === $env ? [] : str_getcsv($env, ',', '"', '');
        }

        if ('trim' === $prefix) {
            return trim($env);
        }

        throw new RuntimeException(\sprintf('Unsupported env var prefix "%s" for env name "%s".', $prefix, $name));
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Extension;

use Symfony\Component\DependencyInjection\ContainerBuilder;

/**
 * ExtensionInterface is the interface implemented by container extension classes.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
interface ExtensionInterface
{
    /**
     * Loads a specific configuration.
     *
     * @param array<array<mixed>> $configs
     *
     * @return void
     *
     * @throws \InvalidArgumentException When provided tag is not defined in this extension
     */
    public function load(array $configs, ContainerBuilder $container);

    /**
     * Returns the namespace to be used for this extension (XML namespace).
     *
     * @return string
     */
    public function getNamespace();

    /**
     * Returns the base path for the XSD files.
     *
     * @return string|false
     */
    public function getXsdValidationBasePath();

    /**
     * Returns the recommended alias to use in XML.
     *
     * This alias is also the mandatory prefix to use when using YAML.
     *
     * @return string
     */
    public function getAlias();
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Extension;

use Symfony\Component\Config\Builder\ConfigBuilderGenerator;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\Config\Loader\DelegatingLoader;
use Symfony\Component\Config\Loader\LoaderResolver;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\ClosureLoader;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symfony\Component\DependencyInjection\Loader\DirectoryLoader;
use Symfony\Component\DependencyInjection\Loader\GlobFileLoader;
use Symfony\Component\DependencyInjection\Loader\IniFileLoader;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;

/**
 * @author Yonel Ceruto <yonelceruto@gmail.com>
 */
trait ExtensionTrait
{
    private function executeConfiguratorCallback(ContainerBuilder $container, \Closure $callback, ConfigurableExtensionInterface $subject): void
    {
        $env = $container->getParameter('kernel.environment');
        $loader = $this->createContainerLoader($container, $env);
        $file = (new \ReflectionObject($subject))->getFileName();
        $bundleLoader = $loader->getResolver()->resolve($file);
        if (!$bundleLoader instanceof PhpFileLoader) {
            throw new \LogicException('Unable to create the ContainerConfigurator.');
        }
        $bundleLoader->setCurrentDir(\dirname($file));
        $instanceof = &\Closure::bind(fn &() => $this->instanceof, $bundleLoader, $bundleLoader)();

        try {
            $callback(new ContainerConfigurator($container, $bundleLoader, $instanceof, $file, $file, $env));
        } finally {
            $instanceof = [];
            $bundleLoader->registerAliasesForSinglyImplementedInterfaces();
        }
    }

    private function createContainerLoader(ContainerBuilder $container, string $env): DelegatingLoader
    {
        $buildDir = $container->getParameter('kernel.build_dir');
        $locator = new FileLocator();
        $resolver = new LoaderResolver([
            new XmlFileLoader($container, $locator, $env),
            new YamlFileLoader($container, $locator, $env),
            new IniFileLoader($container, $locator, $env),
            new PhpFileLoader($container, $locator, $env, new ConfigBuilderGenerator($buildDir)),
            new GlobFileLoader($container, $locator, $env),
            new DirectoryLoader($container, $locator, $env),
            new ClosureLoader($container, $env),
        ]);

        return new DelegatingLoader($resolver);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Extension;

use Symfony\Component\DependencyInjection\ContainerBuilder;

interface PrependExtensionInterface
{
    /**
     * Allow an extension to prepend the extension configurations.
     *
     * @return void
     */
    public function prepend(ContainerBuilder $container);
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Extension;

use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Config\Definition\Processor;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\BadMethodCallException;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\LogicException;

/**
 * Provides useful features shared by many extensions.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
abstract class Extension implements ExtensionInterface, ConfigurationExtensionInterface
{
    private array $processedConfigs = [];

    /**
     * @return string|false
     */
    public function getXsdValidationBasePath()
    {
        return false;
    }

    /**
     * @return string
     */
    public function getNamespace()
    {
        return 'http://example.org/schema/dic/'.$this->getAlias();
    }

    /**
     * Returns the recommended alias to use in XML.
     *
     * This alias is also the mandatory prefix to use when using YAML.
     *
     * This convention is to remove the "Extension" postfix from the class
     * name and then lowercase and underscore the result. So:
     *
     *     AcmeHelloExtension
     *
     * becomes
     *
     *     acme_hello
     *
     * This can be overridden in a sub-class to specify the alias manually.
     *
     * @throws BadMethodCallException When the extension name does not follow conventions
     */
    public function getAlias(): string
    {
        $className = static::class;
        if (!str_ends_with($className, 'Extension')) {
            throw new BadMethodCallException('This extension does not follow the naming convention; you must overwrite the getAlias() method.');
        }
        $classBaseName = substr(strrchr($className, '\\'), 1, -9);

        return Container::underscore($classBaseName);
    }

    /**
     * @return ConfigurationInterface|null
     */
    public function getConfiguration(array $config, ContainerBuilder $container)
    {
        $class = static::class;

        if (str_contains($class, "\0")) {
            return null; // ignore anonymous classes
        }

        $class = substr_replace($class, '\Configuration', strrpos($class, '\\'));
        $class = $container->getReflectionClass($class);

        if (!$class) {
            return null;
        }

        if (!$class->implementsInterface(ConfigurationInterface::class)) {
            throw new LogicException(\sprintf('The extension configuration class "%s" must implement "%s".', $class->getName(), ConfigurationInterface::class));
        }

        if (!($constructor = $class->getConstructor()) || !$constructor->getNumberOfRequiredParameters()) {
            return $class->newInstance();
        }

        return null;
    }

    final protected function processConfiguration(ConfigurationInterface $configuration, array $configs): array
    {
        $processor = new Processor();

        return $this->processedConfigs[] = $processor->processConfiguration($configuration, $configs);
    }

    /**
     * @internal
     */
    final public function getProcessedConfigs(): array
    {
        try {
            return $this->processedConfigs;
        } finally {
            $this->processedConfigs = [];
        }
    }

    /**
     * @throws InvalidArgumentException When the config is not enableable
     */
    protected function isConfigEnabled(ContainerBuilder $container, array $config): bool
    {
        if (!\array_key_exists('enabled', $config)) {
            throw new InvalidArgumentException("The config array has no 'enabled' key.");
        }

        return (bool) $container->getParameterBag()->resolveValue($config['enabled']);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Extension;

use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

/**
 * ConfigurationExtensionInterface is the interface implemented by container extension classes.
 *
 * @author Kevin Bond <kevinbond@gmail.com>
 */
interface ConfigurationExtensionInterface
{
    /**
     * Returns extension configuration.
     *
     * @return ConfigurationInterface|null
     */
    public function getConfiguration(array $config, ContainerBuilder $container);
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Extension;

use Symfony\Component\Config\Definition\ConfigurableInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

/**
 * @author Yonel Ceruto <yonelceruto@gmail.com>
 */
interface ConfigurableExtensionInterface extends ConfigurableInterface
{
    /**
     * Allows an extension to prepend the extension configurations.
     */
    public function prependExtension(ContainerConfigurator $container, ContainerBuilder $builder): void;

    /**
     * Loads a specific configuration.
     */
    public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void;
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Extension;

use Symfony\Component\Config\Definition\Configuration;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

/**
 * An Extension that provides configuration hooks.
 *
 * @author Yonel Ceruto <yonelceruto@gmail.com>
 */
abstract class AbstractExtension extends Extension implements ConfigurableExtensionInterface, PrependExtensionInterface
{
    use ExtensionTrait;

    public function configure(DefinitionConfigurator $definition): void
    {
    }

    public function prependExtension(ContainerConfigurator $container, ContainerBuilder $builder): void
    {
    }

    public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void
    {
    }

    public function getConfiguration(array $config, ContainerBuilder $container): ?ConfigurationInterface
    {
        return new Configuration($this, $container, $this->getAlias());
    }

    final public function prepend(ContainerBuilder $container): void
    {
        $callback = function (ContainerConfigurator $configurator) use ($container) {
            $this->prependExtension($configurator, $container);
        };

        $this->executeConfiguratorCallback($container, $callback, $this);
    }

    final public function load(array $configs, ContainerBuilder $container): void
    {
        $config = $this->processConfiguration($this->getConfiguration([], $container), $configs);

        $callback = function (ContainerConfigurator $configurator) use ($config, $container) {
            $this->loadExtension($config, $configurator, $container);
        };

        $this->executeConfiguratorCallback($container, $callback, $this);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection;

/**
 * ContainerAwareInterface should be implemented by classes that depends on a Container.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 *
 * @deprecated since Symfony 6.4, use dependency injection instead
 */
interface ContainerAwareInterface
{
    /**
     * Sets the container.
     *
     * @return void
     */
    public function setContainer(?ContainerInterface $container);
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Attribute;

#[\Attribute(\Attribute::TARGET_PARAMETER | \Attribute::TARGET_PROPERTY)]
class AutowireDecorated
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Attribute;

use Symfony\Component\DependencyInjection\ContainerInterface;

#[\Attribute(\Attribute::TARGET_CLASS)]
class AsDecorator
{
    public function __construct(
        public string $decorates,
        public int $priority = 0,
        public int $onInvalid = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE,
    ) {
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Attribute;

/**
 * An attribute to tell under which index and priority a service class should be found in tagged iterators/locators.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
#[\Attribute(\Attribute::TARGET_CLASS)]
class AsTaggedItem
{
    public function __construct(
        public ?string $index = null,
        public ?int $priority = null,
    ) {
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Attribute;

use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\LogicException;

/**
 * An attribute to tell how a dependency is used and hint named autowiring aliases.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
#[\Attribute(\Attribute::TARGET_PARAMETER | \Attribute::TARGET_PROPERTY)]
final class Target
{
    public function __construct(
        public ?string $name = null,
    ) {
    }

    public function getParsedName(): string
    {
        if (null === $this->name) {
            throw new LogicException(\sprintf('Cannot parse the name of a #[Target] attribute that has not been resolved. Did you forget to call "%s::parseName()"?', __CLASS__));
        }

        return lcfirst(str_replace(' ', '', ucwords(preg_replace('/[^a-zA-Z0-9\x7f-\xff]++/', ' ', $this->name))));
    }

    public static function parseName(\ReflectionParameter $parameter, ?self &$attribute = null, ?string &$parsedName = null): string
    {
        $attribute = null;
        if (!$target = $parameter->getAttributes(self::class)[0] ?? null) {
            $parsedName = (new self($parameter->name))->getParsedName();

            return $parameter->name;
        }

        $attribute = $target->newInstance();
        $name = $attribute->name ??= $parameter->name;
        $parsedName = $attribute->getParsedName();

        if (!preg_match('/^[a-zA-Z_\x7f-\xff]/', $parsedName)) {
            if (($function = $parameter->getDeclaringFunction()) instanceof \ReflectionMethod) {
                $function = $function->class.'::'.$function->name;
            } else {
                $function = $function->name;
            }

            throw new InvalidArgumentException(\sprintf('Invalid #[Target] name "%s" on parameter "$%s" of "%s()": the first character must be a letter.', $name, $parameter->name, $function));
        }

        return preg_match('/^[a-zA-Z0-9_\x7f-\xff]++$/', $name) ? $name : $parsedName;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Attribute;

/**
 * An attribute to tell under which alias a service should be registered or to use the implemented interface if no parameter is given.
 *
 * @author Alan Poulain <contact@alanpoulain.eu>
 */
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
final class AsAlias
{
    public function __construct(
        public ?string $id = null,
        public bool $public = false,
    ) {
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Attribute;

/**
 * An attribute to tell how a base type should be tagged.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
class AutoconfigureTag extends Autoconfigure
{
    public function __construct(?string $name = null, array $attributes = [])
    {
        parent::__construct(
            tags: [
                [$name ?? 0 => $attributes],
            ]
        );
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Attribute;

#[\Attribute(\Attribute::TARGET_PARAMETER)]
class TaggedIterator extends AutowireIterator
{
    public function __construct(
        public string $tag,
        public ?string $indexAttribute = null,
        public ?string $defaultIndexMethod = null,
        public ?string $defaultPriorityMethod = null,
        public string|array $exclude = [],
        public bool $excludeSelf = true,
    ) {
        parent::__construct($tag, $indexAttribute, $defaultIndexMethod, $defaultPriorityMethod, $exclude, $excludeSelf);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Attribute;

/**
 * An attribute to tell the class should not be registered as service.
 *
 * @author Grégoire Pineau <lyrixx@lyrixx.info>
 */
#[\Attribute(\Attribute::TARGET_CLASS)]
class Exclude
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Attribute;

use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;

/**
 * Autowires an iterator of services based on a tag name.
 */
#[\Attribute(\Attribute::TARGET_PARAMETER)]
class AutowireIterator extends Autowire
{
    /**
     * @param string|string[] $exclude A service or a list of services to exclude
     */
    public function __construct(
        string $tag,
        ?string $indexAttribute = null,
        ?string $defaultIndexMethod = null,
        ?string $defaultPriorityMethod = null,
        string|array $exclude = [],
        bool $excludeSelf = true,
    ) {
        parent::__construct(new TaggedIteratorArgument($tag, $indexAttribute, $defaultIndexMethod, false, $defaultPriorityMethod, (array) $exclude, $excludeSelf));
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Attribute;

use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\Reference;

/**
 * Attribute to wrap a service in a closure that returns it.
 */
#[\Attribute(\Attribute::TARGET_PARAMETER)]
class AutowireServiceClosure extends Autowire
{
    public function __construct(string $service)
    {
        parent::__construct(new ServiceClosureArgument(new Reference($service)));
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Attribute;

trigger_deprecation('symfony/dependency-injection', '6.3', 'The "%s" class is deprecated, use "%s" instead.', MapDecorated::class, AutowireDecorated::class);

/**
 * @deprecated since Symfony 6.3, use AutowireDecorated instead
 */
#[\Attribute(\Attribute::TARGET_PARAMETER)]
class MapDecorated
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Attribute;

/**
 * An attribute to tell how a base type should be autoconfigured.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
class Autoconfigure
{
    public function __construct(
        public ?array $tags = null,
        public ?array $calls = null,
        public ?array $bind = null,
        public bool|string|null $lazy = null,
        public ?bool $public = null,
        public ?bool $shared = null,
        public ?bool $autowire = null,
        public ?array $properties = null,
        public array|string|null $configurator = null,
        public ?string $constructor = null,
    ) {
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Attribute;

use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\TypedReference;
use Symfony\Contracts\Service\Attribute\SubscribedService;
use Symfony\Contracts\Service\ServiceSubscriberInterface;

/**
 * Autowires a service locator based on a tag name or an explicit list of key => service-type pairs.
 */
#[\Attribute(\Attribute::TARGET_PARAMETER)]
class AutowireLocator extends Autowire
{
    /**
     * @see ServiceSubscriberInterface::getSubscribedServices()
     *
     * @param string|array<string|Autowire|SubscribedService> $services An explicit list of services or a tag name
     * @param string|string[]                                 $exclude  A service or a list of services to exclude
     */
    public function __construct(
        string|array $services,
        ?string $indexAttribute = null,
        ?string $defaultIndexMethod = null,
        ?string $defaultPriorityMethod = null,
        string|array $exclude = [],
        bool $excludeSelf = true,
    ) {
        if (\is_string($services)) {
            parent::__construct(new ServiceLocatorArgument(new TaggedIteratorArgument($services, $indexAttribute, $defaultIndexMethod, true, $defaultPriorityMethod, (array) $exclude, $excludeSelf)));

            return;
        }

        $references = [];

        foreach ($services as $key => $type) {
            $attributes = [];

            if ($type instanceof Autowire) {
                $references[$key] = $type;
                continue;
            }

            if ($type instanceof SubscribedService) {
                $key = $type->key ?? $key;
                $attributes = $type->attributes;
                $type = ($type->nullable ? '?' : '').($type->type ?? throw new InvalidArgumentException(\sprintf('When "%s" is used, a type must be set.', SubscribedService::class)));
            }

            if (!\is_string($type) || !preg_match('/(?(DEFINE)(?<cn>[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+))(?(DEFINE)(?<fqcn>(?&cn)(?:\\\\(?&cn))*+))^\??(?&fqcn)(?:(?:\|(?&fqcn))*+|(?:&(?&fqcn))*+)$/', $type)) {
                throw new InvalidArgumentException(\sprintf('"%s" is not a PHP type for key "%s".', \is_string($type) ? $type : get_debug_type($type), $key));
            }
            $optionalBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE;
            if ('?' === $type[0]) {
                $type = substr($type, 1);
                $optionalBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE;
            }
            if (\is_int($name = $key)) {
                $key = $type;
                $name = null;
            }

            $references[$key] = new TypedReference($type, $type, $optionalBehavior, $name, $attributes);
        }

        parent::__construct(new ServiceLocatorArgument($references));
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Attribute;

/**
 * An attribute to tell under which environment this class should be registered as a service.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::TARGET_FUNCTION | \Attribute::IS_REPEATABLE)]
class When
{
    public function __construct(
        public string $env,
    ) {
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Attribute;

use Symfony\Component\DependencyInjection\Argument\ArgumentInterface;
use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\ExpressionLanguage\Expression;

/**
 * Attribute to tell a parameter how to be autowired.
 *
 * @author Kevin Bond <kevinbond@gmail.com>
 */
#[\Attribute(\Attribute::TARGET_PARAMETER | \Attribute::TARGET_PROPERTY)]
class Autowire
{
    public readonly string|array|Expression|Reference|ArgumentInterface|null $value;
    public readonly bool|array $lazy;

    /**
     * Use only ONE of the following.
     *
     * @param string|array|ArgumentInterface|null $value      Value to inject (ie "%kernel.project_dir%/some/path")
     * @param string|null                         $service    Service ID (ie "some.service")
     * @param string|null                         $expression Expression (ie 'service("some.service").someMethod()')
     * @param string|null                         $env        Environment variable name (ie 'SOME_ENV_VARIABLE')
     * @param string|null                         $param      Parameter name (ie 'some.parameter.name')
     * @param bool|class-string|class-string[]    $lazy       Whether to use lazy-loading for this argument
     */
    public function __construct(
        string|array|ArgumentInterface|null $value = null,
        ?string $service = null,
        ?string $expression = null,
        ?string $env = null,
        ?string $param = null,
        bool|string|array $lazy = false,
    ) {
        if ($this->lazy = \is_string($lazy) ? [$lazy] : $lazy) {
            if (null !== ($expression ?? $env ?? $param)) {
                throw new LogicException('#[Autowire] attribute cannot be $lazy and use $expression, $env, or $param.');
            }
            if (null !== $value && null !== $service) {
                throw new LogicException('#[Autowire] attribute cannot declare $value and $service at the same time.');
            }
        } elseif (1 !== (null !== $value) + (null !== $service) + (null !== $expression) + (null !== $env) + (null !== $param)) {
            throw new LogicException('#[Autowire] attribute must declare exactly one of $service, $expression, $env, $param or $value.');
        }

        if (\is_string($value) && str_starts_with($value, '@')) {
            match (true) {
                str_starts_with($value, '@@') => $value = substr($value, 1),
                str_starts_with($value, '@=') => $expression = substr($value, 2),
                default => $service = substr($value, 1),
            };
        }

        $this->value = match (true) {
            null !== $service => new Reference($service),
            null !== $expression => class_exists(Expression::class) ? new Expression($expression) : throw new LogicException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed. Try running "composer require symfony/expression-language".'),
            null !== $env => "%env($env)%",
            null !== $param => "%$param%",
            default => $value,
        };
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Attribute;

use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\DependencyInjection\Reference;

/**
 * Attribute to tell which callable to give to an argument of type Closure.
 */
#[\Attribute(\Attribute::TARGET_PARAMETER)]
class AutowireCallable extends Autowire
{
    /**
     * @param bool|class-string $lazy Whether to use lazy-loading for this argument
     */
    public function __construct(
        string|array|null $callable = null,
        ?string $service = null,
        ?string $method = null,
        bool|string $lazy = false,
    ) {
        if (!(null !== $callable xor null !== $service)) {
            throw new LogicException('#[AutowireCallable] attribute must declare exactly one of $callable or $service.');
        }
        if (null === $service && null !== $method) {
            throw new LogicException('#[AutowireCallable] attribute cannot have a $method without a $service.');
        }

        parent::__construct($callable ?? [new Reference($service), $method ?? '__invoke'], lazy: $lazy);
    }

    public function buildDefinition(mixed $value, ?string $type, \ReflectionParameter $parameter): Definition
    {
        return (new Definition($type = \is_array($this->lazy) ? current($this->lazy) : ($type ?: 'Closure')))
            ->setFactory(['Closure', 'fromCallable'])
            ->setArguments([\is_array($value) ? $value + [1 => '__invoke'] : $value])
            ->setLazy($this->lazy || 'Closure' !== $type && 'callable' !== (string) $parameter->getType());
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Attribute;

#[\Attribute(\Attribute::TARGET_PARAMETER)]
class TaggedLocator extends AutowireLocator
{
    public function __construct(
        public string $tag,
        public ?string $indexAttribute = null,
        public ?string $defaultIndexMethod = null,
        public ?string $defaultPriorityMethod = null,
        public string|array $exclude = [],
        public bool $excludeSelf = true,
    ) {
        parent::__construct($tag, $indexAttribute, $defaultIndexMethod, $defaultPriorityMethod, $exclude, $excludeSelf);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection;

use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage as BaseExpressionLanguage;

if (!class_exists(BaseExpressionLanguage::class)) {
    return;
}

/**
 * Adds some function to the default ExpressionLanguage.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 *
 * @see ExpressionLanguageProvider
 */
class ExpressionLanguage extends BaseExpressionLanguage
{
    public function __construct(?CacheItemPoolInterface $cache = null, array $providers = [], ?callable $serviceCompiler = null, ?\Closure $getEnv = null)
    {
        // prepend the default provider to let users override it easily
        array_unshift($providers, new ExpressionLanguageProvider($serviceCompiler, $getEnv));

        parent::__construct($cache, $providers);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection;

/**
 * Represents a variable.
 *
 *     $var = new Variable('a');
 *
 * will be dumped as
 *
 *     $a
 *
 * by the PHP dumper.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class Variable
{
    private string $name;

    public function __construct(string $name)
    {
        $this->name = $name;
    }

    public function __toString(): string
    {
        return $this->name;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 */
class RegisterReverseContainerPass implements CompilerPassInterface
{
    private bool $beforeRemoving;

    public function __construct(bool $beforeRemoving)
    {
        $this->beforeRemoving = $beforeRemoving;
    }

    /**
     * @return void
     */
    public function process(ContainerBuilder $container)
    {
        if (!$container->hasDefinition('reverse_container')) {
            return;
        }

        $refType = $this->beforeRemoving ? ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE : ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE;
        $services = [];
        foreach ($container->findTaggedServiceIds('container.reversible') as $id => $tags) {
            $services[$id] = new Reference($id, $refType);
        }

        if ($this->beforeRemoving) {
            // prevent inlining of the reverse container
            $services['reverse_container'] = new Reference('reverse_container', $refType);
        }
        $locator = $container->getDefinition('reverse_container')->getArgument(1);

        if ($locator instanceof Reference) {
            $locator = $container->getDefinition((string) $locator);
        }
        if ($locator instanceof Definition) {
            foreach ($services as $id => $ref) {
                $services[$id] = new ServiceClosureArgument($ref);
            }
            $locator->replaceArgument(0, $services);
        } else {
            $locator->setValues($services);
        }
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\Config\Resource\ClassExistenceResource;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\DependencyInjection\Attribute\AutowireCallable;
use Symfony\Component\DependencyInjection\Attribute\AutowireDecorated;
use Symfony\Component\DependencyInjection\Attribute\MapDecorated;
use Symfony\Component\DependencyInjection\Attribute\Target;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\AutowiringFailedException;
use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\TypedReference;
use Symfony\Component\VarExporter\ProxyHelper;

/**
 * Inspects existing service definitions and wires the autowired ones using the type hints of their classes.
 *
 * @author Kévin Dunglas <dunglas@gmail.com>
 * @author Nicolas Grekas <p@tchwork.com>
 */
class AutowirePass extends AbstractRecursivePass
{
    protected bool $skipScalars = true;

    private array $types;
    private array $ambiguousServiceTypes;
    private array $autowiringAliases;
    private ?string $lastFailure = null;
    private bool $throwOnAutowiringException;
    private ?string $decoratedClass = null;
    private ?string $decoratedId = null;
    private ?array $methodCalls = null;
    private object $defaultArgument;
    private ?\Closure $getPreviousValue = null;
    private ?int $decoratedMethodIndex = null;
    private ?int $decoratedMethodArgumentIndex = null;
    private ?self $typesClone = null;

    public function __construct(bool $throwOnAutowireException = true)
    {
        $this->throwOnAutowiringException = $throwOnAutowireException;
        $this->defaultArgument = new class {
            public $value;
            public $names;
            public $bag;

            public function withValue(\ReflectionParameter $parameter): self
            {
                $clone = clone $this;
                $clone->value = $this->bag->escapeValue($parameter->getDefaultValue());

                return $clone;
            }
        };
    }

    /**
     * @return void
     */
    public function process(ContainerBuilder $container)
    {
        $this->defaultArgument->bag = $container->getParameterBag();

        try {
            $this->typesClone = clone $this;
            parent::process($container);
        } finally {
            $this->decoratedClass = null;
            $this->decoratedId = null;
            $this->methodCalls = null;
            $this->defaultArgument->bag = null;
            $this->defaultArgument->names = null;
            $this->getPreviousValue = null;
            $this->decoratedMethodIndex = null;
            $this->decoratedMethodArgumentIndex = null;
            $this->typesClone = null;
        }
    }

    protected function processValue(mixed $value, bool $isRoot = false): mixed
    {
        if ($value instanceof Autowire) {
            $value = $this->processValue($this->container->getParameterBag()->resolveValue($value->value));
            // count env vars referenced by the attribute right away, so that removing the
            // owning service (e.g. unused with an unrelated autowiring error) does not later
            // report the env var as never used
            $this->container->resolveEnvPlaceholders($value);

            return $value;
        }

        if ($value instanceof AutowireDecorated || $value instanceof MapDecorated) {
            $definition = $this->container->getDefinition($this->currentId);

            return new Reference($definition->innerServiceId ?? $this->currentId.'.inner', $definition->decorationOnInvalid ?? ContainerInterface::NULL_ON_INVALID_REFERENCE);
        }

        try {
            return $this->doProcessValue($value, $isRoot);
        } catch (AutowiringFailedException $e) {
            if ($this->throwOnAutowiringException) {
                throw $e;
            }

            $this->container->getDefinition($this->currentId)->addError($e->getMessageCallback() ?? $e->getMessage());

            return parent::processValue($value, $isRoot);
        }
    }

    private function doProcessValue(mixed $value, bool $isRoot = false): mixed
    {
        if ($value instanceof TypedReference) {
            foreach ($value->getAttributes() as $attribute) {
                if ($attribute === $v = $this->processValue($attribute)) {
                    continue;
                }
                if (!$attribute instanceof Autowire || !$v instanceof Reference) {
                    return $v;
                }

                $invalidBehavior = ContainerBuilder::EXCEPTION_ON_INVALID_REFERENCE !== $v->getInvalidBehavior() ? $v->getInvalidBehavior() : $value->getInvalidBehavior();
                $value = $v instanceof TypedReference
                    ? new TypedReference($v, $v->getType(), $invalidBehavior, $v->getName() ?? $value->getName(), array_merge($v->getAttributes(), $value->getAttributes()))
                    : new TypedReference($v, $value->getType(), $invalidBehavior, $value->getName(), $value->getAttributes());
                break;
            }
            if ($ref = $this->getAutowiredReference($value, true)) {
                return $ref;
            }
            if (ContainerBuilder::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE === $value->getInvalidBehavior()) {
                $message = $this->createTypeNotFoundMessageCallback($value, 'it');

                // since the error message varies by referenced id and $this->currentId, so should the id of the dummy errored definition
                $this->container->register($id = \sprintf('.errored.%s.%s', $this->currentId, (string) $value), $value->getType())
                    ->addError($message);

                return new TypedReference($id, $value->getType(), $value->getInvalidBehavior(), $value->getName());
            }
        }
        $value = parent::processValue($value, $isRoot);

        if (!$value instanceof Definition || !$value->isAutowired() || $value->isAbstract() || !$value->getClass()) {
            return $value;
        }
        if (!$reflectionClass = $this->container->getReflectionClass($value->getClass(), false)) {
            $this->container->log($this, \sprintf('Skipping service "%s": Class or interface "%s" cannot be loaded.', $this->currentId, $value->getClass()));

            return $value;
        }

        $this->methodCalls = $value->getMethodCalls();

        try {
            $constructor = $this->getConstructor($value, false);
        } catch (RuntimeException $e) {
            throw new AutowiringFailedException($this->currentId, $e->getMessage(), 0, $e);
        }

        if ($constructor) {
            array_unshift($this->methodCalls, [$constructor, $value->getArguments()]);
        }

        $checkAttributes = !$value->hasTag('container.ignore_attributes');
        $this->methodCalls = $this->autowireCalls($reflectionClass, $isRoot, $checkAttributes);

        if ($constructor) {
            [, $arguments] = array_shift($this->methodCalls);

            if ($arguments !== $value->getArguments()) {
                $value->setArguments($arguments);
            }
        }

        if ($this->methodCalls !== $value->getMethodCalls()) {
            $value->setMethodCalls($this->methodCalls);
        }

        return $value;
    }

    private function autowireCalls(\ReflectionClass $reflectionClass, bool $isRoot, bool $checkAttributes): array
    {
        $this->decoratedId = null;
        $this->decoratedClass = null;
        $this->getPreviousValue = null;

        if ($isRoot && ($definition = $this->container->getDefinition($this->currentId)) && null !== ($this->decoratedId = $definition->innerServiceId) && $this->container->has($this->decoratedId)) {
            $this->decoratedClass = $this->container->findDefinition($this->decoratedId)->getClass();
        }

        $patchedIndexes = [];

        foreach ($this->methodCalls as $i => $call) {
            [$method, $arguments] = $call;

            if ($method instanceof \ReflectionFunctionAbstract) {
                $reflectionMethod = $method;
            } else {
                $definition = new Definition($reflectionClass->name);
                try {
                    $reflectionMethod = $this->getReflectionMethod($definition, $method);
                } catch (RuntimeException $e) {
                    if ($definition->getFactory()) {
                        continue;
                    }
                    throw $e;
                }
            }

            $arguments = $this->autowireMethod($reflectionMethod, $arguments, $checkAttributes, $i);

            if ($arguments !== $call[1]) {
                $this->methodCalls[$i][1] = $arguments;
                $patchedIndexes[] = $i;
            }
        }

        // use named arguments to skip complex default values
        foreach ($patchedIndexes as $i) {
            $namedArguments = null;
            $arguments = $this->methodCalls[$i][1];

            foreach ($arguments as $j => $value) {
                if ($namedArguments && !$value instanceof $this->defaultArgument) {
                    unset($arguments[$j]);
                    $arguments[$namedArguments[$j]] = $value;
                }
                if (!$value instanceof $this->defaultArgument) {
                    continue;
                }

                if (\is_array($value->value) ? $value->value : \is_object($value->value)) {
                    unset($arguments[$j]);
                    $namedArguments = $value->names;
                }

                if ($namedArguments) {
                    unset($arguments[$j]);
                } else {
                    $arguments[$j] = $value->value;
                }
            }

            $this->methodCalls[$i][1] = $arguments;
        }

        return $this->methodCalls;
    }

    /**
     * Autowires the constructor or a method.
     *
     * @throws AutowiringFailedException
     */
    private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, array $arguments, bool $checkAttributes, int $methodIndex): array
    {
        $class = $reflectionMethod instanceof \ReflectionMethod ? $reflectionMethod->class : $this->currentId;
        $method = $reflectionMethod->name;
        $parameters = $reflectionMethod->getParameters();
        if ($reflectionMethod->isVariadic()) {
            array_pop($parameters);
        }
        $this->defaultArgument->names = new \ArrayObject();

        foreach ($parameters as $index => $parameter) {
            $this->defaultArgument->names[$index] = $parameter->name;

            if (\array_key_exists($parameter->name, $arguments)) {
                $arguments[$index] = $arguments[$parameter->name];
                unset($arguments[$parameter->name]);
            }
            if (\array_key_exists($index, $arguments) && '' !== $arguments[$index]) {
                continue;
            }

            $type = ProxyHelper::exportType($parameter, true);
            $target = null;
            $name = Target::parseName($parameter, $target);
            $target = $target ? [$target] : [];

            $getValue = function () use ($type, $parameter, $class, $method, $name, $target) {
                if (!$value = $this->getAutowiredReference($ref = new TypedReference($type, $type, ContainerBuilder::EXCEPTION_ON_INVALID_REFERENCE, $name, $target), false)) {
                    $failureMessage = $this->createTypeNotFoundMessageCallback($ref, \sprintf('argument "$%s" of method "%s()"', $parameter->name, $class !== $this->currentId ? $class.'::'.$method : $method));

                    if ($parameter->isDefaultValueAvailable()) {
                        $value = $this->defaultArgument->withValue($parameter);
                    } elseif (!$parameter->allowsNull()) {
                        throw new AutowiringFailedException($this->currentId, $failureMessage);
                    }
                }

                return $value;
            };

            if ($checkAttributes) {
                foreach ($parameter->getAttributes(Autowire::class, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
                    $attribute = $attribute->newInstance();
                    $invalidBehavior = $parameter->allowsNull() ? ContainerInterface::NULL_ON_INVALID_REFERENCE : ContainerBuilder::EXCEPTION_ON_INVALID_REFERENCE;

                    try {
                        $value = $this->processValue(new TypedReference($type ?: '?', $type ?: 'mixed', $invalidBehavior, $name, [$attribute, ...$target]));
                    } catch (ParameterNotFoundException $e) {
                        if (!$parameter->isDefaultValueAvailable()) {
                            throw new AutowiringFailedException($this->currentId, $e->getMessage(), 0, $e);
                        }
                        $arguments[$index] = clone $this->defaultArgument;
                        $arguments[$index]->value = $parameter->getDefaultValue();

                        continue 2;
                    }

                    if ($attribute instanceof AutowireCallable) {
                        $value = $attribute->buildDefinition($value, $type, $parameter);
                    } elseif ($lazy = $attribute->lazy) {
                        $definition = (new Definition($type))
                            ->setFactory('current')
                            ->setArguments([[$value ??= $getValue()]])
                            ->setLazy(true);

                        if (!\is_array($lazy)) {
                            if (str_contains($type, '|')) {
                                throw new AutowiringFailedException($this->currentId, \sprintf('Cannot use #[Autowire] with option "lazy: true" on union types for service "%s"; set the option to the interface(s) that should be proxied instead.', $this->currentId));
                            }
                            $lazy = str_contains($type, '&') ? explode('&', $type) : [];
                        }

                        if ($lazy) {
                            if (!class_exists($type) && !interface_exists($type, false)) {
                                $definition->setClass('object');
                            }
                            foreach ($lazy as $v) {
                                $definition->addTag('proxy', ['interface' => $v]);
                            }
                        }

                        if ($definition->getClass() !== (string) $value || $definition->getTag('proxy')) {
                            $value .= '.'.$this->container->hash([$definition->getClass(), $definition->getTag('proxy')]);
                        }
                        $this->container->setDefinition($value = '.lazy.'.$value, $definition);
                        $value = new Reference($value);
                    }
                    $arguments[$index] = $value;

                    continue 2;
                }

                foreach ($parameter->getAttributes(AutowireDecorated::class) as $attribute) {
                    $arguments[$index] = $this->processValue($attribute->newInstance());

                    continue 2;
                }

                foreach ($parameter->getAttributes(MapDecorated::class) as $attribute) {
                    $arguments[$index] = $this->processValue($attribute->newInstance());

                    continue 2;
                }
            }

            if (!$type) {
                if (isset($arguments[$index])) {
                    continue;
                }

                // no default value? Then fail
                if (!$parameter->isDefaultValueAvailable()) {
                    // For core classes, isDefaultValueAvailable() can
                    // be false when isOptional() returns true. If the
                    // argument *is* optional, allow it to be missing
                    if ($parameter->isOptional()) {
                        --$index;
                        break;
                    }
                    $type = ProxyHelper::exportType($parameter);
                    $type = $type ? \sprintf('is type-hinted "%s"', preg_replace('/(^|[(|&])\\\\|^\?\\\\?/', '\1', $type)) : 'has no type-hint';

                    throw new AutowiringFailedException($this->currentId, \sprintf('Cannot autowire service "%s": argument "$%s" of method "%s()" %s, you should configure its value explicitly.', $this->currentId, $parameter->name, $class !== $this->currentId ? $class.'::'.$method : $method, $type));
                }

                // specifically pass the default value
                $arguments[$index] = $this->defaultArgument->withValue($parameter);

                continue;
            }

            if ($this->decoratedClass && is_a($this->decoratedClass, $type, true)) {
                if ($this->getPreviousValue) {
                    // The inner service is injected only if there is only 1 argument matching the type of the decorated class
                    // across all arguments of all autowired methods.
                    // If a second matching argument is found, the default behavior is restored.

                    $getPreviousValue = $this->getPreviousValue;
                    $this->methodCalls[$this->decoratedMethodIndex][1][$this->decoratedMethodArgumentIndex] = $getPreviousValue();
                    $this->decoratedClass = null; // Prevent further checks
                } else {
                    $arguments[$index] = new TypedReference($this->decoratedId, $this->decoratedClass);
                    $this->getPreviousValue = $getValue;
                    $this->decoratedMethodIndex = $methodIndex;
                    $this->decoratedMethodArgumentIndex = $index;

                    continue;
                }
            }

            $arguments[$index] = $getValue();
        }

        if ($parameters && !isset($arguments[++$index])) {
            while (0 <= --$index) {
                if (!$arguments[$index] instanceof $this->defaultArgument) {
                    break;
                }
                unset($arguments[$index]);
            }
        }

        // it's possible index 1 was set, then index 0, then 2, etc
        // make sure that we re-order so they're injected as expected
        ksort($arguments, \SORT_NATURAL);

        return $arguments;
    }

    /**
     * Returns a reference to the service matching the given type, if any.
     */
    private function getAutowiredReference(TypedReference $reference, bool $filterType): ?TypedReference
    {
        $this->lastFailure = null;
        $type = $reference->getType();

        if ($type !== (string) $reference) {
            return $reference;
        }

        if ($filterType && false !== $m = strpbrk($type, '&|')) {
            $types = array_diff(explode($m[0], $type), ['int', 'string', 'array', 'bool', 'float', 'iterable', 'object', 'callable', 'null']);

            sort($types);

            $type = implode($m[0], $types);
        }

        $name = $target = (array_filter($reference->getAttributes(), static fn ($a) => $a instanceof Target)[0] ?? null)?->name;

        if (null !== $name ??= $reference->getName()) {
            if ($this->container->has($alias = $type.' $'.$name) && !$this->container->findDefinition($alias)->isAbstract()) {
                return new TypedReference($alias, $type, $reference->getInvalidBehavior());
            }

            if (null !== ($alias = $this->getCombinedAlias($type, $name)) && !$this->container->findDefinition($alias)->isAbstract()) {
                return new TypedReference($alias, $type, $reference->getInvalidBehavior());
            }

            $parsedName = (new Target($name))->getParsedName();

            if ($this->container->has($alias = $type.' $'.$parsedName) && !$this->container->findDefinition($alias)->isAbstract()) {
                return new TypedReference($alias, $type, $reference->getInvalidBehavior());
            }

            if (null !== ($alias = $this->getCombinedAlias($type, $parsedName)) && !$this->container->findDefinition($alias)->isAbstract()) {
                return new TypedReference($alias, $type, $reference->getInvalidBehavior());
            }

            if (($this->container->has($n = $name) && !$this->container->findDefinition($n)->isAbstract())
                || ($this->container->has($n = $parsedName) && !$this->container->findDefinition($n)->isAbstract())
            ) {
                foreach ($this->container->getAliases() as $id => $alias) {
                    if ($n === (string) $alias && str_starts_with($id, $type.' $')) {
                        return new TypedReference($n, $type, $reference->getInvalidBehavior());
                    }
                }
            }

            if (null !== $target) {
                return null;
            }
        }

        if ($this->container->has($type) && !$this->container->findDefinition($type)->isAbstract()) {
            return new TypedReference($type, $type, $reference->getInvalidBehavior());
        }

        if (null !== ($alias = $this->getCombinedAlias($type)) && !$this->container->findDefinition($alias)->isAbstract()) {
            return new TypedReference($alias, $type, $reference->getInvalidBehavior());
        }

        return null;
    }

    /**
     * Populates the list of available types.
     */
    private function populateAvailableTypes(ContainerBuilder $container): void
    {
        $this->types = [];
        $this->ambiguousServiceTypes = [];
        $this->autowiringAliases = [];

        foreach ($container->getDefinitions() as $id => $definition) {
            $this->populateAvailableType($container, $id, $definition);
        }

        $prev = null;
        foreach ($container->getAliases() as $id => $alias) {
            $this->populateAutowiringAlias($id, $prev);
            $prev = $id;
        }
    }

    /**
     * Populates the list of available types for a given definition.
     */
    private function populateAvailableType(ContainerBuilder $container, string $id, Definition $definition): void
    {
        // Never use abstract services
        if ($definition->isAbstract()) {
            return;
        }

        if ('' === $id || '.' === $id[0] || $definition->isDeprecated() || !$reflectionClass = $container->getReflectionClass($definition->getClass(), false)) {
            return;
        }

        foreach ($reflectionClass->getInterfaces() as $reflectionInterface) {
            $this->set($reflectionInterface->name, $id);
        }

        do {
            $this->set($reflectionClass->name, $id);
        } while ($reflectionClass = $reflectionClass->getParentClass());

        $this->populateAutowiringAlias($id);
    }

    /**
     * Associates a type and a service id if applicable.
     */
    private function set(string $type, string $id): void
    {
        // is this already a type/class that is known to match multiple services?
        if (isset($this->ambiguousServiceTypes[$type])) {
            $this->ambiguousServiceTypes[$type][] = $id;

            return;
        }

        // check to make sure the type doesn't match multiple services
        if (!isset($this->types[$type]) || $this->types[$type] === $id) {
            $this->types[$type] = $id;

            return;
        }

        // keep an array of all services matching this type
        if (!isset($this->ambiguousServiceTypes[$type])) {
            $this->ambiguousServiceTypes[$type] = [$this->types[$type]];
            unset($this->types[$type]);
        }
        $this->ambiguousServiceTypes[$type][] = $id;
    }

    private function createTypeNotFoundMessageCallback(TypedReference $reference, string $label): \Closure
    {
        if (!isset($this->typesClone->container)) {
            $this->typesClone->container = new ContainerBuilder($this->container->getParameterBag());
            $this->typesClone->container->setAliases($this->container->getAliases());
            $this->typesClone->container->setDefinitions($this->container->getDefinitions());
            $this->typesClone->container->setResourceTracking(false);
        }
        $currentId = $this->currentId;

        return (fn () => $this->createTypeNotFoundMessage($reference, $label, $currentId))->bindTo($this->typesClone);
    }

    private function createTypeNotFoundMessage(TypedReference $reference, string $label, string $currentId): string
    {
        $type = $reference->getType();

        $i = null;
        $namespace = $type;
        do {
            $namespace = substr($namespace, 0, $i);

            if ($this->container->hasDefinition($namespace) && $tag = $this->container->getDefinition($namespace)->getTag('container.excluded')) {
                return \sprintf('Cannot autowire service "%s": %s needs an instance of "%s" but this type has been excluded %s.', $currentId, $label, $type, $tag[0]['source'] ?? 'from autowiring');
            }
        } while (false !== $i = strrpos($namespace, '\\'));

        if (!$r = $this->container->getReflectionClass($type, false)) {
            // either $type does not exist or a parent class does not exist
            try {
                if (class_exists(ClassExistenceResource::class)) {
                    $resource = new ClassExistenceResource($type, false);
                    // isFresh() will explode ONLY if a parent class/trait does not exist
                    $resource->isFresh(0);
                    $parentMsg = false;
                } else {
                    $parentMsg = "couldn't be loaded. Either it was not found or it is missing a parent class or a trait";
                }
            } catch (\ReflectionException $e) {
                $parentMsg = \sprintf('is missing a parent class (%s)', $e->getMessage());
            }

            $message = \sprintf('has type "%s" but this class %s.', $type, $parentMsg ?: 'was not found');
        } else {
            $alternatives = $this->createTypeAlternatives($this->container, $reference);

            if (null !== $target = (array_filter($reference->getAttributes(), static fn ($a) => $a instanceof Target)[0] ?? null)) {
                $target = null !== $target->name ? "('{$target->name}')" : '';
                $message = \sprintf('has "#[Target%s]" but no such target exists.%s', $target, $alternatives);
            } else {
                $message = $this->container->has($type) ? 'this service is abstract' : 'no such service exists';
                $message = \sprintf('references %s "%s" but %s.%s', $r->isInterface() ? 'interface' : 'class', $type, $message, $alternatives);
            }

            if ($r->isInterface() && !$alternatives) {
                $message .= ' Did you create a class that implements this interface?';
            }
        }

        $message = \sprintf('Cannot autowire service "%s": %s %s', $currentId, $label, $message);

        if (null !== $this->lastFailure) {
            $message = $this->lastFailure."\n".$message;
            $this->lastFailure = null;
        }

        return $message;
    }

    private function createTypeAlternatives(ContainerBuilder $container, TypedReference $reference): string
    {
        // try suggesting available aliases first
        if ($message = $this->getAliasesSuggestionForType($container, $type = $reference->getType())) {
            return ' '.$message;
        }
        if (!isset($this->ambiguousServiceTypes)) {
            $this->populateAvailableTypes($container);
        }

        $servicesAndAliases = $container->getServiceIds();
        $autowiringAliases = $this->autowiringAliases[$type] ?? [];
        unset($autowiringAliases['']);

        if ($autowiringAliases) {
            return \sprintf(' Did you mean to target%s "%s" instead?', 1 < \count($autowiringAliases) ? ' one of' : '', implode('", "', $autowiringAliases));
        }

        if (!$container->has($type) && false !== $key = array_search(strtolower($type), array_map('strtolower', $servicesAndAliases))) {
            return \sprintf(' Did you mean "%s"?', $servicesAndAliases[$key]);
        } elseif (isset($this->ambiguousServiceTypes[$type])) {
            $message = \sprintf('one of these existing services: "%s"', implode('", "', $this->ambiguousServiceTypes[$type]));
        } elseif (isset($this->types[$type])) {
            $message = \sprintf('the existing "%s" service', $this->types[$type]);
        } else {
            return '';
        }

        return \sprintf(' You should maybe alias this %s to %s.', class_exists($type, false) ? 'class' : 'interface', $message);
    }

    private function getAliasesSuggestionForType(ContainerBuilder $container, string $type): ?string
    {
        $aliases = [];
        foreach (class_parents($type) + class_implements($type) as $parent) {
            if ($container->has($parent) && !$container->findDefinition($parent)->isAbstract()) {
                $aliases[] = $parent;
            }
        }

        if (1 < $len = \count($aliases)) {
            $message = 'Try changing the type-hint to one of its parents: ';
            for ($i = 0, --$len; $i < $len; ++$i) {
                $message .= \sprintf('%s "%s", ', class_exists($aliases[$i], false) ? 'class' : 'interface', $aliases[$i]);
            }
            $message .= \sprintf('or %s "%s".', class_exists($aliases[$i], false) ? 'class' : 'interface', $aliases[$i]);

            return $message;
        }

        if ($aliases) {
            return \sprintf('Try changing the type-hint to "%s" instead.', $aliases[0]);
        }

        return null;
    }

    private function populateAutowiringAlias(string $id, ?string $target = null): void
    {
        if (!preg_match('/(?(DEFINE)(?<V>[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+))^((?&V)(?:\\\\(?&V))*+)(?: \$((?&V)))?$/', $id, $m)) {
            return;
        }

        $type = $m[2];
        $name = $m[3] ?? '';

        if (class_exists($type, false) || interface_exists($type, false)) {
            if (null !== $target && str_starts_with($target, '.'.$type.' $')
                && (new Target($target = substr($target, \strlen($type) + 3)))->getParsedName() === $name
            ) {
                $name = $target;
            }

            $this->autowiringAliases[$type][$name] = $name;
        }
    }

    private function getCombinedAlias(string $type, ?string $name = null): ?string
    {
        if (str_contains($type, '&')) {
            $types = explode('&', trim($type, '()'));
        } elseif (str_contains($type, '|')) {
            $types = explode('|', $type);
        } else {
            return null;
        }

        $alias = null;
        $suffix = $name ? ' $'.$name : '';

        foreach ($types as $type) {
            if (!$this->container->hasAlias($type.$suffix)) {
                return null;
            }

            if (null === $alias) {
                $alias = (string) $this->container->getAlias($type.$suffix);
            } elseif ((string) $this->container->getAlias($type.$suffix) !== $alias) {
                return null;
            }
        }

        return $alias;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException;
use Symfony\Component\DependencyInjection\Reference;

/**
 * Detects cycles where a service's factory is `[Definition $b, $method]` and $b's
 * own properties/method calls/configurator transitively require that same service
 * through constructor references.
 *
 * The soft-circular instantiation pattern relies on storing the service's instance
 * before deferred edges fire, but a factory consumes the builder's state at call
 * time: deferring $b's setup until after `$b->method(...)` would feed the factory
 * an unconfigured builder. The cycle is unresolvable; bail out instead of letting
 * the dumper produce silently wrong code or `ContainerBuilder::createService()`
 * silently return a half-built instance.
 */
class CheckFactoryBuilderCircularReferencePass implements CompilerPassInterface
{
    private ContainerBuilder $container;
    private array $visited;
    private array $path;
    private string $currentId;

    public function process(ContainerBuilder $container): void
    {
        $this->container = $container;

        try {
            foreach ($container->getDefinitions() as $id => $definition) {
                $factory = $definition->getFactory();
                if (!\is_array($factory) || !$factory[0] instanceof Definition) {
                    continue;
                }

                $builder = $factory[0];
                if (!$builder->getMethodCalls() && !$builder->getProperties() && null === $builder->getConfigurator()) {
                    continue;
                }

                $this->currentId = $id;
                $this->visited = [$id => true];
                $this->path = [$id];

                $setup = [$builder->getProperties(), $builder->getMethodCalls(), $builder->getConfigurator()];
                if ($this->setupReferencesCurrent($setup)) {
                    $this->path[] = $id;

                    throw new ServiceCircularReferenceException($id, $this->path);
                }
            }
        } finally {
            unset($this->container, $this->visited, $this->path, $this->currentId);
        }
    }

    private function setupReferencesCurrent(mixed $value): bool
    {
        if (\is_array($value)) {
            foreach ($value as $v) {
                if ($this->setupReferencesCurrent($v)) {
                    return true;
                }
            }

            return false;
        }

        if ($value instanceof Reference) {
            $id = (string) $value;
            while ($this->container->hasAlias($id)) {
                $id = (string) $this->container->getAlias($id);
            }

            if ($id === $this->currentId) {
                return true;
            }

            if (isset($this->visited[$id]) || !$this->container->hasDefinition($id)) {
                return false;
            }

            $def = $this->container->getDefinition($id);

            // Lazy services break the eager-construction chain through a proxy.
            if ($def->isLazy() || $def->isSynthetic()) {
                return false;
            }

            $this->visited[$id] = true;
            $this->path[] = $id;

            // Only constructor edges propagate the "needed during construction" reachability.
            if ($this->setupReferencesCurrent([$def->getArguments(), $def->getFactory()])) {
                return true;
            }

            array_pop($this->path);

            return false;
        }

        if ($value instanceof Definition) {
            // Inlined sub-definition: every part of it runs while the outer service
            // is still being constructed, so walk its full structure.
            return $this->setupReferencesCurrent([
                $value->getArguments(),
                $value->getFactory(),
                $value->getProperties(),
                $value->getMethodCalls(),
                $value->getConfigurator(),
            ]);
        }

        return false;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\Config\Definition\BaseNode;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\Extension\ConfigurationExtensionInterface;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Extension\ExtensionInterface;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;

/**
 * Merges extension configs into the container builder.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class MergeExtensionConfigurationPass implements CompilerPassInterface
{
    /**
     * @return void
     */
    public function process(ContainerBuilder $container)
    {
        $parameters = $container->getParameterBag()->all();
        $definitions = $container->getDefinitions();
        $aliases = $container->getAliases();
        $exprLangProviders = $container->getExpressionLanguageProviders();
        $configAvailable = class_exists(BaseNode::class);

        foreach ($container->getExtensions() as $extension) {
            if ($extension instanceof PrependExtensionInterface) {
                $extension->prepend($container);
            }
        }

        foreach ($container->getExtensions() as $name => $extension) {
            if (!$config = $container->getExtensionConfig($name)) {
                // this extension was not called
                continue;
            }
            $resolvingBag = $container->getParameterBag();
            if ($resolvingBag instanceof EnvPlaceholderParameterBag && $extension instanceof Extension) {
                // create a dedicated bag so that we can track env vars per-extension
                $resolvingBag = new MergeExtensionConfigurationParameterBag($resolvingBag);
                if ($configAvailable) {
                    BaseNode::setPlaceholderUniquePrefix($resolvingBag->getEnvPlaceholderUniquePrefix());
                }
            }
            $config = $resolvingBag->resolveValue($config);

            try {
                $tmpContainer = new MergeExtensionConfigurationContainerBuilder($extension, $resolvingBag);
                $tmpContainer->setResourceTracking($container->isTrackingResources());
                $tmpContainer->addObjectResource($extension);
                if ($extension instanceof ConfigurationExtensionInterface && null !== $configuration = $extension->getConfiguration($config, $tmpContainer)) {
                    $tmpContainer->addObjectResource($configuration);
                }

                foreach ($exprLangProviders as $provider) {
                    $tmpContainer->addExpressionLanguageProvider($provider);
                }

                $extension->load($config, $tmpContainer);
            } catch (\Exception $e) {
                if ($resolvingBag instanceof MergeExtensionConfigurationParameterBag) {
                    $container->getParameterBag()->mergeEnvPlaceholders($resolvingBag);
                }

                throw $e;
            }

            if ($resolvingBag instanceof MergeExtensionConfigurationParameterBag) {
                // don't keep track of env vars that are *overridden* when configs are merged
                $resolvingBag->freezeAfterProcessing($extension, $tmpContainer);
            }

            $container->merge($tmpContainer);
            $container->getParameterBag()->add($parameters);
        }

        $container->addDefinitions($definitions);
        $container->addAliases($aliases);
    }
}

/**
 * @internal
 */
class MergeExtensionConfigurationParameterBag extends EnvPlaceholderParameterBag
{
    private array $processedEnvPlaceholders;

    public function __construct(parent $parameterBag)
    {
        parent::__construct($parameterBag->all());
        $this->mergeEnvPlaceholders($parameterBag);
    }

    public function freezeAfterProcessing(Extension $extension, ContainerBuilder $container): void
    {
        if (!$config = $extension->getProcessedConfigs()) {
            // Extension::processConfiguration() wasn't called, we cannot know how configs were merged
            return;
        }
        $this->processedEnvPlaceholders = [];

        // serialize config and container to catch env vars nested in object graphs
        $config = serialize($config).serialize($container->getDefinitions()).serialize($container->getAliases()).serialize($container->getParameterBag()->all());

        if (false === stripos($config, 'env_')) {
            return;
        }

        preg_match_all('/env_[a-f0-9]{16}_\w+_[a-f0-9]{32}/Ui', $config, $matches);
        $usedPlaceholders = array_flip($matches[0]);
        foreach (parent::getEnvPlaceholders() as $env => $placeholders) {
            foreach ($placeholders as $placeholder) {
                if (isset($usedPlaceholders[$placeholder])) {
                    $this->processedEnvPlaceholders[$env] = $placeholders;
                    break;
                }
            }
        }
    }

    public function getEnvPlaceholders(): array
    {
        return $this->processedEnvPlaceholders ?? parent::getEnvPlaceholders();
    }

    public function getUnusedEnvPlaceholders(): array
    {
        return !isset($this->processedEnvPlaceholders) ? [] : array_diff_key(parent::getEnvPlaceholders(), $this->processedEnvPlaceholders);
    }
}

/**
 * A container builder preventing using methods that wouldn't have any effect from extensions.
 *
 * @internal
 */
class MergeExtensionConfigurationContainerBuilder extends ContainerBuilder
{
    private string $extensionClass;

    public function __construct(ExtensionInterface $extension, ?ParameterBagInterface $parameterBag = null)
    {
        parent::__construct($parameterBag);

        $this->extensionClass = $extension::class;
    }

    public function addCompilerPass(CompilerPassInterface $pass, string $type = PassConfig::TYPE_BEFORE_OPTIMIZATION, int $priority = 0): static
    {
        throw new LogicException(\sprintf('You cannot add compiler pass "%s" from extension "%s". Compiler passes must be registered before the container is compiled.', get_debug_type($pass), $this->extensionClass));
    }

    public function registerExtension(ExtensionInterface $extension)
    {
        throw new LogicException(\sprintf('You cannot register extension "%s" from "%s". Extensions must be registered before the container is compiled.', get_debug_type($extension), $this->extensionClass));
    }

    public function compile(bool $resolveEnvPlaceholders = false)
    {
        throw new LogicException(\sprintf('Cannot compile the container in extension "%s".', $this->extensionClass));
    }

    public function resolveEnvPlaceholders(mixed $value, string|bool|null $format = null, ?array &$usedEnvs = null): mixed
    {
        if (true !== $format || !\is_string($value)) {
            return parent::resolveEnvPlaceholders($value, $format, $usedEnvs);
        }

        $bag = $this->getParameterBag();
        $value = $bag->resolveValue($value);

        if (!$bag instanceof EnvPlaceholderParameterBag) {
            return parent::resolveEnvPlaceholders($value, $format, $usedEnvs);
        }

        foreach ($bag->getEnvPlaceholders() as $env => $placeholders) {
            if (!str_contains($env, ':')) {
                continue;
            }
            foreach ($placeholders as $placeholder) {
                if (false !== stripos($value, $placeholder)) {
                    throw new RuntimeException(\sprintf('Using a cast in "env(%s)" is incompatible with resolution at compile time in "%s". The logic in the extension should be moved to a compiler pass, or an env parameter with no cast should be used instead.', $env, $this->extensionClass));
                }
            }
        }

        return parent::resolveEnvPlaceholders($value, $format, $usedEnvs);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Argument\ArgumentInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException;
use Symfony\Component\DependencyInjection\Reference;

/**
 * Inline service definitions where this is possible.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class InlineServiceDefinitionsPass extends AbstractRecursivePass
{
    protected bool $skipScalars = true;

    private ?AnalyzeServiceReferencesPass $analyzingPass;
    private array $cloningIds = [];
    private array $connectedIds = [];
    private array $notInlinedIds = [];
    private array $inlinedIds = [];
    private array $notInlinableIds = [];
    private ?ServiceReferenceGraph $graph = null;

    public function __construct(?AnalyzeServiceReferencesPass $analyzingPass = null)
    {
        $this->analyzingPass = $analyzingPass;
    }

    /**
     * @return void
     */
    public function process(ContainerBuilder $container)
    {
        $this->container = $container;
        if ($this->analyzingPass) {
            $analyzedContainer = new ContainerBuilder();
            $analyzedContainer->setAliases($container->getAliases());
            $analyzedContainer->setDefinitions($container->getDefinitions());
            foreach ($container->getExpressionLanguageProviders() as $provider) {
                $analyzedContainer->addExpressionLanguageProvider($provider);
            }
        } else {
            $analyzedContainer = $container;
        }
        try {
            $notInlinableIds = [];
            $remainingInlinedIds = [];
            $this->connectedIds = $this->notInlinedIds = $container->getDefinitions();
            do {
                if ($this->analyzingPass) {
                    $analyzedContainer->setDefinitions(array_intersect_key($analyzedContainer->getDefinitions(), $this->connectedIds));
                    $this->analyzingPass->process($analyzedContainer);
                }
                $this->graph = $analyzedContainer->getCompiler()->getServiceReferenceGraph();
                $notInlinedIds = $this->notInlinedIds;
                $notInlinableIds += $this->notInlinableIds;
                $this->connectedIds = $this->notInlinedIds = $this->inlinedIds = $this->notInlinableIds = [];

                foreach ($analyzedContainer->getDefinitions() as $id => $definition) {
                    if (!$this->graph->hasNode($id)) {
                        continue;
                    }
                    if ($definition->isPublic()) {
                        $this->connectedIds[$id] = true;
                    }
                    foreach ($this->graph->getNode($id)->getOutEdges() as $edge) {
                        if (isset($notInlinedIds[$edge->getSourceNode()->getId()])) {
                            $this->currentId = $id;
                            $this->processValue($definition, true);
                            break;
                        }
                    }
                }

                foreach ($this->inlinedIds as $id => $isPublicOrNotShared) {
                    if ($isPublicOrNotShared) {
                        $remainingInlinedIds[$id] = $id;
                    } else {
                        $container->removeDefinition($id);
                        $analyzedContainer->removeDefinition($id);
                    }
                }
            } while ($this->inlinedIds && $this->analyzingPass);

            foreach ($remainingInlinedIds as $id) {
                if (isset($notInlinableIds[$id])) {
                    continue;
                }

                $definition = $container->getDefinition($id);

                if (!$definition->isShared() && !$definition->isPublic()) {
                    $container->removeDefinition($id);
                }
            }
        } finally {
            $this->container = null;
            $this->connectedIds = $this->notInlinedIds = $this->inlinedIds = [];
            $this->notInlinableIds = [];
            $this->graph = null;
        }
    }

    protected function processValue(mixed $value, bool $isRoot = false): mixed
    {
        if ($value instanceof ArgumentInterface) {
            // References found in ArgumentInterface::getValues() are not inlineable
            return $value;
        }

        if ($value instanceof Definition && $this->cloningIds) {
            if ($value->isShared()) {
                return $value;
            }
            $value = clone $value;
        }

        if (!$value instanceof Reference) {
            return parent::processValue($value, $isRoot);
        } elseif (!$this->container->hasDefinition($id = (string) $value)) {
            return $value;
        }

        $definition = $this->container->getDefinition($id);

        if (isset($this->notInlinableIds[$id]) || !$this->isInlineableDefinition($id, $definition)) {
            if ($this->currentId !== $id) {
                $this->notInlinableIds[$id] = true;
            }

            return $value;
        }

        $this->container->log($this, \sprintf('Inlined service "%s" to "%s".', $id, $this->currentId));
        $this->inlinedIds[$id] = $definition->isPublic() || !$definition->isShared();
        $this->notInlinedIds[$this->currentId ?? ''] = true;

        if ($definition->isShared()) {
            return $definition;
        }

        if (isset($this->cloningIds[$id])) {
            $ids = array_keys($this->cloningIds);
            $ids[] = $id;

            throw new ServiceCircularReferenceException($id, \array_slice($ids, array_search($id, $ids)));
        }

        $this->cloningIds[$id] = true;
        try {
            return $this->processValue($definition);
        } finally {
            unset($this->cloningIds[$id]);
        }
    }

    /**
     * Checks if the definition is inlineable.
     */
    private function isInlineableDefinition(string $id, Definition $definition): bool
    {
        if ($definition->hasErrors() || $definition->isDeprecated() || $definition->isLazy() || $definition->isSynthetic() || $definition->hasTag('container.do_not_inline')) {
            return false;
        }

        if (!$definition->isShared()) {
            if (!$this->graph->hasNode($id)) {
                return true;
            }

            foreach ($this->graph->getNode($id)->getInEdges() as $edge) {
                $srcId = $edge->getSourceNode()->getId();
                $this->connectedIds[$srcId] = true;
                if ($edge->isWeak() || $edge->isLazy()) {
                    return !$this->connectedIds[$id] = true;
                }
            }

            return true;
        }

        if ($definition->isPublic()
            || $this->currentId === $id
            || !$this->graph->hasNode($id)
        ) {
            return false;
        }

        $this->connectedIds[$id] = true;

        $srcIds = [];
        $srcCount = 0;
        foreach ($this->graph->getNode($id)->getInEdges() as $edge) {
            $srcId = $edge->getSourceNode()->getId();
            $this->connectedIds[$srcId] = true;
            if ($edge->isWeak() || $edge->isLazy()) {
                return false;
            }
            $srcIds[$srcId] = true;
            ++$srcCount;
        }

        if (1 !== \count($srcIds)) {
            $this->notInlinedIds[$id] = true;

            return false;
        }

        if ($srcCount > 1 && \is_array($factory = $definition->getFactory()) && ($factory[0] instanceof Reference || $factory[0] instanceof Definition)) {
            return false;
        }

        $srcDefinition = $this->container->getDefinition($srcId);

        return $srcDefinition->isShared() && !$srcDefinition->isLazy();
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Definition;
use Symfony\Contracts\Service\Attribute\Required;

/**
 * Looks for definitions with autowiring enabled and registers their corresponding "#[Required]" methods as setters.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
class AutowireRequiredMethodsPass extends AbstractRecursivePass
{
    protected bool $skipScalars = true;

    protected function processValue(mixed $value, bool $isRoot = false): mixed
    {
        $value = parent::processValue($value, $isRoot);

        if (!$value instanceof Definition || !$value->isAutowired() || $value->isAbstract() || !$value->getClass()) {
            return $value;
        }
        if (!$reflectionClass = $this->container->getReflectionClass($value->getClass(), false)) {
            return $value;
        }

        $alreadyCalledMethods = [];
        $withers = [];

        foreach ($value->getMethodCalls() as [$method]) {
            $alreadyCalledMethods[strtolower($method)] = true;
        }

        foreach ($reflectionClass->getMethods() as $reflectionMethod) {
            $r = $reflectionMethod;

            if ($r->isConstructor() || isset($alreadyCalledMethods[strtolower($r->name)])) {
                continue;
            }

            while (true) {
                if ($r->getAttributes(Required::class)) {
                    if ($this->isWither($r, $r->getDocComment() ?: '')) {
                        $withers[] = [$r->name, [], true];
                    } else {
                        $value->addMethodCall($r->name, []);
                    }
                    break;
                }
                if (false !== $doc = $r->getDocComment()) {
                    if (false !== stripos($doc, '@required') && preg_match('#(?:^/\*\*|\n\s*+\*)\s*+@required(?:\s|\*/$)#i', $doc)) {
                        trigger_deprecation('symfony/dependency-injection', '6.3', 'Relying on the "@required" annotation on method "%s::%s()" is deprecated, use the "Symfony\Contracts\Service\Attribute\Required" attribute instead.', $reflectionMethod->class, $reflectionMethod->name);

                        if ($this->isWither($reflectionMethod, $doc)) {
                            $withers[] = [$reflectionMethod->name, [], true];
                        } else {
                            $value->addMethodCall($reflectionMethod->name, []);
                        }
                        break;
                    }
                    if (false === stripos($doc, '@inheritdoc') || !preg_match('#(?:^/\*\*|\n\s*+\*)\s*+(?:\{@inheritdoc\}|@inheritdoc)(?:\s|\*/$)#i', $doc)) {
                        break;
                    }
                }
                try {
                    $r = $r->getPrototype();
                } catch (\ReflectionException) {
                    break; // method has no prototype
                }
            }
        }

        if ($withers) {
            // Prepend withers to prevent creating circular loops
            $setters = $value->getMethodCalls();
            $value->setMethodCalls($withers);
            foreach ($setters as $call) {
                $value->addMethodCall($call[0], $call[1], $call[2] ?? false);
            }
        }

        return $value;
    }

    private function isWither(\ReflectionMethod $reflectionMethod, string $doc): bool
    {
        $match = preg_match('#(?:^/\*\*|\n\s*+\*)\s*+@return\s++(static|\$this)[\s\*]#i', $doc, $matches);
        if ($match && 'static' === $matches[1]) {
            return true;
        }

        if ($match && '$this' === $matches[1]) {
            return false;
        }

        $reflectionType = $reflectionMethod->hasReturnType() ? $reflectionMethod->getReturnType() : null;

        return $reflectionType instanceof \ReflectionNamedType && 'static' === $reflectionType->getName();
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
use Symfony\Component\DependencyInjection\Attribute\AsTaggedItem;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\TypedReference;

/**
 * Trait that allows a generic method to find and sort service by priority option in the tag.
 *
 * @author Iltar van der Berg <kjarli@gmail.com>
 */
trait PriorityTaggedServiceTrait
{
    /**
     * Finds all services with the given tag name and order them by their priority.
     *
     * The order of additions must be respected for services having the same priority,
     * and knowing that the \SplPriorityQueue class does not respect the FIFO method,
     * we should not use that class.
     *
     * @see https://bugs.php.net/53710
     * @see https://bugs.php.net/60926
     *
     * @return Reference[]
     */
    private function findAndSortTaggedServices(string|TaggedIteratorArgument $tagName, ContainerBuilder $container, array $exclude = []): array
    {
        $indexAttribute = $defaultIndexMethod = $needsIndexes = $defaultPriorityMethod = null;

        if ($tagName instanceof TaggedIteratorArgument) {
            $indexAttribute = $tagName->getIndexAttribute();
            $defaultIndexMethod = $tagName->getDefaultIndexMethod();
            $needsIndexes = $tagName->needsIndexes();
            $defaultPriorityMethod = $tagName->getDefaultPriorityMethod() ?? 'getDefaultPriority';
            $exclude = array_merge($exclude, $tagName->getExclude());
            $tagName = $tagName->getTag();
        }

        $i = 0;
        $services = [];

        foreach ($container->findTaggedServiceIds($tagName, true) as $serviceId => $attributes) {
            if (\in_array($serviceId, $exclude, true)) {
                continue;
            }

            $defaultPriority = null;
            $defaultIndex = null;
            $definition = $container->getDefinition($serviceId);
            $class = $definition->getClass();
            $class = $container->getParameterBag()->resolveValue($class) ?: null;
            $checkTaggedItem = !$definition->hasTag($definition->isAutoconfigured() ? 'container.ignore_attributes' : $tagName);

            // For decorated services, walk the decoration chain to find #[AsTaggedItem] on the original service
            $innerClass = null;
            $innerDef = $definition;
            while ($innerId = $innerDef->getTag('container.decorator')[0]['inner'] ?? null) {
                if (!$container->has($innerId)) {
                    break;
                }
                $innerDef = $container->findDefinition($innerId);
                $innerClass = $container->getParameterBag()->resolveValue($innerDef->getClass()) ?: null;
            }

            foreach ($attributes as $attribute) {
                $index = $priority = null;

                if (isset($attribute['priority'])) {
                    $priority = $attribute['priority'];
                } elseif (null === $defaultPriority && $defaultPriorityMethod && $class) {
                    $defaultPriority = PriorityTaggedServiceUtil::getDefault($container, $serviceId, $class, $defaultPriorityMethod, $tagName, 'priority', $checkTaggedItem);
                    if (null === $defaultPriority && $innerClass) {
                        $defaultPriority = PriorityTaggedServiceUtil::getDefault($container, $serviceId, $innerClass, $defaultPriorityMethod, $tagName, 'priority', true);
                    }
                }
                $priority ??= $defaultPriority ??= 0;

                if (null === $indexAttribute && !$defaultIndexMethod && !$needsIndexes) {
                    $services[] = [$priority, ++$i, null, $serviceId, null];
                    continue 2;
                }

                if (null !== $indexAttribute && isset($attribute[$indexAttribute])) {
                    $index = $attribute[$indexAttribute];
                } elseif (null === $defaultIndex && $defaultPriorityMethod && $class) {
                    $defaultIndex = PriorityTaggedServiceUtil::getDefault($container, $serviceId, $class, $defaultIndexMethod ?? 'getDefaultName', $tagName, $indexAttribute, $checkTaggedItem);
                    if (null === $defaultIndex && $innerClass) {
                        $defaultIndex = PriorityTaggedServiceUtil::getDefault($container, $serviceId, $innerClass, $defaultIndexMethod ?? 'getDefaultName', $tagName, $indexAttribute, true);
                    }
                }
                $decorated = $definition->getTag('container.decorator')[0]['id'] ?? null;
                $index ??= $defaultIndex ?? $defaultIndex = $decorated ?? $serviceId;

                $services[] = [$priority, ++$i, $index, $serviceId, $class];
            }
        }

        uasort($services, static fn ($a, $b) => $b[0] <=> $a[0] ?: $a[1] <=> $b[1]);

        $refs = [];
        foreach ($services as [, , $index, $serviceId, $class]) {
            if (!$class) {
                $reference = new Reference($serviceId);
            } elseif ($index === $serviceId) {
                $reference = new TypedReference($serviceId, $class);
            } else {
                $reference = new TypedReference($serviceId, $class, ContainerBuilder::EXCEPTION_ON_INVALID_REFERENCE, $index);
            }

            if (null === $index) {
                $refs[] = $reference;
            } else {
                $refs[$index] = $reference;
            }
        }

        return $refs;
    }
}

/**
 * @internal
 */
class PriorityTaggedServiceUtil
{
    public static function getDefault(ContainerBuilder $container, string $serviceId, string $class, string $defaultMethod, string $tagName, ?string $indexAttribute, bool $checkTaggedItem): string|int|null
    {
        if (!($r = $container->getReflectionClass($class)) || (!$checkTaggedItem && !$r->hasMethod($defaultMethod))) {
            return null;
        }

        if ($checkTaggedItem && !$r->hasMethod($defaultMethod)) {
            foreach ($r->getAttributes(AsTaggedItem::class) as $attribute) {
                return 'priority' === $indexAttribute ? $attribute->newInstance()->priority : $attribute->newInstance()->index;
            }

            return null;
        }

        if ($r->isInterface()) {
            return null;
        }

        if (null !== $indexAttribute) {
            $service = $class !== $serviceId ? \sprintf('service "%s"', $serviceId) : 'on the corresponding service';
            $message = [\sprintf('Either method "%s::%s()" should ', $class, $defaultMethod), \sprintf(' or tag "%s" on %s is missing attribute "%s".', $tagName, $service, $indexAttribute)];
        } else {
            $message = [\sprintf('Method "%s::%s()" should ', $class, $defaultMethod), '.'];
        }

        if (!($rm = $r->getMethod($defaultMethod))->isStatic()) {
            throw new InvalidArgumentException(implode('be static', $message));
        }

        if (!$rm->isPublic()) {
            throw new InvalidArgumentException(implode('be public', $message));
        }

        $default = $rm->invoke(null);

        if ('priority' === $indexAttribute) {
            if (!\is_int($default)) {
                throw new InvalidArgumentException(implode(\sprintf('return int (got "%s")', get_debug_type($default)), $message));
            }

            return $default;
        }

        if (\is_int($default)) {
            $default = (string) $default;
        }

        if (!\is_string($default)) {
            throw new InvalidArgumentException(implode(\sprintf('return string|int (got "%s")', get_debug_type($default)), $message));
        }

        return $default;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\InvalidParameterTypeException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\ExpressionLanguage;
use Symfony\Component\DependencyInjection\Parameter;
use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\ExpressionLanguage\Expression;

/**
 * Checks whether injected parameters are compatible with type declarations.
 *
 * This pass should be run after all optimization passes.
 *
 * It can be added either:
 *  * before removing passes to check all services even if they are not currently used,
 *  * after removing passes to check only services are used in the app.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 * @author Julien Maulny <jmaulny@darkmira.fr>
 */
final class CheckTypeDeclarationsPass extends AbstractRecursivePass
{
    protected bool $skipScalars = true;

    private const SCALAR_TYPES = [
        'int' => true,
        'float' => true,
        'bool' => true,
        'string' => true,
    ];

    private const BUILTIN_TYPES = [
        'array' => true,
        'bool' => true,
        'callable' => true,
        'float' => true,
        'int' => true,
        'iterable' => true,
        'object' => true,
        'string' => true,
    ];

    private bool $autoload;
    private array $skippedIds;

    private ExpressionLanguage $expressionLanguage;

    /**
     * @param bool  $autoload   Whether services who's class in not loaded should be checked or not.
     *                          Defaults to false to save loading code during compilation.
     * @param array $skippedIds An array indexed by the service ids to skip
     */
    public function __construct(bool $autoload = false, array $skippedIds = [])
    {
        $this->autoload = $autoload;
        $this->skippedIds = $skippedIds;
    }

    protected function processValue(mixed $value, bool $isRoot = false): mixed
    {
        if (isset($this->skippedIds[$this->currentId ?? ''])) {
            return $value;
        }

        if (!$value instanceof Definition || $value->hasErrors() || $value->isDeprecated()) {
            return parent::processValue($value, $isRoot);
        }

        if (!$this->autoload) {
            if (!$class = $value->getClass()) {
                return parent::processValue($value, $isRoot);
            }
            if (!class_exists($class, false) && !interface_exists($class, false)) {
                return parent::processValue($value, $isRoot);
            }
        }

        if (ServiceLocator::class === $value->getClass()) {
            return parent::processValue($value, $isRoot);
        }

        if ($constructor = $this->getConstructor($value, false)) {
            $this->checkTypeDeclarations($value, $constructor, $value->getArguments());
        }

        foreach ($value->getMethodCalls() as $methodCall) {
            try {
                $reflectionMethod = $this->getReflectionMethod($value, $methodCall[0]);
            } catch (RuntimeException $e) {
                if ($value->getFactory()) {
                    continue;
                }

                throw $e;
            }

            $this->checkTypeDeclarations($value, $reflectionMethod, $methodCall[1]);
        }

        return parent::processValue($value, $isRoot);
    }

    /**
     * @throws InvalidArgumentException When not enough parameters are defined for the method
     */
    private function checkTypeDeclarations(Definition $checkedDefinition, \ReflectionFunctionAbstract $reflectionFunction, array $values): void
    {
        $numberOfRequiredParameters = $reflectionFunction->getNumberOfRequiredParameters();

        if (\count($values) < $numberOfRequiredParameters) {
            throw new InvalidArgumentException(\sprintf('Invalid definition for service "%s": "%s::%s()" requires %d arguments, %d passed.', $this->currentId, $reflectionFunction->class, $reflectionFunction->name, $numberOfRequiredParameters, \count($values)));
        }

        $reflectionParameters = $reflectionFunction->getParameters();
        $checksCount = min($reflectionFunction->getNumberOfParameters(), \count($values));

        $envPlaceholderUniquePrefix = $this->container->getParameterBag() instanceof EnvPlaceholderParameterBag ? $this->container->getParameterBag()->getEnvPlaceholderUniquePrefix() : null;

        for ($i = 0; $i < $checksCount; ++$i) {
            $p = $reflectionParameters[$i];
            if (!$p->hasType() || $p->isVariadic()) {
                continue;
            }
            $key = $i;
            if (\array_key_exists($p->name, $values)) {
                $key = $p->name;
            } elseif (!\array_key_exists($i, $values)) {
                continue;
            }

            $this->checkType($checkedDefinition, $values[$key], $p, $envPlaceholderUniquePrefix);
        }

        if ($reflectionFunction->isVariadic() && ($lastParameter = end($reflectionParameters))->hasType()) {
            $variadicParameters = \array_slice($values, $lastParameter->getPosition());

            foreach ($variadicParameters as $variadicParameter) {
                $this->checkType($checkedDefinition, $variadicParameter, $lastParameter, $envPlaceholderUniquePrefix);
            }
        }
    }

    /**
     * @throws InvalidParameterTypeException When a parameter is not compatible with the declared type
     */
    private function checkType(Definition $checkedDefinition, mixed $value, \ReflectionParameter $parameter, ?string $envPlaceholderUniquePrefix, ?\ReflectionType $reflectionType = null): void
    {
        $reflectionType ??= $parameter->getType();

        if ($reflectionType instanceof \ReflectionUnionType) {
            foreach ($reflectionType->getTypes() as $t) {
                try {
                    $this->checkType($checkedDefinition, $value, $parameter, $envPlaceholderUniquePrefix, $t);

                    return;
                } catch (InvalidParameterTypeException $e) {
                }
            }

            throw new InvalidParameterTypeException($this->currentId, $e->getCode(), $parameter);
        }
        if ($reflectionType instanceof \ReflectionIntersectionType) {
            foreach ($reflectionType->getTypes() as $t) {
                $this->checkType($checkedDefinition, $value, $parameter, $envPlaceholderUniquePrefix, $t);
            }

            return;
        }
        if (!$reflectionType instanceof \ReflectionNamedType) {
            return;
        }

        $type = $reflectionType->getName();

        if ($value instanceof Reference) {
            if (!$this->container->has($value = (string) $value)) {
                return;
            }

            if ('service_container' === $value && is_a($type, Container::class, true)) {
                return;
            }

            $value = $this->container->findDefinition($value);
        }

        if ('self' === $type) {
            $type = $parameter->getDeclaringClass()->getName();
        }

        if ('static' === $type) {
            $type = $checkedDefinition->getClass();
        }

        $class = null;

        if ($value instanceof Definition) {
            if ($value->hasErrors() || $value->getFactory()) {
                return;
            }

            $class = $value->getClass();

            if ($class && isset(self::BUILTIN_TYPES[strtolower($class)])) {
                $class = strtolower($class);
            } elseif (!$class || (!$this->autoload && !class_exists($class, false) && !interface_exists($class, false))) {
                return;
            }
        } elseif ($value instanceof Parameter) {
            $value = $this->container->getParameter($value);
        } elseif ($value instanceof Expression) {
            try {
                $value = $this->getExpressionLanguage()->evaluate($value, ['container' => $this->container]);
            } catch (\Exception) {
                // If a service from the expression cannot be fetched from the container, we skip the validation.
                return;
            }
        } elseif (\is_string($value)) {
            if ('%' === ($value[0] ?? '') && preg_match('/^%([^%]+)%$/', $value, $match)) {
                $value = $this->container->getParameter(substr($value, 1, -1));
            }

            if ($envPlaceholderUniquePrefix && \is_string($value) && str_contains($value, 'env_')) {
                // If the value is an env placeholder that is either mixed with a string or with another env placeholder, then its resolved value will always be a string, so we don't need to resolve it.
                // We don't need to change the value because it is already a string.
                if ('' === preg_replace('/'.$envPlaceholderUniquePrefix.'_\w+_[a-f0-9]{32}/U', '', $value, -1, $c) && 1 === $c) {
                    try {
                        $value = $this->container->resolveEnvPlaceholders($value, true);
                    } catch (\Exception) {
                        // If an env placeholder cannot be resolved, we skip the validation.
                        return;
                    }
                }
            }
        }

        if (null === $value && $parameter->allowsNull()) {
            return;
        }

        if (null === $class) {
            if ($value instanceof IteratorArgument) {
                $class = RewindableGenerator::class;
            } elseif ($value instanceof ServiceClosureArgument) {
                $class = \Closure::class;
            } elseif ($value instanceof ServiceLocatorArgument) {
                $class = ServiceLocator::class;
            } elseif (\is_object($value)) {
                $class = $value::class;
            } else {
                $class = \gettype($value);
                $class = ['integer' => 'int', 'double' => 'float', 'boolean' => 'bool'][$class] ?? $class;
            }
        }

        if (isset(self::SCALAR_TYPES[$type]) && isset(self::SCALAR_TYPES[$class])) {
            return;
        }

        if ('string' === $type && method_exists($class, '__toString')) {
            return;
        }

        if ('callable' === $type && (\Closure::class === $class || method_exists($class, '__invoke'))) {
            return;
        }

        if ('callable' === $type && \is_array($value) && isset($value[0]) && ($value[0] instanceof Reference || $value[0] instanceof Definition || \is_string($value[0]))) {
            return;
        }

        if ('iterable' === $type && (\is_array($value) || 'array' === $class || is_subclass_of($class, \Traversable::class))) {
            return;
        }

        if ($type === $class) {
            return;
        }

        if ('object' === $type && !isset(self::BUILTIN_TYPES[$class])) {
            return;
        }

        if ('mixed' === $type) {
            return;
        }

        if (is_a($class, $type, true)) {
            return;
        }

        if ('false' === $type) {
            if (false === $value) {
                return;
            }
        } elseif ('true' === $type) {
            if (true === $value) {
                return;
            }
        } elseif ($reflectionType->isBuiltin()) {
            $checkFunction = \sprintf('is_%s', $type);
            if ($checkFunction($value)) {
                return;
            }
        }

        throw new InvalidParameterTypeException($this->currentId, \is_object($value) ? $class : get_debug_type($value), $parameter);
    }

    private function getExpressionLanguage(): ExpressionLanguage
    {
        return $this->expressionLanguage ??= new ExpressionLanguage(null, $this->container->getExpressionLanguageProviders());
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Argument\AbstractArgument;
use Symfony\Component\DependencyInjection\Argument\BoundArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\DependencyInjection\Attribute\Target;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\TypedReference;
use Symfony\Component\VarExporter\ProxyHelper;

/**
 * @author Guilhem Niot <guilhem.niot@gmail.com>
 */
class ResolveBindingsPass extends AbstractRecursivePass
{
    protected bool $skipScalars = true;

    private array $usedBindings = [];
    private array $unusedBindings = [];
    private array $errorMessages = [];

    /**
     * @return void
     */
    public function process(ContainerBuilder $container)
    {
        $this->usedBindings = $container->getRemovedBindingIds();

        try {
            parent::process($container);

            foreach ($this->unusedBindings as [$key, $serviceId, $bindingType, $file]) {
                $argumentType = $argumentName = $message = null;

                if (str_contains($key, ' ')) {
                    [$argumentType, $argumentName] = explode(' ', $key, 2);
                } elseif ('$' === $key[0]) {
                    $argumentName = $key;
                } else {
                    $argumentType = $key;
                }

                if ($argumentType) {
                    $message .= \sprintf('of type "%s" ', $argumentType);
                }

                if ($argumentName) {
                    $message .= \sprintf('named "%s" ', $argumentName);
                }

                if (BoundArgument::DEFAULTS_BINDING === $bindingType) {
                    $message .= 'under "_defaults"';
                } elseif (BoundArgument::INSTANCEOF_BINDING === $bindingType) {
                    $message .= 'under "_instanceof"';
                } else {
                    $message .= \sprintf('for service "%s"', $serviceId);
                }

                if ($file) {
                    $message .= \sprintf(' in file "%s"', $file);
                }

                $message = \sprintf('A binding is configured for an argument %s, but no corresponding argument has been found. It may be unused and should be removed, or it may have a typo.', $message);

                if ($this->errorMessages) {
                    $message .= \sprintf("\nCould be related to%s:", 1 < \count($this->errorMessages) ? ' one of' : '');
                }
                foreach ($this->errorMessages as $m) {
                    $message .= "\n - ".$m;
                }
                throw new InvalidArgumentException($message);
            }
        } finally {
            $this->usedBindings = [];
            $this->unusedBindings = [];
            $this->errorMessages = [];
        }
    }

    protected function processValue(mixed $value, bool $isRoot = false): mixed
    {
        if ($value instanceof TypedReference && $value->getType() === (string) $value) {
            // Already checked
            $bindings = $this->container->getDefinition($this->currentId)->getBindings();
            $name = $value->getName();

            if (isset($name, $bindings[$name = $value.' $'.$name])) {
                return $this->getBindingValue($bindings[$name]);
            }

            if (isset($bindings[$value->getType()])) {
                return $this->getBindingValue($bindings[$value->getType()]);
            }

            return parent::processValue($value, $isRoot);
        }

        if (!$value instanceof Definition || !$bindings = $value->getBindings()) {
            return parent::processValue($value, $isRoot);
        }

        $bindingNames = [];

        foreach ($bindings as $key => $binding) {
            [$bindingValue, $bindingId, $used, $bindingType, $file] = $binding->getValues();
            if ($used) {
                $this->usedBindings[$bindingId ?? ''] = true;
                unset($this->unusedBindings[$bindingId ?? '']);
            } elseif (!isset($this->usedBindings[$bindingId ?? ''])) {
                $this->unusedBindings[$bindingId ?? ''] = [$key, $this->currentId, $bindingType, $file];
            }

            if (preg_match('/^(?:(?:array|bool|float|int|string|iterable|([^ $]++)) )\$/', $key, $m)) {
                $bindingNames[substr($key, \strlen($m[0]))] = $binding;
            }

            if (!isset($m[1])) {
                continue;
            }

            if (is_subclass_of($m[1], \UnitEnum::class)) {
                $bindingNames[substr($key, \strlen($m[0]))] = $binding;
                continue;
            }

            if (null !== $bindingValue && !$bindingValue instanceof Reference && !$bindingValue instanceof Definition && !$bindingValue instanceof TaggedIteratorArgument && !$bindingValue instanceof ServiceLocatorArgument) {
                throw new InvalidArgumentException(\sprintf('Invalid value for binding key "%s" for service "%s": expected "%s", "%s", "%s", "%s" or null, "%s" given.', $key, $this->currentId, Reference::class, Definition::class, TaggedIteratorArgument::class, ServiceLocatorArgument::class, get_debug_type($bindingValue)));
            }
        }

        if ($value->isAbstract()) {
            return parent::processValue($value, $isRoot);
        }

        $calls = $value->getMethodCalls();

        try {
            if ($constructor = $this->getConstructor($value, false)) {
                $calls[] = [$constructor, $value->getArguments()];
            }
        } catch (RuntimeException $e) {
            $this->errorMessages[] = $e->getMessage();
            $this->container->getDefinition($this->currentId)->addError($e->getMessage());

            return parent::processValue($value, $isRoot);
        }

        foreach ($calls as $i => $call) {
            [$method, $arguments] = $call;

            if ($method instanceof \ReflectionFunctionAbstract) {
                $reflectionMethod = $method;
            } else {
                try {
                    $reflectionMethod = $this->getReflectionMethod($value, $method);
                } catch (RuntimeException $e) {
                    if ($value->getFactory()) {
                        continue;
                    }
                    throw $e;
                }
            }

            $names = [];

            foreach ($reflectionMethod->getParameters() as $key => $parameter) {
                $names[$key] = $parameter->name;

                if (\array_key_exists($key, $arguments) && '' !== $arguments[$key] && !$arguments[$key] instanceof AbstractArgument) {
                    continue;
                }
                if (\array_key_exists($parameter->name, $arguments) && '' !== $arguments[$parameter->name] && !$arguments[$parameter->name] instanceof AbstractArgument) {
                    continue;
                }
                if (
                    $value->isAutowired()
                    && !$value->hasTag('container.ignore_attributes')
                    && $parameter->getAttributes(Autowire::class, \ReflectionAttribute::IS_INSTANCEOF)
                ) {
                    continue;
                }

                $typeHint = ltrim(ProxyHelper::exportType($parameter) ?? '', '?');

                $name = Target::parseName($parameter, parsedName: $parsedName);

                if ($typeHint && (
                    \array_key_exists($k = preg_replace('/(^|[(|&])\\\\/', '\1', $typeHint).' $'.$name, $bindings)
                    || \array_key_exists($k = preg_replace('/(^|[(|&])\\\\/', '\1', $typeHint).' $'.$parsedName, $bindings)
                )) {
                    $arguments[$key] = $this->getBindingValue($bindings[$k]);

                    continue;
                }

                if (\array_key_exists($k = '$'.$name, $bindings) || \array_key_exists($k = '$'.$parsedName, $bindings)) {
                    $arguments[$key] = $this->getBindingValue($bindings[$k]);

                    continue;
                }

                if ($typeHint && '\\' === $typeHint[0] && isset($bindings[$typeHint = substr($typeHint, 1)])) {
                    $arguments[$key] = $this->getBindingValue($bindings[$typeHint]);

                    continue;
                }

                if (null !== $binding = $bindingNames[$name] ?? $bindingNames[$parsedName] ?? $bindingNames[$parameter->name] ?? null) {
                    $bindingKey = array_search($binding, $bindings, true);
                    $argumentType = substr($bindingKey, 0, strpos($bindingKey, ' '));
                    $this->errorMessages[] = \sprintf('Did you forget to add the type "%s" to argument "$%s" of method "%s::%s()"?', $argumentType, $parameter->name, $reflectionMethod->class, $reflectionMethod->name);
                }
            }

            foreach ($names as $key => $name) {
                if (\array_key_exists($name, $arguments) && (0 === $key || \array_key_exists($key - 1, $arguments))) {
                    if (!\array_key_exists($key, $arguments)) {
                        $arguments[$key] = $arguments[$name];
                    }
                    unset($arguments[$name]);
                }
            }

            if ($arguments !== $call[1]) {
                ksort($arguments, \SORT_NATURAL);
                $calls[$i][1] = $arguments;
            }
        }

        if ($constructor) {
            [, $arguments] = array_pop($calls);

            if ($arguments !== $value->getArguments()) {
                $value->setArguments($arguments);
            }
        }

        if ($calls !== $value->getMethodCalls()) {
            $value->setMethodCalls($calls);
        }

        return parent::processValue($value, $isRoot);
    }

    private function getBindingValue(BoundArgument $binding): mixed
    {
        [$bindingValue, $bindingId] = $binding->getValues();

        $this->usedBindings[$bindingId ?? ''] = true;
        unset($this->unusedBindings[$bindingId ?? '']);

        return $bindingValue;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;

/**
 * Applies instanceof conditionals to definitions.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
class ResolveInstanceofConditionalsPass implements CompilerPassInterface
{
    /**
     * @return void
     */
    public function process(ContainerBuilder $container)
    {
        foreach ($container->getAutoconfiguredInstanceof() as $interface => $definition) {
            if ($definition->getArguments()) {
                throw new InvalidArgumentException(\sprintf('Autoconfigured instanceof for type "%s" defines arguments but these are not supported and should be removed.', $interface));
            }
        }

        $tagsToKeep = [];

        if ($container->hasParameter('container.behavior_describing_tags')) {
            $tagsToKeep = $container->getParameter('container.behavior_describing_tags');
        }

        foreach ($container->getDefinitions() as $id => $definition) {
            $container->setDefinition($id, $this->processDefinition($container, $id, $definition, $tagsToKeep));
        }

        if ($container->hasParameter('container.behavior_describing_tags')) {
            $container->getParameterBag()->remove('container.behavior_describing_tags');
        }
    }

    private function processDefinition(ContainerBuilder $container, string $id, Definition $definition, array $tagsToKeep): Definition
    {
        $instanceofConditionals = $definition->getInstanceofConditionals();
        $autoconfiguredInstanceof = $definition->isAutoconfigured() ? $container->getAutoconfiguredInstanceof() : [];
        if (!$instanceofConditionals && !$autoconfiguredInstanceof) {
            return $definition;
        }

        if (!$class = $container->getParameterBag()->resolveValue($definition->getClass())) {
            return $definition;
        }

        $conditionals = $this->mergeConditionals($autoconfiguredInstanceof, $instanceofConditionals, $container);

        $definition->setInstanceofConditionals([]);
        $shared = null;
        $instanceofTags = [];
        $instanceofCalls = [];
        $instanceofBindings = [];
        $reflectionClass = null;
        $parent = $definition instanceof ChildDefinition ? $definition->getParent() : null;

        foreach ($conditionals as $interface => $instanceofDefs) {
            if ($interface !== $class && !($reflectionClass ??= $container->getReflectionClass($class, false) ?: false)) {
                continue;
            }

            if ($interface !== $class && !is_subclass_of($class, $interface)) {
                continue;
            }

            foreach ($instanceofDefs as $key => $instanceofDef) {
                /** @var ChildDefinition $instanceofDef */
                $instanceofDef = clone $instanceofDef;
                $instanceofDef->setAbstract(true)->setParent($parent ?: '.abstract.instanceof.'.$id);
                $parent = '.instanceof.'.strtr($interface, "\0\r\n", '---').'.'.$key.'.'.$id;
                $container->setDefinition($parent, $instanceofDef);
                $instanceofTags[] = [$interface, $instanceofDef->getTags()];
                $instanceofBindings = $instanceofDef->getBindings() + $instanceofBindings;

                foreach ($instanceofDef->getMethodCalls() as $methodCall) {
                    $instanceofCalls[] = $methodCall;
                }

                $instanceofDef->setTags([]);
                $instanceofDef->setMethodCalls([]);
                $instanceofDef->setBindings([]);

                if (isset($instanceofDef->getChanges()['shared'])) {
                    $shared = $instanceofDef->isShared();
                }
            }
        }

        if ($parent) {
            $bindings = $definition->getBindings();
            $abstract = $container->setDefinition('.abstract.instanceof.'.$id, $definition);
            $definition->setBindings([]);
            $definition = serialize($definition);

            if (Definition::class === $abstract::class) {
                // cast Definition to ChildDefinition
                $definition = substr_replace($definition, '53', 2, 2);
                $definition = substr_replace($definition, 'Child', 44, 0);
            }
            /** @var ChildDefinition $definition */
            $definition = unserialize($definition, ['allowed_classes' => true]);
            $definition->setParent($parent);

            if (null !== $shared && !isset($definition->getChanges()['shared'])) {
                $definition->setShared($shared);
            }

            // Don't add tags to service decorators
            $i = \count($instanceofTags);
            while (0 <= --$i) {
                [$interface, $tags] = $instanceofTags[$i];
                foreach ($tags as $k => $v) {
                    if (null === $definition->getDecoratedService() || $interface === $definition->getClass() || \in_array($k, $tagsToKeep, true)) {
                        foreach ($v as $v) {
                            if ($definition->hasTag($k) && \in_array($v, $definition->getTag($k))) {
                                continue;
                            }
                            $definition->addTag($k, $v);
                        }
                    }
                }
            }

            $definition->setMethodCalls(array_merge($instanceofCalls, $definition->getMethodCalls()));
            $definition->setBindings($bindings + $instanceofBindings);

            // reset fields with "merge" behavior
            $abstract
                ->setBindings([])
                ->setArguments([])
                ->setMethodCalls([])
                ->setDecoratedService(null)
                ->setTags([])
                ->setAbstract(true);
        }

        return $definition;
    }

    private function mergeConditionals(array $autoconfiguredInstanceof, array $instanceofConditionals, ContainerBuilder $container): array
    {
        // make each value an array of ChildDefinition
        $conditionals = array_map(static fn ($childDef) => [$childDef], $autoconfiguredInstanceof);

        foreach ($instanceofConditionals as $interface => $instanceofDef) {
            // make sure the interface/class exists (but don't validate automaticInstanceofConditionals)
            if (!$container->getReflectionClass($interface)) {
                throw new RuntimeException(\sprintf('"%s" is set as an "instanceof" conditional, but it does not exist.', $interface));
            }

            if (!isset($autoconfiguredInstanceof[$interface])) {
                $conditionals[$interface] = [];
            }

            $conditionals[$interface][] = $instanceofDef;
        }

        return $conditionals;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
use Symfony\Component\DependencyInjection\Reference;

/**
 * Replaces aliases with actual service definitions, effectively removing these
 * aliases.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class ReplaceAliasByActualDefinitionPass extends AbstractRecursivePass
{
    protected bool $skipScalars = true;

    private array $replacements;

    /**
     * Process the Container to replace aliases with service definitions.
     *
     * @return void
     *
     * @throws InvalidArgumentException if the service definition does not exist
     */
    public function process(ContainerBuilder $container)
    {
        // First collect all alias targets that need to be replaced
        $seenAliasTargets = [];
        $replacements = [];

        // Sort aliases so non-deprecated ones come first. This ensures that when
        // multiple aliases point to the same private definition, non-deprecated
        // aliases get priority for renaming. Otherwise, the definition might be
        // renamed to a deprecated alias ID, causing the original service ID to
        // become an alias to the deprecated one (inverting the alias chain).
        $aliases = $container->getAliases();
        uasort($aliases, static fn ($a, $b) => $a->isDeprecated() <=> $b->isDeprecated());

        foreach ($aliases as $definitionId => $target) {
            $targetId = (string) $target;
            // Special case: leave this target alone
            if ('service_container' === $targetId) {
                continue;
            }
            // Check if target needs to be replaced
            if (isset($replacements[$targetId])) {
                $container->setAlias($definitionId, $replacements[$targetId])->setPublic($target->isPublic());

                if ($target->isDeprecated()) {
                    $container->getAlias($definitionId)->setDeprecated(...array_values($target->getDeprecation('%alias_id%')));
                }
            }
            // No need to process the same target twice
            if (isset($seenAliasTargets[$targetId])) {
                continue;
            }
            // Process new target
            $seenAliasTargets[$targetId] = true;
            try {
                $definition = $container->getDefinition($targetId);
            } catch (ServiceNotFoundException $e) {
                if ('' !== $e->getId() && '@' === $e->getId()[0]) {
                    throw new ServiceNotFoundException($e->getId(), $e->getSourceId(), null, [substr($e->getId(), 1)]);
                }

                throw $e;
            }
            if ($definition->isPublic()) {
                continue;
            }
            // Remove private definition and schedule for replacement
            $definition->setPublic($target->isPublic());
            $container->setDefinition($definitionId, $definition);
            $container->removeDefinition($targetId);
            $replacements[$targetId] = $definitionId;

            if ($target->isPublic() && $target->isDeprecated()) {
                $definition->addTag('container.private', $target->getDeprecation('%service_id%'));
            }
        }
        $this->replacements = $replacements;

        parent::process($container);
        $this->replacements = [];
    }

    protected function processValue(mixed $value, bool $isRoot = false): mixed
    {
        if ($value instanceof Reference && isset($this->replacements[$referenceId = (string) $value])) {
            // Perform the replacement
            $newId = $this->replacements[$referenceId];
            $value = new Reference($newId, $value->getInvalidBehavior());
            $this->container->log($this, \sprintf('Changed reference of service "%s" previously pointing to "%s" to "%s".', $this->currentId, $referenceId, $newId));
        }

        return parent::processValue($value, $isRoot);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\Reference;

/**
 * Checks the validity of references.
 *
 * The following checks are performed by this pass:
 * - target definitions are not abstract
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class CheckReferenceValidityPass extends AbstractRecursivePass
{
    protected bool $skipScalars = true;

    protected function processValue(mixed $value, bool $isRoot = false): mixed
    {
        if ($isRoot && $value instanceof Definition && ($value->isSynthetic() || $value->isAbstract())) {
            return $value;
        }
        if ($value instanceof Reference && $this->container->hasDefinition((string) $value)) {
            $targetDefinition = $this->container->getDefinition((string) $value);

            if ($targetDefinition->isAbstract()) {
                throw new RuntimeException(\sprintf('The definition "%s" has a reference to an abstract definition "%s". Abstract definitions cannot be the target of references.', $this->currentId, $value));
            }
        }

        return parent::processValue($value, $isRoot);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\EnvParameterException;

/**
 * This class is used to remove circular dependencies between individual passes.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class Compiler
{
    private PassConfig $passConfig;
    private array $log = [];
    private ServiceReferenceGraph $serviceReferenceGraph;

    public function __construct()
    {
        $this->passConfig = new PassConfig();
        $this->serviceReferenceGraph = new ServiceReferenceGraph();
    }

    public function getPassConfig(): PassConfig
    {
        return $this->passConfig;
    }

    public function getServiceReferenceGraph(): ServiceReferenceGraph
    {
        return $this->serviceReferenceGraph;
    }

    /**
     * @return void
     */
    public function addPass(CompilerPassInterface $pass, string $type = PassConfig::TYPE_BEFORE_OPTIMIZATION, int $priority = 0)
    {
        $this->passConfig->addPass($pass, $type, $priority);
    }

    /**
     * @final
     *
     * @return void
     */
    public function log(CompilerPassInterface $pass, string $message)
    {
        if (str_contains($message, "\n")) {
            $message = str_replace("\n", "\n".$pass::class.': ', trim($message));
        }

        $this->log[] = $pass::class.': '.$message;
    }

    public function getLog(): array
    {
        return $this->log;
    }

    /**
     * Run the Compiler and process all Passes.
     *
     * @return void
     */
    public function compile(ContainerBuilder $container)
    {
        try {
            foreach ($this->passConfig->getPasses() as $pass) {
                $pass->process($container);
            }
        } catch (\Exception $e) {
            $usedEnvs = [];
            $prev = $e;

            do {
                $msg = $prev->getMessage();

                if ($msg !== $resolvedMsg = $container->resolveEnvPlaceholders($msg, null, $usedEnvs)) {
                    $r = new \ReflectionProperty($prev, 'message');
                    $r->setValue($prev, $resolvedMsg);
                }
            } while ($prev = $prev->getPrevious());

            if ($usedEnvs) {
                $e = new EnvParameterException($usedEnvs, $e);
            }

            throw $e;
        } finally {
            $this->getServiceReferenceGraph()->clear();
        }
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\ContainerBuilder;

/**
 * Remove private aliases from the container. They were only used to establish
 * dependencies between services, and these dependencies have been resolved in
 * one of the previous passes.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class RemovePrivateAliasesPass implements CompilerPassInterface
{
    /**
     * Removes private aliases from the ContainerBuilder.
     *
     * @return void
     */
    public function process(ContainerBuilder $container)
    {
        foreach ($container->getAliases() as $id => $alias) {
            if ($alias->isPublic()) {
                continue;
            }

            $container->removeAlias($id);
            $container->log($this, \sprintf('Removed service "%s"; reason: private alias.', $id));
        }
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Psr\Container\ContainerInterface as PsrContainerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\Argument\BoundArgument;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\TypedReference;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Contracts\Service\Attribute\SubscribedService;
use Symfony\Contracts\Service\ServiceProviderInterface;
use Symfony\Contracts\Service\ServiceSubscriberInterface;

/**
 * Compiler pass to register tagged services that require a service locator.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
class RegisterServiceSubscribersPass extends AbstractRecursivePass
{
    protected bool $skipScalars = true;

    protected function processValue(mixed $value, bool $isRoot = false): mixed
    {
        if (!$value instanceof Definition || $value->isAbstract() || $value->isSynthetic() || !$value->hasTag('container.service_subscriber')) {
            return parent::processValue($value, $isRoot);
        }

        $serviceMap = [];
        $autowire = $value->isAutowired();

        foreach ($value->getTag('container.service_subscriber') as $attributes) {
            if (!$attributes) {
                $autowire = true;
                continue;
            }
            ksort($attributes);
            if ([] !== array_diff(array_keys($attributes), ['id', 'key'])) {
                throw new InvalidArgumentException(\sprintf('The "container.service_subscriber" tag accepts only the "key" and "id" attributes, "%s" given for service "%s".', implode('", "', array_keys($attributes)), $this->currentId));
            }
            if (!\array_key_exists('id', $attributes)) {
                throw new InvalidArgumentException(\sprintf('Missing "id" attribute on "container.service_subscriber" tag with key="%s" for service "%s".', $attributes['key'], $this->currentId));
            }
            if (!\array_key_exists('key', $attributes)) {
                $attributes['key'] = $attributes['id'];
            }
            if (isset($serviceMap[$attributes['key']])) {
                continue;
            }
            $serviceMap[$attributes['key']] = new Reference($attributes['id']);
        }
        $class = $value->getClass();

        if (!$r = $this->container->getReflectionClass($class)) {
            throw new InvalidArgumentException(\sprintf('Class "%s" used for service "%s" cannot be found.', $class, $this->currentId));
        }
        if (!$r->isSubclassOf(ServiceSubscriberInterface::class)) {
            throw new InvalidArgumentException(\sprintf('Service "%s" must implement interface "%s".', $this->currentId, ServiceSubscriberInterface::class));
        }
        $class = $r->name;
        // to remove when symfony/dependency-injection will stop being compatible with symfony/framework-bundle<6.0
        $replaceDeprecatedSession = $this->container->has('.session.deprecated') && $r->isSubclassOf(AbstractController::class);
        $subscriberMap = [];

        foreach ($class::getSubscribedServices() as $key => $type) {
            $attributes = [];

            if (!isset($serviceMap[$key]) && $type instanceof Autowire) {
                $subscriberMap[$key] = $type;
                continue;
            }

            if ($type instanceof SubscribedService) {
                $key = $type->key ?? $key;
                $attributes = $type->attributes;
                $type = ($type->nullable ? '?' : '').($type->type ?? throw new InvalidArgumentException(\sprintf('When "%s::getSubscribedServices()" returns "%s", a type must be set.', $class, SubscribedService::class)));
            }

            if (!\is_string($type) || !preg_match('/(?(DEFINE)(?<cn>[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+))(?(DEFINE)(?<fqcn>(?&cn)(?:\\\\(?&cn))*+))^\??(?&fqcn)(?:(?:\|(?&fqcn))*+|(?:&(?&fqcn))*+)$/', $type)) {
                throw new InvalidArgumentException(\sprintf('"%s::getSubscribedServices()" must return valid PHP types for service "%s" key "%s", "%s" returned.', $class, $this->currentId, $key, \is_string($type) ? $type : get_debug_type($type)));
            }
            $optionalBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE;
            if ('?' === $type[0]) {
                $type = substr($type, 1);
                $optionalBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE;
            }
            if (\is_int($name = $key)) {
                $key = $type;
                $name = null;
            }
            if (!isset($serviceMap[$key])) {
                if (!$autowire) {
                    throw new InvalidArgumentException(\sprintf('Service "%s" misses a "container.service_subscriber" tag with "key"/"id" attributes corresponding to entry "%s" as returned by "%s::getSubscribedServices()".', $this->currentId, $key, $class));
                }
                if ($replaceDeprecatedSession && SessionInterface::class === $type) {
                    // This prevents triggering the deprecation when building the container
                    // to remove when symfony/dependency-injection will stop being compatible with symfony/framework-bundle<6.0
                    $type = '.session.deprecated';
                }
                $serviceMap[$key] = new Reference($type);
            }

            if ($name) {
                if (false !== $i = strpos($name, '::get')) {
                    $name = lcfirst(substr($name, 5 + $i));
                } elseif (str_contains($name, '::')) {
                    $name = null;
                }
            }

            if (null !== $name && !$this->container->has($name) && !$this->container->has($type.' $'.$name)) {
                $camelCaseName = lcfirst(str_replace(' ', '', ucwords(preg_replace('/[^a-zA-Z0-9\x7f-\xff]++/', ' ', $name))));
                $name = $this->container->has($type.' $'.$camelCaseName) ? $camelCaseName : $name;
            }

            $subscriberMap[$key] = new TypedReference((string) $serviceMap[$key], $type, $optionalBehavior, $name, $attributes);
            unset($serviceMap[$key]);
        }

        if ($serviceMap = array_keys($serviceMap)) {
            $message = \sprintf(1 < \count($serviceMap) ? 'keys "%s" do' : 'key "%s" does', str_replace('%', '%%', implode('", "', $serviceMap)));
            throw new InvalidArgumentException(\sprintf('Service %s not exist in the map returned by "%s::getSubscribedServices()" for service "%s".', $message, $class, $this->currentId));
        }

        $locatorRef = ServiceLocatorTagPass::register($this->container, $subscriberMap, $this->currentId);

        $value->addTag('container.service_subscriber.locator', ['id' => (string) $locatorRef]);

        $value->setBindings([
            PsrContainerInterface::class => new BoundArgument($locatorRef, false),
            ServiceProviderInterface::class => new BoundArgument($locatorRef, false),
        ] + $value->getBindings());

        return parent::processValue($value);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Reference;

final class AliasDeprecatedPublicServicesPass extends AbstractRecursivePass
{
    protected bool $skipScalars = true;

    private array $aliases = [];

    public function process(ContainerBuilder $container): void
    {
        foreach ($container->findTaggedServiceIds('container.private') as $id => $tags) {
            if (null === $package = $tags[0]['package'] ?? null) {
                throw new InvalidArgumentException(\sprintf('The "package" attribute is mandatory for the "container.private" tag on the "%s" service.', $id));
            }

            if (null === $version = $tags[0]['version'] ?? null) {
                throw new InvalidArgumentException(\sprintf('The "version" attribute is mandatory for the "container.private" tag on the "%s" service.', $id));
            }

            $definition = $container->getDefinition($id);
            if (!$definition->isPublic() || $definition->isPrivate()) {
                continue;
            }

            $container
                ->setAlias($id, $aliasId = '.container.private.'.$id)
                ->setPublic(true)
                ->setDeprecated($package, $version, 'Accessing the "%alias_id%" service directly from the container is deprecated, use dependency injection instead.');

            $container->setDefinition($aliasId, $definition);

            $this->aliases[$id] = $aliasId;
        }

        parent::process($container);
    }

    protected function processValue(mixed $value, bool $isRoot = false): mixed
    {
        if ($value instanceof Reference && isset($this->aliases[$id = (string) $value])) {
            return new Reference($this->aliases[$id], $value->getInvalidBehavior());
        }

        return parent::processValue($value, $isRoot);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Argument\ArgumentInterface;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\ExpressionLanguage\Expression;

/**
 * Run this pass before passes that need to know more about the relation of
 * your services.
 *
 * This class will populate the ServiceReferenceGraph with information. You can
 * retrieve the graph in other passes from the compiler.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 * @author Nicolas Grekas <p@tchwork.com>
 */
class AnalyzeServiceReferencesPass extends AbstractRecursivePass
{
    protected bool $skipScalars = true;

    private ServiceReferenceGraph $graph;
    private ?Definition $currentDefinition = null;
    private bool $onlyConstructorArguments;
    private bool $hasProxyDumper;
    private bool $lazy;
    private bool $byConstructor;
    private bool $byFactory;
    private bool $byMultiUseArgument;
    private array $definitions;
    private array $aliases;

    /**
     * @param bool $onlyConstructorArguments Sets this Service Reference pass to ignore method calls
     */
    public function __construct(bool $onlyConstructorArguments = false, bool $hasProxyDumper = true)
    {
        $this->onlyConstructorArguments = $onlyConstructorArguments;
        $this->hasProxyDumper = $hasProxyDumper;
        $this->enableExpressionProcessing();
    }

    /**
     * Processes a ContainerBuilder object to populate the service reference graph.
     *
     * @return void
     */
    public function process(ContainerBuilder $container)
    {
        $this->container = $container;
        $this->graph = $container->getCompiler()->getServiceReferenceGraph();
        $this->graph->clear();
        $this->lazy = false;
        $this->byConstructor = false;
        $this->byFactory = false;
        $this->byMultiUseArgument = false;
        $this->definitions = $container->getDefinitions();
        $this->aliases = $container->getAliases();

        foreach ($this->aliases as $id => $alias) {
            $targetId = $this->getDefinitionId((string) $alias);
            $this->graph->connect($id, $alias, $targetId, null !== $targetId ? $this->container->getDefinition($targetId) : null, null);
        }

        try {
            parent::process($container);
        } finally {
            $this->aliases = $this->definitions = [];
        }
    }

    protected function processValue(mixed $value, bool $isRoot = false): mixed
    {
        $lazy = $this->lazy;
        $inExpression = $this->inExpression();

        if ($value instanceof ArgumentInterface) {
            $this->lazy = !$this->byFactory || !$value instanceof IteratorArgument;
            $byMultiUseArgument = $this->byMultiUseArgument;
            if ($value instanceof IteratorArgument) {
                $this->byMultiUseArgument = true;
            }
            parent::processValue($value->getValues());
            $this->byMultiUseArgument = $byMultiUseArgument;
            $this->lazy = $lazy;

            return $value;
        }
        if ($value instanceof Reference) {
            $targetId = $this->getDefinitionId((string) $value);
            $targetDefinition = null !== $targetId ? $this->container->getDefinition($targetId) : null;

            $this->graph->connect(
                $this->currentId,
                $this->currentDefinition,
                $targetId,
                $targetDefinition,
                $value,
                $this->lazy || ($this->hasProxyDumper && $targetDefinition?->isLazy()),
                ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE === $value->getInvalidBehavior(),
                $this->byConstructor,
                $this->byMultiUseArgument
            );

            if ($inExpression) {
                $this->graph->connect(
                    '.internal.reference_in_expression',
                    null,
                    $targetId,
                    $targetDefinition,
                    $value,
                    $this->lazy || $targetDefinition?->isLazy(),
                    true,
                    $this->byConstructor,
                    $this->byMultiUseArgument
                );
            }

            return $value;
        }
        if (!$value instanceof Definition) {
            return parent::processValue($value, $isRoot);
        }
        if ($isRoot) {
            if ($value->isSynthetic() || $value->isAbstract()) {
                return $value;
            }
            $this->currentDefinition = $value;
        } elseif ($this->currentDefinition === $value) {
            return $value;
        }
        $this->lazy = false;

        $byConstructor = $this->byConstructor;
        $this->byConstructor = $isRoot || $byConstructor;

        $byFactory = $this->byFactory;
        $this->byFactory = true;
        if (\is_string($factory = $value->getFactory()) && str_starts_with($factory, '@=')) {
            if (!class_exists(Expression::class)) {
                throw new LogicException('Expressions cannot be used in service factories without the ExpressionLanguage component. Try running "composer require symfony/expression-language".');
            }

            $factory = new Expression(substr($factory, 2));
        }
        $this->processValue($factory);
        $this->byFactory = $byFactory;

        $this->processValue($value->getArguments());

        $properties = $value->getProperties();
        $setters = $value->getMethodCalls();

        // Any references before a "wither" are part of the constructor-instantiation graph
        $lastWitherIndex = null;
        foreach ($setters as $k => $call) {
            if ($call[2] ?? false) {
                $lastWitherIndex = $k;
            }
        }

        if (null !== $lastWitherIndex) {
            $this->processValue($properties);
            $setters = $properties = [];

            foreach ($value->getMethodCalls() as $k => $call) {
                if (null === $lastWitherIndex) {
                    $setters[] = $call;
                    continue;
                }

                if ($lastWitherIndex === $k) {
                    $lastWitherIndex = null;
                }

                $this->processValue($call);
            }
        }

        $this->byConstructor = $byConstructor;

        if (!$this->onlyConstructorArguments) {
            $this->processValue($properties);
            $this->processValue($setters);
            $this->processValue($value->getConfigurator());
        }
        $this->lazy = $lazy;

        return $value;
    }

    private function getDefinitionId(string $id): ?string
    {
        while (isset($this->aliases[$id])) {
            $id = (string) $this->aliases[$id];
        }

        return isset($this->definitions[$id]) ? $id : null;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;

/**
 * Propagate the "container.no_preload" tag.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
class ResolveNoPreloadPass extends AbstractRecursivePass
{
    private const DO_PRELOAD_TAG = '.container.do_preload';

    protected bool $skipScalars = true;

    private array $resolvedIds = [];

    /**
     * @return void
     */
    public function process(ContainerBuilder $container)
    {
        $this->container = $container;

        try {
            foreach ($container->getDefinitions() as $id => $definition) {
                if ($definition->isPublic() && !$definition->isPrivate() && !isset($this->resolvedIds[$id])) {
                    $this->resolvedIds[$id] = true;
                    $this->processValue($definition, true);
                }
            }

            foreach ($container->getAliases() as $alias) {
                if ($alias->isPublic() && !$alias->isPrivate() && !isset($this->resolvedIds[$id = (string) $alias]) && $container->hasDefinition($id)) {
                    $this->resolvedIds[$id] = true;
                    $this->processValue($container->getDefinition($id), true);
                }
            }
        } finally {
            $this->resolvedIds = [];
            $this->container = null;
        }

        foreach ($container->getDefinitions() as $definition) {
            if ($definition->hasTag(self::DO_PRELOAD_TAG)) {
                $definition->clearTag(self::DO_PRELOAD_TAG);
            } elseif (!$definition->isDeprecated() && !$definition->hasErrors()) {
                $definition->addTag('container.no_preload');
            }
        }
    }

    protected function processValue(mixed $value, bool $isRoot = false): mixed
    {
        if ($value instanceof Reference && ContainerBuilder::IGNORE_ON_UNINITIALIZED_REFERENCE !== $value->getInvalidBehavior() && $this->container->hasDefinition($id = (string) $value)) {
            $definition = $this->container->getDefinition($id);

            if (!isset($this->resolvedIds[$id]) && (!$definition->isPublic() || $definition->isPrivate())) {
                $this->resolvedIds[$id] = true;
                $this->processValue($definition, true);
            }

            return $value;
        }

        if (!$value instanceof Definition) {
            return parent::processValue($value, $isRoot);
        }

        if ($value->hasTag('container.no_preload') || $value->isDeprecated() || $value->hasErrors()) {
            return $value;
        }

        if ($isRoot) {
            $value->addTag(self::DO_PRELOAD_TAG);
        }

        return parent::processValue($value, $isRoot);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 */
class ResolveClassPass implements CompilerPassInterface
{
    /**
     * @return void
     */
    public function process(ContainerBuilder $container)
    {
        foreach ($container->getDefinitions() as $id => $definition) {
            if ($definition->isSynthetic()
                || $definition->hasErrors()
                || null !== $definition->getClass()
                || !preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\\\\[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+)++$/', $id)
            ) {
                continue;
            }
            if ($definition instanceof ChildDefinition && !class_exists($id)) {
                throw new InvalidArgumentException(\sprintf('Service definition "%s" has a parent but no class, and its name looks like an FQCN. Either the class is missing or you want to inherit it from the parent service. To resolve this ambiguity, please rename this service to a non-FQCN (e.g. using dots), or create the missing class.', $id));
            }
            $definition->setClass($id);
        }
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
use Symfony\Component\DependencyInjection\Reference;

/**
 * Overwrites a service but keeps the overridden one.
 *
 * @author Christophe Coevoet <stof@notk.org>
 * @author Fabien Potencier <fabien@symfony.com>
 * @author Diego Saint Esteben <diego@saintesteben.me>
 */
class DecoratorServicePass extends AbstractRecursivePass
{
    protected bool $skipScalars = true;

    /**
     * @return void
     */
    public function process(ContainerBuilder $container)
    {
        $definitions = new \SplPriorityQueue();
        $order = \PHP_INT_MAX;

        foreach ($container->getDefinitions() as $id => $definition) {
            if (!$decorated = $definition->getDecoratedService()) {
                continue;
            }
            $definitions->insert([$id, $definition], [$decorated[2], --$order]);
        }
        $decoratingDefinitions = [];
        $decoratedIds = [];

        $tagsToKeep = $container->hasParameter('container.behavior_describing_tags')
            ? $container->getParameter('container.behavior_describing_tags')
            : ['proxy', 'container.do_not_inline', 'container.service_locator', 'container.service_subscriber', 'container.service_subscriber.locator'];

        foreach ($definitions as [$id, $definition]) {
            $decoratedService = $definition->getDecoratedService();
            [$inner, $renamedId] = $decoratedService;
            $invalidBehavior = $decoratedService[3] ?? ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE;

            $definition->setDecoratedService(null);

            if (!$renamedId) {
                $renamedId = $id.'.inner';
            }

            $decoratedIds[$inner] ??= $renamedId;
            $this->currentId = $renamedId;
            $this->processValue($definition);

            $definition->innerServiceId = $renamedId;
            $definition->decorationOnInvalid = $invalidBehavior;

            // we create a new alias/service for the service we are replacing
            // to be able to reference it in the new one
            if ($container->hasAlias($inner)) {
                $alias = $container->getAlias($inner);
                $public = $alias->isPublic();
                $container->setAlias($renamedId, new Alias((string) $alias, false));
                $decoratedDefinition = $container->findDefinition($alias);
            } elseif ($container->hasDefinition($inner)) {
                $decoratedDefinition = $container->getDefinition($inner);
                $public = $decoratedDefinition->isPublic();
                $decoratedDefinition->setPublic(false);
                $container->setDefinition($renamedId, $decoratedDefinition);
                $decoratingDefinitions[$inner] = $decoratedDefinition;
            } elseif (ContainerInterface::IGNORE_ON_INVALID_REFERENCE === $invalidBehavior) {
                $container->removeDefinition($id);
                continue;
            } elseif (ContainerInterface::NULL_ON_INVALID_REFERENCE === $invalidBehavior) {
                $public = $definition->isPublic();
                $decoratedDefinition = null;
            } else {
                throw new ServiceNotFoundException($inner, $id);
            }

            if ($decoratedDefinition?->isSynthetic()) {
                throw new InvalidArgumentException(\sprintf('A synthetic service cannot be decorated: service "%s" cannot decorate "%s".', $id, $inner));
            }

            if (isset($decoratingDefinitions[$inner])) {
                $decoratingDefinition = $decoratingDefinitions[$inner];

                $decoratingTags = $decoratingDefinition->getTags();
                $resetTags = [];

                // Behavior-describing tags must not be transferred out to decorators
                foreach ($tagsToKeep as $containerTag) {
                    if (isset($decoratingTags[$containerTag])) {
                        $resetTags[$containerTag] = $decoratingTags[$containerTag];
                        unset($decoratingTags[$containerTag]);
                    }
                }

                $definition->setTags(array_merge($decoratingTags, $definition->getTags()));
                $decoratingDefinition->setTags($resetTags);
                $decoratingDefinitions[$inner] = $definition;
            }

            $container->setAlias($inner, $id)->setPublic($public);
        }

        foreach ($decoratingDefinitions as $inner => $definition) {
            $definition->addTag('container.decorator', ['id' => $inner, 'inner' => $decoratedIds[$inner]]);
        }
    }

    protected function processValue(mixed $value, bool $isRoot = false): mixed
    {
        if ($value instanceof Reference && '.inner' === (string) $value) {
            return new Reference($this->currentId, $value->getInvalidBehavior());
        }

        return parent::processValue($value, $isRoot);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException;

/**
 * Checks your services for circular references.
 *
 * References from method calls are ignored since we might be able to resolve
 * these references depending on the order in which services are called.
 *
 * Circular reference from method calls will only be detected at run-time.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class CheckCircularReferencesPass implements CompilerPassInterface
{
    private array $currentPath;
    private array $checkedNodes;
    private array $checkedLazyNodes;

    /**
     * Checks the ContainerBuilder object for circular references.
     *
     * @return void
     */
    public function process(ContainerBuilder $container)
    {
        $graph = $container->getCompiler()->getServiceReferenceGraph();

        $this->checkedNodes = [];
        foreach ($graph->getNodes() as $id => $node) {
            $this->currentPath = [$id];

            $this->checkOutEdges($node->getOutEdges());
        }
    }

    /**
     * Checks for circular references.
     *
     * @param ServiceReferenceGraphEdge[] $edges An array of Edges
     *
     * @throws ServiceCircularReferenceException when a circular reference is found
     */
    private function checkOutEdges(array $edges): void
    {
        foreach ($edges as $edge) {
            $node = $edge->getDestNode();
            $id = $node->getId();

            if (!empty($this->checkedNodes[$id])) {
                continue;
            }

            $isLeaf = (bool) $node->getValue();
            $isConcrete = !$edge->isLazy() && !$edge->isWeak();

            // Skip already checked lazy services if they are still lazy. Will not gain any new information.
            if (!empty($this->checkedLazyNodes[$id]) && (!$isLeaf || !$isConcrete)) {
                continue;
            }

            // Process concrete references, otherwise defer check circular references for lazy edges.
            if (!$isLeaf || $isConcrete) {
                $searchKey = array_search($id, $this->currentPath);
                $this->currentPath[] = $id;

                if (false !== $searchKey) {
                    throw new ServiceCircularReferenceException($id, \array_slice($this->currentPath, $searchKey));
                }

                $this->checkOutEdges($node->getOutEdges());

                $this->checkedNodes[$id] = true;
                unset($this->checkedLazyNodes[$id]);
            } else {
                $this->checkedLazyNodes[$id] = true;
            }

            array_pop($this->currentPath);
        }
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Reference;

/**
 * This is a directed graph of your services.
 *
 * This information can be used by your compiler passes instead of collecting
 * it themselves which improves performance quite a lot.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 *
 * @final
 */
class ServiceReferenceGraph
{
    /**
     * @var ServiceReferenceGraphNode[]
     */
    private array $nodes = [];

    public function hasNode(string $id): bool
    {
        return isset($this->nodes[$id]);
    }

    /**
     * Gets a node by identifier.
     *
     * @throws InvalidArgumentException if no node matches the supplied identifier
     */
    public function getNode(string $id): ServiceReferenceGraphNode
    {
        if (!isset($this->nodes[$id])) {
            throw new InvalidArgumentException(\sprintf('There is no node with id "%s".', $id));
        }

        return $this->nodes[$id];
    }

    /**
     * Returns all nodes.
     *
     * @return ServiceReferenceGraphNode[]
     */
    public function getNodes(): array
    {
        return $this->nodes;
    }

    /**
     * Clears all nodes.
     */
    public function clear(): void
    {
        foreach ($this->nodes as $node) {
            $node->clear();
        }
        $this->nodes = [];
    }

    /**
     * Connects 2 nodes together in the Graph.
     */
    public function connect(?string $sourceId, mixed $sourceValue, ?string $destId, mixed $destValue = null, ?Reference $reference = null, bool $lazy = false, bool $weak = false, bool $byConstructor = false, bool $byMultiUseArgument = false): void
    {
        if (null === $sourceId || null === $destId) {
            return;
        }

        $sourceNode = $this->createNode($sourceId, $sourceValue);
        $destNode = $this->createNode($destId, $destValue);
        $edge = new ServiceReferenceGraphEdge($sourceNode, $destNode, $reference, $lazy, $weak, $byConstructor, $byMultiUseArgument);

        $sourceNode->addOutEdge($edge);
        $destNode->addInEdge($edge);
    }

    private function createNode(string $id, mixed $value): ServiceReferenceGraphNode
    {
        if (isset($this->nodes[$id]) && $this->nodes[$id]->getValue() === $value) {
            return $this->nodes[$id];
        }

        return $this->nodes[$id] = new ServiceReferenceGraphNode($id, $value);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException;
use Symfony\Component\DependencyInjection\Reference;

/**
 * Replaces all references to aliases with references to the actual service.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class ResolveReferencesToAliasesPass extends AbstractRecursivePass
{
    protected bool $skipScalars = true;

    /**
     * @return void
     */
    public function process(ContainerBuilder $container)
    {
        parent::process($container);

        foreach ($container->getAliases() as $id => $alias) {
            $aliasId = (string) $alias;
            $this->currentId = $id;

            if ($aliasId !== $defId = $this->getDefinitionId($aliasId, $container)) {
                $newAlias = $container->setAlias($id, $defId)->setPublic($alias->isPublic());

                if ($alias->isDeprecated()) {
                    $newAlias->setDeprecated(...array_values($alias->getDeprecation('%alias_id%')));
                }
            }
        }
    }

    protected function processValue(mixed $value, bool $isRoot = false): mixed
    {
        if (!$value instanceof Reference) {
            return parent::processValue($value, $isRoot);
        }

        $defId = $this->getDefinitionId($id = (string) $value, $this->container);

        return $defId !== $id ? new Reference($defId, $value->getInvalidBehavior()) : $value;
    }

    private function getDefinitionId(string $id, ContainerBuilder $container): string
    {
        if (!$container->hasAlias($id)) {
            return $id;
        }

        $alias = $container->getAlias($id);

        if ($alias->isDeprecated()) {
            $referencingDefinition = $container->hasDefinition($this->currentId) ? $container->getDefinition($this->currentId) : $container->getAlias($this->currentId);
            if (!$referencingDefinition->isDeprecated()) {
                $deprecation = $alias->getDeprecation($id);
                trigger_deprecation($deprecation['package'], $deprecation['version'], rtrim($deprecation['message'], '. ').'. It is being referenced by the "%s" '.($container->hasDefinition($this->currentId) ? 'service.' : 'alias.'), $this->currentId);
            }
        }

        $seen = [];
        do {
            if (isset($seen[$id])) {
                throw new ServiceCircularReferenceException($id, array_merge(array_keys($seen), [$id]));
            }

            $seen[$id] = true;
            $id = (string) $container->getAlias($id);
        } while ($container->hasAlias($id));

        return $id;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Argument\AbstractArgument;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\VarExporter\ProxyHelper;

/**
 * Resolves named arguments to their corresponding numeric index.
 *
 * @author Kévin Dunglas <dunglas@gmail.com>
 */
class ResolveNamedArgumentsPass extends AbstractRecursivePass
{
    protected bool $skipScalars = true;

    protected function processValue(mixed $value, bool $isRoot = false): mixed
    {
        if ($value instanceof AbstractArgument && $value->getText().'.' === $value->getTextWithContext()) {
            $value->setContext(\sprintf('A value found in service "%s"', $this->currentId));
        }

        if (!$value instanceof Definition) {
            return parent::processValue($value, $isRoot);
        }

        $calls = $value->getMethodCalls();
        $calls[] = ['__construct', $value->getArguments()];

        foreach ($calls as $i => $call) {
            [$method, $arguments] = $call;
            $parameters = null;
            $resolvedKeys = [];
            $resolvedArguments = [];

            foreach ($arguments as $key => $argument) {
                if ($argument instanceof AbstractArgument && $argument->getText().'.' === $argument->getTextWithContext()) {
                    $argument->setContext(\sprintf('Argument '.(\is_int($key) ? 1 + $key : '"%3$s"').' of '.('__construct' === $method ? 'service "%s"' : 'method call "%s::%s()"'), $this->currentId, $method, $key));
                }

                if (\is_int($key)) {
                    $resolvedKeys[$key] = $key;
                    $resolvedArguments[$key] = $argument;
                    continue;
                }

                if (null === $parameters) {
                    $r = $this->getReflectionMethod($value, $method);
                    $class = $r instanceof \ReflectionMethod ? $r->class : $this->currentId;
                    $method = $r->getName();
                    $parameters = $r->getParameters();
                }

                if (isset($key[0]) && '$' !== $key[0] && !class_exists($key) && !interface_exists($key, false)) {
                    throw new InvalidArgumentException(\sprintf('Invalid service "%s": did you forget to add the "$" prefix to argument "%s"?', $this->currentId, $key));
                }

                if (isset($key[0]) && '$' === $key[0]) {
                    foreach ($parameters as $j => $p) {
                        if ($key === '$'.$p->name) {
                            if ($p->isVariadic() && \is_array($argument)) {
                                foreach ($argument as $variadicArgument) {
                                    $resolvedKeys[$j] = $j;
                                    $resolvedArguments[$j++] = $variadicArgument;
                                }
                            } else {
                                $resolvedKeys[$j] = $p->name;
                                $resolvedArguments[$j] = $argument;
                            }

                            continue 2;
                        }
                    }

                    throw new InvalidArgumentException(\sprintf('Invalid service "%s": method "%s()" has no argument named "%s". Check your service definition.', $this->currentId, $class !== $this->currentId ? $class.'::'.$method : $method, $key));
                }

                if (null !== $argument && !$argument instanceof Reference && !$argument instanceof Definition) {
                    throw new InvalidArgumentException(\sprintf('Invalid service "%s": the value of argument "%s" of method "%s()" must be null, an instance of "%s" or an instance of "%s", "%s" given.', $this->currentId, $key, $class !== $this->currentId ? $class.'::'.$method : $method, Reference::class, Definition::class, get_debug_type($argument)));
                }

                $typeFound = false;
                foreach ($parameters as $j => $p) {
                    if (!\array_key_exists($j, $resolvedArguments) && ProxyHelper::exportType($p, true) === $key) {
                        $resolvedKeys[$j] = $p->name;
                        $resolvedArguments[$j] = $argument;
                        $typeFound = true;
                    }
                }

                if (!$typeFound) {
                    throw new InvalidArgumentException(\sprintf('Invalid service "%s": method "%s()" has no argument type-hinted as "%s". Check your service definition.', $this->currentId, $class !== $this->currentId ? $class.'::'.$method : $method, $key));
                }
            }

            if ($resolvedArguments !== $call[1]) {
                ksort($resolvedArguments);

                if (!$value->isAutowired() && !array_is_list($resolvedArguments)) {
                    ksort($resolvedKeys);
                    $resolvedArguments = array_combine($resolvedKeys, $resolvedArguments);
                }

                $calls[$i][1] = $resolvedArguments;
            }
        }

        [, $arguments] = array_pop($calls);

        if ($arguments !== $value->getArguments()) {
            $value->setArguments($arguments);
        }
        if ($calls !== $value->getMethodCalls()) {
            $value->setMethodCalls($calls);
        }

        foreach ($value->getProperties() as $key => $argument) {
            if ($argument instanceof AbstractArgument && $argument->getText().'.' === $argument->getTextWithContext()) {
                $argument->setContext(\sprintf('Property "%s" of service "%s"', $key, $this->currentId));
            }
        }

        return parent::processValue($value, $isRoot);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Definition;

/**
 * Replaces env var placeholders by their current values.
 */
class ResolveEnvPlaceholdersPass extends AbstractRecursivePass
{
    protected bool $skipScalars = false;

    protected function processValue(mixed $value, bool $isRoot = false): mixed
    {
        if (\is_string($value)) {
            return $this->container->resolveEnvPlaceholders($value, true);
        }
        if ($value instanceof Definition) {
            $changes = $value->getChanges();
            if (isset($changes['class'])) {
                $value->setClass($this->container->resolveEnvPlaceholders($value->getClass(), true));
            }
            if (isset($changes['file'])) {
                $value->setFile($this->container->resolveEnvPlaceholders($value->getFile(), true));
            }
        }

        $value = parent::processValue($value, $isRoot);

        if ($value && \is_array($value) && !$isRoot) {
            $value = array_combine($this->container->resolveEnvPlaceholders(array_keys($value), true), $value);
        }

        return $value;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\Definition;

/**
 * Represents a node in your service graph.
 *
 * Value is typically a definition, or an alias.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class ServiceReferenceGraphNode
{
    private string $id;
    private array $inEdges = [];
    private array $outEdges = [];
    private mixed $value;

    public function __construct(string $id, mixed $value)
    {
        $this->id = $id;
        $this->value = $value;
    }

    /**
     * @return void
     */
    public function addInEdge(ServiceReferenceGraphEdge $edge)
    {
        $this->inEdges[] = $edge;
    }

    /**
     * @return void
     */
    public function addOutEdge(ServiceReferenceGraphEdge $edge)
    {
        $this->outEdges[] = $edge;
    }

    /**
     * Checks if the value of this node is an Alias.
     */
    public function isAlias(): bool
    {
        return $this->value instanceof Alias;
    }

    /**
     * Checks if the value of this node is a Definition.
     */
    public function isDefinition(): bool
    {
        return $this->value instanceof Definition;
    }

    /**
     * Returns the identifier.
     */
    public function getId(): string
    {
        return $this->id;
    }

    /**
     * Returns the in edges.
     *
     * @return ServiceReferenceGraphEdge[]
     */
    public function getInEdges(): array
    {
        return $this->inEdges;
    }

    /**
     * Returns the out edges.
     *
     * @return ServiceReferenceGraphEdge[]
     */
    public function getOutEdges(): array
    {
        return $this->outEdges;
    }

    /**
     * Returns the value of this Node.
     */
    public function getValue(): mixed
    {
        return $this->value;
    }

    /**
     * Clears all edges.
     *
     * @return void
     */
    public function clear()
    {
        $this->inEdges = $this->outEdges = [];
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;

/**
 * @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
 */
class ResolveFactoryClassPass extends AbstractRecursivePass
{
    protected bool $skipScalars = true;

    protected function processValue(mixed $value, bool $isRoot = false): mixed
    {
        if ($value instanceof Definition && \is_array($factory = $value->getFactory()) && null === $factory[0]) {
            if (null === $class = $value->getClass()) {
                throw new RuntimeException(\sprintf('The "%s" service is defined to be created by a factory, but is missing the factory class. Did you forget to define the factory or service class?', $this->currentId));
            }

            $factory[0] = $class;
            $value->setFactory($factory);
        }

        return parent::processValue($value, $isRoot);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Argument\ArgumentInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\Reference;

/**
 * Throws an exception for any Definitions that have errors and still exist.
 *
 * @author Ryan Weaver <ryan@knpuniversity.com>
 */
class DefinitionErrorExceptionPass extends AbstractRecursivePass
{
    protected bool $skipScalars = true;

    private array $erroredDefinitions = [];
    private array $sourceReferences = [];

    /**
     * @return void
     */
    public function process(ContainerBuilder $container)
    {
        try {
            parent::process($container);

            $visitedIds = [];

            foreach ($this->erroredDefinitions as $id => $definition) {
                if ($this->isErrorForRuntime($id, $visitedIds)) {
                    continue;
                }

                // only show the first error so the user can focus on it
                $errors = $definition->getErrors();

                throw new RuntimeException(reset($errors));
            }
        } finally {
            $this->erroredDefinitions = [];
            $this->sourceReferences = [];
        }
    }

    protected function processValue(mixed $value, bool $isRoot = false): mixed
    {
        if ($value instanceof ArgumentInterface) {
            parent::processValue($value->getValues());

            return $value;
        }

        if ($value instanceof Reference && $this->currentId !== $targetId = (string) $value) {
            if (
                ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE === $value->getInvalidBehavior()
                || ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE === $value->getInvalidBehavior()
            ) {
                $this->sourceReferences[$targetId][$this->currentId ?? ''] ??= true;
            } else {
                $this->sourceReferences[$targetId][$this->currentId ?? ''] = false;
            }

            return $value;
        }

        if (!$value instanceof Definition || !$value->hasErrors() || $value->hasTag('container.error')) {
            return parent::processValue($value, $isRoot);
        }

        $this->erroredDefinitions[$this->currentId ?? ''] = $value;

        return parent::processValue($value);
    }

    private function isErrorForRuntime(string $id, array &$visitedIds): bool
    {
        if (!isset($this->sourceReferences[$id])) {
            return false;
        }

        if (isset($visitedIds[$id])) {
            return $visitedIds[$id];
        }

        $visitedIds[$id] = true;

        foreach ($this->sourceReferences[$id] as $sourceId => $isRuntime) {
            if ($visitedIds[$sourceId] ?? $visitedIds[$sourceId] = $this->isErrorForRuntime($sourceId, $visitedIds)) {
                continue;
            }

            if (!$isRuntime) {
                return false;
            }
        }

        return true;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;

/**
 * @author Alexander M. Turek <me@derrabus.de>
 */
final class AttributeAutoconfigurationPass extends AbstractRecursivePass
{
    protected bool $skipScalars = true;

    private array $classAttributeConfigurators = [];
    private array $methodAttributeConfigurators = [];
    private array $propertyAttributeConfigurators = [];
    private array $parameterAttributeConfigurators = [];

    public function process(ContainerBuilder $container): void
    {
        if (!$container->getAutoconfiguredAttributes()) {
            return;
        }

        foreach ($container->getAutoconfiguredAttributes() as $attributeName => $callable) {
            $callableReflector = new \ReflectionFunction($callable(...));
            if ($callableReflector->getNumberOfParameters() <= 2) {
                $this->classAttributeConfigurators[$attributeName] = $callable;
                continue;
            }

            $reflectorParameter = $callableReflector->getParameters()[2];
            $parameterType = $reflectorParameter->getType();
            $types = [];
            if ($parameterType instanceof \ReflectionUnionType) {
                foreach ($parameterType->getTypes() as $type) {
                    $types[] = $type->getName();
                }
            } elseif ($parameterType instanceof \ReflectionNamedType) {
                $types[] = $parameterType->getName();
            } else {
                throw new LogicException(\sprintf('Argument "$%s" of attribute autoconfigurator should have a type, use one or more of "\ReflectionClass|\ReflectionMethod|\ReflectionProperty|\ReflectionParameter|\Reflector" in "%s" on line "%d".', $reflectorParameter->getName(), $callableReflector->getFileName(), $callableReflector->getStartLine()));
            }

            try {
                $attributeReflector = new \ReflectionClass($attributeName);
            } catch (\ReflectionException) {
                continue;
            }

            $targets = $attributeReflector->getAttributes(\Attribute::class)[0] ?? 0;
            $targets = $targets ? $targets->getArguments()[0] ?? -1 : 0;

            foreach (['class', 'method', 'property', 'parameter'] as $symbol) {
                if (['Reflector'] !== $types) {
                    if (!\in_array('Reflection'.ucfirst($symbol), $types, true)) {
                        continue;
                    }
                    if (!($targets & \constant('Attribute::TARGET_'.strtoupper($symbol)))) {
                        throw new LogicException(\sprintf('Invalid type "Reflection%s" on argument "$%s": attribute "%s" cannot target a '.$symbol.' in "%s" on line "%d".', ucfirst($symbol), $reflectorParameter->getName(), $attributeName, $callableReflector->getFileName(), $callableReflector->getStartLine()));
                    }
                }
                $this->{$symbol.'AttributeConfigurators'}[$attributeName] = $callable;
            }
        }

        parent::process($container);
    }

    protected function processValue(mixed $value, bool $isRoot = false): mixed
    {
        if (!$value instanceof Definition
            || !$value->isAutoconfigured()
            || $value->isAbstract()
            || $value->hasTag('container.ignore_attributes')
            || !($classReflector = $this->container->getReflectionClass($value->getClass(), false))
        ) {
            return parent::processValue($value, $isRoot);
        }

        $instanceof = $value->getInstanceofConditionals();
        $conditionals = $instanceof[$classReflector->getName()] ?? new ChildDefinition('');

        if ($this->classAttributeConfigurators) {
            foreach ($classReflector->getAttributes() as $attribute) {
                if ($configurator = $this->classAttributeConfigurators[$attribute->getName()] ?? null) {
                    $configurator($conditionals, $attribute->newInstance(), $classReflector);
                }
            }
        }

        if ($this->parameterAttributeConfigurators) {
            try {
                $constructorReflector = $this->getConstructor($value, false);
            } catch (RuntimeException) {
                $constructorReflector = null;
            }

            if ($constructorReflector) {
                foreach ($constructorReflector->getParameters() as $parameterReflector) {
                    foreach ($parameterReflector->getAttributes() as $attribute) {
                        if ($configurator = $this->parameterAttributeConfigurators[$attribute->getName()] ?? null) {
                            $configurator($conditionals, $attribute->newInstance(), $parameterReflector);
                        }
                    }
                }
            }
        }

        if ($this->methodAttributeConfigurators || $this->parameterAttributeConfigurators) {
            foreach ($classReflector->getMethods(\ReflectionMethod::IS_PUBLIC) as $methodReflector) {
                if ($methodReflector->isConstructor() || $methodReflector->isDestructor()) {
                    continue;
                }

                if ($this->methodAttributeConfigurators) {
                    foreach ($methodReflector->getAttributes() as $attribute) {
                        if ($configurator = $this->methodAttributeConfigurators[$attribute->getName()] ?? null) {
                            $configurator($conditionals, $attribute->newInstance(), $methodReflector);
                        }
                    }
                }

                if ($this->parameterAttributeConfigurators) {
                    foreach ($methodReflector->getParameters() as $parameterReflector) {
                        foreach ($parameterReflector->getAttributes() as $attribute) {
                            if ($configurator = $this->parameterAttributeConfigurators[$attribute->getName()] ?? null) {
                                $configurator($conditionals, $attribute->newInstance(), $parameterReflector);
                            }
                        }
                    }
                }
            }
        }

        if ($this->propertyAttributeConfigurators) {
            foreach ($classReflector->getProperties(\ReflectionProperty::IS_PUBLIC) as $propertyReflector) {
                if ($propertyReflector->isStatic()) {
                    continue;
                }

                foreach ($propertyReflector->getAttributes() as $attribute) {
                    if ($configurator = $this->propertyAttributeConfigurators[$attribute->getName()] ?? null) {
                        $configurator($conditionals, $attribute->newInstance(), $propertyReflector);
                    }
                }
            }
        }

        if (!isset($instanceof[$classReflector->getName()]) && new ChildDefinition('') != $conditionals) {
            $instanceof[$classReflector->getName()] = $conditionals;
            $value->setInstanceofConditionals($instanceof);
        }

        return parent::processValue($value, $isRoot);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
use Symfony\Component\DependencyInjection\Reference;

/**
 * Checks that all references are pointing to a valid service.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class CheckExceptionOnInvalidReferenceBehaviorPass extends AbstractRecursivePass
{
    protected bool $skipScalars = true;

    private array $serviceLocatorContextIds = [];

    /**
     * @return void
     */
    public function process(ContainerBuilder $container)
    {
        $this->serviceLocatorContextIds = [];
        foreach ($container->findTaggedServiceIds('container.service_locator_context') as $id => $tags) {
            $this->serviceLocatorContextIds[$id] = $tags[0]['id'];
            $container->getDefinition($id)->clearTag('container.service_locator_context');
        }

        try {
            parent::process($container);
        } finally {
            $this->serviceLocatorContextIds = [];
        }
    }

    protected function processValue(mixed $value, bool $isRoot = false): mixed
    {
        if (!$value instanceof Reference) {
            return parent::processValue($value, $isRoot);
        }
        if (ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE < $value->getInvalidBehavior() || $this->container->has($id = (string) $value)) {
            return $value;
        }

        $currentId = $this->currentId;
        $graph = $this->container->getCompiler()->getServiceReferenceGraph();

        if (isset($this->serviceLocatorContextIds[$currentId])) {
            $currentId = $this->serviceLocatorContextIds[$currentId];
            $locator = $this->container->getDefinition($this->currentId)->getFactory()[0];
            $this->throwServiceNotFoundException($value, $currentId, $locator->getArgument(0));
        }

        if ('.' === $currentId[0] && $graph->hasNode($currentId)) {
            foreach ($graph->getNode($currentId)->getInEdges() as $edge) {
                if (!$edge->getValue() instanceof Reference || ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE < $edge->getValue()->getInvalidBehavior()) {
                    continue;
                }
                $sourceId = $edge->getSourceNode()->getId();

                if ('.' !== $sourceId[0]) {
                    $currentId = $sourceId;
                    break;
                }

                if (isset($this->serviceLocatorContextIds[$sourceId])) {
                    $currentId = $this->serviceLocatorContextIds[$sourceId];
                    $locator = $this->container->getDefinition($this->currentId);
                    $this->throwServiceNotFoundException($value, $currentId, $locator->getArgument(0));
                }
            }
        }

        $this->throwServiceNotFoundException($value, $currentId, $value);
    }

    private function throwServiceNotFoundException(Reference $ref, string $sourceId, $value): void
    {
        $id = (string) $ref;
        $alternatives = [];
        foreach ($this->container->getServiceIds() as $knownId) {
            if ('' === $knownId || '.' === $knownId[0] || $knownId === $this->currentId) {
                continue;
            }

            $lev = levenshtein($id, $knownId);
            if ($lev <= \strlen($id) / 3 || str_contains($knownId, $id)) {
                $alternatives[] = $knownId;
            }
        }

        $pass = new class extends AbstractRecursivePass {
            public Reference $ref;
            public string $sourceId;
            public array $alternatives;

            public function processValue(mixed $value, bool $isRoot = false): mixed
            {
                if ($this->ref !== $value) {
                    return parent::processValue($value, $isRoot);
                }
                $sourceId = $this->sourceId;
                if (null !== $this->currentId && $this->currentId !== (string) $value) {
                    $sourceId = $this->currentId.'" in the container provided to "'.$sourceId;
                }

                throw new ServiceNotFoundException((string) $value, $sourceId, null, $this->alternatives);
            }
        };
        $pass->ref = $ref;
        $pass->sourceId = $sourceId;
        $pass->alternatives = $alternatives;

        $pass->processValue($value, true);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

/**
 * Represents an edge in your service graph.
 *
 * Value is typically a reference.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class ServiceReferenceGraphEdge
{
    private ServiceReferenceGraphNode $sourceNode;
    private ServiceReferenceGraphNode $destNode;
    private mixed $value;
    private bool $lazy;
    private bool $weak;
    private bool $byConstructor;
    private bool $byMultiUseArgument;

    public function __construct(ServiceReferenceGraphNode $sourceNode, ServiceReferenceGraphNode $destNode, mixed $value = null, bool $lazy = false, bool $weak = false, bool $byConstructor = false, bool $byMultiUseArgument = false)
    {
        $this->sourceNode = $sourceNode;
        $this->destNode = $destNode;
        $this->value = $value;
        $this->lazy = $lazy;
        $this->weak = $weak;
        $this->byConstructor = $byConstructor;
        $this->byMultiUseArgument = $byMultiUseArgument;
    }

    /**
     * Returns the value of the edge.
     */
    public function getValue(): mixed
    {
        return $this->value;
    }

    /**
     * Returns the source node.
     */
    public function getSourceNode(): ServiceReferenceGraphNode
    {
        return $this->sourceNode;
    }

    /**
     * Returns the destination node.
     */
    public function getDestNode(): ServiceReferenceGraphNode
    {
        return $this->destNode;
    }

    /**
     * Returns true if the edge is lazy, meaning it's a dependency not requiring direct instantiation.
     */
    public function isLazy(): bool
    {
        return $this->lazy;
    }

    /**
     * Returns true if the edge is weak, meaning it shouldn't prevent removing the target service.
     */
    public function isWeak(): bool
    {
        return $this->weak;
    }

    /**
     * Returns true if the edge links with a constructor argument.
     */
    public function isReferencedByConstructor(): bool
    {
        return $this->byConstructor;
    }

    public function isFromMultiUseArgument(): bool
    {
        return $this->byMultiUseArgument;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException;
use Symfony\Component\DependencyInjection\Reference;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 */
class ResolveDecoratorStackPass implements CompilerPassInterface
{
    /**
     * @return void
     */
    public function process(ContainerBuilder $container)
    {
        $stacks = [];

        foreach ($container->findTaggedServiceIds('container.stack') as $id => $tags) {
            $definition = $container->getDefinition($id);

            if (!$definition instanceof ChildDefinition) {
                throw new InvalidArgumentException(\sprintf('Invalid service "%s": only definitions with a "parent" can have the "container.stack" tag.', $id));
            }

            if (!$stack = $definition->getArguments()) {
                throw new InvalidArgumentException(\sprintf('Invalid service "%s": the stack of decorators is empty.', $id));
            }

            $stacks[$id] = $stack;
        }

        if (!$stacks) {
            return;
        }

        $resolvedDefinitions = [];

        foreach ($container->getDefinitions() as $id => $definition) {
            if (!isset($stacks[$id])) {
                $resolvedDefinitions[$id] = $definition;
                continue;
            }

            foreach (array_reverse($this->resolveStack($stacks, [$id]), true) as $k => $v) {
                $resolvedDefinitions[$k] = $v;
            }

            $alias = $container->setAlias($id, $k);

            if ($definition->getChanges()['public'] ?? false) {
                $alias->setPublic($definition->isPublic());
            }

            if ($definition->isDeprecated()) {
                $alias->setDeprecated(...array_values($definition->getDeprecation('%alias_id%')));
            }
        }

        $container->setDefinitions($resolvedDefinitions);
    }

    private function resolveStack(array $stacks, array $path): array
    {
        $definitions = [];
        $id = end($path);
        $prefix = '.'.$id.'.';

        if (!isset($stacks[$id])) {
            return [$id => new ChildDefinition($id)];
        }

        if (key($path) !== $searchKey = array_search($id, $path)) {
            throw new ServiceCircularReferenceException($id, \array_slice($path, $searchKey));
        }

        foreach ($stacks[$id] as $k => $definition) {
            if ($definition instanceof ChildDefinition && isset($stacks[$definition->getParent()])) {
                $path[] = $definition->getParent();
                $definition = unserialize(serialize($definition), ['allowed_classes' => true]); // deep clone
            } elseif ($definition instanceof Definition) {
                $definitions[$decoratedId = $prefix.$k] = $definition;
                continue;
            } elseif ($definition instanceof Reference || $definition instanceof Alias) {
                $path[] = (string) $definition;
            } else {
                throw new InvalidArgumentException(\sprintf('Invalid service "%s": unexpected value of type "%s" found in the stack of decorators.', $id, get_debug_type($definition)));
            }

            $p = $prefix.$k;

            foreach ($this->resolveStack($stacks, $path) as $k => $v) {
                $definitions[$decoratedId = $p.$k] = $definition instanceof ChildDefinition ? $definition->setParent($k) : new ChildDefinition($k);
                $definition = null;
            }
            array_pop($path);
        }

        if (1 === \count($path)) {
            foreach ($definitions as $k => $definition) {
                $definition->setPublic(false)->setTags([])->setDecoratedService($decoratedId);
            }
            $definition->setDecoratedService(null);
        }

        return $definitions;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\ContainerBuilder;

class RemoveBuildParametersPass implements CompilerPassInterface
{
    /**
     * @var array<string, mixed>
     */
    private array $removedParameters = [];

    public function __construct(
        private bool $preserveArrays = false,
    ) {
    }

    /**
     * @return void
     */
    public function process(ContainerBuilder $container)
    {
        $parameterBag = $container->getParameterBag();
        $this->removedParameters = [];

        foreach ($parameterBag->all() as $name => $value) {
            if ('.' !== ($name[0] ?? '')) {
                continue;
            }
            if (!$this->preserveArrays || !\is_array($value)) {
                $this->removedParameters[$name] = $value;
                $parameterBag->remove($name);
                $container->log($this, \sprintf('Removing build parameter "%s".', $name));
            } else {
                $container->log($this, \sprintf('Keeping array build parameter "%s" for placeholder resolution.', $name));
            }
        }
    }

    /**
     * @return array<string, mixed>
     */
    public function getRemovedParameters(): array
    {
        return $this->removedParameters;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;

/**
 * Compiler Pass Configuration.
 *
 * This class has a default configuration embedded.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class PassConfig
{
    public const TYPE_AFTER_REMOVING = 'afterRemoving';
    public const TYPE_BEFORE_OPTIMIZATION = 'beforeOptimization';
    public const TYPE_BEFORE_REMOVING = 'beforeRemoving';
    public const TYPE_OPTIMIZE = 'optimization';
    public const TYPE_REMOVE = 'removing';

    private MergeExtensionConfigurationPass $mergePass;
    private array $afterRemovingPasses;
    private array $beforeOptimizationPasses;
    private array $beforeRemovingPasses = [];
    private array $optimizationPasses;
    private array $removingPasses;

    public function __construct()
    {
        $this->mergePass = new MergeExtensionConfigurationPass();

        $this->beforeOptimizationPasses = [
            100 => [
                new ResolveClassPass(),
                new RegisterAutoconfigureAttributesPass(),
                new AutowireAsDecoratorPass(),
                new AttributeAutoconfigurationPass(),
                new ResolveInstanceofConditionalsPass(),
                new RegisterEnvVarProcessorsPass(),
            ],
            -1000 => [new ExtensionCompilerPass()],
        ];

        $this->optimizationPasses = [[
            new AutoAliasServicePass(),
            new ValidateEnvPlaceholdersPass(),
            new ResolveDecoratorStackPass(),
            new ResolveChildDefinitionsPass(),
            new RegisterServiceSubscribersPass(),
            new ResolveParameterPlaceHoldersPass(false, false),
            new ResolveFactoryClassPass(),
            new ResolveNamedArgumentsPass(),
            new AutowireRequiredMethodsPass(),
            new AutowireRequiredPropertiesPass(),
            new ResolveBindingsPass(),
            new ServiceLocatorTagPass(),
            new DecoratorServicePass(),
            new CheckDefinitionValidityPass(),
            new AutowirePass(false),
            new ServiceLocatorTagPass(),
            new ResolveTaggedIteratorArgumentPass(),
            new ResolveServiceSubscribersPass(),
            new ResolveReferencesToAliasesPass(),
            new ResolveInvalidReferencesPass(),
            new AnalyzeServiceReferencesPass(true),
            new CheckCircularReferencesPass(),
            new CheckReferenceValidityPass(),
            new CheckArgumentsValidityPass(false),
        ]];

        $this->removingPasses = [[
            new RemovePrivateAliasesPass(),
            new ReplaceAliasByActualDefinitionPass(),
            new RemoveAbstractDefinitionsPass(),
            new RemoveUnusedDefinitionsPass(),
            new AnalyzeServiceReferencesPass(),
            new CheckExceptionOnInvalidReferenceBehaviorPass(),
            new InlineServiceDefinitionsPass(new AnalyzeServiceReferencesPass()),
            new AnalyzeServiceReferencesPass(),
            new CheckFactoryBuilderCircularReferencePass(),
            new DefinitionErrorExceptionPass(),
        ]];

        $this->afterRemovingPasses = [
            0 => [
                new ResolveHotPathPass(),
                new ResolveNoPreloadPass(),
                new AliasDeprecatedPublicServicesPass(),
            ],
            // Let build parameters be available as late as possible
            // Don't remove array parameters since ResolveParameterPlaceHoldersPass doesn't resolve them
            -2048 => [new RemoveBuildParametersPass(true)],
        ];
    }

    /**
     * Returns all passes in order to be processed.
     *
     * @return CompilerPassInterface[]
     */
    public function getPasses(): array
    {
        return array_merge(
            [$this->mergePass],
            $this->getBeforeOptimizationPasses(),
            $this->getOptimizationPasses(),
            $this->getBeforeRemovingPasses(),
            $this->getRemovingPasses(),
            $this->getAfterRemovingPasses()
        );
    }

    /**
     * Adds a pass.
     *
     * @return void
     *
     * @throws InvalidArgumentException when a pass type doesn't exist
     */
    public function addPass(CompilerPassInterface $pass, string $type = self::TYPE_BEFORE_OPTIMIZATION, int $priority = 0)
    {
        $property = $type.'Passes';
        if (!isset($this->$property)) {
            throw new InvalidArgumentException(\sprintf('Invalid type "%s".', $type));
        }

        $passes = &$this->$property;

        if (!isset($passes[$priority])) {
            $passes[$priority] = [];
        }
        $passes[$priority][] = $pass;
    }

    /**
     * Gets all passes for the AfterRemoving pass.
     *
     * @return CompilerPassInterface[]
     */
    public function getAfterRemovingPasses(): array
    {
        return $this->sortPasses($this->afterRemovingPasses);
    }

    /**
     * Gets all passes for the BeforeOptimization pass.
     *
     * @return CompilerPassInterface[]
     */
    public function getBeforeOptimizationPasses(): array
    {
        return $this->sortPasses($this->beforeOptimizationPasses);
    }

    /**
     * Gets all passes for the BeforeRemoving pass.
     *
     * @return CompilerPassInterface[]
     */
    public function getBeforeRemovingPasses(): array
    {
        return $this->sortPasses($this->beforeRemovingPasses);
    }

    /**
     * Gets all passes for the Optimization pass.
     *
     * @return CompilerPassInterface[]
     */
    public function getOptimizationPasses(): array
    {
        return $this->sortPasses($this->optimizationPasses);
    }

    /**
     * Gets all passes for the Removing pass.
     *
     * @return CompilerPassInterface[]
     */
    public function getRemovingPasses(): array
    {
        return $this->sortPasses($this->removingPasses);
    }

    /**
     * Gets the Merge pass.
     */
    public function getMergePass(): CompilerPassInterface
    {
        return $this->mergePass;
    }

    /**
     * @return void
     */
    public function setMergePass(CompilerPassInterface $pass)
    {
        $this->mergePass = $pass;
    }

    /**
     * Sets the AfterRemoving passes.
     *
     * @param CompilerPassInterface[] $passes
     *
     * @return void
     */
    public function setAfterRemovingPasses(array $passes)
    {
        $this->afterRemovingPasses = [$passes];
    }

    /**
     * Sets the BeforeOptimization passes.
     *
     * @param CompilerPassInterface[] $passes
     *
     * @return void
     */
    public function setBeforeOptimizationPasses(array $passes)
    {
        $this->beforeOptimizationPasses = [$passes];
    }

    /**
     * Sets the BeforeRemoving passes.
     *
     * @param CompilerPassInterface[] $passes
     *
     * @return void
     */
    public function setBeforeRemovingPasses(array $passes)
    {
        $this->beforeRemovingPasses = [$passes];
    }

    /**
     * Sets the Optimization passes.
     *
     * @param CompilerPassInterface[] $passes
     *
     * @return void
     */
    public function setOptimizationPasses(array $passes)
    {
        $this->optimizationPasses = [$passes];
    }

    /**
     * Sets the Removing passes.
     *
     * @param CompilerPassInterface[] $passes
     *
     * @return void
     */
    public function setRemovingPasses(array $passes)
    {
        $this->removingPasses = [$passes];
    }

    /**
     * Sort passes by priority.
     *
     * @param array $passes CompilerPassInterface instances with their priority as key
     *
     * @return CompilerPassInterface[]
     */
    private function sortPasses(array $passes): array
    {
        if (0 === \count($passes)) {
            return [];
        }

        krsort($passes);

        // Flatten the array
        return array_merge(...$passes);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;

/**
 * Reads #[AsDecorator] attributes on definitions that are autowired
 * and don't have the "container.ignore_attributes" tag.
 */
final class AutowireAsDecoratorPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container): void
    {
        foreach ($container->getDefinitions() as $definition) {
            if ($this->accept($definition) && $reflectionClass = $container->getReflectionClass($definition->getClass(), false)) {
                $this->processClass($definition, $reflectionClass);
            }
        }
    }

    private function accept(Definition $definition): bool
    {
        return !$definition->hasTag('container.ignore_attributes') && $definition->isAutowired();
    }

    private function processClass(Definition $definition, \ReflectionClass $reflectionClass): void
    {
        foreach ($reflectionClass->getAttributes(AsDecorator::class, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
            $attribute = $attribute->newInstance();

            $definition->setDecoratedService($attribute->decorates, null, $attribute->priority, $attribute->onInvalid);
        }
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Argument\ArgumentInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;

/**
 * Propagate "container.hot_path" tags to referenced services.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
class ResolveHotPathPass extends AbstractRecursivePass
{
    protected bool $skipScalars = true;

    private array $resolvedIds = [];

    /**
     * @return void
     */
    public function process(ContainerBuilder $container)
    {
        try {
            parent::process($container);
            $container->getDefinition('service_container')->clearTag('container.hot_path');
        } finally {
            $this->resolvedIds = [];
        }
    }

    protected function processValue(mixed $value, bool $isRoot = false): mixed
    {
        if ($value instanceof ArgumentInterface) {
            return $value;
        }

        if ($value instanceof Definition && $isRoot) {
            if ($value->isDeprecated()) {
                return $value->clearTag('container.hot_path');
            }

            $this->resolvedIds[$this->currentId ?? ''] = true;

            if (!$value->hasTag('container.hot_path')) {
                return $value;
            }
        }

        if ($value instanceof Reference && ContainerBuilder::IGNORE_ON_UNINITIALIZED_REFERENCE !== $value->getInvalidBehavior() && $this->container->hasDefinition($id = (string) $value)) {
            $definition = $this->container->getDefinition($id);

            if ($definition->isDeprecated() || $definition->hasTag('container.hot_path')) {
                return $value;
            }

            $definition->addTag('container.hot_path');

            if (isset($this->resolvedIds[$id])) {
                parent::processValue($definition, false);
            }

            return $value;
        }

        return parent::processValue($value, $isRoot);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\ContainerBuilder;

/**
 * A pass to automatically process extensions if they implement
 * CompilerPassInterface.
 *
 * @author Wouter J <wouter@wouterj.nl>
 */
class ExtensionCompilerPass implements CompilerPassInterface
{
    /**
     * @return void
     */
    public function process(ContainerBuilder $container)
    {
        foreach ($container->getExtensions() as $extension) {
            if (!$extension instanceof CompilerPassInterface) {
                continue;
            }

            $extension->process($container);
        }
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Argument\ArgumentInterface;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\TypedReference;

/**
 * Emulates the invalid behavior if the reference is not found within the
 * container.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class ResolveInvalidReferencesPass implements CompilerPassInterface
{
    private ContainerBuilder $container;
    private RuntimeException $signalingException;
    private string $currentId;

    /**
     * Process the ContainerBuilder to resolve invalid references.
     *
     * @return void
     */
    public function process(ContainerBuilder $container)
    {
        $this->container = $container;
        $this->signalingException = new RuntimeException('Invalid reference.');

        try {
            foreach ($container->getDefinitions() as $this->currentId => $definition) {
                $this->processValue($definition);
            }
        } finally {
            unset($this->container, $this->signalingException);
        }
    }

    /**
     * Processes arguments to determine invalid references.
     *
     * @throws RuntimeException When an invalid reference is found
     */
    private function processValue(mixed $value, int $rootLevel = 0, int $level = 0): mixed
    {
        if ($value instanceof ServiceClosureArgument) {
            $value->setValues($this->processValue($value->getValues(), 1, 1));
        } elseif ($value instanceof ArgumentInterface) {
            $value->setValues($this->processValue($value->getValues(), $rootLevel, 1 + $level));
        } elseif ($value instanceof Definition) {
            if ($value->isSynthetic() || $value->isAbstract()) {
                return $value;
            }
            $value->setArguments($this->processValue($value->getArguments(), 0));
            $value->setProperties($this->processValue($value->getProperties(), 1));
            $value->setMethodCalls($this->processValue($value->getMethodCalls(), 2));
        } elseif (\is_array($value)) {
            $i = 0;

            foreach ($value as $k => $v) {
                try {
                    if (false !== $i && $k !== $i++) {
                        $i = false;
                    }
                    if ($v !== $processedValue = $this->processValue($v, $rootLevel, 1 + $level)) {
                        $value[$k] = $processedValue;
                    }
                } catch (RuntimeException $e) {
                    if ($rootLevel < $level || ($rootLevel && !$level)) {
                        unset($value[$k]);
                    } elseif ($rootLevel) {
                        throw $e;
                    } else {
                        $value[$k] = null;
                    }
                }
            }

            // Ensure numerically indexed arguments have sequential numeric keys.
            if (false !== $i) {
                $value = array_values($value);
            }
        } elseif ($value instanceof Reference) {
            if ($this->container->hasDefinition($id = (string) $value) ? !$this->container->getDefinition($id)->hasTag('container.excluded') : $this->container->hasAlias($id)) {
                return $value;
            }

            $currentDefinition = $this->container->getDefinition($this->currentId);

            // resolve decorated service behavior depending on decorator service
            if ($currentDefinition->innerServiceId === $id && ContainerInterface::NULL_ON_INVALID_REFERENCE === $currentDefinition->decorationOnInvalid) {
                return null;
            }

            $invalidBehavior = $value->getInvalidBehavior();

            if (ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE === $invalidBehavior && $value instanceof TypedReference && !$this->container->has($id)) {
                $e = new ServiceNotFoundException($id, $this->currentId);

                // since the error message varies by $id and $this->currentId, so should the id of the dummy errored definition
                $this->container->register($id = \sprintf('.errored.%s.%s', $this->currentId, $id), $value->getType())
                    ->addError($e->getMessage());

                return new TypedReference($id, $value->getType(), $value->getInvalidBehavior());
            }

            // resolve invalid behavior
            if (ContainerInterface::NULL_ON_INVALID_REFERENCE === $invalidBehavior) {
                $value = null;
            } elseif (ContainerInterface::IGNORE_ON_INVALID_REFERENCE === $invalidBehavior) {
                if (0 < $level || $rootLevel) {
                    throw $this->signalingException;
                }
                $value = null;
            }
        }

        return $value;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;

/**
 * Resolves all TaggedIteratorArgument arguments.
 *
 * @author Roland Franssen <franssen.roland@gmail.com>
 */
class ResolveTaggedIteratorArgumentPass extends AbstractRecursivePass
{
    use PriorityTaggedServiceTrait;

    protected bool $skipScalars = true;

    protected function processValue(mixed $value, bool $isRoot = false): mixed
    {
        if (!$value instanceof TaggedIteratorArgument) {
            return parent::processValue($value, $isRoot);
        }

        $exclude = $value->getExclude();
        if ($value->excludeSelf()) {
            $exclude[] = $this->currentId;
        }

        $value->setValues($this->findAndSortTaggedServices($value, $this->container, $exclude));

        return $value;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\ContainerBuilder;

/**
 * Removes abstract Definitions.
 */
class RemoveAbstractDefinitionsPass implements CompilerPassInterface
{
    /**
     * Removes abstract definitions from the ContainerBuilder.
     *
     * @return void
     */
    public function process(ContainerBuilder $container)
    {
        foreach ($container->getDefinitions() as $id => $definition) {
            if ($definition->isAbstract()) {
                $container->resolveEnvPlaceholders($definition);
                $container->removeDefinition($id);
                $container->log($this, \sprintf('Removed service "%s"; reason: abstract.', $id));
            }
        }
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Psr\Container\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Contracts\Service\ServiceProviderInterface;

/**
 * Compiler pass to inject their service locator to service subscribers.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
class ResolveServiceSubscribersPass extends AbstractRecursivePass
{
    protected bool $skipScalars = true;

    private ?string $serviceLocator = null;

    protected function processValue(mixed $value, bool $isRoot = false): mixed
    {
        if ($value instanceof Reference && $this->serviceLocator && \in_array((string) $value, [ContainerInterface::class, ServiceProviderInterface::class], true)) {
            return new Reference($this->serviceLocator);
        }

        if (!$value instanceof Definition) {
            return parent::processValue($value, $isRoot);
        }

        $serviceLocator = $this->serviceLocator;
        $this->serviceLocator = null;

        if ($value->hasTag('container.service_subscriber.locator')) {
            $this->serviceLocator = $value->getTag('container.service_subscriber.locator')[0]['id'];
            $value->clearTag('container.service_subscriber.locator');
        }

        try {
            return parent::processValue($value);
        } finally {
            $this->serviceLocator = $serviceLocator;
        }
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;

/**
 * Sets a service to be an alias of another one, given a format pattern.
 */
class AutoAliasServicePass implements CompilerPassInterface
{
    /**
     * @return void
     */
    public function process(ContainerBuilder $container)
    {
        foreach ($container->findTaggedServiceIds('auto_alias') as $serviceId => $tags) {
            foreach ($tags as $tag) {
                if (!isset($tag['format'])) {
                    throw new InvalidArgumentException(\sprintf('Missing tag information "format" on auto_alias service "%s".', $serviceId));
                }

                $aliasId = $container->getParameterBag()->resolveValue($tag['format']);
                if ($container->hasDefinition($aliasId) || $container->hasAlias($aliasId)) {
                    $alias = new Alias($aliasId, $container->getDefinition($serviceId)->isPublic());
                    $container->setAlias($serviceId, $alias);
                }
            }
        }
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;

/**
 * Checks if arguments of methods are properly configured.
 *
 * @author Kévin Dunglas <dunglas@gmail.com>
 * @author Nicolas Grekas <p@tchwork.com>
 */
class CheckArgumentsValidityPass extends AbstractRecursivePass
{
    protected bool $skipScalars = true;

    private bool $throwExceptions;

    public function __construct(bool $throwExceptions = true)
    {
        $this->throwExceptions = $throwExceptions;
    }

    protected function processValue(mixed $value, bool $isRoot = false): mixed
    {
        if (!$value instanceof Definition) {
            return parent::processValue($value, $isRoot);
        }

        $i = 0;
        $hasNamedArgs = false;
        foreach ($value->getArguments() as $k => $v) {
            if (preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $k)) {
                $hasNamedArgs = true;
                continue;
            }

            if ($k !== $i++) {
                if (!\is_int($k)) {
                    $msg = \sprintf('Invalid constructor argument for service "%s": integer expected but found string "%s". Check your service definition.', $this->currentId, $k);
                    $value->addError($msg);
                    if ($this->throwExceptions) {
                        throw new RuntimeException($msg);
                    }

                    break;
                }

                $msg = \sprintf('Invalid constructor argument %d for service "%s": argument %d must be defined before. Check your service definition.', 1 + $k, $this->currentId, $i);
                $value->addError($msg);
                if ($this->throwExceptions) {
                    throw new RuntimeException($msg);
                }
            }

            if ($hasNamedArgs) {
                $msg = \sprintf('Invalid constructor argument for service "%s": cannot use positional argument after named argument. Check your service definition.', $this->currentId);
                $value->addError($msg);
                if ($this->throwExceptions) {
                    throw new RuntimeException($msg);
                }

                break;
            }
        }

        foreach ($value->getMethodCalls() as $methodCall) {
            $i = 0;
            $hasNamedArgs = false;
            foreach ($methodCall[1] as $k => $v) {
                if (preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $k)) {
                    $hasNamedArgs = true;
                    continue;
                }

                if ($k !== $i++) {
                    if (!\is_int($k)) {
                        $msg = \sprintf('Invalid argument for method call "%s" of service "%s": integer expected but found string "%s". Check your service definition.', $methodCall[0], $this->currentId, $k);
                        $value->addError($msg);
                        if ($this->throwExceptions) {
                            throw new RuntimeException($msg);
                        }

                        break;
                    }

                    $msg = \sprintf('Invalid argument %d for method call "%s" of service "%s": argument %d must be defined before. Check your service definition.', 1 + $k, $methodCall[0], $this->currentId, $i);
                    $value->addError($msg);
                    if ($this->throwExceptions) {
                        throw new RuntimeException($msg);
                    }
                }

                if ($hasNamedArgs) {
                    $msg = \sprintf('Invalid argument for method call "%s" of service "%s": cannot use positional argument after named argument. Check your service definition.', $methodCall[0], $this->currentId);
                    $value->addError($msg);
                    if ($this->throwExceptions) {
                        throw new RuntimeException($msg);
                    }

                    break;
                }
            }
        }

        return null;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\EnvParameterException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;

/**
 * This pass validates each definition individually only taking the information
 * into account which is contained in the definition itself.
 *
 * Later passes can rely on the following, and specifically do not need to
 * perform these checks themselves:
 *
 * - non synthetic, non abstract services always have a class set
 * - synthetic services are always public
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class CheckDefinitionValidityPass implements CompilerPassInterface
{
    /**
     * Processes the ContainerBuilder to validate the Definition.
     *
     * @return void
     *
     * @throws RuntimeException When the Definition is invalid
     */
    public function process(ContainerBuilder $container)
    {
        foreach ($container->getDefinitions() as $id => $definition) {
            if ($definition->hasErrors()) {
                continue;
            }

            // synthetic service is public
            if ($definition->isSynthetic() && !$definition->isPublic()) {
                throw new RuntimeException(\sprintf('A synthetic service ("%s") must be public.', $id));
            }

            // non-synthetic, non-abstract service has class
            if (!$definition->isAbstract() && !$definition->isSynthetic() && !$definition->getClass() && !$definition->hasTag('container.service_locator') && (!$definition->getFactory() || !preg_match(ContainerBuilder::ANONYMOUS_ID_REGEXP, $id))) {
                if ($definition->getFactory()) {
                    throw new RuntimeException(\sprintf('Please add the class to service "%s" even if it is constructed by a factory since we might need to add method calls based on compile-time checks.', $id));
                }
                if (class_exists($id) || interface_exists($id, false)) {
                    if (str_starts_with($id, '\\') && 1 < substr_count($id, '\\')) {
                        throw new RuntimeException(\sprintf('The definition for "%s" has no class attribute, and appears to reference a class or interface. Please specify the class attribute explicitly or remove the leading backslash by renaming the service to "%s" to get rid of this error.', $id, substr($id, 1)));
                    }

                    throw new RuntimeException(\sprintf('The definition for "%s" has no class attribute, and appears to reference a class or interface in the global namespace. Leaving out the "class" attribute is only allowed for namespaced classes. Please specify the class attribute explicitly to get rid of this error.', $id));
                }

                throw new RuntimeException(\sprintf('The definition for "%s" has no class. If you intend to inject this service dynamically at runtime, please mark it as synthetic=true. If this is an abstract definition solely used by child definitions, please add abstract=true, otherwise specify a class to get rid of this error.', $id));
            }

            // tag attribute values must be scalars
            foreach ($definition->getTags() as $name => $tags) {
                foreach ($tags as $attributes) {
                    $this->validateAttributes($id, $name, $attributes);
                }
            }

            if ($definition->isPublic() && !$definition->isPrivate()) {
                $resolvedId = $container->resolveEnvPlaceholders($id, null, $usedEnvs);
                if (null !== $usedEnvs) {
                    throw new EnvParameterException([$resolvedId], null, 'A service name ("%s") cannot contain dynamic values.');
                }
            }
        }

        foreach ($container->getAliases() as $id => $alias) {
            if ($alias->isPublic() && !$alias->isPrivate()) {
                $resolvedId = $container->resolveEnvPlaceholders($id, null, $usedEnvs);
                if (null !== $usedEnvs) {
                    throw new EnvParameterException([$resolvedId], null, 'An alias name ("%s") cannot contain dynamic values.');
                }
            }
        }
    }

    private function validateAttributes(string $id, string $tag, array $attributes, array $path = []): void
    {
        foreach ($attributes as $name => $value) {
            if (\is_array($value)) {
                $this->validateAttributes($id, $tag, $value, [...$path, $name]);
            } elseif (!\is_scalar($value) && null !== $value) {
                $name = implode('.', [...$path, $name]);
                throw new RuntimeException(\sprintf('A "tags" attribute must be of a scalar-type for service "%s", tag "%s", attribute "%s".', $id, $tag, $name));
            }
        }
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;

/**
 * Resolves all parameter placeholders "%somevalue%" to their real values.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class ResolveParameterPlaceHoldersPass extends AbstractRecursivePass
{
    protected bool $skipScalars = false;

    private ParameterBagInterface $bag;

    public function __construct(
        private bool $resolveArrays = true,
        private bool $throwOnResolveException = true,
    ) {
    }

    /**
     * @return void
     *
     * @throws ParameterNotFoundException
     */
    public function process(ContainerBuilder $container)
    {
        $this->bag = $container->getParameterBag();

        try {
            parent::process($container);

            $aliases = [];
            foreach ($container->getAliases() as $name => $target) {
                $this->currentId = $name;
                $aliases[$this->bag->resolveValue($name)] = $target;
            }
            $container->setAliases($aliases);
        } catch (ParameterNotFoundException $e) {
            $e->setSourceId($this->currentId);

            throw $e;
        }

        $this->bag->resolve();
        unset($this->bag);
    }

    protected function processValue(mixed $value, bool $isRoot = false): mixed
    {
        if (\is_string($value)) {
            try {
                $v = $this->bag->resolveValue($value);
            } catch (ParameterNotFoundException $e) {
                if ($this->throwOnResolveException) {
                    throw $e;
                }

                $v = null;
                $this->container->getDefinition($this->currentId)->addError($e->getMessage());
            }

            return $this->resolveArrays || !$v || !\is_array($v) ? $v : $value;
        }
        if ($value instanceof Definition) {
            $value->setBindings($this->processValue($value->getBindings()));
            $changes = $value->getChanges();
            if (isset($changes['class'])) {
                $value->setClass($this->bag->resolveValue($value->getClass()));
            }
            if (isset($changes['file'])) {
                $value->setFile($this->bag->resolveValue($value->getFile()));
            }
            $tags = $value->getTags();
            if (isset($tags['proxy'])) {
                $tags['proxy'] = $this->bag->resolveValue($tags['proxy']);
                $value->setTags($tags);
            }
        }

        $value = parent::processValue($value, $isRoot);

        if ($value && \is_array($value)) {
            $value = array_combine($this->bag->resolveValue(array_keys($value)), $value);
        }

        return $value;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\ContainerBuilder;

/**
 * Interface that must be implemented by compilation passes.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
interface CompilerPassInterface
{
    /**
     * You can modify the container here before it is dumped to PHP code.
     *
     * @return void
     */
    public function process(ContainerBuilder $container);
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\ExceptionInterface;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException;

/**
 * This replaces all ChildDefinition instances with their equivalent fully
 * merged Definition instance.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 * @author Nicolas Grekas <p@tchwork.com>
 */
class ResolveChildDefinitionsPass extends AbstractRecursivePass
{
    protected bool $skipScalars = true;

    private array $currentPath;

    protected function processValue(mixed $value, bool $isRoot = false): mixed
    {
        if (!$value instanceof Definition) {
            return parent::processValue($value, $isRoot);
        }
        if ($isRoot) {
            // yes, we are specifically fetching the definition from the
            // container to ensure we are not operating on stale data
            $value = $this->container->getDefinition($this->currentId);
        }
        if ($value instanceof ChildDefinition) {
            $this->currentPath = [];
            $value = $this->resolveDefinition($value);
            if ($isRoot) {
                $this->container->setDefinition($this->currentId, $value);
            }
        }

        return parent::processValue($value, $isRoot);
    }

    /**
     * Resolves the definition.
     *
     * @throws RuntimeException When the definition is invalid
     */
    private function resolveDefinition(ChildDefinition $definition): Definition
    {
        try {
            return $this->doResolveDefinition($definition);
        } catch (ServiceCircularReferenceException $e) {
            throw $e;
        } catch (ExceptionInterface $e) {
            $r = new \ReflectionProperty($e, 'message');
            $r->setValue($e, \sprintf('Service "%s": %s', $this->currentId, $e->getMessage()));

            throw $e;
        }
    }

    private function doResolveDefinition(ChildDefinition $definition): Definition
    {
        if (!$this->container->has($parent = $definition->getParent())) {
            throw new RuntimeException(\sprintf('Parent definition "%s" does not exist.', $parent));
        }

        $searchKey = array_search($parent, $this->currentPath);
        $this->currentPath[] = $parent;

        if (false !== $searchKey) {
            throw new ServiceCircularReferenceException($parent, \array_slice($this->currentPath, $searchKey));
        }

        $parentDef = $this->container->findDefinition($parent);
        if ($parentDef instanceof ChildDefinition) {
            $id = $this->currentId;
            $this->currentId = $parent;
            $parentDef = $this->resolveDefinition($parentDef);
            $this->container->setDefinition($parent, $parentDef);
            $this->currentId = $id;
        }

        $this->container->log($this, \sprintf('Resolving inheritance for "%s" (parent: %s).', $this->currentId, $parent));
        $def = new Definition();

        // merge in parent definition
        // purposely ignored attributes: abstract, shared, tags, autoconfigured
        $def->setClass($parentDef->getClass());
        $def->setArguments($parentDef->getArguments());
        $def->setMethodCalls($parentDef->getMethodCalls());
        $def->setProperties($parentDef->getProperties());
        if ($parentDef->isDeprecated()) {
            $deprecation = $parentDef->getDeprecation('%service_id%');
            $def->setDeprecated($deprecation['package'], $deprecation['version'], $deprecation['message']);
        }
        $def->setFactory($parentDef->getFactory());
        $def->setConfigurator($parentDef->getConfigurator());
        $def->setFile($parentDef->getFile());
        $def->setPublic($parentDef->isPublic());
        $def->setLazy($parentDef->isLazy());
        $def->setAutowired($parentDef->isAutowired());
        $def->setChanges($parentDef->getChanges());

        $def->setBindings($definition->getBindings() + $parentDef->getBindings());

        $def->setSynthetic($definition->isSynthetic());

        // overwrite with values specified in the decorator
        $changes = $definition->getChanges();
        if (isset($changes['class'])) {
            $def->setClass($definition->getClass());
        }
        if (isset($changes['factory'])) {
            $def->setFactory($definition->getFactory());
        }
        if (isset($changes['configurator'])) {
            $def->setConfigurator($definition->getConfigurator());
        }
        if (isset($changes['file'])) {
            $def->setFile($definition->getFile());
        }
        if (isset($changes['public'])) {
            $def->setPublic($definition->isPublic());
        } else {
            $def->setPublic($parentDef->isPublic());
        }
        if (isset($changes['lazy'])) {
            $def->setLazy($definition->isLazy());
        }
        if (isset($changes['deprecated']) && $definition->isDeprecated()) {
            $deprecation = $definition->getDeprecation('%service_id%');
            $def->setDeprecated($deprecation['package'], $deprecation['version'], $deprecation['message']);
        }
        if (isset($changes['autowired'])) {
            $def->setAutowired($definition->isAutowired());
        }
        if (isset($changes['shared'])) {
            $def->setShared($definition->isShared());
        }
        if (isset($changes['decorated_service'])) {
            $decoratedService = $definition->getDecoratedService();
            if (null === $decoratedService) {
                $def->setDecoratedService($decoratedService);
            } else {
                $def->setDecoratedService($decoratedService[0], $decoratedService[1], $decoratedService[2], $decoratedService[3] ?? ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE);
            }
        }

        // merge arguments
        foreach ($definition->getArguments() as $k => $v) {
            if (is_numeric($k)) {
                $def->addArgument($v);
            } elseif (str_starts_with($k, 'index_')) {
                $def->replaceArgument((int) substr($k, \strlen('index_')), $v);
            } else {
                $def->setArgument($k, $v);
            }
        }

        // merge properties
        foreach ($definition->getProperties() as $k => $v) {
            $def->setProperty($k, $v);
        }

        // append method calls
        if ($calls = $definition->getMethodCalls()) {
            $def->setMethodCalls(array_merge($def->getMethodCalls(), $calls));
        }

        $def->addError($parentDef);
        $def->addError($definition);

        // these attributes are always taken from the child
        $def->setAbstract($definition->isAbstract());
        $def->setTags($definition->getTags());
        // autoconfigure is never taken from parent (on purpose)
        // and it's not legal on an instanceof
        $def->setAutoconfigured($definition->isAutoconfigured());

        if (!$def->hasTag('proxy')) {
            foreach ($parentDef->getTag('proxy') as $v) {
                $def->addTag('proxy', $v);
            }
        }

        return $def;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\DependencyInjection\Attribute\AutowireDecorated;
use Symfony\Component\DependencyInjection\Attribute\Target;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\TypedReference;
use Symfony\Contracts\Service\Attribute\Required;

/**
 * Looks for definitions with autowiring enabled and registers their corresponding "#[Required]" properties.
 *
 * @author Sebastien Morel (Plopix) <morel.seb@gmail.com>
 * @author Nicolas Grekas <p@tchwork.com>
 */
class AutowireRequiredPropertiesPass extends AbstractRecursivePass
{
    protected bool $skipScalars = true;

    protected function processValue(mixed $value, bool $isRoot = false): mixed
    {
        $value = parent::processValue($value, $isRoot);

        if (!$value instanceof Definition || !$value->isAutowired() || $value->isAbstract() || !$value->getClass()) {
            return $value;
        }
        if (!$reflectionClass = $this->container->getReflectionClass($value->getClass(), false)) {
            return $value;
        }

        $properties = $value->getProperties();
        foreach ($reflectionClass->getProperties() as $reflectionProperty) {
            if (!($type = $reflectionProperty->getType()) instanceof \ReflectionNamedType) {
                continue;
            }
            $doc = false;
            if (!$reflectionProperty->getAttributes(Required::class)
                && ((false === $doc = $reflectionProperty->getDocComment()) || false === stripos($doc, '@required') || !preg_match('#(?:^/\*\*|\n\s*+\*)\s*+@required(?:\s|\*/$)#i', $doc))
            ) {
                continue;
            }
            if ($doc) {
                trigger_deprecation('symfony/dependency-injection', '6.3', 'Using the "@required" annotation on property "%s::$%s" is deprecated, use the "Symfony\Contracts\Service\Attribute\Required" attribute instead.', $reflectionProperty->class, $reflectionProperty->name);
            }
            if (\array_key_exists($name = $reflectionProperty->getName(), $properties)) {
                continue;
            }

            $type = $type->getName();
            $value->setProperty($name, new TypedReference($type, $type, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, $name, array_map(static fn ($a) => $a->newInstance(), array_merge(
                $reflectionProperty->getAttributes(Autowire::class, \ReflectionAttribute::IS_INSTANCEOF),
                $reflectionProperty->getAttributes(AutowireDecorated::class),
                $reflectionProperty->getAttributes(Target::class),
            ))));
        }

        return $value;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

/**
 * Removes unused service definitions from the container.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 * @author Nicolas Grekas <p@tchwork.com>
 */
class RemoveUnusedDefinitionsPass extends AbstractRecursivePass
{
    protected bool $skipScalars = true;

    private array $connectedIds = [];

    /**
     * Processes the ContainerBuilder to remove unused definitions.
     *
     * @return void
     */
    public function process(ContainerBuilder $container)
    {
        try {
            $this->enableExpressionProcessing();
            $this->container = $container;
            $connectedIds = [];
            $aliases = $container->getAliases();

            foreach ($aliases as $id => $alias) {
                if ($alias->isPublic()) {
                    $this->connectedIds[] = (string) $aliases[$id];
                }
            }

            foreach ($container->getDefinitions() as $id => $definition) {
                if ($definition->isPublic()) {
                    $connectedIds[$id] = true;
                    $this->processValue($definition);
                }
            }

            while ($this->connectedIds) {
                $ids = $this->connectedIds;
                $this->connectedIds = [];
                foreach ($ids as $id) {
                    if (!isset($connectedIds[$id]) && $container->hasDefinition($id)) {
                        $connectedIds[$id] = true;
                        $this->processValue($container->getDefinition($id));
                    }
                }
            }

            foreach ($container->getDefinitions() as $id => $definition) {
                if (!isset($connectedIds[$id])) {
                    $container->removeDefinition($id);
                    $container->resolveEnvPlaceholders(!$definition->hasErrors() ? serialize($definition) : $definition);
                    $container->log($this, \sprintf('Removed service "%s"; reason: unused.', $id));
                }
            }
        } finally {
            $this->container = null;
            $this->connectedIds = [];
        }
    }

    protected function processValue(mixed $value, bool $isRoot = false): mixed
    {
        if (!$value instanceof Reference) {
            return parent::processValue($value, $isRoot);
        }

        if (ContainerBuilder::IGNORE_ON_UNINITIALIZED_REFERENCE !== $value->getInvalidBehavior()) {
            $this->connectedIds[] = (string) $value;
        }

        return $value;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Argument\ArgumentInterface;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\ExpressionLanguage;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\ExpressionLanguage\Expression;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 */
abstract class AbstractRecursivePass implements CompilerPassInterface
{
    /**
     * @var ContainerBuilder
     */
    protected $container;
    protected $currentId;
    protected bool $skipScalars = false;

    private bool $processExpressions = false;
    private ExpressionLanguage $expressionLanguage;
    private bool $inExpression = false;

    /**
     * @return void
     */
    public function process(ContainerBuilder $container)
    {
        $this->container = $container;

        try {
            $this->processValue($container->getDefinitions(), true);
        } finally {
            $this->container = null;
        }
    }

    /**
     * @return void
     */
    protected function enableExpressionProcessing()
    {
        $this->processExpressions = true;
    }

    protected function inExpression(bool $reset = true): bool
    {
        $inExpression = $this->inExpression;
        if ($reset) {
            $this->inExpression = false;
        }

        return $inExpression;
    }

    /**
     * Processes a value found in a definition tree.
     *
     * @return mixed
     */
    protected function processValue(mixed $value, bool $isRoot = false)
    {
        if (\is_array($value)) {
            foreach ($value as $k => $v) {
                if ((!$v || \is_scalar($v)) && $this->skipScalars) {
                    continue;
                }
                if ($isRoot) {
                    if ($v instanceof Definition && $v->hasTag('container.excluded')) {
                        continue;
                    }
                    $this->currentId = $k;
                }
                if ($v !== $processedValue = $this->processValue($v, $isRoot)) {
                    $value[$k] = $processedValue;
                }
            }
        } elseif ($value instanceof ArgumentInterface) {
            $value->setValues($this->processValue($value->getValues()));
        } elseif ($value instanceof Expression && $this->processExpressions) {
            $this->getExpressionLanguage()->compile((string) $value, ['this' => 'container', 'args' => 'args']);
        } elseif ($value instanceof Definition) {
            $value->setArguments($this->processValue($value->getArguments()));
            $value->setProperties($this->processValue($value->getProperties()));
            $value->setMethodCalls($this->processValue($value->getMethodCalls()));

            $changes = $value->getChanges();
            if (isset($changes['factory'])) {
                if (\is_string($factory = $value->getFactory()) && str_starts_with($factory, '@=')) {
                    if (!class_exists(Expression::class)) {
                        throw new LogicException('Expressions cannot be used in service factories without the ExpressionLanguage component. Try running "composer require symfony/expression-language".');
                    }
                    $factory = new Expression(substr($factory, 2));
                }
                if (($factory = $this->processValue($factory)) instanceof Expression) {
                    $factory = '@='.$factory;
                }
                $value->setFactory($factory);
            }
            if (isset($changes['configurator'])) {
                $value->setConfigurator($this->processValue($value->getConfigurator()));
            }
        }

        return $value;
    }

    /**
     * @throws RuntimeException
     */
    protected function getConstructor(Definition $definition, bool $required): ?\ReflectionFunctionAbstract
    {
        if ($definition->isSynthetic()) {
            return null;
        }

        if (\is_string($factory = $definition->getFactory())) {
            if (str_starts_with($factory, '@=')) {
                return new \ReflectionFunction(static function (...$args) {});
            }

            if (!\function_exists($factory)) {
                throw new RuntimeException(\sprintf('Invalid service "%s": function "%s" does not exist.', $this->currentId, $factory));
            }
            $r = new \ReflectionFunction($factory);
            if (false !== $r->getFileName() && file_exists($r->getFileName())) {
                $this->container->fileExists($r->getFileName());
            }

            return $r;
        }

        if ($factory) {
            [$class, $method] = $factory;

            if ('__construct' === $method) {
                throw new RuntimeException(\sprintf('Invalid service "%s": "__construct()" cannot be used as a factory method.', $this->currentId));
            }

            if ($class instanceof Reference) {
                $factoryDefinition = $this->container->findDefinition((string) $class);
                while ((null === $class = $factoryDefinition->getClass()) && $factoryDefinition instanceof ChildDefinition) {
                    $factoryDefinition = $this->container->findDefinition($factoryDefinition->getParent());
                }
            } elseif ($class instanceof Definition) {
                $class = $class->getClass();
            } else {
                $class ??= $definition->getClass();
            }

            return $this->getReflectionMethod(new Definition($class), $method);
        }

        while ((null === $class = $definition->getClass()) && $definition instanceof ChildDefinition) {
            $definition = $this->container->findDefinition($definition->getParent());
        }

        try {
            if (!$r = $this->container->getReflectionClass($class)) {
                if (null === $class) {
                    throw new RuntimeException(\sprintf('Invalid service "%s": the class is not set.', $this->currentId));
                }

                throw new RuntimeException(\sprintf('Invalid service "%s": class "%s" does not exist.', $this->currentId, $class));
            }
        } catch (\ReflectionException $e) {
            throw new RuntimeException(\sprintf('Invalid service "%s": ', $this->currentId).lcfirst($e->getMessage()));
        }
        if (!$r = $r->getConstructor()) {
            if ($required) {
                throw new RuntimeException(\sprintf('Invalid service "%s": class%s has no constructor.', $this->currentId, \sprintf($class !== $this->currentId ? ' "%s"' : '', $class)));
            }
        } elseif (!$r->isPublic()) {
            throw new RuntimeException(\sprintf('Invalid service "%s": ', $this->currentId).\sprintf($class !== $this->currentId ? 'constructor of class "%s"' : 'its constructor', $class).' must be public.');
        }

        return $r;
    }

    /**
     * @throws RuntimeException
     */
    protected function getReflectionMethod(Definition $definition, string $method): \ReflectionFunctionAbstract
    {
        if ('__construct' === $method) {
            return $this->getConstructor($definition, true);
        }

        while ((null === $class = $definition->getClass()) && $definition instanceof ChildDefinition) {
            $definition = $this->container->findDefinition($definition->getParent());
        }

        if (null === $class) {
            throw new RuntimeException(\sprintf('Invalid service "%s": the class is not set.', $this->currentId));
        }

        if (!$r = $this->container->getReflectionClass($class)) {
            throw new RuntimeException(\sprintf('Invalid service "%s": class "%s" does not exist.', $this->currentId, $class));
        }

        if (!$r->hasMethod($method)) {
            if ($r->hasMethod('__call') && ($r = $r->getMethod('__call')) && $r->isPublic()) {
                return new \ReflectionMethod(static function (...$arguments) {}, '__invoke');
            }

            if ($r->hasMethod('__callStatic') && ($r = $r->getMethod('__callStatic')) && $r->isPublic()) {
                return new \ReflectionMethod(static function (...$arguments) {}, '__invoke');
            }

            throw new RuntimeException(\sprintf('Invalid service "%s": method "%s()" does not exist.', $this->currentId, $class !== $this->currentId ? $class.'::'.$method : $method));
        }

        $r = $r->getMethod($method);
        if (!$r->isPublic()) {
            throw new RuntimeException(\sprintf('Invalid service "%s": method "%s()" must be public.', $this->currentId, $class !== $this->currentId ? $class.'::'.$method : $method));
        }

        return $r;
    }

    private function getExpressionLanguage(): ExpressionLanguage
    {
        if (!isset($this->expressionLanguage)) {
            if (!class_exists(ExpressionLanguage::class)) {
                throw new LogicException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed. Try running "composer require symfony/expression-language".');
            }

            $providers = $this->container->getExpressionLanguageProviders();
            $this->expressionLanguage = new ExpressionLanguage(null, $providers, function (string $arg): string {
                if ('""' === substr_replace($arg, '', 1, -1)) {
                    $id = stripcslashes(substr($arg, 1, -1));
                    $this->inExpression = true;
                    $arg = $this->processValue(new Reference($id));
                    $this->inExpression = false;
                    if (!$arg instanceof Reference) {
                        throw new RuntimeException(\sprintf('"%s::processValue()" must return a Reference when processing an expression, "%s" returned for service("%s").', static::class, get_debug_type($arg), $id));
                    }
                    $arg = \sprintf('"%s"', $arg);
                }

                return \sprintf('$this->get(%s)', $arg);
            });
        }

        return $this->expressionLanguage;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ServiceLocator;

/**
 * Applies the "container.service_locator" tag by wrapping references into ServiceClosureArgument instances.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
final class ServiceLocatorTagPass extends AbstractRecursivePass
{
    use PriorityTaggedServiceTrait;

    protected bool $skipScalars = true;

    protected function processValue(mixed $value, bool $isRoot = false): mixed
    {
        if ($value instanceof ServiceLocatorArgument) {
            if ($taggedIterator = $value->getTaggedIteratorArgument()) {
                $exclude = $taggedIterator->getExclude();
                if ($taggedIterator->excludeSelf()) {
                    $exclude[] = $this->currentId;
                }
                $value->setValues($this->findAndSortTaggedServices($taggedIterator, $this->container, $exclude));
            }

            return self::register($this->container, $value->getValues());
        }

        if ($value instanceof Definition) {
            $value->setBindings(parent::processValue($value->getBindings()));
        }

        if (!$value instanceof Definition || !$value->hasTag('container.service_locator')) {
            return parent::processValue($value, $isRoot);
        }

        if (!$value->getClass()) {
            $value->setClass(ServiceLocator::class);
        }

        $services = $value->getArguments()[0] ?? null;

        if ($services instanceof TaggedIteratorArgument) {
            $services = $this->findAndSortTaggedServices($services, $this->container);
        }

        if (!\is_array($services)) {
            throw new InvalidArgumentException(\sprintf('Invalid definition for service "%s": an array of references is expected as first argument when the "container.service_locator" tag is set.', $this->currentId));
        }

        $i = 0;

        foreach ($services as $k => $v) {
            if ($v instanceof ServiceClosureArgument) {
                continue;
            }

            if ($i === $k) {
                if ($v instanceof Reference) {
                    unset($services[$k]);
                    $k = (string) $v;
                }
                ++$i;
            } elseif (\is_int($k)) {
                $i = null;
            }

            $services[$k] = new ServiceClosureArgument($v);
        }
        ksort($services);

        $value->setArgument(0, $services);

        $id = '.service_locator.'.ContainerBuilder::hash($value);

        if ($isRoot) {
            if ($id !== $this->currentId) {
                $this->container->setAlias($id, new Alias($this->currentId, false));
            }

            return $value;
        }

        $this->container->setDefinition($id, $value->setPublic(false));

        return new Reference($id);
    }

    public static function register(ContainerBuilder $container, array $map, ?string $callerId = null): Reference
    {
        foreach ($map as $k => $v) {
            $map[$k] = new ServiceClosureArgument($v);
        }

        $locator = (new Definition(ServiceLocator::class))
            ->addArgument($map)
            ->addTag('container.service_locator');

        if (null !== $callerId && $container->hasDefinition($callerId)) {
            $locator->setBindings($container->getDefinition($callerId)->getBindings());
        }

        if (!$container->hasDefinition($id = '.service_locator.'.ContainerBuilder::hash($locator))) {
            $container->setDefinition($id, $locator);
        }

        if (null !== $callerId) {
            $locatorId = $id;
            // Locators are shared when they hold the exact same list of factories;
            // to have them specialized per consumer service, we use a cloning factory
            // to derivate customized instances from the prototype one.
            $container->register($id .= '.'.$callerId, ServiceLocator::class)
                ->setFactory([new Reference($locatorId), 'withContext'])
                ->addTag('container.service_locator_context', ['id' => $callerId])
                ->addArgument($callerId)
                ->addArgument(new Reference('service_container'));
        }

        return new Reference($id);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;

/**
 * Reads #[Autoconfigure] attributes on definitions that are autoconfigured
 * and don't have the "container.ignore_attributes" tag.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
final class RegisterAutoconfigureAttributesPass implements CompilerPassInterface
{
    private static \Closure $registerForAutoconfiguration;

    public function process(ContainerBuilder $container): void
    {
        foreach ($container->getDefinitions() as $id => $definition) {
            if ($this->accept($definition) && $class = $container->getReflectionClass($definition->getClass(), false)) {
                $this->processClass($container, $class);
            }
        }
    }

    public function accept(Definition $definition): bool
    {
        return $definition->isAutoconfigured() && !$definition->hasTag('container.ignore_attributes');
    }

    public function processClass(ContainerBuilder $container, \ReflectionClass $class): void
    {
        foreach ($class->getAttributes(Autoconfigure::class, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
            self::registerForAutoconfiguration($container, $class, $attribute);
        }
    }

    private static function registerForAutoconfiguration(ContainerBuilder $container, \ReflectionClass $class, \ReflectionAttribute $attribute): void
    {
        if (isset(self::$registerForAutoconfiguration)) {
            (self::$registerForAutoconfiguration)($container, $class, $attribute);

            return;
        }

        $parseDefinitions = new \ReflectionMethod(YamlFileLoader::class, 'parseDefinitions');
        $yamlLoader = $parseDefinitions->getDeclaringClass()->newInstanceWithoutConstructor();

        self::$registerForAutoconfiguration = static function (ContainerBuilder $container, \ReflectionClass $class, \ReflectionAttribute $attribute) use ($parseDefinitions, $yamlLoader) {
            $attribute = (array) $attribute->newInstance();

            foreach ($attribute['tags'] ?? [] as $i => $tag) {
                if (\is_array($tag) && [0] === array_keys($tag)) {
                    $attribute['tags'][$i] = [$class->name => $tag[0]];
                }
            }

            $parseDefinitions->invoke(
                $yamlLoader,
                [
                    'services' => [
                        '_instanceof' => [
                            $class->name => [$container->registerForAutoconfiguration($class->name)] + $attribute,
                        ],
                    ],
                ],
                $class->getFileName(),
                false
            );
        };

        (self::$registerForAutoconfiguration)($container, $class, $attribute);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\EnvVarProcessor;
use Symfony\Component\DependencyInjection\EnvVarProcessorInterface;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag;
use Symfony\Component\DependencyInjection\Reference;

/**
 * Creates the container.env_var_processors_locator service.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
class RegisterEnvVarProcessorsPass implements CompilerPassInterface
{
    private const ALLOWED_TYPES = ['array', 'bool', 'float', 'int', 'string', \BackedEnum::class];

    /**
     * @return void
     */
    public function process(ContainerBuilder $container)
    {
        $bag = $container->getParameterBag();
        $types = [];
        $processors = [];
        foreach ($container->findTaggedServiceIds('container.env_var_processor') as $id => $tags) {
            if (!$r = $container->getReflectionClass($class = $container->getDefinition($id)->getClass())) {
                throw new InvalidArgumentException(\sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id));
            } elseif (!$r->isSubclassOf(EnvVarProcessorInterface::class)) {
                throw new InvalidArgumentException(\sprintf('Service "%s" must implement interface "%s".', $id, EnvVarProcessorInterface::class));
            }
            foreach ($class::getProvidedTypes() as $prefix => $type) {
                $processors[$prefix] = new Reference($id);
                $types[$prefix] = self::validateProvidedTypes($type, $class);
            }
        }

        if ($bag instanceof EnvPlaceholderParameterBag) {
            foreach (EnvVarProcessor::getProvidedTypes() as $prefix => $type) {
                if (!isset($types[$prefix])) {
                    $types[$prefix] = self::validateProvidedTypes($type, EnvVarProcessor::class);
                }
            }
            $bag->setProvidedTypes($types);
        }

        if ($processors) {
            $container->setAlias('container.env_var_processors_locator', (string) ServiceLocatorTagPass::register($container, $processors))
                ->setPublic(true)
            ;
        }
    }

    private static function validateProvidedTypes(string $types, string $class): array
    {
        $types = explode('|', $types);

        foreach ($types as $type) {
            if (!\in_array($type, self::ALLOWED_TYPES)) {
                throw new InvalidArgumentException(\sprintf('Invalid type "%s" returned by "%s::getProvidedTypes()", expected one of "%s".', $type, $class, implode('", "', self::ALLOWED_TYPES)));
            }
        }

        return $types;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\Config\Definition\BaseNode;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Config\Definition\Processor;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\ConfigurationExtensionInterface;
use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;

/**
 * Validates environment variable placeholders used in extension configuration with dummy values.
 *
 * @author Roland Franssen <franssen.roland@gmail.com>
 */
class ValidateEnvPlaceholdersPass implements CompilerPassInterface
{
    private const TYPE_FIXTURES = ['array' => [], 'bool' => false, 'float' => 0.0, 'int' => 0, 'string' => ''];

    private array $extensionConfig = [];

    /**
     * @return void
     */
    public function process(ContainerBuilder $container)
    {
        $this->extensionConfig = [];

        if (!class_exists(BaseNode::class) || !$extensions = $container->getExtensions()) {
            return;
        }

        $resolvingBag = $container->getParameterBag();
        if (!$resolvingBag instanceof EnvPlaceholderParameterBag) {
            return;
        }

        $defaultBag = new ParameterBag($resolvingBag->all());
        $envTypes = $resolvingBag->getProvidedTypes();
        foreach ($resolvingBag->getEnvPlaceholders() + $resolvingBag->getUnusedEnvPlaceholders() as $env => $placeholders) {
            $values = $this->getPlaceholderValues($env, $defaultBag, $envTypes);

            foreach ($placeholders as $placeholder) {
                BaseNode::setPlaceholder($placeholder, $values);
            }
        }

        $processor = new Processor();

        foreach ($extensions as $name => $extension) {
            if (!($extension instanceof ConfigurationExtensionInterface || $extension instanceof ConfigurationInterface)
                || !$config = array_filter($container->getExtensionConfig($name))
            ) {
                // this extension has no semantic configuration or was not called
                continue;
            }

            $config = $resolvingBag->resolveValue($config);

            if ($extension instanceof ConfigurationInterface) {
                $configuration = $extension;
            } elseif (null === $configuration = $extension->getConfiguration($config, $container)) {
                continue;
            }

            $this->extensionConfig[$name] = $processor->processConfiguration($configuration, $config);
        }

        $resolvingBag->clearUnusedEnvPlaceholders();
    }

    /**
     * @internal
     */
    public function getExtensionConfig(): array
    {
        try {
            return $this->extensionConfig;
        } finally {
            $this->extensionConfig = [];
        }
    }

    /**
     * @param array<string, list<string>> $envTypes
     *
     * @return array<string, mixed>
     */
    private function getPlaceholderValues(string $env, ParameterBag $defaultBag, array $envTypes): array
    {
        if (false === $i = strpos($env, ':')) {
            [$default, $defaultType] = $this->getParameterDefaultAndDefaultType("env($env)", $defaultBag);

            return [$defaultType => $default];
        }

        $prefix = substr($env, 0, $i);
        if ('default' === $prefix) {
            $parts = explode(':', $env);
            array_shift($parts); // Remove 'default' prefix
            $parameter = array_shift($parts); // Retrieve and remove parameter

            [$defaultParameter, $defaultParameterType] = $this->getParameterDefaultAndDefaultType($parameter, $defaultBag);

            return [
                $defaultParameterType => $defaultParameter,
                ...$this->getPlaceholderValues(implode(':', $parts), $defaultBag, $envTypes),
            ];
        }

        $values = [];
        foreach ($envTypes[$prefix] ?? ['string'] as $type) {
            $values[$type] = self::TYPE_FIXTURES[$type] ?? null;
        }

        return $values;
    }

    /**
     * @return array{0: string, 1: string}
     */
    private function getParameterDefaultAndDefaultType(string $name, ParameterBag $defaultBag): array
    {
        $default = $defaultBag->has($name) ? $defaultBag->get($name) : self::TYPE_FIXTURES['string'];
        $defaultType = null !== $default ? get_debug_type($default) : 'string';

        return [$default, $defaultType];
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection;

use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;

class Alias
{
    private const DEFAULT_DEPRECATION_TEMPLATE = 'The "%alias_id%" service alias is deprecated. You should stop using it, as it will be removed in the future.';

    private string $id;
    private bool $public;
    private array $deprecation = [];

    public function __construct(string $id, bool $public = false)
    {
        $this->id = $id;
        $this->public = $public;
    }

    /**
     * Checks if this DI Alias should be public or not.
     */
    public function isPublic(): bool
    {
        return $this->public;
    }

    /**
     * Sets if this Alias is public.
     *
     * @return $this
     */
    public function setPublic(bool $boolean): static
    {
        $this->public = $boolean;

        return $this;
    }

    /**
     * Whether this alias is private.
     */
    public function isPrivate(): bool
    {
        return !$this->public;
    }

    /**
     * Whether this alias is deprecated, that means it should not be referenced
     * anymore.
     *
     * @param string $package The name of the composer package that is triggering the deprecation
     * @param string $version The version of the package that introduced the deprecation
     * @param string $message The deprecation message to use
     *
     * @return $this
     *
     * @throws InvalidArgumentException when the message template is invalid
     */
    public function setDeprecated(string $package, string $version, string $message): static
    {
        if ('' !== $message) {
            if (preg_match('#[\r\n]|\*/#', $message)) {
                throw new InvalidArgumentException('Invalid characters found in deprecation template.');
            }

            if (!str_contains($message, '%alias_id%')) {
                throw new InvalidArgumentException('The deprecation template must contain the "%alias_id%" placeholder.');
            }
        }

        $this->deprecation = ['package' => $package, 'version' => $version, 'message' => $message ?: self::DEFAULT_DEPRECATION_TEMPLATE];

        return $this;
    }

    public function isDeprecated(): bool
    {
        return (bool) $this->deprecation;
    }

    /**
     * @param string $id Service id relying on this definition
     */
    public function getDeprecation(string $id): array
    {
        return [
            'package' => $this->deprecation['package'],
            'version' => $this->deprecation['version'],
            'message' => str_replace('%alias_id%', $id, $this->deprecation['message']),
        ];
    }

    public function __toString(): string
    {
        return $this->id;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection;

trigger_deprecation('symfony/dependency-injection', '6.4', '"%s" is deprecated, use dependency injection instead.', ContainerAwareTrait::class);

/**
 * ContainerAware trait.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 *
 * @deprecated since Symfony 6.4, use dependency injection instead
 */
trait ContainerAwareTrait
{
    /**
     * @var ContainerInterface|null
     */
    protected $container;

    /**
     * @return void
     */
    public function setContainer(?ContainerInterface $container = null)
    {
        if (1 > \func_num_args()) {
            trigger_deprecation('symfony/dependency-injection', '6.2', 'Calling "%s::%s()" without any arguments is deprecated, pass null explicitly instead.', __CLASS__, __FUNCTION__);
        }

        $this->container = $container;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Config;

use Symfony\Component\Config\Resource\ResourceInterface;
use Symfony\Component\Config\ResourceCheckerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
 */
class ContainerParametersResourceChecker implements ResourceCheckerInterface
{
    private ContainerInterface $container;

    public function __construct(ContainerInterface $container)
    {
        $this->container = $container;
    }

    public function supports(ResourceInterface $metadata): bool
    {
        return $metadata instanceof ContainerParametersResource;
    }

    public function isFresh(ResourceInterface $resource, int $timestamp): bool
    {
        foreach ($resource->getParameters() as $key => $value) {
            if (!$this->container->hasParameter($key) || $this->container->getParameter($key) !== $value) {
                return false;
            }
        }

        return true;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\Config;

use Symfony\Component\Config\Resource\ResourceInterface;

/**
 * Tracks container parameters.
 *
 * @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
 *
 * @final
 */
class ContainerParametersResource implements ResourceInterface
{
    private array $parameters;

    /**
     * @param array $parameters The container parameters to track
     */
    public function __construct(array $parameters)
    {
        $this->parameters = $parameters;
    }

    public function __toString(): string
    {
        return 'container_parameters_'.hash('xxh128', serialize($this->parameters));
    }

    public function getParameters(): array
    {
        return $this->parameters;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection;

use Psr\Container\ContainerInterface as PsrContainerInterface;
use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException;
use Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;

/**
 * ContainerInterface is the interface implemented by service container classes.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
interface ContainerInterface extends PsrContainerInterface
{
    public const RUNTIME_EXCEPTION_ON_INVALID_REFERENCE = 0;
    public const EXCEPTION_ON_INVALID_REFERENCE = 1;
    public const NULL_ON_INVALID_REFERENCE = 2;
    public const IGNORE_ON_INVALID_REFERENCE = 3;
    public const IGNORE_ON_UNINITIALIZED_REFERENCE = 4;

    /**
     * @return void
     */
    public function set(string $id, ?object $service);

    /**
     * @template B of self::*_REFERENCE
     *
     * @param B $invalidBehavior
     *
     * @psalm-return (B is self::EXCEPTION_ON_INVALID_REFERENCE|self::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE ? object : object|null)
     *
     * @throws ServiceCircularReferenceException When a circular reference is detected
     * @throws ServiceNotFoundException          When the service is not defined
     *
     * @see Reference
     */
    public function get(string $id, int $invalidBehavior = self::EXCEPTION_ON_INVALID_REFERENCE): ?object;

    public function has(string $id): bool;

    /**
     * Check for whether or not a service has been initialized.
     */
    public function initialized(string $id): bool;

    /**
     * @return array|bool|string|int|float|\UnitEnum|null
     *
     * @throws ParameterNotFoundException if the parameter is not defined
     */
    public function getParameter(string $name);

    public function hasParameter(string $name): bool;

    /**
     * @return void
     */
    public function setParameter(string $name, array|bool|string|int|float|\UnitEnum|null $value);
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection;

use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
use Symfony\Contracts\Service\ServiceLocatorTrait;
use Symfony\Contracts\Service\ServiceProviderInterface;
use Symfony\Contracts\Service\ServiceSubscriberInterface;

/**
 * @author Robin Chalas <robin.chalas@gmail.com>
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @template-covariant T of mixed
 *
 * @implements ServiceProviderInterface<T>
 */
class ServiceLocator implements ServiceProviderInterface, \Countable
{
    use ServiceLocatorTrait {
        get as private doGet;
    }

    private ?string $externalId = null;
    private ?Container $container = null;

    public function get(string $id): mixed
    {
        if (!$this->externalId) {
            return $this->doGet($id);
        }

        try {
            return $this->doGet($id);
        } catch (RuntimeException $e) {
            $what = \sprintf('service "%s" required by "%s"', $id, $this->externalId);
            $message = preg_replace('/service "\.service_locator\.[^"]++"/', $what, $e->getMessage());

            if ($e->getMessage() === $message) {
                $message = \sprintf('Cannot resolve %s: %s', $what, $message);
            }

            $r = new \ReflectionProperty($e, 'message');
            $r->setValue($e, $message);

            throw $e;
        }
    }

    /**
     * @return mixed
     */
    public function __invoke(string $id)
    {
        return isset($this->factories[$id]) ? $this->get($id) : null;
    }

    /**
     * @internal
     */
    public function withContext(string $externalId, Container $container): static
    {
        $locator = clone $this;
        $locator->externalId = $externalId;
        $locator->container = $container;

        return $locator;
    }

    public function count(): int
    {
        return \count($this->getProvidedServices());
    }

    private function createNotFoundException(string $id): NotFoundExceptionInterface
    {
        if ($this->loading) {
            $msg = \sprintf('The service "%s" has a dependency on a non-existent service "%s". This locator %s', end($this->loading), $id, $this->formatAlternatives());

            return new ServiceNotFoundException($id, end($this->loading) ?: null, null, [], $msg);
        }

        $class = debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT | \DEBUG_BACKTRACE_IGNORE_ARGS, 4);
        $class = isset($class[3]['object']) ? $class[3]['object']::class : null;
        $externalId = $this->externalId ?: $class;

        $msg = [];
        $msg[] = \sprintf('Service "%s" not found:', $id);

        if (!$this->container) {
            $class = null;
        } elseif ($this->container->has($id) || isset($this->container->getRemovedIds()[$id])) {
            $msg[] = 'even though it exists in the app\'s container,';
        } else {
            try {
                $this->container->get($id);
                $class = null;
            } catch (ServiceNotFoundException $e) {
                if ($e->getAlternatives()) {
                    $msg[] = \sprintf('did you mean %s? Anyway,', $this->formatAlternatives($e->getAlternatives(), 'or'));
                } else {
                    $class = null;
                }
            }
        }
        if ($externalId) {
            $msg[] = \sprintf('the container inside "%s" is a smaller service locator that %s', $externalId, $this->formatAlternatives());
        } else {
            $msg[] = \sprintf('the current service locator %s', $this->formatAlternatives());
        }

        if (!$class) {
            // no-op
        } elseif (is_subclass_of($class, ServiceSubscriberInterface::class)) {
            $msg[] = \sprintf('Unless you need extra laziness, try using dependency injection instead. Otherwise, you need to declare it using "%s::getSubscribedServices()".', preg_replace('/([^\\\\]++\\\\)++/', '', $class));
        } else {
            $msg[] = 'Try using dependency injection instead.';
        }

        return new ServiceNotFoundException($id, end($this->loading) ?: null, null, [], implode(' ', $msg));
    }

    private function createCircularReferenceException(string $id, array $path): ContainerExceptionInterface
    {
        return new ServiceCircularReferenceException($id, $path);
    }

    private function formatAlternatives(?array $alternatives = null, string $separator = 'and'): string
    {
        $format = '"%s"%s';
        if (null === $alternatives) {
            if (!$alternatives = array_keys($this->factories)) {
                return 'is empty...';
            }
            $format = \sprintf('only knows about the %s service%s.', $format, 1 < \count($alternatives) ? 's' : '');
        }
        $last = array_pop($alternatives);

        return \sprintf($format, $alternatives ? implode('", "', $alternatives) : $last, $alternatives ? \sprintf(' %s "%s"', $separator, $last) : '');
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\LazyProxy\PhpDumper;

use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\VarExporter\Exception\LogicException;
use Symfony\Component\VarExporter\ProxyHelper;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 */
final class LazyServiceDumper implements DumperInterface
{
    public function __construct(
        private string $salt = '',
    ) {
    }

    public function isProxyCandidate(Definition $definition, ?bool &$asGhostObject = null, ?string $id = null): bool
    {
        $asGhostObject = false;

        if ($definition->hasTag('proxy')) {
            if (!$definition->isLazy()) {
                throw new InvalidArgumentException(\sprintf('Invalid definition for service "%s": setting the "proxy" tag on a service requires it to be "lazy".', $id ?? $definition->getClass()));
            }

            return true;
        }

        if (!$definition->isLazy()) {
            return false;
        }

        if (!($class = $definition->getClass()) || !(class_exists($class) || interface_exists($class, false))) {
            return false;
        }

        if ($definition->getFactory()) {
            return true;
        }

        foreach ($definition->getMethodCalls() as $call) {
            if ($call[2] ?? false) {
                return true;
            }
        }

        try {
            $asGhostObject = (bool) ProxyHelper::generateLazyGhost(new \ReflectionClass($class));
        } catch (LogicException) {
        }

        return true;
    }

    public function getProxyFactoryCode(Definition $definition, string $id, string $factoryCode): string
    {
        $instantiation = 'return';

        if ($definition->isShared()) {
            $instantiation .= \sprintf(' $container->%s[%s] =', $definition->isPublic() && !$definition->isPrivate() ? 'services' : 'privates', var_export($id, true));
        }

        $asGhostObject = str_contains($factoryCode, '$proxy');
        $proxyClass = $this->getProxyClass($definition, $asGhostObject);

        if (!$asGhostObject) {
            return <<<EOF
                        if (true === \$lazyLoad) {
                            $instantiation \$container->createProxy('$proxyClass', static fn () => \\$proxyClass::createLazyProxy(static fn () => $factoryCode));
                        }


                EOF;
        }

        $factoryCode = \sprintf('static fn ($proxy) => %s', $factoryCode);

        return <<<EOF
                    if (true === \$lazyLoad) {
                        $instantiation \$container->createProxy('$proxyClass', static fn () => \\$proxyClass::createLazyGhost($factoryCode));
                    }


            EOF;
    }

    public function getProxyCode(Definition $definition, ?string $id = null): string
    {
        if (!$this->isProxyCandidate($definition, $asGhostObject, $id)) {
            throw new InvalidArgumentException(\sprintf('Cannot instantiate lazy proxy for service "%s".', $id ?? $definition->getClass()));
        }
        $proxyClass = $this->getProxyClass($definition, $asGhostObject, $class);

        if ($asGhostObject) {
            try {
                return (\PHP_VERSION_ID >= 80200 && $class?->isReadOnly() ? 'readonly ' : '').'class '.$proxyClass.ProxyHelper::generateLazyGhost($class);
            } catch (LogicException $e) {
                throw new InvalidArgumentException(\sprintf('Cannot generate lazy ghost for service "%s".', $id ?? $definition->getClass()), 0, $e);
            }
        }
        $interfaces = [];

        if ($definition->hasTag('proxy')) {
            foreach ($definition->getTag('proxy') as $tag) {
                if (!isset($tag['interface'])) {
                    throw new InvalidArgumentException(\sprintf('Invalid definition for service "%s": the "interface" attribute is missing on a "proxy" tag.', $id ?? $definition->getClass()));
                }
                if (!interface_exists($tag['interface']) && !class_exists($tag['interface'], false)) {
                    throw new InvalidArgumentException(\sprintf('Invalid definition for service "%s": several "proxy" tags found but "%s" is not an interface.', $id ?? $definition->getClass(), $tag['interface']));
                }
                if ('object' !== $definition->getClass() && !is_a($class->name, $tag['interface'], true)) {
                    throw new InvalidArgumentException(\sprintf('Invalid "proxy" tag for service "%s": class "%s" doesn\'t implement "%s".', $id ?? $definition->getClass(), $definition->getClass(), $tag['interface']));
                }
                $interfaces[] = new \ReflectionClass($tag['interface']);
            }

            $class = 1 === \count($interfaces) && !$interfaces[0]->isInterface() ? array_pop($interfaces) : null;
        } elseif ($class->isInterface()) {
            $interfaces = [$class];
            $class = null;
        }

        try {
            return (\PHP_VERSION_ID >= 80200 && $class?->isReadOnly() ? 'readonly ' : '').'class '.$proxyClass.ProxyHelper::generateLazyProxy($class, $interfaces);
        } catch (LogicException $e) {
            throw new InvalidArgumentException(\sprintf('Cannot generate lazy proxy for service "%s".', $id ?? $definition->getClass()), 0, $e);
        }
    }

    public function getProxyClass(Definition $definition, bool $asGhostObject, ?\ReflectionClass &$class = null): string
    {
        $class = 'object' !== $definition->getClass() ? $definition->getClass() : 'stdClass';
        $class = new \ReflectionClass($class);

        return preg_replace('/^.*\\\\/', '', $definition->getClass())
            .($asGhostObject ? 'Ghost' : 'Proxy')
            .ucfirst(substr(hash('sha256', $this->salt.'+'.$class->name.'+'.serialize($definition->getTag('proxy'))), -7));
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\LazyProxy\PhpDumper;

use Symfony\Component\DependencyInjection\Definition;

/**
 * Lazy proxy dumper capable of generating the instantiation logic PHP code for proxied services.
 *
 * @author Marco Pivetta <ocramius@gmail.com>
 */
interface DumperInterface
{
    /**
     * Inspects whether the given definitions should produce proxy instantiation logic in the dumped container.
     *
     * @param bool|null   &$asGhostObject Set to true after the call if the proxy is a ghost object
     * @param string|null $id
     */
    public function isProxyCandidate(Definition $definition/* , ?bool &$asGhostObject = null, ?string $id = null */): bool;

    /**
     * Generates the code to be used to instantiate a proxy in the dumped factory code.
     */
    public function getProxyFactoryCode(Definition $definition, string $id, string $factoryCode): string;

    /**
     * Generates the code for the lazy proxy.
     *
     * @param string|null $id
     */
    public function getProxyCode(Definition $definition/* , ?string $id = null */): string;
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\LazyProxy\PhpDumper;

use Symfony\Component\DependencyInjection\Definition;

/**
 * Null dumper, negates any proxy code generation for any given service definition.
 *
 * @author Marco Pivetta <ocramius@gmail.com>
 *
 * @final
 */
class NullDumper implements DumperInterface
{
    public function isProxyCandidate(Definition $definition, ?bool &$asGhostObject = null, ?string $id = null): bool
    {
        return $asGhostObject = false;
    }

    public function getProxyFactoryCode(Definition $definition, string $id, string $factoryCode): string
    {
        return '';
    }

    public function getProxyCode(Definition $definition, ?string $id = null): string
    {
        return '';
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\LazyProxy\Instantiator;

use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition;

/**
 * Noop proxy instantiator - produces the real service instead of a proxy instance.
 *
 * @author Marco Pivetta <ocramius@gmail.com>
 */
class RealServiceInstantiator implements InstantiatorInterface
{
    public function instantiateProxy(ContainerInterface $container, Definition $definition, string $id, callable $realInstantiator): object
    {
        return $realInstantiator();
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\LazyProxy\Instantiator;

use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition;

/**
 * Lazy proxy instantiator, capable of instantiating a proxy given a container, the
 * service definitions and a callback that produces the real service instance.
 *
 * @author Marco Pivetta <ocramius@gmail.com>
 */
interface InstantiatorInterface
{
    /**
     * Instantiates a proxy object.
     *
     * @param string            $id               Identifier of the requested service
     * @param callable(object=) $realInstantiator A callback that is capable of producing the real service instance
     *
     * @return object
     */
    public function instantiateProxy(ContainerInterface $container, Definition $definition, string $id, callable $realInstantiator);
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\LazyProxy\Instantiator;

use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\LazyProxy\PhpDumper\LazyServiceDumper;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 */
final class LazyServiceInstantiator implements InstantiatorInterface
{
    public function instantiateProxy(ContainerInterface $container, Definition $definition, string $id, callable $realInstantiator): object
    {
        $dumper = new LazyServiceDumper();

        if (!$dumper->isProxyCandidate($definition, $asGhostObject, $id)) {
            throw new InvalidArgumentException(\sprintf('Cannot instantiate lazy proxy for service "%s".', $id));
        }

        if (!class_exists($proxyClass = $dumper->getProxyClass($definition, $asGhostObject), false)) {
            eval($dumper->getProxyCode($definition, $id));
        }

        return $asGhostObject ? $proxyClass::createLazyGhost($realInstantiator) : $proxyClass::createLazyProxy($realInstantiator);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\LazyProxy;

trigger_deprecation('symfony/dependency-injection', '6.2', 'The "%s" class is deprecated, use "%s" instead.', ProxyHelper::class, \Symfony\Component\VarExporter\ProxyHelper::class);

/**
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @deprecated since Symfony 6.2, use VarExporter's ProxyHelper instead
 */
class ProxyHelper
{
    /**
     * @return string|null The FQCN or builtin name of the type hint, or null when the type hint references an invalid self|parent context
     */
    public static function getTypeHint(\ReflectionFunctionAbstract $r, ?\ReflectionParameter $p = null, bool $noBuiltin = false): ?string
    {
        if ($p instanceof \ReflectionParameter) {
            $type = $p->getType();
        } else {
            $type = $r->getReturnType();
        }
        if (!$type) {
            return null;
        }

        return self::getTypeHintForType($type, $r, $noBuiltin);
    }

    private static function getTypeHintForType(\ReflectionType $type, \ReflectionFunctionAbstract $r, bool $noBuiltin): ?string
    {
        $types = [];
        $glue = '|';
        if ($type instanceof \ReflectionUnionType) {
            $reflectionTypes = $type->getTypes();
        } elseif ($type instanceof \ReflectionIntersectionType) {
            $reflectionTypes = $type->getTypes();
            $glue = '&';
        } elseif ($type instanceof \ReflectionNamedType) {
            $reflectionTypes = [$type];
        } else {
            return null;
        }

        foreach ($reflectionTypes as $type) {
            if ($type instanceof \ReflectionIntersectionType) {
                $typeHint = self::getTypeHintForType($type, $r, $noBuiltin);
                if (null === $typeHint) {
                    return null;
                }

                $types[] = \sprintf('(%s)', $typeHint);

                continue;
            }

            if ($type->isBuiltin()) {
                if (!$noBuiltin) {
                    $types[] = $type->getName();
                }
                continue;
            }

            $lcName = strtolower($type->getName());
            $prefix = $noBuiltin ? '' : '\\';

            if ('self' !== $lcName && 'parent' !== $lcName) {
                $types[] = $prefix.$type->getName();
                continue;
            }
            if (!$r instanceof \ReflectionMethod) {
                continue;
            }
            if ('self' === $lcName) {
                $types[] = $prefix.$r->getDeclaringClass()->name;
            } else {
                $types[] = ($parent = $r->getDeclaringClass()->getParentClass()) ? $prefix.$parent->name : null;
            }
        }

        sort($types);

        return $types ? implode($glue, $types) : null;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection;

use Symfony\Component\DependencyInjection\Exception\RuntimeException;

/**
 * The EnvVarProcessorInterface is implemented by objects that manage environment-like variables.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
interface EnvVarProcessorInterface
{
    /**
     * Returns the value of the given variable as managed by the current instance.
     *
     * @param string                  $prefix The namespace of the variable; when the empty string is passed, null values should be kept as is
     * @param string                  $name   The name of the variable within the namespace
     * @param \Closure(string): mixed $getEnv A closure that allows fetching more env vars
     *
     * @throws RuntimeException on error
     */
    public function getEnv(string $prefix, string $name, \Closure $getEnv): mixed;

    /**
     * @return array<string, string> The PHP-types managed by getEnv(), keyed by prefixes
     */
    public static function getProvidedTypes(): array;
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection;

/**
 * Parameter represents a parameter reference.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class Parameter
{
    private string $id;

    public function __construct(string $id)
    {
        $this->id = $id;
    }

    public function __toString(): string
    {
        return $this->id;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection;

use Composer\InstalledVersions;
use Symfony\Component\Config\Resource\ClassExistenceResource;
use Symfony\Component\Config\Resource\ComposerResource;
use Symfony\Component\Config\Resource\DirectoryResource;
use Symfony\Component\Config\Resource\FileExistenceResource;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\Config\Resource\GlobResource;
use Symfony\Component\Config\Resource\ReflectionClassResource;
use Symfony\Component\Config\Resource\ResourceInterface;
use Symfony\Component\DependencyInjection\Argument\AbstractArgument;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\LazyClosure;
use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceLocator;
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\DependencyInjection\Attribute\Target;
use Symfony\Component\DependencyInjection\Compiler\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Symfony\Component\DependencyInjection\Compiler\ResolveEnvPlaceholdersPass;
use Symfony\Component\DependencyInjection\Exception\BadMethodCallException;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
use Symfony\Component\DependencyInjection\Extension\ExtensionInterface;
use Symfony\Component\DependencyInjection\LazyProxy\Instantiator\InstantiatorInterface;
use Symfony\Component\DependencyInjection\LazyProxy\Instantiator\LazyServiceInstantiator;
use Symfony\Component\DependencyInjection\LazyProxy\Instantiator\RealServiceInstantiator;
use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\ExpressionLanguage\Expression;
use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface;

/**
 * ContainerBuilder is a DI container that provides an API to easily describe services.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class ContainerBuilder extends Container implements TaggedContainerInterface
{
    /**
     * Regexp to match anonymous service IDs generated by the DI loaders.
     */
    public const ANONYMOUS_ID_REGEXP = '/^\.\d+_[^~]*+~[._a-zA-Z\d]{7}$/';

    /**
     * @var array<string, ExtensionInterface>
     */
    private array $extensions = [];

    /**
     * @var array<string, ExtensionInterface>
     */
    private array $extensionsByNs = [];

    /**
     * @var array<string, Definition>
     */
    private array $definitions = [];

    /**
     * @var array<string, Alias>
     */
    private array $aliasDefinitions = [];

    /**
     * @var array<string, ResourceInterface>
     */
    private array $resources = [];

    /**
     * @var array<string, array<array<string, mixed>>>
     */
    private array $extensionConfigs = [];

    private Compiler $compiler;
    private bool $trackResources;
    private InstantiatorInterface $proxyInstantiator;
    private ExpressionLanguage $expressionLanguage;

    /**
     * @var ExpressionFunctionProviderInterface[]
     */
    private array $expressionLanguageProviders = [];

    /**
     * @var string[] with tag names used by findTaggedServiceIds
     */
    private array $usedTags = [];

    /**
     * @var string[][] a map of env var names to their placeholders
     */
    private array $envPlaceholders = [];

    /**
     * @var int[] a map of env vars to their resolution counter
     */
    private array $envCounters = [];

    /**
     * @var string[] the list of vendor directories
     */
    private array $vendors;

    /**
     * @var array<string, bool> the cache for paths being in vendor directories
     */
    private array $pathsInVendor = [];

    /**
     * @var array<string, ChildDefinition>
     */
    private array $autoconfiguredInstanceof = [];

    /**
     * @var array<string, callable>
     */
    private array $autoconfiguredAttributes = [];

    /**
     * @var array<string, bool>
     */
    private array $removedIds = [];

    /**
     * @var array<int, bool>
     */
    private array $removedBindingIds = [];

    private const INTERNAL_TYPES = [
        'int' => true,
        'float' => true,
        'string' => true,
        'bool' => true,
        'resource' => true,
        'object' => true,
        'array' => true,
        'null' => true,
        'callable' => true,
        'iterable' => true,
        'mixed' => true,
    ];

    public function __construct(?ParameterBagInterface $parameterBag = null)
    {
        parent::__construct($parameterBag);

        $this->trackResources = interface_exists(ResourceInterface::class);
        $this->setDefinition('service_container', (new Definition(ContainerInterface::class))->setSynthetic(true)->setPublic(true));
    }

    /**
     * @var array<string, \ReflectionClass>
     */
    private array $classReflectors;

    /**
     * Sets the track resources flag.
     *
     * If you are not using the loaders and therefore don't want
     * to depend on the Config component, set this flag to false.
     *
     * @return void
     */
    public function setResourceTracking(bool $track)
    {
        $this->trackResources = $track;
    }

    /**
     * Checks if resources are tracked.
     */
    public function isTrackingResources(): bool
    {
        return $this->trackResources;
    }

    /**
     * Sets the instantiator to be used when fetching proxies.
     *
     * @return void
     */
    public function setProxyInstantiator(InstantiatorInterface $proxyInstantiator)
    {
        $this->proxyInstantiator = $proxyInstantiator;
    }

    /**
     * @return void
     */
    public function registerExtension(ExtensionInterface $extension)
    {
        $this->extensions[$extension->getAlias()] = $extension;

        if (false !== $extension->getNamespace()) {
            $this->extensionsByNs[$extension->getNamespace() ?? ''] = $extension;
        }
    }

    /**
     * Returns an extension by alias or namespace.
     *
     * @throws LogicException if the extension is not registered
     */
    public function getExtension(string $name): ExtensionInterface
    {
        if (isset($this->extensions[$name])) {
            return $this->extensions[$name];
        }

        if (isset($this->extensionsByNs[$name])) {
            return $this->extensionsByNs[$name];
        }

        throw new LogicException(\sprintf('Container extension "%s" is not registered.', $name));
    }

    /**
     * Returns all registered extensions.
     *
     * @return array<string, ExtensionInterface>
     */
    public function getExtensions(): array
    {
        return $this->extensions;
    }

    /**
     * Checks if we have an extension.
     */
    public function hasExtension(string $name): bool
    {
        return isset($this->extensions[$name]) || isset($this->extensionsByNs[$name]);
    }

    /**
     * Returns an array of resources loaded to build this configuration.
     *
     * @return ResourceInterface[]
     */
    public function getResources(): array
    {
        return array_values($this->resources);
    }

    /**
     * @return $this
     */
    public function addResource(ResourceInterface $resource): static
    {
        if (!$this->trackResources) {
            return $this;
        }

        if ($resource instanceof GlobResource && $this->inVendors($resource->getPrefix())) {
            return $this;
        }

        $this->resources[(string) $resource] = $resource;

        return $this;
    }

    /**
     * Sets the resources for this configuration.
     *
     * @param array<string, ResourceInterface> $resources
     *
     * @return $this
     */
    public function setResources(array $resources): static
    {
        if (!$this->trackResources) {
            return $this;
        }

        $this->resources = $resources;

        return $this;
    }

    /**
     * Adds the object class hierarchy as resources.
     *
     * @param object|string $object An object instance or class name
     *
     * @return $this
     */
    public function addObjectResource(object|string $object): static
    {
        if ($this->trackResources) {
            if (\is_object($object)) {
                $object = $object::class;
            }
            if (!isset($this->classReflectors[$object])) {
                $this->classReflectors[$object] = new \ReflectionClass($object);
            }
            $class = $this->classReflectors[$object];

            foreach ($class->getInterfaceNames() as $name) {
                if (null === $interface = &$this->classReflectors[$name]) {
                    $interface = new \ReflectionClass($name);
                }
                $file = $interface->getFileName();
                if (false !== $file && file_exists($file)) {
                    $this->fileExists($file);
                }
            }
            do {
                $file = $class->getFileName();
                if (false !== $file && file_exists($file)) {
                    $this->fileExists($file);
                }
                foreach ($class->getTraitNames() as $name) {
                    $this->addObjectResource($name);
                }
            } while ($class = $class->getParentClass());
        }

        return $this;
    }

    /**
     * Retrieves the requested reflection class and registers it for resource tracking.
     *
     * @throws \ReflectionException when a parent class/interface/trait is not found and $throw is true
     *
     * @final
     */
    public function getReflectionClass(?string $class, bool $throw = true): ?\ReflectionClass
    {
        if (!$class = $this->getParameterBag()->resolveValue($class)) {
            return null;
        }

        if (isset(self::INTERNAL_TYPES[$class])) {
            return null;
        }

        $resource = $classReflector = null;

        try {
            if (isset($this->classReflectors[$class])) {
                $classReflector = $this->classReflectors[$class];
            } elseif (class_exists(ClassExistenceResource::class)) {
                $resource = new ClassExistenceResource($class, false);
                $classReflector = $resource->isFresh(0) ? false : new \ReflectionClass($class);
            } else {
                $classReflector = class_exists($class) || interface_exists($class, false) ? new \ReflectionClass($class) : false;
            }
        } catch (\ReflectionException $e) {
            if ($throw) {
                throw $e;
            }
        }

        if ($this->trackResources) {
            if (!$classReflector) {
                $this->addResource($resource ?? new ClassExistenceResource($class, false));
            } elseif (!$classReflector->isInternal()) {
                $path = $classReflector->getFileName();

                if (!$this->inVendors($path)) {
                    $this->addResource(new ReflectionClassResource($classReflector, $this->vendors));
                }
            }
            $this->classReflectors[$class] = $classReflector;
        }

        return $classReflector ?: null;
    }

    /**
     * Checks whether the requested file or directory exists and registers the result for resource tracking.
     *
     * @param string      $path          The file or directory path for which to check the existence
     * @param bool|string $trackContents Whether to track contents of the given resource. If a string is passed,
     *                                   it will be used as pattern for tracking contents of the requested directory
     *
     * @final
     */
    public function fileExists(string $path, bool|string $trackContents = true): bool
    {
        $exists = file_exists($path);

        if (!$this->trackResources || $this->inVendors($path)) {
            return $exists;
        }

        if (!$exists) {
            $this->addResource(new FileExistenceResource($path));

            return $exists;
        }

        if (is_dir($path)) {
            if ($trackContents) {
                $this->addResource(new DirectoryResource($path, \is_string($trackContents) ? $trackContents : null));
            } else {
                $this->addResource(new GlobResource($path, '/*', false));
            }
        } elseif ($trackContents) {
            $this->addResource(new FileResource($path));
        }

        return $exists;
    }

    /**
     * Loads the configuration for an extension.
     *
     * @param string                    $extension The extension alias or namespace
     * @param array<string, mixed>|null $values    An array of values that customizes the extension
     *
     * @return $this
     *
     * @throws BadMethodCallException When this ContainerBuilder is compiled
     * @throws \LogicException        if the extension is not registered
     */
    public function loadFromExtension(string $extension, ?array $values = null): static
    {
        if ($this->isCompiled()) {
            throw new BadMethodCallException('Cannot load from an extension on a compiled container.');
        }

        $namespace = $this->getExtension($extension)->getAlias();

        $this->extensionConfigs[$namespace][] = $values ?? [];

        return $this;
    }

    /**
     * Adds a compiler pass.
     *
     * @param string $type     The type of compiler pass
     * @param int    $priority Used to sort the passes
     *
     * @return $this
     */
    public function addCompilerPass(CompilerPassInterface $pass, string $type = PassConfig::TYPE_BEFORE_OPTIMIZATION, int $priority = 0): static
    {
        $this->getCompiler()->addPass($pass, $type, $priority);

        $this->addObjectResource($pass);

        return $this;
    }

    /**
     * Returns the compiler pass config which can then be modified.
     */
    public function getCompilerPassConfig(): PassConfig
    {
        return $this->getCompiler()->getPassConfig();
    }

    /**
     * Returns the compiler.
     */
    public function getCompiler(): Compiler
    {
        return $this->compiler ??= new Compiler();
    }

    /**
     * Sets a service.
     *
     * @return void
     *
     * @throws BadMethodCallException When this ContainerBuilder is compiled
     */
    public function set(string $id, ?object $service)
    {
        if ($this->isCompiled() && (isset($this->definitions[$id]) && !$this->definitions[$id]->isSynthetic())) {
            // setting a synthetic service on a compiled container is alright
            throw new BadMethodCallException(\sprintf('Setting service "%s" for an unknown or non-synthetic service definition on a compiled container is not allowed.', $id));
        }

        unset($this->definitions[$id], $this->aliasDefinitions[$id], $this->removedIds[$id]);

        parent::set($id, $service);
    }

    /**
     * Removes a service definition.
     *
     * @return void
     */
    public function removeDefinition(string $id)
    {
        if (isset($this->definitions[$id])) {
            unset($this->definitions[$id]);
            $this->removedIds[$id] = true;
        }
    }

    public function has(string $id): bool
    {
        return isset($this->definitions[$id]) || isset($this->aliasDefinitions[$id]) || parent::has($id);
    }

    /**
     * @throws InvalidArgumentException          when no definitions are available
     * @throws ServiceCircularReferenceException When a circular reference is detected
     * @throws ServiceNotFoundException          When the service is not defined
     * @throws \Exception
     *
     * @see Reference
     */
    public function get(string $id, int $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE): ?object
    {
        if ($this->isCompiled() && isset($this->removedIds[$id])) {
            return ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE >= $invalidBehavior ? parent::get($id) : null;
        }

        return $this->doGet($id, $invalidBehavior);
    }

    private function doGet(string $id, int $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, ?array &$inlineServices = null, bool $isConstructorArgument = false): mixed
    {
        if (isset($inlineServices[$id])) {
            return $inlineServices[$id];
        }
        if (null === $inlineServices) {
            $isConstructorArgument = true;
            $inlineServices = [];
        }
        try {
            if (ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE === $invalidBehavior) {
                return $this->privates[$id] ?? parent::get($id, $invalidBehavior);
            }
            if (null !== $service = $this->privates[$id] ?? parent::get($id, ContainerInterface::NULL_ON_INVALID_REFERENCE)) {
                return $service;
            }
        } catch (ServiceCircularReferenceException $e) {
            if ($isConstructorArgument) {
                throw $e;
            }
        }

        if (!isset($this->definitions[$id]) && isset($this->aliasDefinitions[$id])) {
            $alias = $this->aliasDefinitions[$id];

            if ($alias->isDeprecated()) {
                $deprecation = $alias->getDeprecation($id);
                trigger_deprecation($deprecation['package'], $deprecation['version'], $deprecation['message']);
            }

            return $this->doGet((string) $alias, $invalidBehavior, $inlineServices, $isConstructorArgument);
        }

        try {
            $definition = $this->getDefinition($id);
        } catch (ServiceNotFoundException $e) {
            if (ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE < $invalidBehavior) {
                return null;
            }

            throw $e;
        }

        if ($definition->hasErrors() && $e = $definition->getErrors()) {
            throw new RuntimeException(reset($e));
        }

        if ($isConstructorArgument) {
            $this->loading[$id] = true;
        }

        try {
            return $this->createService($definition, $inlineServices, $isConstructorArgument, $id);
        } finally {
            if ($isConstructorArgument) {
                unset($this->loading[$id]);
            }
        }
    }

    /**
     * Merges a ContainerBuilder with the current ContainerBuilder configuration.
     *
     * Service definitions overrides the current defined ones.
     *
     * But for parameters, they are overridden by the current ones. It allows
     * the parameters passed to the container constructor to have precedence
     * over the loaded ones.
     *
     *     $container = new ContainerBuilder(new ParameterBag(['foo' => 'bar']));
     *     $loader = new LoaderXXX($container);
     *     $loader->load('resource_name');
     *     $container->register('foo', 'stdClass');
     *
     * In the above example, even if the loaded resource defines a foo
     * parameter, the value will still be 'bar' as defined in the ContainerBuilder
     * constructor.
     *
     * @return void
     *
     * @throws BadMethodCallException When this ContainerBuilder is compiled
     */
    public function merge(self $container)
    {
        if ($this->isCompiled()) {
            throw new BadMethodCallException('Cannot merge on a compiled container.');
        }

        foreach ($container->getDefinitions() as $id => $definition) {
            if (!$definition->hasTag('container.excluded') || !$this->has($id)) {
                $this->setDefinition($id, $definition);
            }
        }
        $this->addAliases($container->getAliases());
        $parameterBag = $this->getParameterBag();
        $otherBag = $container->getParameterBag();
        $parameterBag->add($otherBag->all());

        if ($parameterBag instanceof ParameterBag && $otherBag instanceof ParameterBag) {
            foreach ($otherBag->allDeprecated() as $name => $deprecated) {
                $parameterBag->deprecate($name, ...$deprecated);
            }
        }

        if ($this->trackResources) {
            foreach ($container->getResources() as $resource) {
                $this->addResource($resource);
            }
        }

        foreach ($this->extensions as $name => $extension) {
            if (!isset($this->extensionConfigs[$name])) {
                $this->extensionConfigs[$name] = [];
            }

            $this->extensionConfigs[$name] = array_merge($this->extensionConfigs[$name], $container->getExtensionConfig($name));
        }

        if ($parameterBag instanceof EnvPlaceholderParameterBag && $otherBag instanceof EnvPlaceholderParameterBag) {
            $envPlaceholders = $otherBag->getEnvPlaceholders();
            $parameterBag->mergeEnvPlaceholders($otherBag);
        } else {
            $envPlaceholders = [];
        }

        foreach ($container->envCounters as $env => $count) {
            if (!$count && !isset($envPlaceholders[$env])) {
                continue;
            }
            if (!isset($this->envCounters[$env])) {
                $this->envCounters[$env] = $count;
            } else {
                $this->envCounters[$env] += $count;
            }
        }

        foreach ($container->getAutoconfiguredInstanceof() as $interface => $childDefinition) {
            if (isset($this->autoconfiguredInstanceof[$interface])) {
                throw new InvalidArgumentException(\sprintf('"%s" has already been autoconfigured and merge() does not support merging autoconfiguration for the same class/interface.', $interface));
            }

            $this->autoconfiguredInstanceof[$interface] = $childDefinition;
        }

        foreach ($container->getAutoconfiguredAttributes() as $attribute => $configurator) {
            if (isset($this->autoconfiguredAttributes[$attribute])) {
                throw new InvalidArgumentException(\sprintf('"%s" has already been autoconfigured and merge() does not support merging autoconfiguration for the same attribute.', $attribute));
            }

            $this->autoconfiguredAttributes[$attribute] = $configurator;
        }
    }

    /**
     * Returns the configuration array for the given extension.
     *
     * @return array<array<string, mixed>>
     */
    public function getExtensionConfig(string $name): array
    {
        if (!isset($this->extensionConfigs[$name])) {
            $this->extensionConfigs[$name] = [];
        }

        return $this->extensionConfigs[$name];
    }

    /**
     * Prepends a config array to the configs of the given extension.
     *
     * @param array<string, mixed> $config
     *
     * @return void
     */
    public function prependExtensionConfig(string $name, array $config)
    {
        if (!isset($this->extensionConfigs[$name])) {
            $this->extensionConfigs[$name] = [];
        }

        array_unshift($this->extensionConfigs[$name], $config);
    }

    /**
     * Deprecates a service container parameter.
     *
     * @throws ParameterNotFoundException if the parameter is not defined
     */
    public function deprecateParameter(string $name, string $package, string $version, string $message = 'The parameter "%s" is deprecated.'): void
    {
        if (!$this->parameterBag instanceof ParameterBag) {
            throw new BadMethodCallException(\sprintf('The parameter bag must be an instance of "%s" to call "%s".', ParameterBag::class, __METHOD__));
        }

        $this->parameterBag->deprecate($name, $package, $version, $message);
    }

    /**
     * Compiles the container.
     *
     * This method passes the container to compiler
     * passes whose job is to manipulate and optimize
     * the container.
     *
     * The main compiler passes roughly do four things:
     *
     *  * The extension configurations are merged;
     *  * Parameter values are resolved;
     *  * The parameter bag is frozen;
     *  * Extension loading is disabled.
     *
     * @param bool $resolveEnvPlaceholders Whether %env()% parameters should be resolved at build time using
     *                                     the current env var values (true), or be resolved at runtime based
     *                                     on the environment (false). In general, this should be set to "true"
     *                                     when you want to use the current ContainerBuilder directly, and to
     *                                     "false" when the container is dumped instead.
     *
     * @return void
     */
    public function compile(bool $resolveEnvPlaceholders = false)
    {
        $compiler = $this->getCompiler();

        if ($this->trackResources) {
            foreach ($compiler->getPassConfig()->getPasses() as $pass) {
                $this->addObjectResource($pass);
            }
        }
        $bag = $this->getParameterBag();

        if ($resolveEnvPlaceholders && $bag instanceof EnvPlaceholderParameterBag) {
            $compiler->addPass(new ResolveEnvPlaceholdersPass(), PassConfig::TYPE_AFTER_REMOVING, -1000);
        }

        $compiler->compile($this);

        foreach ($this->definitions as $id => $definition) {
            if ($this->trackResources && $definition->isLazy()) {
                $this->getReflectionClass($definition->getClass());
            }
        }

        $this->extensionConfigs = [];

        if ($bag instanceof EnvPlaceholderParameterBag) {
            if ($resolveEnvPlaceholders) {
                $this->parameterBag = new ParameterBag($this->resolveEnvPlaceholders($this->escapeParameters($bag->all()), true));
            }

            $this->envPlaceholders = $bag->getEnvPlaceholders();
        }

        parent::compile();

        foreach ($this->definitions + $this->aliasDefinitions as $id => $definition) {
            if (!$definition->isPublic() || $definition->isPrivate()) {
                $this->removedIds[$id] = true;
            }
        }
    }

    public function getServiceIds(): array
    {
        return array_map('strval', array_unique(array_merge(array_keys($this->getDefinitions()), array_keys($this->aliasDefinitions), parent::getServiceIds())));
    }

    /**
     * Gets removed service or alias ids.
     *
     * @return array<string, bool>
     */
    public function getRemovedIds(): array
    {
        return $this->removedIds;
    }

    /**
     * Adds the service aliases.
     *
     * @param array<string, string|Alias> $aliases
     *
     * @return void
     */
    public function addAliases(array $aliases)
    {
        foreach ($aliases as $alias => $id) {
            $this->setAlias($alias, $id);
        }
    }

    /**
     * Sets the service aliases.
     *
     * @param array<string, string|Alias> $aliases
     *
     * @return void
     */
    public function setAliases(array $aliases)
    {
        $this->aliasDefinitions = [];
        $this->addAliases($aliases);
    }

    /**
     * Sets an alias for an existing service.
     *
     * @throws InvalidArgumentException if the id is not a string or an Alias
     * @throws InvalidArgumentException if the alias is for itself
     */
    public function setAlias(string $alias, string|Alias $id): Alias
    {
        if ('' === $alias || '\\' === $alias[-1] || \strlen($alias) !== strcspn($alias, "\0\r\n'")) {
            throw new InvalidArgumentException(\sprintf('Invalid alias id: "%s".', $alias));
        }

        if (\is_string($id)) {
            $id = new Alias($id);
        }

        if ($alias === (string) $id) {
            throw new InvalidArgumentException(\sprintf('An alias cannot reference itself, got a circular reference on "%s".', $alias));
        }

        unset($this->definitions[$alias], $this->removedIds[$alias]);

        return $this->aliasDefinitions[$alias] = $id;
    }

    /**
     * @return void
     */
    public function removeAlias(string $alias)
    {
        if (isset($this->aliasDefinitions[$alias])) {
            unset($this->aliasDefinitions[$alias]);
            $this->removedIds[$alias] = true;
        }
    }

    public function hasAlias(string $id): bool
    {
        return isset($this->aliasDefinitions[$id]);
    }

    /**
     * @return array<string, Alias>
     */
    public function getAliases(): array
    {
        return $this->aliasDefinitions;
    }

    /**
     * @throws InvalidArgumentException if the alias does not exist
     */
    public function getAlias(string $id): Alias
    {
        if (!isset($this->aliasDefinitions[$id])) {
            throw new InvalidArgumentException(\sprintf('The service alias "%s" does not exist.', $id));
        }

        return $this->aliasDefinitions[$id];
    }

    /**
     * Registers a service definition.
     *
     * This methods allows for simple registration of service definition
     * with a fluid interface.
     */
    public function register(string $id, ?string $class = null): Definition
    {
        return $this->setDefinition($id, new Definition($class));
    }

    /**
     * Registers an autowired service definition.
     *
     * This method implements a shortcut for using setDefinition() with
     * an autowired definition.
     */
    public function autowire(string $id, ?string $class = null): Definition
    {
        return $this->setDefinition($id, (new Definition($class))->setAutowired(true));
    }

    /**
     * Adds the service definitions.
     *
     * @param array<string, Definition> $definitions
     *
     * @return void
     */
    public function addDefinitions(array $definitions)
    {
        foreach ($definitions as $id => $definition) {
            $this->setDefinition($id, $definition);
        }
    }

    /**
     * Sets the service definitions.
     *
     * @param array<string, Definition> $definitions
     *
     * @return void
     */
    public function setDefinitions(array $definitions)
    {
        $this->definitions = [];
        $this->addDefinitions($definitions);
    }

    /**
     * Gets all service definitions.
     *
     * @return array<string, Definition>
     */
    public function getDefinitions(): array
    {
        return $this->definitions;
    }

    /**
     * Sets a service definition.
     *
     * @throws BadMethodCallException When this ContainerBuilder is compiled
     */
    public function setDefinition(string $id, Definition $definition): Definition
    {
        if ($this->isCompiled()) {
            throw new BadMethodCallException('Adding definition to a compiled container is not allowed.');
        }

        if ('' === $id || '\\' === $id[-1] || \strlen($id) !== strcspn($id, "\0\r\n'")) {
            throw new InvalidArgumentException(\sprintf('Invalid service id: "%s".', $id));
        }

        unset($this->aliasDefinitions[$id], $this->removedIds[$id]);

        return $this->definitions[$id] = $definition;
    }

    /**
     * Returns true if a service definition exists under the given identifier.
     */
    public function hasDefinition(string $id): bool
    {
        return isset($this->definitions[$id]);
    }

    /**
     * Gets a service definition.
     *
     * @throws ServiceNotFoundException if the service definition does not exist
     */
    public function getDefinition(string $id): Definition
    {
        if (!isset($this->definitions[$id])) {
            throw new ServiceNotFoundException($id);
        }

        return $this->definitions[$id];
    }

    /**
     * Gets a service definition by id or alias.
     *
     * The method "unaliases" recursively to return a Definition instance.
     *
     * @throws ServiceNotFoundException if the service definition does not exist
     */
    public function findDefinition(string $id): Definition
    {
        $seen = [];
        while (isset($this->aliasDefinitions[$id])) {
            $id = (string) $this->aliasDefinitions[$id];

            if (isset($seen[$id])) {
                $seen = array_values($seen);
                $seen = \array_slice($seen, array_search($id, $seen));
                $seen[] = $id;

                throw new ServiceCircularReferenceException($id, $seen);
            }

            $seen[$id] = $id;
        }

        return $this->getDefinition($id);
    }

    /**
     * Creates a service for a service definition.
     *
     * @throws RuntimeException         When the factory definition is incomplete
     * @throws RuntimeException         When the service is a synthetic service
     * @throws InvalidArgumentException When configure callable is not callable
     */
    private function createService(Definition $definition, array &$inlineServices, bool $isConstructorArgument = false, ?string $id = null, bool|object $tryProxy = true): mixed
    {
        if (null === $id && isset($inlineServices[$h = spl_object_hash($definition)])) {
            return $inlineServices[$h];
        }

        if ($definition instanceof ChildDefinition) {
            throw new RuntimeException(\sprintf('Constructing service "%s" from a parent definition is not supported at build time.', $id));
        }

        if ($definition->isSynthetic()) {
            throw new RuntimeException(\sprintf('You have requested a synthetic service ("%s"). The DIC does not know how to construct this service.', $id));
        }

        if ($definition->isDeprecated()) {
            $deprecation = $definition->getDeprecation($id);
            trigger_deprecation($deprecation['package'], $deprecation['version'], $deprecation['message']);
        }

        $parameterBag = $this->getParameterBag();
        $class = $parameterBag->resolveValue($definition->getClass()) ?: (['Closure', 'fromCallable'] === $definition->getFactory() ? 'Closure' : null);

        if (['Closure', 'fromCallable'] === $definition->getFactory() && ('Closure' !== $class || $definition->isLazy())) {
            $callable = $parameterBag->unescapeValue($parameterBag->resolveValue($definition->getArgument(0)));

            if ($callable instanceof Reference || $callable instanceof Definition) {
                $callable = [$callable, '__invoke'];
            }

            if (\is_array($callable) && (
                'Closure' !== $class
                || $callable[0] instanceof Reference
                || $callable[0] instanceof Definition && !isset($inlineServices[spl_object_hash($callable[0])])
            )) {
                $initializer = function () use ($callable, &$inlineServices) {
                    return $this->doResolveServices($callable[0], $inlineServices);
                };

                $proxy = eval('return '.LazyClosure::getCode('$initializer', $callable, $class, $this, $id).';');
                $this->shareService($definition, $proxy, $id, $inlineServices);

                return $proxy;
            }
        }

        if (true === $tryProxy && $definition->isLazy() && ['Closure', 'fromCallable'] !== $definition->getFactory()
            && !$tryProxy = !($proxy = $this->proxyInstantiator ??= new LazyServiceInstantiator()) || $proxy instanceof RealServiceInstantiator
        ) {
            $proxy = $proxy->instantiateProxy(
                $this,
                (clone $definition)
                    ->setClass($class)
                    ->setTags(($definition->hasTag('proxy') ? ['proxy' => $parameterBag->resolveValue($definition->getTag('proxy'))] : []) + $definition->getTags()),
                $id, function ($proxy = false) use ($definition, &$inlineServices, $id) {
                    return $this->createService($definition, $inlineServices, true, $id, $proxy);
                }
            );
            $this->shareService($definition, $proxy, $id, $inlineServices);

            return $proxy;
        }

        if (null !== $definition->getFile()) {
            require_once $parameterBag->resolveValue($definition->getFile());
        }

        $arguments = $definition->getArguments();

        if (null !== $factory = $definition->getFactory()) {
            if (\is_array($factory)) {
                $factory = [$this->doResolveServices($parameterBag->resolveValue($factory[0]), $inlineServices, $isConstructorArgument), $factory[1]];
            } elseif (!\is_string($factory)) {
                throw new RuntimeException(\sprintf('Cannot create service "%s" because of invalid factory.', $id));
            } elseif (str_starts_with($factory, '@=')) {
                $factory = fn (ServiceLocator $arguments) => $this->getExpressionLanguage()->evaluate(substr($factory, 2), ['container' => $this, 'args' => $arguments]);
                $arguments = [new ServiceLocatorArgument($arguments)];
            }
        }

        $arguments = $this->doResolveServices($parameterBag->unescapeValue($parameterBag->resolveValue($arguments)), $inlineServices, $isConstructorArgument);

        if (null !== $id && $definition->isShared() && (isset($this->services[$id]) || isset($this->privates[$id])) && (true === $tryProxy || !$definition->isLazy())) {
            return $this->services[$id] ?? $this->privates[$id];
        }

        if (!array_is_list($arguments)) {
            $arguments = array_combine(array_map(static fn ($k) => preg_replace('/^.*\\$/', '', $k), array_keys($arguments)), $arguments);
        }

        if (null !== $factory) {
            $service = $factory(...$arguments);

            if (!$definition->isDeprecated() && \is_array($factory) && \is_string($factory[0])) {
                $r = new \ReflectionClass($factory[0]);

                if (0 < strpos($r->getDocComment() ?: '', "\n * @deprecated ")) {
                    trigger_deprecation('', '', 'The "%s" service relies on the deprecated "%s" factory class. It should either be deprecated or its factory upgraded.', $id, $r->name);
                }
            }
        } else {
            $r = new \ReflectionClass($class);

            if (\is_object($tryProxy)) {
                if ($r->getConstructor()) {
                    $tryProxy->__construct(...$arguments);
                }

                $service = $tryProxy;
            } else {
                $service = $r->getConstructor() ? $r->newInstanceArgs($arguments) : $r->newInstance();
            }

            if (!$definition->isDeprecated() && 0 < strpos($r->getDocComment() ?: '', "\n * @deprecated ")) {
                trigger_deprecation('', '', 'The "%s" service relies on the deprecated "%s" class. It should either be deprecated or its implementation upgraded.', $id, $r->name);
            }
        }

        $lastWitherIndex = null;
        foreach ($definition->getMethodCalls() as $k => $call) {
            if ($call[2] ?? false) {
                $lastWitherIndex = $k;
            }
        }

        if (null === $lastWitherIndex && (true === $tryProxy || !$definition->isLazy())) {
            // share only if proxying failed, or if not a proxy, and if no withers are found
            $this->shareService($definition, $service, $id, $inlineServices);
        }

        $properties = $this->doResolveServices($parameterBag->unescapeValue($parameterBag->resolveValue($definition->getProperties())), $inlineServices);
        foreach ($properties as $name => $value) {
            $service->$name = $value;
        }

        foreach ($definition->getMethodCalls() as $k => $call) {
            $service = $this->callMethod($service, $call, $inlineServices);

            if ($lastWitherIndex === $k && (true === $tryProxy || !$definition->isLazy())) {
                // share only if proxying failed, or if not a proxy, and this is the last wither
                $this->shareService($definition, $service, $id, $inlineServices);
            }
        }

        if ($callable = $definition->getConfigurator()) {
            if (\is_array($callable)) {
                $callable[0] = $parameterBag->resolveValue($callable[0]);

                if ($callable[0] instanceof Reference) {
                    $callable[0] = $this->doGet((string) $callable[0], $callable[0]->getInvalidBehavior(), $inlineServices);
                } elseif ($callable[0] instanceof Definition) {
                    $callable[0] = $this->createService($callable[0], $inlineServices);
                }
            }

            if (!\is_callable($callable)) {
                throw new InvalidArgumentException(\sprintf('The configure callable for class "%s" is not a callable.', get_debug_type($service)));
            }

            $callable($service);
        }

        return $service;
    }

    /**
     * Replaces service references by the real service instance and evaluates expressions.
     *
     * @return mixed The same value with all service references replaced by
     *               the real service instances and all expressions evaluated
     */
    public function resolveServices(mixed $value): mixed
    {
        return $this->doResolveServices($value);
    }

    private function doResolveServices(mixed $value, array &$inlineServices = [], bool $isConstructorArgument = false): mixed
    {
        if (\is_array($value)) {
            foreach ($value as $k => $v) {
                $value[$k] = $this->doResolveServices($v, $inlineServices, $isConstructorArgument);
            }
        } elseif ($value instanceof ServiceClosureArgument) {
            $reference = $value->getValues()[0];
            $value = fn () => $this->resolveServices($reference);
        } elseif ($value instanceof IteratorArgument) {
            $value = new RewindableGenerator(function () use ($value, &$inlineServices) {
                foreach ($value->getValues() as $k => $v) {
                    foreach (self::getServiceConditionals($v) as $s) {
                        if (!$this->has($s)) {
                            continue 2;
                        }
                    }
                    foreach (self::getInitializedConditionals($v) as $s) {
                        if (!$this->doGet($s, ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE, $inlineServices)) {
                            continue 2;
                        }
                    }

                    yield $k => $this->doResolveServices($v, $inlineServices);
                }
            }, function () use ($value): int {
                $count = 0;
                foreach ($value->getValues() as $v) {
                    foreach (self::getServiceConditionals($v) as $s) {
                        if (!$this->has($s)) {
                            continue 2;
                        }
                    }
                    foreach (self::getInitializedConditionals($v) as $s) {
                        if (!$this->doGet($s, ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE)) {
                            continue 2;
                        }
                    }

                    ++$count;
                }

                return $count;
            });
        } elseif ($value instanceof ServiceLocatorArgument) {
            $refs = $types = [];
            foreach ($value->getValues() as $k => $v) {
                $refs[$k] = [$v, null];
                $types[$k] = $v instanceof TypedReference ? $v->getType() : '?';
            }
            $value = new ServiceLocator($this->resolveServices(...), $refs, $types);
        } elseif ($value instanceof Reference) {
            $value = $this->doGet((string) $value, $value->getInvalidBehavior(), $inlineServices, $isConstructorArgument);
        } elseif ($value instanceof Definition) {
            $value = $this->createService($value, $inlineServices, $isConstructorArgument);
        } elseif ($value instanceof Parameter) {
            $value = $this->getParameter((string) $value);
        } elseif ($value instanceof Expression) {
            $value = $this->getExpressionLanguage()->evaluate($value, ['container' => $this]);
        } elseif ($value instanceof AbstractArgument) {
            throw new RuntimeException($value->getTextWithContext());
        }

        return $value;
    }

    /**
     * Returns service ids for a given tag.
     *
     * Example:
     *
     *     $container->register('foo')->addTag('my.tag', ['hello' => 'world']);
     *
     *     $serviceIds = $container->findTaggedServiceIds('my.tag');
     *     foreach ($serviceIds as $serviceId => $tags) {
     *         foreach ($tags as $tag) {
     *             echo $tag['hello'];
     *         }
     *     }
     *
     * @return array<string, array> An array of tags with the tagged service as key, holding a list of attribute arrays
     */
    public function findTaggedServiceIds(string $name, bool $throwOnAbstract = false): array
    {
        $this->usedTags[] = $name;
        $tags = [];
        foreach ($this->getDefinitions() as $id => $definition) {
            if ($definition->hasTag($name) && !$definition->hasTag('container.excluded')) {
                if ($throwOnAbstract && $definition->isAbstract()) {
                    throw new InvalidArgumentException(\sprintf('The service "%s" tagged "%s" must not be abstract.', $id, $name));
                }
                $tags[$id] = $definition->getTag($name);
            }
        }

        return $tags;
    }

    /**
     * Returns all tags the defined services use.
     *
     * @return string[]
     */
    public function findTags(): array
    {
        $tags = [];
        foreach ($this->getDefinitions() as $id => $definition) {
            $tags[] = array_keys($definition->getTags());
        }

        return array_unique(array_merge([], ...$tags));
    }

    /**
     * Returns all tags not queried by findTaggedServiceIds.
     *
     * @return string[]
     */
    public function findUnusedTags(): array
    {
        return array_values(array_diff($this->findTags(), $this->usedTags));
    }

    /**
     * @return void
     */
    public function addExpressionLanguageProvider(ExpressionFunctionProviderInterface $provider)
    {
        $this->expressionLanguageProviders[] = $provider;
    }

    /**
     * @return ExpressionFunctionProviderInterface[]
     */
    public function getExpressionLanguageProviders(): array
    {
        return $this->expressionLanguageProviders;
    }

    /**
     * Returns a ChildDefinition that will be used for autoconfiguring the interface/class.
     */
    public function registerForAutoconfiguration(string $interface): ChildDefinition
    {
        if (!isset($this->autoconfiguredInstanceof[$interface])) {
            $this->autoconfiguredInstanceof[$interface] = new ChildDefinition('');
        }

        return $this->autoconfiguredInstanceof[$interface];
    }

    /**
     * Registers an attribute that will be used for autoconfiguring annotated classes.
     *
     * The third argument passed to the callable is the reflector of the
     * class/method/property/parameter that the attribute targets. Using one or many of
     * \ReflectionClass|\ReflectionMethod|\ReflectionProperty|\ReflectionParameter as a type-hint
     * for this argument allows filtering which attributes should be passed to the callable.
     *
     * @template T
     *
     * @param class-string<T>                                $attributeClass
     * @param callable(ChildDefinition, T, \Reflector): void $configurator
     */
    public function registerAttributeForAutoconfiguration(string $attributeClass, callable $configurator): void
    {
        $this->autoconfiguredAttributes[$attributeClass] = $configurator;
    }

    /**
     * Registers an autowiring alias that only binds to a specific argument name.
     *
     * The argument name is derived from $name if provided (from $id otherwise)
     * using camel case: "foo.bar" or "foo_bar" creates an alias bound to
     * "$fooBar"-named arguments with $type as type-hint. Such arguments will
     * receive the service $id when autowiring is used.
     */
    public function registerAliasForArgument(string $id, string $type, ?string $name = null): Alias
    {
        $parsedName = (new Target($name ??= $id))->getParsedName();

        if (!preg_match('/^[a-zA-Z_\x7f-\xff]/', $parsedName)) {
            if ($id !== $name) {
                $id = \sprintf(' for service "%s"', $id);
            }

            throw new InvalidArgumentException(\sprintf('Invalid argument name "%s"'.$id.': the first character must be a letter.', $name));
        }

        if ($parsedName !== $name) {
            $this->setAlias('.'.$type.' $'.$name, $type.' $'.$parsedName);
        }

        return $this->setAlias($type.' $'.$parsedName, $id);
    }

    /**
     * Returns an array of ChildDefinition[] keyed by interface.
     *
     * @return array<string, ChildDefinition>
     */
    public function getAutoconfiguredInstanceof(): array
    {
        return $this->autoconfiguredInstanceof;
    }

    /**
     * @return array<string, callable>
     */
    public function getAutoconfiguredAttributes(): array
    {
        return $this->autoconfiguredAttributes;
    }

    /**
     * Resolves env parameter placeholders in a string or an array.
     *
     * @param string|true|null $format    A sprintf() format returning the replacement for each env var name or
     *                                    null to resolve back to the original "%env(VAR)%" format or
     *                                    true to resolve to the actual values of the referenced env vars
     * @param array            &$usedEnvs Env vars found while resolving are added to this array
     *
     * @return mixed The value with env parameters resolved if a string or an array is passed
     */
    public function resolveEnvPlaceholders(mixed $value, string|bool|null $format = null, ?array &$usedEnvs = null): mixed
    {
        $bag = $this->getParameterBag();
        if (true === $format ??= '%%env(%s)%%') {
            $value = $bag->resolveValue($value);
        }

        if ($value instanceof Definition) {
            $value = (array) $value;
        }

        if (\is_array($value)) {
            $result = [];
            foreach ($value as $k => $v) {
                $result[\is_string($k) ? $this->resolveEnvPlaceholders($k, $format, $usedEnvs) : $k] = $this->resolveEnvPlaceholders($v, $format, $usedEnvs);
            }

            return $result;
        }

        if (!\is_string($value) || 38 > \strlen($value) || false === stripos($value, 'env_')) {
            return $value;
        }
        $envPlaceholders = $bag instanceof EnvPlaceholderParameterBag ? $bag->getEnvPlaceholders() : $this->envPlaceholders;

        $completed = false;
        preg_match_all('/env_[a-f0-9]{16}_\w+_[a-f0-9]{32}/Ui', $value, $matches);
        $usedPlaceholders = array_flip($matches[0]);
        foreach ($envPlaceholders as $env => $placeholders) {
            foreach ($placeholders as $placeholder) {
                if (isset($usedPlaceholders[$placeholder])) {
                    if (true === $format) {
                        $resolved = $bag->escapeValue($this->getEnv($env));
                    } else {
                        $resolved = \sprintf($format, $env);
                    }
                    if ($placeholder === $value) {
                        $value = $resolved;
                        $completed = true;
                    } else {
                        if (!\is_string($resolved) && !is_numeric($resolved)) {
                            throw new RuntimeException(\sprintf('A string value must be composed of strings and/or numbers, but found parameter "env(%s)" of type "%s" inside string value "%s".', $env, get_debug_type($resolved), $this->resolveEnvPlaceholders($value)));
                        }
                        $value = str_ireplace($placeholder, $resolved, $value);
                    }
                    $usedEnvs[$env] = $env;
                    $this->envCounters[$env] = isset($this->envCounters[$env]) ? 1 + $this->envCounters[$env] : 1;

                    if ($completed) {
                        break 2;
                    }
                }
            }
        }

        return $value;
    }

    /**
     * Get statistics about env usage.
     *
     * @return int[] The number of time each env vars has been resolved
     */
    public function getEnvCounters(): array
    {
        $bag = $this->getParameterBag();
        $envPlaceholders = $bag instanceof EnvPlaceholderParameterBag ? $bag->getEnvPlaceholders() : $this->envPlaceholders;

        foreach ($envPlaceholders as $env => $placeholders) {
            if (!isset($this->envCounters[$env])) {
                $this->envCounters[$env] = 0;
            }
        }

        return $this->envCounters;
    }

    /**
     * @final
     */
    public function log(CompilerPassInterface $pass, string $message): void
    {
        $this->getCompiler()->log($pass, $this->resolveEnvPlaceholders($message));
    }

    /**
     * Checks whether a class is available and will remain available in the "no-dev" mode of Composer.
     *
     * When parent packages are provided and if any of them is in dev-only mode,
     * the class will be considered available even if it is also in dev-only mode.
     *
     * @throws \LogicException If dependencies have been installed with Composer 1
     */
    final public static function willBeAvailable(string $package, string $class, array $parentPackages): bool
    {
        if (!class_exists(InstalledVersions::class)) {
            throw new \LogicException(\sprintf('Calling "%s" when dependencies have been installed with Composer 1 is not supported. Consider upgrading to Composer 2.', __METHOD__));
        }

        if (!class_exists($class) && !interface_exists($class, false) && !trait_exists($class, false)) {
            return false;
        }

        if (!InstalledVersions::isInstalled($package) || InstalledVersions::isInstalled($package, false)) {
            return true;
        }

        // the package is installed but in dev-mode only, check if this applies to one of the parent packages too

        $rootPackage = InstalledVersions::getRootPackage()['name'] ?? '';

        if ('symfony/symfony' === $rootPackage) {
            return true;
        }

        foreach ($parentPackages as $parentPackage) {
            if ($rootPackage === $parentPackage || (InstalledVersions::isInstalled($parentPackage) && !InstalledVersions::isInstalled($parentPackage, false))) {
                return true;
            }
        }

        return false;
    }

    /**
     * Gets removed binding ids.
     *
     * @return array<int, bool>
     *
     * @internal
     */
    public function getRemovedBindingIds(): array
    {
        return $this->removedBindingIds;
    }

    /**
     * Removes bindings for a service.
     *
     * @internal
     */
    public function removeBindings(string $id): void
    {
        if ($this->hasDefinition($id)) {
            foreach ($this->getDefinition($id)->getBindings() as $key => $binding) {
                [, $bindingId] = $binding->getValues();
                $this->removedBindingIds[(int) $bindingId] = true;
            }
        }
    }

    /**
     * @return string[]
     *
     * @internal
     */
    public static function getServiceConditionals(mixed $value): array
    {
        $services = [];

        if (\is_array($value)) {
            foreach ($value as $v) {
                $services = array_unique(array_merge($services, self::getServiceConditionals($v)));
            }
        } elseif ($value instanceof Reference && ContainerInterface::IGNORE_ON_INVALID_REFERENCE === $value->getInvalidBehavior()) {
            $services[] = (string) $value;
        }

        return $services;
    }

    /**
     * @return string[]
     *
     * @internal
     */
    public static function getInitializedConditionals(mixed $value): array
    {
        $services = [];

        if (\is_array($value)) {
            foreach ($value as $v) {
                $services = array_unique(array_merge($services, self::getInitializedConditionals($v)));
            }
        } elseif ($value instanceof Reference && ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE === $value->getInvalidBehavior()) {
            $services[] = (string) $value;
        }

        return $services;
    }

    /**
     * Computes a reasonably unique hash of a serializable value.
     */
    public static function hash(mixed $value): string
    {
        $hash = substr(base64_encode(hash('sha256', serialize($value), true)), 0, 7);

        return str_replace(['/', '+'], ['.', '_'], $hash);
    }

    protected function getEnv(string $name): mixed
    {
        $value = parent::getEnv($name);
        $bag = $this->getParameterBag();

        if (!\is_string($value) || !$bag instanceof EnvPlaceholderParameterBag) {
            return $value;
        }

        $envPlaceholders = $bag->getEnvPlaceholders();
        if (isset($envPlaceholders[$name][$value])) {
            $bag = new ParameterBag($bag->all());

            return $bag->unescapeValue($bag->get("env($name)"));
        }
        foreach ($envPlaceholders as $env => $placeholders) {
            if (isset($placeholders[$value])) {
                return $this->getEnv($env);
            }
        }

        $this->resolving["env($name)"] = true;
        try {
            return $bag->unescapeValue($this->resolveEnvPlaceholders($bag->escapeValue($value), true));
        } finally {
            unset($this->resolving["env($name)"]);
        }
    }

    private function callMethod(object $service, array $call, array &$inlineServices): mixed
    {
        foreach (self::getServiceConditionals($call[1]) as $s) {
            if (!$this->has($s)) {
                return $service;
            }
        }
        foreach (self::getInitializedConditionals($call[1]) as $s) {
            if (!$this->doGet($s, ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE, $inlineServices)) {
                return $service;
            }
        }

        $result = $service->{$call[0]}(...$this->doResolveServices($this->getParameterBag()->unescapeValue($this->getParameterBag()->resolveValue($call[1])), $inlineServices));

        return empty($call[2]) ? $service : $result;
    }

    private function shareService(Definition $definition, mixed $service, ?string $id, array &$inlineServices): void
    {
        $inlineServices[$id ?? spl_object_hash($definition)] = $service;

        if (null !== $id && $definition->isShared()) {
            if ($definition->isPrivate() && $this->isCompiled()) {
                $this->privates[$id] = $service;
            } else {
                $this->services[$id] = $service;
            }
            unset($this->loading[$id]);
        }
    }

    private function getExpressionLanguage(): ExpressionLanguage
    {
        if (!isset($this->expressionLanguage)) {
            if (!class_exists(Expression::class)) {
                throw new LogicException('Expressions cannot be used without the ExpressionLanguage component. Try running "composer require symfony/expression-language".');
            }
            $this->expressionLanguage = new ExpressionLanguage(null, $this->expressionLanguageProviders, null, $this->getEnv(...));
        }

        return $this->expressionLanguage;
    }

    private function inVendors(string $path): bool
    {
        $path = is_file($path) ? \dirname($path) : $path;

        if (isset($this->pathsInVendor[$path])) {
            return $this->pathsInVendor[$path];
        }

        $this->vendors ??= (new ComposerResource())->getVendors();
        $path = realpath($path) ?: $path;

        if (isset($this->pathsInVendor[$path])) {
            return $this->pathsInVendor[$path];
        }

        foreach ($this->vendors as $vendor) {
            if (str_starts_with($path, $vendor) && false !== strpbrk(substr($path, \strlen($vendor), 1), '/'.\DIRECTORY_SEPARATOR)) {
                $this->addResource(new FileResource($vendor.'/composer/installed.json'));

                return $this->pathsInVendor[$path] = true;
            }
        }

        return $this->pathsInVendor[$path] = false;
    }

    private function escapeParameters(array $parameters): array
    {
        $params = [];
        foreach ($parameters as $k => $v) {
            $params[$k] = match (true) {
                \is_array($v) => $this->escapeParameters($v),
                \is_string($v) => str_replace('%', '%%', $v),
                default => $v,
            };
        }

        return $params;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\ParameterBag;

use Symfony\Component\DependencyInjection\Exception\ParameterCircularReferenceException;
use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;

/**
 * Holds parameters.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class ParameterBag implements ParameterBagInterface
{
    protected $parameters = [];
    protected $resolved = false;
    protected array $deprecatedParameters = [];

    public function __construct(array $parameters = [])
    {
        $this->add($parameters);
    }

    /**
     * @return void
     */
    public function clear()
    {
        $this->parameters = [];
    }

    /**
     * @return void
     */
    public function add(array $parameters)
    {
        foreach ($parameters as $key => $value) {
            $this->set($key, $value);
        }
    }

    public function all(): array
    {
        return $this->parameters;
    }

    public function allDeprecated(): array
    {
        return $this->deprecatedParameters;
    }

    public function get(string $name): array|bool|string|int|float|\UnitEnum|null
    {
        if (!\array_key_exists($name, $this->parameters)) {
            if (!$name) {
                throw new ParameterNotFoundException($name);
            }

            $alternatives = [];
            foreach ($this->parameters as $key => $parameterValue) {
                $lev = levenshtein($name, $key);
                if ($lev <= \strlen($name) / 3 || str_contains($key, $name)) {
                    $alternatives[] = $key;
                }
            }

            $nonNestedAlternative = null;
            if (!\count($alternatives) && str_contains($name, '.')) {
                $namePartsLength = array_map('strlen', explode('.', $name));
                $key = substr($name, 0, -1 * (1 + array_pop($namePartsLength)));
                while (\count($namePartsLength)) {
                    if ($this->has($key)) {
                        if (\is_array($this->get($key))) {
                            $nonNestedAlternative = $key;
                        }
                        break;
                    }

                    $key = substr($key, 0, -1 * (1 + array_pop($namePartsLength)));
                }
            }

            throw new ParameterNotFoundException($name, null, null, null, $alternatives, $nonNestedAlternative);
        }

        if (isset($this->deprecatedParameters[$name])) {
            trigger_deprecation(...$this->deprecatedParameters[$name]);
        }

        return $this->parameters[$name];
    }

    /**
     * @return void
     */
    public function set(string $name, array|bool|string|int|float|\UnitEnum|null $value)
    {
        if (is_numeric($name)) {
            trigger_deprecation('symfony/dependency-injection', '6.2', \sprintf('Using numeric parameter name "%s" is deprecated and will throw as of 7.0.', $name));
            // uncomment the following line in 7.0
            // throw new InvalidArgumentException(sprintf('The parameter name "%s" cannot be numeric.', $name));
        }

        $this->parameters[$name] = $value;
    }

    /**
     * Deprecates a service container parameter.
     *
     * @return void
     *
     * @throws ParameterNotFoundException if the parameter is not defined
     */
    public function deprecate(string $name, string $package, string $version, string $message = 'The parameter "%s" is deprecated.')
    {
        if (!\array_key_exists($name, $this->parameters)) {
            throw new ParameterNotFoundException($name);
        }

        $this->deprecatedParameters[$name] = [$package, $version, $message, $name];
    }

    public function has(string $name): bool
    {
        return \array_key_exists($name, $this->parameters);
    }

    /**
     * @return void
     */
    public function remove(string $name)
    {
        unset($this->parameters[$name], $this->deprecatedParameters[$name]);
    }

    /**
     * @return void
     */
    public function resolve()
    {
        if ($this->resolved) {
            return;
        }

        $parameters = [];
        foreach ($this->parameters as $key => $value) {
            try {
                $value = $this->resolveValue($value);
                $parameters[$key] = $this->unescapeValue($value);
            } catch (ParameterNotFoundException $e) {
                $e->setSourceKey($key);

                throw $e;
            }
        }

        $this->parameters = $parameters;
        $this->resolved = true;
    }

    /**
     * Replaces parameter placeholders (%name%) by their values.
     *
     * @template TValue of array<array|scalar>|scalar
     *
     * @param TValue $value
     * @param array  $resolving An array of keys that are being resolved (used internally to detect circular references)
     *
     * @psalm-return (TValue is scalar ? array|scalar : array<array|scalar>)
     *
     * @throws ParameterNotFoundException          if a placeholder references a parameter that does not exist
     * @throws ParameterCircularReferenceException if a circular reference if detected
     * @throws RuntimeException                    when a given parameter has a type problem
     */
    public function resolveValue(mixed $value, array $resolving = []): mixed
    {
        if (\is_array($value)) {
            $args = [];
            foreach ($value as $key => $v) {
                $resolvedKey = \is_string($key) ? $this->resolveValue($key, $resolving) : $key;
                if (!\is_scalar($resolvedKey) && !$resolvedKey instanceof \Stringable) {
                    throw new RuntimeException(\sprintf('Array keys must be a scalar-value, but found key "%s" to resolve to type "%s".', $key, get_debug_type($resolvedKey)));
                }

                $args[$resolvedKey] = $this->resolveValue($v, $resolving);
            }

            return $args;
        }

        if (!\is_string($value) || '' === $value || !str_contains($value, '%')) {
            return $value;
        }

        return $this->resolveString($value, $resolving);
    }

    /**
     * Resolves parameters inside a string.
     *
     * @param array $resolving An array of keys that are being resolved (used internally to detect circular references)
     *
     * @throws ParameterNotFoundException          if a placeholder references a parameter that does not exist
     * @throws ParameterCircularReferenceException if a circular reference if detected
     * @throws RuntimeException                    when a given parameter has a type problem
     */
    public function resolveString(string $value, array $resolving = []): mixed
    {
        // we do this to deal with non string values (Boolean, integer, ...)
        // as the preg_replace_callback throw an exception when trying
        // a non-string in a parameter value
        if (preg_match('/^%([^%\s]+)%$/', $value, $match)) {
            $key = $match[1];

            if (isset($resolving[$key])) {
                throw new ParameterCircularReferenceException(array_keys($resolving));
            }

            $resolving[$key] = true;

            return $this->resolved ? $this->get($key) : $this->resolveValue($this->get($key), $resolving);
        }

        return preg_replace_callback('/%%|%([^%\s]+)%/', function ($match) use ($resolving, $value) {
            // skip %%
            if (!isset($match[1])) {
                return '%%';
            }

            $key = $match[1];
            if (isset($resolving[$key])) {
                throw new ParameterCircularReferenceException(array_keys($resolving));
            }

            $resolved = $this->get($key);

            if (!\is_string($resolved) && !is_numeric($resolved)) {
                throw new RuntimeException(\sprintf('A string value must be composed of strings and/or numbers, but found parameter "%s" of type "%s" inside string value "%s".', $key, get_debug_type($resolved), $value));
            }

            $resolved = (string) $resolved;
            $resolving[$key] = true;

            return $this->isResolved() ? $resolved : $this->resolveString($resolved, $resolving);
        }, $value);
    }

    /**
     * @return bool
     */
    public function isResolved()
    {
        return $this->resolved;
    }

    public function escapeValue(mixed $value): mixed
    {
        if (\is_string($value)) {
            return str_replace('%', '%%', $value);
        }

        if (\is_array($value)) {
            $result = [];
            foreach ($value as $k => $v) {
                $result[$k] = $this->escapeValue($v);
            }

            return $result;
        }

        return $value;
    }

    public function unescapeValue(mixed $value): mixed
    {
        if (\is_string($value)) {
            return str_replace('%%', '%', $value);
        }

        if (\is_array($value)) {
            $result = [];
            foreach ($value as $k => $v) {
                $result[$k] = $this->unescapeValue($v);
            }

            return $result;
        }

        return $value;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\ParameterBag;

use Psr\Container\ContainerInterface;
use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException;

/**
 * ContainerBagInterface is the interface implemented by objects that manage service container parameters.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
interface ContainerBagInterface extends ContainerInterface
{
    /**
     * Gets the service container parameters.
     */
    public function all(): array;

    /**
     * Replaces parameter placeholders (%name%) by their values.
     *
     * @template TValue of array<array|scalar>|scalar
     *
     * @param TValue $value
     *
     * @return mixed
     *
     * @psalm-return (TValue is scalar ? array|scalar : array<array|scalar>)
     *
     * @throws ParameterNotFoundException if a placeholder references a parameter that does not exist
     */
    public function resolveValue(mixed $value);

    /**
     * Escape parameter placeholders %.
     */
    public function escapeValue(mixed $value): mixed;

    /**
     * Unescape parameter placeholders %.
     */
    public function unescapeValue(mixed $value): mixed;
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\ParameterBag;

use Symfony\Component\DependencyInjection\Exception\LogicException;

/**
 * Holds read-only parameters.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class FrozenParameterBag extends ParameterBag
{
    /**
     * For performance reasons, the constructor assumes that
     * all keys are already lowercased.
     *
     * This is always the case when used internally.
     */
    public function __construct(
        array $parameters = [],
        protected array $deprecatedParameters = [],
    ) {
        $this->parameters = $parameters;
        $this->resolved = true;
    }

    /**
     * @return never
     */
    public function clear()
    {
        throw new LogicException('Impossible to call clear() on a frozen ParameterBag.');
    }

    /**
     * @return never
     */
    public function add(array $parameters)
    {
        throw new LogicException('Impossible to call add() on a frozen ParameterBag.');
    }

    /**
     * @return never
     */
    public function set(string $name, array|bool|string|int|float|\UnitEnum|null $value)
    {
        throw new LogicException('Impossible to call set() on a frozen ParameterBag.');
    }

    /**
     * @return never
     */
    public function deprecate(string $name, string $package, string $version, string $message = 'The parameter "%s" is deprecated.')
    {
        throw new LogicException('Impossible to call deprecate() on a frozen ParameterBag.');
    }

    /**
     * @return never
     */
    public function remove(string $name)
    {
        throw new LogicException('Impossible to call remove() on a frozen ParameterBag.');
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\ParameterBag;

use Symfony\Component\DependencyInjection\Container;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 */
class ContainerBag extends FrozenParameterBag implements ContainerBagInterface
{
    private Container $container;

    public function __construct(Container $container)
    {
        $this->container = $container;
    }

    public function all(): array
    {
        return $this->container->getParameterBag()->all();
    }

    public function get(string $name): array|bool|string|int|float|\UnitEnum|null
    {
        return $this->container->getParameter($name);
    }

    public function has(string $name): bool
    {
        return $this->container->hasParameter($name);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\ParameterBag;

use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 */
class EnvPlaceholderParameterBag extends ParameterBag
{
    private string $envPlaceholderUniquePrefix;
    private array $envPlaceholders = [];
    private array $unusedEnvPlaceholders = [];
    private array $providedTypes = [];

    private static int $counter = 0;

    public function get(string $name): array|bool|string|int|float|\UnitEnum|null
    {
        if (str_starts_with($name, 'env(') && str_ends_with($name, ')') && 'env()' !== $name) {
            $env = substr($name, 4, -1);

            if (isset($this->envPlaceholders[$env])) {
                foreach ($this->envPlaceholders[$env] as $placeholder) {
                    return $placeholder; // return first result
                }
            }
            if (isset($this->unusedEnvPlaceholders[$env])) {
                foreach ($this->unusedEnvPlaceholders[$env] as $placeholder) {
                    return $placeholder; // return first result
                }
            }
            if (!preg_match('/^(?:[-.\w\\\\]*+:)*+\w*+$/', $env)) {
                throw new InvalidArgumentException(\sprintf('Invalid %s name: only "word" characters are allowed.', $name));
            }
            if ($this->has($name) && null !== ($defaultValue = parent::get($name)) && !\is_string($defaultValue)) {
                throw new RuntimeException(\sprintf('The default value of an env() parameter must be a string or null, but "%s" given to "%s".', get_debug_type($defaultValue), $name));
            }

            $uniqueName = hash('xxh128', $name.'_'.self::$counter++);
            $placeholder = \sprintf('%s_%s_%s', $this->getEnvPlaceholderUniquePrefix(), strtr($env, ':-.\\', '____'), $uniqueName);
            $this->envPlaceholders[$env][$placeholder] = $placeholder;

            return $placeholder;
        }

        return parent::get($name);
    }

    /**
     * Gets the common env placeholder prefix for env vars created by this bag.
     */
    public function getEnvPlaceholderUniquePrefix(): string
    {
        if (!isset($this->envPlaceholderUniquePrefix)) {
            $reproducibleEntropy = unserialize(serialize($this->parameters), ['allowed_classes' => true]);
            array_walk_recursive($reproducibleEntropy, static function (&$v) { $v = null; });
            $this->envPlaceholderUniquePrefix = 'env_'.substr(hash('xxh128', serialize($reproducibleEntropy)), -16);
        }

        return $this->envPlaceholderUniquePrefix;
    }

    /**
     * Returns the map of env vars used in the resolved parameter values to their placeholders.
     *
     * @return string[][] A map of env var names to their placeholders
     */
    public function getEnvPlaceholders(): array
    {
        return $this->envPlaceholders;
    }

    public function getUnusedEnvPlaceholders(): array
    {
        return $this->unusedEnvPlaceholders;
    }

    /**
     * @return void
     */
    public function clearUnusedEnvPlaceholders()
    {
        $this->unusedEnvPlaceholders = [];
    }

    /**
     * Merges the env placeholders of another EnvPlaceholderParameterBag.
     *
     * @return void
     */
    public function mergeEnvPlaceholders(self $bag)
    {
        if ($newPlaceholders = $bag->getEnvPlaceholders()) {
            $this->envPlaceholders += $newPlaceholders;

            foreach ($newPlaceholders as $env => $placeholders) {
                $this->envPlaceholders[$env] += $placeholders;
            }
        }

        if ($newUnusedPlaceholders = $bag->getUnusedEnvPlaceholders()) {
            $this->unusedEnvPlaceholders += $newUnusedPlaceholders;

            foreach ($newUnusedPlaceholders as $env => $placeholders) {
                $this->unusedEnvPlaceholders[$env] += $placeholders;
            }
        }
    }

    /**
     * Maps env prefixes to their corresponding PHP types.
     *
     * @return void
     */
    public function setProvidedTypes(array $providedTypes)
    {
        $this->providedTypes = $providedTypes;
    }

    /**
     * Gets the PHP types corresponding to env() parameter prefixes.
     *
     * @return string[][]
     */
    public function getProvidedTypes(): array
    {
        return $this->providedTypes;
    }

    /**
     * @return void
     */
    public function resolve()
    {
        if ($this->resolved) {
            return;
        }
        parent::resolve();

        foreach ($this->envPlaceholders as $env => $placeholders) {
            if ($this->has($name = "env($env)") && null !== ($default = $this->parameters[$name]) && !\is_string($default)) {
                throw new RuntimeException(\sprintf('The default value of env parameter "%s" must be a string or null, "%s" given.', $env, get_debug_type($default)));
            }
        }
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection\ParameterBag;

use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException;

/**
 * ParameterBagInterface is the interface implemented by objects that manage service container parameters.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
interface ParameterBagInterface
{
    /**
     * Clears all parameters.
     *
     * @return void
     *
     * @throws LogicException if the ParameterBagInterface cannot be cleared
     */
    public function clear();

    /**
     * Adds parameters to the service container parameters.
     *
     * @return void
     *
     * @throws LogicException if the parameter cannot be added
     */
    public function add(array $parameters);

    /**
     * Gets the service container parameters.
     */
    public function all(): array;

    /**
     * Gets a service container parameter.
     *
     * @throws ParameterNotFoundException if the parameter is not defined
     */
    public function get(string $name): array|bool|string|int|float|\UnitEnum|null;

    /**
     * Removes a parameter.
     *
     * @return void
     */
    public function remove(string $name);

    /**
     * Sets a service container parameter.
     *
     * @return void
     *
     * @throws LogicException if the parameter cannot be set
     */
    public function set(string $name, array|bool|string|int|float|\UnitEnum|null $value);

    /**
     * Returns true if a parameter name is defined.
     */
    public function has(string $name): bool;

    /**
     * Replaces parameter placeholders (%name%) by their values for all parameters.
     *
     * @return void
     */
    public function resolve();

    /**
     * Replaces parameter placeholders (%name%) by their values.
     *
     * @return mixed
     *
     * @throws ParameterNotFoundException if a placeholder references a parameter that does not exist
     */
    public function resolveValue(mixed $value);

    /**
     * Escape parameter placeholders %.
     */
    public function escapeValue(mixed $value): mixed;

    /**
     * Unescape parameter placeholders %.
     */
    public function unescapeValue(mixed $value): mixed;
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection;

/**
 * EnvVarLoaderInterface objects return key/value pairs that are added to the list of available env vars.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
interface EnvVarLoaderInterface
{
    /**
     * @return array<string|\Stringable> Key/value pairs that can be accessed using the regular "%env()%" syntax
     */
    public function loadEnvVars(): array;
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection;

use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\OutOfBoundsException;

/**
 * This definition extends another definition.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class ChildDefinition extends Definition
{
    private string $parent;

    /**
     * @param string $parent The id of Definition instance to decorate
     */
    public function __construct(string $parent)
    {
        $this->parent = $parent;
    }

    /**
     * Returns the Definition to inherit from.
     */
    public function getParent(): string
    {
        return $this->parent;
    }

    /**
     * Sets the Definition to inherit from.
     *
     * @return $this
     */
    public function setParent(string $parent): static
    {
        $this->parent = $parent;

        return $this;
    }

    /**
     * Gets an argument to pass to the service constructor/factory method.
     *
     * If replaceArgument() has been used to replace an argument, this method
     * will return the replacement value.
     *
     * @throws OutOfBoundsException When the argument does not exist
     */
    public function getArgument(int|string $index): mixed
    {
        if (\array_key_exists('index_'.$index, $this->arguments)) {
            return $this->arguments['index_'.$index];
        }

        return parent::getArgument($index);
    }

    /**
     * You should always use this method when overwriting existing arguments
     * of the parent definition.
     *
     * If you directly call setArguments() keep in mind that you must follow
     * certain conventions when you want to overwrite the arguments of the
     * parent definition, otherwise your arguments will only be appended.
     *
     * @return $this
     *
     * @throws InvalidArgumentException when $index isn't an integer
     */
    public function replaceArgument(int|string $index, mixed $value): static
    {
        if (\is_int($index)) {
            $this->arguments['index_'.$index] = $value;
        } elseif (str_starts_with($index, '$')) {
            $this->arguments[$index] = $value;
        } else {
            throw new InvalidArgumentException('The argument must be an existing index or the name of a constructor\'s parameter.');
        }

        return $this;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection;

/**
 * Reference represents a service reference.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class Reference
{
    private string $id;
    private int $invalidBehavior;

    public function __construct(string $id, int $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE)
    {
        $this->id = $id;
        $this->invalidBehavior = $invalidBehavior;
    }

    public function __toString(): string
    {
        return $this->id;
    }

    /**
     * Returns the behavior to be used when the service does not exist.
     */
    public function getInvalidBehavior(): int
    {
        return $this->invalidBehavior;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\DependencyInjection;

use Symfony\Component\DependencyInjection\Argument\BoundArgument;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\OutOfBoundsException;

/**
 * Definition represents a service definition.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class Definition
{
    private const DEFAULT_DEPRECATION_TEMPLATE = 'The "%service_id%" service is deprecated. You should stop using it, as it will be removed in the future.';

    private ?string $class = null;
    private ?string $file = null;
    private string|array|null $factory = null;
    private bool $shared = true;
    private array $deprecation = [];
    private array $properties = [];
    private array $calls = [];
    private array $instanceof = [];
    private bool $autoconfigured = false;
    private string|array|null $configurator = null;
    private array $tags = [];
    private bool $public = false;
    private bool $synthetic = false;
    private bool $abstract = false;
    private bool $lazy = false;
    private ?array $decoratedService = null;
    private bool $autowired = false;
    private array $changes = [];
    private array $bindings = [];
    private array $errors = [];

    protected $arguments = [];

    /**
     * @internal
     *
     * Used to store the name of the inner id when using service decoration together with autowiring
     */
    public ?string $innerServiceId = null;

    /**
     * @internal
     *
     * Used to store the behavior to follow when using service decoration and the decorated service is invalid
     */
    public ?int $decorationOnInvalid = null;

    public function __construct(?string $class = null, array $arguments = [])
    {
        if (null !== $class) {
            $this->setClass($class);
        }
        $this->arguments = $arguments;
    }

    /**
     * Returns all changes tracked for the Definition object.
     */
    public function getChanges(): array
    {
        return $this->changes;
    }

    /**
     * Sets the tracked changes for the Definition object.
     *
     * @param array $changes An array of changes for this Definition
     *
     * @return $this
     */
    public function setChanges(array $changes): static
    {
        $this->changes = $changes;

        return $this;
    }

    /**
     * Sets a factory.
     *
     * @param string|array|Reference|null $factory A PHP function, reference or an array containing a class/Reference and a method to call
     *
     * @return $this
     */
    public function setFactory(string|array|Reference|null $factory): static
    {
        $this->changes['factory'] = true;

        if (\is_string($factory) && str_contains($factory, '::')) {
            $factory = explode('::', $factory, 2);
        } elseif ($factory instanceof Reference) {
            $factory = [$factory, '__invoke'];
        }

        $this->factory = $factory;

        return $this;
    }

    /**
     * Gets the factory.
     *
     * @return string|array|null The PHP function or an array containing a class/Reference and a method to call
     */
    public function getFactory(): string|array|null
    {
        return $this->factory;
    }

    /**
     * Sets the service that this service is decorating.
     *
     * @param string|null $id        The decorated service id, use null to remove decoration
     * @param string|null $renamedId The new decorated service id
     *
     * @return $this
     *
     * @throws InvalidArgumentException in case the decorated service id and the new decorated service id are equals
     */
    public function setDecoratedService(?string $id, ?string $renamedId = null, int $priority = 0, int $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE): static
    {
        if ($renamedId && $id === $renamedId) {
            throw new InvalidArgumentException(\sprintf('The decorated service inner name for "%s" must be different than the service name itself.', $id));
        }

        $this->changes['decorated_service'] = true;

        if (null === $id) {
            $this->decoratedService = null;
        } else {
            $this->decoratedService = [$id, $renamedId, $priority];

            if (ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE !== $invalidBehavior) {
                $this->decoratedService[] = $invalidBehavior;
            }
        }

        return $this;
    }

    /**
     * Gets the service that this service is decorating.
     *
     * @return array|null An array composed of the decorated service id, the new id for it and the priority of decoration, null if no service is decorated
     */
    public function getDecoratedService(): ?array
    {
        return $this->decoratedService;
    }

    /**
     * Sets the service class.
     *
     * @return $this
     */
    public function setClass(?string $class): static
    {
        $this->changes['class'] = true;

        $this->class = $class;

        return $this;
    }

    /**
     * Gets the service class.
     */
    public function getClass(): ?string
    {
        return $this->class;
    }

    /**
     * Sets the arguments to pass to the service constructor/factory method.
     *
     * @return $this
     */
    public function setArguments(array $arguments): static
    {
        $this->arguments = $arguments;

        return $this;
    }

    /**
     * Sets the properties to define when creating the service.
     *
     * @return $this
     */
    public function setProperties(array $properties): static
    {
        $this->properties = $properties;

        return $this;
    }

    /**
     * Gets the properties to define when creating the service.
     */
    public function getProperties(): array
    {
        return $this->properties;
    }

    /**
     * Sets a specific property.
     *
     * @return $this
     */
    public function setProperty(string $name, mixed $value): static
    {
        $this->properties[$name] = $value;

        return $this;
    }

    /**
     * Adds an argument to pass to the service constructor/factory method.
     *
     * @return $this
     */
    public function addArgument(mixed $argument): static
    {
        $this->arguments[] = $argument;

        return $this;
    }

    /**
     * Replaces a specific argument.
     *
     * @return $this
     *
     * @throws OutOfBoundsException When the replaced argument does not exist
     */
    public function replaceArgument(int|string $index, mixed $argument): static
    {
        if (0 === \count($this->arguments)) {
            throw new OutOfBoundsException(\sprintf('Cannot replace arguments for class "%s" if none have been configured yet.', $this->class));
        }

        if (\is_int($index) && ($index < 0 || $index > \count($this->arguments) - 1)) {
            throw new OutOfBoundsException(\sprintf('The index "%d" is not in the range [0, %d] of the arguments of class "%s".', $index, \count($this->arguments) - 1, $this->class));
        }

        if (!\array_key_exists($index, $this->arguments)) {
            throw new OutOfBoundsException(\sprintf('The argument "%s" doesn\'t exist in class "%s".', $index, $this->class));
        }

        $this->arguments[$index] = $argument;

        return $this;
    }

    /**
     * Sets a specific argument.
     *
     * @return $this
     */
    public function setArgument(int|string $key, mixed $value): static
    {
        $this->arguments[$key] = $value;

        return $this;
    }

    /**
     * Gets the arguments to pass to the service constructor/factory method.
     */
    public function getArguments(): array
    {
        return $this->arguments;
    }

    /**
     * Gets an argument to pass to the service constructor/factory method.
     *
     * @throws OutOfBoundsException When the argument does not exist
     */
    public function getArgument(int|string $index): mixed
    {
        if (!\array_key_exists($index, $this->arguments)) {
            throw new OutOfBoundsException(\sprintf('The argument "%s" doesn\'t exist in class "%s".', $index, $this->class));
        }

        return $this->arguments[$index];
    }

    /**
     * Sets the methods to call after service initialization.
     *
     * @return $this
     */
    public function setMethodCalls(array $calls = []): static
    {
        $this->calls = [];
        foreach ($calls as $call) {
            $this->addMethodCall($call[0], $call[1], $call[2] ?? false);
        }

        return $this;
    }

    /**
     * Adds a method to call after service initialization.
     *
     * @param string $method       The method name to call
     * @param array  $arguments    An array of arguments to pass to the method call
     * @param bool   $returnsClone Whether the call returns the service instance or not
     *
     * @return $this
     *
     * @throws InvalidArgumentException on empty $method param
     */
    public function addMethodCall(string $method, array $arguments = [], bool $returnsClone = false): static
    {
        if (empty($method)) {
            throw new InvalidArgumentException('Method name cannot be empty.');
        }
        $this->calls[] = $returnsClone ? [$method, $arguments, true] : [$method, $arguments];

        return $this;
    }

    /**
     * Removes a method to call after service initialization.
     *
     * @return $this
     */
    public function removeMethodCall(string $method): static
    {
        foreach ($this->calls as $i => $call) {
            if ($call[0] === $method) {
                unset($this->calls[$i]);
            }
        }

        return $this;
    }

    /**
     * Check if the current definition has a given method to call after service initialization.
     */
    public function hasMethodCall(string $method): bool
    {
        foreach ($this->calls as $call) {
            if ($call[0] === $method) {
                return true;
            }
        }

        return false;
    }

    /**
     * Gets the methods to call after service initialization.
     */
    public function getMethodCalls(): array
    {
        return $this->calls;
    }

    /**
     * Sets the definition templates to conditionally apply on the current definition, keyed by parent interface/class.
     *
     * @param ChildDefinition[] $instanceof
     *
     * @return $this
     */
    public function setInstanceofConditionals(array $instanceof): static
    {
        $this->instanceof = $instanceof;

        return $this;
    }

    /**
     * Gets the definition templates to conditionally apply on the current definition, keyed by parent interface/class.
     *
     * @return ChildDefinition[]
     */
    public function getInstanceofConditionals(): array
    {
        return $this->instanceof;
    }

    /**
     * Sets whether or not instanceof conditionals should be prepended with a global set.
     *
     * @return $this
     */
    public function setAutoconfigured(bool $autoconfigured): static
    {
        $this->changes['autoconfigured'] = true;

        $this->autoconfigured = $autoconfigured;

        return $this;
    }

    public function isAutoconfigured(): bool
    {
        return $this->autoconfigured;
    }

    /**
     * Sets tags for this definition.
     *
     * @return $this
     */
    public function setTags(array $tags): static
    {
        $this->tags = $tags;

        return $this;
    }

    /**
     * Returns all tags.
     */
    public function getTags(): array
    {
        return $this->tags;
    }

    /**
     * Gets a tag by name.
     */
    public function getTag(string $name): array
    {
        return $this->tags[$name] ?? [];
    }

    /**
     * Adds a tag for this definition.
     *
     * @return $this
     */
    public function addTag(string $name, array $attributes = []): static
    {
        $this->tags[$name][] = $attributes;

        return $this;
    }

    /**
     * Whether this definition has a tag with the given name.
     */
    public function hasTag(string $name): bool
    {
        return isset($this->tags[$name]);
    }

    /**
     * Clears all tags for a given name.
     *
     * @return $this
     */
    public function clearTag(string $name): static
    {
        unset($this->tags[$name]);

        return $this;
    }

    /**
     * Clears the tags for this definition.
     *
     * @return $this
     */
    public function clearTags(): static
    {
        $this->tags = [];

        return $this;
    }

    /**
     * Sets a file to require before creating the service.
     *
     * @return $this
     */
    public function setFile(?string $file): static
    {
        $this->changes['file'] = true;

        $this->file = $file;

        return $this;
    }

    /**
     * Gets the file to require before creating the service.
     */
    public function getFile(): ?string
    {
        return $this->file;
    }

    /**
     * Sets if the service must be shared or not.
     *
     * @return $this
     */
    public function setShared(bool $shared): static
    {
        $this->changes['shared'] = true;

        $this->shared = $shared;

        return $this;
    }

    /**
     * Whether this service is shared.
     */
    public function isShared(): bool
    {
        return $this->shared;
    }

    /**
     * Sets the visibility of this service.
     *
     * @return $this
     */
    public function setPublic(bool $boolean): static
    {
        $this->changes['public'] = true;

        $this->public = $boolean;

        return $this;
    }

    /**
     * Whether this service is public facing.
     */
    public function isPublic(): bool
    {
        return $this->public;
    }

    /**
     * Whether this service is private.
     */
    public function isPrivate(): bool
    {
        return !$this->public;
    }

    /**
     * Sets the lazy flag of this service.
     *
     * @return $this
     */
    public function setLazy(bool $lazy): static
    {
        $this->changes['lazy'] = true;

        $this->lazy = $lazy;

        return $this;
    }

    /**
     * Whether this service is lazy.
     */
    public function isLazy(): bool
    {
        return $this->lazy;
    }

    /**
     * Sets whether this definition is synthetic, that is not constructed by the
     * container, but dynamically injected.
     *
     * @return $this
     */
    public function setSynthetic(bool $boolean): static
    {
        $this->synthetic = $boolean;

        if (!isset($this->changes['public'])) {
            $this->setPublic(true);
        }

        return $this;
    }

    /**
     * Whether this definition is synthetic, that is not constructed by the
     * container, but dynamically injected.
     */
    public function isSynthetic(): bool
    {
        return $this->synthetic;
    }

    /**
     * Whether this definition is abstract, that means it merely serves as a
     * template for other definitions.
     *
     * @return $this
     */
    public function setAbstract(bool $boolean): static
    {
        $this->abstract = $boolean;

        return $this;
    }

    /**
     * Whether this definition is abstract, that means it merely serves as a
     * template for other definitions.
     */
    public function isAbstract(): bool
    {
        return $this->abstract;
    }

    /**
     * Whether this definition is deprecated, that means it should not be called
     * anymore.
     *
     * @param string $package The name of the composer package that is triggering the deprecation
     * @param string $version The version of the package that introduced the deprecation
     * @param string $message The deprecation message to use
     *
     * @return $this
     *
     * @throws InvalidArgumentException when the message template is invalid
     */
    public function setDeprecated(string $package, string $version, string $message): static
    {
        if ('' !== $message) {
            if (preg_match('#[\r\n]|\*/#', $message)) {
                throw new InvalidArgumentException('Invalid characters found in deprecation template.');
            }

            if (!str_contains($message, '%service_id%')) {
                throw new InvalidArgumentException('The deprecation template must contain the "%service_id%" placeholder.');
            }
        }

        $this->changes['deprecated'] = true;
        $this->deprecation = ['package' => $package, 'version' => $version, 'message' => $message ?: self::DEFAULT_DEPRECATION_TEMPLATE];

        return $this;
    }

    /**
     * Whether this definition is deprecated, that means it should not be called
     * anymore.
     */
    public function isDeprecated(): bool
    {
        return (bool) $this->deprecation;
    }

    /**
     * @param string $id Service id relying on this definition
     */
    public function getDeprecation(string $id): array
    {
        return [
            'package' => $this->deprecation['package'],
            'version' => $this->deprecation['version'],
            'message' => str_replace('%service_id%', $id, $this->deprecation['message']),
        ];
    }

    /**
     * Sets a configurator to call after the service is fully initialized.
     *
     * @param string|array|Reference|null $configurator A PHP function, reference or an array containing a class/Reference and a method to call
     *
     * @return $this
     */
    public function setConfigurator(string|array|Reference|null $configurator): static
    {
        $this->changes['configurator'] = true;

        if (\is_string($configurator) && str_contains($configurator, '::')) {
            $configurator = explode('::', $configurator, 2);
        } elseif ($configurator instanceof Reference) {
            $configurator = [$configurator, '__invoke'];
        }

        $this->configurator = $configurator;

        return $this;
    }

    /**
     * Gets the configurator to call after the service is fully initialized.
     */
    public function getConfigurator(): string|array|null
    {
        return $this->configurator;
    }

    /**
     * Is the definition autowired?
     */
    public function isAutowired(): bool
    {
        return $this->autowired;
    }

    /**
     * Enables/disables autowiring.
     *
     * @return $this
     */
    public function setAutowired(bool $autowired): static
    {
        $this->changes['autowired'] = true;

        $this->autowired = $autowired;

        return $this;
    }

    /**
     * Gets bindings.
     *
     * @return BoundArgument[]
     */
    public function getBindings(): array
    {
        return $this->bindings;
    }

    /**
     * Sets bindings.
     *
     * Bindings map $named or FQCN arguments to values that should be
     * injected in the matching parameters (of the constructor, of methods
     * called and of controller actions).
     *
     * @return $this
     */
    public function setBindings(array $bindings): static
    {
        foreach ($bindings as $key => $binding) {
            if (0 < strpos($key, '$') && $key !== $k = preg_replace('/[ \t]*\$/', ' $', $key)) {
                unset($bindings[$key]);
                $bindings[$key = $k] = $binding;
            }
            if (!$binding instanceof BoundArgument) {
                $bindings[$key] = new BoundArgument($binding);
            }
        }

        $this->bindings = $bindings;

        return $this;
    }

    /**
     * Add an error that occurred when building this Definition.
     *
     * @return $this
     */
    public function addError(string|\Closure|self $error): static
    {
        if ($error instanceof self) {
            $this->errors = array_merge($this->errors, $error->errors);
        } else {
            $this->errors[] = $error;
        }

        return $this;
    }

    /**
     * Returns any errors that occurred while building this Definition.
     */
    public function getErrors(): array
    {
        foreach ($this->errors as $i => $error) {
            if ($error instanceof \Closure) {
                $this->errors[$i] = (string) $error();
            } elseif (!\is_string($error)) {
                $this->errors[$i] = (string) $error;
            }
        }

        return $this->errors;
    }

    public function hasErrors(): bool
    {
        return (bool) $this->errors;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\EventDispatcher\DependencyInjection;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

/**
 * This pass allows bundles to extend the list of event aliases.
 *
 * @author Alexander M. Turek <me@derrabus.de>
 */
class AddEventAliasesPass implements CompilerPassInterface
{
    private array $eventAliases;

    public function __construct(array $eventAliases)
    {
        $this->eventAliases = $eventAliases;
    }

    public function process(ContainerBuilder $container): void
    {
        $eventAliases = $container->hasParameter('event_dispatcher.event_aliases') ? $container->getParameter('event_dispatcher.event_aliases') : [];

        $container->setParameter(
            'event_dispatcher.event_aliases',
            array_merge($eventAliases, $this->eventAliases)
        );
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\EventDispatcher\DependencyInjection;

use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Contracts\EventDispatcher\Event;

/**
 * Compiler pass to register tagged services for an event dispatcher.
 */
class RegisterListenersPass implements CompilerPassInterface
{
    private array $hotPathEvents = [];
    private array $noPreloadEvents = [];

    /**
     * @return $this
     */
    public function setHotPathEvents(array $hotPathEvents): static
    {
        $this->hotPathEvents = array_flip($hotPathEvents);

        return $this;
    }

    /**
     * @return $this
     */
    public function setNoPreloadEvents(array $noPreloadEvents): static
    {
        $this->noPreloadEvents = array_flip($noPreloadEvents);

        return $this;
    }

    /**
     * @return void
     */
    public function process(ContainerBuilder $container)
    {
        if (!$container->hasDefinition('event_dispatcher') && !$container->hasAlias('event_dispatcher')) {
            return;
        }

        $aliases = [];

        if ($container->hasParameter('event_dispatcher.event_aliases')) {
            $aliases = $container->getParameter('event_dispatcher.event_aliases');
        }

        $globalDispatcherDefinition = $container->findDefinition('event_dispatcher');

        foreach ($container->findTaggedServiceIds('kernel.event_listener', true) as $id => $events) {
            $noPreload = 0;

            foreach ($events as $event) {
                $priority = $event['priority'] ?? 0;

                if (!isset($event['event'])) {
                    if ($container->getDefinition($id)->hasTag('kernel.event_subscriber')) {
                        continue;
                    }

                    $event['method'] ??= '__invoke';
                    $event['event'] = $this->getEventFromTypeDeclaration($container, $id, $event['method']);
                }

                $event['event'] = $aliases[$event['event']] ?? $event['event'];

                if (!isset($event['method'])) {
                    $event['method'] = 'on'.preg_replace_callback([
                        '/(?<=\b|_)[a-z]/i',
                        '/[^a-z0-9]/i',
                    ], static fn ($matches) => strtoupper($matches[0]), $event['event']);
                    $event['method'] = preg_replace('/[^a-z0-9]/i', '', $event['method']);

                    if (null !== ($class = $container->getDefinition($id)->getClass()) && ($r = $container->getReflectionClass($class, false)) && !$r->hasMethod($event['method'])) {
                        if (!$r->hasMethod('__invoke')) {
                            throw new InvalidArgumentException(\sprintf('None of the "%s" or "__invoke" methods exist for the service "%s". Please define the "method" attribute on "kernel.event_listener" tags.', $event['method'], $id));
                        }

                        $event['method'] = '__invoke';
                    }
                }

                $dispatcherDefinition = $globalDispatcherDefinition;
                if (isset($event['dispatcher'])) {
                    $dispatcherDefinition = $container->findDefinition($event['dispatcher']);
                }

                $dispatcherDefinition->addMethodCall('addListener', [$event['event'], [new ServiceClosureArgument(new Reference($id)), $event['method']], $priority]);

                if (isset($this->hotPathEvents[$event['event']])) {
                    $container->getDefinition($id)->addTag('container.hot_path');
                } elseif (isset($this->noPreloadEvents[$event['event']])) {
                    ++$noPreload;
                }
            }

            if ($noPreload && \count($events) === $noPreload) {
                $container->getDefinition($id)->addTag('container.no_preload');
            }
        }

        $extractingDispatcher = new ExtractingEventDispatcher();

        foreach ($container->findTaggedServiceIds('kernel.event_subscriber', true) as $id => $tags) {
            $def = $container->getDefinition($id);

            // We must assume that the class value has been correctly filled, even if the service is created by a factory
            $class = $def->getClass();

            if (!$r = $container->getReflectionClass($class)) {
                throw new InvalidArgumentException(\sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id));
            }
            if (!$r->isSubclassOf(EventSubscriberInterface::class)) {
                throw new InvalidArgumentException(\sprintf('Service "%s" must implement interface "%s".', $id, EventSubscriberInterface::class));
            }
            $class = $r->name;

            $dispatcherDefinitions = [];
            foreach ($tags as $attributes) {
                if (!isset($attributes['dispatcher']) || isset($dispatcherDefinitions[$attributes['dispatcher']])) {
                    continue;
                }

                $dispatcherDefinitions[$attributes['dispatcher']] = $container->findDefinition($attributes['dispatcher']);
            }

            if (!$dispatcherDefinitions) {
                $dispatcherDefinitions = [$globalDispatcherDefinition];
            }

            $noPreload = 0;
            ExtractingEventDispatcher::$aliases = $aliases;
            ExtractingEventDispatcher::$subscriber = $class;
            $extractingDispatcher->addSubscriber($extractingDispatcher);
            foreach ($extractingDispatcher->listeners as $args) {
                $args[1] = [new ServiceClosureArgument(new Reference($id)), $args[1]];
                foreach ($dispatcherDefinitions as $dispatcherDefinition) {
                    $dispatcherDefinition->addMethodCall('addListener', $args);
                }

                if (isset($this->hotPathEvents[$args[0]])) {
                    $container->getDefinition($id)->addTag('container.hot_path');
                } elseif (isset($this->noPreloadEvents[$args[0]])) {
                    ++$noPreload;
                }
            }
            if ($noPreload && \count($extractingDispatcher->listeners) === $noPreload) {
                $container->getDefinition($id)->addTag('container.no_preload');
            }
            $extractingDispatcher->listeners = [];
            ExtractingEventDispatcher::$aliases = [];
        }
    }

    private function getEventFromTypeDeclaration(ContainerBuilder $container, string $id, string $method): string
    {
        if (
            null === ($class = $container->getDefinition($id)->getClass())
            || !($r = $container->getReflectionClass($class, false))
            || !$r->hasMethod($method)
            || 1 > ($m = $r->getMethod($method))->getNumberOfParameters()
            || !($type = $m->getParameters()[0]->getType()) instanceof \ReflectionNamedType
            || $type->isBuiltin()
            || Event::class === ($name = $type->getName())
        ) {
            throw new InvalidArgumentException(\sprintf('Service "%s" must define the "event" attribute on "kernel.event_listener" tags.', $id));
        }

        return $name;
    }
}

/**
 * @internal
 */
class ExtractingEventDispatcher extends EventDispatcher implements EventSubscriberInterface
{
    public array $listeners = [];

    public static array $aliases = [];
    public static string $subscriber;

    public function addListener(string $eventName, callable|array $listener, int $priority = 0): void
    {
        $this->listeners[] = [$eventName, $listener[1], $priority];
    }

    public static function getSubscribedEvents(): array
    {
        $events = [];

        foreach ([self::$subscriber, 'getSubscribedEvents']() as $eventName => $params) {
            $events[self::$aliases[$eventName] ?? $eventName] = $params;
        }

        return $events;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\EventDispatcher;

use Symfony\Contracts\EventDispatcher\EventDispatcherInterface as ContractsEventDispatcherInterface;

/**
 * The EventDispatcherInterface is the central point of Symfony's event listener system.
 * Listeners are registered on the manager and events are dispatched through the
 * manager.
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
interface EventDispatcherInterface extends ContractsEventDispatcherInterface
{
    /**
     * Adds an event listener that listens on the specified events.
     *
     * @param int $priority The higher this value, the earlier an event
     *                      listener will be triggered in the chain (defaults to 0)
     *
     * @return void
     */
    public function addListener(string $eventName, callable $listener, int $priority = 0);

    /**
     * Adds an event subscriber.
     *
     * The subscriber is asked for all the events it is
     * interested in and added as a listener for these events.
     *
     * @return void
     */
    public function addSubscriber(EventSubscriberInterface $subscriber);

    /**
     * Removes an event listener from the specified events.
     *
     * @return void
     */
    public function removeListener(string $eventName, callable $listener);

    /**
     * @return void
     */
    public function removeSubscriber(EventSubscriberInterface $subscriber);

    /**
     * Gets the listeners of a specific event or all listeners sorted by descending priority.
     *
     * @return array<callable[]|callable>
     */
    public function getListeners(?string $eventName = null): array;

    /**
     * Gets the listener priority for a specific event.
     *
     * Returns null if the event or the listener does not exist.
     */
    public function getListenerPriority(string $eventName, callable $listener): ?int;

    /**
     * Checks whether an event has any registered listeners.
     */
    public function hasListeners(?string $eventName = null): bool;
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\EventDispatcher\Debug;

use Psr\EventDispatcher\StoppableEventInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Stopwatch\Stopwatch;
use Symfony\Component\VarDumper\Caster\ClassStub;

/**
 * @author Fabien Potencier <fabien@symfony.com>
 */
final class WrappedListener
{
    private string|array|object $listener;
    private ?\Closure $optimizedListener;
    private string $name;
    private bool $called = false;
    private bool $stoppedPropagation = false;
    private Stopwatch $stopwatch;
    private ?EventDispatcherInterface $dispatcher;
    private string $pretty;
    private string $callableRef;
    private ClassStub|string $stub;
    private ?int $priority = null;
    private static bool $hasClassStub;

    public function __construct(callable|array $listener, ?string $name, Stopwatch $stopwatch, ?EventDispatcherInterface $dispatcher = null, ?int $priority = null)
    {
        $this->listener = $listener;
        $this->optimizedListener = $listener instanceof \Closure ? $listener : (\is_callable($listener) ? $listener(...) : null);
        $this->stopwatch = $stopwatch;
        $this->dispatcher = $dispatcher;
        $this->priority = $priority;

        if (\is_array($listener)) {
            [$this->name, $this->callableRef] = $this->parseListener($listener);
            $this->pretty = $this->name.'::'.$listener[1];
            $this->callableRef .= '::'.$listener[1];
        } elseif ($listener instanceof \Closure) {
            $r = new \ReflectionFunction($listener);
            if (str_contains($r->name, '{closure')) {
                $this->pretty = $this->name = 'closure';
            } elseif ($class = \PHP_VERSION_ID >= 80111 ? $r->getClosureCalledClass() : $r->getClosureScopeClass()) {
                $this->name = $class->name;
                $this->pretty = $this->name.'::'.$r->name;
            } else {
                $this->pretty = $this->name = $r->name;
            }
        } elseif (\is_string($listener)) {
            $this->pretty = $this->name = $listener;
        } else {
            $this->name = get_debug_type($listener);
            $this->pretty = $this->name.'::__invoke';
            $this->callableRef = $listener::class.'::__invoke';
        }

        if (null !== $name) {
            $this->name = $name;
        }

        self::$hasClassStub ??= class_exists(ClassStub::class);
    }

    public function getWrappedListener(): callable|array
    {
        return $this->listener;
    }

    public function wasCalled(): bool
    {
        return $this->called;
    }

    public function stoppedPropagation(): bool
    {
        return $this->stoppedPropagation;
    }

    public function getPretty(): string
    {
        return $this->pretty;
    }

    public function getInfo(string $eventName): array
    {
        $this->stub ??= self::$hasClassStub ? new ClassStub($this->pretty.'()', $this->callableRef ?? $this->listener) : $this->pretty.'()';

        return [
            'event' => $eventName,
            'priority' => $this->priority ??= $this->dispatcher?->getListenerPriority($eventName, $this->listener),
            'pretty' => $this->pretty,
            'stub' => $this->stub,
        ];
    }

    public function __invoke(object $event, string $eventName, EventDispatcherInterface $dispatcher): void
    {
        $dispatcher = $this->dispatcher ?: $dispatcher;

        $this->called = true;
        $this->priority ??= $dispatcher->getListenerPriority($eventName, $this->listener);

        $e = $this->stopwatch->start($this->name, 'event_listener');

        try {
            ($this->optimizedListener ?? $this->listener)($event, $eventName, $dispatcher);
        } finally {
            if ($e->isStarted()) {
                $e->stop();
            }
        }

        if ($event instanceof StoppableEventInterface && $event->isPropagationStopped()) {
            $this->stoppedPropagation = true;
        }
    }

    private function parseListener(array $listener): array
    {
        if ($listener[0] instanceof \Closure) {
            foreach ((new \ReflectionFunction($listener[0]))->getAttributes(\Closure::class) as $attribute) {
                if ($name = $attribute->getArguments()['name'] ?? false) {
                    return [$name, $attribute->getArguments()['class'] ?? $name];
                }
            }
        }

        if (\is_object($listener[0])) {
            return [get_debug_type($listener[0]), $listener[0]::class];
        }

        return [$listener[0], $listener[0]];
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\EventDispatcher\Debug;

use Psr\EventDispatcher\StoppableEventInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Stopwatch\Stopwatch;
use Symfony\Contracts\Service\ResetInterface;

/**
 * Collects some data about event listeners.
 *
 * This event dispatcher delegates the dispatching to another one.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class TraceableEventDispatcher implements EventDispatcherInterface, ResetInterface
{
    protected $logger;
    protected $stopwatch;

    /**
     * @var \SplObjectStorage<WrappedListener, array{string, string}>|null
     */
    private ?\SplObjectStorage $callStack = null;
    private EventDispatcherInterface $dispatcher;
    private array $wrappedListeners = [];
    private array $orphanedEvents = [];
    private array $dispatchDepth = [];
    private array $calledListenerInfos = [];
    private array $calledOriginalListeners = [];
    private ?RequestStack $requestStack;
    private string $currentRequestHash = '';

    public function __construct(EventDispatcherInterface $dispatcher, Stopwatch $stopwatch, ?LoggerInterface $logger = null, ?RequestStack $requestStack = null)
    {
        $this->dispatcher = $dispatcher;
        $this->stopwatch = $stopwatch;
        $this->logger = $logger;
        $this->requestStack = $requestStack;
    }

    /**
     * @return void
     */
    public function addListener(string $eventName, callable|array $listener, int $priority = 0)
    {
        $this->dispatcher->addListener($eventName, $listener, $priority);
    }

    /**
     * @return void
     */
    public function addSubscriber(EventSubscriberInterface $subscriber)
    {
        $this->dispatcher->addSubscriber($subscriber);
    }

    /**
     * @return void
     */
    public function removeListener(string $eventName, callable|array $listener)
    {
        if (isset($this->wrappedListeners[$eventName])) {
            foreach ($this->wrappedListeners[$eventName] as $index => $wrappedListener) {
                if ($wrappedListener->getWrappedListener() === $listener || ($listener instanceof \Closure && $wrappedListener->getWrappedListener() == $listener)) {
                    $listener = $wrappedListener;
                    unset($this->wrappedListeners[$eventName][$index]);
                    break;
                }
            }
        }

        $this->dispatcher->removeListener($eventName, $listener);
    }

    /**
     * @return void
     */
    public function removeSubscriber(EventSubscriberInterface $subscriber)
    {
        $this->dispatcher->removeSubscriber($subscriber);
    }

    public function getListeners(?string $eventName = null): array
    {
        return $this->dispatcher->getListeners($eventName);
    }

    public function getListenerPriority(string $eventName, callable|array $listener): ?int
    {
        // we might have wrapped listeners for the event (if called while dispatching)
        // in that case get the priority by wrapper
        if (isset($this->wrappedListeners[$eventName])) {
            foreach ($this->wrappedListeners[$eventName] as $wrappedListener) {
                if ($wrappedListener->getWrappedListener() === $listener || ($listener instanceof \Closure && $wrappedListener->getWrappedListener() == $listener)) {
                    return $this->dispatcher->getListenerPriority($eventName, $wrappedListener);
                }
            }
        }

        return $this->dispatcher->getListenerPriority($eventName, $listener);
    }

    public function hasListeners(?string $eventName = null): bool
    {
        return $this->dispatcher->hasListeners($eventName);
    }

    public function dispatch(object $event, ?string $eventName = null): object
    {
        $eventName ??= $event::class;

        $this->callStack ??= new \SplObjectStorage();

        $currentRequestHash = $this->currentRequestHash = $this->requestStack && ($request = $this->requestStack->getCurrentRequest()) ? spl_object_hash($request) : '';

        if (null !== $this->logger && $event instanceof StoppableEventInterface && $event->isPropagationStopped()) {
            $this->logger->debug(\sprintf('The "%s" event is already stopped. No listeners have been called.', $eventName));
        }

        $this->preProcess($eventName);
        try {
            $this->beforeDispatch($eventName, $event);
            try {
                $e = $this->stopwatch->start($eventName, 'section');
                try {
                    $this->dispatcher->dispatch($event, $eventName);
                } finally {
                    if ($e->isStarted()) {
                        $e->stop();
                    }
                }
            } finally {
                $this->afterDispatch($eventName, $event);
            }
        } finally {
            $this->currentRequestHash = $currentRequestHash;
            $this->postProcess($eventName);
        }

        return $event;
    }

    public function getCalledListeners(?Request $request = null): array
    {
        if (!$this->calledListenerInfos) {
            return [];
        }

        $hash = $request ? spl_object_hash($request) : null;
        $called = [];

        foreach ($this->calledListenerInfos as $requestHash => $eventInfos) {
            if (null !== $hash && $hash !== $requestHash) {
                continue;
            }
            foreach ($eventInfos as $infos) {
                foreach ($infos as ['info' => $info, 'count' => $count]) {
                    while ($count--) {
                        $called[] = $info;
                    }
                }
            }
        }

        return $called;
    }

    public function getNotCalledListeners(?Request $request = null): array
    {
        try {
            $allListeners = $this->dispatcher instanceof EventDispatcher ? $this->getListenersWithPriority() : $this->getListenersWithoutPriority();
        } catch (\Exception $e) {
            $this->logger?->info('An exception was thrown while getting the uncalled listeners.', ['exception' => $e]);

            // unable to retrieve the uncalled listeners
            return [];
        }

        $hash = $request ? spl_object_hash($request) : null;
        $calledListeners = [];

        foreach ($this->calledOriginalListeners as $requestHash => $eventListeners) {
            if (null === $hash || $hash === $requestHash) {
                $calledListeners[] = array_merge(...array_values($eventListeners));
            }
        }

        $calledListeners = $calledListeners ? array_merge(...$calledListeners) : [];

        $notCalled = [];

        foreach ($allListeners as $eventName => $listeners) {
            foreach ($listeners as [$listener, $priority]) {
                if (!\in_array($listener, $calledListeners, true)) {
                    if (!$listener instanceof WrappedListener) {
                        $listener = new WrappedListener($listener, null, $this->stopwatch, $this, $priority);
                    }
                    $notCalled[] = $listener->getInfo($eventName);
                }
            }
        }

        uasort($notCalled, $this->sortNotCalledListeners(...));

        return $notCalled;
    }

    public function getOrphanedEvents(?Request $request = null): array
    {
        if ($request) {
            return $this->orphanedEvents[spl_object_hash($request)] ?? [];
        }

        if (!$this->orphanedEvents) {
            return [];
        }

        return array_merge(...array_values($this->orphanedEvents));
    }

    /**
     * @return void
     */
    public function reset()
    {
        $this->callStack = null;
        $this->wrappedListeners = [];
        $this->orphanedEvents = [];
        $this->currentRequestHash = '';
        $this->dispatchDepth = [];
        $this->calledListenerInfos = [];
        $this->calledOriginalListeners = [];
    }

    /**
     * Proxies all method calls to the original event dispatcher.
     *
     * @param string $method    The method name
     * @param array  $arguments The method arguments
     */
    public function __call(string $method, array $arguments): mixed
    {
        return $this->dispatcher->{$method}(...$arguments);
    }

    /**
     * Called before dispatching the event.
     *
     * @return void
     */
    protected function beforeDispatch(string $eventName, object $event)
    {
    }

    /**
     * Called after dispatching the event.
     *
     * @return void
     */
    protected function afterDispatch(string $eventName, object $event)
    {
    }

    private function preProcess(string $eventName): void
    {
        $this->dispatchDepth[$eventName] = ($this->dispatchDepth[$eventName] ?? 0) + 1;

        if (!$this->dispatcher->hasListeners($eventName)) {
            $this->orphanedEvents[$this->currentRequestHash][] = $eventName;

            return;
        }

        foreach ($this->dispatcher->getListeners($eventName) as $listener) {
            $priority = $this->getListenerPriority($eventName, $listener) ?? 0;
            $wrappedListener = new WrappedListener($listener instanceof WrappedListener ? $listener->getWrappedListener() : $listener, null, $this->stopwatch, $this);
            $this->wrappedListeners[$eventName][] = $wrappedListener;
            $this->dispatcher->removeListener($eventName, $listener);
            $this->dispatcher->addListener($eventName, $wrappedListener, $priority);
            $this->callStack[$wrappedListener] = [$eventName, $this->currentRequestHash];
        }
    }

    private function postProcess(string $eventName): void
    {
        if (null === $this->callStack) {
            return;
        }

        $this->dispatchDepth[$eventName] = ($this->dispatchDepth[$eventName] ?? 1) - 1;

        unset($this->wrappedListeners[$eventName]);
        $skipped = false;
        foreach ($this->dispatcher->getListeners($eventName) as $listener) {
            if (!$listener instanceof WrappedListener) { // #12845: a new listener was added during dispatch.
                continue;
            }
            // Unwrap listener
            $priority = $this->getListenerPriority($eventName, $listener);
            $this->dispatcher->removeListener($eventName, $listener);
            $this->dispatcher->addListener($eventName, $listener->getWrappedListener(), $priority);

            if (null !== $this->logger) {
                $context = ['event' => $eventName, 'listener' => $listener->getPretty()];
            }

            if ($listener->wasCalled()) {
                $this->logger?->debug('Notified event "{event}" to listener "{listener}".', $context);

                $this->collectCalledListener($listener, $eventName);
            }

            unset($this->callStack[$listener]);

            if (null !== $this->logger && $skipped) {
                $this->logger->debug('Listener "{listener}" was not called for event "{event}".', $context);
            }

            if ($listener->stoppedPropagation()) {
                $this->logger?->debug('Listener "{listener}" stopped propagation of the event "{event}".', $context);

                $skipped = true;
            }
        }

        if (0 < $this->dispatchDepth[$eventName]) {
            return;
        }

        // Clean up stale callStack entries left by nested same-event dispatches
        $stale = [];
        foreach ($this->callStack as $listener) {
            if ($this->callStack->getInfo()[0] === $eventName) {
                $stale[] = $listener;
            }
        }
        foreach ($stale as $listener) {
            if ($listener->wasCalled()) {
                $this->collectCalledListener($listener, $eventName);
            }
            unset($this->callStack[$listener]);
        }
    }

    /**
     * Counts an invocation without storing per-invocation state, so that the
     * aggregate cannot grow when the same listeners are called repeatedly.
     */
    private function collectCalledListener(WrappedListener $listener, string $eventName): void
    {
        $original = $listener->getWrappedListener();

        if (false === $i = array_search($original, $this->calledOriginalListeners[$this->currentRequestHash][$eventName] ?? [], true)) {
            $this->calledOriginalListeners[$this->currentRequestHash][$eventName][] = $original;
            $this->calledListenerInfos[$this->currentRequestHash][$eventName][] = ['info' => $listener->getInfo($eventName), 'count' => 1];
        } else {
            ++$this->calledListenerInfos[$this->currentRequestHash][$eventName][$i]['count'];
        }
    }

    private function sortNotCalledListeners(array $a, array $b): int
    {
        if (0 !== $cmp = strcmp($a['event'], $b['event'])) {
            return $cmp;
        }

        if (\is_int($a['priority']) && !\is_int($b['priority'])) {
            return 1;
        }

        if (!\is_int($a['priority']) && \is_int($b['priority'])) {
            return -1;
        }

        if ($a['priority'] === $b['priority']) {
            return 0;
        }

        if ($a['priority'] > $b['priority']) {
            return -1;
        }

        return 1;
    }

    private function getListenersWithPriority(): array
    {
        $result = [];

        $allListeners = new \ReflectionProperty(EventDispatcher::class, 'listeners');

        foreach ($allListeners->getValue($this->dispatcher) as $eventName => $listenersByPriority) {
            foreach ($listenersByPriority as $priority => $listeners) {
                foreach ($listeners as $listener) {
                    $result[$eventName][] = [$listener, $priority];
                }
            }
        }

        return $result;
    }

    private function getListenersWithoutPriority(): array
    {
        $result = [];

        foreach ($this->getListeners() as $eventName => $listeners) {
            foreach ($listeners as $listener) {
                $result[$eventName][] = [$listener, null];
            }
        }

        return $result;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\EventDispatcher\Attribute;

/**
 * Service tag to autoconfigure event listeners.
 *
 * @author Alexander M. Turek <me@derrabus.de>
 */
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
class AsEventListener
{
    public function __construct(
        public ?string $event = null,
        public ?string $method = null,
        public int $priority = 0,
        public ?string $dispatcher = null,
    ) {
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\EventDispatcher;

use Psr\EventDispatcher\StoppableEventInterface;
use Symfony\Component\EventDispatcher\Debug\WrappedListener;

/**
 * The EventDispatcherInterface is the central point of Symfony's event listener system.
 *
 * Listeners are registered on the manager and events are dispatched through the
 * manager.
 *
 * @author Guilherme Blanco <guilhermeblanco@hotmail.com>
 * @author Jonathan Wage <jonwage@gmail.com>
 * @author Roman Borschel <roman@code-factory.org>
 * @author Bernhard Schussek <bschussek@gmail.com>
 * @author Fabien Potencier <fabien@symfony.com>
 * @author Jordi Boggiano <j.boggiano@seld.be>
 * @author Jordan Alliot <jordan.alliot@gmail.com>
 * @author Nicolas Grekas <p@tchwork.com>
 */
class EventDispatcher implements EventDispatcherInterface
{
    private array $listeners = [];
    private array $sorted = [];
    private array $optimized;

    public function __construct()
    {
        if (__CLASS__ === static::class) {
            $this->optimized = [];
        }
    }

    public function dispatch(object $event, ?string $eventName = null): object
    {
        $eventName ??= $event::class;

        if (isset($this->optimized)) {
            $listeners = $this->optimized[$eventName] ?? (empty($this->listeners[$eventName]) ? [] : $this->optimizeListeners($eventName));
        } else {
            $listeners = $this->getListeners($eventName);
        }

        if ($listeners) {
            $this->callListeners($listeners, $eventName, $event);
        }

        return $event;
    }

    public function getListeners(?string $eventName = null): array
    {
        if (null !== $eventName) {
            if (empty($this->listeners[$eventName])) {
                return [];
            }

            if (!isset($this->sorted[$eventName])) {
                $this->sortListeners($eventName);
            }

            return $this->sorted[$eventName];
        }

        foreach ($this->listeners as $eventName => $eventListeners) {
            if (!isset($this->sorted[$eventName])) {
                $this->sortListeners($eventName);
            }
        }

        return array_filter($this->sorted);
    }

    public function getListenerPriority(string $eventName, callable|array $listener): ?int
    {
        if (empty($this->listeners[$eventName])) {
            return null;
        }

        if (\is_array($listener) && isset($listener[0]) && $listener[0] instanceof \Closure && 2 >= \count($listener)) {
            $listener[0] = $listener[0]();
            $listener[1] ??= '__invoke';
        }

        foreach ($this->listeners[$eventName] as $priority => &$listeners) {
            foreach ($listeners as &$v) {
                if ($v !== $listener && \is_array($v) && isset($v[0]) && $v[0] instanceof \Closure && 2 >= \count($v)) {
                    $v[0] = $v[0]();
                    $v[1] ??= '__invoke';
                }
                if ($v === $listener || ($listener instanceof \Closure && $v == $listener)) {
                    return $priority;
                }
            }
        }

        return null;
    }

    public function hasListeners(?string $eventName = null): bool
    {
        if (null !== $eventName) {
            return !empty($this->listeners[$eventName]);
        }

        foreach ($this->listeners as $eventListeners) {
            if ($eventListeners) {
                return true;
            }
        }

        return false;
    }

    /**
     * @return void
     */
    public function addListener(string $eventName, callable|array $listener, int $priority = 0)
    {
        $this->listeners[$eventName][$priority][] = $listener;
        unset($this->sorted[$eventName], $this->optimized[$eventName]);
    }

    /**
     * @return void
     */
    public function removeListener(string $eventName, callable|array $listener)
    {
        if (empty($this->listeners[$eventName])) {
            return;
        }

        if (\is_array($listener) && isset($listener[0]) && $listener[0] instanceof \Closure && 2 >= \count($listener)) {
            $listener[0] = $listener[0]();
            $listener[1] ??= '__invoke';
        }

        foreach ($this->listeners[$eventName] as $priority => &$listeners) {
            foreach ($listeners as $k => &$v) {
                if ($v !== $listener && \is_array($v) && isset($v[0]) && $v[0] instanceof \Closure && 2 >= \count($v)) {
                    $v[0] = $v[0]();
                    $v[1] ??= '__invoke';
                }
                if ($v === $listener || ($listener instanceof \Closure && $v == $listener)) {
                    unset($listeners[$k], $this->sorted[$eventName], $this->optimized[$eventName]);
                }
            }

            if (!$listeners) {
                unset($this->listeners[$eventName][$priority]);
            }
        }
    }

    /**
     * @return void
     */
    public function addSubscriber(EventSubscriberInterface $subscriber)
    {
        foreach ($subscriber->getSubscribedEvents() as $eventName => $params) {
            if (\is_string($params)) {
                $this->addListener($eventName, [$subscriber, $params]);
            } elseif (\is_string($params[0])) {
                $this->addListener($eventName, [$subscriber, $params[0]], $params[1] ?? 0);
            } else {
                foreach ($params as $listener) {
                    $this->addListener($eventName, [$subscriber, $listener[0]], $listener[1] ?? 0);
                }
            }
        }
    }

    /**
     * @return void
     */
    public function removeSubscriber(EventSubscriberInterface $subscriber)
    {
        foreach ($subscriber->getSubscribedEvents() as $eventName => $params) {
            if (\is_array($params) && \is_array($params[0])) {
                foreach ($params as $listener) {
                    $this->removeListener($eventName, [$subscriber, $listener[0]]);
                }
            } else {
                $this->removeListener($eventName, [$subscriber, \is_string($params) ? $params : $params[0]]);
            }
        }
    }

    /**
     * Triggers the listeners of an event.
     *
     * This method can be overridden to add functionality that is executed
     * for each listener.
     *
     * @param callable[] $listeners The event listeners
     * @param string     $eventName The name of the event to dispatch
     * @param object     $event     The event object to pass to the event handlers/listeners
     *
     * @return void
     */
    protected function callListeners(iterable $listeners, string $eventName, object $event)
    {
        $stoppable = $event instanceof StoppableEventInterface;

        foreach ($listeners as $listener) {
            if ($stoppable && $event->isPropagationStopped()) {
                break;
            }
            $listener($event, $eventName, $this);
        }
    }

    /**
     * Sorts the internal list of listeners for the given event by priority.
     */
    private function sortListeners(string $eventName): void
    {
        krsort($this->listeners[$eventName]);
        $this->sorted[$eventName] = [];

        foreach ($this->listeners[$eventName] as &$listeners) {
            foreach ($listeners as &$listener) {
                if (\is_array($listener) && isset($listener[0]) && $listener[0] instanceof \Closure && 2 >= \count($listener)) {
                    $listener[0] = $listener[0]();
                    $listener[1] ??= '__invoke';
                }
                $this->sorted[$eventName][] = $listener;
            }
        }
    }

    /**
     * Optimizes the internal list of listeners for the given event by priority.
     */
    private function optimizeListeners(string $eventName): array
    {
        krsort($this->listeners[$eventName]);
        $this->optimized[$eventName] = [];

        foreach ($this->listeners[$eventName] as &$listeners) {
            foreach ($listeners as &$listener) {
                $closure = &$this->optimized[$eventName][];
                if (\is_array($listener) && isset($listener[0]) && $listener[0] instanceof \Closure && 2 >= \count($listener)) {
                    $closure = static function (...$args) use (&$listener, &$closure) {
                        if ($listener[0] instanceof \Closure) {
                            $listener[0] = $listener[0]();
                            $listener[1] ??= '__invoke';
                        }
                        ($closure = $listener(...))(...$args);
                    };
                } else {
                    $closure = $listener instanceof WrappedListener ? $listener : $listener(...);
                }
            }
        }

        return $this->optimized[$eventName];
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\EventDispatcher;

/**
 * A read-only proxy for an event dispatcher.
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
class ImmutableEventDispatcher implements EventDispatcherInterface
{
    private EventDispatcherInterface $dispatcher;

    public function __construct(EventDispatcherInterface $dispatcher)
    {
        $this->dispatcher = $dispatcher;
    }

    public function dispatch(object $event, ?string $eventName = null): object
    {
        return $this->dispatcher->dispatch($event, $eventName);
    }

    /**
     * @return never
     */
    public function addListener(string $eventName, callable|array $listener, int $priority = 0)
    {
        throw new \BadMethodCallException('Unmodifiable event dispatchers must not be modified.');
    }

    /**
     * @return never
     */
    public function addSubscriber(EventSubscriberInterface $subscriber)
    {
        throw new \BadMethodCallException('Unmodifiable event dispatchers must not be modified.');
    }

    /**
     * @return never
     */
    public function removeListener(string $eventName, callable|array $listener)
    {
        throw new \BadMethodCallException('Unmodifiable event dispatchers must not be modified.');
    }

    /**
     * @return never
     */
    public function removeSubscriber(EventSubscriberInterface $subscriber)
    {
        throw new \BadMethodCallException('Unmodifiable event dispatchers must not be modified.');
    }

    public function getListeners(?string $eventName = null): array
    {
        return $this->dispatcher->getListeners($eventName);
    }

    public function getListenerPriority(string $eventName, callable|array $listener): ?int
    {
        return $this->dispatcher->getListenerPriority($eventName, $listener);
    }

    public function hasListeners(?string $eventName = null): bool
    {
        return $this->dispatcher->hasListeners($eventName);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\EventDispatcher;

use Symfony\Contracts\EventDispatcher\Event;

/**
 * Event encapsulation class.
 *
 * Encapsulates events thus decoupling the observer from the subject they encapsulate.
 *
 * @author Drak <drak@zikula.org>
 *
 * @implements \ArrayAccess<string, mixed>
 * @implements \IteratorAggregate<string, mixed>
 */
class GenericEvent extends Event implements \ArrayAccess, \IteratorAggregate
{
    protected $subject;
    protected $arguments;

    /**
     * Encapsulate an event with $subject and $arguments.
     *
     * @param mixed $subject   The subject of the event, usually an object or a callable
     * @param array $arguments Arguments to store in the event
     */
    public function __construct(mixed $subject = null, array $arguments = [])
    {
        $this->subject = $subject;
        $this->arguments = $arguments;
    }

    /**
     * Getter for subject property.
     */
    public function getSubject(): mixed
    {
        return $this->subject;
    }

    /**
     * Get argument by key.
     *
     * @throws \InvalidArgumentException if key is not found
     */
    public function getArgument(string $key): mixed
    {
        if ($this->hasArgument($key)) {
            return $this->arguments[$key];
        }

        throw new \InvalidArgumentException(\sprintf('Argument "%s" not found.', $key));
    }

    /**
     * Add argument to event.
     *
     * @return $this
     */
    public function setArgument(string $key, mixed $value): static
    {
        $this->arguments[$key] = $value;

        return $this;
    }

    /**
     * Getter for all arguments.
     */
    public function getArguments(): array
    {
        return $this->arguments;
    }

    /**
     * Set args property.
     *
     * @return $this
     */
    public function setArguments(array $args = []): static
    {
        $this->arguments = $args;

        return $this;
    }

    /**
     * Has argument.
     */
    public function hasArgument(string $key): bool
    {
        return \array_key_exists($key, $this->arguments);
    }

    /**
     * ArrayAccess for argument getter.
     *
     * @param string $key Array key
     *
     * @throws \InvalidArgumentException if key does not exist in $this->args
     */
    public function offsetGet(mixed $key): mixed
    {
        return $this->getArgument($key);
    }

    /**
     * ArrayAccess for argument setter.
     *
     * @param string $key Array key to set
     */
    public function offsetSet(mixed $key, mixed $value): void
    {
        $this->setArgument($key, $value);
    }

    /**
     * ArrayAccess for unset argument.
     *
     * @param string $key Array key
     */
    public function offsetUnset(mixed $key): void
    {
        if ($this->hasArgument($key)) {
            unset($this->arguments[$key]);
        }
    }

    /**
     * ArrayAccess has argument.
     *
     * @param string $key Array key
     */
    public function offsetExists(mixed $key): bool
    {
        return $this->hasArgument($key);
    }

    /**
     * IteratorAggregate for iterating over the object like an array.
     *
     * @return \ArrayIterator<string, mixed>
     */
    public function getIterator(): \ArrayIterator
    {
        return new \ArrayIterator($this->arguments);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\EventDispatcher;

/**
 * An EventSubscriber knows itself what events it is interested in.
 * If an EventSubscriber is added to an EventDispatcherInterface, the manager invokes
 * {@link getSubscribedEvents} and registers the subscriber as a listener for all
 * returned events.
 *
 * @author Guilherme Blanco <guilhermeblanco@hotmail.com>
 * @author Jonathan Wage <jonwage@gmail.com>
 * @author Roman Borschel <roman@code-factory.org>
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
interface EventSubscriberInterface
{
    /**
     * Returns an array of event names this subscriber wants to listen to.
     *
     * The array keys are event names and the value can be:
     *
     *  * The method name to call (priority defaults to 0)
     *  * An array composed of the method name to call and the priority
     *  * An array of arrays composed of the method names to call and respective
     *    priorities, or 0 if unset
     *
     * For instance:
     *
     *  * ['eventName' => 'methodName']
     *  * ['eventName' => ['methodName', $priority]]
     *  * ['eventName' => [['methodName1', $priority], ['methodName2']]]
     *
     * The code must not depend on runtime state as it will only be called at compile time.
     * All logic depending on runtime state must be put into the individual methods handling the events.
     *
     * @return array<string, string|array{0: string, 1: int}|list<array{0: string, 1?: int}>>
     */
    public static function getSubscribedEvents();
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Util;

use Symfony\Component\Config\Util\Exception\InvalidXmlException;
use Symfony\Component\Config\Util\Exception\XmlParsingException;

/**
 * XMLUtils is a bunch of utility methods to XML operations.
 *
 * This class contains static methods only and is not meant to be instantiated.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 * @author Martin Hasoň <martin.hason@gmail.com>
 * @author Ole Rößner <ole@roessner.it>
 */
class XmlUtils
{
    /**
     * This class should not be instantiated.
     */
    private function __construct()
    {
    }

    /**
     * Parses an XML string.
     *
     * @param string               $content          An XML string
     * @param string|callable|null $schemaOrCallable An XSD schema file path, a callable, or null to disable validation
     *
     * @throws XmlParsingException When parsing of XML file returns error
     * @throws InvalidXmlException When parsing of XML with schema or callable produces any errors unrelated to the XML parsing itself
     * @throws \RuntimeException   When DOM extension is missing
     */
    public static function parse(string $content, string|callable|null $schemaOrCallable = null): \DOMDocument
    {
        if (!\extension_loaded('dom')) {
            throw new \LogicException('Extension DOM is required.');
        }

        $internalErrors = libxml_use_internal_errors(true);
        libxml_clear_errors();

        $dom = new \DOMDocument();
        $dom->validateOnParse = true;
        if (!$dom->loadXML($content, \LIBXML_NONET | \LIBXML_COMPACT)) {
            throw new XmlParsingException(implode("\n", static::getXmlErrors($internalErrors)));
        }

        $dom->normalizeDocument();

        libxml_use_internal_errors($internalErrors);

        foreach ($dom->childNodes as $child) {
            if (\XML_DOCUMENT_TYPE_NODE === $child->nodeType) {
                throw new XmlParsingException('Document types are not allowed.');
            }
        }

        if (null !== $schemaOrCallable) {
            $internalErrors = libxml_use_internal_errors(true);
            libxml_clear_errors();

            $e = null;
            if (\is_callable($schemaOrCallable)) {
                try {
                    $valid = $schemaOrCallable($dom, $internalErrors);
                } catch (\Exception $e) {
                    $valid = false;
                }
            } elseif (is_file($schemaOrCallable)) {
                $schemaSource = file_get_contents((string) $schemaOrCallable);
                $valid = @$dom->schemaValidateSource($schemaSource);
            } else {
                libxml_use_internal_errors($internalErrors);

                throw new XmlParsingException(\sprintf('Invalid XSD file: "%s".', $schemaOrCallable));
            }

            if (!$valid) {
                $messages = static::getXmlErrors($internalErrors);
                if (!$messages) {
                    throw new InvalidXmlException('The XML is not valid.', 0, $e);
                }
                throw new XmlParsingException(implode("\n", $messages), 0, $e);
            }
        }

        libxml_clear_errors();
        libxml_use_internal_errors($internalErrors);

        return $dom;
    }

    /**
     * Loads an XML file.
     *
     * @param string               $file             An XML file path
     * @param string|callable|null $schemaOrCallable An XSD schema file path, a callable, or null to disable validation
     *
     * @throws \InvalidArgumentException When loading of XML file returns error
     * @throws XmlParsingException       When XML parsing returns any errors
     * @throws \RuntimeException         When DOM extension is missing
     */
    public static function loadFile(string $file, string|callable|null $schemaOrCallable = null): \DOMDocument
    {
        if (!is_file($file)) {
            throw new \InvalidArgumentException(\sprintf('Resource "%s" is not a file.', $file));
        }

        if (!is_readable($file)) {
            throw new \InvalidArgumentException(\sprintf('File "%s" is not readable.', $file));
        }

        $content = @file_get_contents($file);

        if ('' === trim($content)) {
            throw new \InvalidArgumentException(\sprintf('File "%s" does not contain valid XML, it is empty.', $file));
        }

        try {
            return static::parse($content, $schemaOrCallable);
        } catch (InvalidXmlException $e) {
            throw new XmlParsingException(\sprintf('The XML file "%s" is not valid.', $file), 0, $e->getPrevious());
        }
    }

    /**
     * Converts a \DOMElement object to a PHP array.
     *
     * The following rules applies during the conversion:
     *
     *  * Each tag is converted to a key value or an array
     *    if there is more than one "value"
     *
     *  * The content of a tag is set under a "value" key (<foo>bar</foo>)
     *    if the tag also has some nested tags
     *
     *  * The attributes are converted to keys (<foo foo="bar"/>)
     *
     *  * The nested-tags are converted to keys (<foo><foo>bar</foo></foo>)
     *
     * @param \DOMElement $element     A \DOMElement instance
     * @param bool        $checkPrefix Check prefix in an element or an attribute name
     */
    public static function convertDomElementToArray(\DOMElement $element, bool $checkPrefix = true): mixed
    {
        $prefix = (string) $element->prefix;
        $empty = true;
        $config = [];
        foreach ($element->attributes as $name => $node) {
            if ($checkPrefix && !\in_array((string) $node->prefix, ['', $prefix], true)) {
                continue;
            }
            $config[$name] = static::phpize($node->value);
            $empty = false;
        }

        $nodeValue = false;
        foreach ($element->childNodes as $node) {
            if ($node instanceof \DOMText) {
                if ('' !== trim($node->nodeValue)) {
                    $nodeValue = trim($node->nodeValue);
                    $empty = false;
                }
            } elseif ($checkPrefix && $prefix != (string) $node->prefix) {
                continue;
            } elseif (!$node instanceof \DOMComment) {
                $value = static::convertDomElementToArray($node, $checkPrefix);

                $key = $node->localName;
                if (isset($config[$key])) {
                    if (!\is_array($config[$key]) || !\is_int(key($config[$key]))) {
                        $config[$key] = [$config[$key]];
                    }
                    $config[$key][] = $value;
                } else {
                    $config[$key] = $value;
                }

                $empty = false;
            }
        }

        if (false !== $nodeValue) {
            $value = static::phpize($nodeValue);
            if (\count($config)) {
                $config['value'] = $value;
            } else {
                $config = $value;
            }
        }

        return !$empty ? $config : null;
    }

    /**
     * Converts an xml value to a PHP type.
     */
    public static function phpize(string|\Stringable $value): mixed
    {
        $value = (string) $value;
        $lowercaseValue = strtolower($value);

        switch (true) {
            case 'null' === $lowercaseValue:
                return null;
            case ctype_digit($value):
            case isset($value[1]) && '-' === $value[0] && ctype_digit(substr($value, 1)):
                $raw = $value;
                $cast = (int) $value;

                return self::isOctal($value) ? \intval($value, 8) : (($raw === (string) $cast) ? $cast : $raw);
            case 'true' === $lowercaseValue:
                return true;
            case 'false' === $lowercaseValue:
                return false;
            case isset($value[1]) && '0b' == $value[0].$value[1] && preg_match('/^0b[01]*$/', $value):
                return bindec($value);
            case is_numeric($value):
                return '0x' === $value[0].$value[1] ? hexdec($value) : (float) $value;
            case preg_match('/^0x[0-9a-f]++$/i', $value):
                return hexdec($value);
            case preg_match('/^[+-]?[0-9]+(\.[0-9]+)?$/', $value):
                return (float) $value;
            default:
                return $value;
        }
    }

    /**
     * @return array
     */
    protected static function getXmlErrors(bool $internalErrors)
    {
        $errors = [];
        foreach (libxml_get_errors() as $error) {
            $errors[] = \sprintf('[%s %s] %s (in %s - line %d, column %d)',
                \LIBXML_ERR_WARNING == $error->level ? 'WARNING' : 'ERROR',
                $error->code,
                trim($error->message),
                $error->file ?: 'n/a',
                $error->line,
                $error->column
            );
        }

        libxml_clear_errors();
        libxml_use_internal_errors($internalErrors);

        return $errors;
    }

    private static function isOctal(string $str): bool
    {
        if ('-' === $str[0]) {
            $str = substr($str, 1);
        }

        return $str === '0'.decoct(\intval($str, 8));
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Util\Exception;

/**
 * Exception class for when XML parsing with an XSD schema file path or a callable validator produces errors unrelated
 * to the actual XML parsing.
 *
 * @author Ole Rößner <ole@roessner.it>
 */
class InvalidXmlException extends XmlParsingException
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Util\Exception;

/**
 * Exception class for when XML cannot be parsed properly.
 *
 * @author Ole Rößner <ole@roessner.it>
 */
class XmlParsingException extends \InvalidArgumentException
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Exception;

/**
 * Exception class for when a resource cannot be loaded or imported.
 *
 * @author Ryan Weaver <ryan@thatsquality.com>
 */
class LoaderLoadException extends \Exception
{
    /**
     * @param mixed           $resource       The resource that could not be imported
     * @param string|null     $sourceResource The original resource importing the new resource
     * @param int             $code           The error code
     * @param \Throwable|null $previous       A previous exception
     * @param string|null     $type           The type of resource
     */
    public function __construct(mixed $resource, ?string $sourceResource = null, int $code = 0, ?\Throwable $previous = null, ?string $type = null)
    {
        if (!\is_string($resource)) {
            try {
                $resource = json_encode($resource, \JSON_THROW_ON_ERROR);
            } catch (\JsonException) {
                $resource = \sprintf('resource of type "%s"', get_debug_type($resource));
            }
        }

        $message = '';
        if ($previous) {
            // Include the previous exception, to help the user see what might be the underlying cause

            // Trim the trailing period of the previous message. We only want 1 period remove so no rtrim...
            if (str_ends_with($previous->getMessage(), '.')) {
                $trimmedMessage = substr($previous->getMessage(), 0, -1);
                $message .= \sprintf('%s', $trimmedMessage).' in ';
            } else {
                $message .= \sprintf('%s', $previous->getMessage()).' in ';
            }
            $message .= $resource.' ';

            // show tweaked trace to complete the human readable sentence
            if (null === $sourceResource) {
                $message .= \sprintf('(which is loaded in resource "%s")', $resource);
            } else {
                $message .= \sprintf('(which is being imported from "%s")', $sourceResource);
            }
            $message .= '.';

        // if there's no previous message, present it the default way
        } elseif (null === $sourceResource) {
            $message .= \sprintf('Cannot load resource "%s".', $resource);
        } else {
            $message .= \sprintf('Cannot import resource "%s" from "%s".', $resource, $sourceResource);
        }

        // Is the resource located inside a bundle?
        if ('@' === $resource[0]) {
            $parts = explode(\DIRECTORY_SEPARATOR, $resource);
            $bundle = substr($parts[0], 1);
            $message .= \sprintf(' Make sure the "%s" bundle is correctly registered and loaded in the application kernel class.', $bundle);
            $message .= \sprintf(' If the bundle is registered, make sure the bundle path "%s" is not empty.', $resource);
        } elseif (null !== $type) {
            $message .= \sprintf(' Make sure there is a loader supporting the "%s" type.', $type);
        }

        parent::__construct($message, $code, $previous);
    }

    /**
     * @return string
     */
    protected function varToString(mixed $var)
    {
        if (\is_object($var)) {
            return \sprintf('Object(%s)', $var::class);
        }

        if (\is_array($var)) {
            $a = [];
            foreach ($var as $k => $v) {
                $a[] = \sprintf('%s => %s', $k, $this->varToString($v));
            }

            return \sprintf('Array(%s)', implode(', ', $a));
        }

        if (\is_resource($var)) {
            return \sprintf('Resource(%s)', get_resource_type($var));
        }

        if (null === $var) {
            return 'null';
        }

        if (false === $var) {
            return 'false';
        }

        if (true === $var) {
            return 'true';
        }

        return (string) $var;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Exception;

/**
 * Exception class for when a circular reference is detected when importing resources.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class FileLoaderImportCircularReferenceException extends LoaderLoadException
{
    public function __construct(array $resources, int $code = 0, ?\Throwable $previous = null)
    {
        $message = \sprintf('Circular reference detected in "%s" ("%s" > "%s").', $this->varToString($resources[0]), implode('" > "', $resources), $resources[0]);

        \Exception::__construct($message, $code, $previous);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Exception;

/**
 * File locator exception if a file does not exist.
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
class FileLocatorFileNotFoundException extends \InvalidArgumentException
{
    private array $paths;

    public function __construct(string $message = '', int $code = 0, ?\Throwable $previous = null, array $paths = [])
    {
        parent::__construct($message, $code, $previous);

        $this->paths = $paths;
    }

    /**
     * @return array
     */
    public function getPaths()
    {
        return $this->paths;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Loader;

use Symfony\Component\Config\Exception\FileLoaderImportCircularReferenceException;
use Symfony\Component\Config\Exception\FileLocatorFileNotFoundException;
use Symfony\Component\Config\Exception\LoaderLoadException;
use Symfony\Component\Config\FileLocatorInterface;
use Symfony\Component\Config\Resource\FileExistenceResource;
use Symfony\Component\Config\Resource\GlobResource;

/**
 * FileLoader is the abstract class used by all built-in loaders that are file based.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
abstract class FileLoader extends Loader
{
    protected static $loading = [];

    protected $locator;

    private ?string $currentDir = null;

    public function __construct(FileLocatorInterface $locator, ?string $env = null)
    {
        $this->locator = $locator;
        parent::__construct($env);
    }

    /**
     * Sets the current directory.
     *
     * @return void
     */
    public function setCurrentDir(string $dir)
    {
        $this->currentDir = $dir;
    }

    /**
     * Returns the file locator used by this loader.
     */
    public function getLocator(): FileLocatorInterface
    {
        return $this->locator;
    }

    /**
     * Imports a resource.
     *
     * @param mixed                $resource       A Resource
     * @param string|null          $type           The resource type or null if unknown
     * @param bool                 $ignoreErrors   Whether to ignore import errors or not
     * @param string|null          $sourceResource The original resource importing the new resource
     * @param string|string[]|null $exclude        Glob patterns to exclude from the import
     *
     * @return mixed
     *
     * @throws LoaderLoadException
     * @throws FileLoaderImportCircularReferenceException
     * @throws FileLocatorFileNotFoundException
     */
    public function import(mixed $resource, ?string $type = null, bool $ignoreErrors = false, ?string $sourceResource = null, string|array|null $exclude = null)
    {
        $excluded = [];
        foreach ((array) $exclude as $pattern) {
            foreach ($this->glob($pattern, true, $_, false, true) as $path => $info) {
                // normalize Windows slashes and remove trailing slashes
                $excluded[rtrim(str_replace('\\', '/', $path), '/')] = true;
            }
        }

        if (\is_string($resource) && !class_exists($resource)) {
            $isGlobPattern = \strlen($resource) !== strcspn($resource, '*?{[');

            if (!$isGlobPattern && $excluded) {
                $resource = rtrim(str_replace('\\', '/', $resource), '/');
                $resource .= '/**/*';
                $isGlobPattern = true;
            }

            if ($isGlobPattern && !str_contains($resource, "\n")) {
                $ret = [];
                $i = strcspn($resource, '*?{[');
                $isSubpath = 0 !== $i && str_contains(substr($resource, 0, $i), '/');
                foreach ($this->glob($resource, false, $_, $ignoreErrors || !$isSubpath, false, $excluded) as $path => $info) {
                    if (null !== $res = $this->doImport($path, 'glob' === $type ? null : $type, $ignoreErrors, $sourceResource)) {
                        $ret[] = $res;
                    }
                    $isSubpath = true;
                }

                if ($isSubpath) {
                    return isset($ret[1]) ? $ret : ($ret[0] ?? null);
                }
            }
        } elseif (\is_array($resource) && $excluded) {
            $resource['_excluded'] = $excluded;
        }

        return $this->doImport($resource, $type, $ignoreErrors, $sourceResource);
    }

    /**
     * @internal
     */
    protected function glob(string $pattern, bool $recursive, array|GlobResource|null &$resource = null, bool $ignoreErrors = false, bool $forExclusion = false, array $excluded = []): iterable
    {
        if (\strlen($pattern) === $i = strcspn($pattern, '*?{[')) {
            $prefix = $pattern;
            $pattern = '';
        } elseif (0 === $i || !str_contains(substr($pattern, 0, $i), '/')) {
            $prefix = '.';
            $pattern = '/'.$pattern;
        } else {
            $prefix = \dirname(substr($pattern, 0, 1 + $i));
            $pattern = substr($pattern, \strlen($prefix));
        }

        try {
            $prefix = $this->locator->locate($prefix, $this->currentDir, true);
        } catch (FileLocatorFileNotFoundException $e) {
            if (!$ignoreErrors) {
                throw $e;
            }

            $resource = [];
            foreach ($e->getPaths() as $path) {
                $resource[] = new FileExistenceResource($path);
            }

            return;
        }
        $resource = new GlobResource($prefix, $pattern, $recursive, $forExclusion, $excluded);

        yield from $resource;
    }

    private function doImport(mixed $resource, ?string $type = null, bool $ignoreErrors = false, ?string $sourceResource = null): mixed
    {
        try {
            $loader = $this->resolve($resource, $type);

            if ($loader instanceof DirectoryAwareLoaderInterface) {
                $loader = $loader->forDirectory($this->currentDir);
            }

            if (!$loader instanceof self) {
                return $loader->load($resource, $type);
            }

            if (null !== $this->currentDir) {
                $resource = $loader->getLocator()->locate($resource, $this->currentDir, false);
            }

            $resources = \is_array($resource) ? $resource : [$resource];
            for ($i = 0; $i < $resourcesCount = \count($resources); ++$i) {
                if (isset(self::$loading[$resources[$i]])) {
                    if ($i == $resourcesCount - 1) {
                        throw new FileLoaderImportCircularReferenceException(array_keys(self::$loading));
                    }
                } else {
                    $resource = $resources[$i];
                    break;
                }
            }
            self::$loading[$resource] = true;

            try {
                $ret = $loader->load($resource, $type);
            } finally {
                unset(self::$loading[$resource]);
            }

            return $ret;
        } catch (FileLoaderImportCircularReferenceException $e) {
            throw $e;
        } catch (\Exception $e) {
            if (!$ignoreErrors) {
                // prevent embedded imports from nesting multiple exceptions
                if ($e instanceof LoaderLoadException) {
                    throw $e;
                }

                throw new LoaderLoadException($resource, $sourceResource, 0, $e, $type);
            }
        }

        return null;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Loader;

/**
 * LoaderInterface is the interface implemented by all loader classes.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
interface LoaderInterface
{
    /**
     * Loads a resource.
     *
     * @return mixed
     *
     * @throws \Exception If something went wrong
     */
    public function load(mixed $resource, ?string $type = null);

    /**
     * Returns whether this class supports the given resource.
     *
     * @param mixed $resource A resource
     *
     * @return bool
     */
    public function supports(mixed $resource, ?string $type = null);

    /**
     * Gets the loader resolver.
     *
     * @return LoaderResolverInterface
     */
    public function getResolver();

    /**
     * Sets the loader resolver.
     *
     * @return void
     */
    public function setResolver(LoaderResolverInterface $resolver);
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Loader;

/**
 * GlobFileLoader loads files from a glob pattern.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class GlobFileLoader extends FileLoader
{
    public function load(mixed $resource, ?string $type = null): mixed
    {
        return $this->import($resource);
    }

    public function supports(mixed $resource, ?string $type = null): bool
    {
        return 'glob' === $type;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Loader;

/**
 * LoaderResolver selects a loader for a given resource.
 *
 * A resource can be anything (e.g. a full path to a config file or a Closure).
 * Each loader determines whether it can load a resource and how.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class LoaderResolver implements LoaderResolverInterface
{
    /**
     * @var LoaderInterface[] An array of LoaderInterface objects
     */
    private array $loaders = [];

    /**
     * @param LoaderInterface[] $loaders An array of loaders
     */
    public function __construct(array $loaders = [])
    {
        foreach ($loaders as $loader) {
            $this->addLoader($loader);
        }
    }

    public function resolve(mixed $resource, ?string $type = null): LoaderInterface|false
    {
        foreach ($this->loaders as $loader) {
            if ($loader->supports($resource, $type)) {
                return $loader;
            }
        }

        return false;
    }

    /**
     * @return void
     */
    public function addLoader(LoaderInterface $loader)
    {
        $this->loaders[] = $loader;
        $loader->setResolver($this);
    }

    /**
     * Returns the registered loaders.
     *
     * @return LoaderInterface[]
     */
    public function getLoaders(): array
    {
        return $this->loaders;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Loader;

use Symfony\Component\Config\Exception\LoaderLoadException;

/**
 * DelegatingLoader delegates loading to other loaders using a loader resolver.
 *
 * This loader acts as an array of LoaderInterface objects - each having
 * a chance to load a given resource (handled by the resolver)
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class DelegatingLoader extends Loader
{
    public function __construct(LoaderResolverInterface $resolver)
    {
        $this->resolver = $resolver;
    }

    public function load(mixed $resource, ?string $type = null): mixed
    {
        if (false === $loader = $this->resolver->resolve($resource, $type)) {
            throw new LoaderLoadException($resource, null, 0, null, $type);
        }

        return $loader->load($resource, $type);
    }

    public function supports(mixed $resource, ?string $type = null): bool
    {
        return false !== $this->resolver->resolve($resource, $type);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Loader;

/**
 * Placeholder for a parameter.
 *
 * @author Tobias Nyholm <tobias.nyholm@gmail.com>
 */
class ParamConfigurator
{
    private string $name;

    public function __construct(string $name)
    {
        $this->name = $name;
    }

    public function __toString(): string
    {
        return '%'.$this->name.'%';
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Loader;

/**
 * LoaderResolverInterface selects a loader for a given resource.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
interface LoaderResolverInterface
{
    /**
     * Returns a loader able to load the resource.
     *
     * @param string|null $type The resource type or null if unknown
     */
    public function resolve(mixed $resource, ?string $type = null): LoaderInterface|false;
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Loader;

/**
 * A loader that can be scoped to a given filesystem directory.
 *
 * @author Alexander M. Turek <me@derrabus.de>
 */
interface DirectoryAwareLoaderInterface
{
    public function forDirectory(string $currentDirectory): static;
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Loader;

use Symfony\Component\Config\Exception\LoaderLoadException;

/**
 * Loader is the abstract class used by all built-in loaders.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
abstract class Loader implements LoaderInterface
{
    protected $resolver;
    protected $env;

    public function __construct(?string $env = null)
    {
        $this->env = $env;
    }

    public function getResolver(): LoaderResolverInterface
    {
        return $this->resolver;
    }

    /**
     * @return void
     */
    public function setResolver(LoaderResolverInterface $resolver)
    {
        $this->resolver = $resolver;
    }

    /**
     * Imports a resource.
     *
     * @return mixed
     */
    public function import(mixed $resource, ?string $type = null)
    {
        return $this->resolve($resource, $type)->load($resource, $type);
    }

    /**
     * Finds a loader able to load an imported resource.
     *
     * @throws LoaderLoadException If no loader is found
     */
    public function resolve(mixed $resource, ?string $type = null): LoaderInterface
    {
        if ($this->supports($resource, $type)) {
            return $this;
        }

        $loader = null === $this->resolver ? false : $this->resolver->resolve($resource, $type);

        if (false === $loader) {
            throw new LoaderLoadException($resource, null, 0, null, $type);
        }

        return $loader;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config;

use Symfony\Component\Config\Resource\ResourceInterface;
use Symfony\Component\Filesystem\Exception\IOException;
use Symfony\Component\Filesystem\Filesystem;

/**
 * ResourceCheckerConfigCache uses instances of ResourceCheckerInterface
 * to check whether cached data is still fresh.
 *
 * @author Matthias Pigulla <mp@webfactory.de>
 */
class ResourceCheckerConfigCache implements ConfigCacheInterface
{
    private string $file;

    /**
     * @var iterable<mixed, ResourceCheckerInterface>
     */
    private iterable $resourceCheckers;

    /**
     * @param string                                    $file             The absolute cache path
     * @param iterable<mixed, ResourceCheckerInterface> $resourceCheckers The ResourceCheckers to use for the freshness check
     */
    public function __construct(string $file, iterable $resourceCheckers = [])
    {
        $this->file = $file;
        $this->resourceCheckers = $resourceCheckers;
    }

    public function getPath(): string
    {
        return $this->file;
    }

    /**
     * Checks if the cache is still fresh.
     *
     * This implementation will make a decision solely based on the ResourceCheckers
     * passed in the constructor.
     *
     * The first ResourceChecker that supports a given resource is considered authoritative.
     * Resources with no matching ResourceChecker will silently be ignored and considered fresh.
     */
    public function isFresh(): bool
    {
        if (!is_file($this->file)) {
            return false;
        }

        if ($this->resourceCheckers instanceof \Traversable && !$this->resourceCheckers instanceof \Countable) {
            $this->resourceCheckers = iterator_to_array($this->resourceCheckers);
        }

        if (!\count($this->resourceCheckers)) {
            return true; // shortcut - if we don't have any checkers we don't need to bother with the meta file at all
        }

        $metadata = $this->getMetaFile();

        if (!is_file($metadata)) {
            return false;
        }

        $meta = $this->safelyUnserialize($metadata);

        if (false === $meta) {
            return false;
        }

        $time = filemtime($this->file);

        foreach ($meta as $resource) {
            foreach ($this->resourceCheckers as $checker) {
                if (!$checker->supports($resource)) {
                    continue; // next checker
                }
                if ($checker->isFresh($resource, $time)) {
                    break; // no need to further check this resource
                }

                return false; // cache is stale
            }
            // no suitable checker found, ignore this resource
        }

        return true;
    }

    /**
     * Writes cache.
     *
     * @param string              $content  The content to write in the cache
     * @param ResourceInterface[] $metadata An array of metadata
     *
     * @return void
     *
     * @throws \RuntimeException When cache file can't be written
     */
    public function write(string $content, ?array $metadata = null)
    {
        $mode = 0o666;
        $umask = umask();
        $filesystem = new Filesystem();
        $filesystem->dumpFile($this->file, $content);
        try {
            $filesystem->chmod($this->file, $mode, $umask);
        } catch (IOException) {
            // discard chmod failure (some filesystem may not support it)
        }

        if (null !== $metadata) {
            $filesystem->dumpFile($this->getMetaFile(), serialize($metadata));
            try {
                $filesystem->chmod($this->getMetaFile(), $mode, $umask);
            } catch (IOException) {
                // discard chmod failure (some filesystem may not support it)
            }
        }

        if (\function_exists('opcache_invalidate') && filter_var(\ini_get('opcache.enable'), \FILTER_VALIDATE_BOOL)) {
            @opcache_invalidate($this->file, true);
        }
    }

    /**
     * Gets the meta file path.
     */
    private function getMetaFile(): string
    {
        return $this->file.'.meta';
    }

    private function safelyUnserialize(string $file): mixed
    {
        $meta = false;
        $content = file_get_contents($file);
        $signalingException = new \UnexpectedValueException();
        $prevUnserializeHandler = ini_set('unserialize_callback_func', self::class.'::handleUnserializeCallback');
        $prevErrorHandler = set_error_handler(static function ($type, $msg, $file, $line, $context = []) use (&$prevErrorHandler, $signalingException) {
            if (__FILE__ === $file && !\in_array($type, [\E_DEPRECATED, \E_USER_DEPRECATED], true)) {
                throw $signalingException;
            }

            return $prevErrorHandler ? $prevErrorHandler($type, $msg, $file, $line, $context) : false;
        });

        try {
            $meta = unserialize($content, ['allowed_classes' => true]);
        } catch (\Throwable $e) {
            if ($e !== $signalingException) {
                throw $e;
            }
        } finally {
            restore_error_handler();
            ini_set('unserialize_callback_func', $prevUnserializeHandler);
        }

        return $meta;
    }

    /**
     * @internal
     */
    public static function handleUnserializeCallback(string $class): void
    {
        trigger_error('Class not found: '.$class);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Definition;

/**
 * This class is the entry point for config normalization/merging/finalization.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 *
 * @final
 */
class Processor
{
    /**
     * Processes an array of configurations.
     *
     * @param array $configs An array of configuration items to process
     */
    public function process(NodeInterface $configTree, array $configs): array
    {
        $currentConfig = [];
        foreach ($configs as $config) {
            $config = $configTree->normalize($config);
            $currentConfig = $configTree->merge($currentConfig, $config);
        }

        return $configTree->finalize($currentConfig);
    }

    /**
     * Processes an array of configurations.
     *
     * @param array $configs An array of configuration items to process
     */
    public function processConfiguration(ConfigurationInterface $configuration, array $configs): array
    {
        return $this->process($configuration->getConfigTreeBuilder()->buildTree(), $configs);
    }

    /**
     * Normalizes a configuration entry.
     *
     * This method returns a normalize configuration array for a given key
     * to remove the differences due to the original format (YAML and XML mainly).
     *
     * Here is an example.
     *
     * The configuration in XML:
     *
     * <twig:extension>twig.extension.foo</twig:extension>
     * <twig:extension>twig.extension.bar</twig:extension>
     *
     * And the same configuration in YAML:
     *
     * extensions: ['twig.extension.foo', 'twig.extension.bar']
     *
     * @param array       $config A config array
     * @param string      $key    The key to normalize
     * @param string|null $plural The plural form of the key if it is irregular
     */
    public static function normalizeConfig(array $config, string $key, ?string $plural = null): array
    {
        $plural ??= $key.'s';

        if (isset($config[$plural])) {
            return $config[$plural];
        }

        if (isset($config[$key])) {
            if (\is_string($config[$key]) || !\is_int(key($config[$key]))) {
                // only one
                return [$config[$key]];
            }

            return $config[$key];
        }

        return [];
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Definition\Exception;

/**
 * This exception is thrown whenever the key of an array is not unique. This can
 * only be the case if the configuration is coming from an XML file.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class DuplicateKeyException extends InvalidConfigurationException
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Definition\Exception;

/**
 * This exception is thrown when a configuration path is overwritten from a
 * subsequent configuration file, but the entry node specifically forbids this.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class ForbiddenOverwriteException extends InvalidConfigurationException
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Definition\Exception;

/**
 * Thrown when an error is detected in a node Definition.
 *
 * @author Victor Berchet <victor.berchet@suumit.com>
 */
class InvalidDefinitionException extends Exception
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Definition\Exception;

/**
 * A very general exception which can be thrown whenever non of the more specific
 * exceptions is suitable.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class InvalidConfigurationException extends Exception
{
    private ?string $path = null;
    private bool $containsHints = false;

    /**
     * @return void
     */
    public function setPath(string $path)
    {
        $this->path = $path;
    }

    public function getPath(): ?string
    {
        return $this->path;
    }

    /**
     * Adds extra information that is suffixed to the original exception message.
     *
     * @return void
     */
    public function addHint(string $hint)
    {
        if (!$this->containsHints) {
            $this->message .= "\nHint: ".$hint;
            $this->containsHints = true;
        } else {
            $this->message .= ', '.$hint;
        }
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Definition\Exception;

/**
 * Base exception for all configuration exceptions.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class Exception extends \RuntimeException
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Definition\Exception;

/**
 * This exception is usually not encountered by the end-user, but only used
 * internally to signal the parent scope to unset a key.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class UnsetKeyException extends Exception
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Definition\Exception;

/**
 * This exception is thrown if an invalid type is encountered.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class InvalidTypeException extends InvalidConfigurationException
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Definition\Dumper;

use Symfony\Component\Config\Definition\ArrayNode;
use Symfony\Component\Config\Definition\BaseNode;
use Symfony\Component\Config\Definition\BooleanNode;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Config\Definition\EnumNode;
use Symfony\Component\Config\Definition\FloatNode;
use Symfony\Component\Config\Definition\IntegerNode;
use Symfony\Component\Config\Definition\NodeInterface;
use Symfony\Component\Config\Definition\PrototypedArrayNode;
use Symfony\Component\Config\Definition\ScalarNode;

/**
 * Dumps an XML reference configuration for the given configuration/node instance.
 *
 * @author Wouter J <waldio.webdesign@gmail.com>
 */
class XmlReferenceDumper
{
    private ?string $reference = null;

    /**
     * @return string
     */
    public function dump(ConfigurationInterface $configuration, ?string $namespace = null)
    {
        return $this->dumpNode($configuration->getConfigTreeBuilder()->buildTree(), $namespace);
    }

    /**
     * @return string
     */
    public function dumpNode(NodeInterface $node, ?string $namespace = null)
    {
        $this->reference = '';
        $this->writeNode($node, 0, true, $namespace);
        $ref = $this->reference;
        $this->reference = null;

        return $ref;
    }

    private function writeNode(NodeInterface $node, int $depth = 0, bool $root = false, ?string $namespace = null): void
    {
        $rootName = ($root ? 'config' : $node->getName());
        $rootNamespace = ($namespace ?: ($root ? 'http://example.org/schema/dic/'.$node->getName() : null));

        // xml remapping
        if ($node->getParent()) {
            $remapping = array_filter($node->getParent()->getXmlRemappings(), static fn (array $mapping) => $rootName === $mapping[1]);

            if (\count($remapping)) {
                [$singular] = current($remapping);
                $rootName = $singular;
            }
        }
        $rootName = str_replace('_', '-', $rootName);

        $rootAttributes = [];
        $rootAttributeComments = [];
        $rootChildren = [];
        $rootComments = [];

        if ($node instanceof ArrayNode) {
            $children = $node->getChildren();

            // comments about the root node
            if ($rootInfo = $node->getInfo()) {
                $rootComments[] = $rootInfo;
            }

            if ($rootNamespace) {
                $rootComments[] = 'Namespace: '.$rootNamespace;
            }

            // render prototyped nodes
            if ($node instanceof PrototypedArrayNode) {
                $prototype = $node->getPrototype();

                $info = 'prototype';
                if (null !== $prototype->getInfo()) {
                    $info .= ': '.$prototype->getInfo();
                }
                array_unshift($rootComments, $info);

                if ($key = $node->getKeyAttribute()) {
                    $rootAttributes[$key] = str_replace('-', ' ', $rootName).' '.$key;
                }

                if ($prototype instanceof PrototypedArrayNode) {
                    $prototype->setName($key ?? '');
                    $children = [$key => $prototype];
                } elseif ($prototype instanceof ArrayNode) {
                    $children = $prototype->getChildren();
                } else {
                    if ($prototype->hasDefaultValue()) {
                        $prototypeValue = $prototype->getDefaultValue();
                    } else {
                        $prototypeValue = match ($prototype::class) {
                            ScalarNode::class => 'scalar value',
                            FloatNode::class,
                            IntegerNode::class => 'numeric value',
                            BooleanNode::class => 'true|false',
                            EnumNode::class => $prototype->getPermissibleValues('|'),
                            default => 'value',
                        };
                    }
                }
            }

            // get attributes and elements
            foreach ($children as $child) {
                if ($child instanceof ArrayNode) {
                    // get elements
                    $rootChildren[] = $child;

                    continue;
                }

                // get attributes

                // metadata
                $name = str_replace('_', '-', $child->getName());
                $value = '%%%%not_defined%%%%'; // use a string which isn't used in the normal world

                // comments
                $comments = [];
                if ($child instanceof BaseNode && $info = $child->getInfo()) {
                    $comments[] = $info;
                }

                if ($child instanceof BaseNode && $example = $child->getExample()) {
                    $comments[] = 'Example: '.(\is_array($example) ? implode(', ', $example) : $example);
                }

                if ($child->isRequired()) {
                    $comments[] = 'Required';
                }

                if ($child instanceof BaseNode && $child->isDeprecated()) {
                    $deprecation = $child->getDeprecation($child->getName(), $node->getPath());
                    $comments[] = \sprintf('Deprecated (%s)', ($deprecation['package'] || $deprecation['version'] ? "Since {$deprecation['package']} {$deprecation['version']}: " : '').$deprecation['message']);
                }

                if ($child instanceof EnumNode) {
                    $comments[] = 'One of '.$child->getPermissibleValues('; ');
                }

                if (\count($comments)) {
                    $rootAttributeComments[$name] = implode(";\n", $comments);
                }

                // default values
                if ($child->hasDefaultValue()) {
                    $value = $child->getDefaultValue();
                }

                // append attribute
                $rootAttributes[$name] = $value;
            }
        }

        // render comments

        // root node comment
        if (\count($rootComments)) {
            foreach ($rootComments as $comment) {
                $this->writeLine('<!-- '.$comment.' -->', $depth);
            }
        }

        // attribute comments
        if (\count($rootAttributeComments)) {
            foreach ($rootAttributeComments as $attrName => $comment) {
                $commentDepth = $depth + 4 + \strlen($attrName) + 2;
                $commentLines = explode("\n", $comment);
                $multiline = (\count($commentLines) > 1);
                $comment = implode(\PHP_EOL.str_repeat(' ', $commentDepth), $commentLines);

                if ($multiline) {
                    $this->writeLine('<!--', $depth);
                    $this->writeLine($attrName.': '.$comment, $depth + 4);
                    $this->writeLine('-->', $depth);
                } else {
                    $this->writeLine('<!-- '.$attrName.': '.$comment.' -->', $depth);
                }
            }
        }

        // render start tag + attributes
        $rootIsVariablePrototype = isset($prototypeValue);
        $rootIsEmptyTag = (0 === \count($rootChildren) && !$rootIsVariablePrototype);
        $rootOpenTag = '<'.$rootName;
        if (1 >= ($attributesCount = \count($rootAttributes))) {
            if (1 === $attributesCount) {
                $rootOpenTag .= \sprintf(' %s="%s"', current(array_keys($rootAttributes)), $this->writeValue(current($rootAttributes)));
            }

            $rootOpenTag .= $rootIsEmptyTag ? ' />' : '>';

            if ($rootIsVariablePrototype) {
                $rootOpenTag .= $prototypeValue.'</'.$rootName.'>';
            }

            $this->writeLine($rootOpenTag, $depth);
        } else {
            $this->writeLine($rootOpenTag, $depth);

            $i = 1;

            foreach ($rootAttributes as $attrName => $attrValue) {
                $attr = \sprintf('%s="%s"', $attrName, $this->writeValue($attrValue));

                $this->writeLine($attr, $depth + 4);

                if ($attributesCount === $i++) {
                    $this->writeLine($rootIsEmptyTag ? '/>' : '>', $depth);

                    if ($rootIsVariablePrototype) {
                        $rootOpenTag .= $prototypeValue.'</'.$rootName.'>';
                    }
                }
            }
        }

        // render children tags
        foreach ($rootChildren as $child) {
            $this->writeLine('');
            $this->writeNode($child, $depth + 4);
        }

        // render end tag
        if (!$rootIsEmptyTag && !$rootIsVariablePrototype) {
            $this->writeLine('');

            $rootEndTag = '</'.$rootName.'>';
            $this->writeLine($rootEndTag, $depth);
        }
    }

    /**
     * Outputs a single config reference line.
     */
    private function writeLine(string $text, int $indent = 0): void
    {
        $indent = \strlen($text) + $indent;
        $format = '%'.$indent.'s';

        $this->reference .= \sprintf($format, $text).\PHP_EOL;
    }

    /**
     * Renders the string conversion of the value.
     */
    private function writeValue(mixed $value): string
    {
        if ('%%%%not_defined%%%%' === $value) {
            return '';
        }

        if (\is_string($value) || is_numeric($value)) {
            return $value;
        }

        if (false === $value) {
            return 'false';
        }

        if (true === $value) {
            return 'true';
        }

        if (null === $value) {
            return 'null';
        }

        if (empty($value)) {
            return '';
        }

        if (\is_array($value)) {
            return implode(',', $value);
        }

        return '';
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Definition\Dumper;

use Symfony\Component\Config\Definition\ArrayNode;
use Symfony\Component\Config\Definition\BaseNode;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Config\Definition\EnumNode;
use Symfony\Component\Config\Definition\NodeInterface;
use Symfony\Component\Config\Definition\PrototypedArrayNode;
use Symfony\Component\Config\Definition\ScalarNode;
use Symfony\Component\Yaml\Inline;

/**
 * Dumps a Yaml reference configuration for the given configuration/node instance.
 *
 * @author Kevin Bond <kevinbond@gmail.com>
 */
class YamlReferenceDumper
{
    private ?string $reference = null;

    /**
     * @return string
     */
    public function dump(ConfigurationInterface $configuration)
    {
        return $this->dumpNode($configuration->getConfigTreeBuilder()->buildTree());
    }

    /**
     * @return string
     */
    public function dumpAtPath(ConfigurationInterface $configuration, string $path)
    {
        $rootNode = $node = $configuration->getConfigTreeBuilder()->buildTree();

        foreach (explode('.', $path) as $step) {
            if (!$node instanceof ArrayNode) {
                throw new \UnexpectedValueException(\sprintf('Unable to find node at path "%s.%s".', $rootNode->getName(), $path));
            }

            /** @var NodeInterface[] $children */
            $children = $node instanceof PrototypedArrayNode ? $this->getPrototypeChildren($node) : $node->getChildren();

            foreach ($children as $child) {
                if ($child->getName() === $step) {
                    $node = $child;

                    continue 2;
                }
            }

            throw new \UnexpectedValueException(\sprintf('Unable to find node at path "%s.%s".', $rootNode->getName(), $path));
        }

        return $this->dumpNode($node);
    }

    /**
     * @return string
     */
    public function dumpNode(NodeInterface $node)
    {
        $this->reference = '';
        $this->writeNode($node);
        $ref = $this->reference;
        $this->reference = null;

        return $ref;
    }

    private function writeNode(NodeInterface $node, ?NodeInterface $parentNode = null, int $depth = 0, bool $prototypedArray = false): void
    {
        $comments = [];
        $default = '';
        $defaultArray = null;
        $children = null;
        $example = null;
        if ($node instanceof BaseNode) {
            $example = $node->getExample();
        }

        // defaults
        if ($node instanceof ArrayNode) {
            $children = $node->getChildren();

            if ($node instanceof PrototypedArrayNode) {
                $children = $this->getPrototypeChildren($node);
            }

            if (!$children && !($node->hasDefaultValue() && \count($defaultArray = $node->getDefaultValue()))) {
                $default = '[]';
            }
        } elseif ($node instanceof EnumNode) {
            $comments[] = 'One of '.$node->getPermissibleValues('; ');
            $default = $node->hasDefaultValue() ? Inline::dump($node->getDefaultValue()) : '~';
        } else {
            $default = '~';

            if ($node->hasDefaultValue()) {
                $default = $node->getDefaultValue();

                if (\is_array($default)) {
                    if (\count($defaultArray = $node->getDefaultValue())) {
                        $default = '';
                    } elseif (!\is_array($example)) {
                        $default = '[]';
                    }
                } else {
                    $default = Inline::dump($default);
                }
            }
        }

        // required?
        if ($node->isRequired()) {
            $comments[] = 'Required';
        }

        // deprecated?
        if ($node instanceof BaseNode && $node->isDeprecated()) {
            $deprecation = $node->getDeprecation($node->getName(), $parentNode ? $parentNode->getPath() : $node->getPath());
            $comments[] = \sprintf('Deprecated (%s)', ($deprecation['package'] || $deprecation['version'] ? "Since {$deprecation['package']} {$deprecation['version']}: " : '').$deprecation['message']);
        }

        // example
        if ($example && !\is_array($example)) {
            $comments[] = 'Example: '.Inline::dump($example);
        }

        $default = '' != (string) $default ? ' '.$default : '';
        $comments = \count($comments) ? '# '.implode(', ', $comments) : '';

        $key = $prototypedArray ? '-' : $node->getName().':';
        $text = rtrim(\sprintf('%-21s%s %s', $key, $default, $comments), ' ');

        if ($node instanceof BaseNode && $info = $node->getInfo()) {
            $this->writeLine('');
            // indenting multi-line info
            $info = str_replace("\n", \sprintf("\n%".($depth * 4).'s# ', ' '), $info);
            $this->writeLine('# '.$info, $depth * 4);
        }

        $this->writeLine($text, $depth * 4);

        // output defaults
        if ($defaultArray) {
            $this->writeLine('');

            $message = \count($defaultArray) > 1 ? 'Defaults' : 'Default';

            $this->writeLine('# '.$message.':', $depth * 4 + 4);

            $this->writeArray($defaultArray, $depth + 1);
        }

        if (\is_array($example)) {
            $this->writeLine('');

            $message = \count($example) > 1 ? 'Examples' : 'Example';

            $this->writeLine('# '.$message.':', $depth * 4 + 4);

            $this->writeArray(array_map(Inline::dump(...), $example), $depth + 1, true);
        }

        if ($children) {
            foreach ($children as $childNode) {
                $this->writeNode($childNode, $node, $depth + 1, $node instanceof PrototypedArrayNode && !$node->getKeyAttribute());
            }
        }
    }

    /**
     * Outputs a single config reference line.
     */
    private function writeLine(string $text, int $indent = 0): void
    {
        $indent = \strlen($text) + $indent;
        $format = '%'.$indent.'s';

        $this->reference .= \sprintf($format, $text)."\n";
    }

    private function writeArray(array $array, int $depth, bool $asComment = false): void
    {
        $isIndexed = array_is_list($array);

        foreach ($array as $key => $value) {
            if (\is_array($value)) {
                $val = '';
            } else {
                $val = $value;
            }

            $prefix = $asComment ? '# ' : '';

            if ($isIndexed) {
                $this->writeLine($prefix.'- '.$val, $depth * 4);
            } else {
                $this->writeLine(\sprintf('%s%-20s %s', $prefix, $key.':', $val), $depth * 4);
            }

            if (\is_array($value)) {
                $this->writeArray($value, $depth + 1, $asComment);
            }
        }
    }

    private function getPrototypeChildren(PrototypedArrayNode $node): array
    {
        $prototype = $node->getPrototype();
        $key = $node->getKeyAttribute();

        // Do not expand prototype if it isn't an array node nor uses attribute as key
        if (!$key && !$prototype instanceof ArrayNode) {
            return $node->getChildren();
        }

        if ($prototype instanceof ArrayNode) {
            $keyNode = new ArrayNode($key, $node);
            $children = $prototype->getChildren();

            if ($prototype instanceof PrototypedArrayNode && $prototype->getKeyAttribute()) {
                $children = $this->getPrototypeChildren($prototype);
            }

            // add children
            foreach ($children as $childNode) {
                $keyNode->addChild($childNode);
            }
        } else {
            $keyNode = new ScalarNode($key, $node);
        }

        $info = 'Prototype';
        if (null !== $prototype->getInfo()) {
            $info .= ': '.$prototype->getInfo();
        }
        $keyNode->setInfo($info);

        return [$key ?? '' => $keyNode];
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Definition\Loader;

use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator;
use Symfony\Component\Config\FileLocatorInterface;
use Symfony\Component\Config\Loader\FileLoader;
use Symfony\Component\DependencyInjection\ContainerBuilder;

/**
 * DefinitionFileLoader loads config definitions from a PHP file.
 *
 * The PHP file is required.
 *
 * @author Yonel Ceruto <yonelceruto@gmail.com>
 */
class DefinitionFileLoader extends FileLoader
{
    public function __construct(
        private TreeBuilder $treeBuilder,
        FileLocatorInterface $locator,
        private ?ContainerBuilder $container = null,
    ) {
        parent::__construct($locator);
    }

    public function load(mixed $resource, ?string $type = null): mixed
    {
        // the loader variable is exposed to the included file below
        $loader = $this;

        $path = $this->locator->locate($resource);
        $this->setCurrentDir(\dirname($path));
        $this->container?->fileExists($path);

        // the closure forbids access to the private scope in the included file
        $load = \Closure::bind(static function ($file) use ($loader) {
            return include $file;
        }, null, ProtectedDefinitionFileLoader::class);

        $callback = $load($path);

        if (\is_object($callback) && \is_callable($callback)) {
            $this->executeCallback($callback, new DefinitionConfigurator($this->treeBuilder, $this, $path, $resource), $path);
        }

        return null;
    }

    public function supports(mixed $resource, ?string $type = null): bool
    {
        if (!\is_string($resource)) {
            return false;
        }

        if (null === $type && 'php' === pathinfo($resource, \PATHINFO_EXTENSION)) {
            return true;
        }

        return 'php' === $type;
    }

    private function executeCallback(callable $callback, DefinitionConfigurator $configurator, string $path): void
    {
        $callback = $callback(...);

        $arguments = [];
        $r = new \ReflectionFunction($callback);

        foreach ($r->getParameters() as $parameter) {
            $reflectionType = $parameter->getType();

            if (!$reflectionType instanceof \ReflectionNamedType) {
                throw new \InvalidArgumentException(\sprintf('Could not resolve argument "$%s" for "%s". You must typehint it (for example with "%s").', $parameter->getName(), $path, DefinitionConfigurator::class));
            }

            $arguments[] = match ($reflectionType->getName()) {
                DefinitionConfigurator::class => $configurator,
                TreeBuilder::class => $this->treeBuilder,
                FileLoader::class, self::class => $this,
            };
        }

        $callback(...$arguments);
    }
}

/**
 * @internal
 */
final class ProtectedDefinitionFileLoader extends DefinitionFileLoader
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Definition;

/**
 * This interface must be implemented by nodes which can be used as prototypes.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
interface PrototypeNodeInterface extends NodeInterface
{
    /**
     * Sets the name of the node.
     *
     * @return void
     */
    public function setName(string $name);
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Definition;

use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator;

/**
 * @author Yonel Ceruto <yonelceruto@gmail.com>
 */
interface ConfigurableInterface
{
    /**
     * Generates the configuration tree builder.
     */
    public function configure(DefinitionConfigurator $definition): void;
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Definition\Builder;

use Symfony\Component\Config\Definition\FloatNode;

/**
 * This class provides a fluent interface for defining a float node.
 *
 * @author Jeanmonod David <david.jeanmonod@gmail.com>
 */
class FloatNodeDefinition extends NumericNodeDefinition
{
    /**
     * Instantiates a Node.
     */
    protected function instantiateNode(): FloatNode
    {
        return new FloatNode($this->name, $this->parent, $this->min, $this->max, $this->pathSeparator);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Definition\Builder;

use Symfony\Component\Config\Definition\Exception\InvalidDefinitionException;

/**
 * Abstract class that contains common code of integer and float node definitions.
 *
 * @author David Jeanmonod <david.jeanmonod@gmail.com>
 */
abstract class NumericNodeDefinition extends ScalarNodeDefinition
{
    protected $min;
    protected $max;

    /**
     * Ensures that the value is smaller than the given reference.
     *
     * @return $this
     *
     * @throws \InvalidArgumentException when the constraint is inconsistent
     */
    public function max(int|float $max): static
    {
        if (isset($this->min) && $this->min > $max) {
            throw new \InvalidArgumentException(\sprintf('You cannot define a max(%s) as you already have a min(%s).', $max, $this->min));
        }
        $this->max = $max;

        return $this;
    }

    /**
     * Ensures that the value is bigger than the given reference.
     *
     * @return $this
     *
     * @throws \InvalidArgumentException when the constraint is inconsistent
     */
    public function min(int|float $min): static
    {
        if (isset($this->max) && $this->max < $min) {
            throw new \InvalidArgumentException(\sprintf('You cannot define a min(%s) as you already have a max(%s).', $min, $this->max));
        }
        $this->min = $min;

        return $this;
    }

    /**
     * @throws InvalidDefinitionException
     */
    public function cannotBeEmpty(): static
    {
        throw new InvalidDefinitionException('->cannotBeEmpty() is not applicable to NumericNodeDefinition.');
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Definition\Builder;

use Symfony\Component\Config\Definition\ScalarNode;

/**
 * This class provides a fluent interface for defining a node.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class ScalarNodeDefinition extends VariableNodeDefinition
{
    /**
     * Instantiate a Node.
     */
    protected function instantiateNode(): ScalarNode
    {
        return new ScalarNode($this->name, $this->parent, $this->pathSeparator);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Definition\Builder;

use Symfony\Component\Config\Definition\ArrayNode;
use Symfony\Component\Config\Definition\Exception\InvalidDefinitionException;
use Symfony\Component\Config\Definition\NodeInterface;
use Symfony\Component\Config\Definition\PrototypedArrayNode;

/**
 * This class provides a fluent interface for defining an array node.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class ArrayNodeDefinition extends NodeDefinition implements ParentNodeDefinitionInterface
{
    protected $performDeepMerging = true;
    protected $ignoreExtraKeys = false;
    protected $removeExtraKeys = true;
    protected $children = [];
    protected $prototype;
    protected $atLeastOne = false;
    protected $allowNewKeys = true;
    protected $key;
    protected $removeKeyItem;
    protected $addDefaults = false;
    protected $addDefaultChildren = false;
    protected $nodeBuilder;
    protected $normalizeKeys = true;

    public function __construct(?string $name, ?NodeParentInterface $parent = null)
    {
        parent::__construct($name, $parent);

        $this->nullEquivalent = [];
        $this->trueEquivalent = [];
    }

    /**
     * @return void
     */
    public function setBuilder(NodeBuilder $builder)
    {
        $this->nodeBuilder = $builder;
    }

    public function children(): NodeBuilder
    {
        return $this->getNodeBuilder();
    }

    /**
     * Sets a prototype for child nodes.
     */
    public function prototype(string $type): NodeDefinition
    {
        return $this->prototype = $this->getNodeBuilder()->node(null, $type)->setParent($this);
    }

    public function variablePrototype(): VariableNodeDefinition
    {
        return $this->prototype('variable');
    }

    public function scalarPrototype(): ScalarNodeDefinition
    {
        return $this->prototype('scalar');
    }

    public function booleanPrototype(): BooleanNodeDefinition
    {
        return $this->prototype('boolean');
    }

    public function integerPrototype(): IntegerNodeDefinition
    {
        return $this->prototype('integer');
    }

    public function floatPrototype(): FloatNodeDefinition
    {
        return $this->prototype('float');
    }

    public function arrayPrototype(): self
    {
        return $this->prototype('array');
    }

    public function enumPrototype(): EnumNodeDefinition
    {
        return $this->prototype('enum');
    }

    /**
     * Adds the default value if the node is not set in the configuration.
     *
     * This method is applicable to concrete nodes only (not to prototype nodes).
     * If this function has been called and the node is not set during the finalization
     * phase, it's default value will be derived from its children default values.
     *
     * @return $this
     */
    public function addDefaultsIfNotSet(): static
    {
        $this->addDefaults = true;

        return $this;
    }

    /**
     * Adds children with a default value when none are defined.
     *
     * This method is applicable to prototype nodes only.
     *
     * @param int|string|array|null $children The number of children|The child name|The children names to be added
     *
     * @return $this
     */
    public function addDefaultChildrenIfNoneSet(int|string|array|null $children = null): static
    {
        $this->addDefaultChildren = $children;

        return $this;
    }

    /**
     * Requires the node to have at least one element.
     *
     * This method is applicable to prototype nodes only.
     *
     * @return $this
     */
    public function requiresAtLeastOneElement(): static
    {
        $this->atLeastOne = true;

        return $this;
    }

    /**
     * Disallows adding news keys in a subsequent configuration.
     *
     * If used all keys have to be defined in the same configuration file.
     *
     * @return $this
     */
    public function disallowNewKeysInSubsequentConfigs(): static
    {
        $this->allowNewKeys = false;

        return $this;
    }

    /**
     * Sets a normalization rule for XML configurations.
     *
     * @param string      $singular The key to remap
     * @param string|null $plural   The plural of the key for irregular plurals
     *
     * @return $this
     */
    public function fixXmlConfig(string $singular, ?string $plural = null): static
    {
        $this->normalization()->remap($singular, $plural);

        return $this;
    }

    /**
     * Sets the attribute which value is to be used as key.
     *
     * This is useful when you have an indexed array that should be an
     * associative array. You can select an item from within the array
     * to be the key of the particular item. For example, if "id" is the
     * "key", then:
     *
     *     [
     *         ['id' => 'my_name', 'foo' => 'bar'],
     *     ];
     *
     *   becomes
     *
     *     [
     *         'my_name' => ['foo' => 'bar'],
     *     ];
     *
     * If you'd like "'id' => 'my_name'" to still be present in the resulting
     * array, then you can set the second argument of this method to false.
     *
     * This method is applicable to prototype nodes only.
     *
     * @param string $name          The name of the key
     * @param bool   $removeKeyItem Whether or not the key item should be removed
     *
     * @return $this
     */
    public function useAttributeAsKey(string $name, bool $removeKeyItem = true): static
    {
        $this->key = $name;
        $this->removeKeyItem = $removeKeyItem;

        return $this;
    }

    /**
     * Sets whether the node can be unset.
     *
     * @return $this
     */
    public function canBeUnset(bool $allow = true): static
    {
        $this->merge()->allowUnset($allow);

        return $this;
    }

    /**
     * Adds an "enabled" boolean to enable the current section.
     *
     * By default, the section is disabled. If any configuration is specified then
     * the node will be automatically enabled:
     *
     * enableableArrayNode: {enabled: true, ...}   # The config is enabled & default values get overridden
     * enableableArrayNode: ~                      # The config is enabled & use the default values
     * enableableArrayNode: true                   # The config is enabled & use the default values
     * enableableArrayNode: {other: value, ...}    # The config is enabled & default values get overridden
     * enableableArrayNode: {enabled: false, ...}  # The config is disabled
     * enableableArrayNode: false                  # The config is disabled
     *
     * @return $this
     */
    public function canBeEnabled(): static
    {
        $this
            ->addDefaultsIfNotSet()
            ->treatFalseLike(['enabled' => false])
            ->treatTrueLike(['enabled' => true])
            ->treatNullLike(['enabled' => true])
            ->beforeNormalization()
                ->ifArray()
                ->then(static function (array $v) {
                    $v['enabled'] ??= true;

                    return $v;
                })
            ->end()
            ->children()
                ->booleanNode('enabled')
                    ->defaultFalse()
        ;

        return $this;
    }

    /**
     * Adds an "enabled" boolean to enable the current section.
     *
     * By default, the section is enabled.
     *
     * @return $this
     */
    public function canBeDisabled(): static
    {
        $this
            ->addDefaultsIfNotSet()
            ->treatFalseLike(['enabled' => false])
            ->treatTrueLike(['enabled' => true])
            ->treatNullLike(['enabled' => true])
            ->children()
                ->booleanNode('enabled')
                    ->defaultTrue()
        ;

        return $this;
    }

    /**
     * Disables the deep merging of the node.
     *
     * @return $this
     */
    public function performNoDeepMerging(): static
    {
        $this->performDeepMerging = false;

        return $this;
    }

    /**
     * Allows extra config keys to be specified under an array without
     * throwing an exception.
     *
     * Those config values are ignored and removed from the resulting
     * array. This should be used only in special cases where you want
     * to send an entire configuration array through a special tree that
     * processes only part of the array.
     *
     * @param bool $remove Whether to remove the extra keys
     *
     * @return $this
     */
    public function ignoreExtraKeys(bool $remove = true): static
    {
        $this->ignoreExtraKeys = true;
        $this->removeExtraKeys = $remove;

        return $this;
    }

    /**
     * Sets whether to enable key normalization.
     *
     * @return $this
     */
    public function normalizeKeys(bool $bool): static
    {
        $this->normalizeKeys = $bool;

        return $this;
    }

    public function append(NodeDefinition $node): static
    {
        $this->children[$node->name ?? ''] = $node->setParent($this);

        return $this;
    }

    /**
     * Returns a node builder to be used to add children and prototype.
     */
    protected function getNodeBuilder(): NodeBuilder
    {
        $this->nodeBuilder ??= new NodeBuilder();

        return $this->nodeBuilder->setParent($this);
    }

    protected function createNode(): NodeInterface
    {
        if (!isset($this->prototype)) {
            $node = new ArrayNode($this->name, $this->parent, $this->pathSeparator);

            $this->validateConcreteNode($node);

            $node->setAddIfNotSet($this->addDefaults);

            foreach ($this->children as $child) {
                $child->parent = $node;
                $node->addChild($child->getNode());
            }
        } else {
            $node = new PrototypedArrayNode($this->name, $this->parent, $this->pathSeparator);

            $this->validatePrototypeNode($node);

            if (null !== $this->key) {
                $node->setKeyAttribute($this->key, $this->removeKeyItem);
            }

            if (true === $this->atLeastOne || false === $this->allowEmptyValue) {
                $node->setMinNumberOfElements(1);
            }

            if ($this->default) {
                if (!\is_array($this->defaultValue)) {
                    throw new \InvalidArgumentException(\sprintf('%s: the default value of an array node has to be an array.', $node->getPath()));
                }

                $node->setDefaultValue($this->defaultValue);
            }

            if (false !== $this->addDefaultChildren) {
                $node->setAddChildrenIfNoneSet($this->addDefaultChildren);
                if ($this->prototype instanceof static && !isset($this->prototype->prototype)) {
                    $this->prototype->addDefaultsIfNotSet();
                }
            }

            $this->prototype->parent = $node;
            $node->setPrototype($this->prototype->getNode());
        }

        $node->setAllowNewKeys($this->allowNewKeys);
        $node->addEquivalentValue(null, $this->nullEquivalent);
        $node->addEquivalentValue(true, $this->trueEquivalent);
        $node->addEquivalentValue(false, $this->falseEquivalent);
        $node->setPerformDeepMerging($this->performDeepMerging);
        $node->setRequired($this->required);
        $node->setIgnoreExtraKeys($this->ignoreExtraKeys, $this->removeExtraKeys);
        $node->setNormalizeKeys($this->normalizeKeys);

        if ($this->deprecation) {
            $node->setDeprecated($this->deprecation['package'], $this->deprecation['version'], $this->deprecation['message']);
        }

        if (isset($this->normalization)) {
            $node->setNormalizationClosures($this->normalization->before);
            $node->setNormalizedTypes($this->normalization->declaredTypes);
            $node->setXmlRemappings($this->normalization->remappings);
        }

        if (isset($this->merge)) {
            $node->setAllowOverwrite($this->merge->allowOverwrite);
            $node->setAllowFalse($this->merge->allowFalse);
        }

        if (isset($this->validation)) {
            $node->setFinalValidationClosures($this->validation->rules);
        }

        return $node;
    }

    /**
     * Validate the configuration of a concrete node.
     *
     * @return void
     *
     * @throws InvalidDefinitionException
     */
    protected function validateConcreteNode(ArrayNode $node)
    {
        $path = $node->getPath();

        if (null !== $this->key) {
            throw new InvalidDefinitionException(\sprintf('->useAttributeAsKey() is not applicable to concrete nodes at path "%s".', $path));
        }

        if (false === $this->allowEmptyValue) {
            throw new InvalidDefinitionException(\sprintf('->cannotBeEmpty() is not applicable to concrete nodes at path "%s".', $path));
        }

        if (true === $this->atLeastOne) {
            throw new InvalidDefinitionException(\sprintf('->requiresAtLeastOneElement() is not applicable to concrete nodes at path "%s".', $path));
        }

        if ($this->default) {
            throw new InvalidDefinitionException(\sprintf('->defaultValue() is not applicable to concrete nodes at path "%s".', $path));
        }

        if (false !== $this->addDefaultChildren) {
            throw new InvalidDefinitionException(\sprintf('->addDefaultChildrenIfNoneSet() is not applicable to concrete nodes at path "%s".', $path));
        }
    }

    /**
     * Validate the configuration of a prototype node.
     *
     * @return void
     *
     * @throws InvalidDefinitionException
     */
    protected function validatePrototypeNode(PrototypedArrayNode $node)
    {
        $path = $node->getPath();

        if ($this->addDefaults) {
            throw new InvalidDefinitionException(\sprintf('->addDefaultsIfNotSet() is not applicable to prototype nodes at path "%s".', $path));
        }

        if (false !== $this->addDefaultChildren) {
            if ($this->default) {
                throw new InvalidDefinitionException(\sprintf('A default value and default children might not be used together at path "%s".', $path));
            }

            if (null !== $this->key && (null === $this->addDefaultChildren || \is_int($this->addDefaultChildren) && $this->addDefaultChildren > 0)) {
                throw new InvalidDefinitionException(\sprintf('->addDefaultChildrenIfNoneSet() should set default children names as ->useAttributeAsKey() is used at path "%s".', $path));
            }

            if (null === $this->key && (\is_string($this->addDefaultChildren) || \is_array($this->addDefaultChildren))) {
                throw new InvalidDefinitionException(\sprintf('->addDefaultChildrenIfNoneSet() might not set default children names as ->useAttributeAsKey() is not used at path "%s".', $path));
            }
        }
    }

    /**
     * @return NodeDefinition[]
     */
    public function getChildNodeDefinitions(): array
    {
        return $this->children;
    }

    /**
     * Finds a node defined by the given $nodePath.
     *
     * @param string $nodePath The path of the node to find. e.g "doctrine.orm.mappings"
     */
    public function find(string $nodePath): NodeDefinition
    {
        $firstPathSegment = (false === $pathSeparatorPos = strpos($nodePath, $this->pathSeparator))
            ? $nodePath
            : substr($nodePath, 0, $pathSeparatorPos);

        if (null === $node = ($this->children[$firstPathSegment] ?? null)) {
            throw new \RuntimeException(\sprintf('Node with name "%s" does not exist in the current node "%s".', $firstPathSegment, $this->name));
        }

        if (false === $pathSeparatorPos) {
            return $node;
        }

        return $node->find(substr($nodePath, $pathSeparatorPos + \strlen($this->pathSeparator)));
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Definition\Builder;

use Symfony\Component\Config\Definition\NodeInterface;

/**
 * This is the entry class for building a config tree.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class TreeBuilder implements NodeParentInterface
{
    /**
     * @var NodeInterface|null
     */
    protected $tree;

    /**
     * @var NodeDefinition
     */
    protected $root;

    public function __construct(string $name, string $type = 'array', ?NodeBuilder $builder = null)
    {
        $builder ??= new NodeBuilder();
        $this->root = $builder->node($name, $type)->setParent($this);
    }

    /**
     * @return NodeDefinition|ArrayNodeDefinition The root node (as an ArrayNodeDefinition when the type is 'array')
     */
    public function getRootNode(): NodeDefinition|ArrayNodeDefinition
    {
        return $this->root;
    }

    /**
     * Builds the tree.
     *
     * @throws \RuntimeException
     */
    public function buildTree(): NodeInterface
    {
        return $this->tree ??= $this->root->getNode(true);
    }

    /**
     * @return void
     */
    public function setPathSeparator(string $separator)
    {
        // unset last built as changing path separator changes all nodes
        $this->tree = null;

        $this->root->setPathSeparator($separator);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Definition\Builder;

/**
 * This class builds validation conditions.
 *
 * @author Christophe Coevoet <stof@notk.org>
 */
class ValidationBuilder
{
    protected $node;
    public $rules = [];

    public function __construct(NodeDefinition $node)
    {
        $this->node = $node;
    }

    /**
     * Registers a closure to run as normalization or an expression builder to build it if null is provided.
     *
     * @return ExprBuilder|$this
     */
    public function rule(?\Closure $closure = null): ExprBuilder|static
    {
        if (null !== $closure) {
            $this->rules[] = $closure;

            return $this;
        }

        return $this->rules[] = new ExprBuilder($this->node);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Definition\Builder;

/**
 * This class provides a fluent interface for building a node.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class NodeBuilder implements NodeParentInterface
{
    protected $parent;
    protected $nodeMapping;

    public function __construct()
    {
        $this->nodeMapping = [
            'variable' => VariableNodeDefinition::class,
            'scalar' => ScalarNodeDefinition::class,
            'boolean' => BooleanNodeDefinition::class,
            'integer' => IntegerNodeDefinition::class,
            'float' => FloatNodeDefinition::class,
            'array' => ArrayNodeDefinition::class,
            'enum' => EnumNodeDefinition::class,
        ];
    }

    /**
     * Set the parent node.
     *
     * @return $this
     */
    public function setParent(?ParentNodeDefinitionInterface $parent = null): static
    {
        if (1 > \func_num_args()) {
            trigger_deprecation('symfony/form', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__);
        }
        $this->parent = $parent;

        return $this;
    }

    /**
     * Creates a child array node.
     */
    public function arrayNode(string $name): ArrayNodeDefinition
    {
        return $this->node($name, 'array');
    }

    /**
     * Creates a child scalar node.
     */
    public function scalarNode(string $name): ScalarNodeDefinition
    {
        return $this->node($name, 'scalar');
    }

    /**
     * Creates a child Boolean node.
     */
    public function booleanNode(string $name): BooleanNodeDefinition
    {
        return $this->node($name, 'boolean');
    }

    /**
     * Creates a child integer node.
     */
    public function integerNode(string $name): IntegerNodeDefinition
    {
        return $this->node($name, 'integer');
    }

    /**
     * Creates a child float node.
     */
    public function floatNode(string $name): FloatNodeDefinition
    {
        return $this->node($name, 'float');
    }

    /**
     * Creates a child EnumNode.
     */
    public function enumNode(string $name): EnumNodeDefinition
    {
        return $this->node($name, 'enum');
    }

    /**
     * Creates a child variable node.
     */
    public function variableNode(string $name): VariableNodeDefinition
    {
        return $this->node($name, 'variable');
    }

    /**
     * Returns the parent node.
     *
     * @return NodeDefinition&ParentNodeDefinitionInterface
     */
    public function end()
    {
        return $this->parent;
    }

    /**
     * Creates a child node.
     *
     * @throws \RuntimeException When the node type is not registered
     * @throws \RuntimeException When the node class is not found
     */
    public function node(?string $name, string $type): NodeDefinition
    {
        $class = $this->getNodeClass($type);

        $node = new $class($name);

        $this->append($node);

        return $node;
    }

    /**
     * Appends a node definition.
     *
     * Usage:
     *
     *     $node = new ArrayNodeDefinition('name')
     *         ->children()
     *             ->scalarNode('foo')->end()
     *             ->scalarNode('baz')->end()
     *             ->append($this->getBarNodeDefinition())
     *         ->end()
     *     ;
     *
     * @return $this
     */
    public function append(NodeDefinition $node): static
    {
        if ($node instanceof BuilderAwareInterface) {
            $builder = clone $this;
            $builder->setParent(null);
            $node->setBuilder($builder);
        }

        if (null !== $this->parent) {
            $this->parent->append($node);
            // Make this builder the node parent to allow for a fluid interface
            $node->setParent($this);
        }

        return $this;
    }

    /**
     * Adds or overrides a node Type.
     *
     * @param string $type  The name of the type
     * @param string $class The fully qualified name the node definition class
     *
     * @return $this
     */
    public function setNodeClass(string $type, string $class): static
    {
        $this->nodeMapping[strtolower($type)] = $class;

        return $this;
    }

    /**
     * Returns the class name of the node definition.
     *
     * @throws \RuntimeException When the node type is not registered
     * @throws \RuntimeException When the node class is not found
     */
    protected function getNodeClass(string $type): string
    {
        $type = strtolower($type);

        if (!isset($this->nodeMapping[$type])) {
            throw new \RuntimeException(\sprintf('The node type "%s" is not registered.', $type));
        }

        $class = $this->nodeMapping[$type];

        if (!class_exists($class)) {
            throw new \RuntimeException(\sprintf('The node class "%s" does not exist.', $class));
        }

        return $class;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Definition\Builder;

use Symfony\Component\Config\Definition\NodeInterface;
use Symfony\Component\Config\Definition\VariableNode;

/**
 * This class provides a fluent interface for defining a node.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class VariableNodeDefinition extends NodeDefinition
{
    /**
     * Instantiate a Node.
     */
    protected function instantiateNode(): VariableNode
    {
        return new VariableNode($this->name, $this->parent, $this->pathSeparator);
    }

    protected function createNode(): NodeInterface
    {
        $node = $this->instantiateNode();

        if (isset($this->normalization)) {
            $node->setNormalizationClosures($this->normalization->before);
        }

        if (isset($this->merge)) {
            $node->setAllowOverwrite($this->merge->allowOverwrite);
        }

        if (true === $this->default) {
            $node->setDefaultValue($this->defaultValue);
        }

        $node->setAllowEmptyValue($this->allowEmptyValue);
        $node->addEquivalentValue(null, $this->nullEquivalent);
        $node->addEquivalentValue(true, $this->trueEquivalent);
        $node->addEquivalentValue(false, $this->falseEquivalent);
        $node->setRequired($this->required);

        if ($this->deprecation) {
            $node->setDeprecated($this->deprecation['package'], $this->deprecation['version'], $this->deprecation['message']);
        }

        if (isset($this->validation)) {
            $node->setFinalValidationClosures($this->validation->rules);
        }

        return $node;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Definition\Builder;

use Symfony\Component\Config\Definition\Exception\UnsetKeyException;

/**
 * This class builds an if expression.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 * @author Christophe Coevoet <stof@notk.org>
 */
class ExprBuilder
{
    public const TYPE_ANY = 'any';
    public const TYPE_STRING = 'string';
    public const TYPE_NULL = 'null';
    public const TYPE_ARRAY = 'array';

    protected $node;

    public $allowedTypes;
    public $ifPart;
    public $thenPart;

    public function __construct(NodeDefinition $node)
    {
        $this->node = $node;
    }

    /**
     * Marks the expression as being always used.
     *
     * @return $this
     */
    public function always(?\Closure $then = null): static
    {
        $this->ifPart = static fn () => true;
        $this->allowedTypes = self::TYPE_ANY;

        if (null !== $then) {
            $this->thenPart = $then;
        }

        return $this;
    }

    /**
     * Sets a closure to use as tests.
     *
     * The default one tests if the value is true.
     *
     * @return $this
     */
    public function ifTrue(?\Closure $closure = null): static
    {
        $this->ifPart = $closure ?? static fn ($v) => true === $v;
        $this->allowedTypes = self::TYPE_ANY;

        return $this;
    }

    /**
     * Tests if the value is a string.
     *
     * @return $this
     */
    public function ifString(): static
    {
        $this->ifPart = \is_string(...);
        $this->allowedTypes = self::TYPE_STRING;

        return $this;
    }

    /**
     * Tests if the value is null.
     *
     * @return $this
     */
    public function ifNull(): static
    {
        $this->ifPart = \is_null(...);
        $this->allowedTypes = self::TYPE_NULL;

        return $this;
    }

    /**
     * Tests if the value is empty.
     *
     * @return $this
     */
    public function ifEmpty(): static
    {
        $this->ifPart = static fn ($v) => empty($v);
        $this->allowedTypes = self::TYPE_ANY;

        return $this;
    }

    /**
     * Tests if the value is an array.
     *
     * @return $this
     */
    public function ifArray(): static
    {
        $this->ifPart = \is_array(...);
        $this->allowedTypes = self::TYPE_ARRAY;

        return $this;
    }

    /**
     * Tests if the value is in an array.
     *
     * @return $this
     */
    public function ifInArray(array $array): static
    {
        $this->ifPart = static fn ($v) => \in_array($v, $array, true);
        $this->allowedTypes = self::TYPE_ANY;

        return $this;
    }

    /**
     * Tests if the value is not in an array.
     *
     * @return $this
     */
    public function ifNotInArray(array $array): static
    {
        $this->ifPart = static fn ($v) => !\in_array($v, $array, true);
        $this->allowedTypes = self::TYPE_ANY;

        return $this;
    }

    /**
     * Transforms variables of any type into an array.
     *
     * @return $this
     */
    public function castToArray(): static
    {
        $this->ifPart = static fn ($v) => !\is_array($v);
        $this->allowedTypes = self::TYPE_ANY;
        $this->thenPart = static fn ($v) => [$v];

        return $this;
    }

    /**
     * Sets the closure to run if the test pass.
     *
     * @return $this
     */
    public function then(\Closure $closure): static
    {
        $this->thenPart = $closure;

        return $this;
    }

    /**
     * Sets a closure returning an empty array.
     *
     * @return $this
     */
    public function thenEmptyArray(): static
    {
        $this->thenPart = static fn () => [];

        return $this;
    }

    /**
     * Sets a closure marking the value as invalid at processing time.
     *
     * if you want to add the value of the node in your message just use a %s placeholder.
     *
     * @return $this
     *
     * @throws \InvalidArgumentException
     */
    public function thenInvalid(string $message): static
    {
        $this->thenPart = static fn ($v) => throw new \InvalidArgumentException(\sprintf($message, json_encode($v)));

        return $this;
    }

    /**
     * Sets a closure unsetting this key of the array at processing time.
     *
     * @return $this
     *
     * @throws UnsetKeyException
     */
    public function thenUnset(): static
    {
        $this->thenPart = static fn () => throw new UnsetKeyException('Unsetting key.');

        return $this;
    }

    /**
     * Returns the related node.
     *
     * @throws \RuntimeException
     */
    public function end(): NodeDefinition|ArrayNodeDefinition|VariableNodeDefinition
    {
        if (null === $this->ifPart) {
            throw new \RuntimeException('You must specify an if part.');
        }
        if (null === $this->thenPart) {
            throw new \RuntimeException('You must specify a then part.');
        }

        return $this->node;
    }

    /**
     * Builds the expressions.
     *
     * @param ExprBuilder[] $expressions An array of ExprBuilder instances to build
     */
    public static function buildExpressions(array $expressions): array
    {
        foreach ($expressions as $k => $expr) {
            if ($expr instanceof self) {
                $if = $expr->ifPart;
                $then = $expr->thenPart;
                $expressions[$k] = static fn ($v) => $if($v) ? $then($v) : $v;
            }
        }

        return $expressions;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Definition\Builder;

/**
 * This class builds merge conditions.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class MergeBuilder
{
    protected $node;
    public $allowFalse = false;
    public $allowOverwrite = true;

    public function __construct(NodeDefinition $node)
    {
        $this->node = $node;
    }

    /**
     * Sets whether the node can be unset.
     *
     * @return $this
     */
    public function allowUnset(bool $allow = true): static
    {
        $this->allowFalse = $allow;

        return $this;
    }

    /**
     * Sets whether the node can be overwritten.
     *
     * @return $this
     */
    public function denyOverwrite(bool $deny = true): static
    {
        $this->allowOverwrite = !$deny;

        return $this;
    }

    /**
     * Returns the related node.
     */
    public function end(): NodeDefinition|ArrayNodeDefinition|VariableNodeDefinition
    {
        return $this->node;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Definition\Builder;

/**
 * An interface that must be implemented by all node parents.
 *
 * @author Victor Berchet <victor@suumit.com>
 */
interface NodeParentInterface
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Definition\Builder;

use Symfony\Component\Config\Definition\BaseNode;
use Symfony\Component\Config\Definition\Exception\InvalidDefinitionException;
use Symfony\Component\Config\Definition\NodeInterface;

/**
 * This class provides a fluent interface for defining a node.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
abstract class NodeDefinition implements NodeParentInterface
{
    protected $name;
    protected $normalization;
    protected $validation;
    protected $defaultValue;
    protected $default = false;
    protected $required = false;
    protected $deprecation = [];
    protected $merge;
    protected $allowEmptyValue = true;
    protected $nullEquivalent;
    protected $trueEquivalent = true;
    protected $falseEquivalent = false;
    protected $pathSeparator = BaseNode::DEFAULT_PATH_SEPARATOR;
    protected $parent;
    protected $attributes = [];

    public function __construct(?string $name, ?NodeParentInterface $parent = null)
    {
        $this->parent = $parent;
        $this->name = $name;
    }

    /**
     * Sets the parent node.
     *
     * @return $this
     */
    public function setParent(NodeParentInterface $parent): static
    {
        $this->parent = $parent;

        return $this;
    }

    /**
     * Sets info message.
     *
     * @return $this
     */
    public function info(string $info): static
    {
        return $this->attribute('info', $info);
    }

    /**
     * Sets example configuration.
     *
     * @return $this
     */
    public function example(string|array $example): static
    {
        return $this->attribute('example', $example);
    }

    /**
     * Sets an attribute on the node.
     *
     * @return $this
     */
    public function attribute(string $key, mixed $value): static
    {
        $this->attributes[$key] = $value;

        return $this;
    }

    /**
     * Returns the parent node.
     */
    public function end(): NodeParentInterface|NodeBuilder|self|ArrayNodeDefinition|VariableNodeDefinition|null
    {
        return $this->parent;
    }

    /**
     * Creates the node.
     */
    public function getNode(bool $forceRootNode = false): NodeInterface
    {
        if ($forceRootNode) {
            $this->parent = null;
        }

        if (isset($this->normalization)) {
            $allowedTypes = [];
            foreach ($this->normalization->before as $expr) {
                $allowedTypes[] = $expr->allowedTypes;
            }
            $allowedTypes = array_unique($allowedTypes);
            $this->normalization->before = ExprBuilder::buildExpressions($this->normalization->before);
            $this->normalization->declaredTypes = $allowedTypes;
        }

        if (isset($this->validation)) {
            $this->validation->rules = ExprBuilder::buildExpressions($this->validation->rules);
        }

        $node = $this->createNode();
        if ($node instanceof BaseNode) {
            $node->setAttributes($this->attributes);
        }

        return $node;
    }

    /**
     * Sets the default value.
     *
     * @return $this
     */
    public function defaultValue(mixed $value): static
    {
        $this->default = true;
        $this->defaultValue = $value;

        return $this;
    }

    /**
     * Sets the node as required.
     *
     * @return $this
     */
    public function isRequired(): static
    {
        $this->required = true;

        return $this;
    }

    /**
     * Sets the node as deprecated.
     *
     * @param string $package The name of the composer package that is triggering the deprecation
     * @param string $version The version of the package that introduced the deprecation
     * @param string $message the deprecation message to use
     *
     * You can use %node% and %path% placeholders in your message to display,
     * respectively, the node name and its complete path
     *
     * @return $this
     */
    public function setDeprecated(string $package, string $version, string $message = 'The child node "%node%" at path "%path%" is deprecated.'): static
    {
        $this->deprecation = [
            'package' => $package,
            'version' => $version,
            'message' => $message,
        ];

        return $this;
    }

    /**
     * Sets the equivalent value used when the node contains null.
     *
     * @return $this
     */
    public function treatNullLike(mixed $value): static
    {
        $this->nullEquivalent = $value;

        return $this;
    }

    /**
     * Sets the equivalent value used when the node contains true.
     *
     * @return $this
     */
    public function treatTrueLike(mixed $value): static
    {
        $this->trueEquivalent = $value;

        return $this;
    }

    /**
     * Sets the equivalent value used when the node contains false.
     *
     * @return $this
     */
    public function treatFalseLike(mixed $value): static
    {
        $this->falseEquivalent = $value;

        return $this;
    }

    /**
     * Sets null as the default value.
     *
     * @return $this
     */
    public function defaultNull(): static
    {
        return $this->defaultValue(null);
    }

    /**
     * Sets true as the default value.
     *
     * @return $this
     */
    public function defaultTrue(): static
    {
        return $this->defaultValue(true);
    }

    /**
     * Sets false as the default value.
     *
     * @return $this
     */
    public function defaultFalse(): static
    {
        return $this->defaultValue(false);
    }

    /**
     * Sets an expression to run before the normalization.
     */
    public function beforeNormalization(): ExprBuilder
    {
        return $this->normalization()->before();
    }

    /**
     * Denies the node value being empty.
     *
     * @return $this
     */
    public function cannotBeEmpty(): static
    {
        $this->allowEmptyValue = false;

        return $this;
    }

    /**
     * Sets an expression to run for the validation.
     *
     * The expression receives the value of the node and must return it. It can
     * modify it.
     * An exception should be thrown when the node is not valid.
     */
    public function validate(): ExprBuilder
    {
        return $this->validation()->rule();
    }

    /**
     * Sets whether the node can be overwritten.
     *
     * @return $this
     */
    public function cannotBeOverwritten(bool $deny = true): static
    {
        $this->merge()->denyOverwrite($deny);

        return $this;
    }

    /**
     * Gets the builder for validation rules.
     */
    protected function validation(): ValidationBuilder
    {
        return $this->validation ??= new ValidationBuilder($this);
    }

    /**
     * Gets the builder for merging rules.
     */
    protected function merge(): MergeBuilder
    {
        return $this->merge ??= new MergeBuilder($this);
    }

    /**
     * Gets the builder for normalization rules.
     */
    protected function normalization(): NormalizationBuilder
    {
        return $this->normalization ??= new NormalizationBuilder($this);
    }

    /**
     * Instantiate and configure the node according to this definition.
     *
     * @throws InvalidDefinitionException When the definition is invalid
     */
    abstract protected function createNode(): NodeInterface;

    /**
     * Set PathSeparator to use.
     *
     * @return $this
     */
    public function setPathSeparator(string $separator): static
    {
        if ($this instanceof ParentNodeDefinitionInterface) {
            foreach ($this->getChildNodeDefinitions() as $child) {
                $child->setPathSeparator($separator);
            }
        }

        $this->pathSeparator = $separator;

        return $this;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Definition\Builder;

/**
 * An interface that can be implemented by nodes which build other nodes.
 *
 * @author Roland Franssen <franssen.roland@gmail.com>
 */
interface BuilderAwareInterface
{
    /**
     * Sets a custom children builder.
     *
     * @return void
     */
    public function setBuilder(NodeBuilder $builder);
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Definition\Builder;

/**
 * This class builds normalization conditions.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class NormalizationBuilder
{
    protected $node;
    public $before = [];
    public $declaredTypes = [];
    public $remappings = [];

    public function __construct(NodeDefinition $node)
    {
        $this->node = $node;
    }

    /**
     * Registers a key to remap to its plural form.
     *
     * @param string      $key    The key to remap
     * @param string|null $plural The plural of the key in case of irregular plural
     *
     * @return $this
     */
    public function remap(string $key, ?string $plural = null): static
    {
        $this->remappings[] = [$key, null === $plural ? $key.'s' : $plural];

        return $this;
    }

    /**
     * Registers a closure to run before the normalization or an expression builder to build it if null is provided.
     *
     * @return ExprBuilder|$this
     */
    public function before(?\Closure $closure = null): ExprBuilder|static
    {
        if (null !== $closure) {
            $this->before[] = $closure;

            return $this;
        }

        return $this->before[] = new ExprBuilder($this->node);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Definition\Builder;

use Symfony\Component\Config\Definition\EnumNode;

/**
 * Enum Node Definition.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class EnumNodeDefinition extends ScalarNodeDefinition
{
    private array $values;

    /**
     * @return $this
     */
    public function values(array $values): static
    {
        if (!$values) {
            throw new \InvalidArgumentException('->values() must be called with at least one value.');
        }

        $this->values = $values;

        return $this;
    }

    /**
     * Instantiate a Node.
     *
     * @throws \RuntimeException
     */
    protected function instantiateNode(): EnumNode
    {
        if (!isset($this->values)) {
            throw new \RuntimeException('You must call ->values() on enum nodes.');
        }

        return new EnumNode($this->name, $this->parent, $this->values, $this->pathSeparator);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Definition\Builder;

use Symfony\Component\Config\Definition\IntegerNode;

/**
 * This class provides a fluent interface for defining an integer node.
 *
 * @author Jeanmonod David <david.jeanmonod@gmail.com>
 */
class IntegerNodeDefinition extends NumericNodeDefinition
{
    /**
     * Instantiates a Node.
     */
    protected function instantiateNode(): IntegerNode
    {
        return new IntegerNode($this->name, $this->parent, $this->min, $this->max, $this->pathSeparator);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Definition\Builder;

/**
 * An interface that must be implemented by nodes which can have children.
 *
 * @author Victor Berchet <victor@suumit.com>
 */
interface ParentNodeDefinitionInterface extends BuilderAwareInterface
{
    /**
     * Returns a builder to add children nodes.
     */
    public function children(): NodeBuilder;

    /**
     * Appends a node definition.
     *
     * Usage:
     *
     *     $node = $parentNode
     *         ->children()
     *             ->scalarNode('foo')->end()
     *             ->scalarNode('baz')->end()
     *             ->append($this->getBarNodeDefinition())
     *         ->end()
     *     ;
     *
     * @return $this
     */
    public function append(NodeDefinition $node): static;

    /**
     * Gets the child node definitions.
     *
     * @return NodeDefinition[]
     */
    public function getChildNodeDefinitions(): array;
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Definition\Builder;

use Symfony\Component\Config\Definition\BooleanNode;
use Symfony\Component\Config\Definition\Exception\InvalidDefinitionException;

/**
 * This class provides a fluent interface for defining a node.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class BooleanNodeDefinition extends ScalarNodeDefinition
{
    public function __construct(?string $name, ?NodeParentInterface $parent = null)
    {
        parent::__construct($name, $parent);

        $this->nullEquivalent = true;
    }

    /**
     * Instantiate a Node.
     */
    protected function instantiateNode(): BooleanNode
    {
        return new BooleanNode($this->name, $this->parent, $this->pathSeparator);
    }

    /**
     * @throws InvalidDefinitionException
     */
    public function cannotBeEmpty(): static
    {
        throw new InvalidDefinitionException('->cannotBeEmpty() is not applicable to BooleanNodeDefinition.');
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Definition;

use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;

/**
 * Node which only allows a finite set of values.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class EnumNode extends ScalarNode
{
    private array $values;

    public function __construct(?string $name, ?NodeInterface $parent = null, array $values = [], string $pathSeparator = BaseNode::DEFAULT_PATH_SEPARATOR)
    {
        if (!$values) {
            throw new \InvalidArgumentException('$values must contain at least one element.');
        }

        foreach ($values as $value) {
            if (null === $value || \is_scalar($value)) {
                continue;
            }

            if (!$value instanceof \UnitEnum) {
                throw new \InvalidArgumentException(\sprintf('"%s" only supports scalar, enum, or null values, "%s" given.', __CLASS__, get_debug_type($value)));
            }

            if ($value::class !== ($enumClass ??= $value::class)) {
                throw new \InvalidArgumentException(\sprintf('"%s" only supports one type of enum, "%s" and "%s" passed.', __CLASS__, $enumClass, $value::class));
            }
        }

        parent::__construct($name, $parent, $pathSeparator);
        $this->values = $values;
    }

    /**
     * @return array
     */
    public function getValues()
    {
        return $this->values;
    }

    /**
     * @internal
     */
    public function getPermissibleValues(string $separator): string
    {
        return implode($separator, array_unique(array_map(static function (mixed $value): string {
            if (!$value instanceof \UnitEnum) {
                return json_encode($value);
            }

            return ltrim(var_export($value, true), '\\');
        }, $this->values)));
    }

    /**
     * @return void
     */
    protected function validateType(mixed $value)
    {
        if ($value instanceof \UnitEnum) {
            return;
        }

        parent::validateType($value);
    }

    protected function finalizeValue(mixed $value): mixed
    {
        $value = parent::finalizeValue($value);

        if (!\in_array($value, $this->values, true)) {
            $ex = new InvalidConfigurationException(\sprintf('The value %s is not allowed for path "%s". Permissible values: %s', json_encode($value), $this->getPath(), $this->getPermissibleValues(', ')));
            $ex->setPath($this->getPath());

            throw $ex;
        }

        return $value;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Definition;

use Symfony\Component\Config\Definition\Exception\InvalidTypeException;

/**
 * This node represents a scalar value in the config tree.
 *
 * The following values are considered scalars:
 *   * booleans
 *   * strings
 *   * null
 *   * integers
 *   * floats
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class ScalarNode extends VariableNode
{
    /**
     * @return void
     */
    protected function validateType(mixed $value)
    {
        if (!\is_scalar($value) && null !== $value) {
            $ex = new InvalidTypeException(\sprintf('Invalid type for path "%s". Expected "scalar", but got "%s".', $this->getPath(), get_debug_type($value)));
            if ($hint = $this->getInfo()) {
                $ex->addHint($hint);
            }
            $ex->setPath($this->getPath());

            throw $ex;
        }
    }

    protected function isValueEmpty(mixed $value): bool
    {
        // assume environment variables are never empty (which in practice is likely to be true during runtime)
        // not doing so breaks many configs that are valid today
        if ($this->isHandlingPlaceholder()) {
            return false;
        }

        return null === $value || '' === $value;
    }

    protected function getValidPlaceholderTypes(): array
    {
        return ['bool', 'int', 'float', 'string'];
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Definition;

use Symfony\Component\Config\Definition\Exception\DuplicateKeyException;
use Symfony\Component\Config\Definition\Exception\Exception;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\Config\Definition\Exception\UnsetKeyException;

/**
 * Represents a prototyped Array node in the config tree.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class PrototypedArrayNode extends ArrayNode
{
    protected $prototype;
    protected $keyAttribute;
    protected $removeKeyAttribute = false;
    protected $minNumberOfElements = 0;
    protected $defaultValue = [];
    protected $defaultChildren;
    /**
     * @var NodeInterface[] An array of the prototypes of the simplified value children
     */
    private array $valuePrototypes = [];

    /**
     * Sets the minimum number of elements that a prototype based node must
     * contain. By default this is zero, meaning no elements.
     *
     * @return void
     */
    public function setMinNumberOfElements(int $number)
    {
        $this->minNumberOfElements = $number;
    }

    /**
     * Sets the attribute which value is to be used as key.
     *
     * This is useful when you have an indexed array that should be an
     * associative array. You can select an item from within the array
     * to be the key of the particular item. For example, if "id" is the
     * "key", then:
     *
     *     [
     *         ['id' => 'my_name', 'foo' => 'bar'],
     *     ];
     *
     *  becomes
     *
     *      [
     *          'my_name' => ['foo' => 'bar'],
     *      ];
     *
     * If you'd like "'id' => 'my_name'" to still be present in the resulting
     * array, then you can set the second argument of this method to false.
     *
     * @param string $attribute The name of the attribute which value is to be used as a key
     * @param bool   $remove    Whether or not to remove the key
     *
     * @return void
     */
    public function setKeyAttribute(string $attribute, bool $remove = true)
    {
        $this->keyAttribute = $attribute;
        $this->removeKeyAttribute = $remove;
    }

    /**
     * Retrieves the name of the attribute which value should be used as key.
     */
    public function getKeyAttribute(): ?string
    {
        return $this->keyAttribute;
    }

    /**
     * Sets the default value of this node.
     *
     * @return void
     */
    public function setDefaultValue(array $value)
    {
        $this->defaultValue = $value;
    }

    public function hasDefaultValue(): bool
    {
        return true;
    }

    /**
     * Adds default children when none are set.
     *
     * @param int|string|array|null $children The number of children|The child name|The children names to be added
     *
     * @return void
     */
    public function setAddChildrenIfNoneSet(int|string|array|null $children = ['defaults'])
    {
        if (null === $children) {
            $this->defaultChildren = ['defaults'];
        } else {
            $this->defaultChildren = \is_int($children) && $children > 0 ? range(1, $children) : (array) $children;
        }
    }

    /**
     * The default value could be either explicited or derived from the prototype
     * default value.
     */
    public function getDefaultValue(): mixed
    {
        if (null !== $this->defaultChildren) {
            $default = $this->prototype->hasDefaultValue() ? $this->prototype->getDefaultValue() : [];
            $defaults = [];
            foreach (array_values($this->defaultChildren) as $i => $name) {
                $defaults[null === $this->keyAttribute ? $i : $name] = $default;
            }

            return $defaults;
        }

        return $this->defaultValue;
    }

    /**
     * Sets the node prototype.
     *
     * @return void
     */
    public function setPrototype(PrototypeNodeInterface $node)
    {
        $this->prototype = $node;
    }

    /**
     * Retrieves the prototype.
     */
    public function getPrototype(): PrototypeNodeInterface
    {
        return $this->prototype;
    }

    /**
     * Disable adding concrete children for prototyped nodes.
     *
     * @return never
     *
     * @throws Exception
     */
    public function addChild(NodeInterface $node)
    {
        throw new Exception('A prototyped array node cannot have concrete children.');
    }

    protected function finalizeValue(mixed $value): mixed
    {
        if (false === $value) {
            throw new UnsetKeyException(\sprintf('Unsetting key for path "%s", value: %s.', $this->getPath(), json_encode($value)));
        }

        foreach ($value as $k => $v) {
            $prototype = $this->getPrototypeForChild($k);
            try {
                $value[$k] = $prototype->finalize($v);
            } catch (UnsetKeyException) {
                unset($value[$k]);
            }
        }

        if (\count($value) < $this->minNumberOfElements) {
            $ex = new InvalidConfigurationException(\sprintf('The path "%s" should have at least %d element(s) defined.', $this->getPath(), $this->minNumberOfElements));
            $ex->setPath($this->getPath());

            throw $ex;
        }

        return $value;
    }

    /**
     * @throws DuplicateKeyException
     */
    protected function normalizeValue(mixed $value): mixed
    {
        if (false === $value) {
            return $value;
        }

        $value = $this->remapXml($value);

        $isList = array_is_list($value);
        $normalized = [];
        foreach ($value as $k => $v) {
            if (null !== $this->keyAttribute && \is_array($v)) {
                if (!isset($v[$this->keyAttribute]) && \is_int($k) && $isList) {
                    $ex = new InvalidConfigurationException(\sprintf('The attribute "%s" must be set for path "%s".', $this->keyAttribute, $this->getPath()));
                    $ex->setPath($this->getPath());

                    throw $ex;
                } elseif (isset($v[$this->keyAttribute])) {
                    $k = $v[$this->keyAttribute];

                    if (\is_float($k)) {
                        $k = var_export($k, true);
                    }

                    // remove the key attribute when required
                    if ($this->removeKeyAttribute) {
                        unset($v[$this->keyAttribute]);
                    }

                    // if only "value" is left
                    if (array_keys($v) === ['value']) {
                        $v = $v['value'];
                        if ($this->prototype instanceof ArrayNode && ($children = $this->prototype->getChildren()) && \array_key_exists('value', $children)) {
                            $valuePrototype = current($this->valuePrototypes) ?: clone $children['value'];
                            $valuePrototype->parent = $this;
                            $originalClosures = $this->prototype->normalizationClosures;
                            if (\is_array($originalClosures)) {
                                $valuePrototypeClosures = $valuePrototype->normalizationClosures;
                                $valuePrototype->normalizationClosures = \is_array($valuePrototypeClosures) ? array_merge($originalClosures, $valuePrototypeClosures) : $originalClosures;
                            }
                            $this->valuePrototypes[$k] = $valuePrototype;
                        }
                    }
                }

                if (\array_key_exists($k, $normalized)) {
                    $ex = new DuplicateKeyException(\sprintf('Duplicate key "%s" for path "%s".', $k, $this->getPath()));
                    $ex->setPath($this->getPath());

                    throw $ex;
                }
            }

            $prototype = $this->getPrototypeForChild($k);
            if (null !== $this->keyAttribute || !$isList) {
                $normalized[$k] = $prototype->normalize($v);
            } else {
                $normalized[] = $prototype->normalize($v);
            }
        }

        return $normalized;
    }

    protected function mergeValues(mixed $leftSide, mixed $rightSide): mixed
    {
        if (false === $rightSide) {
            // if this is still false after the last config has been merged the
            // finalization pass will take care of removing this key entirely
            return false;
        }

        if (false === $leftSide || !$this->performDeepMerging) {
            return $rightSide;
        }

        $isList = array_is_list($rightSide);
        foreach ($rightSide as $k => $v) {
            // prototype, and key is irrelevant there are no named keys, append the element
            if (null === $this->keyAttribute && $isList) {
                $leftSide[] = $v;
                continue;
            }

            // no conflict
            if (!\array_key_exists($k, $leftSide)) {
                if (!$this->allowNewKeys) {
                    $ex = new InvalidConfigurationException(\sprintf('You are not allowed to define new elements for path "%s". Please define all elements for this path in one config file.', $this->getPath()));
                    $ex->setPath($this->getPath());

                    throw $ex;
                }

                $leftSide[$k] = $v;
                continue;
            }

            $prototype = $this->getPrototypeForChild($k);
            $leftSide[$k] = $prototype->merge($leftSide[$k], $v);
        }

        return $leftSide;
    }

    /**
     * Returns a prototype for the child node that is associated to $key in the value array.
     * For general child nodes, this will be $this->prototype.
     * But if $this->removeKeyAttribute is true and there are only two keys in the child node:
     * one is same as this->keyAttribute and the other is 'value', then the prototype will be different.
     *
     * For example, assume $this->keyAttribute is 'name' and the value array is as follows:
     *
     *     [
     *         [
     *             'name' => 'name001',
     *             'value' => 'value001'
     *         ]
     *     ]
     *
     * Now, the key is 0 and the child node is:
     *
     *     [
     *        'name' => 'name001',
     *        'value' => 'value001'
     *     ]
     *
     * When normalizing the value array, the 'name' element will removed from the child node
     * and its value becomes the new key of the child node:
     *
     *     [
     *         'name001' => ['value' => 'value001']
     *     ]
     *
     * Now only 'value' element is left in the child node which can be further simplified into a string:
     *
     *     ['name001' => 'value001']
     *
     * Now, the key becomes 'name001' and the child node becomes 'value001' and
     * the prototype of child node 'name001' should be a ScalarNode instead of an ArrayNode instance.
     */
    private function getPrototypeForChild(string $key): mixed
    {
        $prototype = $this->valuePrototypes[$key] ?? $this->prototype;
        $prototype->setName($key);

        return $prototype;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Definition;

use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\Config\Definition\Exception\InvalidTypeException;
use Symfony\Component\Config\Definition\Exception\UnsetKeyException;

/**
 * Represents an Array node in the config tree.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class ArrayNode extends BaseNode implements PrototypeNodeInterface
{
    protected $xmlRemappings = [];
    protected $children = [];
    protected $allowFalse = false;
    protected $allowNewKeys = true;
    protected $addIfNotSet = false;
    protected $performDeepMerging = true;
    protected $ignoreExtraKeys = false;
    protected $removeExtraKeys = true;
    protected $normalizeKeys = true;

    /**
     * @return void
     */
    public function setNormalizeKeys(bool $normalizeKeys)
    {
        $this->normalizeKeys = $normalizeKeys;
    }

    /**
     * Namely, you mostly have foo_bar in YAML while you have foo-bar in XML.
     * After running this method, all keys are normalized to foo_bar.
     *
     * If you have a mixed key like foo-bar_moo, it will not be altered.
     * The key will also not be altered if the target key already exists.
     */
    protected function preNormalize(mixed $value): mixed
    {
        if (!$this->normalizeKeys || !\is_array($value)) {
            return $value;
        }

        $normalized = [];

        foreach ($value as $k => $v) {
            if (str_contains($k, '-') && !str_contains($k, '_') && !\array_key_exists($normalizedKey = str_replace('-', '_', $k), $value)) {
                $normalized[$normalizedKey] = $v;
            } else {
                $normalized[$k] = $v;
            }
        }

        return $normalized;
    }

    /**
     * Retrieves the children of this node.
     *
     * @return array<string, NodeInterface>
     */
    public function getChildren(): array
    {
        return $this->children;
    }

    /**
     * Sets the xml remappings that should be performed.
     *
     * @param array $remappings An array of the form [[string, string]]
     *
     * @return void
     */
    public function setXmlRemappings(array $remappings)
    {
        $this->xmlRemappings = $remappings;
    }

    /**
     * Gets the xml remappings that should be performed.
     *
     * @return array an array of the form [[string, string]]
     */
    public function getXmlRemappings(): array
    {
        return $this->xmlRemappings;
    }

    /**
     * Sets whether to add default values for this array if it has not been
     * defined in any of the configuration files.
     *
     * @return void
     */
    public function setAddIfNotSet(bool $boolean)
    {
        $this->addIfNotSet = $boolean;
    }

    /**
     * Sets whether false is allowed as value indicating that the array should be unset.
     *
     * @return void
     */
    public function setAllowFalse(bool $allow)
    {
        $this->allowFalse = $allow;
    }

    /**
     * Sets whether new keys can be defined in subsequent configurations.
     *
     * @return void
     */
    public function setAllowNewKeys(bool $allow)
    {
        $this->allowNewKeys = $allow;
    }

    /**
     * Sets if deep merging should occur.
     *
     * @return void
     */
    public function setPerformDeepMerging(bool $boolean)
    {
        $this->performDeepMerging = $boolean;
    }

    /**
     * Whether extra keys should just be ignored without an exception.
     *
     * @param bool $boolean To allow extra keys
     * @param bool $remove  To remove extra keys
     *
     * @return void
     */
    public function setIgnoreExtraKeys(bool $boolean, bool $remove = true)
    {
        $this->ignoreExtraKeys = $boolean;
        $this->removeExtraKeys = $this->ignoreExtraKeys && $remove;
    }

    /**
     * Returns true when extra keys should be ignored without an exception.
     */
    public function shouldIgnoreExtraKeys(): bool
    {
        return $this->ignoreExtraKeys;
    }

    /**
     * @return void
     */
    public function setName(string $name)
    {
        $this->name = $name;
    }

    public function hasDefaultValue(): bool
    {
        return $this->addIfNotSet;
    }

    public function getDefaultValue(): mixed
    {
        if (!$this->hasDefaultValue()) {
            throw new \RuntimeException(\sprintf('The node at path "%s" has no default value.', $this->getPath()));
        }

        $defaults = [];
        foreach ($this->children as $name => $child) {
            if ($child->hasDefaultValue()) {
                $defaults[$name] = $child->getDefaultValue();
            }
        }

        return $defaults;
    }

    /**
     * Adds a child node.
     *
     * @return void
     *
     * @throws \InvalidArgumentException when the child node has no name
     * @throws \InvalidArgumentException when the child node's name is not unique
     */
    public function addChild(NodeInterface $node)
    {
        $name = $node->getName();
        if ('' === $name) {
            throw new \InvalidArgumentException('Child nodes must be named.');
        }
        if (isset($this->children[$name])) {
            throw new \InvalidArgumentException(\sprintf('A child node named "%s" already exists.', $name));
        }

        $this->children[$name] = $node;
    }

    /**
     * @throws UnsetKeyException
     * @throws InvalidConfigurationException if the node doesn't have enough children
     */
    protected function finalizeValue(mixed $value): mixed
    {
        if (false === $value) {
            throw new UnsetKeyException(\sprintf('Unsetting key for path "%s", value: %s.', $this->getPath(), json_encode($value)));
        }

        foreach ($this->children as $name => $child) {
            if (!\array_key_exists($name, $value)) {
                if ($child->isRequired()) {
                    $message = \sprintf('The child config "%s" under "%s" must be configured', $name, $this->getPath());
                    if ($child->getInfo()) {
                        $message .= \sprintf(': %s', $child->getInfo());
                    } else {
                        $message .= '.';
                    }
                    $ex = new InvalidConfigurationException($message);
                    $ex->setPath($this->getPath());

                    throw $ex;
                }

                if ($child->hasDefaultValue()) {
                    $value[$name] = $child->getDefaultValue();
                }

                continue;
            }

            if ($child->isDeprecated()) {
                $deprecation = $child->getDeprecation($name, $this->getPath());
                trigger_deprecation($deprecation['package'], $deprecation['version'], $deprecation['message']);
            }

            try {
                $value[$name] = $child->finalize($value[$name]);
            } catch (UnsetKeyException) {
                unset($value[$name]);
            }
        }

        return $value;
    }

    /**
     * @return void
     */
    protected function validateType(mixed $value)
    {
        if (!\is_array($value) && (!$this->allowFalse || false !== $value)) {
            $ex = new InvalidTypeException(\sprintf('Invalid type for path "%s". Expected "array", but got "%s"', $this->getPath(), get_debug_type($value)));
            if ($hint = $this->getInfo()) {
                $ex->addHint($hint);
            }
            $ex->setPath($this->getPath());

            throw $ex;
        }
    }

    /**
     * @throws InvalidConfigurationException
     */
    protected function normalizeValue(mixed $value): mixed
    {
        if (false === $value) {
            return $value;
        }

        $value = $this->remapXml($value);

        $normalized = [];
        foreach ($value as $name => $val) {
            if (isset($this->children[$name])) {
                try {
                    $normalized[$name] = $this->children[$name]->normalize($val);
                } catch (UnsetKeyException) {
                }
                unset($value[$name]);
            } elseif (!$this->removeExtraKeys) {
                $normalized[$name] = $val;
            }
        }

        // if extra fields are present, throw exception
        if (\count($value) && !$this->ignoreExtraKeys) {
            $proposals = array_keys($this->children);
            sort($proposals);
            $guesses = [];

            foreach (array_keys($value) as $subject) {
                $minScore = \INF;
                foreach ($proposals as $proposal) {
                    $distance = levenshtein($subject, $proposal);
                    if ($distance <= $minScore && $distance < 3) {
                        $guesses[$proposal] = $distance;
                        $minScore = $distance;
                    }
                }
            }

            $msg = \sprintf('Unrecognized option%s "%s" under "%s"', 1 === \count($value) ? '' : 's', implode(', ', array_keys($value)), $this->getPath());

            if (\count($guesses)) {
                asort($guesses);
                $msg .= \sprintf('. Did you mean "%s"?', implode('", "', array_keys($guesses)));
            } else {
                $msg .= \sprintf('. Available option%s %s "%s".', 1 === \count($proposals) ? '' : 's', 1 === \count($proposals) ? 'is' : 'are', implode('", "', $proposals));
            }

            $ex = new InvalidConfigurationException($msg);
            $ex->setPath($this->getPath());

            throw $ex;
        }

        return $normalized;
    }

    /**
     * Remaps multiple singular values to a single plural value.
     */
    protected function remapXml(array $value): array
    {
        foreach ($this->xmlRemappings as [$singular, $plural]) {
            if (!isset($value[$singular])) {
                continue;
            }

            $value[$plural] = Processor::normalizeConfig($value, $singular, $plural);
            unset($value[$singular]);
        }

        return $value;
    }

    /**
     * @throws InvalidConfigurationException
     * @throws \RuntimeException
     */
    protected function mergeValues(mixed $leftSide, mixed $rightSide): mixed
    {
        if (false === $rightSide) {
            // if this is still false after the last config has been merged the
            // finalization pass will take care of removing this key entirely
            return false;
        }

        if (false === $leftSide || !$this->performDeepMerging) {
            return $rightSide;
        }

        foreach ($rightSide as $k => $v) {
            // no conflict
            if (!\array_key_exists($k, $leftSide)) {
                if (!$this->allowNewKeys) {
                    $ex = new InvalidConfigurationException(\sprintf('You are not allowed to define new elements for path "%s". Please define all elements for this path in one config file. If you are trying to overwrite an element, make sure you redefine it with the same name.', $this->getPath()));
                    $ex->setPath($this->getPath());

                    throw $ex;
                }

                $leftSide[$k] = $v;
                continue;
            }

            if (!isset($this->children[$k])) {
                if (!$this->ignoreExtraKeys || $this->removeExtraKeys) {
                    throw new \RuntimeException('merge() expects a normalized config array.');
                }

                $leftSide[$k] = $v;
                continue;
            }

            $leftSide[$k] = $this->children[$k]->merge($leftSide[$k], $v);
        }

        return $leftSide;
    }

    protected function allowPlaceholders(): bool
    {
        return false;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Definition;

use Symfony\Component\Config\Definition\Exception\Exception;
use Symfony\Component\Config\Definition\Exception\ForbiddenOverwriteException;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\Config\Definition\Exception\InvalidTypeException;
use Symfony\Component\Config\Definition\Exception\UnsetKeyException;

/**
 * The base node class.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
abstract class BaseNode implements NodeInterface
{
    public const DEFAULT_PATH_SEPARATOR = '.';

    private static array $placeholderUniquePrefixes = [];
    private static array $placeholders = [];

    protected $name;
    protected $parent;
    protected $normalizationClosures = [];
    protected $normalizedTypes = [];
    protected $finalValidationClosures = [];
    protected $allowOverwrite = true;
    protected $required = false;
    protected $deprecation = [];
    protected $equivalentValues = [];
    protected $attributes = [];
    protected $pathSeparator;

    private mixed $handlingPlaceholder = null;

    /**
     * @throws \InvalidArgumentException if the name contains a period
     */
    public function __construct(?string $name, ?NodeInterface $parent = null, string $pathSeparator = self::DEFAULT_PATH_SEPARATOR)
    {
        if (str_contains($name = (string) $name, $pathSeparator)) {
            throw new \InvalidArgumentException('The name must not contain ".'.$pathSeparator.'".');
        }

        $this->name = $name;
        $this->parent = $parent;
        $this->pathSeparator = $pathSeparator;
    }

    /**
     * Register possible (dummy) values for a dynamic placeholder value.
     *
     * Matching configuration values will be processed with a provided value, one by one. After a provided value is
     * successfully processed the configuration value is returned as is, thus preserving the placeholder.
     *
     * @internal
     */
    public static function setPlaceholder(string $placeholder, array $values): void
    {
        if (!$values) {
            throw new \InvalidArgumentException('At least one value must be provided.');
        }

        self::$placeholders[$placeholder] = $values;
    }

    /**
     * Adds a common prefix for dynamic placeholder values.
     *
     * Matching configuration values will be skipped from being processed and are returned as is, thus preserving the
     * placeholder. An exact match provided by {@see setPlaceholder()} might take precedence.
     *
     * @internal
     */
    public static function setPlaceholderUniquePrefix(string $prefix): void
    {
        self::$placeholderUniquePrefixes[] = $prefix;
    }

    /**
     * Resets all current placeholders available.
     *
     * @internal
     */
    public static function resetPlaceholders(): void
    {
        self::$placeholderUniquePrefixes = [];
        self::$placeholders = [];
    }

    /**
     * @return void
     */
    public function setAttribute(string $key, mixed $value)
    {
        $this->attributes[$key] = $value;
    }

    public function getAttribute(string $key, mixed $default = null): mixed
    {
        return $this->attributes[$key] ?? $default;
    }

    public function hasAttribute(string $key): bool
    {
        return isset($this->attributes[$key]);
    }

    public function getAttributes(): array
    {
        return $this->attributes;
    }

    /**
     * @return void
     */
    public function setAttributes(array $attributes)
    {
        $this->attributes = $attributes;
    }

    /**
     * @return void
     */
    public function removeAttribute(string $key)
    {
        unset($this->attributes[$key]);
    }

    /**
     * Sets an info message.
     *
     * @return void
     */
    public function setInfo(string $info)
    {
        $this->setAttribute('info', $info);
    }

    /**
     * Returns info message.
     */
    public function getInfo(): ?string
    {
        return $this->getAttribute('info');
    }

    /**
     * Sets the example configuration for this node.
     *
     * @return void
     */
    public function setExample(string|array $example)
    {
        $this->setAttribute('example', $example);
    }

    /**
     * Retrieves the example configuration for this node.
     */
    public function getExample(): string|array|null
    {
        return $this->getAttribute('example');
    }

    /**
     * Adds an equivalent value.
     *
     * @return void
     */
    public function addEquivalentValue(mixed $originalValue, mixed $equivalentValue)
    {
        $this->equivalentValues[] = [$originalValue, $equivalentValue];
    }

    /**
     * Set this node as required.
     *
     * @return void
     */
    public function setRequired(bool $boolean)
    {
        $this->required = $boolean;
    }

    /**
     * Sets this node as deprecated.
     *
     * You can use %node% and %path% placeholders in your message to display,
     * respectively, the node name and its complete path.
     *
     * @param string $package The name of the composer package that is triggering the deprecation
     * @param string $version The version of the package that introduced the deprecation
     * @param string $message the deprecation message to use
     *
     * @return void
     */
    public function setDeprecated(string $package, string $version, string $message = 'The child node "%node%" at path "%path%" is deprecated.')
    {
        $this->deprecation = [
            'package' => $package,
            'version' => $version,
            'message' => $message,
        ];
    }

    /**
     * Sets if this node can be overridden.
     *
     * @return void
     */
    public function setAllowOverwrite(bool $allow)
    {
        $this->allowOverwrite = $allow;
    }

    /**
     * Sets the closures used for normalization.
     *
     * @param \Closure[] $closures An array of Closures used for normalization
     *
     * @return void
     */
    public function setNormalizationClosures(array $closures)
    {
        $this->normalizationClosures = $closures;
    }

    /**
     * Sets the list of types supported by normalization.
     *
     * see ExprBuilder::TYPE_* constants.
     *
     * @return void
     */
    public function setNormalizedTypes(array $types)
    {
        $this->normalizedTypes = $types;
    }

    /**
     * Gets the list of types supported by normalization.
     *
     * see ExprBuilder::TYPE_* constants.
     */
    public function getNormalizedTypes(): array
    {
        return $this->normalizedTypes;
    }

    /**
     * Sets the closures used for final validation.
     *
     * @param \Closure[] $closures An array of Closures used for final validation
     *
     * @return void
     */
    public function setFinalValidationClosures(array $closures)
    {
        $this->finalValidationClosures = $closures;
    }

    public function isRequired(): bool
    {
        return $this->required;
    }

    /**
     * Checks if this node is deprecated.
     */
    public function isDeprecated(): bool
    {
        return (bool) $this->deprecation;
    }

    /**
     * @param string $node The configuration node name
     * @param string $path The path of the node
     */
    public function getDeprecation(string $node, string $path): array
    {
        return [
            'package' => $this->deprecation['package'],
            'version' => $this->deprecation['version'],
            'message' => strtr($this->deprecation['message'], ['%node%' => $node, '%path%' => $path]),
        ];
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function getPath(): string
    {
        if (null !== $this->parent) {
            return $this->parent->getPath().$this->pathSeparator.$this->name;
        }

        return $this->name;
    }

    final public function merge(mixed $leftSide, mixed $rightSide): mixed
    {
        if (!$this->allowOverwrite) {
            throw new ForbiddenOverwriteException(\sprintf('Configuration path "%s" cannot be overwritten. You have to define all options for this path, and any of its sub-paths in one configuration section.', $this->getPath()));
        }

        if ($leftSide !== $leftPlaceholders = self::resolvePlaceholderValue($leftSide)) {
            foreach ($leftPlaceholders as $leftPlaceholder) {
                $this->handlingPlaceholder = $leftSide;
                try {
                    $this->merge($leftPlaceholder, $rightSide);
                } finally {
                    $this->handlingPlaceholder = null;
                }
            }

            return $rightSide;
        }

        if ($rightSide !== $rightPlaceholders = self::resolvePlaceholderValue($rightSide)) {
            foreach ($rightPlaceholders as $rightPlaceholder) {
                $this->handlingPlaceholder = $rightSide;
                try {
                    $this->merge($leftSide, $rightPlaceholder);
                } finally {
                    $this->handlingPlaceholder = null;
                }
            }

            return $rightSide;
        }

        $this->doValidateType($leftSide);
        $this->doValidateType($rightSide);

        return $this->mergeValues($leftSide, $rightSide);
    }

    final public function normalize(mixed $value): mixed
    {
        $value = $this->preNormalize($value);

        // run custom normalization closures
        foreach ($this->normalizationClosures as $closure) {
            $value = $closure($value);
        }

        // resolve placeholder value
        if ($value !== $placeholders = self::resolvePlaceholderValue($value)) {
            foreach ($placeholders as $placeholder) {
                $this->handlingPlaceholder = $value;
                try {
                    $this->normalize($placeholder);
                } finally {
                    $this->handlingPlaceholder = null;
                }
            }

            return $value;
        }

        // replace value with their equivalent
        foreach ($this->equivalentValues as $data) {
            if ($data[0] === $value) {
                $value = $data[1];
            }
        }

        // validate type
        $this->doValidateType($value);

        // normalize value
        return $this->normalizeValue($value);
    }

    /**
     * Normalizes the value before any other normalization is applied.
     */
    protected function preNormalize(mixed $value): mixed
    {
        return $value;
    }

    /**
     * Returns parent node for this node.
     */
    public function getParent(): ?NodeInterface
    {
        return $this->parent;
    }

    final public function finalize(mixed $value): mixed
    {
        if ($value !== $placeholders = self::resolvePlaceholderValue($value)) {
            foreach ($placeholders as $placeholder) {
                $this->handlingPlaceholder = $value;
                try {
                    $this->finalize($placeholder);
                } finally {
                    $this->handlingPlaceholder = null;
                }
            }

            return $value;
        }

        $this->doValidateType($value);

        $value = $this->finalizeValue($value);

        // Perform validation on the final value if a closure has been set.
        // The closure is also allowed to return another value.
        foreach ($this->finalValidationClosures as $closure) {
            try {
                $value = $closure($value);
            } catch (Exception $e) {
                if ($e instanceof UnsetKeyException && null !== $this->handlingPlaceholder) {
                    continue;
                }

                throw $e;
            } catch (\Exception $e) {
                throw new InvalidConfigurationException(\sprintf('Invalid configuration for path "%s": ', $this->getPath()).$e->getMessage(), $e->getCode(), $e);
            }
        }

        return $value;
    }

    /**
     * Validates the type of a Node.
     *
     * @return void
     *
     * @throws InvalidTypeException when the value is invalid
     */
    abstract protected function validateType(mixed $value);

    /**
     * Normalizes the value.
     */
    abstract protected function normalizeValue(mixed $value): mixed;

    /**
     * Merges two values together.
     */
    abstract protected function mergeValues(mixed $leftSide, mixed $rightSide): mixed;

    /**
     * Finalizes a value.
     */
    abstract protected function finalizeValue(mixed $value): mixed;

    /**
     * Tests if placeholder values are allowed for this node.
     */
    protected function allowPlaceholders(): bool
    {
        return true;
    }

    /**
     * Tests if a placeholder is being handled currently.
     */
    protected function isHandlingPlaceholder(): bool
    {
        return null !== $this->handlingPlaceholder;
    }

    /**
     * Gets allowed dynamic types for this node.
     */
    protected function getValidPlaceholderTypes(): array
    {
        return [];
    }

    private static function resolvePlaceholderValue(mixed $value): mixed
    {
        if (\is_string($value)) {
            if (isset(self::$placeholders[$value])) {
                return self::$placeholders[$value];
            }

            foreach (self::$placeholderUniquePrefixes as $placeholderUniquePrefix) {
                if (str_starts_with($value, $placeholderUniquePrefix)) {
                    return [];
                }
            }
        }

        return $value;
    }

    private function doValidateType(mixed $value): void
    {
        if (null !== $this->handlingPlaceholder && !$this->allowPlaceholders()) {
            $e = new InvalidTypeException(\sprintf('A dynamic value is not compatible with a "%s" node type at path "%s".', static::class, $this->getPath()));
            $e->setPath($this->getPath());

            throw $e;
        }

        if (null === $this->handlingPlaceholder || null === $value) {
            $this->validateType($value);

            return;
        }

        $knownTypes = array_keys(self::$placeholders[$this->handlingPlaceholder]);
        $validTypes = $this->getValidPlaceholderTypes();

        if ($validTypes && array_diff($knownTypes, $validTypes)) {
            $e = new InvalidTypeException(\sprintf(
                'Invalid type for path "%s". Expected %s, but got %s.',
                $this->getPath(),
                1 === \count($validTypes) ? '"'.reset($validTypes).'"' : 'one of "'.implode('", "', $validTypes).'"',
                1 === \count($knownTypes) ? '"'.reset($knownTypes).'"' : 'one of "'.implode('", "', $knownTypes).'"'
            ));
            if ($hint = $this->getInfo()) {
                $e->addHint($hint);
            }
            $e->setPath($this->getPath());

            throw $e;
        }

        $this->validateType($value);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Definition;

use Symfony\Component\Config\Definition\Builder\TreeBuilder;

/**
 * Configuration interface.
 *
 * @author Victor Berchet <victor@suumit.com>
 */
interface ConfigurationInterface
{
    /**
     * Generates the configuration tree builder.
     *
     * @return TreeBuilder
     */
    public function getConfigTreeBuilder();
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Definition;

use Symfony\Component\Config\Definition\Exception\InvalidTypeException;

/**
 * This node represents a float value in the config tree.
 *
 * @author Jeanmonod David <david.jeanmonod@gmail.com>
 */
class FloatNode extends NumericNode
{
    /**
     * @return void
     */
    protected function validateType(mixed $value)
    {
        // Integers are also accepted, we just cast them
        if (\is_int($value)) {
            $value = (float) $value;
        }

        if (!\is_float($value)) {
            $ex = new InvalidTypeException(\sprintf('Invalid type for path "%s". Expected "float", but got "%s".', $this->getPath(), get_debug_type($value)));
            if ($hint = $this->getInfo()) {
                $ex->addHint($hint);
            }
            $ex->setPath($this->getPath());

            throw $ex;
        }
    }

    protected function getValidPlaceholderTypes(): array
    {
        return ['float'];
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Definition;

use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator;
use Symfony\Component\Config\Definition\Loader\DefinitionFileLoader;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;

/**
 * @author Yonel Ceruto <yonelceruto@gmail.com>
 *
 * @final
 */
class Configuration implements ConfigurationInterface
{
    public function __construct(
        private ConfigurableInterface $subject,
        private ?ContainerBuilder $container,
        private string $alias,
    ) {
    }

    public function getConfigTreeBuilder(): TreeBuilder
    {
        $treeBuilder = new TreeBuilder($this->alias);
        $file = (new \ReflectionObject($this->subject))->getFileName();
        $loader = new DefinitionFileLoader($treeBuilder, new FileLocator(\dirname($file)), $this->container);
        $configurator = new DefinitionConfigurator($treeBuilder, $loader, $file, $file);

        $this->subject->configure($configurator);

        return $treeBuilder;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Definition;

use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;

/**
 * This node represents a value of variable type in the config tree.
 *
 * This node is intended for values of arbitrary type.
 * Any PHP type is accepted as a value.
 *
 * @author Jeremy Mikola <jmikola@gmail.com>
 */
class VariableNode extends BaseNode implements PrototypeNodeInterface
{
    protected $defaultValueSet = false;
    protected $defaultValue;
    protected $allowEmptyValue = true;

    /**
     * @return void
     */
    public function setDefaultValue(mixed $value)
    {
        $this->defaultValueSet = true;
        $this->defaultValue = $value;
    }

    public function hasDefaultValue(): bool
    {
        return $this->defaultValueSet;
    }

    public function getDefaultValue(): mixed
    {
        $v = $this->defaultValue;

        return $v instanceof \Closure ? $v() : $v;
    }

    /**
     * Sets if this node is allowed to have an empty value.
     *
     * @param bool $boolean True if this entity will accept empty values
     *
     * @return void
     */
    public function setAllowEmptyValue(bool $boolean)
    {
        $this->allowEmptyValue = $boolean;
    }

    /**
     * @return void
     */
    public function setName(string $name)
    {
        $this->name = $name;
    }

    /**
     * @return void
     */
    protected function validateType(mixed $value)
    {
    }

    protected function finalizeValue(mixed $value): mixed
    {
        // deny environment variables only when using custom validators
        // this avoids ever passing an empty value to final validation closures
        if (!$this->allowEmptyValue && $this->isHandlingPlaceholder() && $this->finalValidationClosures) {
            $e = new InvalidConfigurationException(\sprintf('The path "%s" cannot contain an environment variable when empty values are not allowed by definition and are validated.', $this->getPath()));
            if ($hint = $this->getInfo()) {
                $e->addHint($hint);
            }
            $e->setPath($this->getPath());

            throw $e;
        }

        if (!$this->allowEmptyValue && $this->isValueEmpty($value)) {
            $ex = new InvalidConfigurationException(\sprintf('The path "%s" cannot contain an empty value, but got %s.', $this->getPath(), json_encode($value)));
            if ($hint = $this->getInfo()) {
                $ex->addHint($hint);
            }
            $ex->setPath($this->getPath());

            throw $ex;
        }

        return $value;
    }

    protected function normalizeValue(mixed $value): mixed
    {
        return $value;
    }

    protected function mergeValues(mixed $leftSide, mixed $rightSide): mixed
    {
        return $rightSide;
    }

    /**
     * Evaluates if the given value is to be treated as empty.
     *
     * By default, PHP's empty() function is used to test for emptiness. This
     * method may be overridden by subtypes to better match their understanding
     * of empty data.
     *
     * @see finalizeValue()
     */
    protected function isValueEmpty(mixed $value): bool
    {
        return empty($value);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Definition;

use Symfony\Component\Config\Definition\Exception\InvalidTypeException;

/**
 * This node represents an integer value in the config tree.
 *
 * @author Jeanmonod David <david.jeanmonod@gmail.com>
 */
class IntegerNode extends NumericNode
{
    /**
     * @return void
     */
    protected function validateType(mixed $value)
    {
        if (!\is_int($value)) {
            $ex = new InvalidTypeException(\sprintf('Invalid type for path "%s". Expected "int", but got "%s".', $this->getPath(), get_debug_type($value)));
            if ($hint = $this->getInfo()) {
                $ex->addHint($hint);
            }
            $ex->setPath($this->getPath());

            throw $ex;
        }
    }

    protected function getValidPlaceholderTypes(): array
    {
        return ['int'];
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Definition;

use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;

/**
 * This node represents a numeric value in the config tree.
 *
 * @author David Jeanmonod <david.jeanmonod@gmail.com>
 */
class NumericNode extends ScalarNode
{
    protected $min;
    protected $max;

    public function __construct(?string $name, ?NodeInterface $parent = null, int|float|null $min = null, int|float|null $max = null, string $pathSeparator = BaseNode::DEFAULT_PATH_SEPARATOR)
    {
        parent::__construct($name, $parent, $pathSeparator);
        $this->min = $min;
        $this->max = $max;
    }

    protected function finalizeValue(mixed $value): mixed
    {
        $value = parent::finalizeValue($value);

        if ($this->isHandlingPlaceholder()) {
            return $value;
        }

        $errorMsg = null;
        if (isset($this->min) && $value < $this->min) {
            $errorMsg = \sprintf('The value %s is too small for path "%s". Should be greater than or equal to %s', $value, $this->getPath(), $this->min);
        }
        if (isset($this->max) && $value > $this->max) {
            $errorMsg = \sprintf('The value %s is too big for path "%s". Should be less than or equal to %s', $value, $this->getPath(), $this->max);
        }
        if (isset($errorMsg)) {
            $ex = new InvalidConfigurationException($errorMsg);
            $ex->setPath($this->getPath());
            throw $ex;
        }

        return $value;
    }

    protected function isValueEmpty(mixed $value): bool
    {
        // a numeric value cannot be empty
        return false;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Definition\Configurator;

use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\Loader\DefinitionFileLoader;

/**
 * @author Yonel Ceruto <yonelceruto@gmail.com>
 */
class DefinitionConfigurator
{
    public function __construct(
        private TreeBuilder $treeBuilder,
        private DefinitionFileLoader $loader,
        private string $path,
        private string $file,
    ) {
    }

    public function import(string $resource, ?string $type = null, bool $ignoreErrors = false): void
    {
        $this->loader->setCurrentDir(\dirname($this->path));
        $this->loader->import($resource, $type, $ignoreErrors, $this->file);
    }

    public function rootNode(): NodeDefinition|ArrayNodeDefinition
    {
        return $this->treeBuilder->getRootNode();
    }

    public function setPathSeparator(string $separator): void
    {
        $this->treeBuilder->setPathSeparator($separator);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Definition;

use Symfony\Component\Config\Definition\Exception\ForbiddenOverwriteException;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\Config\Definition\Exception\InvalidTypeException;

/**
 * Common Interface among all nodes.
 *
 * In most cases, it is better to inherit from BaseNode instead of implementing
 * this interface yourself.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
interface NodeInterface
{
    /**
     * Returns the name of the node.
     */
    public function getName(): string;

    /**
     * Returns the path of the node.
     */
    public function getPath(): string;

    /**
     * Returns true when the node is required.
     */
    public function isRequired(): bool;

    /**
     * Returns true when the node has a default value.
     */
    public function hasDefaultValue(): bool;

    /**
     * Returns the default value of the node.
     *
     * @throws \RuntimeException if the node has no default value
     */
    public function getDefaultValue(): mixed;

    /**
     * Normalizes a value.
     *
     * @throws InvalidTypeException if the value type is invalid
     */
    public function normalize(mixed $value): mixed;

    /**
     * Merges two values together.
     *
     * @throws ForbiddenOverwriteException if the configuration path cannot be overwritten
     * @throws InvalidTypeException        if the value type is invalid
     */
    public function merge(mixed $leftSide, mixed $rightSide): mixed;

    /**
     * Finalizes a value.
     *
     * @throws InvalidTypeException          if the value type is invalid
     * @throws InvalidConfigurationException if the value is invalid configuration
     */
    public function finalize(mixed $value): mixed;
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Definition;

use Symfony\Component\Config\Definition\Exception\InvalidTypeException;

/**
 * This node represents a Boolean value in the config tree.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class BooleanNode extends ScalarNode
{
    /**
     * @return void
     */
    protected function validateType(mixed $value)
    {
        if (!\is_bool($value)) {
            $ex = new InvalidTypeException(\sprintf('Invalid type for path "%s". Expected "bool", but got "%s".', $this->getPath(), get_debug_type($value)));
            if ($hint = $this->getInfo()) {
                $ex->addHint($hint);
            }
            $ex->setPath($this->getPath());

            throw $ex;
        }
    }

    protected function isValueEmpty(mixed $value): bool
    {
        // a boolean value cannot be empty
        return false;
    }

    protected function getValidPlaceholderTypes(): array
    {
        return ['bool'];
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config;

/**
 * Basic implementation of ConfigCacheFactoryInterface that
 * creates an instance of the default ConfigCache.
 *
 * This factory and/or cache <em>do not</em> support cache validation
 * by means of ResourceChecker instances (that is, service-based).
 *
 * @author Matthias Pigulla <mp@webfactory.de>
 */
class ConfigCacheFactory implements ConfigCacheFactoryInterface
{
    private bool $debug;

    /**
     * @param bool $debug The debug flag to pass to ConfigCache
     */
    public function __construct(bool $debug)
    {
        $this->debug = $debug;
    }

    public function cache(string $file, callable $callback): ConfigCacheInterface
    {
        $cache = new ConfigCache($file, $this->debug);
        if (!$cache->isFresh()) {
            $callback($cache);
        }

        return $cache;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Builder;

/**
 * A ConfigBuilder provides helper methods to build a large complex array.
 *
 * @author Tobias Nyholm <tobias.nyholm@gmail.com>
 */
interface ConfigBuilderInterface
{
    /**
     * Gets all configuration represented as an array.
     */
    public function toArray(): array;

    /**
     * Gets the alias for the extension which config we are building.
     */
    public function getExtensionAlias(): string;
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Builder;

/**
 * Represents a property when building classes.
 *
 * @internal
 *
 * @author Tobias Nyholm <tobias.nyholm@gmail.com>
 */
class Property
{
    private string $name;
    private string $originalName;
    private bool $array = false;
    private bool $scalarsAllowed = false;
    private ?string $type = null;
    private ?string $content = null;

    public function __construct(string $originalName, string $name)
    {
        $this->name = $name;
        $this->originalName = $originalName;
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function getOriginalName(): string
    {
        return $this->originalName;
    }

    public function setType(string $type): void
    {
        $this->array = false;
        $this->type = $type;

        if (str_ends_with($type, '|scalar')) {
            $this->scalarsAllowed = true;
            $this->type = $type = substr($type, 0, -7);
        }

        if (str_ends_with($type, '[]')) {
            $this->array = true;
            $this->type = substr($type, 0, -2);
        }
    }

    public function getType(): ?string
    {
        return $this->type;
    }

    public function getContent(): ?string
    {
        return $this->content;
    }

    public function setContent(string $content): void
    {
        $this->content = $content;
    }

    public function isArray(): bool
    {
        return $this->array;
    }

    public function areScalarsAllowed(): bool
    {
        return $this->scalarsAllowed;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Builder;

use Symfony\Component\Config\Definition\ArrayNode;
use Symfony\Component\Config\Definition\BaseNode;
use Symfony\Component\Config\Definition\BooleanNode;
use Symfony\Component\Config\Definition\Builder\ExprBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Config\Definition\EnumNode;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\Config\Definition\FloatNode;
use Symfony\Component\Config\Definition\IntegerNode;
use Symfony\Component\Config\Definition\NodeInterface;
use Symfony\Component\Config\Definition\PrototypedArrayNode;
use Symfony\Component\Config\Definition\ScalarNode;
use Symfony\Component\Config\Definition\VariableNode;
use Symfony\Component\Config\Loader\ParamConfigurator;

/**
 * Generate ConfigBuilders to help create valid config.
 *
 * @author Tobias Nyholm <tobias.nyholm@gmail.com>
 */
class ConfigBuilderGenerator implements ConfigBuilderGeneratorInterface
{
    /**
     * @var ClassBuilder[]
     */
    private array $classes = [];
    private string $outputDir;

    public function __construct(string $outputDir)
    {
        $this->outputDir = $outputDir;
    }

    /**
     * @return \Closure that will return the root config class
     */
    public function build(ConfigurationInterface $configuration): \Closure
    {
        $this->classes = [];

        $rootNode = $configuration->getConfigTreeBuilder()->buildTree();
        $rootClass = new ClassBuilder('Symfony\\Config', $rootNode->getName());

        $path = $this->getFullPath($rootClass);
        if (!is_file($path)) {
            // Generate the class if the file not exists
            $this->classes[] = $rootClass;
            $this->buildNode($rootNode, $rootClass, $this->getSubNamespace($rootClass));
            $rootClass->addImplements(ConfigBuilderInterface::class);
            $rootClass->addMethod('getExtensionAlias', '
public function NAME(): string
{
    return \'ALIAS\';
}', ['ALIAS' => $rootNode->getPath()]);

            $this->writeClasses();
        }

        return function () use ($path, $rootClass) {
            require_once $path;
            $className = $rootClass->getFqcn();

            return new $className();
        };
    }

    private function getFullPath(ClassBuilder $class): string
    {
        $directory = $this->outputDir.\DIRECTORY_SEPARATOR.$class->getDirectory();
        if (!is_dir($directory)) {
            @mkdir($directory, 0o777, true);
        }

        return $directory.\DIRECTORY_SEPARATOR.$class->getFilename();
    }

    private function writeClasses(): void
    {
        foreach ($this->classes as $class) {
            $this->buildConstructor($class);
            $this->buildToArray($class);
            if ($class->getProperties()) {
                $class->addProperty('_usedProperties', null, '[]');
            }
            $this->buildSetExtraKey($class);

            file_put_contents($this->getFullPath($class), $class->build());
        }

        $this->classes = [];
    }

    private function buildNode(NodeInterface $node, ClassBuilder $class, string $namespace): void
    {
        if (!$node instanceof ArrayNode) {
            throw new \LogicException('The node was expected to be an ArrayNode. This Configuration includes an edge case not supported yet.');
        }

        foreach ($node->getChildren() as $child) {
            match (true) {
                $child instanceof ScalarNode => $this->handleScalarNode($child, $class),
                $child instanceof PrototypedArrayNode => $this->handlePrototypedArrayNode($child, $class, $namespace),
                $child instanceof VariableNode => $this->handleVariableNode($child, $class),
                $child instanceof ArrayNode => $this->handleArrayNode($child, $class, $namespace),
                default => throw new \RuntimeException(\sprintf('Unknown node "%s".', $child::class)),
            };
        }
    }

    private function handleArrayNode(ArrayNode $node, ClassBuilder $class, string $namespace): void
    {
        $childClass = new ClassBuilder($namespace, $node->getName());
        $childClass->setAllowExtraKeys($node->shouldIgnoreExtraKeys());
        $class->addRequire($childClass);
        $this->classes[] = $childClass;

        $nodeTypes = $this->getParameterTypes($node);
        $paramType = $this->getParamType($nodeTypes);

        $hasNormalizationClosures = $this->hasNormalizationClosures($node);
        $comment = $this->getComment($node);
        if ($hasNormalizationClosures && 'array' !== $paramType) {
            $comment = \sprintf(" * @template TValue of %s\n * @param TValue \$value\n%s", $paramType, $comment);
            $comment .= \sprintf(' * @return %s|$this'."\n", $childClass->getFqcn());
            $comment .= \sprintf(' * @psalm-return (TValue is array ? %s : static)'."\n ", $childClass->getFqcn());
        }
        if ('' !== $comment) {
            $comment = "/**\n$comment*/\n";
        }

        $property = $class->addProperty(
            $node->getName(),
            $this->getType($childClass->getFqcn(), $hasNormalizationClosures)
        );
        $body = $hasNormalizationClosures && 'array' !== $paramType ? '
COMMENTpublic function NAME(PARAM_TYPE $value = []): CLASS|static
{
    if (!\is_array($value)) {
        $this->_usedProperties[\'PROPERTY\'] = true;
        $this->PROPERTY = $value;

        return $this;
    }

    if (!$this->PROPERTY instanceof CLASS) {
        $this->_usedProperties[\'PROPERTY\'] = true;
        $this->PROPERTY = new CLASS($value);
    } elseif (0 < \func_num_args()) {
        throw new InvalidConfigurationException(\'The node created by "NAME()" has already been initialized. You cannot pass values the second time you call NAME().\');
    }

    return $this->PROPERTY;
}' : '
COMMENTpublic function NAME(array $value = []): CLASS
{
    if (null === $this->PROPERTY) {
        $this->_usedProperties[\'PROPERTY\'] = true;
        $this->PROPERTY = new CLASS($value);
    } elseif (0 < \func_num_args()) {
        throw new InvalidConfigurationException(\'The node created by "NAME()" has already been initialized. You cannot pass values the second time you call NAME().\');
    }

    return $this->PROPERTY;
}';
        $class->addUse(InvalidConfigurationException::class);
        $class->addMethod($node->getName(), $body, [
            'COMMENT' => $comment,
            'PROPERTY' => $property->getName(),
            'CLASS' => $childClass->getFqcn(),
            'PARAM_TYPE' => $paramType,
        ]);

        $this->buildNode($node, $childClass, $this->getSubNamespace($childClass));
    }

    private function handleVariableNode(VariableNode $node, ClassBuilder $class): void
    {
        $comment = $this->getComment($node);
        $property = $class->addProperty($node->getName());
        $class->addUse(ParamConfigurator::class);

        $body = '
/**
COMMENT *
 * @return $this
 */
public function NAME(mixed $valueDEFAULT): static
{
    $this->_usedProperties[\'PROPERTY\'] = true;
    $this->PROPERTY = $value;

    return $this;
}';
        $class->addMethod($node->getName(), $body, [
            'PROPERTY' => $property->getName(),
            'COMMENT' => $comment,
            'DEFAULT' => $node->hasDefaultValue() ? ' = '.var_export($node->getDefaultValue(), true) : '',
        ]);
    }

    private function handlePrototypedArrayNode(PrototypedArrayNode $node, ClassBuilder $class, string $namespace): void
    {
        $name = $this->getSingularName($node);
        $prototype = $node->getPrototype();
        $methodName = $name;
        $hasNormalizationClosures = $this->hasNormalizationClosures($node) || $this->hasNormalizationClosures($prototype);

        $nodeParameterTypes = $this->getParameterTypes($node);
        $prototypeParameterTypes = $this->getParameterTypes($prototype);
        $noKey = null === $key = $node->getKeyAttribute();
        if (!$prototype instanceof ArrayNode || ($prototype instanceof PrototypedArrayNode && $prototype->getPrototype() instanceof ScalarNode)) {
            $class->addUse(ParamConfigurator::class);
            $property = $class->addProperty($node->getName());
            if ($noKey) {
                // This is an array of values; don't use singular name
                $nodeTypesWithoutArray = array_filter($nodeParameterTypes, static fn ($type) => 'array' !== $type);
                $body = '
/**
 * @param ParamConfigurator|list<ParamConfigurator|PROTOTYPE_TYPE>EXTRA_TYPE $value
 *
 * @return $this
 */
public function NAME(PARAM_TYPE $value): static
{
    $this->_usedProperties[\'PROPERTY\'] = true;
    $this->PROPERTY = $value;

    return $this;
}';

                $class->addMethod($node->getName(), $body, [
                    'PROPERTY' => $property->getName(),
                    'PROTOTYPE_TYPE' => implode('|', $prototypeParameterTypes),
                    'EXTRA_TYPE' => $nodeTypesWithoutArray ? '|'.implode('|', $nodeTypesWithoutArray) : '',
                    'PARAM_TYPE' => $this->getParamType($nodeParameterTypes, true),
                ]);
            } else {
                $body = '
/**
 * @return $this
 */
public function NAME(string $VAR, TYPE $VALUE): static
{
    $this->_usedProperties[\'PROPERTY\'] = true;
    $this->PROPERTY[$VAR] = $VALUE;

    return $this;
}';

                $class->addMethod($methodName, $body, [
                    'PROPERTY' => $property->getName(),
                    'TYPE' => $this->getParamType($prototypeParameterTypes, true),
                    'VAR' => '' === $key ? 'key' : $key,
                    'VALUE' => 'value' === $key ? 'data' : 'value',
                ]);
            }

            return;
        }

        $childClass = new ClassBuilder($namespace, $name);
        if ($prototype instanceof ArrayNode) {
            $childClass->setAllowExtraKeys($prototype->shouldIgnoreExtraKeys());
        }
        $class->addRequire($childClass);
        $this->classes[] = $childClass;

        $property = $class->addProperty(
            $node->getName(),
            $this->getType($childClass->getFqcn().'[]', $hasNormalizationClosures)
        );

        $paramType = $this->getParamType($noKey ? $nodeParameterTypes : $prototypeParameterTypes);

        $comment = $this->getComment($node);
        if ($hasNormalizationClosures && 'array' !== $paramType) {
            $comment = \sprintf(" * @template TValue of %s\n * @param TValue \$value\n%s", $paramType, $comment);
            $comment .= \sprintf(' * @return %s|$this'."\n", $childClass->getFqcn());
            $comment .= \sprintf(' * @psalm-return (TValue is array ? %s : static)'."\n ", $childClass->getFqcn());
        }
        if ('' !== $comment) {
            $comment = "/**\n$comment*/\n";
        }

        if ($noKey) {
            $body = $hasNormalizationClosures && 'array' !== $paramType ? '
COMMENTpublic function NAME(PARAM_TYPE $value = []): CLASS|static
{
    $this->_usedProperties[\'PROPERTY\'] = true;
    if (!\is_array($value)) {
        $this->PROPERTY[] = $value;

        return $this;
    }

    return $this->PROPERTY[] = new CLASS($value);
}' : '
COMMENTpublic function NAME(array $value = []): CLASS
{
    $this->_usedProperties[\'PROPERTY\'] = true;

    return $this->PROPERTY[] = new CLASS($value);
}';
            $class->addMethod($methodName, $body, [
                'COMMENT' => $comment,
                'PROPERTY' => $property->getName(),
                'CLASS' => $childClass->getFqcn(),
                'PARAM_TYPE' => $paramType,
            ]);
        } else {
            $body = $hasNormalizationClosures && 'array' !== $paramType ? '
COMMENTpublic function NAME(string $VAR, PARAM_TYPE $VALUE = []): CLASS|static
{
    if (!\is_array($VALUE)) {
        $this->_usedProperties[\'PROPERTY\'] = true;
        $this->PROPERTY[$VAR] = $VALUE;

        return $this;
    }

    if (!isset($this->PROPERTY[$VAR]) || !$this->PROPERTY[$VAR] instanceof CLASS) {
        $this->_usedProperties[\'PROPERTY\'] = true;
        $this->PROPERTY[$VAR] = new CLASS($VALUE);
    } elseif (1 < \func_num_args()) {
        throw new InvalidConfigurationException(\'The node created by "NAME()" has already been initialized. You cannot pass values the second time you call NAME().\');
    }

    return $this->PROPERTY[$VAR];
}' : '
COMMENTpublic function NAME(string $VAR, array $VALUE = []): CLASS
{
    if (!isset($this->PROPERTY[$VAR])) {
        $this->_usedProperties[\'PROPERTY\'] = true;
        $this->PROPERTY[$VAR] = new CLASS($VALUE);
    } elseif (1 < \func_num_args()) {
        throw new InvalidConfigurationException(\'The node created by "NAME()" has already been initialized. You cannot pass values the second time you call NAME().\');
    }

    return $this->PROPERTY[$VAR];
}';
            $class->addUse(InvalidConfigurationException::class);
            $class->addMethod($methodName, str_replace('$value', '$VAR', $body), [
                'COMMENT' => $comment, 'PROPERTY' => $property->getName(),
                'CLASS' => $childClass->getFqcn(),
                'VAR' => '' === $key ? 'key' : $key,
                'VALUE' => 'value' === $key ? 'data' : 'value',
                'PARAM_TYPE' => $paramType,
            ]);
        }

        $this->buildNode($prototype, $childClass, $namespace.'\\'.$childClass->getName());
    }

    private function handleScalarNode(ScalarNode $node, ClassBuilder $class): void
    {
        $comment = $this->getComment($node);
        $property = $class->addProperty($node->getName());
        $class->addUse(ParamConfigurator::class);

        $body = '
/**
COMMENT * @return $this
 */
public function NAME($value): static
{
    $this->_usedProperties[\'PROPERTY\'] = true;
    $this->PROPERTY = $value;

    return $this;
}';

        $class->addMethod($node->getName(), $body, ['PROPERTY' => $property->getName(), 'COMMENT' => $comment]);
    }

    private function getParameterTypes(NodeInterface $node): array
    {
        $paramTypes = [];
        if ($node instanceof BaseNode) {
            $types = $node->getNormalizedTypes();
            if (\in_array(ExprBuilder::TYPE_ANY, $types, true)) {
                $paramTypes[] = 'mixed';
            }
            if (\in_array(ExprBuilder::TYPE_STRING, $types, true)) {
                $paramTypes[] = 'string';
            }
        }
        if ($node instanceof BooleanNode) {
            $paramTypes[] = 'bool';
        } elseif ($node instanceof IntegerNode) {
            $paramTypes[] = 'int';
        } elseif ($node instanceof FloatNode) {
            $paramTypes[] = 'float';
        } elseif ($node instanceof EnumNode) {
            $paramTypes[] = 'mixed';
        } elseif ($node instanceof ArrayNode) {
            $paramTypes[] = 'array';
        } elseif ($node instanceof VariableNode) {
            $paramTypes[] = 'mixed';
        }

        return array_unique($paramTypes);
    }

    private function getComment(BaseNode $node): string
    {
        $comment = '';
        if ('' !== $info = (string) $node->getInfo()) {
            $comment .= $info."\n";
        }

        if (!$node instanceof ArrayNode) {
            foreach ((array) ($node->getExample() ?? []) as $example) {
                $comment .= '@example '.$example."\n";
            }

            if ('' !== $default = $node->getDefaultValue()) {
                $comment .= '@default '.(null === $default ? 'null' : var_export($default, true))."\n";
            }

            if ($node instanceof EnumNode) {
                $comment .= \sprintf('@param ParamConfigurator|%s $value', implode('|', array_unique(array_map(static fn ($a) => !$a instanceof \UnitEnum ? var_export($a, true) : '\\'.ltrim(var_export($a, true), '\\'), $node->getValues()))))."\n";
            } else {
                $parameterTypes = $this->getParameterTypes($node);
                $comment .= '@param ParamConfigurator|'.implode('|', $parameterTypes).' $value'."\n";
            }
        } else {
            foreach ((array) ($node->getExample() ?? []) as $example) {
                $comment .= '@example '.json_encode($example)."\n";
            }

            if ($node->hasDefaultValue() && [] != $default = $node->getDefaultValue()) {
                $comment .= '@default '.json_encode($default)."\n";
            }
        }

        if ($node->isDeprecated()) {
            $comment .= '@deprecated '.$node->getDeprecation($node->getName(), $node->getParent()->getName())['message']."\n";
        }

        return $comment ? ' * '.str_replace(['*/', "\n"], ['*\/', "\n * "], rtrim($comment, "\n"))."\n" : '';
    }

    /**
     * Pick a good singular name.
     */
    private function getSingularName(PrototypedArrayNode $node): string
    {
        $name = $node->getName();
        if (!str_ends_with($name, 's')) {
            return $name;
        }

        $parent = $node->getParent();
        $mappings = $parent instanceof ArrayNode ? $parent->getXmlRemappings() : [];
        foreach ($mappings as $map) {
            if ($map[1] === $name) {
                $name = $map[0];
                break;
            }
        }

        return $name;
    }

    private function buildToArray(ClassBuilder $class): void
    {
        $body = '$output = [];';
        foreach ($class->getProperties() as $p) {
            $code = '$this->PROPERTY';
            if (null !== $p->getType()) {
                if ($p->isArray()) {
                    $code = $p->areScalarsAllowed()
                        ? 'array_map(fn ($v) => $v instanceof CLASS ? $v->toArray() : $v, $this->PROPERTY)'
                        : 'array_map(fn ($v) => $v->toArray(), $this->PROPERTY)'
                    ;
                } else {
                    $code = $p->areScalarsAllowed()
                        ? '$this->PROPERTY instanceof CLASS ? $this->PROPERTY->toArray() : $this->PROPERTY'
                        : '$this->PROPERTY->toArray()'
                    ;
                }
            }

            $body .= strtr('
    if (isset($this->_usedProperties[\'PROPERTY\'])) {
        $output[\'ORG_NAME\'] = '.$code.';
    }', ['PROPERTY' => $p->getName(), 'ORG_NAME' => $p->getOriginalName(), 'CLASS' => $p->getType()]);
        }

        $extraKeys = $class->shouldAllowExtraKeys() ? ' + $this->_extraKeys' : '';

        $class->addMethod('toArray', '
public function NAME(): array
{
    '.$body.'

    return $output'.$extraKeys.';
}');
    }

    private function buildConstructor(ClassBuilder $class): void
    {
        $body = '';
        foreach ($class->getProperties() as $p) {
            $code = '$value[\'ORG_NAME\']';
            if (null !== $p->getType()) {
                if ($p->isArray()) {
                    $code = $p->areScalarsAllowed()
                        ? 'array_map(fn ($v) => \is_array($v) ? new '.$p->getType().'($v) : $v, $value[\'ORG_NAME\'])'
                        : 'array_map(fn ($v) => new '.$p->getType().'($v), $value[\'ORG_NAME\'])'
                    ;
                } else {
                    $code = $p->areScalarsAllowed()
                        ? '\is_array($value[\'ORG_NAME\']) ? new '.$p->getType().'($value[\'ORG_NAME\']) : $value[\'ORG_NAME\']'
                        : 'new '.$p->getType().'($value[\'ORG_NAME\'])'
                    ;
                }
            }

            $body .= strtr('
    if (array_key_exists(\'ORG_NAME\', $value)) {
        $this->_usedProperties[\'PROPERTY\'] = true;
        $this->PROPERTY = '.$code.';
        unset($value[\'ORG_NAME\']);
    }
', ['PROPERTY' => $p->getName(), 'ORG_NAME' => $p->getOriginalName()]);
        }

        if ($class->shouldAllowExtraKeys()) {
            $body .= '
    $this->_extraKeys = $value;
';
        } else {
            $body .= '
    if ([] !== $value) {
        throw new InvalidConfigurationException(sprintf(\'The following keys are not supported by "%s": \', __CLASS__).implode(\', \', array_keys($value)));
    }';

            $class->addUse(InvalidConfigurationException::class);
        }

        $class->addMethod('__construct', '
public function __construct(array $value = [])
{'.$body.'
}');
    }

    private function buildSetExtraKey(ClassBuilder $class): void
    {
        if (!$class->shouldAllowExtraKeys()) {
            return;
        }

        $class->addUse(ParamConfigurator::class);

        $class->addProperty('_extraKeys');

        $class->addMethod('set', '
/**
 * @param ParamConfigurator|mixed $value
 *
 * @return $this
 */
public function NAME(string $key, mixed $value): static
{
    $this->_extraKeys[$key] = $value;

    return $this;
}');
    }

    private function getSubNamespace(ClassBuilder $rootClass): string
    {
        return \sprintf('%s\\%s', $rootClass->getNamespace(), substr($rootClass->getName(), 0, -6));
    }

    private function hasNormalizationClosures(NodeInterface $node): bool
    {
        try {
            $r = new \ReflectionProperty($node, 'normalizationClosures');
        } catch (\ReflectionException) {
            return false;
        }

        return [] !== $r->getValue($node);
    }

    private function getType(string $classType, bool $hasNormalizationClosures): string
    {
        return $classType.($hasNormalizationClosures ? '|scalar' : '');
    }

    private function getParamType(array $types, bool $withParamConfigurator = false): string
    {
        return \in_array('mixed', $types, true) ? 'mixed' : ($withParamConfigurator ? 'ParamConfigurator|' : '').implode('|', $types);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Builder;

use Symfony\Component\Config\Definition\ConfigurationInterface;

/**
 * Generates ConfigBuilders to help create valid config.
 *
 * @author Tobias Nyholm <tobias.nyholm@gmail.com>
 */
interface ConfigBuilderGeneratorInterface
{
    /**
     * @return \Closure that will return the root config class
     */
    public function build(ConfigurationInterface $configuration): \Closure;
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Builder;

/**
 * Represents a method when building classes.
 *
 * @internal
 *
 * @author Tobias Nyholm <tobias.nyholm@gmail.com>
 */
class Method
{
    private string $content;

    public function __construct(string $content)
    {
        $this->content = $content;
    }

    public function getContent(): string
    {
        return $this->content;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Builder;

/**
 * Build PHP classes to generate config.
 *
 * @internal
 *
 * @author Tobias Nyholm <tobias.nyholm@gmail.com>
 */
class ClassBuilder
{
    private string $namespace;
    private string $name;

    /** @var Property[] */
    private array $properties = [];

    /** @var Method[] */
    private array $methods = [];
    private array $require = [];
    private array $use = [];
    private array $implements = [];
    private bool $allowExtraKeys = false;

    public function __construct(string $namespace, string $name)
    {
        $this->namespace = $namespace;
        $this->name = ucfirst($this->camelCase($name)).'Config';
    }

    public function getDirectory(): string
    {
        return str_replace('\\', \DIRECTORY_SEPARATOR, $this->namespace);
    }

    public function getFilename(): string
    {
        return $this->name.'.php';
    }

    public function build(): string
    {
        $rootPath = explode(\DIRECTORY_SEPARATOR, $this->getDirectory());
        $require = '';
        foreach ($this->require as $class) {
            // figure out relative path.
            $path = explode(\DIRECTORY_SEPARATOR, $class->getDirectory());
            $path[] = $class->getFilename();
            foreach ($rootPath as $key => $value) {
                if ($path[$key] !== $value) {
                    break;
                }
                unset($path[$key]);
            }
            $require .= \sprintf('require_once __DIR__.\DIRECTORY_SEPARATOR.\'%s\';', implode('\'.\DIRECTORY_SEPARATOR.\'', $path))."\n";
        }
        $use = $require ? "\n" : '';
        foreach (array_keys($this->use) as $statement) {
            $use .= \sprintf('use %s;', $statement)."\n";
        }

        $implements = [] === $this->implements ? '' : 'implements '.implode(', ', $this->implements);
        $body = '';
        foreach ($this->properties as $property) {
            $body .= '    '.$property->getContent()."\n";
        }
        foreach ($this->methods as $method) {
            $lines = explode("\n", $method->getContent());
            foreach ($lines as $line) {
                $body .= ($line ? '    '.$line : '')."\n";
            }
        }

        $content = strtr('<?php

namespace NAMESPACE;

REQUIREUSE
/**
 * This class is automatically generated to help in creating a config.
 */
class CLASS IMPLEMENTS
{
BODY
}
', ['NAMESPACE' => $this->namespace, 'REQUIRE' => $require, 'USE' => $use, 'CLASS' => $this->getName(), 'IMPLEMENTS' => $implements, 'BODY' => $body]);

        return $content;
    }

    public function addRequire(self $class): void
    {
        $this->require[] = $class;
    }

    public function addUse(string $class): void
    {
        $this->use[$class] = true;
    }

    public function addImplements(string $interface): void
    {
        $this->implements[] = '\\'.ltrim($interface, '\\');
    }

    public function addMethod(string $name, string $body, array $params = []): void
    {
        $this->methods[] = new Method(strtr($body, ['NAME' => $this->camelCase($name)] + $params));
    }

    public function addProperty(string $name, ?string $classType = null, ?string $defaultValue = null): Property
    {
        $property = new Property($name, '_' !== $name[0] ? $this->camelCase($name) : $name);
        if (null !== $classType) {
            $property->setType($classType);
        }
        $this->properties[] = $property;
        $defaultValue = null !== $defaultValue ? \sprintf(' = %s', $defaultValue) : '';
        $property->setContent(\sprintf('private $%s%s;', $property->getName(), $defaultValue));

        return $property;
    }

    public function getProperties(): array
    {
        return $this->properties;
    }

    private function camelCase(string $input): string
    {
        $output = lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $input))));

        return preg_replace('#\W#', '', $output);
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function getNamespace(): string
    {
        return $this->namespace;
    }

    public function getFqcn(): string
    {
        return '\\'.$this->namespace.'\\'.$this->name;
    }

    public function setAllowExtraKeys(bool $allowExtraKeys): void
    {
        $this->allowExtraKeys = $allowExtraKeys;
    }

    public function shouldAllowExtraKeys(): bool
    {
        return $this->allowExtraKeys;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config;

use Symfony\Component\Config\Exception\FileLocatorFileNotFoundException;

/**
 * FileLocator uses an array of pre-defined paths to find files.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class FileLocator implements FileLocatorInterface
{
    protected $paths;

    /**
     * @param string|string[] $paths A path or an array of paths where to look for resources
     */
    public function __construct(string|array $paths = [])
    {
        $this->paths = (array) $paths;
    }

    /**
     * @return string|string[]
     *
     * @psalm-return ($first is true ? string : string[])
     */
    public function locate(string $name, ?string $currentPath = null, bool $first = true)
    {
        if ('' === $name) {
            throw new \InvalidArgumentException('An empty file name is not valid to be located.');
        }

        if ($this->isAbsolutePath($name)) {
            if (!file_exists($name)) {
                throw new FileLocatorFileNotFoundException(\sprintf('The file "%s" does not exist.', $name), 0, null, [$name]);
            }

            return $name;
        }

        $paths = $this->paths;

        if (null !== $currentPath) {
            array_unshift($paths, $currentPath);
        }

        $paths = array_unique($paths);
        $filepaths = $notfound = [];

        foreach ($paths as $path) {
            if (@file_exists($file = $path.\DIRECTORY_SEPARATOR.$name)) {
                if (true === $first) {
                    return $file;
                }
                $filepaths[] = $file;
            } else {
                $notfound[] = $file;
            }
        }

        if (!$filepaths) {
            throw new FileLocatorFileNotFoundException(\sprintf('The file "%s" does not exist (in: "%s").', $name, implode('", "', $paths)), 0, null, $notfound);
        }

        return $filepaths;
    }

    /**
     * Returns whether the file path is an absolute path.
     */
    private function isAbsolutePath(string $file): bool
    {
        if ('/' === $file[0] || '\\' === $file[0]
            || (\strlen($file) > 3 && ctype_alpha($file[0])
                && ':' === $file[1]
                && ('\\' === $file[2] || '/' === $file[2])
            )
            || parse_url($file, \PHP_URL_SCHEME)
            || str_starts_with($file, 'phar:///') // "parse_url()" doesn't handle absolute phar path, despite being valid
        ) {
            return true;
        }

        return false;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config;

use Symfony\Component\Config\Exception\FileLocatorFileNotFoundException;

/**
 * @author Fabien Potencier <fabien@symfony.com>
 */
interface FileLocatorInterface
{
    /**
     * Returns a full path for a given file name.
     *
     * @param string      $name        The file name to locate
     * @param string|null $currentPath The current path
     * @param bool        $first       Whether to return the first occurrence or an array of filenames
     *
     * @return string|string[] The full path to the file or an array of file paths
     *
     * @psalm-return ($first is true ? string : string[])
     *
     * @throws \InvalidArgumentException        If $name is empty
     * @throws FileLocatorFileNotFoundException If a file is not found
     */
    public function locate(string $name, ?string $currentPath = null, bool $first = true);
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config;

/**
 * A ConfigCacheFactory implementation that validates the
 * cache with an arbitrary set of ResourceCheckers.
 *
 * @author Matthias Pigulla <mp@webfactory.de>
 */
class ResourceCheckerConfigCacheFactory implements ConfigCacheFactoryInterface
{
    private iterable $resourceCheckers = [];

    /**
     * @param iterable<int, ResourceCheckerInterface> $resourceCheckers
     */
    public function __construct(iterable $resourceCheckers = [])
    {
        $this->resourceCheckers = $resourceCheckers;
    }

    public function cache(string $file, callable $callable): ConfigCacheInterface
    {
        $cache = new ResourceCheckerConfigCache($file, $this->resourceCheckers);
        if (!$cache->isFresh()) {
            $callable($cache);
        }

        return $cache;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config;

/**
 * Interface for a ConfigCache factory. This factory creates
 * an instance of ConfigCacheInterface and initializes the
 * cache if necessary.
 *
 * @author Matthias Pigulla <mp@webfactory.de>
 */
interface ConfigCacheFactoryInterface
{
    /**
     * Creates a cache instance and (re-)initializes it if necessary.
     *
     * @param string   $file     The absolute cache file path
     * @param callable $callable The callable to be executed when the cache needs to be filled (i. e. is not fresh). The cache will be passed as the only parameter to this callback
     */
    public function cache(string $file, callable $callable): ConfigCacheInterface;
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config;

use Symfony\Component\Config\Resource\ResourceInterface;

/**
 * Interface for ConfigCache.
 *
 * @author Matthias Pigulla <mp@webfactory.de>
 */
interface ConfigCacheInterface
{
    /**
     * Gets the cache file path.
     */
    public function getPath(): string;

    /**
     * Checks if the cache is still fresh.
     *
     * This check should take the metadata passed to the write() method into consideration.
     */
    public function isFresh(): bool;

    /**
     * Writes the given content into the cache file. Metadata will be stored
     * independently and can be used to check cache freshness at a later time.
     *
     * @param string                   $content  The content to write into the cache
     * @param ResourceInterface[]|null $metadata An array of ResourceInterface instances
     *
     * @return void
     *
     * @throws \RuntimeException When the cache file cannot be written
     */
    public function write(string $content, ?array $metadata = null);
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config;

use Symfony\Component\Config\Resource\SelfCheckingResourceChecker;

/**
 * ConfigCache caches arbitrary content in files on disk.
 *
 * When in debug mode, those metadata resources that implement
 * \Symfony\Component\Config\Resource\SelfCheckingResourceInterface will
 * be used to check cache freshness.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 * @author Matthias Pigulla <mp@webfactory.de>
 */
class ConfigCache extends ResourceCheckerConfigCache
{
    private bool $debug;

    /**
     * @param string $file  The absolute cache path
     * @param bool   $debug Whether debugging is enabled or not
     */
    public function __construct(string $file, bool $debug)
    {
        $this->debug = $debug;

        $checkers = [];
        if (true === $this->debug) {
            $checkers = [new SelfCheckingResourceChecker()];
        }

        parent::__construct($file, $checkers);
    }

    /**
     * Checks if the cache is still fresh.
     *
     * This implementation always returns true when debug is off and the
     * cache file exists.
     */
    public function isFresh(): bool
    {
        if (!$this->debug && is_file($this->getPath())) {
            return true;
        }

        return parent::isFresh();
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Resource;

/**
 * FileExistenceResource represents a resource stored on the filesystem.
 * Freshness is only evaluated against resource creation or deletion.
 *
 * The resource can be a file or a directory.
 *
 * @author Charles-Henri Bruyand <charleshenri.bruyand@gmail.com>
 *
 * @final
 */
class FileExistenceResource implements SelfCheckingResourceInterface
{
    private string $resource;

    private bool $exists;

    /**
     * @param string $resource The file path to the resource
     */
    public function __construct(string $resource)
    {
        $this->resource = $resource;
        $this->exists = file_exists($resource);
    }

    public function __toString(): string
    {
        return 'existence.'.$this->resource;
    }

    public function getResource(): string
    {
        return $this->resource;
    }

    public function isFresh(int $timestamp): bool
    {
        return file_exists($this->resource) === $this->exists;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Resource;

/**
 * DirectoryResource represents a resources stored in a subdirectory tree.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 *
 * @final
 */
class DirectoryResource implements SelfCheckingResourceInterface
{
    private string $resource;
    private ?string $pattern;

    /**
     * @param string      $resource The file path to the resource
     * @param string|null $pattern  A pattern to restrict monitored files
     *
     * @throws \InvalidArgumentException
     */
    public function __construct(string $resource, ?string $pattern = null)
    {
        $resolvedResource = realpath($resource) ?: (file_exists($resource) ? $resource : false);
        $this->pattern = $pattern;

        if (false === $resolvedResource || !is_dir($resolvedResource)) {
            throw new \InvalidArgumentException(\sprintf('The directory "%s" does not exist.', $resource));
        }

        $this->resource = $resolvedResource;
    }

    public function __toString(): string
    {
        return hash('xxh128', serialize([$this->resource, $this->pattern]));
    }

    public function getResource(): string
    {
        return $this->resource;
    }

    public function getPattern(): ?string
    {
        return $this->pattern;
    }

    public function isFresh(int $timestamp): bool
    {
        if (!is_dir($this->resource)) {
            return false;
        }

        if ($timestamp < filemtime($this->resource)) {
            return false;
        }

        foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($this->resource), \RecursiveIteratorIterator::SELF_FIRST) as $file) {
            // if regex filtering is enabled only check matching files
            if ($this->pattern && $file->isFile() && !preg_match($this->pattern, $file->getBasename())) {
                continue;
            }

            // always monitor directories for changes, except the .. entries
            // (otherwise deleted files wouldn't get detected)
            if ($file->isDir() && str_ends_with($file, '/..')) {
                continue;
            }

            // for broken links
            try {
                $fileMTime = $file->getMTime();
            } catch (\RuntimeException) {
                continue;
            }

            // early return if a file's mtime exceeds the passed timestamp
            if ($timestamp < $fileMTime) {
                return false;
            }
        }

        return true;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Resource;

/**
 * ResourceInterface is the interface that must be implemented by all Resource classes.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
interface ResourceInterface extends \Stringable
{
    /**
     * Returns a string representation of the Resource.
     *
     * This method is necessary to allow for resource de-duplication, for example by means
     * of array_unique(). The string returned need not have a particular meaning, but has
     * to be identical for different ResourceInterface instances referring to the same
     * resource; and it should be unlikely to collide with that of other, unrelated
     * resource instances.
     */
    public function __toString(): string;
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Resource;

/**
 * ComposerResource tracks the PHP version and Composer dependencies.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @final
 */
class ComposerResource implements SelfCheckingResourceInterface
{
    private array $vendors;

    private static array $runtimeVendors;

    public function __construct()
    {
        self::refresh();
        $this->vendors = self::$runtimeVendors;
    }

    public function getVendors(): array
    {
        return array_keys($this->vendors);
    }

    public function __toString(): string
    {
        return __CLASS__;
    }

    public function isFresh(int $timestamp): bool
    {
        self::refresh();

        return array_values(self::$runtimeVendors) === array_values($this->vendors);
    }

    private static function refresh(): void
    {
        self::$runtimeVendors = [];

        foreach (get_declared_classes() as $class) {
            if ('C' === $class[0] && str_starts_with($class, 'ComposerAutoloaderInit')) {
                $r = new \ReflectionClass($class);
                $v = \dirname($r->getFileName(), 2);
                if (is_file($v.'/composer/installed.json')) {
                    self::$runtimeVendors[$v] = @filemtime($v.'/composer/installed.json');
                }
            }
        }
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Resource;

use Symfony\Component\Config\ResourceCheckerInterface;

/**
 * Resource checker for instances of SelfCheckingResourceInterface.
 *
 * As these resources perform the actual check themselves, we can provide
 * this class as a standard way of validating them.
 *
 * @author Matthias Pigulla <mp@webfactory.de>
 */
class SelfCheckingResourceChecker implements ResourceCheckerInterface
{
    // Common shared cache, because this checker can be used in different
    // situations. For example, when using the full stack framework, the router
    // and the container have their own cache. But they may check the very same
    // resources
    private static array $cache = [];

    public function supports(ResourceInterface $metadata): bool
    {
        return $metadata instanceof SelfCheckingResourceInterface;
    }

    /**
     * @param SelfCheckingResourceInterface $resource
     */
    public function isFresh(ResourceInterface $resource, int $timestamp): bool
    {
        $key = "$resource:$timestamp";

        return self::$cache[$key] ??= $resource->isFresh($timestamp);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Resource;

/**
 * Interface for Resources that can check for freshness autonomously,
 * without special support from external services.
 *
 * @author Matthias Pigulla <mp@webfactory.de>
 */
interface SelfCheckingResourceInterface extends ResourceInterface
{
    /**
     * Returns true if the resource has not been updated since the given timestamp.
     *
     * @param int $timestamp The last time the resource was loaded
     */
    public function isFresh(int $timestamp): bool;
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Resource;

/**
 * FileResource represents a resource stored on the filesystem.
 *
 * The resource can be a file or a directory.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 *
 * @final
 */
class FileResource implements SelfCheckingResourceInterface
{
    private string $resource;

    /**
     * @param string $resource The file path to the resource
     *
     * @throws \InvalidArgumentException
     */
    public function __construct(string $resource)
    {
        $resolvedResource = realpath($resource) ?: (file_exists($resource) ? $resource : false);

        if (false === $resolvedResource) {
            throw new \InvalidArgumentException(\sprintf('The file "%s" does not exist.', $resource));
        }

        $this->resource = $resolvedResource;
    }

    public function __toString(): string
    {
        return $this->resource;
    }

    /**
     * Returns the canonicalized, absolute path to the resource.
     */
    public function getResource(): string
    {
        return $this->resource;
    }

    public function isFresh(int $timestamp): bool
    {
        return false !== ($filemtime = @filemtime($this->resource)) && $filemtime <= $timestamp;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Resource;

/**
 * ClassExistenceResource represents a class existence.
 * Freshness is only evaluated against resource existence.
 *
 * The resource must be a fully-qualified class name.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 *
 * @final
 */
class ClassExistenceResource implements SelfCheckingResourceInterface
{
    private string $resource;
    private ?array $exists = null;

    private static int $autoloadLevel = 0;
    private static ?string $autoloadedClass = null;
    private static array $existsCache = [];

    /**
     * @param string    $resource The fully-qualified class name
     * @param bool|null $exists   Boolean when the existence check has already been done
     */
    public function __construct(string $resource, ?bool $exists = null)
    {
        $this->resource = $resource;
        if (null !== $exists) {
            $this->exists = [$exists, null];
        }
    }

    public function __toString(): string
    {
        return $this->resource;
    }

    public function getResource(): string
    {
        return $this->resource;
    }

    /**
     * @throws \ReflectionException when a parent class/interface/trait is not found
     */
    public function isFresh(int $timestamp): bool
    {
        $loaded = class_exists($this->resource, false) || interface_exists($this->resource, false) || trait_exists($this->resource, false);

        if (null !== $exists = &self::$existsCache[$this->resource]) {
            if ($loaded) {
                $exists = [true, null];
            } elseif (0 >= $timestamp && !$exists[0] && null !== $exists[1]) {
                throw new \ReflectionException($exists[1]);
            }
        } elseif ([false, null] === $exists = [$loaded, null]) {
            if (!self::$autoloadLevel++) {
                spl_autoload_register(__CLASS__.'::throwOnRequiredClass');
            }
            $autoloadedClass = self::$autoloadedClass;
            self::$autoloadedClass = ltrim($this->resource, '\\');

            try {
                $exists[0] = class_exists($this->resource) || interface_exists($this->resource, false) || trait_exists($this->resource, false);
            } catch (\Exception $e) {
                $exists[1] = $e->getMessage();

                try {
                    self::throwOnRequiredClass($this->resource, $e);
                } catch (\ReflectionException $e) {
                    if (0 >= $timestamp) {
                        throw $e;
                    }
                }
            } catch (\Throwable $e) {
                $exists[1] = $e->getMessage();

                throw $e;
            } finally {
                self::$autoloadedClass = $autoloadedClass;
                if (!--self::$autoloadLevel) {
                    spl_autoload_unregister(__CLASS__.'::throwOnRequiredClass');
                }
            }
        }

        $this->exists ??= $exists;

        return $this->exists[0] xor !$exists[0];
    }

    public function __serialize(): array
    {
        if (null === $this->exists) {
            $this->isFresh(0);
        }

        return [
            'resource' => $this->resource,
            'exists' => $this->exists,
        ];
    }

    public function __unserialize(array $data): void
    {
        foreach ($data as $value) {
            if ($value instanceof \Stringable) {
                throw new \BadMethodCallException('Cannot unserialize '.self::class);
            }
        }

        $this->resource = array_shift($data);
        $this->exists = array_shift($data);

        if (\is_bool($this->exists)) {
            $this->exists = [$this->exists, null];
        }
    }

    /**
     * Throws a reflection exception when the passed class does not exist but is required.
     *
     * A class is considered "not required" when it's loaded as part of a "class_exists" or similar check.
     *
     * This function can be used as an autoload function to throw a reflection
     * exception if the class was not found by previous autoload functions.
     *
     * A previous exception can be passed. In this case, the class is considered as being
     * required totally, so if it doesn't exist, a reflection exception is always thrown.
     * If it exists, the previous exception is rethrown.
     *
     * @throws \ReflectionException
     *
     * @internal
     */
    public static function throwOnRequiredClass(string $class, ?\Exception $previous = null): void
    {
        // If the passed class is the resource being checked, we shouldn't throw.
        if (null === $previous && self::$autoloadedClass === $class) {
            return;
        }

        if (class_exists($class, false) || interface_exists($class, false) || trait_exists($class, false)) {
            if (null !== $previous) {
                throw $previous;
            }

            return;
        }

        if ($previous instanceof \ReflectionException) {
            throw $previous;
        }

        $message = \sprintf('Class "%s" not found.', $class);

        if ($class !== (self::$autoloadedClass ?? $class)) {
            $message = substr_replace($message, \sprintf(' while loading "%s"', self::$autoloadedClass), -1, 0);
        }

        if (null !== $previous) {
            $message = $previous->getMessage();
        }

        $e = new \ReflectionException($message, 0, $previous);

        if (null !== $previous) {
            throw $e;
        }

        $trace = debug_backtrace();
        $autoloadFrame = [
            'function' => 'spl_autoload_call',
            'args' => [$class],
        ];

        if (isset($trace[1])) {
            $callerFrame = $trace[1];
            $i = 2;
        } elseif (false !== $i = array_search($autoloadFrame, $trace, true)) {
            $callerFrame = $trace[++$i];
        } else {
            throw $e;
        }

        if (isset($callerFrame['function']) && !isset($callerFrame['class'])) {
            switch ($callerFrame['function']) {
                case 'get_class_methods':
                case 'get_class_vars':
                case 'get_parent_class':
                case 'is_a':
                case 'is_subclass_of':
                case 'class_exists':
                case 'class_implements':
                case 'class_parents':
                case 'trait_exists':
                case 'defined':
                case 'interface_exists':
                case 'method_exists':
                case 'property_exists':
                case 'is_callable':
                    return;
            }

            $props = [
                'file' => $callerFrame['file'] ?? null,
                'line' => $callerFrame['line'] ?? null,
                'trace' => \array_slice($trace, 1 + $i),
            ];

            foreach ($props as $p => $v) {
                if (null !== $v) {
                    $r = new \ReflectionProperty(\Exception::class, $p);
                    $r->setValue($e, $v);
                }
            }
        }

        throw $e;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Resource;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormTypeExtensionInterface;
use Symfony\Component\Messenger\Handler\MessageSubscriberInterface;
use Symfony\Contracts\Service\ServiceSubscriberInterface;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @final
 */
class ReflectionClassResource implements SelfCheckingResourceInterface
{
    private array $files = [];
    private string $className;
    private \ReflectionClass $classReflector;
    private array $excludedVendors = [];
    private string $hash;

    public function __construct(\ReflectionClass $classReflector, array $excludedVendors = [])
    {
        $this->className = $classReflector->name;
        $this->classReflector = $classReflector;
        $this->excludedVendors = $excludedVendors;
    }

    public function isFresh(int $timestamp): bool
    {
        if (!isset($this->hash)) {
            $this->hash = $this->computeHash();
            $this->loadFiles($this->classReflector);
        }

        foreach ($this->files as $file => $v) {
            if (false === $filemtime = @filemtime($file)) {
                return false;
            }

            if ($filemtime > $timestamp) {
                return $this->hash === $this->computeHash();
            }
        }

        return true;
    }

    public function __toString(): string
    {
        return 'reflection.'.$this->className;
    }

    public function __serialize(): array
    {
        if (!isset($this->hash)) {
            $this->hash = $this->computeHash();
            $this->loadFiles($this->classReflector);
        }

        return [
            'files' => $this->files,
            'className' => $this->className,
            'excludedVendors' => $this->excludedVendors,
            'hash' => $this->hash,
        ];
    }

    private function loadFiles(\ReflectionClass $class): void
    {
        foreach ($class->getInterfaces() as $v) {
            $this->loadFiles($v);
        }
        do {
            $file = $class->getFileName();
            if (false !== $file && is_file($file)) {
                foreach ($this->excludedVendors as $vendor) {
                    if (str_starts_with($file, $vendor) && false !== strpbrk(substr($file, \strlen($vendor), 1), '/'.\DIRECTORY_SEPARATOR)) {
                        $file = false;
                        break;
                    }
                }
                if ($file) {
                    $this->files[$file] = null;
                }
            }
            foreach ($class->getTraits() as $v) {
                $this->loadFiles($v);
            }
        } while ($class = $class->getParentClass());
    }

    private function computeHash(): string
    {
        try {
            $this->classReflector ??= new \ReflectionClass($this->className);
        } catch (\ReflectionException) {
            // the class does not exist anymore
            return false;
        }
        $hash = hash_init('xxh128');

        foreach ($this->generateSignature($this->classReflector) as $info) {
            hash_update($hash, $info);
        }

        return hash_final($hash);
    }

    private function generateSignature(\ReflectionClass $class): iterable
    {
        $attributes = [];
        foreach ($class->getAttributes() as $a) {
            $attributes[] = [$a->getName(), (string) $a];
        }
        yield print_r($attributes, true);
        $attributes = [];

        yield $class->getDocComment() ?: '';
        yield (int) $class->isFinal();
        yield (int) $class->isAbstract();

        if ($class->isTrait()) {
            yield print_r(class_uses($class->name), true);
        } else {
            yield print_r(class_parents($class->name), true);
            yield print_r(class_implements($class->name), true);
            yield print_r($class->getConstants(), true);
        }

        foreach ($class->getReflectionConstants() as $constant) {
            foreach ($constant->getAttributes() as $a) {
                $attributes[] = [$a->getName(), (string) $a];
            }
            yield $constant->name.print_r($attributes, true);
            $attributes = [];
        }

        if (!$class->isInterface()) {
            $defaults = $class->getDefaultProperties();

            foreach ($class->getProperties(\ReflectionProperty::IS_PUBLIC | \ReflectionProperty::IS_PROTECTED) as $p) {
                foreach ($p->getAttributes() as $a) {
                    $attributes[] = [$a->getName(), (string) $a];
                }
                yield print_r($attributes, true);
                $attributes = [];

                yield $p->getDocComment() ?: '';
                yield $p->isDefault() ? '<default>' : '';
                yield $p->isPublic() ? 'public' : 'protected';
                yield $p->isStatic() ? 'static' : '';
                yield '$'.$p->name;
                yield print_r(isset($defaults[$p->name]) && !\is_object($defaults[$p->name]) ? $defaults[$p->name] : null, true);
            }
        }

        foreach ($class->getMethods(\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED) as $m) {
            foreach ($m->getAttributes() as $a) {
                $attributes[] = [$a->getName(), (string) $a];
            }
            yield print_r($attributes, true);
            $attributes = [];

            $defaults = [];
            foreach ($m->getParameters() as $p) {
                foreach ($p->getAttributes() as $a) {
                    $attributes[] = [$a->getName(), (string) $a];
                }
                yield print_r($attributes, true);
                $attributes = [];

                if (!$p->isDefaultValueAvailable()) {
                    $defaults[$p->name] = null;

                    continue;
                }

                $defaults[$p->name] = (string) $p;
            }

            yield preg_replace('/^  @@.*/m', '', $m);
            yield print_r($defaults, true);
        }

        if ($class->isAbstract() || $class->isInterface() || $class->isTrait()) {
            return;
        }

        if (interface_exists(EventSubscriberInterface::class, false) && $class->isSubclassOf(EventSubscriberInterface::class)) {
            yield EventSubscriberInterface::class;
            yield print_r($class->name::getSubscribedEvents(), true);
        }

        if (interface_exists(MessageSubscriberInterface::class, false) && $class->isSubclassOf(MessageSubscriberInterface::class)) {
            yield MessageSubscriberInterface::class;
            foreach ($class->name::getHandledMessages() as $key => $value) {
                yield $key.print_r($value, true);
            }
        }

        if (interface_exists(ServiceSubscriberInterface::class, false) && $class->isSubclassOf(ServiceSubscriberInterface::class)) {
            yield ServiceSubscriberInterface::class;
            yield print_r($class->name::getSubscribedServices(), true);
        }

        if (interface_exists(FormTypeExtensionInterface::class, false) && $class->isSubclassOf(FormTypeExtensionInterface::class)) {
            yield FormTypeExtensionInterface::class;
            foreach ($class->name::getExtendedTypes() as $key => $value) {
                yield $key.print_r($value, true);
            }
        }
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config\Resource;

use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\Glob;

/**
 * GlobResource represents a set of resources stored on the filesystem.
 *
 * Only existence/removal is tracked (not mtimes.)
 *
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @final
 *
 * @implements \IteratorAggregate<string, \SplFileInfo>
 */
class GlobResource implements \IteratorAggregate, SelfCheckingResourceInterface
{
    private string $prefix;
    private string $pattern;
    private bool $recursive;
    private string $hash;
    private bool $forExclusion;
    private array $excludedPrefixes;
    private int $globBrace;

    /**
     * @param string $prefix    A directory prefix
     * @param string $pattern   A glob pattern
     * @param bool   $recursive Whether directories should be scanned recursively or not
     *
     * @throws \InvalidArgumentException
     */
    public function __construct(string $prefix, string $pattern, bool $recursive, bool $forExclusion = false, array $excludedPrefixes = [])
    {
        ksort($excludedPrefixes);
        $resolvedPrefix = realpath($prefix) ?: (file_exists($prefix) ? $prefix : false);
        $this->pattern = $pattern;
        $this->recursive = $recursive;
        $this->forExclusion = $forExclusion;
        $this->excludedPrefixes = $excludedPrefixes;
        $this->globBrace = \defined('GLOB_BRACE') ? \GLOB_BRACE : 0;

        if (false === $resolvedPrefix) {
            throw new \InvalidArgumentException(\sprintf('The path "%s" does not exist.', $prefix));
        }

        $this->prefix = $resolvedPrefix;
    }

    public function getPrefix(): string
    {
        return $this->prefix;
    }

    public function __toString(): string
    {
        return 'glob.'.$this->prefix.(int) $this->recursive.$this->pattern.(int) $this->forExclusion.implode("\0", $this->excludedPrefixes);
    }

    public function isFresh(int $timestamp): bool
    {
        $hash = $this->computeHash();
        $this->hash ??= $hash;

        return $this->hash === $hash;
    }

    public function __serialize(): array
    {
        $this->hash ??= $this->computeHash();

        return [
            'prefix' => $this->prefix,
            'pattern' => $this->pattern,
            'recursive' => $this->recursive,
            'hash' => $this->hash,
            'forExclusion' => $this->forExclusion,
            'excludedPrefixes' => $this->excludedPrefixes,
        ];
    }

    public function __unserialize(array $data): void
    {
        foreach ($data as $value) {
            if ($value instanceof \Stringable) {
                throw new \BadMethodCallException('Cannot unserialize '.self::class);
            }
        }

        $this->prefix = array_shift($data);
        $this->pattern = array_shift($data);
        $this->recursive = array_shift($data);
        $this->hash = array_shift($data);
        $this->forExclusion = array_shift($data);
        $this->excludedPrefixes = array_shift($data);
        $this->globBrace = \defined('GLOB_BRACE') ? \GLOB_BRACE : 0;
    }

    public function getIterator(): \Traversable
    {
        if ((!$this->recursive && '' === $this->pattern) || !file_exists($this->prefix)) {
            return;
        }

        if (is_file($prefix = str_replace('\\', '/', $this->prefix))) {
            $prefix = \dirname($prefix);
            $pattern = basename($prefix).$this->pattern;
        } else {
            $pattern = $this->pattern;
        }

        if (class_exists(Finder::class)) {
            $regex = Glob::toRegex($pattern);
            if ($this->recursive) {
                $regex = substr_replace($regex, str_ends_with($pattern, '/') ? '' : '(/|$)', -2, 1);
            }
        } else {
            $regex = null;
        }

        $prefixLen = \strlen($prefix);
        $paths = null;

        if ('' === $this->pattern && is_file($this->prefix)) {
            $paths = [$this->prefix => null];
        } elseif (!str_starts_with($this->prefix, 'phar://') && (null !== $regex || !str_contains($this->pattern, '/**/'))) {
            if (!str_contains($this->pattern, '/**/') && ($this->globBrace || !str_contains($this->pattern, '{'))) {
                $paths = array_fill_keys(glob($this->prefix.$this->pattern, \GLOB_NOSORT | $this->globBrace), null);
            } elseif (!str_contains($this->pattern, '\\') || !preg_match('/\\\\[,{}]/', $this->pattern)) {
                $paths = [];
                foreach ($this->expandGlob($this->pattern) as $p) {
                    if (false !== $i = strpos($p, '/**/')) {
                        $p = substr_replace($p, '/*', $i);
                    }
                    $paths += array_fill_keys(glob($this->prefix.$p, \GLOB_NOSORT), false !== $i ? $regex : null);
                }
            }
        }

        if (null !== $paths) {
            uksort($paths, 'strnatcmp');
            foreach ($paths as $path => $regex) {
                if ($this->excludedPrefixes) {
                    $normalizedPath = str_replace('\\', '/', $path);
                    do {
                        if (isset($this->excludedPrefixes[$dirPath = $normalizedPath])) {
                            continue 2;
                        }
                    } while ($prefix !== $dirPath && $dirPath !== $normalizedPath = \dirname($dirPath));
                }

                if ((null === $regex || preg_match($regex, substr(str_replace('\\', '/', $path), $prefixLen))) && is_file($path)) {
                    yield $path => new \SplFileInfo($path);
                }
                if (!is_dir($path)) {
                    continue;
                }
                if ($this->forExclusion && (null === $regex || preg_match($regex, substr(str_replace('\\', '/', $path), $prefixLen)))) {
                    yield $path => new \SplFileInfo($path);
                    continue;
                }
                if (!($this->recursive || null !== $regex) || isset($this->excludedPrefixes[str_replace('\\', '/', $path)])) {
                    continue;
                }
                $files = iterator_to_array(new \RecursiveIteratorIterator(
                    new \RecursiveCallbackFilterIterator(
                        new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS),
                        fn (\SplFileInfo $file, $path) => !isset($this->excludedPrefixes[$path = str_replace('\\', '/', $path)])
                            && (null === $regex || preg_match($regex, substr($path, $prefixLen)) || $file->isDir())
                            && '.' !== $file->getBasename()[0]
                    ),
                    \RecursiveIteratorIterator::LEAVES_ONLY
                ));
                uksort($files, 'strnatcmp');

                foreach ($files as $path => $info) {
                    if ($info->isFile()) {
                        yield $path => $info;
                    }
                }
            }

            return;
        }

        if (!class_exists(Finder::class)) {
            throw new \LogicException('Extended glob patterns cannot be used as the Finder component is not installed. Try running "composer require symfony/finder".');
        }

        yield from (new Finder())
            ->followLinks()
            ->filter(function (\SplFileInfo $info) use ($regex, $prefixLen, $prefix) {
                $normalizedPath = str_replace('\\', '/', $info->getPathname());
                if (!preg_match($regex, substr($normalizedPath, $prefixLen)) || !$info->isFile()) {
                    return false;
                }
                if ($this->excludedPrefixes) {
                    do {
                        if (isset($this->excludedPrefixes[$dirPath = $normalizedPath])) {
                            return false;
                        }
                    } while ($prefix !== $dirPath && $dirPath !== $normalizedPath = \dirname($dirPath));
                }
            })
            ->sortByName()
            ->in($prefix)
        ;
    }

    private function computeHash(): string
    {
        $hash = hash_init('xxh128');

        foreach ($this->getIterator() as $path => $info) {
            hash_update($hash, $path."\n");
        }

        return hash_final($hash);
    }

    private function expandGlob(string $pattern): array
    {
        $segments = preg_split('/\{([^{}]*+)\}/', $pattern, -1, \PREG_SPLIT_DELIM_CAPTURE);
        $paths = [$segments[0]];
        $patterns = [];

        for ($i = 1; $i < \count($segments); $i += 2) {
            $patterns = [];

            foreach (explode(',', $segments[$i]) as $s) {
                foreach ($paths as $p) {
                    $patterns[] = $p.$s.$segments[1 + $i];
                }
            }

            $paths = $patterns;
        }

        $j = 0;
        foreach ($patterns as $i => $p) {
            if (str_contains($p, '{')) {
                $p = $this->expandGlob($p);
                array_splice($paths, $i + $j, 1, $p);
                $j += \count($p) - 1;
            }
        }

        return $paths;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Config;

use Symfony\Component\Config\Resource\ResourceInterface;

/**
 * Interface for ResourceCheckers.
 *
 * When a ResourceCheckerConfigCache instance is checked for freshness, all its associated
 * metadata resources are passed to ResourceCheckers. The ResourceCheckers
 * can then inspect the resources and decide whether the cache can be considered
 * fresh or not.
 *
 * @author Matthias Pigulla <mp@webfactory.de>
 * @author Benjamin Klotz <bk@webfactory.de>
 */
interface ResourceCheckerInterface
{
    /**
     * Queries the ResourceChecker whether it can validate a given
     * resource or not.
     *
     * @return bool
     */
    public function supports(ResourceInterface $metadata);

    /**
     * Validates the resource.
     *
     * @param int $timestamp The timestamp at which the cache associated with this resource was created
     *
     * @return bool
     */
    public function isFresh(ResourceInterface $resource, int $timestamp);
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

use Symfony\Polyfill\Intl\Normalizer as p;

if (!function_exists('normalizer_is_normalized')) {
    function normalizer_is_normalized(?string $string, ?int $form = p\Normalizer::FORM_C): bool { return p\Normalizer::isNormalized((string) $string, (int) $form); }
}
if (!function_exists('normalizer_normalize')) {
    function normalizer_normalize(?string $string, ?int $form = p\Normalizer::FORM_C): string|false { return p\Normalizer::normalize((string) $string, (int) $form); }
}
if (!function_exists('normalizer_get_raw_decomposition')) {
    function normalizer_get_raw_decomposition(?string $string, ?int $form = p\Normalizer::FORM_C): ?string { return p\Normalizer::getRawDecomposition((string) $string, (int) $form); }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

use Symfony\Polyfill\Intl\Normalizer as p;

if (\PHP_VERSION_ID >= 80000) {
    return require __DIR__.'/bootstrap80.php';
}

if (!function_exists('normalizer_is_normalized')) {
    function normalizer_is_normalized($string, $form = p\Normalizer::FORM_C) { return p\Normalizer::isNormalized($string, $form); }
}
if (!function_exists('normalizer_normalize')) {
    function normalizer_normalize($string, $form = p\Normalizer::FORM_C) { return p\Normalizer::normalize($string, $form); }
}
if (!function_exists('normalizer_get_raw_decomposition')) {
    function normalizer_get_raw_decomposition(?string $string, ?int $form = p\Normalizer::FORM_C) { return p\Normalizer::getRawDecomposition((string) $string, (int) $form); }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Polyfill\Intl\Normalizer;

/**
 * Normalizer is a PHP fallback implementation of the Normalizer class provided by the intl extension.
 *
 * It has been validated with Unicode 6.3 Normalization Conformance Test.
 * See http://www.unicode.org/reports/tr15/ for detailed info about Unicode normalizations.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @internal
 */
class Normalizer
{
    public const FORM_D = \Normalizer::FORM_D;
    public const FORM_KD = \Normalizer::FORM_KD;
    public const FORM_C = \Normalizer::FORM_C;
    public const FORM_KC = \Normalizer::FORM_KC;
    public const NFD = \Normalizer::NFD;
    public const NFKD = \Normalizer::NFKD;
    public const NFC = \Normalizer::NFC;
    public const NFKC = \Normalizer::NFKC;

    private static $C;
    private static $D;
    private static $KD;
    private static $cC;
    private static $rawD;
    private static $rawKD;
    private static $ulenMask = ["\xC0" => 2, "\xD0" => 2, "\xE0" => 3, "\xF0" => 4];
    private static $ASCII = "\x20\x65\x69\x61\x73\x6E\x74\x72\x6F\x6C\x75\x64\x5D\x5B\x63\x6D\x70\x27\x0A\x67\x7C\x68\x76\x2E\x66\x62\x2C\x3A\x3D\x2D\x71\x31\x30\x43\x32\x2A\x79\x78\x29\x28\x4C\x39\x41\x53\x2F\x50\x22\x45\x6A\x4D\x49\x6B\x33\x3E\x35\x54\x3C\x44\x34\x7D\x42\x7B\x38\x46\x77\x52\x36\x37\x55\x47\x4E\x3B\x4A\x7A\x56\x23\x48\x4F\x57\x5F\x26\x21\x4B\x3F\x58\x51\x25\x59\x5C\x09\x5A\x2B\x7E\x5E\x24\x40\x60\x7F\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F";

    public static function isNormalized(string $s, int $form = self::FORM_C)
    {
        if (!\in_array($form, [self::NFD, self::NFKD, self::NFC, self::NFKC])) {
            return false;
        }
        if (!isset($s[strspn($s, self::$ASCII)])) {
            return true;
        }
        if (self::NFC == $form && preg_match('//u', $s) && !preg_match('/[^\x00-\x{2FF}]/u', $s)) {
            return true;
        }

        return self::normalize($s, $form) === $s;
    }

    public static function getRawDecomposition(string $s, int $form = self::FORM_C)
    {
        if ('' === $s || !preg_match('//u', $s)) {
            return null;
        }

        $ulen = $s[0] < "\x80" ? 1 : (self::$ulenMask[$s[0] & "\xF0"] ?? 0);
        if (!$ulen || \strlen($s) !== $ulen) {
            return null;
        }

        if (self::NFC !== $form && self::NFD !== $form && self::NFKC !== $form && self::NFKD !== $form) {
            return '';
        }

        if ($s >= "\xEA\xB0\x80" && $s <= "\xED\x9E\xA3") {
            $u = unpack('C*', $s);
            $j = (($u[1] - 224) << 12) + (($u[2] - 128) << 6) + $u[3] - 0xAC80;

            if ($t = $j % 28) {
                $j -= $t;
                $lv = 0xAC00 + $j;
                $r = \chr(0xE0 | $lv >> 12).\chr(0x80 | $lv >> 6 & 0x3F).\chr(0x80 | $lv & 0x3F);
                $r .= $t < 25
                    ? ("\xE1\x86".\chr(0xA7 + $t))
                    : ("\xE1\x87".\chr(0x67 + $t));

                return $r;
            }

            return "\xE1\x84".\chr(0x80 + (int) ($j / 588))
                  ."\xE1\x85".\chr(0xA1 + (int) (($j % 588) / 28));
        }

        if (null === self::$rawD) {
            self::$rawD = self::getData('rawCanonicalDecomposition');
        }

        if (isset(self::$rawD[$s])) {
            return self::$rawD[$s];
        }

        if (self::NFKC === $form || self::NFKD === $form) {
            if (null === self::$rawKD) {
                self::$rawKD = self::getData('rawCompatibilityDecomposition');
            }

            return self::$rawKD[$s] ?? null;
        }

        return null;
    }

    public static function normalize(string $s, int $form = self::FORM_C)
    {
        if (!preg_match('//u', $s)) {
            return false;
        }

        switch ($form) {
            case self::NFC: $C = true; $K = false; break;
            case self::NFD: $C = false; $K = false; break;
            case self::NFKC: $C = true; $K = true; break;
            case self::NFKD: $C = false; $K = true; break;
            default:
                if (\defined('Normalizer::NONE') && \Normalizer::NONE == $form) {
                    return $s;
                }

                if (80000 > \PHP_VERSION_ID) {
                    return false;
                }

                throw new \ValueError('normalizer_normalize(): Argument #2 ($form) must be a a valid normalization form');
        }

        if ('' === $s) {
            return '';
        }

        if ($K && null === self::$KD) {
            self::$KD = self::getData('compatibilityDecomposition');
        }

        if (null === self::$D) {
            self::$D = self::getData('canonicalDecomposition');
            self::$cC = self::getData('combiningClass');
        }

        if (null !== $mbEncoding = (2 /* MB_OVERLOAD_STRING */ & (int) \ini_get('mbstring.func_overload')) ? mb_internal_encoding() : null) {
            mb_internal_encoding('8bit');
        }

        $r = self::decompose($s, $K);

        if ($C) {
            if (null === self::$C) {
                self::$C = self::getData('canonicalComposition');
            }

            $r = self::recompose($r);
        }
        if (null !== $mbEncoding) {
            mb_internal_encoding($mbEncoding);
        }

        return $r;
    }

    private static function recompose($s)
    {
        $ASCII = self::$ASCII;
        $compMap = self::$C;
        $combClass = self::$cC;
        $ulenMask = self::$ulenMask;

        $result = $tail = '';

        $i = $s[0] < "\x80" ? 1 : $ulenMask[$s[0] & "\xF0"];
        $len = \strlen($s);

        $lastUchr = substr($s, 0, $i);
        $lastUcls = isset($combClass[$lastUchr]) ? 256 : 0;

        while ($i < $len) {
            if ($s[$i] < "\x80") {
                // ASCII chars

                if ($tail) {
                    $lastUchr .= $tail;
                    $tail = '';
                }

                if ($j = strspn($s, $ASCII, $i + 1)) {
                    $lastUchr .= substr($s, $i, $j);
                    $i += $j;
                }

                $result .= $lastUchr;
                $lastUchr = $s[$i];
                $lastUcls = 0;
                ++$i;
                continue;
            }

            $ulen = $ulenMask[$s[$i] & "\xF0"];
            $uchr = substr($s, $i, $ulen);

            if ($lastUchr < "\xE1\x84\x80" || "\xE1\x84\x92" < $lastUchr
                || $uchr < "\xE1\x85\xA1" || "\xE1\x85\xB5" < $uchr
                || $lastUcls) {
                // Table lookup and combining chars composition

                $ucls = $combClass[$uchr] ?? 0;

                if (isset($compMap[$lastUchr.$uchr]) && (!$lastUcls || $lastUcls < $ucls)) {
                    $lastUchr = $compMap[$lastUchr.$uchr];
                } elseif ($lastUcls = $ucls) {
                    $tail .= $uchr;
                } else {
                    if ($tail) {
                        $lastUchr .= $tail;
                        $tail = '';
                    }

                    $result .= $lastUchr;
                    $lastUchr = $uchr;
                }
            } else {
                // Hangul chars

                $L = \ord($lastUchr[2]) - 0x80;
                $V = \ord($uchr[2]) - 0xA1;
                $T = 0;

                $uchr = substr($s, $i + $ulen, 3);

                if ("\xE1\x86\xA7" <= $uchr && $uchr <= "\xE1\x87\x82") {
                    $T = \ord($uchr[2]) - 0xA7;
                    0 > $T && $T += 0x40;
                    $ulen += 3;
                }

                $L = 0xAC00 + ($L * 21 + $V) * 28 + $T;
                $lastUchr = \chr(0xE0 | $L >> 12).\chr(0x80 | $L >> 6 & 0x3F).\chr(0x80 | $L & 0x3F);
            }

            $i += $ulen;
        }

        return $result.$lastUchr.$tail;
    }

    private static function decompose($s, $c)
    {
        $result = '';

        $ASCII = self::$ASCII;
        $decompMap = self::$D;
        $combClass = self::$cC;
        $ulenMask = self::$ulenMask;
        if ($c) {
            $compatMap = self::$KD;
        }

        $c = [];
        $i = 0;
        $len = \strlen($s);

        while ($i < $len) {
            if ($s[$i] < "\x80") {
                // ASCII chars

                if ($c) {
                    ksort($c);
                    $result .= implode('', $c);
                    $c = [];
                }

                $j = 1 + strspn($s, $ASCII, $i + 1);
                $result .= substr($s, $i, $j);
                $i += $j;
                continue;
            }

            $ulen = $ulenMask[$s[$i] & "\xF0"];
            $uchr = substr($s, $i, $ulen);
            $i += $ulen;

            if ($uchr < "\xEA\xB0\x80" || "\xED\x9E\xA3" < $uchr) {
                // Table lookup

                if ($uchr !== $j = $compatMap[$uchr] ?? ($decompMap[$uchr] ?? $uchr)) {
                    $uchr = $j;

                    $j = \strlen($uchr);
                    $ulen = $uchr[0] < "\x80" ? 1 : $ulenMask[$uchr[0] & "\xF0"];

                    if ($ulen != $j) {
                        // Put trailing chars in $s

                        $j -= $ulen;
                        $i -= $j;

                        if (0 > $i) {
                            $s = str_repeat(' ', -$i).$s;
                            $len -= $i;
                            $i = 0;
                        }

                        while ($j--) {
                            $s[$i + $j] = $uchr[$ulen + $j];
                        }

                        $uchr = substr($uchr, 0, $ulen);
                    }
                }
                if (isset($combClass[$uchr])) {
                    // Combining chars, for sorting

                    if (!isset($c[$combClass[$uchr]])) {
                        $c[$combClass[$uchr]] = '';
                    }
                    $c[$combClass[$uchr]] .= $uchr;
                    continue;
                }
            } else {
                // Hangul chars

                $uchr = unpack('C*', $uchr);
                $j = (($uchr[1] - 224) << 12) + (($uchr[2] - 128) << 6) + $uchr[3] - 0xAC80;

                $uchr = "\xE1\x84".\chr(0x80 + (int) ($j / 588))
                       ."\xE1\x85".\chr(0xA1 + (int) (($j % 588) / 28));

                if ($j %= 28) {
                    $uchr .= $j < 25
                        ? ("\xE1\x86".\chr(0xA7 + $j))
                        : ("\xE1\x87".\chr(0x67 + $j));
                }
            }
            if ($c) {
                ksort($c);
                $result .= implode('', $c);
                $c = [];
            }

            $result .= $uchr;
        }

        if ($c) {
            ksort($c);
            $result .= implode('', $c);
        }

        return $result;
    }

    private static function getData($file)
    {
        if (file_exists($file = __DIR__.'/Resources/unidata/'.$file.'.php')) {
            return require $file;
        }

        return false;
    }
}
<?php

return array (
  ' ' => ' ',
  '¨' => ' ̈',
  'ª' => 'a',
  '¯' => ' ̄',
  '²' => '2',
  '³' => '3',
  '´' => ' ́',
  'µ' => 'μ',
  '¸' => ' ̧',
  '¹' => '1',
  'º' => 'o',
  '¼' => '1⁄4',
  '½' => '1⁄2',
  '¾' => '3⁄4',
  'Ĳ' => 'IJ',
  'ĳ' => 'ij',
  'Ŀ' => 'L·',
  'ŀ' => 'l·',
  'ŉ' => 'ʼn',
  'ſ' => 's',
  'Ǆ' => 'DŽ',
  'ǅ' => 'Dž',
  'ǆ' => 'dž',
  'Ǉ' => 'LJ',
  'ǈ' => 'Lj',
  'ǉ' => 'lj',
  'Ǌ' => 'NJ',
  'ǋ' => 'Nj',
  'ǌ' => 'nj',
  'Ǳ' => 'DZ',
  'ǲ' => 'Dz',
  'ǳ' => 'dz',
  'ʰ' => 'h',
  'ʱ' => 'ɦ',
  'ʲ' => 'j',
  'ʳ' => 'r',
  'ʴ' => 'ɹ',
  'ʵ' => 'ɻ',
  'ʶ' => 'ʁ',
  'ʷ' => 'w',
  'ʸ' => 'y',
  '˘' => ' ̆',
  '˙' => ' ̇',
  '˚' => ' ̊',
  '˛' => ' ̨',
  '˜' => ' ̃',
  '˝' => ' ̋',
  'ˠ' => 'ɣ',
  'ˡ' => 'l',
  'ˢ' => 's',
  'ˣ' => 'x',
  'ˤ' => 'ʕ',
  'ͺ' => ' ͅ',
  '΄' => ' ́',
  'ϐ' => 'β',
  'ϑ' => 'θ',
  'ϒ' => 'Υ',
  'ϕ' => 'φ',
  'ϖ' => 'π',
  'ϰ' => 'κ',
  'ϱ' => 'ρ',
  'ϲ' => 'ς',
  'ϴ' => 'Θ',
  'ϵ' => 'ε',
  'Ϲ' => 'Σ',
  'և' => 'եւ',
  'ٵ' => 'اٴ',
  'ٶ' => 'وٴ',
  'ٷ' => 'ۇٴ',
  'ٸ' => 'يٴ',
  'ำ' => 'ํา',
  'ຳ' => 'ໍາ',
  'ໜ' => 'ຫນ',
  'ໝ' => 'ຫມ',
  '༌' => '་',
  'ཷ' => 'ྲཱྀ',
  'ཹ' => 'ླཱྀ',
  'ჼ' => 'ნ',
  'ᴬ' => 'A',
  'ᴭ' => 'Æ',
  'ᴮ' => 'B',
  'ᴰ' => 'D',
  'ᴱ' => 'E',
  'ᴲ' => 'Ǝ',
  'ᴳ' => 'G',
  'ᴴ' => 'H',
  'ᴵ' => 'I',
  'ᴶ' => 'J',
  'ᴷ' => 'K',
  'ᴸ' => 'L',
  'ᴹ' => 'M',
  'ᴺ' => 'N',
  'ᴼ' => 'O',
  'ᴽ' => 'Ȣ',
  'ᴾ' => 'P',
  'ᴿ' => 'R',
  'ᵀ' => 'T',
  'ᵁ' => 'U',
  'ᵂ' => 'W',
  'ᵃ' => 'a',
  'ᵄ' => 'ɐ',
  'ᵅ' => 'ɑ',
  'ᵆ' => 'ᴂ',
  'ᵇ' => 'b',
  'ᵈ' => 'd',
  'ᵉ' => 'e',
  'ᵊ' => 'ə',
  'ᵋ' => 'ɛ',
  'ᵌ' => 'ɜ',
  'ᵍ' => 'g',
  'ᵏ' => 'k',
  'ᵐ' => 'm',
  'ᵑ' => 'ŋ',
  'ᵒ' => 'o',
  'ᵓ' => 'ɔ',
  'ᵔ' => 'ᴖ',
  'ᵕ' => 'ᴗ',
  'ᵖ' => 'p',
  'ᵗ' => 't',
  'ᵘ' => 'u',
  'ᵙ' => 'ᴝ',
  'ᵚ' => 'ɯ',
  'ᵛ' => 'v',
  'ᵜ' => 'ᴥ',
  'ᵝ' => 'β',
  'ᵞ' => 'γ',
  'ᵟ' => 'δ',
  'ᵠ' => 'φ',
  'ᵡ' => 'χ',
  'ᵢ' => 'i',
  'ᵣ' => 'r',
  'ᵤ' => 'u',
  'ᵥ' => 'v',
  'ᵦ' => 'β',
  'ᵧ' => 'γ',
  'ᵨ' => 'ρ',
  'ᵩ' => 'φ',
  'ᵪ' => 'χ',
  'ᵸ' => 'н',
  'ᶛ' => 'ɒ',
  'ᶜ' => 'c',
  'ᶝ' => 'ɕ',
  'ᶞ' => 'ð',
  'ᶟ' => 'ɜ',
  'ᶠ' => 'f',
  'ᶡ' => 'ɟ',
  'ᶢ' => 'ɡ',
  'ᶣ' => 'ɥ',
  'ᶤ' => 'ɨ',
  'ᶥ' => 'ɩ',
  'ᶦ' => 'ɪ',
  'ᶧ' => 'ᵻ',
  'ᶨ' => 'ʝ',
  'ᶩ' => 'ɭ',
  'ᶪ' => 'ᶅ',
  'ᶫ' => 'ʟ',
  'ᶬ' => 'ɱ',
  'ᶭ' => 'ɰ',
  'ᶮ' => 'ɲ',
  'ᶯ' => 'ɳ',
  'ᶰ' => 'ɴ',
  'ᶱ' => 'ɵ',
  'ᶲ' => 'ɸ',
  'ᶳ' => 'ʂ',
  'ᶴ' => 'ʃ',
  'ᶵ' => 'ƫ',
  'ᶶ' => 'ʉ',
  'ᶷ' => 'ʊ',
  'ᶸ' => 'ᴜ',
  'ᶹ' => 'ʋ',
  'ᶺ' => 'ʌ',
  'ᶻ' => 'z',
  'ᶼ' => 'ʐ',
  'ᶽ' => 'ʑ',
  'ᶾ' => 'ʒ',
  'ᶿ' => 'θ',
  'ẚ' => 'aʾ',
  '᾽' => ' ̓',
  '᾿' => ' ̓',
  '῀' => ' ͂',
  '῾' => ' ̔',
  ' ' => ' ',
  ' ' => ' ',
  ' ' => ' ',
  ' ' => ' ',
  ' ' => ' ',
  ' ' => ' ',
  ' ' => ' ',
  ' ' => ' ',
  ' ' => ' ',
  '‑' => '‐',
  '‗' => ' ̳',
  '․' => '.',
  '‥' => '..',
  '…' => '...',
  ' ' => ' ',
  '″' => '′′',
  '‴' => '′′′',
  '‶' => '‵‵',
  '‷' => '‵‵‵',
  '‼' => '!!',
  '‾' => ' ̅',
  '⁇' => '??',
  '⁈' => '?!',
  '⁉' => '!?',
  '⁗' => '′′′′',
  ' ' => ' ',
  '⁰' => '0',
  'ⁱ' => 'i',
  '⁴' => '4',
  '⁵' => '5',
  '⁶' => '6',
  '⁷' => '7',
  '⁸' => '8',
  '⁹' => '9',
  '⁺' => '+',
  '⁻' => '−',
  '⁼' => '=',
  '⁽' => '(',
  '⁾' => ')',
  'ⁿ' => 'n',
  '₀' => '0',
  '₁' => '1',
  '₂' => '2',
  '₃' => '3',
  '₄' => '4',
  '₅' => '5',
  '₆' => '6',
  '₇' => '7',
  '₈' => '8',
  '₉' => '9',
  '₊' => '+',
  '₋' => '−',
  '₌' => '=',
  '₍' => '(',
  '₎' => ')',
  'ₐ' => 'a',
  'ₑ' => 'e',
  'ₒ' => 'o',
  'ₓ' => 'x',
  'ₔ' => 'ə',
  'ₕ' => 'h',
  'ₖ' => 'k',
  'ₗ' => 'l',
  'ₘ' => 'm',
  'ₙ' => 'n',
  'ₚ' => 'p',
  'ₛ' => 's',
  'ₜ' => 't',
  '₨' => 'Rs',
  '℀' => 'a/c',
  '℁' => 'a/s',
  'ℂ' => 'C',
  '℃' => '°C',
  '℅' => 'c/o',
  '℆' => 'c/u',
  'ℇ' => 'Ɛ',
  '℉' => '°F',
  'ℊ' => 'g',
  'ℋ' => 'H',
  'ℌ' => 'H',
  'ℍ' => 'H',
  'ℎ' => 'h',
  'ℏ' => 'ħ',
  'ℐ' => 'I',
  'ℑ' => 'I',
  'ℒ' => 'L',
  'ℓ' => 'l',
  'ℕ' => 'N',
  '№' => 'No',
  'ℙ' => 'P',
  'ℚ' => 'Q',
  'ℛ' => 'R',
  'ℜ' => 'R',
  'ℝ' => 'R',
  '℠' => 'SM',
  '℡' => 'TEL',
  '™' => 'TM',
  'ℤ' => 'Z',
  'ℨ' => 'Z',
  'ℬ' => 'B',
  'ℭ' => 'C',
  'ℯ' => 'e',
  'ℰ' => 'E',
  'ℱ' => 'F',
  'ℳ' => 'M',
  'ℴ' => 'o',
  'ℵ' => 'א',
  'ℶ' => 'ב',
  'ℷ' => 'ג',
  'ℸ' => 'ד',
  'ℹ' => 'i',
  '℻' => 'FAX',
  'ℼ' => 'π',
  'ℽ' => 'γ',
  'ℾ' => 'Γ',
  'ℿ' => 'Π',
  '⅀' => '∑',
  'ⅅ' => 'D',
  'ⅆ' => 'd',
  'ⅇ' => 'e',
  'ⅈ' => 'i',
  'ⅉ' => 'j',
  '⅐' => '1⁄7',
  '⅑' => '1⁄9',
  '⅒' => '1⁄10',
  '⅓' => '1⁄3',
  '⅔' => '2⁄3',
  '⅕' => '1⁄5',
  '⅖' => '2⁄5',
  '⅗' => '3⁄5',
  '⅘' => '4⁄5',
  '⅙' => '1⁄6',
  '⅚' => '5⁄6',
  '⅛' => '1⁄8',
  '⅜' => '3⁄8',
  '⅝' => '5⁄8',
  '⅞' => '7⁄8',
  '⅟' => '1⁄',
  'Ⅰ' => 'I',
  'Ⅱ' => 'II',
  'Ⅲ' => 'III',
  'Ⅳ' => 'IV',
  'Ⅴ' => 'V',
  'Ⅵ' => 'VI',
  'Ⅶ' => 'VII',
  'Ⅷ' => 'VIII',
  'Ⅸ' => 'IX',
  'Ⅹ' => 'X',
  'Ⅺ' => 'XI',
  'Ⅻ' => 'XII',
  'Ⅼ' => 'L',
  'Ⅽ' => 'C',
  'Ⅾ' => 'D',
  'Ⅿ' => 'M',
  'ⅰ' => 'i',
  'ⅱ' => 'ii',
  'ⅲ' => 'iii',
  'ⅳ' => 'iv',
  'ⅴ' => 'v',
  'ⅵ' => 'vi',
  'ⅶ' => 'vii',
  'ⅷ' => 'viii',
  'ⅸ' => 'ix',
  'ⅹ' => 'x',
  'ⅺ' => 'xi',
  'ⅻ' => 'xii',
  'ⅼ' => 'l',
  'ⅽ' => 'c',
  'ⅾ' => 'd',
  'ⅿ' => 'm',
  '↉' => '0⁄3',
  '∬' => '∫∫',
  '∭' => '∫∫∫',
  '∯' => '∮∮',
  '∰' => '∮∮∮',
  '①' => '1',
  '②' => '2',
  '③' => '3',
  '④' => '4',
  '⑤' => '5',
  '⑥' => '6',
  '⑦' => '7',
  '⑧' => '8',
  '⑨' => '9',
  '⑩' => '10',
  '⑪' => '11',
  '⑫' => '12',
  '⑬' => '13',
  '⑭' => '14',
  '⑮' => '15',
  '⑯' => '16',
  '⑰' => '17',
  '⑱' => '18',
  '⑲' => '19',
  '⑳' => '20',
  '⑴' => '(1)',
  '⑵' => '(2)',
  '⑶' => '(3)',
  '⑷' => '(4)',
  '⑸' => '(5)',
  '⑹' => '(6)',
  '⑺' => '(7)',
  '⑻' => '(8)',
  '⑼' => '(9)',
  '⑽' => '(10)',
  '⑾' => '(11)',
  '⑿' => '(12)',
  '⒀' => '(13)',
  '⒁' => '(14)',
  '⒂' => '(15)',
  '⒃' => '(16)',
  '⒄' => '(17)',
  '⒅' => '(18)',
  '⒆' => '(19)',
  '⒇' => '(20)',
  '⒈' => '1.',
  '⒉' => '2.',
  '⒊' => '3.',
  '⒋' => '4.',
  '⒌' => '5.',
  '⒍' => '6.',
  '⒎' => '7.',
  '⒏' => '8.',
  '⒐' => '9.',
  '⒑' => '10.',
  '⒒' => '11.',
  '⒓' => '12.',
  '⒔' => '13.',
  '⒕' => '14.',
  '⒖' => '15.',
  '⒗' => '16.',
  '⒘' => '17.',
  '⒙' => '18.',
  '⒚' => '19.',
  '⒛' => '20.',
  '⒜' => '(a)',
  '⒝' => '(b)',
  '⒞' => '(c)',
  '⒟' => '(d)',
  '⒠' => '(e)',
  '⒡' => '(f)',
  '⒢' => '(g)',
  '⒣' => '(h)',
  '⒤' => '(i)',
  '⒥' => '(j)',
  '⒦' => '(k)',
  '⒧' => '(l)',
  '⒨' => '(m)',
  '⒩' => '(n)',
  '⒪' => '(o)',
  '⒫' => '(p)',
  '⒬' => '(q)',
  '⒭' => '(r)',
  '⒮' => '(s)',
  '⒯' => '(t)',
  '⒰' => '(u)',
  '⒱' => '(v)',
  '⒲' => '(w)',
  '⒳' => '(x)',
  '⒴' => '(y)',
  '⒵' => '(z)',
  'Ⓐ' => 'A',
  'Ⓑ' => 'B',
  'Ⓒ' => 'C',
  'Ⓓ' => 'D',
  'Ⓔ' => 'E',
  'Ⓕ' => 'F',
  'Ⓖ' => 'G',
  'Ⓗ' => 'H',
  'Ⓘ' => 'I',
  'Ⓙ' => 'J',
  'Ⓚ' => 'K',
  'Ⓛ' => 'L',
  'Ⓜ' => 'M',
  'Ⓝ' => 'N',
  'Ⓞ' => 'O',
  'Ⓟ' => 'P',
  'Ⓠ' => 'Q',
  'Ⓡ' => 'R',
  'Ⓢ' => 'S',
  'Ⓣ' => 'T',
  'Ⓤ' => 'U',
  'Ⓥ' => 'V',
  'Ⓦ' => 'W',
  'Ⓧ' => 'X',
  'Ⓨ' => 'Y',
  'Ⓩ' => 'Z',
  'ⓐ' => 'a',
  'ⓑ' => 'b',
  'ⓒ' => 'c',
  'ⓓ' => 'd',
  'ⓔ' => 'e',
  'ⓕ' => 'f',
  'ⓖ' => 'g',
  'ⓗ' => 'h',
  'ⓘ' => 'i',
  'ⓙ' => 'j',
  'ⓚ' => 'k',
  'ⓛ' => 'l',
  'ⓜ' => 'm',
  'ⓝ' => 'n',
  'ⓞ' => 'o',
  'ⓟ' => 'p',
  'ⓠ' => 'q',
  'ⓡ' => 'r',
  'ⓢ' => 's',
  'ⓣ' => 't',
  'ⓤ' => 'u',
  'ⓥ' => 'v',
  'ⓦ' => 'w',
  'ⓧ' => 'x',
  'ⓨ' => 'y',
  'ⓩ' => 'z',
  '⓪' => '0',
  '⨌' => '∫∫∫∫',
  '⩴' => '::=',
  '⩵' => '==',
  '⩶' => '===',
  'ⱼ' => 'j',
  'ⱽ' => 'V',
  'ⵯ' => 'ⵡ',
  '⺟' => '母',
  '⻳' => '龟',
  '⼀' => '一',
  '⼁' => '丨',
  '⼂' => '丶',
  '⼃' => '丿',
  '⼄' => '乙',
  '⼅' => '亅',
  '⼆' => '二',
  '⼇' => '亠',
  '⼈' => '人',
  '⼉' => '儿',
  '⼊' => '入',
  '⼋' => '八',
  '⼌' => '冂',
  '⼍' => '冖',
  '⼎' => '冫',
  '⼏' => '几',
  '⼐' => '凵',
  '⼑' => '刀',
  '⼒' => '力',
  '⼓' => '勹',
  '⼔' => '匕',
  '⼕' => '匚',
  '⼖' => '匸',
  '⼗' => '十',
  '⼘' => '卜',
  '⼙' => '卩',
  '⼚' => '厂',
  '⼛' => '厶',
  '⼜' => '又',
  '⼝' => '口',
  '⼞' => '囗',
  '⼟' => '土',
  '⼠' => '士',
  '⼡' => '夂',
  '⼢' => '夊',
  '⼣' => '夕',
  '⼤' => '大',
  '⼥' => '女',
  '⼦' => '子',
  '⼧' => '宀',
  '⼨' => '寸',
  '⼩' => '小',
  '⼪' => '尢',
  '⼫' => '尸',
  '⼬' => '屮',
  '⼭' => '山',
  '⼮' => '巛',
  '⼯' => '工',
  '⼰' => '己',
  '⼱' => '巾',
  '⼲' => '干',
  '⼳' => '幺',
  '⼴' => '广',
  '⼵' => '廴',
  '⼶' => '廾',
  '⼷' => '弋',
  '⼸' => '弓',
  '⼹' => '彐',
  '⼺' => '彡',
  '⼻' => '彳',
  '⼼' => '心',
  '⼽' => '戈',
  '⼾' => '戶',
  '⼿' => '手',
  '⽀' => '支',
  '⽁' => '攴',
  '⽂' => '文',
  '⽃' => '斗',
  '⽄' => '斤',
  '⽅' => '方',
  '⽆' => '无',
  '⽇' => '日',
  '⽈' => '曰',
  '⽉' => '月',
  '⽊' => '木',
  '⽋' => '欠',
  '⽌' => '止',
  '⽍' => '歹',
  '⽎' => '殳',
  '⽏' => '毋',
  '⽐' => '比',
  '⽑' => '毛',
  '⽒' => '氏',
  '⽓' => '气',
  '⽔' => '水',
  '⽕' => '火',
  '⽖' => '爪',
  '⽗' => '父',
  '⽘' => '爻',
  '⽙' => '爿',
  '⽚' => '片',
  '⽛' => '牙',
  '⽜' => '牛',
  '⽝' => '犬',
  '⽞' => '玄',
  '⽟' => '玉',
  '⽠' => '瓜',
  '⽡' => '瓦',
  '⽢' => '甘',
  '⽣' => '生',
  '⽤' => '用',
  '⽥' => '田',
  '⽦' => '疋',
  '⽧' => '疒',
  '⽨' => '癶',
  '⽩' => '白',
  '⽪' => '皮',
  '⽫' => '皿',
  '⽬' => '目',
  '⽭' => '矛',
  '⽮' => '矢',
  '⽯' => '石',
  '⽰' => '示',
  '⽱' => '禸',
  '⽲' => '禾',
  '⽳' => '穴',
  '⽴' => '立',
  '⽵' => '竹',
  '⽶' => '米',
  '⽷' => '糸',
  '⽸' => '缶',
  '⽹' => '网',
  '⽺' => '羊',
  '⽻' => '羽',
  '⽼' => '老',
  '⽽' => '而',
  '⽾' => '耒',
  '⽿' => '耳',
  '⾀' => '聿',
  '⾁' => '肉',
  '⾂' => '臣',
  '⾃' => '自',
  '⾄' => '至',
  '⾅' => '臼',
  '⾆' => '舌',
  '⾇' => '舛',
  '⾈' => '舟',
  '⾉' => '艮',
  '⾊' => '色',
  '⾋' => '艸',
  '⾌' => '虍',
  '⾍' => '虫',
  '⾎' => '血',
  '⾏' => '行',
  '⾐' => '衣',
  '⾑' => '襾',
  '⾒' => '見',
  '⾓' => '角',
  '⾔' => '言',
  '⾕' => '谷',
  '⾖' => '豆',
  '⾗' => '豕',
  '⾘' => '豸',
  '⾙' => '貝',
  '⾚' => '赤',
  '⾛' => '走',
  '⾜' => '足',
  '⾝' => '身',
  '⾞' => '車',
  '⾟' => '辛',
  '⾠' => '辰',
  '⾡' => '辵',
  '⾢' => '邑',
  '⾣' => '酉',
  '⾤' => '釆',
  '⾥' => '里',
  '⾦' => '金',
  '⾧' => '長',
  '⾨' => '門',
  '⾩' => '阜',
  '⾪' => '隶',
  '⾫' => '隹',
  '⾬' => '雨',
  '⾭' => '靑',
  '⾮' => '非',
  '⾯' => '面',
  '⾰' => '革',
  '⾱' => '韋',
  '⾲' => '韭',
  '⾳' => '音',
  '⾴' => '頁',
  '⾵' => '風',
  '⾶' => '飛',
  '⾷' => '食',
  '⾸' => '首',
  '⾹' => '香',
  '⾺' => '馬',
  '⾻' => '骨',
  '⾼' => '高',
  '⾽' => '髟',
  '⾾' => '鬥',
  '⾿' => '鬯',
  '⿀' => '鬲',
  '⿁' => '鬼',
  '⿂' => '魚',
  '⿃' => '鳥',
  '⿄' => '鹵',
  '⿅' => '鹿',
  '⿆' => '麥',
  '⿇' => '麻',
  '⿈' => '黃',
  '⿉' => '黍',
  '⿊' => '黑',
  '⿋' => '黹',
  '⿌' => '黽',
  '⿍' => '鼎',
  '⿎' => '鼓',
  '⿏' => '鼠',
  '⿐' => '鼻',
  '⿑' => '齊',
  '⿒' => '齒',
  '⿓' => '龍',
  '⿔' => '龜',
  '⿕' => '龠',
  '　' => ' ',
  '〶' => '〒',
  '〸' => '十',
  '〹' => '卄',
  '〺' => '卅',
  '゛' => ' ゙',
  '゜' => ' ゚',
  'ゟ' => 'より',
  'ヿ' => 'コト',
  'ㄱ' => 'ᄀ',
  'ㄲ' => 'ᄁ',
  'ㄳ' => 'ᆪ',
  'ㄴ' => 'ᄂ',
  'ㄵ' => 'ᆬ',
  'ㄶ' => 'ᆭ',
  'ㄷ' => 'ᄃ',
  'ㄸ' => 'ᄄ',
  'ㄹ' => 'ᄅ',
  'ㄺ' => 'ᆰ',
  'ㄻ' => 'ᆱ',
  'ㄼ' => 'ᆲ',
  'ㄽ' => 'ᆳ',
  'ㄾ' => 'ᆴ',
  'ㄿ' => 'ᆵ',
  'ㅀ' => 'ᄚ',
  'ㅁ' => 'ᄆ',
  'ㅂ' => 'ᄇ',
  'ㅃ' => 'ᄈ',
  'ㅄ' => 'ᄡ',
  'ㅅ' => 'ᄉ',
  'ㅆ' => 'ᄊ',
  'ㅇ' => 'ᄋ',
  'ㅈ' => 'ᄌ',
  'ㅉ' => 'ᄍ',
  'ㅊ' => 'ᄎ',
  'ㅋ' => 'ᄏ',
  'ㅌ' => 'ᄐ',
  'ㅍ' => 'ᄑ',
  'ㅎ' => 'ᄒ',
  'ㅏ' => 'ᅡ',
  'ㅐ' => 'ᅢ',
  'ㅑ' => 'ᅣ',
  'ㅒ' => 'ᅤ',
  'ㅓ' => 'ᅥ',
  'ㅔ' => 'ᅦ',
  'ㅕ' => 'ᅧ',
  'ㅖ' => 'ᅨ',
  'ㅗ' => 'ᅩ',
  'ㅘ' => 'ᅪ',
  'ㅙ' => 'ᅫ',
  'ㅚ' => 'ᅬ',
  'ㅛ' => 'ᅭ',
  'ㅜ' => 'ᅮ',
  'ㅝ' => 'ᅯ',
  'ㅞ' => 'ᅰ',
  'ㅟ' => 'ᅱ',
  'ㅠ' => 'ᅲ',
  'ㅡ' => 'ᅳ',
  'ㅢ' => 'ᅴ',
  'ㅣ' => 'ᅵ',
  'ㅤ' => 'ᅠ',
  'ㅥ' => 'ᄔ',
  'ㅦ' => 'ᄕ',
  'ㅧ' => 'ᇇ',
  'ㅨ' => 'ᇈ',
  'ㅩ' => 'ᇌ',
  'ㅪ' => 'ᇎ',
  'ㅫ' => 'ᇓ',
  'ㅬ' => 'ᇗ',
  'ㅭ' => 'ᇙ',
  'ㅮ' => 'ᄜ',
  'ㅯ' => 'ᇝ',
  'ㅰ' => 'ᇟ',
  'ㅱ' => 'ᄝ',
  'ㅲ' => 'ᄞ',
  'ㅳ' => 'ᄠ',
  'ㅴ' => 'ᄢ',
  'ㅵ' => 'ᄣ',
  'ㅶ' => 'ᄧ',
  'ㅷ' => 'ᄩ',
  'ㅸ' => 'ᄫ',
  'ㅹ' => 'ᄬ',
  'ㅺ' => 'ᄭ',
  'ㅻ' => 'ᄮ',
  'ㅼ' => 'ᄯ',
  'ㅽ' => 'ᄲ',
  'ㅾ' => 'ᄶ',
  'ㅿ' => 'ᅀ',
  'ㆀ' => 'ᅇ',
  'ㆁ' => 'ᅌ',
  'ㆂ' => 'ᇱ',
  'ㆃ' => 'ᇲ',
  'ㆄ' => 'ᅗ',
  'ㆅ' => 'ᅘ',
  'ㆆ' => 'ᅙ',
  'ㆇ' => 'ᆄ',
  'ㆈ' => 'ᆅ',
  'ㆉ' => 'ᆈ',
  'ㆊ' => 'ᆑ',
  'ㆋ' => 'ᆒ',
  'ㆌ' => 'ᆔ',
  'ㆍ' => 'ᆞ',
  'ㆎ' => 'ᆡ',
  '㆒' => '一',
  '㆓' => '二',
  '㆔' => '三',
  '㆕' => '四',
  '㆖' => '上',
  '㆗' => '中',
  '㆘' => '下',
  '㆙' => '甲',
  '㆚' => '乙',
  '㆛' => '丙',
  '㆜' => '丁',
  '㆝' => '天',
  '㆞' => '地',
  '㆟' => '人',
  '㈀' => '(ᄀ)',
  '㈁' => '(ᄂ)',
  '㈂' => '(ᄃ)',
  '㈃' => '(ᄅ)',
  '㈄' => '(ᄆ)',
  '㈅' => '(ᄇ)',
  '㈆' => '(ᄉ)',
  '㈇' => '(ᄋ)',
  '㈈' => '(ᄌ)',
  '㈉' => '(ᄎ)',
  '㈊' => '(ᄏ)',
  '㈋' => '(ᄐ)',
  '㈌' => '(ᄑ)',
  '㈍' => '(ᄒ)',
  '㈎' => '(가)',
  '㈏' => '(나)',
  '㈐' => '(다)',
  '㈑' => '(라)',
  '㈒' => '(마)',
  '㈓' => '(바)',
  '㈔' => '(사)',
  '㈕' => '(아)',
  '㈖' => '(자)',
  '㈗' => '(차)',
  '㈘' => '(카)',
  '㈙' => '(타)',
  '㈚' => '(파)',
  '㈛' => '(하)',
  '㈜' => '(주)',
  '㈝' => '(오전)',
  '㈞' => '(오후)',
  '㈠' => '(一)',
  '㈡' => '(二)',
  '㈢' => '(三)',
  '㈣' => '(四)',
  '㈤' => '(五)',
  '㈥' => '(六)',
  '㈦' => '(七)',
  '㈧' => '(八)',
  '㈨' => '(九)',
  '㈩' => '(十)',
  '㈪' => '(月)',
  '㈫' => '(火)',
  '㈬' => '(水)',
  '㈭' => '(木)',
  '㈮' => '(金)',
  '㈯' => '(土)',
  '㈰' => '(日)',
  '㈱' => '(株)',
  '㈲' => '(有)',
  '㈳' => '(社)',
  '㈴' => '(名)',
  '㈵' => '(特)',
  '㈶' => '(財)',
  '㈷' => '(祝)',
  '㈸' => '(労)',
  '㈹' => '(代)',
  '㈺' => '(呼)',
  '㈻' => '(学)',
  '㈼' => '(監)',
  '㈽' => '(企)',
  '㈾' => '(資)',
  '㈿' => '(協)',
  '㉀' => '(祭)',
  '㉁' => '(休)',
  '㉂' => '(自)',
  '㉃' => '(至)',
  '㉄' => '問',
  '㉅' => '幼',
  '㉆' => '文',
  '㉇' => '箏',
  '㉐' => 'PTE',
  '㉑' => '21',
  '㉒' => '22',
  '㉓' => '23',
  '㉔' => '24',
  '㉕' => '25',
  '㉖' => '26',
  '㉗' => '27',
  '㉘' => '28',
  '㉙' => '29',
  '㉚' => '30',
  '㉛' => '31',
  '㉜' => '32',
  '㉝' => '33',
  '㉞' => '34',
  '㉟' => '35',
  '㉠' => 'ᄀ',
  '㉡' => 'ᄂ',
  '㉢' => 'ᄃ',
  '㉣' => 'ᄅ',
  '㉤' => 'ᄆ',
  '㉥' => 'ᄇ',
  '㉦' => 'ᄉ',
  '㉧' => 'ᄋ',
  '㉨' => 'ᄌ',
  '㉩' => 'ᄎ',
  '㉪' => 'ᄏ',
  '㉫' => 'ᄐ',
  '㉬' => 'ᄑ',
  '㉭' => 'ᄒ',
  '㉮' => '가',
  '㉯' => '나',
  '㉰' => '다',
  '㉱' => '라',
  '㉲' => '마',
  '㉳' => '바',
  '㉴' => '사',
  '㉵' => '아',
  '㉶' => '자',
  '㉷' => '차',
  '㉸' => '카',
  '㉹' => '타',
  '㉺' => '파',
  '㉻' => '하',
  '㉼' => '참고',
  '㉽' => '주의',
  '㉾' => '우',
  '㊀' => '一',
  '㊁' => '二',
  '㊂' => '三',
  '㊃' => '四',
  '㊄' => '五',
  '㊅' => '六',
  '㊆' => '七',
  '㊇' => '八',
  '㊈' => '九',
  '㊉' => '十',
  '㊊' => '月',
  '㊋' => '火',
  '㊌' => '水',
  '㊍' => '木',
  '㊎' => '金',
  '㊏' => '土',
  '㊐' => '日',
  '㊑' => '株',
  '㊒' => '有',
  '㊓' => '社',
  '㊔' => '名',
  '㊕' => '特',
  '㊖' => '財',
  '㊗' => '祝',
  '㊘' => '労',
  '㊙' => '秘',
  '㊚' => '男',
  '㊛' => '女',
  '㊜' => '適',
  '㊝' => '優',
  '㊞' => '印',
  '㊟' => '注',
  '㊠' => '項',
  '㊡' => '休',
  '㊢' => '写',
  '㊣' => '正',
  '㊤' => '上',
  '㊥' => '中',
  '㊦' => '下',
  '㊧' => '左',
  '㊨' => '右',
  '㊩' => '医',
  '㊪' => '宗',
  '㊫' => '学',
  '㊬' => '監',
  '㊭' => '企',
  '㊮' => '資',
  '㊯' => '協',
  '㊰' => '夜',
  '㊱' => '36',
  '㊲' => '37',
  '㊳' => '38',
  '㊴' => '39',
  '㊵' => '40',
  '㊶' => '41',
  '㊷' => '42',
  '㊸' => '43',
  '㊹' => '44',
  '㊺' => '45',
  '㊻' => '46',
  '㊼' => '47',
  '㊽' => '48',
  '㊾' => '49',
  '㊿' => '50',
  '㋀' => '1月',
  '㋁' => '2月',
  '㋂' => '3月',
  '㋃' => '4月',
  '㋄' => '5月',
  '㋅' => '6月',
  '㋆' => '7月',
  '㋇' => '8月',
  '㋈' => '9月',
  '㋉' => '10月',
  '㋊' => '11月',
  '㋋' => '12月',
  '㋌' => 'Hg',
  '㋍' => 'erg',
  '㋎' => 'eV',
  '㋏' => 'LTD',
  '㋐' => 'ア',
  '㋑' => 'イ',
  '㋒' => 'ウ',
  '㋓' => 'エ',
  '㋔' => 'オ',
  '㋕' => 'カ',
  '㋖' => 'キ',
  '㋗' => 'ク',
  '㋘' => 'ケ',
  '㋙' => 'コ',
  '㋚' => 'サ',
  '㋛' => 'シ',
  '㋜' => 'ス',
  '㋝' => 'セ',
  '㋞' => 'ソ',
  '㋟' => 'タ',
  '㋠' => 'チ',
  '㋡' => 'ツ',
  '㋢' => 'テ',
  '㋣' => 'ト',
  '㋤' => 'ナ',
  '㋥' => 'ニ',
  '㋦' => 'ヌ',
  '㋧' => 'ネ',
  '㋨' => 'ノ',
  '㋩' => 'ハ',
  '㋪' => 'ヒ',
  '㋫' => 'フ',
  '㋬' => 'ヘ',
  '㋭' => 'ホ',
  '㋮' => 'マ',
  '㋯' => 'ミ',
  '㋰' => 'ム',
  '㋱' => 'メ',
  '㋲' => 'モ',
  '㋳' => 'ヤ',
  '㋴' => 'ユ',
  '㋵' => 'ヨ',
  '㋶' => 'ラ',
  '㋷' => 'リ',
  '㋸' => 'ル',
  '㋹' => 'レ',
  '㋺' => 'ロ',
  '㋻' => 'ワ',
  '㋼' => 'ヰ',
  '㋽' => 'ヱ',
  '㋾' => 'ヲ',
  '㋿' => '令和',
  '㌀' => 'アパート',
  '㌁' => 'アルファ',
  '㌂' => 'アンペア',
  '㌃' => 'アール',
  '㌄' => 'イニング',
  '㌅' => 'インチ',
  '㌆' => 'ウォン',
  '㌇' => 'エスクード',
  '㌈' => 'エーカー',
  '㌉' => 'オンス',
  '㌊' => 'オーム',
  '㌋' => 'カイリ',
  '㌌' => 'カラット',
  '㌍' => 'カロリー',
  '㌎' => 'ガロン',
  '㌏' => 'ガンマ',
  '㌐' => 'ギガ',
  '㌑' => 'ギニー',
  '㌒' => 'キュリー',
  '㌓' => 'ギルダー',
  '㌔' => 'キロ',
  '㌕' => 'キログラム',
  '㌖' => 'キロメートル',
  '㌗' => 'キロワット',
  '㌘' => 'グラム',
  '㌙' => 'グラムトン',
  '㌚' => 'クルゼイロ',
  '㌛' => 'クローネ',
  '㌜' => 'ケース',
  '㌝' => 'コルナ',
  '㌞' => 'コーポ',
  '㌟' => 'サイクル',
  '㌠' => 'サンチーム',
  '㌡' => 'シリング',
  '㌢' => 'センチ',
  '㌣' => 'セント',
  '㌤' => 'ダース',
  '㌥' => 'デシ',
  '㌦' => 'ドル',
  '㌧' => 'トン',
  '㌨' => 'ナノ',
  '㌩' => 'ノット',
  '㌪' => 'ハイツ',
  '㌫' => 'パーセント',
  '㌬' => 'パーツ',
  '㌭' => 'バーレル',
  '㌮' => 'ピアストル',
  '㌯' => 'ピクル',
  '㌰' => 'ピコ',
  '㌱' => 'ビル',
  '㌲' => 'ファラッド',
  '㌳' => 'フィート',
  '㌴' => 'ブッシェル',
  '㌵' => 'フラン',
  '㌶' => 'ヘクタール',
  '㌷' => 'ペソ',
  '㌸' => 'ペニヒ',
  '㌹' => 'ヘルツ',
  '㌺' => 'ペンス',
  '㌻' => 'ページ',
  '㌼' => 'ベータ',
  '㌽' => 'ポイント',
  '㌾' => 'ボルト',
  '㌿' => 'ホン',
  '㍀' => 'ポンド',
  '㍁' => 'ホール',
  '㍂' => 'ホーン',
  '㍃' => 'マイクロ',
  '㍄' => 'マイル',
  '㍅' => 'マッハ',
  '㍆' => 'マルク',
  '㍇' => 'マンション',
  '㍈' => 'ミクロン',
  '㍉' => 'ミリ',
  '㍊' => 'ミリバール',
  '㍋' => 'メガ',
  '㍌' => 'メガトン',
  '㍍' => 'メートル',
  '㍎' => 'ヤード',
  '㍏' => 'ヤール',
  '㍐' => 'ユアン',
  '㍑' => 'リットル',
  '㍒' => 'リラ',
  '㍓' => 'ルピー',
  '㍔' => 'ルーブル',
  '㍕' => 'レム',
  '㍖' => 'レントゲン',
  '㍗' => 'ワット',
  '㍘' => '0点',
  '㍙' => '1点',
  '㍚' => '2点',
  '㍛' => '3点',
  '㍜' => '4点',
  '㍝' => '5点',
  '㍞' => '6点',
  '㍟' => '7点',
  '㍠' => '8点',
  '㍡' => '9点',
  '㍢' => '10点',
  '㍣' => '11点',
  '㍤' => '12点',
  '㍥' => '13点',
  '㍦' => '14点',
  '㍧' => '15点',
  '㍨' => '16点',
  '㍩' => '17点',
  '㍪' => '18点',
  '㍫' => '19点',
  '㍬' => '20点',
  '㍭' => '21点',
  '㍮' => '22点',
  '㍯' => '23点',
  '㍰' => '24点',
  '㍱' => 'hPa',
  '㍲' => 'da',
  '㍳' => 'AU',
  '㍴' => 'bar',
  '㍵' => 'oV',
  '㍶' => 'pc',
  '㍷' => 'dm',
  '㍸' => 'dm²',
  '㍹' => 'dm³',
  '㍺' => 'IU',
  '㍻' => '平成',
  '㍼' => '昭和',
  '㍽' => '大正',
  '㍾' => '明治',
  '㍿' => '株式会社',
  '㎀' => 'pA',
  '㎁' => 'nA',
  '㎂' => 'μA',
  '㎃' => 'mA',
  '㎄' => 'kA',
  '㎅' => 'KB',
  '㎆' => 'MB',
  '㎇' => 'GB',
  '㎈' => 'cal',
  '㎉' => 'kcal',
  '㎊' => 'pF',
  '㎋' => 'nF',
  '㎌' => 'μF',
  '㎍' => 'μg',
  '㎎' => 'mg',
  '㎏' => 'kg',
  '㎐' => 'Hz',
  '㎑' => 'kHz',
  '㎒' => 'MHz',
  '㎓' => 'GHz',
  '㎔' => 'THz',
  '㎕' => 'μℓ',
  '㎖' => 'mℓ',
  '㎗' => 'dℓ',
  '㎘' => 'kℓ',
  '㎙' => 'fm',
  '㎚' => 'nm',
  '㎛' => 'μm',
  '㎜' => 'mm',
  '㎝' => 'cm',
  '㎞' => 'km',
  '㎟' => 'mm²',
  '㎠' => 'cm²',
  '㎡' => 'm²',
  '㎢' => 'km²',
  '㎣' => 'mm³',
  '㎤' => 'cm³',
  '㎥' => 'm³',
  '㎦' => 'km³',
  '㎧' => 'm∕s',
  '㎨' => 'm∕s²',
  '㎩' => 'Pa',
  '㎪' => 'kPa',
  '㎫' => 'MPa',
  '㎬' => 'GPa',
  '㎭' => 'rad',
  '㎮' => 'rad∕s',
  '㎯' => 'rad∕s²',
  '㎰' => 'ps',
  '㎱' => 'ns',
  '㎲' => 'μs',
  '㎳' => 'ms',
  '㎴' => 'pV',
  '㎵' => 'nV',
  '㎶' => 'μV',
  '㎷' => 'mV',
  '㎸' => 'kV',
  '㎹' => 'MV',
  '㎺' => 'pW',
  '㎻' => 'nW',
  '㎼' => 'μW',
  '㎽' => 'mW',
  '㎾' => 'kW',
  '㎿' => 'MW',
  '㏀' => 'kΩ',
  '㏁' => 'MΩ',
  '㏂' => 'a.m.',
  '㏃' => 'Bq',
  '㏄' => 'cc',
  '㏅' => 'cd',
  '㏆' => 'C∕kg',
  '㏇' => 'Co.',
  '㏈' => 'dB',
  '㏉' => 'Gy',
  '㏊' => 'ha',
  '㏋' => 'HP',
  '㏌' => 'in',
  '㏍' => 'KK',
  '㏎' => 'KM',
  '㏏' => 'kt',
  '㏐' => 'lm',
  '㏑' => 'ln',
  '㏒' => 'log',
  '㏓' => 'lx',
  '㏔' => 'mb',
  '㏕' => 'mil',
  '㏖' => 'mol',
  '㏗' => 'PH',
  '㏘' => 'p.m.',
  '㏙' => 'PPM',
  '㏚' => 'PR',
  '㏛' => 'sr',
  '㏜' => 'Sv',
  '㏝' => 'Wb',
  '㏞' => 'V∕m',
  '㏟' => 'A∕m',
  '㏠' => '1日',
  '㏡' => '2日',
  '㏢' => '3日',
  '㏣' => '4日',
  '㏤' => '5日',
  '㏥' => '6日',
  '㏦' => '7日',
  '㏧' => '8日',
  '㏨' => '9日',
  '㏩' => '10日',
  '㏪' => '11日',
  '㏫' => '12日',
  '㏬' => '13日',
  '㏭' => '14日',
  '㏮' => '15日',
  '㏯' => '16日',
  '㏰' => '17日',
  '㏱' => '18日',
  '㏲' => '19日',
  '㏳' => '20日',
  '㏴' => '21日',
  '㏵' => '22日',
  '㏶' => '23日',
  '㏷' => '24日',
  '㏸' => '25日',
  '㏹' => '26日',
  '㏺' => '27日',
  '㏻' => '28日',
  '㏼' => '29日',
  '㏽' => '30日',
  '㏾' => '31日',
  '㏿' => 'gal',
  'ꚜ' => 'ъ',
  'ꚝ' => 'ь',
  'ꝰ' => 'ꝯ',
  'ꟲ' => 'C',
  'ꟳ' => 'F',
  'ꟴ' => 'Q',
  'ꟸ' => 'Ħ',
  'ꟹ' => 'œ',
  'ꭜ' => 'ꜧ',
  'ꭝ' => 'ꬷ',
  'ꭞ' => 'ɫ',
  'ꭟ' => 'ꭒ',
  'ꭩ' => 'ʍ',
  'ﬀ' => 'ff',
  'ﬁ' => 'fi',
  'ﬂ' => 'fl',
  'ﬃ' => 'ffi',
  'ﬄ' => 'ffl',
  'ﬅ' => 'ſt',
  'ﬆ' => 'st',
  'ﬓ' => 'մն',
  'ﬔ' => 'մե',
  'ﬕ' => 'մի',
  'ﬖ' => 'վն',
  'ﬗ' => 'մխ',
  'ﬠ' => 'ע',
  'ﬡ' => 'א',
  'ﬢ' => 'ד',
  'ﬣ' => 'ה',
  'ﬤ' => 'כ',
  'ﬥ' => 'ל',
  'ﬦ' => 'ם',
  'ﬧ' => 'ר',
  'ﬨ' => 'ת',
  '﬩' => '+',
  'ﭏ' => 'אל',
  'ﭐ' => 'ٱ',
  'ﭑ' => 'ٱ',
  'ﭒ' => 'ٻ',
  'ﭓ' => 'ٻ',
  'ﭔ' => 'ٻ',
  'ﭕ' => 'ٻ',
  'ﭖ' => 'پ',
  'ﭗ' => 'پ',
  'ﭘ' => 'پ',
  'ﭙ' => 'پ',
  'ﭚ' => 'ڀ',
  'ﭛ' => 'ڀ',
  'ﭜ' => 'ڀ',
  'ﭝ' => 'ڀ',
  'ﭞ' => 'ٺ',
  'ﭟ' => 'ٺ',
  'ﭠ' => 'ٺ',
  'ﭡ' => 'ٺ',
  'ﭢ' => 'ٿ',
  'ﭣ' => 'ٿ',
  'ﭤ' => 'ٿ',
  'ﭥ' => 'ٿ',
  'ﭦ' => 'ٹ',
  'ﭧ' => 'ٹ',
  'ﭨ' => 'ٹ',
  'ﭩ' => 'ٹ',
  'ﭪ' => 'ڤ',
  'ﭫ' => 'ڤ',
  'ﭬ' => 'ڤ',
  'ﭭ' => 'ڤ',
  'ﭮ' => 'ڦ',
  'ﭯ' => 'ڦ',
  'ﭰ' => 'ڦ',
  'ﭱ' => 'ڦ',
  'ﭲ' => 'ڄ',
  'ﭳ' => 'ڄ',
  'ﭴ' => 'ڄ',
  'ﭵ' => 'ڄ',
  'ﭶ' => 'ڃ',
  'ﭷ' => 'ڃ',
  'ﭸ' => 'ڃ',
  'ﭹ' => 'ڃ',
  'ﭺ' => 'چ',
  'ﭻ' => 'چ',
  'ﭼ' => 'چ',
  'ﭽ' => 'چ',
  'ﭾ' => 'ڇ',
  'ﭿ' => 'ڇ',
  'ﮀ' => 'ڇ',
  'ﮁ' => 'ڇ',
  'ﮂ' => 'ڍ',
  'ﮃ' => 'ڍ',
  'ﮄ' => 'ڌ',
  'ﮅ' => 'ڌ',
  'ﮆ' => 'ڎ',
  'ﮇ' => 'ڎ',
  'ﮈ' => 'ڈ',
  'ﮉ' => 'ڈ',
  'ﮊ' => 'ژ',
  'ﮋ' => 'ژ',
  'ﮌ' => 'ڑ',
  'ﮍ' => 'ڑ',
  'ﮎ' => 'ک',
  'ﮏ' => 'ک',
  'ﮐ' => 'ک',
  'ﮑ' => 'ک',
  'ﮒ' => 'گ',
  'ﮓ' => 'گ',
  'ﮔ' => 'گ',
  'ﮕ' => 'گ',
  'ﮖ' => 'ڳ',
  'ﮗ' => 'ڳ',
  'ﮘ' => 'ڳ',
  'ﮙ' => 'ڳ',
  'ﮚ' => 'ڱ',
  'ﮛ' => 'ڱ',
  'ﮜ' => 'ڱ',
  'ﮝ' => 'ڱ',
  'ﮞ' => 'ں',
  'ﮟ' => 'ں',
  'ﮠ' => 'ڻ',
  'ﮡ' => 'ڻ',
  'ﮢ' => 'ڻ',
  'ﮣ' => 'ڻ',
  'ﮤ' => 'ۀ',
  'ﮥ' => 'ۀ',
  'ﮦ' => 'ہ',
  'ﮧ' => 'ہ',
  'ﮨ' => 'ہ',
  'ﮩ' => 'ہ',
  'ﮪ' => 'ھ',
  'ﮫ' => 'ھ',
  'ﮬ' => 'ھ',
  'ﮭ' => 'ھ',
  'ﮮ' => 'ے',
  'ﮯ' => 'ے',
  'ﮰ' => 'ۓ',
  'ﮱ' => 'ۓ',
  'ﯓ' => 'ڭ',
  'ﯔ' => 'ڭ',
  'ﯕ' => 'ڭ',
  'ﯖ' => 'ڭ',
  'ﯗ' => 'ۇ',
  'ﯘ' => 'ۇ',
  'ﯙ' => 'ۆ',
  'ﯚ' => 'ۆ',
  'ﯛ' => 'ۈ',
  'ﯜ' => 'ۈ',
  'ﯝ' => 'ٷ',
  'ﯞ' => 'ۋ',
  'ﯟ' => 'ۋ',
  'ﯠ' => 'ۅ',
  'ﯡ' => 'ۅ',
  'ﯢ' => 'ۉ',
  'ﯣ' => 'ۉ',
  'ﯤ' => 'ې',
  'ﯥ' => 'ې',
  'ﯦ' => 'ې',
  'ﯧ' => 'ې',
  'ﯨ' => 'ى',
  'ﯩ' => 'ى',
  'ﯪ' => 'ئا',
  'ﯫ' => 'ئا',
  'ﯬ' => 'ئە',
  'ﯭ' => 'ئە',
  'ﯮ' => 'ئو',
  'ﯯ' => 'ئو',
  'ﯰ' => 'ئۇ',
  'ﯱ' => 'ئۇ',
  'ﯲ' => 'ئۆ',
  'ﯳ' => 'ئۆ',
  'ﯴ' => 'ئۈ',
  'ﯵ' => 'ئۈ',
  'ﯶ' => 'ئې',
  'ﯷ' => 'ئې',
  'ﯸ' => 'ئې',
  'ﯹ' => 'ئى',
  'ﯺ' => 'ئى',
  'ﯻ' => 'ئى',
  'ﯼ' => 'ی',
  'ﯽ' => 'ی',
  'ﯾ' => 'ی',
  'ﯿ' => 'ی',
  'ﰀ' => 'ئج',
  'ﰁ' => 'ئح',
  'ﰂ' => 'ئم',
  'ﰃ' => 'ئى',
  'ﰄ' => 'ئي',
  'ﰅ' => 'بج',
  'ﰆ' => 'بح',
  'ﰇ' => 'بخ',
  'ﰈ' => 'بم',
  'ﰉ' => 'بى',
  'ﰊ' => 'بي',
  'ﰋ' => 'تج',
  'ﰌ' => 'تح',
  'ﰍ' => 'تخ',
  'ﰎ' => 'تم',
  'ﰏ' => 'تى',
  'ﰐ' => 'تي',
  'ﰑ' => 'ثج',
  'ﰒ' => 'ثم',
  'ﰓ' => 'ثى',
  'ﰔ' => 'ثي',
  'ﰕ' => 'جح',
  'ﰖ' => 'جم',
  'ﰗ' => 'حج',
  'ﰘ' => 'حم',
  'ﰙ' => 'خج',
  'ﰚ' => 'خح',
  'ﰛ' => 'خم',
  'ﰜ' => 'سج',
  'ﰝ' => 'سح',
  'ﰞ' => 'سخ',
  'ﰟ' => 'سم',
  'ﰠ' => 'صح',
  'ﰡ' => 'صم',
  'ﰢ' => 'ضج',
  'ﰣ' => 'ضح',
  'ﰤ' => 'ضخ',
  'ﰥ' => 'ضم',
  'ﰦ' => 'طح',
  'ﰧ' => 'طم',
  'ﰨ' => 'ظم',
  'ﰩ' => 'عج',
  'ﰪ' => 'عم',
  'ﰫ' => 'غج',
  'ﰬ' => 'غم',
  'ﰭ' => 'فج',
  'ﰮ' => 'فح',
  'ﰯ' => 'فخ',
  'ﰰ' => 'فم',
  'ﰱ' => 'فى',
  'ﰲ' => 'في',
  'ﰳ' => 'قح',
  'ﰴ' => 'قم',
  'ﰵ' => 'قى',
  'ﰶ' => 'قي',
  'ﰷ' => 'كا',
  'ﰸ' => 'كج',
  'ﰹ' => 'كح',
  'ﰺ' => 'كخ',
  'ﰻ' => 'كل',
  'ﰼ' => 'كم',
  'ﰽ' => 'كى',
  'ﰾ' => 'كي',
  'ﰿ' => 'لج',
  'ﱀ' => 'لح',
  'ﱁ' => 'لخ',
  'ﱂ' => 'لم',
  'ﱃ' => 'لى',
  'ﱄ' => 'لي',
  'ﱅ' => 'مج',
  'ﱆ' => 'مح',
  'ﱇ' => 'مخ',
  'ﱈ' => 'مم',
  'ﱉ' => 'مى',
  'ﱊ' => 'مي',
  'ﱋ' => 'نج',
  'ﱌ' => 'نح',
  'ﱍ' => 'نخ',
  'ﱎ' => 'نم',
  'ﱏ' => 'نى',
  'ﱐ' => 'ني',
  'ﱑ' => 'هج',
  'ﱒ' => 'هم',
  'ﱓ' => 'هى',
  'ﱔ' => 'هي',
  'ﱕ' => 'يج',
  'ﱖ' => 'يح',
  'ﱗ' => 'يخ',
  'ﱘ' => 'يم',
  'ﱙ' => 'يى',
  'ﱚ' => 'يي',
  'ﱛ' => 'ذٰ',
  'ﱜ' => 'رٰ',
  'ﱝ' => 'ىٰ',
  'ﱞ' => ' ٌّ',
  'ﱟ' => ' ٍّ',
  'ﱠ' => ' َّ',
  'ﱡ' => ' ُّ',
  'ﱢ' => ' ِّ',
  'ﱣ' => ' ّٰ',
  'ﱤ' => 'ئر',
  'ﱥ' => 'ئز',
  'ﱦ' => 'ئم',
  'ﱧ' => 'ئن',
  'ﱨ' => 'ئى',
  'ﱩ' => 'ئي',
  'ﱪ' => 'بر',
  'ﱫ' => 'بز',
  'ﱬ' => 'بم',
  'ﱭ' => 'بن',
  'ﱮ' => 'بى',
  'ﱯ' => 'بي',
  'ﱰ' => 'تر',
  'ﱱ' => 'تز',
  'ﱲ' => 'تم',
  'ﱳ' => 'تن',
  'ﱴ' => 'تى',
  'ﱵ' => 'تي',
  'ﱶ' => 'ثر',
  'ﱷ' => 'ثز',
  'ﱸ' => 'ثم',
  'ﱹ' => 'ثن',
  'ﱺ' => 'ثى',
  'ﱻ' => 'ثي',
  'ﱼ' => 'فى',
  'ﱽ' => 'في',
  'ﱾ' => 'قى',
  'ﱿ' => 'قي',
  'ﲀ' => 'كا',
  'ﲁ' => 'كل',
  'ﲂ' => 'كم',
  'ﲃ' => 'كى',
  'ﲄ' => 'كي',
  'ﲅ' => 'لم',
  'ﲆ' => 'لى',
  'ﲇ' => 'لي',
  'ﲈ' => 'ما',
  'ﲉ' => 'مم',
  'ﲊ' => 'نر',
  'ﲋ' => 'نز',
  'ﲌ' => 'نم',
  'ﲍ' => 'نن',
  'ﲎ' => 'نى',
  'ﲏ' => 'ني',
  'ﲐ' => 'ىٰ',
  'ﲑ' => 'ير',
  'ﲒ' => 'يز',
  'ﲓ' => 'يم',
  'ﲔ' => 'ين',
  'ﲕ' => 'يى',
  'ﲖ' => 'يي',
  'ﲗ' => 'ئج',
  'ﲘ' => 'ئح',
  'ﲙ' => 'ئخ',
  'ﲚ' => 'ئم',
  'ﲛ' => 'ئه',
  'ﲜ' => 'بج',
  'ﲝ' => 'بح',
  'ﲞ' => 'بخ',
  'ﲟ' => 'بم',
  'ﲠ' => 'به',
  'ﲡ' => 'تج',
  'ﲢ' => 'تح',
  'ﲣ' => 'تخ',
  'ﲤ' => 'تم',
  'ﲥ' => 'ته',
  'ﲦ' => 'ثم',
  'ﲧ' => 'جح',
  'ﲨ' => 'جم',
  'ﲩ' => 'حج',
  'ﲪ' => 'حم',
  'ﲫ' => 'خج',
  'ﲬ' => 'خم',
  'ﲭ' => 'سج',
  'ﲮ' => 'سح',
  'ﲯ' => 'سخ',
  'ﲰ' => 'سم',
  'ﲱ' => 'صح',
  'ﲲ' => 'صخ',
  'ﲳ' => 'صم',
  'ﲴ' => 'ضج',
  'ﲵ' => 'ضح',
  'ﲶ' => 'ضخ',
  'ﲷ' => 'ضم',
  'ﲸ' => 'طح',
  'ﲹ' => 'ظم',
  'ﲺ' => 'عج',
  'ﲻ' => 'عم',
  'ﲼ' => 'غج',
  'ﲽ' => 'غم',
  'ﲾ' => 'فج',
  'ﲿ' => 'فح',
  'ﳀ' => 'فخ',
  'ﳁ' => 'فم',
  'ﳂ' => 'قح',
  'ﳃ' => 'قم',
  'ﳄ' => 'كج',
  'ﳅ' => 'كح',
  'ﳆ' => 'كخ',
  'ﳇ' => 'كل',
  'ﳈ' => 'كم',
  'ﳉ' => 'لج',
  'ﳊ' => 'لح',
  'ﳋ' => 'لخ',
  'ﳌ' => 'لم',
  'ﳍ' => 'له',
  'ﳎ' => 'مج',
  'ﳏ' => 'مح',
  'ﳐ' => 'مخ',
  'ﳑ' => 'مم',
  'ﳒ' => 'نج',
  'ﳓ' => 'نح',
  'ﳔ' => 'نخ',
  'ﳕ' => 'نم',
  'ﳖ' => 'نه',
  'ﳗ' => 'هج',
  'ﳘ' => 'هم',
  'ﳙ' => 'هٰ',
  'ﳚ' => 'يج',
  'ﳛ' => 'يح',
  'ﳜ' => 'يخ',
  'ﳝ' => 'يم',
  'ﳞ' => 'يه',
  'ﳟ' => 'ئم',
  'ﳠ' => 'ئه',
  'ﳡ' => 'بم',
  'ﳢ' => 'به',
  'ﳣ' => 'تم',
  'ﳤ' => 'ته',
  'ﳥ' => 'ثم',
  'ﳦ' => 'ثه',
  'ﳧ' => 'سم',
  'ﳨ' => 'سه',
  'ﳩ' => 'شم',
  'ﳪ' => 'شه',
  'ﳫ' => 'كل',
  'ﳬ' => 'كم',
  'ﳭ' => 'لم',
  'ﳮ' => 'نم',
  'ﳯ' => 'نه',
  'ﳰ' => 'يم',
  'ﳱ' => 'يه',
  'ﳲ' => 'ـَّ',
  'ﳳ' => 'ـُّ',
  'ﳴ' => 'ـِّ',
  'ﳵ' => 'طى',
  'ﳶ' => 'طي',
  'ﳷ' => 'عى',
  'ﳸ' => 'عي',
  'ﳹ' => 'غى',
  'ﳺ' => 'غي',
  'ﳻ' => 'سى',
  'ﳼ' => 'سي',
  'ﳽ' => 'شى',
  'ﳾ' => 'شي',
  'ﳿ' => 'حى',
  'ﴀ' => 'حي',
  'ﴁ' => 'جى',
  'ﴂ' => 'جي',
  'ﴃ' => 'خى',
  'ﴄ' => 'خي',
  'ﴅ' => 'صى',
  'ﴆ' => 'صي',
  'ﴇ' => 'ضى',
  'ﴈ' => 'ضي',
  'ﴉ' => 'شج',
  'ﴊ' => 'شح',
  'ﴋ' => 'شخ',
  'ﴌ' => 'شم',
  'ﴍ' => 'شر',
  'ﴎ' => 'سر',
  'ﴏ' => 'صر',
  'ﴐ' => 'ضر',
  'ﴑ' => 'طى',
  'ﴒ' => 'طي',
  'ﴓ' => 'عى',
  'ﴔ' => 'عي',
  'ﴕ' => 'غى',
  'ﴖ' => 'غي',
  'ﴗ' => 'سى',
  'ﴘ' => 'سي',
  'ﴙ' => 'شى',
  'ﴚ' => 'شي',
  'ﴛ' => 'حى',
  'ﴜ' => 'حي',
  'ﴝ' => 'جى',
  'ﴞ' => 'جي',
  'ﴟ' => 'خى',
  'ﴠ' => 'خي',
  'ﴡ' => 'صى',
  'ﴢ' => 'صي',
  'ﴣ' => 'ضى',
  'ﴤ' => 'ضي',
  'ﴥ' => 'شج',
  'ﴦ' => 'شح',
  'ﴧ' => 'شخ',
  'ﴨ' => 'شم',
  'ﴩ' => 'شر',
  'ﴪ' => 'سر',
  'ﴫ' => 'صر',
  'ﴬ' => 'ضر',
  'ﴭ' => 'شج',
  'ﴮ' => 'شح',
  'ﴯ' => 'شخ',
  'ﴰ' => 'شم',
  'ﴱ' => 'سه',
  'ﴲ' => 'شه',
  'ﴳ' => 'طم',
  'ﴴ' => 'سج',
  'ﴵ' => 'سح',
  'ﴶ' => 'سخ',
  'ﴷ' => 'شج',
  'ﴸ' => 'شح',
  'ﴹ' => 'شخ',
  'ﴺ' => 'طم',
  'ﴻ' => 'ظم',
  'ﴼ' => 'اً',
  'ﴽ' => 'اً',
  'ﵐ' => 'تجم',
  'ﵑ' => 'تحج',
  'ﵒ' => 'تحج',
  'ﵓ' => 'تحم',
  'ﵔ' => 'تخم',
  'ﵕ' => 'تمج',
  'ﵖ' => 'تمح',
  'ﵗ' => 'تمخ',
  'ﵘ' => 'جمح',
  'ﵙ' => 'جمح',
  'ﵚ' => 'حمي',
  'ﵛ' => 'حمى',
  'ﵜ' => 'سحج',
  'ﵝ' => 'سجح',
  'ﵞ' => 'سجى',
  'ﵟ' => 'سمح',
  'ﵠ' => 'سمح',
  'ﵡ' => 'سمج',
  'ﵢ' => 'سمم',
  'ﵣ' => 'سمم',
  'ﵤ' => 'صحح',
  'ﵥ' => 'صحح',
  'ﵦ' => 'صمم',
  'ﵧ' => 'شحم',
  'ﵨ' => 'شحم',
  'ﵩ' => 'شجي',
  'ﵪ' => 'شمخ',
  'ﵫ' => 'شمخ',
  'ﵬ' => 'شمم',
  'ﵭ' => 'شمم',
  'ﵮ' => 'ضحى',
  'ﵯ' => 'ضخم',
  'ﵰ' => 'ضخم',
  'ﵱ' => 'طمح',
  'ﵲ' => 'طمح',
  'ﵳ' => 'طمم',
  'ﵴ' => 'طمي',
  'ﵵ' => 'عجم',
  'ﵶ' => 'عمم',
  'ﵷ' => 'عمم',
  'ﵸ' => 'عمى',
  'ﵹ' => 'غمم',
  'ﵺ' => 'غمي',
  'ﵻ' => 'غمى',
  'ﵼ' => 'فخم',
  'ﵽ' => 'فخم',
  'ﵾ' => 'قمح',
  'ﵿ' => 'قمم',
  'ﶀ' => 'لحم',
  'ﶁ' => 'لحي',
  'ﶂ' => 'لحى',
  'ﶃ' => 'لجج',
  'ﶄ' => 'لجج',
  'ﶅ' => 'لخم',
  'ﶆ' => 'لخم',
  'ﶇ' => 'لمح',
  'ﶈ' => 'لمح',
  'ﶉ' => 'محج',
  'ﶊ' => 'محم',
  'ﶋ' => 'محي',
  'ﶌ' => 'مجح',
  'ﶍ' => 'مجم',
  'ﶎ' => 'مخج',
  'ﶏ' => 'مخم',
  'ﶒ' => 'مجخ',
  'ﶓ' => 'همج',
  'ﶔ' => 'همم',
  'ﶕ' => 'نحم',
  'ﶖ' => 'نحى',
  'ﶗ' => 'نجم',
  'ﶘ' => 'نجم',
  'ﶙ' => 'نجى',
  'ﶚ' => 'نمي',
  'ﶛ' => 'نمى',
  'ﶜ' => 'يمم',
  'ﶝ' => 'يمم',
  'ﶞ' => 'بخي',
  'ﶟ' => 'تجي',
  'ﶠ' => 'تجى',
  'ﶡ' => 'تخي',
  'ﶢ' => 'تخى',
  'ﶣ' => 'تمي',
  'ﶤ' => 'تمى',
  'ﶥ' => 'جمي',
  'ﶦ' => 'جحى',
  'ﶧ' => 'جمى',
  'ﶨ' => 'سخى',
  'ﶩ' => 'صحي',
  'ﶪ' => 'شحي',
  'ﶫ' => 'ضحي',
  'ﶬ' => 'لجي',
  'ﶭ' => 'لمي',
  'ﶮ' => 'يحي',
  'ﶯ' => 'يجي',
  'ﶰ' => 'يمي',
  'ﶱ' => 'ممي',
  'ﶲ' => 'قمي',
  'ﶳ' => 'نحي',
  'ﶴ' => 'قمح',
  'ﶵ' => 'لحم',
  'ﶶ' => 'عمي',
  'ﶷ' => 'كمي',
  'ﶸ' => 'نجح',
  'ﶹ' => 'مخي',
  'ﶺ' => 'لجم',
  'ﶻ' => 'كمم',
  'ﶼ' => 'لجم',
  'ﶽ' => 'نجح',
  'ﶾ' => 'جحي',
  'ﶿ' => 'حجي',
  'ﷀ' => 'مجي',
  'ﷁ' => 'فمي',
  'ﷂ' => 'بحي',
  'ﷃ' => 'كمم',
  'ﷄ' => 'عجم',
  'ﷅ' => 'صمم',
  'ﷆ' => 'سخي',
  'ﷇ' => 'نجي',
  'ﷰ' => 'صلے',
  'ﷱ' => 'قلے',
  'ﷲ' => 'الله',
  'ﷳ' => 'اكبر',
  'ﷴ' => 'محمد',
  'ﷵ' => 'صلعم',
  'ﷶ' => 'رسول',
  'ﷷ' => 'عليه',
  'ﷸ' => 'وسلم',
  'ﷹ' => 'صلى',
  'ﷺ' => 'صلى الله عليه وسلم',
  'ﷻ' => 'جل جلاله',
  '﷼' => 'ریال',
  '︐' => ',',
  '︑' => '、',
  '︒' => '。',
  '︓' => ':',
  '︔' => ';',
  '︕' => '!',
  '︖' => '?',
  '︗' => '〖',
  '︘' => '〗',
  '︙' => '…',
  '︰' => '‥',
  '︱' => '—',
  '︲' => '–',
  '︳' => '_',
  '︴' => '_',
  '︵' => '(',
  '︶' => ')',
  '︷' => '{',
  '︸' => '}',
  '︹' => '〔',
  '︺' => '〕',
  '︻' => '【',
  '︼' => '】',
  '︽' => '《',
  '︾' => '》',
  '︿' => '〈',
  '﹀' => '〉',
  '﹁' => '「',
  '﹂' => '」',
  '﹃' => '『',
  '﹄' => '』',
  '﹇' => '[',
  '﹈' => ']',
  '﹉' => '‾',
  '﹊' => '‾',
  '﹋' => '‾',
  '﹌' => '‾',
  '﹍' => '_',
  '﹎' => '_',
  '﹏' => '_',
  '﹐' => ',',
  '﹑' => '、',
  '﹒' => '.',
  '﹔' => ';',
  '﹕' => ':',
  '﹖' => '?',
  '﹗' => '!',
  '﹘' => '—',
  '﹙' => '(',
  '﹚' => ')',
  '﹛' => '{',
  '﹜' => '}',
  '﹝' => '〔',
  '﹞' => '〕',
  '﹟' => '#',
  '﹠' => '&',
  '﹡' => '*',
  '﹢' => '+',
  '﹣' => '-',
  '﹤' => '<',
  '﹥' => '>',
  '﹦' => '=',
  '﹨' => '\\',
  '﹩' => '$',
  '﹪' => '%',
  '﹫' => '@',
  'ﹰ' => ' ً',
  'ﹱ' => 'ـً',
  'ﹲ' => ' ٌ',
  'ﹴ' => ' ٍ',
  'ﹶ' => ' َ',
  'ﹷ' => 'ـَ',
  'ﹸ' => ' ُ',
  'ﹹ' => 'ـُ',
  'ﹺ' => ' ِ',
  'ﹻ' => 'ـِ',
  'ﹼ' => ' ّ',
  'ﹽ' => 'ـّ',
  'ﹾ' => ' ْ',
  'ﹿ' => 'ـْ',
  'ﺀ' => 'ء',
  'ﺁ' => 'آ',
  'ﺂ' => 'آ',
  'ﺃ' => 'أ',
  'ﺄ' => 'أ',
  'ﺅ' => 'ؤ',
  'ﺆ' => 'ؤ',
  'ﺇ' => 'إ',
  'ﺈ' => 'إ',
  'ﺉ' => 'ئ',
  'ﺊ' => 'ئ',
  'ﺋ' => 'ئ',
  'ﺌ' => 'ئ',
  'ﺍ' => 'ا',
  'ﺎ' => 'ا',
  'ﺏ' => 'ب',
  'ﺐ' => 'ب',
  'ﺑ' => 'ب',
  'ﺒ' => 'ب',
  'ﺓ' => 'ة',
  'ﺔ' => 'ة',
  'ﺕ' => 'ت',
  'ﺖ' => 'ت',
  'ﺗ' => 'ت',
  'ﺘ' => 'ت',
  'ﺙ' => 'ث',
  'ﺚ' => 'ث',
  'ﺛ' => 'ث',
  'ﺜ' => 'ث',
  'ﺝ' => 'ج',
  'ﺞ' => 'ج',
  'ﺟ' => 'ج',
  'ﺠ' => 'ج',
  'ﺡ' => 'ح',
  'ﺢ' => 'ح',
  'ﺣ' => 'ح',
  'ﺤ' => 'ح',
  'ﺥ' => 'خ',
  'ﺦ' => 'خ',
  'ﺧ' => 'خ',
  'ﺨ' => 'خ',
  'ﺩ' => 'د',
  'ﺪ' => 'د',
  'ﺫ' => 'ذ',
  'ﺬ' => 'ذ',
  'ﺭ' => 'ر',
  'ﺮ' => 'ر',
  'ﺯ' => 'ز',
  'ﺰ' => 'ز',
  'ﺱ' => 'س',
  'ﺲ' => 'س',
  'ﺳ' => 'س',
  'ﺴ' => 'س',
  'ﺵ' => 'ش',
  'ﺶ' => 'ش',
  'ﺷ' => 'ش',
  'ﺸ' => 'ش',
  'ﺹ' => 'ص',
  'ﺺ' => 'ص',
  'ﺻ' => 'ص',
  'ﺼ' => 'ص',
  'ﺽ' => 'ض',
  'ﺾ' => 'ض',
  'ﺿ' => 'ض',
  'ﻀ' => 'ض',
  'ﻁ' => 'ط',
  'ﻂ' => 'ط',
  'ﻃ' => 'ط',
  'ﻄ' => 'ط',
  'ﻅ' => 'ظ',
  'ﻆ' => 'ظ',
  'ﻇ' => 'ظ',
  'ﻈ' => 'ظ',
  'ﻉ' => 'ع',
  'ﻊ' => 'ع',
  'ﻋ' => 'ع',
  'ﻌ' => 'ع',
  'ﻍ' => 'غ',
  'ﻎ' => 'غ',
  'ﻏ' => 'غ',
  'ﻐ' => 'غ',
  'ﻑ' => 'ف',
  'ﻒ' => 'ف',
  'ﻓ' => 'ف',
  'ﻔ' => 'ف',
  'ﻕ' => 'ق',
  'ﻖ' => 'ق',
  'ﻗ' => 'ق',
  'ﻘ' => 'ق',
  'ﻙ' => 'ك',
  'ﻚ' => 'ك',
  'ﻛ' => 'ك',
  'ﻜ' => 'ك',
  'ﻝ' => 'ل',
  'ﻞ' => 'ل',
  'ﻟ' => 'ل',
  'ﻠ' => 'ل',
  'ﻡ' => 'م',
  'ﻢ' => 'م',
  'ﻣ' => 'م',
  'ﻤ' => 'م',
  'ﻥ' => 'ن',
  'ﻦ' => 'ن',
  'ﻧ' => 'ن',
  'ﻨ' => 'ن',
  'ﻩ' => 'ه',
  'ﻪ' => 'ه',
  'ﻫ' => 'ه',
  'ﻬ' => 'ه',
  'ﻭ' => 'و',
  'ﻮ' => 'و',
  'ﻯ' => 'ى',
  'ﻰ' => 'ى',
  'ﻱ' => 'ي',
  'ﻲ' => 'ي',
  'ﻳ' => 'ي',
  'ﻴ' => 'ي',
  'ﻵ' => 'لآ',
  'ﻶ' => 'لآ',
  'ﻷ' => 'لأ',
  'ﻸ' => 'لأ',
  'ﻹ' => 'لإ',
  'ﻺ' => 'لإ',
  'ﻻ' => 'لا',
  'ﻼ' => 'لا',
  '！' => '!',
  '＂' => '"',
  '＃' => '#',
  '＄' => '$',
  '％' => '%',
  '＆' => '&',
  '＇' => '\'',
  '（' => '(',
  '）' => ')',
  '＊' => '*',
  '＋' => '+',
  '，' => ',',
  '－' => '-',
  '．' => '.',
  '／' => '/',
  '０' => '0',
  '１' => '1',
  '２' => '2',
  '３' => '3',
  '４' => '4',
  '５' => '5',
  '６' => '6',
  '７' => '7',
  '８' => '8',
  '９' => '9',
  '：' => ':',
  '；' => ';',
  '＜' => '<',
  '＝' => '=',
  '＞' => '>',
  '？' => '?',
  '＠' => '@',
  'Ａ' => 'A',
  'Ｂ' => 'B',
  'Ｃ' => 'C',
  'Ｄ' => 'D',
  'Ｅ' => 'E',
  'Ｆ' => 'F',
  'Ｇ' => 'G',
  'Ｈ' => 'H',
  'Ｉ' => 'I',
  'Ｊ' => 'J',
  'Ｋ' => 'K',
  'Ｌ' => 'L',
  'Ｍ' => 'M',
  'Ｎ' => 'N',
  'Ｏ' => 'O',
  'Ｐ' => 'P',
  'Ｑ' => 'Q',
  'Ｒ' => 'R',
  'Ｓ' => 'S',
  'Ｔ' => 'T',
  'Ｕ' => 'U',
  'Ｖ' => 'V',
  'Ｗ' => 'W',
  'Ｘ' => 'X',
  'Ｙ' => 'Y',
  'Ｚ' => 'Z',
  '［' => '[',
  '＼' => '\\',
  '］' => ']',
  '＾' => '^',
  '＿' => '_',
  '｀' => '`',
  'ａ' => 'a',
  'ｂ' => 'b',
  'ｃ' => 'c',
  'ｄ' => 'd',
  'ｅ' => 'e',
  'ｆ' => 'f',
  'ｇ' => 'g',
  'ｈ' => 'h',
  'ｉ' => 'i',
  'ｊ' => 'j',
  'ｋ' => 'k',
  'ｌ' => 'l',
  'ｍ' => 'm',
  'ｎ' => 'n',
  'ｏ' => 'o',
  'ｐ' => 'p',
  'ｑ' => 'q',
  'ｒ' => 'r',
  'ｓ' => 's',
  'ｔ' => 't',
  'ｕ' => 'u',
  'ｖ' => 'v',
  'ｗ' => 'w',
  'ｘ' => 'x',
  'ｙ' => 'y',
  'ｚ' => 'z',
  '｛' => '{',
  '｜' => '|',
  '｝' => '}',
  '～' => '~',
  '｟' => '⦅',
  '｠' => '⦆',
  '｡' => '。',
  '｢' => '「',
  '｣' => '」',
  '､' => '、',
  '･' => '・',
  'ｦ' => 'ヲ',
  'ｧ' => 'ァ',
  'ｨ' => 'ィ',
  'ｩ' => 'ゥ',
  'ｪ' => 'ェ',
  'ｫ' => 'ォ',
  'ｬ' => 'ャ',
  'ｭ' => 'ュ',
  'ｮ' => 'ョ',
  'ｯ' => 'ッ',
  'ｰ' => 'ー',
  'ｱ' => 'ア',
  'ｲ' => 'イ',
  'ｳ' => 'ウ',
  'ｴ' => 'エ',
  'ｵ' => 'オ',
  'ｶ' => 'カ',
  'ｷ' => 'キ',
  'ｸ' => 'ク',
  'ｹ' => 'ケ',
  'ｺ' => 'コ',
  'ｻ' => 'サ',
  'ｼ' => 'シ',
  'ｽ' => 'ス',
  'ｾ' => 'セ',
  'ｿ' => 'ソ',
  'ﾀ' => 'タ',
  'ﾁ' => 'チ',
  'ﾂ' => 'ツ',
  'ﾃ' => 'テ',
  'ﾄ' => 'ト',
  'ﾅ' => 'ナ',
  'ﾆ' => 'ニ',
  'ﾇ' => 'ヌ',
  'ﾈ' => 'ネ',
  'ﾉ' => 'ノ',
  'ﾊ' => 'ハ',
  'ﾋ' => 'ヒ',
  'ﾌ' => 'フ',
  'ﾍ' => 'ヘ',
  'ﾎ' => 'ホ',
  'ﾏ' => 'マ',
  'ﾐ' => 'ミ',
  'ﾑ' => 'ム',
  'ﾒ' => 'メ',
  'ﾓ' => 'モ',
  'ﾔ' => 'ヤ',
  'ﾕ' => 'ユ',
  'ﾖ' => 'ヨ',
  'ﾗ' => 'ラ',
  'ﾘ' => 'リ',
  'ﾙ' => 'ル',
  'ﾚ' => 'レ',
  'ﾛ' => 'ロ',
  'ﾜ' => 'ワ',
  'ﾝ' => 'ン',
  'ﾞ' => '゙',
  'ﾟ' => '゚',
  'ﾠ' => 'ㅤ',
  'ﾡ' => 'ㄱ',
  'ﾢ' => 'ㄲ',
  'ﾣ' => 'ㄳ',
  'ﾤ' => 'ㄴ',
  'ﾥ' => 'ㄵ',
  'ﾦ' => 'ㄶ',
  'ﾧ' => 'ㄷ',
  'ﾨ' => 'ㄸ',
  'ﾩ' => 'ㄹ',
  'ﾪ' => 'ㄺ',
  'ﾫ' => 'ㄻ',
  'ﾬ' => 'ㄼ',
  'ﾭ' => 'ㄽ',
  'ﾮ' => 'ㄾ',
  'ﾯ' => 'ㄿ',
  'ﾰ' => 'ㅀ',
  'ﾱ' => 'ㅁ',
  'ﾲ' => 'ㅂ',
  'ﾳ' => 'ㅃ',
  'ﾴ' => 'ㅄ',
  'ﾵ' => 'ㅅ',
  'ﾶ' => 'ㅆ',
  'ﾷ' => 'ㅇ',
  'ﾸ' => 'ㅈ',
  'ﾹ' => 'ㅉ',
  'ﾺ' => 'ㅊ',
  'ﾻ' => 'ㅋ',
  'ﾼ' => 'ㅌ',
  'ﾽ' => 'ㅍ',
  'ﾾ' => 'ㅎ',
  'ￂ' => 'ㅏ',
  'ￃ' => 'ㅐ',
  'ￄ' => 'ㅑ',
  'ￅ' => 'ㅒ',
  'ￆ' => 'ㅓ',
  'ￇ' => 'ㅔ',
  'ￊ' => 'ㅕ',
  'ￋ' => 'ㅖ',
  'ￌ' => 'ㅗ',
  'ￍ' => 'ㅘ',
  'ￎ' => 'ㅙ',
  'ￏ' => 'ㅚ',
  'ￒ' => 'ㅛ',
  'ￓ' => 'ㅜ',
  'ￔ' => 'ㅝ',
  'ￕ' => 'ㅞ',
  'ￖ' => 'ㅟ',
  'ￗ' => 'ㅠ',
  'ￚ' => 'ㅡ',
  'ￛ' => 'ㅢ',
  'ￜ' => 'ㅣ',
  '￠' => '¢',
  '￡' => '£',
  '￢' => '¬',
  '￣' => '¯',
  '￤' => '¦',
  '￥' => '¥',
  '￦' => '₩',
  '￨' => '│',
  '￩' => '←',
  '￪' => '↑',
  '￫' => '→',
  '￬' => '↓',
  '￭' => '■',
  '￮' => '○',
  '𐞁' => 'ː',
  '𐞂' => 'ˑ',
  '𐞃' => 'æ',
  '𐞄' => 'ʙ',
  '𐞅' => 'ɓ',
  '𐞇' => 'ʣ',
  '𐞈' => 'ꭦ',
  '𐞉' => 'ʥ',
  '𐞊' => 'ʤ',
  '𐞋' => 'ɖ',
  '𐞌' => 'ɗ',
  '𐞍' => 'ᶑ',
  '𐞎' => 'ɘ',
  '𐞏' => 'ɞ',
  '𐞐' => 'ʩ',
  '𐞑' => 'ɤ',
  '𐞒' => 'ɢ',
  '𐞓' => 'ɠ',
  '𐞔' => 'ʛ',
  '𐞕' => 'ħ',
  '𐞖' => 'ʜ',
  '𐞗' => 'ɧ',
  '𐞘' => 'ʄ',
  '𐞙' => 'ʪ',
  '𐞚' => 'ʫ',
  '𐞛' => 'ɬ',
  '𐞜' => '𝼄',
  '𐞝' => 'ꞎ',
  '𐞞' => 'ɮ',
  '𐞟' => '𝼅',
  '𐞠' => 'ʎ',
  '𐞡' => '𝼆',
  '𐞢' => 'ø',
  '𐞣' => 'ɶ',
  '𐞤' => 'ɷ',
  '𐞥' => 'q',
  '𐞦' => 'ɺ',
  '𐞧' => '𝼈',
  '𐞨' => 'ɽ',
  '𐞩' => 'ɾ',
  '𐞪' => 'ʀ',
  '𐞫' => 'ʨ',
  '𐞬' => 'ʦ',
  '𐞭' => 'ꭧ',
  '𐞮' => 'ʧ',
  '𐞯' => 'ʈ',
  '𐞰' => 'ⱱ',
  '𐞲' => 'ʏ',
  '𐞳' => 'ʡ',
  '𐞴' => 'ʢ',
  '𐞵' => 'ʘ',
  '𐞶' => 'ǀ',
  '𐞷' => 'ǁ',
  '𐞸' => 'ǂ',
  '𐞹' => '𝼊',
  '𐞺' => '𝼞',
  '𝐀' => 'A',
  '𝐁' => 'B',
  '𝐂' => 'C',
  '𝐃' => 'D',
  '𝐄' => 'E',
  '𝐅' => 'F',
  '𝐆' => 'G',
  '𝐇' => 'H',
  '𝐈' => 'I',
  '𝐉' => 'J',
  '𝐊' => 'K',
  '𝐋' => 'L',
  '𝐌' => 'M',
  '𝐍' => 'N',
  '𝐎' => 'O',
  '𝐏' => 'P',
  '𝐐' => 'Q',
  '𝐑' => 'R',
  '𝐒' => 'S',
  '𝐓' => 'T',
  '𝐔' => 'U',
  '𝐕' => 'V',
  '𝐖' => 'W',
  '𝐗' => 'X',
  '𝐘' => 'Y',
  '𝐙' => 'Z',
  '𝐚' => 'a',
  '𝐛' => 'b',
  '𝐜' => 'c',
  '𝐝' => 'd',
  '𝐞' => 'e',
  '𝐟' => 'f',
  '𝐠' => 'g',
  '𝐡' => 'h',
  '𝐢' => 'i',
  '𝐣' => 'j',
  '𝐤' => 'k',
  '𝐥' => 'l',
  '𝐦' => 'm',
  '𝐧' => 'n',
  '𝐨' => 'o',
  '𝐩' => 'p',
  '𝐪' => 'q',
  '𝐫' => 'r',
  '𝐬' => 's',
  '𝐭' => 't',
  '𝐮' => 'u',
  '𝐯' => 'v',
  '𝐰' => 'w',
  '𝐱' => 'x',
  '𝐲' => 'y',
  '𝐳' => 'z',
  '𝐴' => 'A',
  '𝐵' => 'B',
  '𝐶' => 'C',
  '𝐷' => 'D',
  '𝐸' => 'E',
  '𝐹' => 'F',
  '𝐺' => 'G',
  '𝐻' => 'H',
  '𝐼' => 'I',
  '𝐽' => 'J',
  '𝐾' => 'K',
  '𝐿' => 'L',
  '𝑀' => 'M',
  '𝑁' => 'N',
  '𝑂' => 'O',
  '𝑃' => 'P',
  '𝑄' => 'Q',
  '𝑅' => 'R',
  '𝑆' => 'S',
  '𝑇' => 'T',
  '𝑈' => 'U',
  '𝑉' => 'V',
  '𝑊' => 'W',
  '𝑋' => 'X',
  '𝑌' => 'Y',
  '𝑍' => 'Z',
  '𝑎' => 'a',
  '𝑏' => 'b',
  '𝑐' => 'c',
  '𝑑' => 'd',
  '𝑒' => 'e',
  '𝑓' => 'f',
  '𝑔' => 'g',
  '𝑖' => 'i',
  '𝑗' => 'j',
  '𝑘' => 'k',
  '𝑙' => 'l',
  '𝑚' => 'm',
  '𝑛' => 'n',
  '𝑜' => 'o',
  '𝑝' => 'p',
  '𝑞' => 'q',
  '𝑟' => 'r',
  '𝑠' => 's',
  '𝑡' => 't',
  '𝑢' => 'u',
  '𝑣' => 'v',
  '𝑤' => 'w',
  '𝑥' => 'x',
  '𝑦' => 'y',
  '𝑧' => 'z',
  '𝑨' => 'A',
  '𝑩' => 'B',
  '𝑪' => 'C',
  '𝑫' => 'D',
  '𝑬' => 'E',
  '𝑭' => 'F',
  '𝑮' => 'G',
  '𝑯' => 'H',
  '𝑰' => 'I',
  '𝑱' => 'J',
  '𝑲' => 'K',
  '𝑳' => 'L',
  '𝑴' => 'M',
  '𝑵' => 'N',
  '𝑶' => 'O',
  '𝑷' => 'P',
  '𝑸' => 'Q',
  '𝑹' => 'R',
  '𝑺' => 'S',
  '𝑻' => 'T',
  '𝑼' => 'U',
  '𝑽' => 'V',
  '𝑾' => 'W',
  '𝑿' => 'X',
  '𝒀' => 'Y',
  '𝒁' => 'Z',
  '𝒂' => 'a',
  '𝒃' => 'b',
  '𝒄' => 'c',
  '𝒅' => 'd',
  '𝒆' => 'e',
  '𝒇' => 'f',
  '𝒈' => 'g',
  '𝒉' => 'h',
  '𝒊' => 'i',
  '𝒋' => 'j',
  '𝒌' => 'k',
  '𝒍' => 'l',
  '𝒎' => 'm',
  '𝒏' => 'n',
  '𝒐' => 'o',
  '𝒑' => 'p',
  '𝒒' => 'q',
  '𝒓' => 'r',
  '𝒔' => 's',
  '𝒕' => 't',
  '𝒖' => 'u',
  '𝒗' => 'v',
  '𝒘' => 'w',
  '𝒙' => 'x',
  '𝒚' => 'y',
  '𝒛' => 'z',
  '𝒜' => 'A',
  '𝒞' => 'C',
  '𝒟' => 'D',
  '𝒢' => 'G',
  '𝒥' => 'J',
  '𝒦' => 'K',
  '𝒩' => 'N',
  '𝒪' => 'O',
  '𝒫' => 'P',
  '𝒬' => 'Q',
  '𝒮' => 'S',
  '𝒯' => 'T',
  '𝒰' => 'U',
  '𝒱' => 'V',
  '𝒲' => 'W',
  '𝒳' => 'X',
  '𝒴' => 'Y',
  '𝒵' => 'Z',
  '𝒶' => 'a',
  '𝒷' => 'b',
  '𝒸' => 'c',
  '𝒹' => 'd',
  '𝒻' => 'f',
  '𝒽' => 'h',
  '𝒾' => 'i',
  '𝒿' => 'j',
  '𝓀' => 'k',
  '𝓁' => 'l',
  '𝓂' => 'm',
  '𝓃' => 'n',
  '𝓅' => 'p',
  '𝓆' => 'q',
  '𝓇' => 'r',
  '𝓈' => 's',
  '𝓉' => 't',
  '𝓊' => 'u',
  '𝓋' => 'v',
  '𝓌' => 'w',
  '𝓍' => 'x',
  '𝓎' => 'y',
  '𝓏' => 'z',
  '𝓐' => 'A',
  '𝓑' => 'B',
  '𝓒' => 'C',
  '𝓓' => 'D',
  '𝓔' => 'E',
  '𝓕' => 'F',
  '𝓖' => 'G',
  '𝓗' => 'H',
  '𝓘' => 'I',
  '𝓙' => 'J',
  '𝓚' => 'K',
  '𝓛' => 'L',
  '𝓜' => 'M',
  '𝓝' => 'N',
  '𝓞' => 'O',
  '𝓟' => 'P',
  '𝓠' => 'Q',
  '𝓡' => 'R',
  '𝓢' => 'S',
  '𝓣' => 'T',
  '𝓤' => 'U',
  '𝓥' => 'V',
  '𝓦' => 'W',
  '𝓧' => 'X',
  '𝓨' => 'Y',
  '𝓩' => 'Z',
  '𝓪' => 'a',
  '𝓫' => 'b',
  '𝓬' => 'c',
  '𝓭' => 'd',
  '𝓮' => 'e',
  '𝓯' => 'f',
  '𝓰' => 'g',
  '𝓱' => 'h',
  '𝓲' => 'i',
  '𝓳' => 'j',
  '𝓴' => 'k',
  '𝓵' => 'l',
  '𝓶' => 'm',
  '𝓷' => 'n',
  '𝓸' => 'o',
  '𝓹' => 'p',
  '𝓺' => 'q',
  '𝓻' => 'r',
  '𝓼' => 's',
  '𝓽' => 't',
  '𝓾' => 'u',
  '𝓿' => 'v',
  '𝔀' => 'w',
  '𝔁' => 'x',
  '𝔂' => 'y',
  '𝔃' => 'z',
  '𝔄' => 'A',
  '𝔅' => 'B',
  '𝔇' => 'D',
  '𝔈' => 'E',
  '𝔉' => 'F',
  '𝔊' => 'G',
  '𝔍' => 'J',
  '𝔎' => 'K',
  '𝔏' => 'L',
  '𝔐' => 'M',
  '𝔑' => 'N',
  '𝔒' => 'O',
  '𝔓' => 'P',
  '𝔔' => 'Q',
  '𝔖' => 'S',
  '𝔗' => 'T',
  '𝔘' => 'U',
  '𝔙' => 'V',
  '𝔚' => 'W',
  '𝔛' => 'X',
  '𝔜' => 'Y',
  '𝔞' => 'a',
  '𝔟' => 'b',
  '𝔠' => 'c',
  '𝔡' => 'd',
  '𝔢' => 'e',
  '𝔣' => 'f',
  '𝔤' => 'g',
  '𝔥' => 'h',
  '𝔦' => 'i',
  '𝔧' => 'j',
  '𝔨' => 'k',
  '𝔩' => 'l',
  '𝔪' => 'm',
  '𝔫' => 'n',
  '𝔬' => 'o',
  '𝔭' => 'p',
  '𝔮' => 'q',
  '𝔯' => 'r',
  '𝔰' => 's',
  '𝔱' => 't',
  '𝔲' => 'u',
  '𝔳' => 'v',
  '𝔴' => 'w',
  '𝔵' => 'x',
  '𝔶' => 'y',
  '𝔷' => 'z',
  '𝔸' => 'A',
  '𝔹' => 'B',
  '𝔻' => 'D',
  '𝔼' => 'E',
  '𝔽' => 'F',
  '𝔾' => 'G',
  '𝕀' => 'I',
  '𝕁' => 'J',
  '𝕂' => 'K',
  '𝕃' => 'L',
  '𝕄' => 'M',
  '𝕆' => 'O',
  '𝕊' => 'S',
  '𝕋' => 'T',
  '𝕌' => 'U',
  '𝕍' => 'V',
  '𝕎' => 'W',
  '𝕏' => 'X',
  '𝕐' => 'Y',
  '𝕒' => 'a',
  '𝕓' => 'b',
  '𝕔' => 'c',
  '𝕕' => 'd',
  '𝕖' => 'e',
  '𝕗' => 'f',
  '𝕘' => 'g',
  '𝕙' => 'h',
  '𝕚' => 'i',
  '𝕛' => 'j',
  '𝕜' => 'k',
  '𝕝' => 'l',
  '𝕞' => 'm',
  '𝕟' => 'n',
  '𝕠' => 'o',
  '𝕡' => 'p',
  '𝕢' => 'q',
  '𝕣' => 'r',
  '𝕤' => 's',
  '𝕥' => 't',
  '𝕦' => 'u',
  '𝕧' => 'v',
  '𝕨' => 'w',
  '𝕩' => 'x',
  '𝕪' => 'y',
  '𝕫' => 'z',
  '𝕬' => 'A',
  '𝕭' => 'B',
  '𝕮' => 'C',
  '𝕯' => 'D',
  '𝕰' => 'E',
  '𝕱' => 'F',
  '𝕲' => 'G',
  '𝕳' => 'H',
  '𝕴' => 'I',
  '𝕵' => 'J',
  '𝕶' => 'K',
  '𝕷' => 'L',
  '𝕸' => 'M',
  '𝕹' => 'N',
  '𝕺' => 'O',
  '𝕻' => 'P',
  '𝕼' => 'Q',
  '𝕽' => 'R',
  '𝕾' => 'S',
  '𝕿' => 'T',
  '𝖀' => 'U',
  '𝖁' => 'V',
  '𝖂' => 'W',
  '𝖃' => 'X',
  '𝖄' => 'Y',
  '𝖅' => 'Z',
  '𝖆' => 'a',
  '𝖇' => 'b',
  '𝖈' => 'c',
  '𝖉' => 'd',
  '𝖊' => 'e',
  '𝖋' => 'f',
  '𝖌' => 'g',
  '𝖍' => 'h',
  '𝖎' => 'i',
  '𝖏' => 'j',
  '𝖐' => 'k',
  '𝖑' => 'l',
  '𝖒' => 'm',
  '𝖓' => 'n',
  '𝖔' => 'o',
  '𝖕' => 'p',
  '𝖖' => 'q',
  '𝖗' => 'r',
  '𝖘' => 's',
  '𝖙' => 't',
  '𝖚' => 'u',
  '𝖛' => 'v',
  '𝖜' => 'w',
  '𝖝' => 'x',
  '𝖞' => 'y',
  '𝖟' => 'z',
  '𝖠' => 'A',
  '𝖡' => 'B',
  '𝖢' => 'C',
  '𝖣' => 'D',
  '𝖤' => 'E',
  '𝖥' => 'F',
  '𝖦' => 'G',
  '𝖧' => 'H',
  '𝖨' => 'I',
  '𝖩' => 'J',
  '𝖪' => 'K',
  '𝖫' => 'L',
  '𝖬' => 'M',
  '𝖭' => 'N',
  '𝖮' => 'O',
  '𝖯' => 'P',
  '𝖰' => 'Q',
  '𝖱' => 'R',
  '𝖲' => 'S',
  '𝖳' => 'T',
  '𝖴' => 'U',
  '𝖵' => 'V',
  '𝖶' => 'W',
  '𝖷' => 'X',
  '𝖸' => 'Y',
  '𝖹' => 'Z',
  '𝖺' => 'a',
  '𝖻' => 'b',
  '𝖼' => 'c',
  '𝖽' => 'd',
  '𝖾' => 'e',
  '𝖿' => 'f',
  '𝗀' => 'g',
  '𝗁' => 'h',
  '𝗂' => 'i',
  '𝗃' => 'j',
  '𝗄' => 'k',
  '𝗅' => 'l',
  '𝗆' => 'm',
  '𝗇' => 'n',
  '𝗈' => 'o',
  '𝗉' => 'p',
  '𝗊' => 'q',
  '𝗋' => 'r',
  '𝗌' => 's',
  '𝗍' => 't',
  '𝗎' => 'u',
  '𝗏' => 'v',
  '𝗐' => 'w',
  '𝗑' => 'x',
  '𝗒' => 'y',
  '𝗓' => 'z',
  '𝗔' => 'A',
  '𝗕' => 'B',
  '𝗖' => 'C',
  '𝗗' => 'D',
  '𝗘' => 'E',
  '𝗙' => 'F',
  '𝗚' => 'G',
  '𝗛' => 'H',
  '𝗜' => 'I',
  '𝗝' => 'J',
  '𝗞' => 'K',
  '𝗟' => 'L',
  '𝗠' => 'M',
  '𝗡' => 'N',
  '𝗢' => 'O',
  '𝗣' => 'P',
  '𝗤' => 'Q',
  '𝗥' => 'R',
  '𝗦' => 'S',
  '𝗧' => 'T',
  '𝗨' => 'U',
  '𝗩' => 'V',
  '𝗪' => 'W',
  '𝗫' => 'X',
  '𝗬' => 'Y',
  '𝗭' => 'Z',
  '𝗮' => 'a',
  '𝗯' => 'b',
  '𝗰' => 'c',
  '𝗱' => 'd',
  '𝗲' => 'e',
  '𝗳' => 'f',
  '𝗴' => 'g',
  '𝗵' => 'h',
  '𝗶' => 'i',
  '𝗷' => 'j',
  '𝗸' => 'k',
  '𝗹' => 'l',
  '𝗺' => 'm',
  '𝗻' => 'n',
  '𝗼' => 'o',
  '𝗽' => 'p',
  '𝗾' => 'q',
  '𝗿' => 'r',
  '𝘀' => 's',
  '𝘁' => 't',
  '𝘂' => 'u',
  '𝘃' => 'v',
  '𝘄' => 'w',
  '𝘅' => 'x',
  '𝘆' => 'y',
  '𝘇' => 'z',
  '𝘈' => 'A',
  '𝘉' => 'B',
  '𝘊' => 'C',
  '𝘋' => 'D',
  '𝘌' => 'E',
  '𝘍' => 'F',
  '𝘎' => 'G',
  '𝘏' => 'H',
  '𝘐' => 'I',
  '𝘑' => 'J',
  '𝘒' => 'K',
  '𝘓' => 'L',
  '𝘔' => 'M',
  '𝘕' => 'N',
  '𝘖' => 'O',
  '𝘗' => 'P',
  '𝘘' => 'Q',
  '𝘙' => 'R',
  '𝘚' => 'S',
  '𝘛' => 'T',
  '𝘜' => 'U',
  '𝘝' => 'V',
  '𝘞' => 'W',
  '𝘟' => 'X',
  '𝘠' => 'Y',
  '𝘡' => 'Z',
  '𝘢' => 'a',
  '𝘣' => 'b',
  '𝘤' => 'c',
  '𝘥' => 'd',
  '𝘦' => 'e',
  '𝘧' => 'f',
  '𝘨' => 'g',
  '𝘩' => 'h',
  '𝘪' => 'i',
  '𝘫' => 'j',
  '𝘬' => 'k',
  '𝘭' => 'l',
  '𝘮' => 'm',
  '𝘯' => 'n',
  '𝘰' => 'o',
  '𝘱' => 'p',
  '𝘲' => 'q',
  '𝘳' => 'r',
  '𝘴' => 's',
  '𝘵' => 't',
  '𝘶' => 'u',
  '𝘷' => 'v',
  '𝘸' => 'w',
  '𝘹' => 'x',
  '𝘺' => 'y',
  '𝘻' => 'z',
  '𝘼' => 'A',
  '𝘽' => 'B',
  '𝘾' => 'C',
  '𝘿' => 'D',
  '𝙀' => 'E',
  '𝙁' => 'F',
  '𝙂' => 'G',
  '𝙃' => 'H',
  '𝙄' => 'I',
  '𝙅' => 'J',
  '𝙆' => 'K',
  '𝙇' => 'L',
  '𝙈' => 'M',
  '𝙉' => 'N',
  '𝙊' => 'O',
  '𝙋' => 'P',
  '𝙌' => 'Q',
  '𝙍' => 'R',
  '𝙎' => 'S',
  '𝙏' => 'T',
  '𝙐' => 'U',
  '𝙑' => 'V',
  '𝙒' => 'W',
  '𝙓' => 'X',
  '𝙔' => 'Y',
  '𝙕' => 'Z',
  '𝙖' => 'a',
  '𝙗' => 'b',
  '𝙘' => 'c',
  '𝙙' => 'd',
  '𝙚' => 'e',
  '𝙛' => 'f',
  '𝙜' => 'g',
  '𝙝' => 'h',
  '𝙞' => 'i',
  '𝙟' => 'j',
  '𝙠' => 'k',
  '𝙡' => 'l',
  '𝙢' => 'm',
  '𝙣' => 'n',
  '𝙤' => 'o',
  '𝙥' => 'p',
  '𝙦' => 'q',
  '𝙧' => 'r',
  '𝙨' => 's',
  '𝙩' => 't',
  '𝙪' => 'u',
  '𝙫' => 'v',
  '𝙬' => 'w',
  '𝙭' => 'x',
  '𝙮' => 'y',
  '𝙯' => 'z',
  '𝙰' => 'A',
  '𝙱' => 'B',
  '𝙲' => 'C',
  '𝙳' => 'D',
  '𝙴' => 'E',
  '𝙵' => 'F',
  '𝙶' => 'G',
  '𝙷' => 'H',
  '𝙸' => 'I',
  '𝙹' => 'J',
  '𝙺' => 'K',
  '𝙻' => 'L',
  '𝙼' => 'M',
  '𝙽' => 'N',
  '𝙾' => 'O',
  '𝙿' => 'P',
  '𝚀' => 'Q',
  '𝚁' => 'R',
  '𝚂' => 'S',
  '𝚃' => 'T',
  '𝚄' => 'U',
  '𝚅' => 'V',
  '𝚆' => 'W',
  '𝚇' => 'X',
  '𝚈' => 'Y',
  '𝚉' => 'Z',
  '𝚊' => 'a',
  '𝚋' => 'b',
  '𝚌' => 'c',
  '𝚍' => 'd',
  '𝚎' => 'e',
  '𝚏' => 'f',
  '𝚐' => 'g',
  '𝚑' => 'h',
  '𝚒' => 'i',
  '𝚓' => 'j',
  '𝚔' => 'k',
  '𝚕' => 'l',
  '𝚖' => 'm',
  '𝚗' => 'n',
  '𝚘' => 'o',
  '𝚙' => 'p',
  '𝚚' => 'q',
  '𝚛' => 'r',
  '𝚜' => 's',
  '𝚝' => 't',
  '𝚞' => 'u',
  '𝚟' => 'v',
  '𝚠' => 'w',
  '𝚡' => 'x',
  '𝚢' => 'y',
  '𝚣' => 'z',
  '𝚤' => 'ı',
  '𝚥' => 'ȷ',
  '𝚨' => 'Α',
  '𝚩' => 'Β',
  '𝚪' => 'Γ',
  '𝚫' => 'Δ',
  '𝚬' => 'Ε',
  '𝚭' => 'Ζ',
  '𝚮' => 'Η',
  '𝚯' => 'Θ',
  '𝚰' => 'Ι',
  '𝚱' => 'Κ',
  '𝚲' => 'Λ',
  '𝚳' => 'Μ',
  '𝚴' => 'Ν',
  '𝚵' => 'Ξ',
  '𝚶' => 'Ο',
  '𝚷' => 'Π',
  '𝚸' => 'Ρ',
  '𝚹' => 'ϴ',
  '𝚺' => 'Σ',
  '𝚻' => 'Τ',
  '𝚼' => 'Υ',
  '𝚽' => 'Φ',
  '𝚾' => 'Χ',
  '𝚿' => 'Ψ',
  '𝛀' => 'Ω',
  '𝛁' => '∇',
  '𝛂' => 'α',
  '𝛃' => 'β',
  '𝛄' => 'γ',
  '𝛅' => 'δ',
  '𝛆' => 'ε',
  '𝛇' => 'ζ',
  '𝛈' => 'η',
  '𝛉' => 'θ',
  '𝛊' => 'ι',
  '𝛋' => 'κ',
  '𝛌' => 'λ',
  '𝛍' => 'μ',
  '𝛎' => 'ν',
  '𝛏' => 'ξ',
  '𝛐' => 'ο',
  '𝛑' => 'π',
  '𝛒' => 'ρ',
  '𝛓' => 'ς',
  '𝛔' => 'σ',
  '𝛕' => 'τ',
  '𝛖' => 'υ',
  '𝛗' => 'φ',
  '𝛘' => 'χ',
  '𝛙' => 'ψ',
  '𝛚' => 'ω',
  '𝛛' => '∂',
  '𝛜' => 'ϵ',
  '𝛝' => 'ϑ',
  '𝛞' => 'ϰ',
  '𝛟' => 'ϕ',
  '𝛠' => 'ϱ',
  '𝛡' => 'ϖ',
  '𝛢' => 'Α',
  '𝛣' => 'Β',
  '𝛤' => 'Γ',
  '𝛥' => 'Δ',
  '𝛦' => 'Ε',
  '𝛧' => 'Ζ',
  '𝛨' => 'Η',
  '𝛩' => 'Θ',
  '𝛪' => 'Ι',
  '𝛫' => 'Κ',
  '𝛬' => 'Λ',
  '𝛭' => 'Μ',
  '𝛮' => 'Ν',
  '𝛯' => 'Ξ',
  '𝛰' => 'Ο',
  '𝛱' => 'Π',
  '𝛲' => 'Ρ',
  '𝛳' => 'ϴ',
  '𝛴' => 'Σ',
  '𝛵' => 'Τ',
  '𝛶' => 'Υ',
  '𝛷' => 'Φ',
  '𝛸' => 'Χ',
  '𝛹' => 'Ψ',
  '𝛺' => 'Ω',
  '𝛻' => '∇',
  '𝛼' => 'α',
  '𝛽' => 'β',
  '𝛾' => 'γ',
  '𝛿' => 'δ',
  '𝜀' => 'ε',
  '𝜁' => 'ζ',
  '𝜂' => 'η',
  '𝜃' => 'θ',
  '𝜄' => 'ι',
  '𝜅' => 'κ',
  '𝜆' => 'λ',
  '𝜇' => 'μ',
  '𝜈' => 'ν',
  '𝜉' => 'ξ',
  '𝜊' => 'ο',
  '𝜋' => 'π',
  '𝜌' => 'ρ',
  '𝜍' => 'ς',
  '𝜎' => 'σ',
  '𝜏' => 'τ',
  '𝜐' => 'υ',
  '𝜑' => 'φ',
  '𝜒' => 'χ',
  '𝜓' => 'ψ',
  '𝜔' => 'ω',
  '𝜕' => '∂',
  '𝜖' => 'ϵ',
  '𝜗' => 'ϑ',
  '𝜘' => 'ϰ',
  '𝜙' => 'ϕ',
  '𝜚' => 'ϱ',
  '𝜛' => 'ϖ',
  '𝜜' => 'Α',
  '𝜝' => 'Β',
  '𝜞' => 'Γ',
  '𝜟' => 'Δ',
  '𝜠' => 'Ε',
  '𝜡' => 'Ζ',
  '𝜢' => 'Η',
  '𝜣' => 'Θ',
  '𝜤' => 'Ι',
  '𝜥' => 'Κ',
  '𝜦' => 'Λ',
  '𝜧' => 'Μ',
  '𝜨' => 'Ν',
  '𝜩' => 'Ξ',
  '𝜪' => 'Ο',
  '𝜫' => 'Π',
  '𝜬' => 'Ρ',
  '𝜭' => 'ϴ',
  '𝜮' => 'Σ',
  '𝜯' => 'Τ',
  '𝜰' => 'Υ',
  '𝜱' => 'Φ',
  '𝜲' => 'Χ',
  '𝜳' => 'Ψ',
  '𝜴' => 'Ω',
  '𝜵' => '∇',
  '𝜶' => 'α',
  '𝜷' => 'β',
  '𝜸' => 'γ',
  '𝜹' => 'δ',
  '𝜺' => 'ε',
  '𝜻' => 'ζ',
  '𝜼' => 'η',
  '𝜽' => 'θ',
  '𝜾' => 'ι',
  '𝜿' => 'κ',
  '𝝀' => 'λ',
  '𝝁' => 'μ',
  '𝝂' => 'ν',
  '𝝃' => 'ξ',
  '𝝄' => 'ο',
  '𝝅' => 'π',
  '𝝆' => 'ρ',
  '𝝇' => 'ς',
  '𝝈' => 'σ',
  '𝝉' => 'τ',
  '𝝊' => 'υ',
  '𝝋' => 'φ',
  '𝝌' => 'χ',
  '𝝍' => 'ψ',
  '𝝎' => 'ω',
  '𝝏' => '∂',
  '𝝐' => 'ϵ',
  '𝝑' => 'ϑ',
  '𝝒' => 'ϰ',
  '𝝓' => 'ϕ',
  '𝝔' => 'ϱ',
  '𝝕' => 'ϖ',
  '𝝖' => 'Α',
  '𝝗' => 'Β',
  '𝝘' => 'Γ',
  '𝝙' => 'Δ',
  '𝝚' => 'Ε',
  '𝝛' => 'Ζ',
  '𝝜' => 'Η',
  '𝝝' => 'Θ',
  '𝝞' => 'Ι',
  '𝝟' => 'Κ',
  '𝝠' => 'Λ',
  '𝝡' => 'Μ',
  '𝝢' => 'Ν',
  '𝝣' => 'Ξ',
  '𝝤' => 'Ο',
  '𝝥' => 'Π',
  '𝝦' => 'Ρ',
  '𝝧' => 'ϴ',
  '𝝨' => 'Σ',
  '𝝩' => 'Τ',
  '𝝪' => 'Υ',
  '𝝫' => 'Φ',
  '𝝬' => 'Χ',
  '𝝭' => 'Ψ',
  '𝝮' => 'Ω',
  '𝝯' => '∇',
  '𝝰' => 'α',
  '𝝱' => 'β',
  '𝝲' => 'γ',
  '𝝳' => 'δ',
  '𝝴' => 'ε',
  '𝝵' => 'ζ',
  '𝝶' => 'η',
  '𝝷' => 'θ',
  '𝝸' => 'ι',
  '𝝹' => 'κ',
  '𝝺' => 'λ',
  '𝝻' => 'μ',
  '𝝼' => 'ν',
  '𝝽' => 'ξ',
  '𝝾' => 'ο',
  '𝝿' => 'π',
  '𝞀' => 'ρ',
  '𝞁' => 'ς',
  '𝞂' => 'σ',
  '𝞃' => 'τ',
  '𝞄' => 'υ',
  '𝞅' => 'φ',
  '𝞆' => 'χ',
  '𝞇' => 'ψ',
  '𝞈' => 'ω',
  '𝞉' => '∂',
  '𝞊' => 'ϵ',
  '𝞋' => 'ϑ',
  '𝞌' => 'ϰ',
  '𝞍' => 'ϕ',
  '𝞎' => 'ϱ',
  '𝞏' => 'ϖ',
  '𝞐' => 'Α',
  '𝞑' => 'Β',
  '𝞒' => 'Γ',
  '𝞓' => 'Δ',
  '𝞔' => 'Ε',
  '𝞕' => 'Ζ',
  '𝞖' => 'Η',
  '𝞗' => 'Θ',
  '𝞘' => 'Ι',
  '𝞙' => 'Κ',
  '𝞚' => 'Λ',
  '𝞛' => 'Μ',
  '𝞜' => 'Ν',
  '𝞝' => 'Ξ',
  '𝞞' => 'Ο',
  '𝞟' => 'Π',
  '𝞠' => 'Ρ',
  '𝞡' => 'ϴ',
  '𝞢' => 'Σ',
  '𝞣' => 'Τ',
  '𝞤' => 'Υ',
  '𝞥' => 'Φ',
  '𝞦' => 'Χ',
  '𝞧' => 'Ψ',
  '𝞨' => 'Ω',
  '𝞩' => '∇',
  '𝞪' => 'α',
  '𝞫' => 'β',
  '𝞬' => 'γ',
  '𝞭' => 'δ',
  '𝞮' => 'ε',
  '𝞯' => 'ζ',
  '𝞰' => 'η',
  '𝞱' => 'θ',
  '𝞲' => 'ι',
  '𝞳' => 'κ',
  '𝞴' => 'λ',
  '𝞵' => 'μ',
  '𝞶' => 'ν',
  '𝞷' => 'ξ',
  '𝞸' => 'ο',
  '𝞹' => 'π',
  '𝞺' => 'ρ',
  '𝞻' => 'ς',
  '𝞼' => 'σ',
  '𝞽' => 'τ',
  '𝞾' => 'υ',
  '𝞿' => 'φ',
  '𝟀' => 'χ',
  '𝟁' => 'ψ',
  '𝟂' => 'ω',
  '𝟃' => '∂',
  '𝟄' => 'ϵ',
  '𝟅' => 'ϑ',
  '𝟆' => 'ϰ',
  '𝟇' => 'ϕ',
  '𝟈' => 'ϱ',
  '𝟉' => 'ϖ',
  '𝟊' => 'Ϝ',
  '𝟋' => 'ϝ',
  '𝟎' => '0',
  '𝟏' => '1',
  '𝟐' => '2',
  '𝟑' => '3',
  '𝟒' => '4',
  '𝟓' => '5',
  '𝟔' => '6',
  '𝟕' => '7',
  '𝟖' => '8',
  '𝟗' => '9',
  '𝟘' => '0',
  '𝟙' => '1',
  '𝟚' => '2',
  '𝟛' => '3',
  '𝟜' => '4',
  '𝟝' => '5',
  '𝟞' => '6',
  '𝟟' => '7',
  '𝟠' => '8',
  '𝟡' => '9',
  '𝟢' => '0',
  '𝟣' => '1',
  '𝟤' => '2',
  '𝟥' => '3',
  '𝟦' => '4',
  '𝟧' => '5',
  '𝟨' => '6',
  '𝟩' => '7',
  '𝟪' => '8',
  '𝟫' => '9',
  '𝟬' => '0',
  '𝟭' => '1',
  '𝟮' => '2',
  '𝟯' => '3',
  '𝟰' => '4',
  '𝟱' => '5',
  '𝟲' => '6',
  '𝟳' => '7',
  '𝟴' => '8',
  '𝟵' => '9',
  '𝟶' => '0',
  '𝟷' => '1',
  '𝟸' => '2',
  '𝟹' => '3',
  '𝟺' => '4',
  '𝟻' => '5',
  '𝟼' => '6',
  '𝟽' => '7',
  '𝟾' => '8',
  '𝟿' => '9',
  '𞀰' => 'а',
  '𞀱' => 'б',
  '𞀲' => 'в',
  '𞀳' => 'г',
  '𞀴' => 'д',
  '𞀵' => 'е',
  '𞀶' => 'ж',
  '𞀷' => 'з',
  '𞀸' => 'и',
  '𞀹' => 'к',
  '𞀺' => 'л',
  '𞀻' => 'м',
  '𞀼' => 'о',
  '𞀽' => 'п',
  '𞀾' => 'р',
  '𞀿' => 'с',
  '𞁀' => 'т',
  '𞁁' => 'у',
  '𞁂' => 'ф',
  '𞁃' => 'х',
  '𞁄' => 'ц',
  '𞁅' => 'ч',
  '𞁆' => 'ш',
  '𞁇' => 'ы',
  '𞁈' => 'э',
  '𞁉' => 'ю',
  '𞁊' => 'ꚉ',
  '𞁋' => 'ә',
  '𞁌' => 'і',
  '𞁍' => 'ј',
  '𞁎' => 'ө',
  '𞁏' => 'ү',
  '𞁐' => 'ӏ',
  '𞁑' => 'а',
  '𞁒' => 'б',
  '𞁓' => 'в',
  '𞁔' => 'г',
  '𞁕' => 'д',
  '𞁖' => 'е',
  '𞁗' => 'ж',
  '𞁘' => 'з',
  '𞁙' => 'и',
  '𞁚' => 'к',
  '𞁛' => 'л',
  '𞁜' => 'о',
  '𞁝' => 'п',
  '𞁞' => 'с',
  '𞁟' => 'у',
  '𞁠' => 'ф',
  '𞁡' => 'х',
  '𞁢' => 'ц',
  '𞁣' => 'ч',
  '𞁤' => 'ш',
  '𞁥' => 'ъ',
  '𞁦' => 'ы',
  '𞁧' => 'ґ',
  '𞁨' => 'і',
  '𞁩' => 'ѕ',
  '𞁪' => 'џ',
  '𞁫' => 'ҫ',
  '𞁬' => 'ꙑ',
  '𞁭' => 'ұ',
  '𞸀' => 'ا',
  '𞸁' => 'ب',
  '𞸂' => 'ج',
  '𞸃' => 'د',
  '𞸅' => 'و',
  '𞸆' => 'ز',
  '𞸇' => 'ح',
  '𞸈' => 'ط',
  '𞸉' => 'ي',
  '𞸊' => 'ك',
  '𞸋' => 'ل',
  '𞸌' => 'م',
  '𞸍' => 'ن',
  '𞸎' => 'س',
  '𞸏' => 'ع',
  '𞸐' => 'ف',
  '𞸑' => 'ص',
  '𞸒' => 'ق',
  '𞸓' => 'ر',
  '𞸔' => 'ش',
  '𞸕' => 'ت',
  '𞸖' => 'ث',
  '𞸗' => 'خ',
  '𞸘' => 'ذ',
  '𞸙' => 'ض',
  '𞸚' => 'ظ',
  '𞸛' => 'غ',
  '𞸜' => 'ٮ',
  '𞸝' => 'ں',
  '𞸞' => 'ڡ',
  '𞸟' => 'ٯ',
  '𞸡' => 'ب',
  '𞸢' => 'ج',
  '𞸤' => 'ه',
  '𞸧' => 'ح',
  '𞸩' => 'ي',
  '𞸪' => 'ك',
  '𞸫' => 'ل',
  '𞸬' => 'م',
  '𞸭' => 'ن',
  '𞸮' => 'س',
  '𞸯' => 'ع',
  '𞸰' => 'ف',
  '𞸱' => 'ص',
  '𞸲' => 'ق',
  '𞸴' => 'ش',
  '𞸵' => 'ت',
  '𞸶' => 'ث',
  '𞸷' => 'خ',
  '𞸹' => 'ض',
  '𞸻' => 'غ',
  '𞹂' => 'ج',
  '𞹇' => 'ح',
  '𞹉' => 'ي',
  '𞹋' => 'ل',
  '𞹍' => 'ن',
  '𞹎' => 'س',
  '𞹏' => 'ع',
  '𞹑' => 'ص',
  '𞹒' => 'ق',
  '𞹔' => 'ش',
  '𞹗' => 'خ',
  '𞹙' => 'ض',
  '𞹛' => 'غ',
  '𞹝' => 'ں',
  '𞹟' => 'ٯ',
  '𞹡' => 'ب',
  '𞹢' => 'ج',
  '𞹤' => 'ه',
  '𞹧' => 'ح',
  '𞹨' => 'ط',
  '𞹩' => 'ي',
  '𞹪' => 'ك',
  '𞹬' => 'م',
  '𞹭' => 'ن',
  '𞹮' => 'س',
  '𞹯' => 'ع',
  '𞹰' => 'ف',
  '𞹱' => 'ص',
  '𞹲' => 'ق',
  '𞹴' => 'ش',
  '𞹵' => 'ت',
  '𞹶' => 'ث',
  '𞹷' => 'خ',
  '𞹹' => 'ض',
  '𞹺' => 'ظ',
  '𞹻' => 'غ',
  '𞹼' => 'ٮ',
  '𞹾' => 'ڡ',
  '𞺀' => 'ا',
  '𞺁' => 'ب',
  '𞺂' => 'ج',
  '𞺃' => 'د',
  '𞺄' => 'ه',
  '𞺅' => 'و',
  '𞺆' => 'ز',
  '𞺇' => 'ح',
  '𞺈' => 'ط',
  '𞺉' => 'ي',
  '𞺋' => 'ل',
  '𞺌' => 'م',
  '𞺍' => 'ن',
  '𞺎' => 'س',
  '𞺏' => 'ع',
  '𞺐' => 'ف',
  '𞺑' => 'ص',
  '𞺒' => 'ق',
  '𞺓' => 'ر',
  '𞺔' => 'ش',
  '𞺕' => 'ت',
  '𞺖' => 'ث',
  '𞺗' => 'خ',
  '𞺘' => 'ذ',
  '𞺙' => 'ض',
  '𞺚' => 'ظ',
  '𞺛' => 'غ',
  '𞺡' => 'ب',
  '𞺢' => 'ج',
  '𞺣' => 'د',
  '𞺥' => 'و',
  '𞺦' => 'ز',
  '𞺧' => 'ح',
  '𞺨' => 'ط',
  '𞺩' => 'ي',
  '𞺫' => 'ل',
  '𞺬' => 'م',
  '𞺭' => 'ن',
  '𞺮' => 'س',
  '𞺯' => 'ع',
  '𞺰' => 'ف',
  '𞺱' => 'ص',
  '𞺲' => 'ق',
  '𞺳' => 'ر',
  '𞺴' => 'ش',
  '𞺵' => 'ت',
  '𞺶' => 'ث',
  '𞺷' => 'خ',
  '𞺸' => 'ذ',
  '𞺹' => 'ض',
  '𞺺' => 'ظ',
  '𞺻' => 'غ',
  '🄀' => '0.',
  '🄁' => '0,',
  '🄂' => '1,',
  '🄃' => '2,',
  '🄄' => '3,',
  '🄅' => '4,',
  '🄆' => '5,',
  '🄇' => '6,',
  '🄈' => '7,',
  '🄉' => '8,',
  '🄊' => '9,',
  '🄐' => '(A)',
  '🄑' => '(B)',
  '🄒' => '(C)',
  '🄓' => '(D)',
  '🄔' => '(E)',
  '🄕' => '(F)',
  '🄖' => '(G)',
  '🄗' => '(H)',
  '🄘' => '(I)',
  '🄙' => '(J)',
  '🄚' => '(K)',
  '🄛' => '(L)',
  '🄜' => '(M)',
  '🄝' => '(N)',
  '🄞' => '(O)',
  '🄟' => '(P)',
  '🄠' => '(Q)',
  '🄡' => '(R)',
  '🄢' => '(S)',
  '🄣' => '(T)',
  '🄤' => '(U)',
  '🄥' => '(V)',
  '🄦' => '(W)',
  '🄧' => '(X)',
  '🄨' => '(Y)',
  '🄩' => '(Z)',
  '🄪' => '〔S〕',
  '🄫' => 'C',
  '🄬' => 'R',
  '🄭' => 'CD',
  '🄮' => 'WZ',
  '🄰' => 'A',
  '🄱' => 'B',
  '🄲' => 'C',
  '🄳' => 'D',
  '🄴' => 'E',
  '🄵' => 'F',
  '🄶' => 'G',
  '🄷' => 'H',
  '🄸' => 'I',
  '🄹' => 'J',
  '🄺' => 'K',
  '🄻' => 'L',
  '🄼' => 'M',
  '🄽' => 'N',
  '🄾' => 'O',
  '🄿' => 'P',
  '🅀' => 'Q',
  '🅁' => 'R',
  '🅂' => 'S',
  '🅃' => 'T',
  '🅄' => 'U',
  '🅅' => 'V',
  '🅆' => 'W',
  '🅇' => 'X',
  '🅈' => 'Y',
  '🅉' => 'Z',
  '🅊' => 'HV',
  '🅋' => 'MV',
  '🅌' => 'SD',
  '🅍' => 'SS',
  '🅎' => 'PPV',
  '🅏' => 'WC',
  '🅪' => 'MC',
  '🅫' => 'MD',
  '🅬' => 'MR',
  '🆐' => 'DJ',
  '🈀' => 'ほか',
  '🈁' => 'ココ',
  '🈂' => 'サ',
  '🈐' => '手',
  '🈑' => '字',
  '🈒' => '双',
  '🈓' => 'デ',
  '🈔' => '二',
  '🈕' => '多',
  '🈖' => '解',
  '🈗' => '天',
  '🈘' => '交',
  '🈙' => '映',
  '🈚' => '無',
  '🈛' => '料',
  '🈜' => '前',
  '🈝' => '後',
  '🈞' => '再',
  '🈟' => '新',
  '🈠' => '初',
  '🈡' => '終',
  '🈢' => '生',
  '🈣' => '販',
  '🈤' => '声',
  '🈥' => '吹',
  '🈦' => '演',
  '🈧' => '投',
  '🈨' => '捕',
  '🈩' => '一',
  '🈪' => '三',
  '🈫' => '遊',
  '🈬' => '左',
  '🈭' => '中',
  '🈮' => '右',
  '🈯' => '指',
  '🈰' => '走',
  '🈱' => '打',
  '🈲' => '禁',
  '🈳' => '空',
  '🈴' => '合',
  '🈵' => '満',
  '🈶' => '有',
  '🈷' => '月',
  '🈸' => '申',
  '🈹' => '割',
  '🈺' => '営',
  '🈻' => '配',
  '🉀' => '〔本〕',
  '🉁' => '〔三〕',
  '🉂' => '〔二〕',
  '🉃' => '〔安〕',
  '🉄' => '〔点〕',
  '🉅' => '〔打〕',
  '🉆' => '〔盗〕',
  '🉇' => '〔勝〕',
  '🉈' => '〔敗〕',
  '🉐' => '得',
  '🉑' => '可',
  '🯰' => '0',
  '🯱' => '1',
  '🯲' => '2',
  '🯳' => '3',
  '🯴' => '4',
  '🯵' => '5',
  '🯶' => '6',
  '🯷' => '7',
  '🯸' => '8',
  '🯹' => '9',
);
<?php

return array (
  'À' => 'À',
  'Á' => 'Á',
  'Â' => 'Â',
  'Ã' => 'Ã',
  'Ä' => 'Ä',
  'Å' => 'Å',
  'Ç' => 'Ç',
  'È' => 'È',
  'É' => 'É',
  'Ê' => 'Ê',
  'Ë' => 'Ë',
  'Ì' => 'Ì',
  'Í' => 'Í',
  'Î' => 'Î',
  'Ï' => 'Ï',
  'Ñ' => 'Ñ',
  'Ò' => 'Ò',
  'Ó' => 'Ó',
  'Ô' => 'Ô',
  'Õ' => 'Õ',
  'Ö' => 'Ö',
  'Ù' => 'Ù',
  'Ú' => 'Ú',
  'Û' => 'Û',
  'Ü' => 'Ü',
  'Ý' => 'Ý',
  'à' => 'à',
  'á' => 'á',
  'â' => 'â',
  'ã' => 'ã',
  'ä' => 'ä',
  'å' => 'å',
  'ç' => 'ç',
  'è' => 'è',
  'é' => 'é',
  'ê' => 'ê',
  'ë' => 'ë',
  'ì' => 'ì',
  'í' => 'í',
  'î' => 'î',
  'ï' => 'ï',
  'ñ' => 'ñ',
  'ò' => 'ò',
  'ó' => 'ó',
  'ô' => 'ô',
  'õ' => 'õ',
  'ö' => 'ö',
  'ù' => 'ù',
  'ú' => 'ú',
  'û' => 'û',
  'ü' => 'ü',
  'ý' => 'ý',
  'ÿ' => 'ÿ',
  'Ā' => 'Ā',
  'ā' => 'ā',
  'Ă' => 'Ă',
  'ă' => 'ă',
  'Ą' => 'Ą',
  'ą' => 'ą',
  'Ć' => 'Ć',
  'ć' => 'ć',
  'Ĉ' => 'Ĉ',
  'ĉ' => 'ĉ',
  'Ċ' => 'Ċ',
  'ċ' => 'ċ',
  'Č' => 'Č',
  'č' => 'č',
  'Ď' => 'Ď',
  'ď' => 'ď',
  'Ē' => 'Ē',
  'ē' => 'ē',
  'Ĕ' => 'Ĕ',
  'ĕ' => 'ĕ',
  'Ė' => 'Ė',
  'ė' => 'ė',
  'Ę' => 'Ę',
  'ę' => 'ę',
  'Ě' => 'Ě',
  'ě' => 'ě',
  'Ĝ' => 'Ĝ',
  'ĝ' => 'ĝ',
  'Ğ' => 'Ğ',
  'ğ' => 'ğ',
  'Ġ' => 'Ġ',
  'ġ' => 'ġ',
  'Ģ' => 'Ģ',
  'ģ' => 'ģ',
  'Ĥ' => 'Ĥ',
  'ĥ' => 'ĥ',
  'Ĩ' => 'Ĩ',
  'ĩ' => 'ĩ',
  'Ī' => 'Ī',
  'ī' => 'ī',
  'Ĭ' => 'Ĭ',
  'ĭ' => 'ĭ',
  'Į' => 'Į',
  'į' => 'į',
  'İ' => 'İ',
  'Ĵ' => 'Ĵ',
  'ĵ' => 'ĵ',
  'Ķ' => 'Ķ',
  'ķ' => 'ķ',
  'Ĺ' => 'Ĺ',
  'ĺ' => 'ĺ',
  'Ļ' => 'Ļ',
  'ļ' => 'ļ',
  'Ľ' => 'Ľ',
  'ľ' => 'ľ',
  'Ń' => 'Ń',
  'ń' => 'ń',
  'Ņ' => 'Ņ',
  'ņ' => 'ņ',
  'Ň' => 'Ň',
  'ň' => 'ň',
  'Ō' => 'Ō',
  'ō' => 'ō',
  'Ŏ' => 'Ŏ',
  'ŏ' => 'ŏ',
  'Ő' => 'Ő',
  'ő' => 'ő',
  'Ŕ' => 'Ŕ',
  'ŕ' => 'ŕ',
  'Ŗ' => 'Ŗ',
  'ŗ' => 'ŗ',
  'Ř' => 'Ř',
  'ř' => 'ř',
  'Ś' => 'Ś',
  'ś' => 'ś',
  'Ŝ' => 'Ŝ',
  'ŝ' => 'ŝ',
  'Ş' => 'Ş',
  'ş' => 'ş',
  'Š' => 'Š',
  'š' => 'š',
  'Ţ' => 'Ţ',
  'ţ' => 'ţ',
  'Ť' => 'Ť',
  'ť' => 'ť',
  'Ũ' => 'Ũ',
  'ũ' => 'ũ',
  'Ū' => 'Ū',
  'ū' => 'ū',
  'Ŭ' => 'Ŭ',
  'ŭ' => 'ŭ',
  'Ů' => 'Ů',
  'ů' => 'ů',
  'Ű' => 'Ű',
  'ű' => 'ű',
  'Ų' => 'Ų',
  'ų' => 'ų',
  'Ŵ' => 'Ŵ',
  'ŵ' => 'ŵ',
  'Ŷ' => 'Ŷ',
  'ŷ' => 'ŷ',
  'Ÿ' => 'Ÿ',
  'Ź' => 'Ź',
  'ź' => 'ź',
  'Ż' => 'Ż',
  'ż' => 'ż',
  'Ž' => 'Ž',
  'ž' => 'ž',
  'Ơ' => 'Ơ',
  'ơ' => 'ơ',
  'Ư' => 'Ư',
  'ư' => 'ư',
  'Ǎ' => 'Ǎ',
  'ǎ' => 'ǎ',
  'Ǐ' => 'Ǐ',
  'ǐ' => 'ǐ',
  'Ǒ' => 'Ǒ',
  'ǒ' => 'ǒ',
  'Ǔ' => 'Ǔ',
  'ǔ' => 'ǔ',
  'Ǖ' => 'Ǖ',
  'ǖ' => 'ǖ',
  'Ǘ' => 'Ǘ',
  'ǘ' => 'ǘ',
  'Ǚ' => 'Ǚ',
  'ǚ' => 'ǚ',
  'Ǜ' => 'Ǜ',
  'ǜ' => 'ǜ',
  'Ǟ' => 'Ǟ',
  'ǟ' => 'ǟ',
  'Ǡ' => 'Ǡ',
  'ǡ' => 'ǡ',
  'Ǣ' => 'Ǣ',
  'ǣ' => 'ǣ',
  'Ǧ' => 'Ǧ',
  'ǧ' => 'ǧ',
  'Ǩ' => 'Ǩ',
  'ǩ' => 'ǩ',
  'Ǫ' => 'Ǫ',
  'ǫ' => 'ǫ',
  'Ǭ' => 'Ǭ',
  'ǭ' => 'ǭ',
  'Ǯ' => 'Ǯ',
  'ǯ' => 'ǯ',
  'ǰ' => 'ǰ',
  'Ǵ' => 'Ǵ',
  'ǵ' => 'ǵ',
  'Ǹ' => 'Ǹ',
  'ǹ' => 'ǹ',
  'Ǻ' => 'Ǻ',
  'ǻ' => 'ǻ',
  'Ǽ' => 'Ǽ',
  'ǽ' => 'ǽ',
  'Ǿ' => 'Ǿ',
  'ǿ' => 'ǿ',
  'Ȁ' => 'Ȁ',
  'ȁ' => 'ȁ',
  'Ȃ' => 'Ȃ',
  'ȃ' => 'ȃ',
  'Ȅ' => 'Ȅ',
  'ȅ' => 'ȅ',
  'Ȇ' => 'Ȇ',
  'ȇ' => 'ȇ',
  'Ȉ' => 'Ȉ',
  'ȉ' => 'ȉ',
  'Ȋ' => 'Ȋ',
  'ȋ' => 'ȋ',
  'Ȍ' => 'Ȍ',
  'ȍ' => 'ȍ',
  'Ȏ' => 'Ȏ',
  'ȏ' => 'ȏ',
  'Ȑ' => 'Ȑ',
  'ȑ' => 'ȑ',
  'Ȓ' => 'Ȓ',
  'ȓ' => 'ȓ',
  'Ȕ' => 'Ȕ',
  'ȕ' => 'ȕ',
  'Ȗ' => 'Ȗ',
  'ȗ' => 'ȗ',
  'Ș' => 'Ș',
  'ș' => 'ș',
  'Ț' => 'Ț',
  'ț' => 'ț',
  'Ȟ' => 'Ȟ',
  'ȟ' => 'ȟ',
  'Ȧ' => 'Ȧ',
  'ȧ' => 'ȧ',
  'Ȩ' => 'Ȩ',
  'ȩ' => 'ȩ',
  'Ȫ' => 'Ȫ',
  'ȫ' => 'ȫ',
  'Ȭ' => 'Ȭ',
  'ȭ' => 'ȭ',
  'Ȯ' => 'Ȯ',
  'ȯ' => 'ȯ',
  'Ȱ' => 'Ȱ',
  'ȱ' => 'ȱ',
  'Ȳ' => 'Ȳ',
  'ȳ' => 'ȳ',
  '̀' => '̀',
  '́' => '́',
  '̓' => '̓',
  '̈́' => '̈́',
  'ʹ' => 'ʹ',
  ';' => ';',
  '΅' => '΅',
  'Ά' => 'Ά',
  '·' => '·',
  'Έ' => 'Έ',
  'Ή' => 'Ή',
  'Ί' => 'Ί',
  'Ό' => 'Ό',
  'Ύ' => 'Ύ',
  'Ώ' => 'Ώ',
  'ΐ' => 'ΐ',
  'Ϊ' => 'Ϊ',
  'Ϋ' => 'Ϋ',
  'ά' => 'ά',
  'έ' => 'έ',
  'ή' => 'ή',
  'ί' => 'ί',
  'ΰ' => 'ΰ',
  'ϊ' => 'ϊ',
  'ϋ' => 'ϋ',
  'ό' => 'ό',
  'ύ' => 'ύ',
  'ώ' => 'ώ',
  'ϓ' => 'ϓ',
  'ϔ' => 'ϔ',
  'Ѐ' => 'Ѐ',
  'Ё' => 'Ё',
  'Ѓ' => 'Ѓ',
  'Ї' => 'Ї',
  'Ќ' => 'Ќ',
  'Ѝ' => 'Ѝ',
  'Ў' => 'Ў',
  'Й' => 'Й',
  'й' => 'й',
  'ѐ' => 'ѐ',
  'ё' => 'ё',
  'ѓ' => 'ѓ',
  'ї' => 'ї',
  'ќ' => 'ќ',
  'ѝ' => 'ѝ',
  'ў' => 'ў',
  'Ѷ' => 'Ѷ',
  'ѷ' => 'ѷ',
  'Ӂ' => 'Ӂ',
  'ӂ' => 'ӂ',
  'Ӑ' => 'Ӑ',
  'ӑ' => 'ӑ',
  'Ӓ' => 'Ӓ',
  'ӓ' => 'ӓ',
  'Ӗ' => 'Ӗ',
  'ӗ' => 'ӗ',
  'Ӛ' => 'Ӛ',
  'ӛ' => 'ӛ',
  'Ӝ' => 'Ӝ',
  'ӝ' => 'ӝ',
  'Ӟ' => 'Ӟ',
  'ӟ' => 'ӟ',
  'Ӣ' => 'Ӣ',
  'ӣ' => 'ӣ',
  'Ӥ' => 'Ӥ',
  'ӥ' => 'ӥ',
  'Ӧ' => 'Ӧ',
  'ӧ' => 'ӧ',
  'Ӫ' => 'Ӫ',
  'ӫ' => 'ӫ',
  'Ӭ' => 'Ӭ',
  'ӭ' => 'ӭ',
  'Ӯ' => 'Ӯ',
  'ӯ' => 'ӯ',
  'Ӱ' => 'Ӱ',
  'ӱ' => 'ӱ',
  'Ӳ' => 'Ӳ',
  'ӳ' => 'ӳ',
  'Ӵ' => 'Ӵ',
  'ӵ' => 'ӵ',
  'Ӹ' => 'Ӹ',
  'ӹ' => 'ӹ',
  'آ' => 'آ',
  'أ' => 'أ',
  'ؤ' => 'ؤ',
  'إ' => 'إ',
  'ئ' => 'ئ',
  'ۀ' => 'ۀ',
  'ۂ' => 'ۂ',
  'ۓ' => 'ۓ',
  'ऩ' => 'ऩ',
  'ऱ' => 'ऱ',
  'ऴ' => 'ऴ',
  'क़' => 'क़',
  'ख़' => 'ख़',
  'ग़' => 'ग़',
  'ज़' => 'ज़',
  'ड़' => 'ड़',
  'ढ़' => 'ढ़',
  'फ़' => 'फ़',
  'य़' => 'य़',
  'ো' => 'ো',
  'ৌ' => 'ৌ',
  'ড়' => 'ড়',
  'ঢ়' => 'ঢ়',
  'য়' => 'য়',
  'ਲ਼' => 'ਲ਼',
  'ਸ਼' => 'ਸ਼',
  'ਖ਼' => 'ਖ਼',
  'ਗ਼' => 'ਗ਼',
  'ਜ਼' => 'ਜ਼',
  'ਫ਼' => 'ਫ਼',
  'ୈ' => 'ୈ',
  'ୋ' => 'ୋ',
  'ୌ' => 'ୌ',
  'ଡ଼' => 'ଡ଼',
  'ଢ଼' => 'ଢ଼',
  'ஔ' => 'ஔ',
  'ொ' => 'ொ',
  'ோ' => 'ோ',
  'ௌ' => 'ௌ',
  'ై' => 'ై',
  'ೀ' => 'ೀ',
  'ೇ' => 'ೇ',
  'ೈ' => 'ೈ',
  'ೊ' => 'ೊ',
  'ೋ' => 'ೋ',
  'ൊ' => 'ൊ',
  'ോ' => 'ോ',
  'ൌ' => 'ൌ',
  'ේ' => 'ේ',
  'ො' => 'ො',
  'ෝ' => 'ෝ',
  'ෞ' => 'ෞ',
  'གྷ' => 'གྷ',
  'ཌྷ' => 'ཌྷ',
  'དྷ' => 'དྷ',
  'བྷ' => 'བྷ',
  'ཛྷ' => 'ཛྷ',
  'ཀྵ' => 'ཀྵ',
  'ཱི' => 'ཱི',
  'ཱུ' => 'ཱུ',
  'ྲྀ' => 'ྲྀ',
  'ླྀ' => 'ླྀ',
  'ཱྀ' => 'ཱྀ',
  'ྒྷ' => 'ྒྷ',
  'ྜྷ' => 'ྜྷ',
  'ྡྷ' => 'ྡྷ',
  'ྦྷ' => 'ྦྷ',
  'ྫྷ' => 'ྫྷ',
  'ྐྵ' => 'ྐྵ',
  'ဦ' => 'ဦ',
  'ᬆ' => 'ᬆ',
  'ᬈ' => 'ᬈ',
  'ᬊ' => 'ᬊ',
  'ᬌ' => 'ᬌ',
  'ᬎ' => 'ᬎ',
  'ᬒ' => 'ᬒ',
  'ᬻ' => 'ᬻ',
  'ᬽ' => 'ᬽ',
  'ᭀ' => 'ᭀ',
  'ᭁ' => 'ᭁ',
  'ᭃ' => 'ᭃ',
  'Ḁ' => 'Ḁ',
  'ḁ' => 'ḁ',
  'Ḃ' => 'Ḃ',
  'ḃ' => 'ḃ',
  'Ḅ' => 'Ḅ',
  'ḅ' => 'ḅ',
  'Ḇ' => 'Ḇ',
  'ḇ' => 'ḇ',
  'Ḉ' => 'Ḉ',
  'ḉ' => 'ḉ',
  'Ḋ' => 'Ḋ',
  'ḋ' => 'ḋ',
  'Ḍ' => 'Ḍ',
  'ḍ' => 'ḍ',
  'Ḏ' => 'Ḏ',
  'ḏ' => 'ḏ',
  'Ḑ' => 'Ḑ',
  'ḑ' => 'ḑ',
  'Ḓ' => 'Ḓ',
  'ḓ' => 'ḓ',
  'Ḕ' => 'Ḕ',
  'ḕ' => 'ḕ',
  'Ḗ' => 'Ḗ',
  'ḗ' => 'ḗ',
  'Ḙ' => 'Ḙ',
  'ḙ' => 'ḙ',
  'Ḛ' => 'Ḛ',
  'ḛ' => 'ḛ',
  'Ḝ' => 'Ḝ',
  'ḝ' => 'ḝ',
  'Ḟ' => 'Ḟ',
  'ḟ' => 'ḟ',
  'Ḡ' => 'Ḡ',
  'ḡ' => 'ḡ',
  'Ḣ' => 'Ḣ',
  'ḣ' => 'ḣ',
  'Ḥ' => 'Ḥ',
  'ḥ' => 'ḥ',
  'Ḧ' => 'Ḧ',
  'ḧ' => 'ḧ',
  'Ḩ' => 'Ḩ',
  'ḩ' => 'ḩ',
  'Ḫ' => 'Ḫ',
  'ḫ' => 'ḫ',
  'Ḭ' => 'Ḭ',
  'ḭ' => 'ḭ',
  'Ḯ' => 'Ḯ',
  'ḯ' => 'ḯ',
  'Ḱ' => 'Ḱ',
  'ḱ' => 'ḱ',
  'Ḳ' => 'Ḳ',
  'ḳ' => 'ḳ',
  'Ḵ' => 'Ḵ',
  'ḵ' => 'ḵ',
  'Ḷ' => 'Ḷ',
  'ḷ' => 'ḷ',
  'Ḹ' => 'Ḹ',
  'ḹ' => 'ḹ',
  'Ḻ' => 'Ḻ',
  'ḻ' => 'ḻ',
  'Ḽ' => 'Ḽ',
  'ḽ' => 'ḽ',
  'Ḿ' => 'Ḿ',
  'ḿ' => 'ḿ',
  'Ṁ' => 'Ṁ',
  'ṁ' => 'ṁ',
  'Ṃ' => 'Ṃ',
  'ṃ' => 'ṃ',
  'Ṅ' => 'Ṅ',
  'ṅ' => 'ṅ',
  'Ṇ' => 'Ṇ',
  'ṇ' => 'ṇ',
  'Ṉ' => 'Ṉ',
  'ṉ' => 'ṉ',
  'Ṋ' => 'Ṋ',
  'ṋ' => 'ṋ',
  'Ṍ' => 'Ṍ',
  'ṍ' => 'ṍ',
  'Ṏ' => 'Ṏ',
  'ṏ' => 'ṏ',
  'Ṑ' => 'Ṑ',
  'ṑ' => 'ṑ',
  'Ṓ' => 'Ṓ',
  'ṓ' => 'ṓ',
  'Ṕ' => 'Ṕ',
  'ṕ' => 'ṕ',
  'Ṗ' => 'Ṗ',
  'ṗ' => 'ṗ',
  'Ṙ' => 'Ṙ',
  'ṙ' => 'ṙ',
  'Ṛ' => 'Ṛ',
  'ṛ' => 'ṛ',
  'Ṝ' => 'Ṝ',
  'ṝ' => 'ṝ',
  'Ṟ' => 'Ṟ',
  'ṟ' => 'ṟ',
  'Ṡ' => 'Ṡ',
  'ṡ' => 'ṡ',
  'Ṣ' => 'Ṣ',
  'ṣ' => 'ṣ',
  'Ṥ' => 'Ṥ',
  'ṥ' => 'ṥ',
  'Ṧ' => 'Ṧ',
  'ṧ' => 'ṧ',
  'Ṩ' => 'Ṩ',
  'ṩ' => 'ṩ',
  'Ṫ' => 'Ṫ',
  'ṫ' => 'ṫ',
  'Ṭ' => 'Ṭ',
  'ṭ' => 'ṭ',
  'Ṯ' => 'Ṯ',
  'ṯ' => 'ṯ',
  'Ṱ' => 'Ṱ',
  'ṱ' => 'ṱ',
  'Ṳ' => 'Ṳ',
  'ṳ' => 'ṳ',
  'Ṵ' => 'Ṵ',
  'ṵ' => 'ṵ',
  'Ṷ' => 'Ṷ',
  'ṷ' => 'ṷ',
  'Ṹ' => 'Ṹ',
  'ṹ' => 'ṹ',
  'Ṻ' => 'Ṻ',
  'ṻ' => 'ṻ',
  'Ṽ' => 'Ṽ',
  'ṽ' => 'ṽ',
  'Ṿ' => 'Ṿ',
  'ṿ' => 'ṿ',
  'Ẁ' => 'Ẁ',
  'ẁ' => 'ẁ',
  'Ẃ' => 'Ẃ',
  'ẃ' => 'ẃ',
  'Ẅ' => 'Ẅ',
  'ẅ' => 'ẅ',
  'Ẇ' => 'Ẇ',
  'ẇ' => 'ẇ',
  'Ẉ' => 'Ẉ',
  'ẉ' => 'ẉ',
  'Ẋ' => 'Ẋ',
  'ẋ' => 'ẋ',
  'Ẍ' => 'Ẍ',
  'ẍ' => 'ẍ',
  'Ẏ' => 'Ẏ',
  'ẏ' => 'ẏ',
  'Ẑ' => 'Ẑ',
  'ẑ' => 'ẑ',
  'Ẓ' => 'Ẓ',
  'ẓ' => 'ẓ',
  'Ẕ' => 'Ẕ',
  'ẕ' => 'ẕ',
  'ẖ' => 'ẖ',
  'ẗ' => 'ẗ',
  'ẘ' => 'ẘ',
  'ẙ' => 'ẙ',
  'ẛ' => 'ẛ',
  'Ạ' => 'Ạ',
  'ạ' => 'ạ',
  'Ả' => 'Ả',
  'ả' => 'ả',
  'Ấ' => 'Ấ',
  'ấ' => 'ấ',
  'Ầ' => 'Ầ',
  'ầ' => 'ầ',
  'Ẩ' => 'Ẩ',
  'ẩ' => 'ẩ',
  'Ẫ' => 'Ẫ',
  'ẫ' => 'ẫ',
  'Ậ' => 'Ậ',
  'ậ' => 'ậ',
  'Ắ' => 'Ắ',
  'ắ' => 'ắ',
  'Ằ' => 'Ằ',
  'ằ' => 'ằ',
  'Ẳ' => 'Ẳ',
  'ẳ' => 'ẳ',
  'Ẵ' => 'Ẵ',
  'ẵ' => 'ẵ',
  'Ặ' => 'Ặ',
  'ặ' => 'ặ',
  'Ẹ' => 'Ẹ',
  'ẹ' => 'ẹ',
  'Ẻ' => 'Ẻ',
  'ẻ' => 'ẻ',
  'Ẽ' => 'Ẽ',
  'ẽ' => 'ẽ',
  'Ế' => 'Ế',
  'ế' => 'ế',
  'Ề' => 'Ề',
  'ề' => 'ề',
  'Ể' => 'Ể',
  'ể' => 'ể',
  'Ễ' => 'Ễ',
  'ễ' => 'ễ',
  'Ệ' => 'Ệ',
  'ệ' => 'ệ',
  'Ỉ' => 'Ỉ',
  'ỉ' => 'ỉ',
  'Ị' => 'Ị',
  'ị' => 'ị',
  'Ọ' => 'Ọ',
  'ọ' => 'ọ',
  'Ỏ' => 'Ỏ',
  'ỏ' => 'ỏ',
  'Ố' => 'Ố',
  'ố' => 'ố',
  'Ồ' => 'Ồ',
  'ồ' => 'ồ',
  'Ổ' => 'Ổ',
  'ổ' => 'ổ',
  'Ỗ' => 'Ỗ',
  'ỗ' => 'ỗ',
  'Ộ' => 'Ộ',
  'ộ' => 'ộ',
  'Ớ' => 'Ớ',
  'ớ' => 'ớ',
  'Ờ' => 'Ờ',
  'ờ' => 'ờ',
  'Ở' => 'Ở',
  'ở' => 'ở',
  'Ỡ' => 'Ỡ',
  'ỡ' => 'ỡ',
  'Ợ' => 'Ợ',
  'ợ' => 'ợ',
  'Ụ' => 'Ụ',
  'ụ' => 'ụ',
  'Ủ' => 'Ủ',
  'ủ' => 'ủ',
  'Ứ' => 'Ứ',
  'ứ' => 'ứ',
  'Ừ' => 'Ừ',
  'ừ' => 'ừ',
  'Ử' => 'Ử',
  'ử' => 'ử',
  'Ữ' => 'Ữ',
  'ữ' => 'ữ',
  'Ự' => 'Ự',
  'ự' => 'ự',
  'Ỳ' => 'Ỳ',
  'ỳ' => 'ỳ',
  'Ỵ' => 'Ỵ',
  'ỵ' => 'ỵ',
  'Ỷ' => 'Ỷ',
  'ỷ' => 'ỷ',
  'Ỹ' => 'Ỹ',
  'ỹ' => 'ỹ',
  'ἀ' => 'ἀ',
  'ἁ' => 'ἁ',
  'ἂ' => 'ἂ',
  'ἃ' => 'ἃ',
  'ἄ' => 'ἄ',
  'ἅ' => 'ἅ',
  'ἆ' => 'ἆ',
  'ἇ' => 'ἇ',
  'Ἀ' => 'Ἀ',
  'Ἁ' => 'Ἁ',
  'Ἂ' => 'Ἂ',
  'Ἃ' => 'Ἃ',
  'Ἄ' => 'Ἄ',
  'Ἅ' => 'Ἅ',
  'Ἆ' => 'Ἆ',
  'Ἇ' => 'Ἇ',
  'ἐ' => 'ἐ',
  'ἑ' => 'ἑ',
  'ἒ' => 'ἒ',
  'ἓ' => 'ἓ',
  'ἔ' => 'ἔ',
  'ἕ' => 'ἕ',
  'Ἐ' => 'Ἐ',
  'Ἑ' => 'Ἑ',
  'Ἒ' => 'Ἒ',
  'Ἓ' => 'Ἓ',
  'Ἔ' => 'Ἔ',
  'Ἕ' => 'Ἕ',
  'ἠ' => 'ἠ',
  'ἡ' => 'ἡ',
  'ἢ' => 'ἢ',
  'ἣ' => 'ἣ',
  'ἤ' => 'ἤ',
  'ἥ' => 'ἥ',
  'ἦ' => 'ἦ',
  'ἧ' => 'ἧ',
  'Ἠ' => 'Ἠ',
  'Ἡ' => 'Ἡ',
  'Ἢ' => 'Ἢ',
  'Ἣ' => 'Ἣ',
  'Ἤ' => 'Ἤ',
  'Ἥ' => 'Ἥ',
  'Ἦ' => 'Ἦ',
  'Ἧ' => 'Ἧ',
  'ἰ' => 'ἰ',
  'ἱ' => 'ἱ',
  'ἲ' => 'ἲ',
  'ἳ' => 'ἳ',
  'ἴ' => 'ἴ',
  'ἵ' => 'ἵ',
  'ἶ' => 'ἶ',
  'ἷ' => 'ἷ',
  'Ἰ' => 'Ἰ',
  'Ἱ' => 'Ἱ',
  'Ἲ' => 'Ἲ',
  'Ἳ' => 'Ἳ',
  'Ἴ' => 'Ἴ',
  'Ἵ' => 'Ἵ',
  'Ἶ' => 'Ἶ',
  'Ἷ' => 'Ἷ',
  'ὀ' => 'ὀ',
  'ὁ' => 'ὁ',
  'ὂ' => 'ὂ',
  'ὃ' => 'ὃ',
  'ὄ' => 'ὄ',
  'ὅ' => 'ὅ',
  'Ὀ' => 'Ὀ',
  'Ὁ' => 'Ὁ',
  'Ὂ' => 'Ὂ',
  'Ὃ' => 'Ὃ',
  'Ὄ' => 'Ὄ',
  'Ὅ' => 'Ὅ',
  'ὐ' => 'ὐ',
  'ὑ' => 'ὑ',
  'ὒ' => 'ὒ',
  'ὓ' => 'ὓ',
  'ὔ' => 'ὔ',
  'ὕ' => 'ὕ',
  'ὖ' => 'ὖ',
  'ὗ' => 'ὗ',
  'Ὑ' => 'Ὑ',
  'Ὓ' => 'Ὓ',
  'Ὕ' => 'Ὕ',
  'Ὗ' => 'Ὗ',
  'ὠ' => 'ὠ',
  'ὡ' => 'ὡ',
  'ὢ' => 'ὢ',
  'ὣ' => 'ὣ',
  'ὤ' => 'ὤ',
  'ὥ' => 'ὥ',
  'ὦ' => 'ὦ',
  'ὧ' => 'ὧ',
  'Ὠ' => 'Ὠ',
  'Ὡ' => 'Ὡ',
  'Ὢ' => 'Ὢ',
  'Ὣ' => 'Ὣ',
  'Ὤ' => 'Ὤ',
  'Ὥ' => 'Ὥ',
  'Ὦ' => 'Ὦ',
  'Ὧ' => 'Ὧ',
  'ὰ' => 'ὰ',
  'ά' => 'ά',
  'ὲ' => 'ὲ',
  'έ' => 'έ',
  'ὴ' => 'ὴ',
  'ή' => 'ή',
  'ὶ' => 'ὶ',
  'ί' => 'ί',
  'ὸ' => 'ὸ',
  'ό' => 'ό',
  'ὺ' => 'ὺ',
  'ύ' => 'ύ',
  'ὼ' => 'ὼ',
  'ώ' => 'ώ',
  'ᾀ' => 'ᾀ',
  'ᾁ' => 'ᾁ',
  'ᾂ' => 'ᾂ',
  'ᾃ' => 'ᾃ',
  'ᾄ' => 'ᾄ',
  'ᾅ' => 'ᾅ',
  'ᾆ' => 'ᾆ',
  'ᾇ' => 'ᾇ',
  'ᾈ' => 'ᾈ',
  'ᾉ' => 'ᾉ',
  'ᾊ' => 'ᾊ',
  'ᾋ' => 'ᾋ',
  'ᾌ' => 'ᾌ',
  'ᾍ' => 'ᾍ',
  'ᾎ' => 'ᾎ',
  'ᾏ' => 'ᾏ',
  'ᾐ' => 'ᾐ',
  'ᾑ' => 'ᾑ',
  'ᾒ' => 'ᾒ',
  'ᾓ' => 'ᾓ',
  'ᾔ' => 'ᾔ',
  'ᾕ' => 'ᾕ',
  'ᾖ' => 'ᾖ',
  'ᾗ' => 'ᾗ',
  'ᾘ' => 'ᾘ',
  'ᾙ' => 'ᾙ',
  'ᾚ' => 'ᾚ',
  'ᾛ' => 'ᾛ',
  'ᾜ' => 'ᾜ',
  'ᾝ' => 'ᾝ',
  'ᾞ' => 'ᾞ',
  'ᾟ' => 'ᾟ',
  'ᾠ' => 'ᾠ',
  'ᾡ' => 'ᾡ',
  'ᾢ' => 'ᾢ',
  'ᾣ' => 'ᾣ',
  'ᾤ' => 'ᾤ',
  'ᾥ' => 'ᾥ',
  'ᾦ' => 'ᾦ',
  'ᾧ' => 'ᾧ',
  'ᾨ' => 'ᾨ',
  'ᾩ' => 'ᾩ',
  'ᾪ' => 'ᾪ',
  'ᾫ' => 'ᾫ',
  'ᾬ' => 'ᾬ',
  'ᾭ' => 'ᾭ',
  'ᾮ' => 'ᾮ',
  'ᾯ' => 'ᾯ',
  'ᾰ' => 'ᾰ',
  'ᾱ' => 'ᾱ',
  'ᾲ' => 'ᾲ',
  'ᾳ' => 'ᾳ',
  'ᾴ' => 'ᾴ',
  'ᾶ' => 'ᾶ',
  'ᾷ' => 'ᾷ',
  'Ᾰ' => 'Ᾰ',
  'Ᾱ' => 'Ᾱ',
  'Ὰ' => 'Ὰ',
  'Ά' => 'Ά',
  'ᾼ' => 'ᾼ',
  'ι' => 'ι',
  '῁' => '῁',
  'ῂ' => 'ῂ',
  'ῃ' => 'ῃ',
  'ῄ' => 'ῄ',
  'ῆ' => 'ῆ',
  'ῇ' => 'ῇ',
  'Ὲ' => 'Ὲ',
  'Έ' => 'Έ',
  'Ὴ' => 'Ὴ',
  'Ή' => 'Ή',
  'ῌ' => 'ῌ',
  '῍' => '῍',
  '῎' => '῎',
  '῏' => '῏',
  'ῐ' => 'ῐ',
  'ῑ' => 'ῑ',
  'ῒ' => 'ῒ',
  'ΐ' => 'ΐ',
  'ῖ' => 'ῖ',
  'ῗ' => 'ῗ',
  'Ῐ' => 'Ῐ',
  'Ῑ' => 'Ῑ',
  'Ὶ' => 'Ὶ',
  'Ί' => 'Ί',
  '῝' => '῝',
  '῞' => '῞',
  '῟' => '῟',
  'ῠ' => 'ῠ',
  'ῡ' => 'ῡ',
  'ῢ' => 'ῢ',
  'ΰ' => 'ΰ',
  'ῤ' => 'ῤ',
  'ῥ' => 'ῥ',
  'ῦ' => 'ῦ',
  'ῧ' => 'ῧ',
  'Ῠ' => 'Ῠ',
  'Ῡ' => 'Ῡ',
  'Ὺ' => 'Ὺ',
  'Ύ' => 'Ύ',
  'Ῥ' => 'Ῥ',
  '῭' => '῭',
  '΅' => '΅',
  '`' => '`',
  'ῲ' => 'ῲ',
  'ῳ' => 'ῳ',
  'ῴ' => 'ῴ',
  'ῶ' => 'ῶ',
  'ῷ' => 'ῷ',
  'Ὸ' => 'Ὸ',
  'Ό' => 'Ό',
  'Ὼ' => 'Ὼ',
  'Ώ' => 'Ώ',
  'ῼ' => 'ῼ',
  '´' => '´',
  ' ' => ' ',
  ' ' => ' ',
  'Ω' => 'Ω',
  'K' => 'K',
  'Å' => 'Å',
  '↚' => '↚',
  '↛' => '↛',
  '↮' => '↮',
  '⇍' => '⇍',
  '⇎' => '⇎',
  '⇏' => '⇏',
  '∄' => '∄',
  '∉' => '∉',
  '∌' => '∌',
  '∤' => '∤',
  '∦' => '∦',
  '≁' => '≁',
  '≄' => '≄',
  '≇' => '≇',
  '≉' => '≉',
  '≠' => '≠',
  '≢' => '≢',
  '≭' => '≭',
  '≮' => '≮',
  '≯' => '≯',
  '≰' => '≰',
  '≱' => '≱',
  '≴' => '≴',
  '≵' => '≵',
  '≸' => '≸',
  '≹' => '≹',
  '⊀' => '⊀',
  '⊁' => '⊁',
  '⊄' => '⊄',
  '⊅' => '⊅',
  '⊈' => '⊈',
  '⊉' => '⊉',
  '⊬' => '⊬',
  '⊭' => '⊭',
  '⊮' => '⊮',
  '⊯' => '⊯',
  '⋠' => '⋠',
  '⋡' => '⋡',
  '⋢' => '⋢',
  '⋣' => '⋣',
  '⋪' => '⋪',
  '⋫' => '⋫',
  '⋬' => '⋬',
  '⋭' => '⋭',
  '〈' => '〈',
  '〉' => '〉',
  '⫝̸' => '⫝̸',
  'が' => 'が',
  'ぎ' => 'ぎ',
  'ぐ' => 'ぐ',
  'げ' => 'げ',
  'ご' => 'ご',
  'ざ' => 'ざ',
  'じ' => 'じ',
  'ず' => 'ず',
  'ぜ' => 'ぜ',
  'ぞ' => 'ぞ',
  'だ' => 'だ',
  'ぢ' => 'ぢ',
  'づ' => 'づ',
  'で' => 'で',
  'ど' => 'ど',
  'ば' => 'ば',
  'ぱ' => 'ぱ',
  'び' => 'び',
  'ぴ' => 'ぴ',
  'ぶ' => 'ぶ',
  'ぷ' => 'ぷ',
  'べ' => 'べ',
  'ぺ' => 'ぺ',
  'ぼ' => 'ぼ',
  'ぽ' => 'ぽ',
  'ゔ' => 'ゔ',
  'ゞ' => 'ゞ',
  'ガ' => 'ガ',
  'ギ' => 'ギ',
  'グ' => 'グ',
  'ゲ' => 'ゲ',
  'ゴ' => 'ゴ',
  'ザ' => 'ザ',
  'ジ' => 'ジ',
  'ズ' => 'ズ',
  'ゼ' => 'ゼ',
  'ゾ' => 'ゾ',
  'ダ' => 'ダ',
  'ヂ' => 'ヂ',
  'ヅ' => 'ヅ',
  'デ' => 'デ',
  'ド' => 'ド',
  'バ' => 'バ',
  'パ' => 'パ',
  'ビ' => 'ビ',
  'ピ' => 'ピ',
  'ブ' => 'ブ',
  'プ' => 'プ',
  'ベ' => 'ベ',
  'ペ' => 'ペ',
  'ボ' => 'ボ',
  'ポ' => 'ポ',
  'ヴ' => 'ヴ',
  'ヷ' => 'ヷ',
  'ヸ' => 'ヸ',
  'ヹ' => 'ヹ',
  'ヺ' => 'ヺ',
  'ヾ' => 'ヾ',
  '豈' => '豈',
  '更' => '更',
  '車' => '車',
  '賈' => '賈',
  '滑' => '滑',
  '串' => '串',
  '句' => '句',
  '龜' => '龜',
  '龜' => '龜',
  '契' => '契',
  '金' => '金',
  '喇' => '喇',
  '奈' => '奈',
  '懶' => '懶',
  '癩' => '癩',
  '羅' => '羅',
  '蘿' => '蘿',
  '螺' => '螺',
  '裸' => '裸',
  '邏' => '邏',
  '樂' => '樂',
  '洛' => '洛',
  '烙' => '烙',
  '珞' => '珞',
  '落' => '落',
  '酪' => '酪',
  '駱' => '駱',
  '亂' => '亂',
  '卵' => '卵',
  '欄' => '欄',
  '爛' => '爛',
  '蘭' => '蘭',
  '鸞' => '鸞',
  '嵐' => '嵐',
  '濫' => '濫',
  '藍' => '藍',
  '襤' => '襤',
  '拉' => '拉',
  '臘' => '臘',
  '蠟' => '蠟',
  '廊' => '廊',
  '朗' => '朗',
  '浪' => '浪',
  '狼' => '狼',
  '郎' => '郎',
  '來' => '來',
  '冷' => '冷',
  '勞' => '勞',
  '擄' => '擄',
  '櫓' => '櫓',
  '爐' => '爐',
  '盧' => '盧',
  '老' => '老',
  '蘆' => '蘆',
  '虜' => '虜',
  '路' => '路',
  '露' => '露',
  '魯' => '魯',
  '鷺' => '鷺',
  '碌' => '碌',
  '祿' => '祿',
  '綠' => '綠',
  '菉' => '菉',
  '錄' => '錄',
  '鹿' => '鹿',
  '論' => '論',
  '壟' => '壟',
  '弄' => '弄',
  '籠' => '籠',
  '聾' => '聾',
  '牢' => '牢',
  '磊' => '磊',
  '賂' => '賂',
  '雷' => '雷',
  '壘' => '壘',
  '屢' => '屢',
  '樓' => '樓',
  '淚' => '淚',
  '漏' => '漏',
  '累' => '累',
  '縷' => '縷',
  '陋' => '陋',
  '勒' => '勒',
  '肋' => '肋',
  '凜' => '凜',
  '凌' => '凌',
  '稜' => '稜',
  '綾' => '綾',
  '菱' => '菱',
  '陵' => '陵',
  '讀' => '讀',
  '拏' => '拏',
  '樂' => '樂',
  '諾' => '諾',
  '丹' => '丹',
  '寧' => '寧',
  '怒' => '怒',
  '率' => '率',
  '異' => '異',
  '北' => '北',
  '磻' => '磻',
  '便' => '便',
  '復' => '復',
  '不' => '不',
  '泌' => '泌',
  '數' => '數',
  '索' => '索',
  '參' => '參',
  '塞' => '塞',
  '省' => '省',
  '葉' => '葉',
  '說' => '說',
  '殺' => '殺',
  '辰' => '辰',
  '沈' => '沈',
  '拾' => '拾',
  '若' => '若',
  '掠' => '掠',
  '略' => '略',
  '亮' => '亮',
  '兩' => '兩',
  '凉' => '凉',
  '梁' => '梁',
  '糧' => '糧',
  '良' => '良',
  '諒' => '諒',
  '量' => '量',
  '勵' => '勵',
  '呂' => '呂',
  '女' => '女',
  '廬' => '廬',
  '旅' => '旅',
  '濾' => '濾',
  '礪' => '礪',
  '閭' => '閭',
  '驪' => '驪',
  '麗' => '麗',
  '黎' => '黎',
  '力' => '力',
  '曆' => '曆',
  '歷' => '歷',
  '轢' => '轢',
  '年' => '年',
  '憐' => '憐',
  '戀' => '戀',
  '撚' => '撚',
  '漣' => '漣',
  '煉' => '煉',
  '璉' => '璉',
  '秊' => '秊',
  '練' => '練',
  '聯' => '聯',
  '輦' => '輦',
  '蓮' => '蓮',
  '連' => '連',
  '鍊' => '鍊',
  '列' => '列',
  '劣' => '劣',
  '咽' => '咽',
  '烈' => '烈',
  '裂' => '裂',
  '說' => '說',
  '廉' => '廉',
  '念' => '念',
  '捻' => '捻',
  '殮' => '殮',
  '簾' => '簾',
  '獵' => '獵',
  '令' => '令',
  '囹' => '囹',
  '寧' => '寧',
  '嶺' => '嶺',
  '怜' => '怜',
  '玲' => '玲',
  '瑩' => '瑩',
  '羚' => '羚',
  '聆' => '聆',
  '鈴' => '鈴',
  '零' => '零',
  '靈' => '靈',
  '領' => '領',
  '例' => '例',
  '禮' => '禮',
  '醴' => '醴',
  '隸' => '隸',
  '惡' => '惡',
  '了' => '了',
  '僚' => '僚',
  '寮' => '寮',
  '尿' => '尿',
  '料' => '料',
  '樂' => '樂',
  '燎' => '燎',
  '療' => '療',
  '蓼' => '蓼',
  '遼' => '遼',
  '龍' => '龍',
  '暈' => '暈',
  '阮' => '阮',
  '劉' => '劉',
  '杻' => '杻',
  '柳' => '柳',
  '流' => '流',
  '溜' => '溜',
  '琉' => '琉',
  '留' => '留',
  '硫' => '硫',
  '紐' => '紐',
  '類' => '類',
  '六' => '六',
  '戮' => '戮',
  '陸' => '陸',
  '倫' => '倫',
  '崙' => '崙',
  '淪' => '淪',
  '輪' => '輪',
  '律' => '律',
  '慄' => '慄',
  '栗' => '栗',
  '率' => '率',
  '隆' => '隆',
  '利' => '利',
  '吏' => '吏',
  '履' => '履',
  '易' => '易',
  '李' => '李',
  '梨' => '梨',
  '泥' => '泥',
  '理' => '理',
  '痢' => '痢',
  '罹' => '罹',
  '裏' => '裏',
  '裡' => '裡',
  '里' => '里',
  '離' => '離',
  '匿' => '匿',
  '溺' => '溺',
  '吝' => '吝',
  '燐' => '燐',
  '璘' => '璘',
  '藺' => '藺',
  '隣' => '隣',
  '鱗' => '鱗',
  '麟' => '麟',
  '林' => '林',
  '淋' => '淋',
  '臨' => '臨',
  '立' => '立',
  '笠' => '笠',
  '粒' => '粒',
  '狀' => '狀',
  '炙' => '炙',
  '識' => '識',
  '什' => '什',
  '茶' => '茶',
  '刺' => '刺',
  '切' => '切',
  '度' => '度',
  '拓' => '拓',
  '糖' => '糖',
  '宅' => '宅',
  '洞' => '洞',
  '暴' => '暴',
  '輻' => '輻',
  '行' => '行',
  '降' => '降',
  '見' => '見',
  '廓' => '廓',
  '兀' => '兀',
  '嗀' => '嗀',
  '塚' => '塚',
  '晴' => '晴',
  '凞' => '凞',
  '猪' => '猪',
  '益' => '益',
  '礼' => '礼',
  '神' => '神',
  '祥' => '祥',
  '福' => '福',
  '靖' => '靖',
  '精' => '精',
  '羽' => '羽',
  '蘒' => '蘒',
  '諸' => '諸',
  '逸' => '逸',
  '都' => '都',
  '飯' => '飯',
  '飼' => '飼',
  '館' => '館',
  '鶴' => '鶴',
  '郞' => '郞',
  '隷' => '隷',
  '侮' => '侮',
  '僧' => '僧',
  '免' => '免',
  '勉' => '勉',
  '勤' => '勤',
  '卑' => '卑',
  '喝' => '喝',
  '嘆' => '嘆',
  '器' => '器',
  '塀' => '塀',
  '墨' => '墨',
  '層' => '層',
  '屮' => '屮',
  '悔' => '悔',
  '慨' => '慨',
  '憎' => '憎',
  '懲' => '懲',
  '敏' => '敏',
  '既' => '既',
  '暑' => '暑',
  '梅' => '梅',
  '海' => '海',
  '渚' => '渚',
  '漢' => '漢',
  '煮' => '煮',
  '爫' => '爫',
  '琢' => '琢',
  '碑' => '碑',
  '社' => '社',
  '祉' => '祉',
  '祈' => '祈',
  '祐' => '祐',
  '祖' => '祖',
  '祝' => '祝',
  '禍' => '禍',
  '禎' => '禎',
  '穀' => '穀',
  '突' => '突',
  '節' => '節',
  '練' => '練',
  '縉' => '縉',
  '繁' => '繁',
  '署' => '署',
  '者' => '者',
  '臭' => '臭',
  '艹' => '艹',
  '艹' => '艹',
  '著' => '著',
  '褐' => '褐',
  '視' => '視',
  '謁' => '謁',
  '謹' => '謹',
  '賓' => '賓',
  '贈' => '贈',
  '辶' => '辶',
  '逸' => '逸',
  '難' => '難',
  '響' => '響',
  '頻' => '頻',
  '恵' => '恵',
  '𤋮' => '𤋮',
  '舘' => '舘',
  '並' => '並',
  '况' => '况',
  '全' => '全',
  '侀' => '侀',
  '充' => '充',
  '冀' => '冀',
  '勇' => '勇',
  '勺' => '勺',
  '喝' => '喝',
  '啕' => '啕',
  '喙' => '喙',
  '嗢' => '嗢',
  '塚' => '塚',
  '墳' => '墳',
  '奄' => '奄',
  '奔' => '奔',
  '婢' => '婢',
  '嬨' => '嬨',
  '廒' => '廒',
  '廙' => '廙',
  '彩' => '彩',
  '徭' => '徭',
  '惘' => '惘',
  '慎' => '慎',
  '愈' => '愈',
  '憎' => '憎',
  '慠' => '慠',
  '懲' => '懲',
  '戴' => '戴',
  '揄' => '揄',
  '搜' => '搜',
  '摒' => '摒',
  '敖' => '敖',
  '晴' => '晴',
  '朗' => '朗',
  '望' => '望',
  '杖' => '杖',
  '歹' => '歹',
  '殺' => '殺',
  '流' => '流',
  '滛' => '滛',
  '滋' => '滋',
  '漢' => '漢',
  '瀞' => '瀞',
  '煮' => '煮',
  '瞧' => '瞧',
  '爵' => '爵',
  '犯' => '犯',
  '猪' => '猪',
  '瑱' => '瑱',
  '甆' => '甆',
  '画' => '画',
  '瘝' => '瘝',
  '瘟' => '瘟',
  '益' => '益',
  '盛' => '盛',
  '直' => '直',
  '睊' => '睊',
  '着' => '着',
  '磌' => '磌',
  '窱' => '窱',
  '節' => '節',
  '类' => '类',
  '絛' => '絛',
  '練' => '練',
  '缾' => '缾',
  '者' => '者',
  '荒' => '荒',
  '華' => '華',
  '蝹' => '蝹',
  '襁' => '襁',
  '覆' => '覆',
  '視' => '視',
  '調' => '調',
  '諸' => '諸',
  '請' => '請',
  '謁' => '謁',
  '諾' => '諾',
  '諭' => '諭',
  '謹' => '謹',
  '變' => '變',
  '贈' => '贈',
  '輸' => '輸',
  '遲' => '遲',
  '醙' => '醙',
  '鉶' => '鉶',
  '陼' => '陼',
  '難' => '難',
  '靖' => '靖',
  '韛' => '韛',
  '響' => '響',
  '頋' => '頋',
  '頻' => '頻',
  '鬒' => '鬒',
  '龜' => '龜',
  '𢡊' => '𢡊',
  '𢡄' => '𢡄',
  '𣏕' => '𣏕',
  '㮝' => '㮝',
  '䀘' => '䀘',
  '䀹' => '䀹',
  '𥉉' => '𥉉',
  '𥳐' => '𥳐',
  '𧻓' => '𧻓',
  '齃' => '齃',
  '龎' => '龎',
  'יִ' => 'יִ',
  'ײַ' => 'ײַ',
  'שׁ' => 'שׁ',
  'שׂ' => 'שׂ',
  'שּׁ' => 'שּׁ',
  'שּׂ' => 'שּׂ',
  'אַ' => 'אַ',
  'אָ' => 'אָ',
  'אּ' => 'אּ',
  'בּ' => 'בּ',
  'גּ' => 'גּ',
  'דּ' => 'דּ',
  'הּ' => 'הּ',
  'וּ' => 'וּ',
  'זּ' => 'זּ',
  'טּ' => 'טּ',
  'יּ' => 'יּ',
  'ךּ' => 'ךּ',
  'כּ' => 'כּ',
  'לּ' => 'לּ',
  'מּ' => 'מּ',
  'נּ' => 'נּ',
  'סּ' => 'סּ',
  'ףּ' => 'ףּ',
  'פּ' => 'פּ',
  'צּ' => 'צּ',
  'קּ' => 'קּ',
  'רּ' => 'רּ',
  'שּ' => 'שּ',
  'תּ' => 'תּ',
  'וֹ' => 'וֹ',
  'בֿ' => 'בֿ',
  'כֿ' => 'כֿ',
  'פֿ' => 'פֿ',
  '𑂚' => '𑂚',
  '𑂜' => '𑂜',
  '𑂫' => '𑂫',
  '𑄮' => '𑄮',
  '𑄯' => '𑄯',
  '𑍋' => '𑍋',
  '𑍌' => '𑍌',
  '𑒻' => '𑒻',
  '𑒼' => '𑒼',
  '𑒾' => '𑒾',
  '𑖺' => '𑖺',
  '𑖻' => '𑖻',
  '𑤸' => '𑤸',
  '𝅗𝅥' => '𝅗𝅥',
  '𝅘𝅥' => '𝅘𝅥',
  '𝅘𝅥𝅮' => '𝅘𝅥𝅮',
  '𝅘𝅥𝅯' => '𝅘𝅥𝅯',
  '𝅘𝅥𝅰' => '𝅘𝅥𝅰',
  '𝅘𝅥𝅱' => '𝅘𝅥𝅱',
  '𝅘𝅥𝅲' => '𝅘𝅥𝅲',
  '𝆹𝅥' => '𝆹𝅥',
  '𝆺𝅥' => '𝆺𝅥',
  '𝆹𝅥𝅮' => '𝆹𝅥𝅮',
  '𝆺𝅥𝅮' => '𝆺𝅥𝅮',
  '𝆹𝅥𝅯' => '𝆹𝅥𝅯',
  '𝆺𝅥𝅯' => '𝆺𝅥𝅯',
  '丽' => '丽',
  '丸' => '丸',
  '乁' => '乁',
  '𠄢' => '𠄢',
  '你' => '你',
  '侮' => '侮',
  '侻' => '侻',
  '倂' => '倂',
  '偺' => '偺',
  '備' => '備',
  '僧' => '僧',
  '像' => '像',
  '㒞' => '㒞',
  '𠘺' => '𠘺',
  '免' => '免',
  '兔' => '兔',
  '兤' => '兤',
  '具' => '具',
  '𠔜' => '𠔜',
  '㒹' => '㒹',
  '內' => '內',
  '再' => '再',
  '𠕋' => '𠕋',
  '冗' => '冗',
  '冤' => '冤',
  '仌' => '仌',
  '冬' => '冬',
  '况' => '况',
  '𩇟' => '𩇟',
  '凵' => '凵',
  '刃' => '刃',
  '㓟' => '㓟',
  '刻' => '刻',
  '剆' => '剆',
  '割' => '割',
  '剷' => '剷',
  '㔕' => '㔕',
  '勇' => '勇',
  '勉' => '勉',
  '勤' => '勤',
  '勺' => '勺',
  '包' => '包',
  '匆' => '匆',
  '北' => '北',
  '卉' => '卉',
  '卑' => '卑',
  '博' => '博',
  '即' => '即',
  '卽' => '卽',
  '卿' => '卿',
  '卿' => '卿',
  '卿' => '卿',
  '𠨬' => '𠨬',
  '灰' => '灰',
  '及' => '及',
  '叟' => '叟',
  '𠭣' => '𠭣',
  '叫' => '叫',
  '叱' => '叱',
  '吆' => '吆',
  '咞' => '咞',
  '吸' => '吸',
  '呈' => '呈',
  '周' => '周',
  '咢' => '咢',
  '哶' => '哶',
  '唐' => '唐',
  '啓' => '啓',
  '啣' => '啣',
  '善' => '善',
  '善' => '善',
  '喙' => '喙',
  '喫' => '喫',
  '喳' => '喳',
  '嗂' => '嗂',
  '圖' => '圖',
  '嘆' => '嘆',
  '圗' => '圗',
  '噑' => '噑',
  '噴' => '噴',
  '切' => '切',
  '壮' => '壮',
  '城' => '城',
  '埴' => '埴',
  '堍' => '堍',
  '型' => '型',
  '堲' => '堲',
  '報' => '報',
  '墬' => '墬',
  '𡓤' => '𡓤',
  '売' => '売',
  '壷' => '壷',
  '夆' => '夆',
  '多' => '多',
  '夢' => '夢',
  '奢' => '奢',
  '𡚨' => '𡚨',
  '𡛪' => '𡛪',
  '姬' => '姬',
  '娛' => '娛',
  '娧' => '娧',
  '姘' => '姘',
  '婦' => '婦',
  '㛮' => '㛮',
  '㛼' => '㛼',
  '嬈' => '嬈',
  '嬾' => '嬾',
  '嬾' => '嬾',
  '𡧈' => '𡧈',
  '寃' => '寃',
  '寘' => '寘',
  '寧' => '寧',
  '寳' => '寳',
  '𡬘' => '𡬘',
  '寿' => '寿',
  '将' => '将',
  '当' => '当',
  '尢' => '尢',
  '㞁' => '㞁',
  '屠' => '屠',
  '屮' => '屮',
  '峀' => '峀',
  '岍' => '岍',
  '𡷤' => '𡷤',
  '嵃' => '嵃',
  '𡷦' => '𡷦',
  '嵮' => '嵮',
  '嵫' => '嵫',
  '嵼' => '嵼',
  '巡' => '巡',
  '巢' => '巢',
  '㠯' => '㠯',
  '巽' => '巽',
  '帨' => '帨',
  '帽' => '帽',
  '幩' => '幩',
  '㡢' => '㡢',
  '𢆃' => '𢆃',
  '㡼' => '㡼',
  '庰' => '庰',
  '庳' => '庳',
  '庶' => '庶',
  '廊' => '廊',
  '𪎒' => '𪎒',
  '廾' => '廾',
  '𢌱' => '𢌱',
  '𢌱' => '𢌱',
  '舁' => '舁',
  '弢' => '弢',
  '弢' => '弢',
  '㣇' => '㣇',
  '𣊸' => '𣊸',
  '𦇚' => '𦇚',
  '形' => '形',
  '彫' => '彫',
  '㣣' => '㣣',
  '徚' => '徚',
  '忍' => '忍',
  '志' => '志',
  '忹' => '忹',
  '悁' => '悁',
  '㤺' => '㤺',
  '㤜' => '㤜',
  '悔' => '悔',
  '𢛔' => '𢛔',
  '惇' => '惇',
  '慈' => '慈',
  '慌' => '慌',
  '慎' => '慎',
  '慌' => '慌',
  '慺' => '慺',
  '憎' => '憎',
  '憲' => '憲',
  '憤' => '憤',
  '憯' => '憯',
  '懞' => '懞',
  '懲' => '懲',
  '懶' => '懶',
  '成' => '成',
  '戛' => '戛',
  '扝' => '扝',
  '抱' => '抱',
  '拔' => '拔',
  '捐' => '捐',
  '𢬌' => '𢬌',
  '挽' => '挽',
  '拼' => '拼',
  '捨' => '捨',
  '掃' => '掃',
  '揤' => '揤',
  '𢯱' => '𢯱',
  '搢' => '搢',
  '揅' => '揅',
  '掩' => '掩',
  '㨮' => '㨮',
  '摩' => '摩',
  '摾' => '摾',
  '撝' => '撝',
  '摷' => '摷',
  '㩬' => '㩬',
  '敏' => '敏',
  '敬' => '敬',
  '𣀊' => '𣀊',
  '旣' => '旣',
  '書' => '書',
  '晉' => '晉',
  '㬙' => '㬙',
  '暑' => '暑',
  '㬈' => '㬈',
  '㫤' => '㫤',
  '冒' => '冒',
  '冕' => '冕',
  '最' => '最',
  '暜' => '暜',
  '肭' => '肭',
  '䏙' => '䏙',
  '朗' => '朗',
  '望' => '望',
  '朡' => '朡',
  '杞' => '杞',
  '杓' => '杓',
  '𣏃' => '𣏃',
  '㭉' => '㭉',
  '柺' => '柺',
  '枅' => '枅',
  '桒' => '桒',
  '梅' => '梅',
  '𣑭' => '𣑭',
  '梎' => '梎',
  '栟' => '栟',
  '椔' => '椔',
  '㮝' => '㮝',
  '楂' => '楂',
  '榣' => '榣',
  '槪' => '槪',
  '檨' => '檨',
  '𣚣' => '𣚣',
  '櫛' => '櫛',
  '㰘' => '㰘',
  '次' => '次',
  '𣢧' => '𣢧',
  '歔' => '歔',
  '㱎' => '㱎',
  '歲' => '歲',
  '殟' => '殟',
  '殺' => '殺',
  '殻' => '殻',
  '𣪍' => '𣪍',
  '𡴋' => '𡴋',
  '𣫺' => '𣫺',
  '汎' => '汎',
  '𣲼' => '𣲼',
  '沿' => '沿',
  '泍' => '泍',
  '汧' => '汧',
  '洖' => '洖',
  '派' => '派',
  '海' => '海',
  '流' => '流',
  '浩' => '浩',
  '浸' => '浸',
  '涅' => '涅',
  '𣴞' => '𣴞',
  '洴' => '洴',
  '港' => '港',
  '湮' => '湮',
  '㴳' => '㴳',
  '滋' => '滋',
  '滇' => '滇',
  '𣻑' => '𣻑',
  '淹' => '淹',
  '潮' => '潮',
  '𣽞' => '𣽞',
  '𣾎' => '𣾎',
  '濆' => '濆',
  '瀹' => '瀹',
  '瀞' => '瀞',
  '瀛' => '瀛',
  '㶖' => '㶖',
  '灊' => '灊',
  '災' => '災',
  '灷' => '灷',
  '炭' => '炭',
  '𠔥' => '𠔥',
  '煅' => '煅',
  '𤉣' => '𤉣',
  '熜' => '熜',
  '𤎫' => '𤎫',
  '爨' => '爨',
  '爵' => '爵',
  '牐' => '牐',
  '𤘈' => '𤘈',
  '犀' => '犀',
  '犕' => '犕',
  '𤜵' => '𤜵',
  '𤠔' => '𤠔',
  '獺' => '獺',
  '王' => '王',
  '㺬' => '㺬',
  '玥' => '玥',
  '㺸' => '㺸',
  '㺸' => '㺸',
  '瑇' => '瑇',
  '瑜' => '瑜',
  '瑱' => '瑱',
  '璅' => '璅',
  '瓊' => '瓊',
  '㼛' => '㼛',
  '甤' => '甤',
  '𤰶' => '𤰶',
  '甾' => '甾',
  '𤲒' => '𤲒',
  '異' => '異',
  '𢆟' => '𢆟',
  '瘐' => '瘐',
  '𤾡' => '𤾡',
  '𤾸' => '𤾸',
  '𥁄' => '𥁄',
  '㿼' => '㿼',
  '䀈' => '䀈',
  '直' => '直',
  '𥃳' => '𥃳',
  '𥃲' => '𥃲',
  '𥄙' => '𥄙',
  '𥄳' => '𥄳',
  '眞' => '眞',
  '真' => '真',
  '真' => '真',
  '睊' => '睊',
  '䀹' => '䀹',
  '瞋' => '瞋',
  '䁆' => '䁆',
  '䂖' => '䂖',
  '𥐝' => '𥐝',
  '硎' => '硎',
  '碌' => '碌',
  '磌' => '磌',
  '䃣' => '䃣',
  '𥘦' => '𥘦',
  '祖' => '祖',
  '𥚚' => '𥚚',
  '𥛅' => '𥛅',
  '福' => '福',
  '秫' => '秫',
  '䄯' => '䄯',
  '穀' => '穀',
  '穊' => '穊',
  '穏' => '穏',
  '𥥼' => '𥥼',
  '𥪧' => '𥪧',
  '𥪧' => '𥪧',
  '竮' => '竮',
  '䈂' => '䈂',
  '𥮫' => '𥮫',
  '篆' => '篆',
  '築' => '築',
  '䈧' => '䈧',
  '𥲀' => '𥲀',
  '糒' => '糒',
  '䊠' => '䊠',
  '糨' => '糨',
  '糣' => '糣',
  '紀' => '紀',
  '𥾆' => '𥾆',
  '絣' => '絣',
  '䌁' => '䌁',
  '緇' => '緇',
  '縂' => '縂',
  '繅' => '繅',
  '䌴' => '䌴',
  '𦈨' => '𦈨',
  '𦉇' => '𦉇',
  '䍙' => '䍙',
  '𦋙' => '𦋙',
  '罺' => '罺',
  '𦌾' => '𦌾',
  '羕' => '羕',
  '翺' => '翺',
  '者' => '者',
  '𦓚' => '𦓚',
  '𦔣' => '𦔣',
  '聠' => '聠',
  '𦖨' => '𦖨',
  '聰' => '聰',
  '𣍟' => '𣍟',
  '䏕' => '䏕',
  '育' => '育',
  '脃' => '脃',
  '䐋' => '䐋',
  '脾' => '脾',
  '媵' => '媵',
  '𦞧' => '𦞧',
  '𦞵' => '𦞵',
  '𣎓' => '𣎓',
  '𣎜' => '𣎜',
  '舁' => '舁',
  '舄' => '舄',
  '辞' => '辞',
  '䑫' => '䑫',
  '芑' => '芑',
  '芋' => '芋',
  '芝' => '芝',
  '劳' => '劳',
  '花' => '花',
  '芳' => '芳',
  '芽' => '芽',
  '苦' => '苦',
  '𦬼' => '𦬼',
  '若' => '若',
  '茝' => '茝',
  '荣' => '荣',
  '莭' => '莭',
  '茣' => '茣',
  '莽' => '莽',
  '菧' => '菧',
  '著' => '著',
  '荓' => '荓',
  '菊' => '菊',
  '菌' => '菌',
  '菜' => '菜',
  '𦰶' => '𦰶',
  '𦵫' => '𦵫',
  '𦳕' => '𦳕',
  '䔫' => '䔫',
  '蓱' => '蓱',
  '蓳' => '蓳',
  '蔖' => '蔖',
  '𧏊' => '𧏊',
  '蕤' => '蕤',
  '𦼬' => '𦼬',
  '䕝' => '䕝',
  '䕡' => '䕡',
  '𦾱' => '𦾱',
  '𧃒' => '𧃒',
  '䕫' => '䕫',
  '虐' => '虐',
  '虜' => '虜',
  '虧' => '虧',
  '虩' => '虩',
  '蚩' => '蚩',
  '蚈' => '蚈',
  '蜎' => '蜎',
  '蛢' => '蛢',
  '蝹' => '蝹',
  '蜨' => '蜨',
  '蝫' => '蝫',
  '螆' => '螆',
  '䗗' => '䗗',
  '蟡' => '蟡',
  '蠁' => '蠁',
  '䗹' => '䗹',
  '衠' => '衠',
  '衣' => '衣',
  '𧙧' => '𧙧',
  '裗' => '裗',
  '裞' => '裞',
  '䘵' => '䘵',
  '裺' => '裺',
  '㒻' => '㒻',
  '𧢮' => '𧢮',
  '𧥦' => '𧥦',
  '䚾' => '䚾',
  '䛇' => '䛇',
  '誠' => '誠',
  '諭' => '諭',
  '變' => '變',
  '豕' => '豕',
  '𧲨' => '𧲨',
  '貫' => '貫',
  '賁' => '賁',
  '贛' => '贛',
  '起' => '起',
  '𧼯' => '𧼯',
  '𠠄' => '𠠄',
  '跋' => '跋',
  '趼' => '趼',
  '跰' => '跰',
  '𠣞' => '𠣞',
  '軔' => '軔',
  '輸' => '輸',
  '𨗒' => '𨗒',
  '𨗭' => '𨗭',
  '邔' => '邔',
  '郱' => '郱',
  '鄑' => '鄑',
  '𨜮' => '𨜮',
  '鄛' => '鄛',
  '鈸' => '鈸',
  '鋗' => '鋗',
  '鋘' => '鋘',
  '鉼' => '鉼',
  '鏹' => '鏹',
  '鐕' => '鐕',
  '𨯺' => '𨯺',
  '開' => '開',
  '䦕' => '䦕',
  '閷' => '閷',
  '𨵷' => '𨵷',
  '䧦' => '䧦',
  '雃' => '雃',
  '嶲' => '嶲',
  '霣' => '霣',
  '𩅅' => '𩅅',
  '𩈚' => '𩈚',
  '䩮' => '䩮',
  '䩶' => '䩶',
  '韠' => '韠',
  '𩐊' => '𩐊',
  '䪲' => '䪲',
  '𩒖' => '𩒖',
  '頋' => '頋',
  '頋' => '頋',
  '頩' => '頩',
  '𩖶' => '𩖶',
  '飢' => '飢',
  '䬳' => '䬳',
  '餩' => '餩',
  '馧' => '馧',
  '駂' => '駂',
  '駾' => '駾',
  '䯎' => '䯎',
  '𩬰' => '𩬰',
  '鬒' => '鬒',
  '鱀' => '鱀',
  '鳽' => '鳽',
  '䳎' => '䳎',
  '䳭' => '䳭',
  '鵧' => '鵧',
  '𪃎' => '𪃎',
  '䳸' => '䳸',
  '𪄅' => '𪄅',
  '𪈎' => '𪈎',
  '𪊑' => '𪊑',
  '麻' => '麻',
  '䵖' => '䵖',
  '黹' => '黹',
  '黾' => '黾',
  '鼅' => '鼅',
  '鼏' => '鼏',
  '鼖' => '鼖',
  '鼻' => '鼻',
  '𪘀' => '𪘀',
);
<?php

return array (
  ' ' => ' ',
  '¨' => ' ̈',
  'ª' => 'a',
  '¯' => ' ̄',
  '²' => '2',
  '³' => '3',
  '´' => ' ́',
  'µ' => 'μ',
  '¸' => ' ̧',
  '¹' => '1',
  'º' => 'o',
  '¼' => '1⁄4',
  '½' => '1⁄2',
  '¾' => '3⁄4',
  'Ĳ' => 'IJ',
  'ĳ' => 'ij',
  'Ŀ' => 'L·',
  'ŀ' => 'l·',
  'ŉ' => 'ʼn',
  'ſ' => 's',
  'Ǆ' => 'DŽ',
  'ǅ' => 'Dž',
  'ǆ' => 'dž',
  'Ǉ' => 'LJ',
  'ǈ' => 'Lj',
  'ǉ' => 'lj',
  'Ǌ' => 'NJ',
  'ǋ' => 'Nj',
  'ǌ' => 'nj',
  'Ǳ' => 'DZ',
  'ǲ' => 'Dz',
  'ǳ' => 'dz',
  'ʰ' => 'h',
  'ʱ' => 'ɦ',
  'ʲ' => 'j',
  'ʳ' => 'r',
  'ʴ' => 'ɹ',
  'ʵ' => 'ɻ',
  'ʶ' => 'ʁ',
  'ʷ' => 'w',
  'ʸ' => 'y',
  '˘' => ' ̆',
  '˙' => ' ̇',
  '˚' => ' ̊',
  '˛' => ' ̨',
  '˜' => ' ̃',
  '˝' => ' ̋',
  'ˠ' => 'ɣ',
  'ˡ' => 'l',
  'ˢ' => 's',
  'ˣ' => 'x',
  'ˤ' => 'ʕ',
  'ͺ' => ' ͅ',
  '΄' => ' ́',
  '΅' => ' ̈́',
  'ϐ' => 'β',
  'ϑ' => 'θ',
  'ϒ' => 'Υ',
  'ϓ' => 'Ύ',
  'ϔ' => 'Ϋ',
  'ϕ' => 'φ',
  'ϖ' => 'π',
  'ϰ' => 'κ',
  'ϱ' => 'ρ',
  'ϲ' => 'ς',
  'ϴ' => 'Θ',
  'ϵ' => 'ε',
  'Ϲ' => 'Σ',
  'և' => 'եւ',
  'ٵ' => 'اٴ',
  'ٶ' => 'وٴ',
  'ٷ' => 'ۇٴ',
  'ٸ' => 'يٴ',
  'ำ' => 'ํา',
  'ຳ' => 'ໍາ',
  'ໜ' => 'ຫນ',
  'ໝ' => 'ຫມ',
  '༌' => '་',
  'ཷ' => 'ྲཱྀ',
  'ཹ' => 'ླཱྀ',
  'ჼ' => 'ნ',
  'ᴬ' => 'A',
  'ᴭ' => 'Æ',
  'ᴮ' => 'B',
  'ᴰ' => 'D',
  'ᴱ' => 'E',
  'ᴲ' => 'Ǝ',
  'ᴳ' => 'G',
  'ᴴ' => 'H',
  'ᴵ' => 'I',
  'ᴶ' => 'J',
  'ᴷ' => 'K',
  'ᴸ' => 'L',
  'ᴹ' => 'M',
  'ᴺ' => 'N',
  'ᴼ' => 'O',
  'ᴽ' => 'Ȣ',
  'ᴾ' => 'P',
  'ᴿ' => 'R',
  'ᵀ' => 'T',
  'ᵁ' => 'U',
  'ᵂ' => 'W',
  'ᵃ' => 'a',
  'ᵄ' => 'ɐ',
  'ᵅ' => 'ɑ',
  'ᵆ' => 'ᴂ',
  'ᵇ' => 'b',
  'ᵈ' => 'd',
  'ᵉ' => 'e',
  'ᵊ' => 'ə',
  'ᵋ' => 'ɛ',
  'ᵌ' => 'ɜ',
  'ᵍ' => 'g',
  'ᵏ' => 'k',
  'ᵐ' => 'm',
  'ᵑ' => 'ŋ',
  'ᵒ' => 'o',
  'ᵓ' => 'ɔ',
  'ᵔ' => 'ᴖ',
  'ᵕ' => 'ᴗ',
  'ᵖ' => 'p',
  'ᵗ' => 't',
  'ᵘ' => 'u',
  'ᵙ' => 'ᴝ',
  'ᵚ' => 'ɯ',
  'ᵛ' => 'v',
  'ᵜ' => 'ᴥ',
  'ᵝ' => 'β',
  'ᵞ' => 'γ',
  'ᵟ' => 'δ',
  'ᵠ' => 'φ',
  'ᵡ' => 'χ',
  'ᵢ' => 'i',
  'ᵣ' => 'r',
  'ᵤ' => 'u',
  'ᵥ' => 'v',
  'ᵦ' => 'β',
  'ᵧ' => 'γ',
  'ᵨ' => 'ρ',
  'ᵩ' => 'φ',
  'ᵪ' => 'χ',
  'ᵸ' => 'н',
  'ᶛ' => 'ɒ',
  'ᶜ' => 'c',
  'ᶝ' => 'ɕ',
  'ᶞ' => 'ð',
  'ᶟ' => 'ɜ',
  'ᶠ' => 'f',
  'ᶡ' => 'ɟ',
  'ᶢ' => 'ɡ',
  'ᶣ' => 'ɥ',
  'ᶤ' => 'ɨ',
  'ᶥ' => 'ɩ',
  'ᶦ' => 'ɪ',
  'ᶧ' => 'ᵻ',
  'ᶨ' => 'ʝ',
  'ᶩ' => 'ɭ',
  'ᶪ' => 'ᶅ',
  'ᶫ' => 'ʟ',
  'ᶬ' => 'ɱ',
  'ᶭ' => 'ɰ',
  'ᶮ' => 'ɲ',
  'ᶯ' => 'ɳ',
  'ᶰ' => 'ɴ',
  'ᶱ' => 'ɵ',
  'ᶲ' => 'ɸ',
  'ᶳ' => 'ʂ',
  'ᶴ' => 'ʃ',
  'ᶵ' => 'ƫ',
  'ᶶ' => 'ʉ',
  'ᶷ' => 'ʊ',
  'ᶸ' => 'ᴜ',
  'ᶹ' => 'ʋ',
  'ᶺ' => 'ʌ',
  'ᶻ' => 'z',
  'ᶼ' => 'ʐ',
  'ᶽ' => 'ʑ',
  'ᶾ' => 'ʒ',
  'ᶿ' => 'θ',
  'ẚ' => 'aʾ',
  'ẛ' => 'ṡ',
  '᾽' => ' ̓',
  '᾿' => ' ̓',
  '῀' => ' ͂',
  '῁' => ' ̈͂',
  '῍' => ' ̓̀',
  '῎' => ' ̓́',
  '῏' => ' ̓͂',
  '῝' => ' ̔̀',
  '῞' => ' ̔́',
  '῟' => ' ̔͂',
  '῭' => ' ̈̀',
  '΅' => ' ̈́',
  '´' => ' ́',
  '῾' => ' ̔',
  ' ' => ' ',
  ' ' => ' ',
  ' ' => ' ',
  ' ' => ' ',
  ' ' => ' ',
  ' ' => ' ',
  ' ' => ' ',
  ' ' => ' ',
  ' ' => ' ',
  ' ' => ' ',
  ' ' => ' ',
  '‑' => '‐',
  '‗' => ' ̳',
  '․' => '.',
  '‥' => '..',
  '…' => '...',
  ' ' => ' ',
  '″' => '′′',
  '‴' => '′′′',
  '‶' => '‵‵',
  '‷' => '‵‵‵',
  '‼' => '!!',
  '‾' => ' ̅',
  '⁇' => '??',
  '⁈' => '?!',
  '⁉' => '!?',
  '⁗' => '′′′′',
  ' ' => ' ',
  '⁰' => '0',
  'ⁱ' => 'i',
  '⁴' => '4',
  '⁵' => '5',
  '⁶' => '6',
  '⁷' => '7',
  '⁸' => '8',
  '⁹' => '9',
  '⁺' => '+',
  '⁻' => '−',
  '⁼' => '=',
  '⁽' => '(',
  '⁾' => ')',
  'ⁿ' => 'n',
  '₀' => '0',
  '₁' => '1',
  '₂' => '2',
  '₃' => '3',
  '₄' => '4',
  '₅' => '5',
  '₆' => '6',
  '₇' => '7',
  '₈' => '8',
  '₉' => '9',
  '₊' => '+',
  '₋' => '−',
  '₌' => '=',
  '₍' => '(',
  '₎' => ')',
  'ₐ' => 'a',
  'ₑ' => 'e',
  'ₒ' => 'o',
  'ₓ' => 'x',
  'ₔ' => 'ə',
  'ₕ' => 'h',
  'ₖ' => 'k',
  'ₗ' => 'l',
  'ₘ' => 'm',
  'ₙ' => 'n',
  'ₚ' => 'p',
  'ₛ' => 's',
  'ₜ' => 't',
  '₨' => 'Rs',
  '℀' => 'a/c',
  '℁' => 'a/s',
  'ℂ' => 'C',
  '℃' => '°C',
  '℅' => 'c/o',
  '℆' => 'c/u',
  'ℇ' => 'Ɛ',
  '℉' => '°F',
  'ℊ' => 'g',
  'ℋ' => 'H',
  'ℌ' => 'H',
  'ℍ' => 'H',
  'ℎ' => 'h',
  'ℏ' => 'ħ',
  'ℐ' => 'I',
  'ℑ' => 'I',
  'ℒ' => 'L',
  'ℓ' => 'l',
  'ℕ' => 'N',
  '№' => 'No',
  'ℙ' => 'P',
  'ℚ' => 'Q',
  'ℛ' => 'R',
  'ℜ' => 'R',
  'ℝ' => 'R',
  '℠' => 'SM',
  '℡' => 'TEL',
  '™' => 'TM',
  'ℤ' => 'Z',
  'ℨ' => 'Z',
  'ℬ' => 'B',
  'ℭ' => 'C',
  'ℯ' => 'e',
  'ℰ' => 'E',
  'ℱ' => 'F',
  'ℳ' => 'M',
  'ℴ' => 'o',
  'ℵ' => 'א',
  'ℶ' => 'ב',
  'ℷ' => 'ג',
  'ℸ' => 'ד',
  'ℹ' => 'i',
  '℻' => 'FAX',
  'ℼ' => 'π',
  'ℽ' => 'γ',
  'ℾ' => 'Γ',
  'ℿ' => 'Π',
  '⅀' => '∑',
  'ⅅ' => 'D',
  'ⅆ' => 'd',
  'ⅇ' => 'e',
  'ⅈ' => 'i',
  'ⅉ' => 'j',
  '⅐' => '1⁄7',
  '⅑' => '1⁄9',
  '⅒' => '1⁄10',
  '⅓' => '1⁄3',
  '⅔' => '2⁄3',
  '⅕' => '1⁄5',
  '⅖' => '2⁄5',
  '⅗' => '3⁄5',
  '⅘' => '4⁄5',
  '⅙' => '1⁄6',
  '⅚' => '5⁄6',
  '⅛' => '1⁄8',
  '⅜' => '3⁄8',
  '⅝' => '5⁄8',
  '⅞' => '7⁄8',
  '⅟' => '1⁄',
  'Ⅰ' => 'I',
  'Ⅱ' => 'II',
  'Ⅲ' => 'III',
  'Ⅳ' => 'IV',
  'Ⅴ' => 'V',
  'Ⅵ' => 'VI',
  'Ⅶ' => 'VII',
  'Ⅷ' => 'VIII',
  'Ⅸ' => 'IX',
  'Ⅹ' => 'X',
  'Ⅺ' => 'XI',
  'Ⅻ' => 'XII',
  'Ⅼ' => 'L',
  'Ⅽ' => 'C',
  'Ⅾ' => 'D',
  'Ⅿ' => 'M',
  'ⅰ' => 'i',
  'ⅱ' => 'ii',
  'ⅲ' => 'iii',
  'ⅳ' => 'iv',
  'ⅴ' => 'v',
  'ⅵ' => 'vi',
  'ⅶ' => 'vii',
  'ⅷ' => 'viii',
  'ⅸ' => 'ix',
  'ⅹ' => 'x',
  'ⅺ' => 'xi',
  'ⅻ' => 'xii',
  'ⅼ' => 'l',
  'ⅽ' => 'c',
  'ⅾ' => 'd',
  'ⅿ' => 'm',
  '↉' => '0⁄3',
  '∬' => '∫∫',
  '∭' => '∫∫∫',
  '∯' => '∮∮',
  '∰' => '∮∮∮',
  '①' => '1',
  '②' => '2',
  '③' => '3',
  '④' => '4',
  '⑤' => '5',
  '⑥' => '6',
  '⑦' => '7',
  '⑧' => '8',
  '⑨' => '9',
  '⑩' => '10',
  '⑪' => '11',
  '⑫' => '12',
  '⑬' => '13',
  '⑭' => '14',
  '⑮' => '15',
  '⑯' => '16',
  '⑰' => '17',
  '⑱' => '18',
  '⑲' => '19',
  '⑳' => '20',
  '⑴' => '(1)',
  '⑵' => '(2)',
  '⑶' => '(3)',
  '⑷' => '(4)',
  '⑸' => '(5)',
  '⑹' => '(6)',
  '⑺' => '(7)',
  '⑻' => '(8)',
  '⑼' => '(9)',
  '⑽' => '(10)',
  '⑾' => '(11)',
  '⑿' => '(12)',
  '⒀' => '(13)',
  '⒁' => '(14)',
  '⒂' => '(15)',
  '⒃' => '(16)',
  '⒄' => '(17)',
  '⒅' => '(18)',
  '⒆' => '(19)',
  '⒇' => '(20)',
  '⒈' => '1.',
  '⒉' => '2.',
  '⒊' => '3.',
  '⒋' => '4.',
  '⒌' => '5.',
  '⒍' => '6.',
  '⒎' => '7.',
  '⒏' => '8.',
  '⒐' => '9.',
  '⒑' => '10.',
  '⒒' => '11.',
  '⒓' => '12.',
  '⒔' => '13.',
  '⒕' => '14.',
  '⒖' => '15.',
  '⒗' => '16.',
  '⒘' => '17.',
  '⒙' => '18.',
  '⒚' => '19.',
  '⒛' => '20.',
  '⒜' => '(a)',
  '⒝' => '(b)',
  '⒞' => '(c)',
  '⒟' => '(d)',
  '⒠' => '(e)',
  '⒡' => '(f)',
  '⒢' => '(g)',
  '⒣' => '(h)',
  '⒤' => '(i)',
  '⒥' => '(j)',
  '⒦' => '(k)',
  '⒧' => '(l)',
  '⒨' => '(m)',
  '⒩' => '(n)',
  '⒪' => '(o)',
  '⒫' => '(p)',
  '⒬' => '(q)',
  '⒭' => '(r)',
  '⒮' => '(s)',
  '⒯' => '(t)',
  '⒰' => '(u)',
  '⒱' => '(v)',
  '⒲' => '(w)',
  '⒳' => '(x)',
  '⒴' => '(y)',
  '⒵' => '(z)',
  'Ⓐ' => 'A',
  'Ⓑ' => 'B',
  'Ⓒ' => 'C',
  'Ⓓ' => 'D',
  'Ⓔ' => 'E',
  'Ⓕ' => 'F',
  'Ⓖ' => 'G',
  'Ⓗ' => 'H',
  'Ⓘ' => 'I',
  'Ⓙ' => 'J',
  'Ⓚ' => 'K',
  'Ⓛ' => 'L',
  'Ⓜ' => 'M',
  'Ⓝ' => 'N',
  'Ⓞ' => 'O',
  'Ⓟ' => 'P',
  'Ⓠ' => 'Q',
  'Ⓡ' => 'R',
  'Ⓢ' => 'S',
  'Ⓣ' => 'T',
  'Ⓤ' => 'U',
  'Ⓥ' => 'V',
  'Ⓦ' => 'W',
  'Ⓧ' => 'X',
  'Ⓨ' => 'Y',
  'Ⓩ' => 'Z',
  'ⓐ' => 'a',
  'ⓑ' => 'b',
  'ⓒ' => 'c',
  'ⓓ' => 'd',
  'ⓔ' => 'e',
  'ⓕ' => 'f',
  'ⓖ' => 'g',
  'ⓗ' => 'h',
  'ⓘ' => 'i',
  'ⓙ' => 'j',
  'ⓚ' => 'k',
  'ⓛ' => 'l',
  'ⓜ' => 'm',
  'ⓝ' => 'n',
  'ⓞ' => 'o',
  'ⓟ' => 'p',
  'ⓠ' => 'q',
  'ⓡ' => 'r',
  'ⓢ' => 's',
  'ⓣ' => 't',
  'ⓤ' => 'u',
  'ⓥ' => 'v',
  'ⓦ' => 'w',
  'ⓧ' => 'x',
  'ⓨ' => 'y',
  'ⓩ' => 'z',
  '⓪' => '0',
  '⨌' => '∫∫∫∫',
  '⩴' => '::=',
  '⩵' => '==',
  '⩶' => '===',
  'ⱼ' => 'j',
  'ⱽ' => 'V',
  'ⵯ' => 'ⵡ',
  '⺟' => '母',
  '⻳' => '龟',
  '⼀' => '一',
  '⼁' => '丨',
  '⼂' => '丶',
  '⼃' => '丿',
  '⼄' => '乙',
  '⼅' => '亅',
  '⼆' => '二',
  '⼇' => '亠',
  '⼈' => '人',
  '⼉' => '儿',
  '⼊' => '入',
  '⼋' => '八',
  '⼌' => '冂',
  '⼍' => '冖',
  '⼎' => '冫',
  '⼏' => '几',
  '⼐' => '凵',
  '⼑' => '刀',
  '⼒' => '力',
  '⼓' => '勹',
  '⼔' => '匕',
  '⼕' => '匚',
  '⼖' => '匸',
  '⼗' => '十',
  '⼘' => '卜',
  '⼙' => '卩',
  '⼚' => '厂',
  '⼛' => '厶',
  '⼜' => '又',
  '⼝' => '口',
  '⼞' => '囗',
  '⼟' => '土',
  '⼠' => '士',
  '⼡' => '夂',
  '⼢' => '夊',
  '⼣' => '夕',
  '⼤' => '大',
  '⼥' => '女',
  '⼦' => '子',
  '⼧' => '宀',
  '⼨' => '寸',
  '⼩' => '小',
  '⼪' => '尢',
  '⼫' => '尸',
  '⼬' => '屮',
  '⼭' => '山',
  '⼮' => '巛',
  '⼯' => '工',
  '⼰' => '己',
  '⼱' => '巾',
  '⼲' => '干',
  '⼳' => '幺',
  '⼴' => '广',
  '⼵' => '廴',
  '⼶' => '廾',
  '⼷' => '弋',
  '⼸' => '弓',
  '⼹' => '彐',
  '⼺' => '彡',
  '⼻' => '彳',
  '⼼' => '心',
  '⼽' => '戈',
  '⼾' => '戶',
  '⼿' => '手',
  '⽀' => '支',
  '⽁' => '攴',
  '⽂' => '文',
  '⽃' => '斗',
  '⽄' => '斤',
  '⽅' => '方',
  '⽆' => '无',
  '⽇' => '日',
  '⽈' => '曰',
  '⽉' => '月',
  '⽊' => '木',
  '⽋' => '欠',
  '⽌' => '止',
  '⽍' => '歹',
  '⽎' => '殳',
  '⽏' => '毋',
  '⽐' => '比',
  '⽑' => '毛',
  '⽒' => '氏',
  '⽓' => '气',
  '⽔' => '水',
  '⽕' => '火',
  '⽖' => '爪',
  '⽗' => '父',
  '⽘' => '爻',
  '⽙' => '爿',
  '⽚' => '片',
  '⽛' => '牙',
  '⽜' => '牛',
  '⽝' => '犬',
  '⽞' => '玄',
  '⽟' => '玉',
  '⽠' => '瓜',
  '⽡' => '瓦',
  '⽢' => '甘',
  '⽣' => '生',
  '⽤' => '用',
  '⽥' => '田',
  '⽦' => '疋',
  '⽧' => '疒',
  '⽨' => '癶',
  '⽩' => '白',
  '⽪' => '皮',
  '⽫' => '皿',
  '⽬' => '目',
  '⽭' => '矛',
  '⽮' => '矢',
  '⽯' => '石',
  '⽰' => '示',
  '⽱' => '禸',
  '⽲' => '禾',
  '⽳' => '穴',
  '⽴' => '立',
  '⽵' => '竹',
  '⽶' => '米',
  '⽷' => '糸',
  '⽸' => '缶',
  '⽹' => '网',
  '⽺' => '羊',
  '⽻' => '羽',
  '⽼' => '老',
  '⽽' => '而',
  '⽾' => '耒',
  '⽿' => '耳',
  '⾀' => '聿',
  '⾁' => '肉',
  '⾂' => '臣',
  '⾃' => '自',
  '⾄' => '至',
  '⾅' => '臼',
  '⾆' => '舌',
  '⾇' => '舛',
  '⾈' => '舟',
  '⾉' => '艮',
  '⾊' => '色',
  '⾋' => '艸',
  '⾌' => '虍',
  '⾍' => '虫',
  '⾎' => '血',
  '⾏' => '行',
  '⾐' => '衣',
  '⾑' => '襾',
  '⾒' => '見',
  '⾓' => '角',
  '⾔' => '言',
  '⾕' => '谷',
  '⾖' => '豆',
  '⾗' => '豕',
  '⾘' => '豸',
  '⾙' => '貝',
  '⾚' => '赤',
  '⾛' => '走',
  '⾜' => '足',
  '⾝' => '身',
  '⾞' => '車',
  '⾟' => '辛',
  '⾠' => '辰',
  '⾡' => '辵',
  '⾢' => '邑',
  '⾣' => '酉',
  '⾤' => '釆',
  '⾥' => '里',
  '⾦' => '金',
  '⾧' => '長',
  '⾨' => '門',
  '⾩' => '阜',
  '⾪' => '隶',
  '⾫' => '隹',
  '⾬' => '雨',
  '⾭' => '靑',
  '⾮' => '非',
  '⾯' => '面',
  '⾰' => '革',
  '⾱' => '韋',
  '⾲' => '韭',
  '⾳' => '音',
  '⾴' => '頁',
  '⾵' => '風',
  '⾶' => '飛',
  '⾷' => '食',
  '⾸' => '首',
  '⾹' => '香',
  '⾺' => '馬',
  '⾻' => '骨',
  '⾼' => '高',
  '⾽' => '髟',
  '⾾' => '鬥',
  '⾿' => '鬯',
  '⿀' => '鬲',
  '⿁' => '鬼',
  '⿂' => '魚',
  '⿃' => '鳥',
  '⿄' => '鹵',
  '⿅' => '鹿',
  '⿆' => '麥',
  '⿇' => '麻',
  '⿈' => '黃',
  '⿉' => '黍',
  '⿊' => '黑',
  '⿋' => '黹',
  '⿌' => '黽',
  '⿍' => '鼎',
  '⿎' => '鼓',
  '⿏' => '鼠',
  '⿐' => '鼻',
  '⿑' => '齊',
  '⿒' => '齒',
  '⿓' => '龍',
  '⿔' => '龜',
  '⿕' => '龠',
  '　' => ' ',
  '〶' => '〒',
  '〸' => '十',
  '〹' => '卄',
  '〺' => '卅',
  '゛' => ' ゙',
  '゜' => ' ゚',
  'ゟ' => 'より',
  'ヿ' => 'コト',
  'ㄱ' => 'ᄀ',
  'ㄲ' => 'ᄁ',
  'ㄳ' => 'ᆪ',
  'ㄴ' => 'ᄂ',
  'ㄵ' => 'ᆬ',
  'ㄶ' => 'ᆭ',
  'ㄷ' => 'ᄃ',
  'ㄸ' => 'ᄄ',
  'ㄹ' => 'ᄅ',
  'ㄺ' => 'ᆰ',
  'ㄻ' => 'ᆱ',
  'ㄼ' => 'ᆲ',
  'ㄽ' => 'ᆳ',
  'ㄾ' => 'ᆴ',
  'ㄿ' => 'ᆵ',
  'ㅀ' => 'ᄚ',
  'ㅁ' => 'ᄆ',
  'ㅂ' => 'ᄇ',
  'ㅃ' => 'ᄈ',
  'ㅄ' => 'ᄡ',
  'ㅅ' => 'ᄉ',
  'ㅆ' => 'ᄊ',
  'ㅇ' => 'ᄋ',
  'ㅈ' => 'ᄌ',
  'ㅉ' => 'ᄍ',
  'ㅊ' => 'ᄎ',
  'ㅋ' => 'ᄏ',
  'ㅌ' => 'ᄐ',
  'ㅍ' => 'ᄑ',
  'ㅎ' => 'ᄒ',
  'ㅏ' => 'ᅡ',
  'ㅐ' => 'ᅢ',
  'ㅑ' => 'ᅣ',
  'ㅒ' => 'ᅤ',
  'ㅓ' => 'ᅥ',
  'ㅔ' => 'ᅦ',
  'ㅕ' => 'ᅧ',
  'ㅖ' => 'ᅨ',
  'ㅗ' => 'ᅩ',
  'ㅘ' => 'ᅪ',
  'ㅙ' => 'ᅫ',
  'ㅚ' => 'ᅬ',
  'ㅛ' => 'ᅭ',
  'ㅜ' => 'ᅮ',
  'ㅝ' => 'ᅯ',
  'ㅞ' => 'ᅰ',
  'ㅟ' => 'ᅱ',
  'ㅠ' => 'ᅲ',
  'ㅡ' => 'ᅳ',
  'ㅢ' => 'ᅴ',
  'ㅣ' => 'ᅵ',
  'ㅤ' => 'ᅠ',
  'ㅥ' => 'ᄔ',
  'ㅦ' => 'ᄕ',
  'ㅧ' => 'ᇇ',
  'ㅨ' => 'ᇈ',
  'ㅩ' => 'ᇌ',
  'ㅪ' => 'ᇎ',
  'ㅫ' => 'ᇓ',
  'ㅬ' => 'ᇗ',
  'ㅭ' => 'ᇙ',
  'ㅮ' => 'ᄜ',
  'ㅯ' => 'ᇝ',
  'ㅰ' => 'ᇟ',
  'ㅱ' => 'ᄝ',
  'ㅲ' => 'ᄞ',
  'ㅳ' => 'ᄠ',
  'ㅴ' => 'ᄢ',
  'ㅵ' => 'ᄣ',
  'ㅶ' => 'ᄧ',
  'ㅷ' => 'ᄩ',
  'ㅸ' => 'ᄫ',
  'ㅹ' => 'ᄬ',
  'ㅺ' => 'ᄭ',
  'ㅻ' => 'ᄮ',
  'ㅼ' => 'ᄯ',
  'ㅽ' => 'ᄲ',
  'ㅾ' => 'ᄶ',
  'ㅿ' => 'ᅀ',
  'ㆀ' => 'ᅇ',
  'ㆁ' => 'ᅌ',
  'ㆂ' => 'ᇱ',
  'ㆃ' => 'ᇲ',
  'ㆄ' => 'ᅗ',
  'ㆅ' => 'ᅘ',
  'ㆆ' => 'ᅙ',
  'ㆇ' => 'ᆄ',
  'ㆈ' => 'ᆅ',
  'ㆉ' => 'ᆈ',
  'ㆊ' => 'ᆑ',
  'ㆋ' => 'ᆒ',
  'ㆌ' => 'ᆔ',
  'ㆍ' => 'ᆞ',
  'ㆎ' => 'ᆡ',
  '㆒' => '一',
  '㆓' => '二',
  '㆔' => '三',
  '㆕' => '四',
  '㆖' => '上',
  '㆗' => '中',
  '㆘' => '下',
  '㆙' => '甲',
  '㆚' => '乙',
  '㆛' => '丙',
  '㆜' => '丁',
  '㆝' => '天',
  '㆞' => '地',
  '㆟' => '人',
  '㈀' => '(ᄀ)',
  '㈁' => '(ᄂ)',
  '㈂' => '(ᄃ)',
  '㈃' => '(ᄅ)',
  '㈄' => '(ᄆ)',
  '㈅' => '(ᄇ)',
  '㈆' => '(ᄉ)',
  '㈇' => '(ᄋ)',
  '㈈' => '(ᄌ)',
  '㈉' => '(ᄎ)',
  '㈊' => '(ᄏ)',
  '㈋' => '(ᄐ)',
  '㈌' => '(ᄑ)',
  '㈍' => '(ᄒ)',
  '㈎' => '(가)',
  '㈏' => '(나)',
  '㈐' => '(다)',
  '㈑' => '(라)',
  '㈒' => '(마)',
  '㈓' => '(바)',
  '㈔' => '(사)',
  '㈕' => '(아)',
  '㈖' => '(자)',
  '㈗' => '(차)',
  '㈘' => '(카)',
  '㈙' => '(타)',
  '㈚' => '(파)',
  '㈛' => '(하)',
  '㈜' => '(주)',
  '㈝' => '(오전)',
  '㈞' => '(오후)',
  '㈠' => '(一)',
  '㈡' => '(二)',
  '㈢' => '(三)',
  '㈣' => '(四)',
  '㈤' => '(五)',
  '㈥' => '(六)',
  '㈦' => '(七)',
  '㈧' => '(八)',
  '㈨' => '(九)',
  '㈩' => '(十)',
  '㈪' => '(月)',
  '㈫' => '(火)',
  '㈬' => '(水)',
  '㈭' => '(木)',
  '㈮' => '(金)',
  '㈯' => '(土)',
  '㈰' => '(日)',
  '㈱' => '(株)',
  '㈲' => '(有)',
  '㈳' => '(社)',
  '㈴' => '(名)',
  '㈵' => '(特)',
  '㈶' => '(財)',
  '㈷' => '(祝)',
  '㈸' => '(労)',
  '㈹' => '(代)',
  '㈺' => '(呼)',
  '㈻' => '(学)',
  '㈼' => '(監)',
  '㈽' => '(企)',
  '㈾' => '(資)',
  '㈿' => '(協)',
  '㉀' => '(祭)',
  '㉁' => '(休)',
  '㉂' => '(自)',
  '㉃' => '(至)',
  '㉄' => '問',
  '㉅' => '幼',
  '㉆' => '文',
  '㉇' => '箏',
  '㉐' => 'PTE',
  '㉑' => '21',
  '㉒' => '22',
  '㉓' => '23',
  '㉔' => '24',
  '㉕' => '25',
  '㉖' => '26',
  '㉗' => '27',
  '㉘' => '28',
  '㉙' => '29',
  '㉚' => '30',
  '㉛' => '31',
  '㉜' => '32',
  '㉝' => '33',
  '㉞' => '34',
  '㉟' => '35',
  '㉠' => 'ᄀ',
  '㉡' => 'ᄂ',
  '㉢' => 'ᄃ',
  '㉣' => 'ᄅ',
  '㉤' => 'ᄆ',
  '㉥' => 'ᄇ',
  '㉦' => 'ᄉ',
  '㉧' => 'ᄋ',
  '㉨' => 'ᄌ',
  '㉩' => 'ᄎ',
  '㉪' => 'ᄏ',
  '㉫' => 'ᄐ',
  '㉬' => 'ᄑ',
  '㉭' => 'ᄒ',
  '㉮' => '가',
  '㉯' => '나',
  '㉰' => '다',
  '㉱' => '라',
  '㉲' => '마',
  '㉳' => '바',
  '㉴' => '사',
  '㉵' => '아',
  '㉶' => '자',
  '㉷' => '차',
  '㉸' => '카',
  '㉹' => '타',
  '㉺' => '파',
  '㉻' => '하',
  '㉼' => '참고',
  '㉽' => '주의',
  '㉾' => '우',
  '㊀' => '一',
  '㊁' => '二',
  '㊂' => '三',
  '㊃' => '四',
  '㊄' => '五',
  '㊅' => '六',
  '㊆' => '七',
  '㊇' => '八',
  '㊈' => '九',
  '㊉' => '十',
  '㊊' => '月',
  '㊋' => '火',
  '㊌' => '水',
  '㊍' => '木',
  '㊎' => '金',
  '㊏' => '土',
  '㊐' => '日',
  '㊑' => '株',
  '㊒' => '有',
  '㊓' => '社',
  '㊔' => '名',
  '㊕' => '特',
  '㊖' => '財',
  '㊗' => '祝',
  '㊘' => '労',
  '㊙' => '秘',
  '㊚' => '男',
  '㊛' => '女',
  '㊜' => '適',
  '㊝' => '優',
  '㊞' => '印',
  '㊟' => '注',
  '㊠' => '項',
  '㊡' => '休',
  '㊢' => '写',
  '㊣' => '正',
  '㊤' => '上',
  '㊥' => '中',
  '㊦' => '下',
  '㊧' => '左',
  '㊨' => '右',
  '㊩' => '医',
  '㊪' => '宗',
  '㊫' => '学',
  '㊬' => '監',
  '㊭' => '企',
  '㊮' => '資',
  '㊯' => '協',
  '㊰' => '夜',
  '㊱' => '36',
  '㊲' => '37',
  '㊳' => '38',
  '㊴' => '39',
  '㊵' => '40',
  '㊶' => '41',
  '㊷' => '42',
  '㊸' => '43',
  '㊹' => '44',
  '㊺' => '45',
  '㊻' => '46',
  '㊼' => '47',
  '㊽' => '48',
  '㊾' => '49',
  '㊿' => '50',
  '㋀' => '1月',
  '㋁' => '2月',
  '㋂' => '3月',
  '㋃' => '4月',
  '㋄' => '5月',
  '㋅' => '6月',
  '㋆' => '7月',
  '㋇' => '8月',
  '㋈' => '9月',
  '㋉' => '10月',
  '㋊' => '11月',
  '㋋' => '12月',
  '㋌' => 'Hg',
  '㋍' => 'erg',
  '㋎' => 'eV',
  '㋏' => 'LTD',
  '㋐' => 'ア',
  '㋑' => 'イ',
  '㋒' => 'ウ',
  '㋓' => 'エ',
  '㋔' => 'オ',
  '㋕' => 'カ',
  '㋖' => 'キ',
  '㋗' => 'ク',
  '㋘' => 'ケ',
  '㋙' => 'コ',
  '㋚' => 'サ',
  '㋛' => 'シ',
  '㋜' => 'ス',
  '㋝' => 'セ',
  '㋞' => 'ソ',
  '㋟' => 'タ',
  '㋠' => 'チ',
  '㋡' => 'ツ',
  '㋢' => 'テ',
  '㋣' => 'ト',
  '㋤' => 'ナ',
  '㋥' => 'ニ',
  '㋦' => 'ヌ',
  '㋧' => 'ネ',
  '㋨' => 'ノ',
  '㋩' => 'ハ',
  '㋪' => 'ヒ',
  '㋫' => 'フ',
  '㋬' => 'ヘ',
  '㋭' => 'ホ',
  '㋮' => 'マ',
  '㋯' => 'ミ',
  '㋰' => 'ム',
  '㋱' => 'メ',
  '㋲' => 'モ',
  '㋳' => 'ヤ',
  '㋴' => 'ユ',
  '㋵' => 'ヨ',
  '㋶' => 'ラ',
  '㋷' => 'リ',
  '㋸' => 'ル',
  '㋹' => 'レ',
  '㋺' => 'ロ',
  '㋻' => 'ワ',
  '㋼' => 'ヰ',
  '㋽' => 'ヱ',
  '㋾' => 'ヲ',
  '㋿' => '令和',
  '㌀' => 'アパート',
  '㌁' => 'アルファ',
  '㌂' => 'アンペア',
  '㌃' => 'アール',
  '㌄' => 'イニング',
  '㌅' => 'インチ',
  '㌆' => 'ウォン',
  '㌇' => 'エスクード',
  '㌈' => 'エーカー',
  '㌉' => 'オンス',
  '㌊' => 'オーム',
  '㌋' => 'カイリ',
  '㌌' => 'カラット',
  '㌍' => 'カロリー',
  '㌎' => 'ガロン',
  '㌏' => 'ガンマ',
  '㌐' => 'ギガ',
  '㌑' => 'ギニー',
  '㌒' => 'キュリー',
  '㌓' => 'ギルダー',
  '㌔' => 'キロ',
  '㌕' => 'キログラム',
  '㌖' => 'キロメートル',
  '㌗' => 'キロワット',
  '㌘' => 'グラム',
  '㌙' => 'グラムトン',
  '㌚' => 'クルゼイロ',
  '㌛' => 'クローネ',
  '㌜' => 'ケース',
  '㌝' => 'コルナ',
  '㌞' => 'コーポ',
  '㌟' => 'サイクル',
  '㌠' => 'サンチーム',
  '㌡' => 'シリング',
  '㌢' => 'センチ',
  '㌣' => 'セント',
  '㌤' => 'ダース',
  '㌥' => 'デシ',
  '㌦' => 'ドル',
  '㌧' => 'トン',
  '㌨' => 'ナノ',
  '㌩' => 'ノット',
  '㌪' => 'ハイツ',
  '㌫' => 'パーセント',
  '㌬' => 'パーツ',
  '㌭' => 'バーレル',
  '㌮' => 'ピアストル',
  '㌯' => 'ピクル',
  '㌰' => 'ピコ',
  '㌱' => 'ビル',
  '㌲' => 'ファラッド',
  '㌳' => 'フィート',
  '㌴' => 'ブッシェル',
  '㌵' => 'フラン',
  '㌶' => 'ヘクタール',
  '㌷' => 'ペソ',
  '㌸' => 'ペニヒ',
  '㌹' => 'ヘルツ',
  '㌺' => 'ペンス',
  '㌻' => 'ページ',
  '㌼' => 'ベータ',
  '㌽' => 'ポイント',
  '㌾' => 'ボルト',
  '㌿' => 'ホン',
  '㍀' => 'ポンド',
  '㍁' => 'ホール',
  '㍂' => 'ホーン',
  '㍃' => 'マイクロ',
  '㍄' => 'マイル',
  '㍅' => 'マッハ',
  '㍆' => 'マルク',
  '㍇' => 'マンション',
  '㍈' => 'ミクロン',
  '㍉' => 'ミリ',
  '㍊' => 'ミリバール',
  '㍋' => 'メガ',
  '㍌' => 'メガトン',
  '㍍' => 'メートル',
  '㍎' => 'ヤード',
  '㍏' => 'ヤール',
  '㍐' => 'ユアン',
  '㍑' => 'リットル',
  '㍒' => 'リラ',
  '㍓' => 'ルピー',
  '㍔' => 'ルーブル',
  '㍕' => 'レム',
  '㍖' => 'レントゲン',
  '㍗' => 'ワット',
  '㍘' => '0点',
  '㍙' => '1点',
  '㍚' => '2点',
  '㍛' => '3点',
  '㍜' => '4点',
  '㍝' => '5点',
  '㍞' => '6点',
  '㍟' => '7点',
  '㍠' => '8点',
  '㍡' => '9点',
  '㍢' => '10点',
  '㍣' => '11点',
  '㍤' => '12点',
  '㍥' => '13点',
  '㍦' => '14点',
  '㍧' => '15点',
  '㍨' => '16点',
  '㍩' => '17点',
  '㍪' => '18点',
  '㍫' => '19点',
  '㍬' => '20点',
  '㍭' => '21点',
  '㍮' => '22点',
  '㍯' => '23点',
  '㍰' => '24点',
  '㍱' => 'hPa',
  '㍲' => 'da',
  '㍳' => 'AU',
  '㍴' => 'bar',
  '㍵' => 'oV',
  '㍶' => 'pc',
  '㍷' => 'dm',
  '㍸' => 'dm2',
  '㍹' => 'dm3',
  '㍺' => 'IU',
  '㍻' => '平成',
  '㍼' => '昭和',
  '㍽' => '大正',
  '㍾' => '明治',
  '㍿' => '株式会社',
  '㎀' => 'pA',
  '㎁' => 'nA',
  '㎂' => 'μA',
  '㎃' => 'mA',
  '㎄' => 'kA',
  '㎅' => 'KB',
  '㎆' => 'MB',
  '㎇' => 'GB',
  '㎈' => 'cal',
  '㎉' => 'kcal',
  '㎊' => 'pF',
  '㎋' => 'nF',
  '㎌' => 'μF',
  '㎍' => 'μg',
  '㎎' => 'mg',
  '㎏' => 'kg',
  '㎐' => 'Hz',
  '㎑' => 'kHz',
  '㎒' => 'MHz',
  '㎓' => 'GHz',
  '㎔' => 'THz',
  '㎕' => 'μl',
  '㎖' => 'ml',
  '㎗' => 'dl',
  '㎘' => 'kl',
  '㎙' => 'fm',
  '㎚' => 'nm',
  '㎛' => 'μm',
  '㎜' => 'mm',
  '㎝' => 'cm',
  '㎞' => 'km',
  '㎟' => 'mm2',
  '㎠' => 'cm2',
  '㎡' => 'm2',
  '㎢' => 'km2',
  '㎣' => 'mm3',
  '㎤' => 'cm3',
  '㎥' => 'm3',
  '㎦' => 'km3',
  '㎧' => 'm∕s',
  '㎨' => 'm∕s2',
  '㎩' => 'Pa',
  '㎪' => 'kPa',
  '㎫' => 'MPa',
  '㎬' => 'GPa',
  '㎭' => 'rad',
  '㎮' => 'rad∕s',
  '㎯' => 'rad∕s2',
  '㎰' => 'ps',
  '㎱' => 'ns',
  '㎲' => 'μs',
  '㎳' => 'ms',
  '㎴' => 'pV',
  '㎵' => 'nV',
  '㎶' => 'μV',
  '㎷' => 'mV',
  '㎸' => 'kV',
  '㎹' => 'MV',
  '㎺' => 'pW',
  '㎻' => 'nW',
  '㎼' => 'μW',
  '㎽' => 'mW',
  '㎾' => 'kW',
  '㎿' => 'MW',
  '㏀' => 'kΩ',
  '㏁' => 'MΩ',
  '㏂' => 'a.m.',
  '㏃' => 'Bq',
  '㏄' => 'cc',
  '㏅' => 'cd',
  '㏆' => 'C∕kg',
  '㏇' => 'Co.',
  '㏈' => 'dB',
  '㏉' => 'Gy',
  '㏊' => 'ha',
  '㏋' => 'HP',
  '㏌' => 'in',
  '㏍' => 'KK',
  '㏎' => 'KM',
  '㏏' => 'kt',
  '㏐' => 'lm',
  '㏑' => 'ln',
  '㏒' => 'log',
  '㏓' => 'lx',
  '㏔' => 'mb',
  '㏕' => 'mil',
  '㏖' => 'mol',
  '㏗' => 'PH',
  '㏘' => 'p.m.',
  '㏙' => 'PPM',
  '㏚' => 'PR',
  '㏛' => 'sr',
  '㏜' => 'Sv',
  '㏝' => 'Wb',
  '㏞' => 'V∕m',
  '㏟' => 'A∕m',
  '㏠' => '1日',
  '㏡' => '2日',
  '㏢' => '3日',
  '㏣' => '4日',
  '㏤' => '5日',
  '㏥' => '6日',
  '㏦' => '7日',
  '㏧' => '8日',
  '㏨' => '9日',
  '㏩' => '10日',
  '㏪' => '11日',
  '㏫' => '12日',
  '㏬' => '13日',
  '㏭' => '14日',
  '㏮' => '15日',
  '㏯' => '16日',
  '㏰' => '17日',
  '㏱' => '18日',
  '㏲' => '19日',
  '㏳' => '20日',
  '㏴' => '21日',
  '㏵' => '22日',
  '㏶' => '23日',
  '㏷' => '24日',
  '㏸' => '25日',
  '㏹' => '26日',
  '㏺' => '27日',
  '㏻' => '28日',
  '㏼' => '29日',
  '㏽' => '30日',
  '㏾' => '31日',
  '㏿' => 'gal',
  'ꚜ' => 'ъ',
  'ꚝ' => 'ь',
  'ꝰ' => 'ꝯ',
  'ꟸ' => 'Ħ',
  'ꟹ' => 'œ',
  'ꭜ' => 'ꜧ',
  'ꭝ' => 'ꬷ',
  'ꭞ' => 'ɫ',
  'ꭟ' => 'ꭒ',
  'ꭩ' => 'ʍ',
  'ﬀ' => 'ff',
  'ﬁ' => 'fi',
  'ﬂ' => 'fl',
  'ﬃ' => 'ffi',
  'ﬄ' => 'ffl',
  'ﬅ' => 'st',
  'ﬆ' => 'st',
  'ﬓ' => 'մն',
  'ﬔ' => 'մե',
  'ﬕ' => 'մի',
  'ﬖ' => 'վն',
  'ﬗ' => 'մխ',
  'ﬠ' => 'ע',
  'ﬡ' => 'א',
  'ﬢ' => 'ד',
  'ﬣ' => 'ה',
  'ﬤ' => 'כ',
  'ﬥ' => 'ל',
  'ﬦ' => 'ם',
  'ﬧ' => 'ר',
  'ﬨ' => 'ת',
  '﬩' => '+',
  'ﭏ' => 'אל',
  'ﭐ' => 'ٱ',
  'ﭑ' => 'ٱ',
  'ﭒ' => 'ٻ',
  'ﭓ' => 'ٻ',
  'ﭔ' => 'ٻ',
  'ﭕ' => 'ٻ',
  'ﭖ' => 'پ',
  'ﭗ' => 'پ',
  'ﭘ' => 'پ',
  'ﭙ' => 'پ',
  'ﭚ' => 'ڀ',
  'ﭛ' => 'ڀ',
  'ﭜ' => 'ڀ',
  'ﭝ' => 'ڀ',
  'ﭞ' => 'ٺ',
  'ﭟ' => 'ٺ',
  'ﭠ' => 'ٺ',
  'ﭡ' => 'ٺ',
  'ﭢ' => 'ٿ',
  'ﭣ' => 'ٿ',
  'ﭤ' => 'ٿ',
  'ﭥ' => 'ٿ',
  'ﭦ' => 'ٹ',
  'ﭧ' => 'ٹ',
  'ﭨ' => 'ٹ',
  'ﭩ' => 'ٹ',
  'ﭪ' => 'ڤ',
  'ﭫ' => 'ڤ',
  'ﭬ' => 'ڤ',
  'ﭭ' => 'ڤ',
  'ﭮ' => 'ڦ',
  'ﭯ' => 'ڦ',
  'ﭰ' => 'ڦ',
  'ﭱ' => 'ڦ',
  'ﭲ' => 'ڄ',
  'ﭳ' => 'ڄ',
  'ﭴ' => 'ڄ',
  'ﭵ' => 'ڄ',
  'ﭶ' => 'ڃ',
  'ﭷ' => 'ڃ',
  'ﭸ' => 'ڃ',
  'ﭹ' => 'ڃ',
  'ﭺ' => 'چ',
  'ﭻ' => 'چ',
  'ﭼ' => 'چ',
  'ﭽ' => 'چ',
  'ﭾ' => 'ڇ',
  'ﭿ' => 'ڇ',
  'ﮀ' => 'ڇ',
  'ﮁ' => 'ڇ',
  'ﮂ' => 'ڍ',
  'ﮃ' => 'ڍ',
  'ﮄ' => 'ڌ',
  'ﮅ' => 'ڌ',
  'ﮆ' => 'ڎ',
  'ﮇ' => 'ڎ',
  'ﮈ' => 'ڈ',
  'ﮉ' => 'ڈ',
  'ﮊ' => 'ژ',
  'ﮋ' => 'ژ',
  'ﮌ' => 'ڑ',
  'ﮍ' => 'ڑ',
  'ﮎ' => 'ک',
  'ﮏ' => 'ک',
  'ﮐ' => 'ک',
  'ﮑ' => 'ک',
  'ﮒ' => 'گ',
  'ﮓ' => 'گ',
  'ﮔ' => 'گ',
  'ﮕ' => 'گ',
  'ﮖ' => 'ڳ',
  'ﮗ' => 'ڳ',
  'ﮘ' => 'ڳ',
  'ﮙ' => 'ڳ',
  'ﮚ' => 'ڱ',
  'ﮛ' => 'ڱ',
  'ﮜ' => 'ڱ',
  'ﮝ' => 'ڱ',
  'ﮞ' => 'ں',
  'ﮟ' => 'ں',
  'ﮠ' => 'ڻ',
  'ﮡ' => 'ڻ',
  'ﮢ' => 'ڻ',
  'ﮣ' => 'ڻ',
  'ﮤ' => 'ۀ',
  'ﮥ' => 'ۀ',
  'ﮦ' => 'ہ',
  'ﮧ' => 'ہ',
  'ﮨ' => 'ہ',
  'ﮩ' => 'ہ',
  'ﮪ' => 'ھ',
  'ﮫ' => 'ھ',
  'ﮬ' => 'ھ',
  'ﮭ' => 'ھ',
  'ﮮ' => 'ے',
  'ﮯ' => 'ے',
  'ﮰ' => 'ۓ',
  'ﮱ' => 'ۓ',
  'ﯓ' => 'ڭ',
  'ﯔ' => 'ڭ',
  'ﯕ' => 'ڭ',
  'ﯖ' => 'ڭ',
  'ﯗ' => 'ۇ',
  'ﯘ' => 'ۇ',
  'ﯙ' => 'ۆ',
  'ﯚ' => 'ۆ',
  'ﯛ' => 'ۈ',
  'ﯜ' => 'ۈ',
  'ﯝ' => 'ۇٴ',
  'ﯞ' => 'ۋ',
  'ﯟ' => 'ۋ',
  'ﯠ' => 'ۅ',
  'ﯡ' => 'ۅ',
  'ﯢ' => 'ۉ',
  'ﯣ' => 'ۉ',
  'ﯤ' => 'ې',
  'ﯥ' => 'ې',
  'ﯦ' => 'ې',
  'ﯧ' => 'ې',
  'ﯨ' => 'ى',
  'ﯩ' => 'ى',
  'ﯪ' => 'ئا',
  'ﯫ' => 'ئا',
  'ﯬ' => 'ئە',
  'ﯭ' => 'ئە',
  'ﯮ' => 'ئو',
  'ﯯ' => 'ئو',
  'ﯰ' => 'ئۇ',
  'ﯱ' => 'ئۇ',
  'ﯲ' => 'ئۆ',
  'ﯳ' => 'ئۆ',
  'ﯴ' => 'ئۈ',
  'ﯵ' => 'ئۈ',
  'ﯶ' => 'ئې',
  'ﯷ' => 'ئې',
  'ﯸ' => 'ئې',
  'ﯹ' => 'ئى',
  'ﯺ' => 'ئى',
  'ﯻ' => 'ئى',
  'ﯼ' => 'ی',
  'ﯽ' => 'ی',
  'ﯾ' => 'ی',
  'ﯿ' => 'ی',
  'ﰀ' => 'ئج',
  'ﰁ' => 'ئح',
  'ﰂ' => 'ئم',
  'ﰃ' => 'ئى',
  'ﰄ' => 'ئي',
  'ﰅ' => 'بج',
  'ﰆ' => 'بح',
  'ﰇ' => 'بخ',
  'ﰈ' => 'بم',
  'ﰉ' => 'بى',
  'ﰊ' => 'بي',
  'ﰋ' => 'تج',
  'ﰌ' => 'تح',
  'ﰍ' => 'تخ',
  'ﰎ' => 'تم',
  'ﰏ' => 'تى',
  'ﰐ' => 'تي',
  'ﰑ' => 'ثج',
  'ﰒ' => 'ثم',
  'ﰓ' => 'ثى',
  'ﰔ' => 'ثي',
  'ﰕ' => 'جح',
  'ﰖ' => 'جم',
  'ﰗ' => 'حج',
  'ﰘ' => 'حم',
  'ﰙ' => 'خج',
  'ﰚ' => 'خح',
  'ﰛ' => 'خم',
  'ﰜ' => 'سج',
  'ﰝ' => 'سح',
  'ﰞ' => 'سخ',
  'ﰟ' => 'سم',
  'ﰠ' => 'صح',
  'ﰡ' => 'صم',
  'ﰢ' => 'ضج',
  'ﰣ' => 'ضح',
  'ﰤ' => 'ضخ',
  'ﰥ' => 'ضم',
  'ﰦ' => 'طح',
  'ﰧ' => 'طم',
  'ﰨ' => 'ظم',
  'ﰩ' => 'عج',
  'ﰪ' => 'عم',
  'ﰫ' => 'غج',
  'ﰬ' => 'غم',
  'ﰭ' => 'فج',
  'ﰮ' => 'فح',
  'ﰯ' => 'فخ',
  'ﰰ' => 'فم',
  'ﰱ' => 'فى',
  'ﰲ' => 'في',
  'ﰳ' => 'قح',
  'ﰴ' => 'قم',
  'ﰵ' => 'قى',
  'ﰶ' => 'قي',
  'ﰷ' => 'كا',
  'ﰸ' => 'كج',
  'ﰹ' => 'كح',
  'ﰺ' => 'كخ',
  'ﰻ' => 'كل',
  'ﰼ' => 'كم',
  'ﰽ' => 'كى',
  'ﰾ' => 'كي',
  'ﰿ' => 'لج',
  'ﱀ' => 'لح',
  'ﱁ' => 'لخ',
  'ﱂ' => 'لم',
  'ﱃ' => 'لى',
  'ﱄ' => 'لي',
  'ﱅ' => 'مج',
  'ﱆ' => 'مح',
  'ﱇ' => 'مخ',
  'ﱈ' => 'مم',
  'ﱉ' => 'مى',
  'ﱊ' => 'مي',
  'ﱋ' => 'نج',
  'ﱌ' => 'نح',
  'ﱍ' => 'نخ',
  'ﱎ' => 'نم',
  'ﱏ' => 'نى',
  'ﱐ' => 'ني',
  'ﱑ' => 'هج',
  'ﱒ' => 'هم',
  'ﱓ' => 'هى',
  'ﱔ' => 'هي',
  'ﱕ' => 'يج',
  'ﱖ' => 'يح',
  'ﱗ' => 'يخ',
  'ﱘ' => 'يم',
  'ﱙ' => 'يى',
  'ﱚ' => 'يي',
  'ﱛ' => 'ذٰ',
  'ﱜ' => 'رٰ',
  'ﱝ' => 'ىٰ',
  'ﱞ' => ' ٌّ',
  'ﱟ' => ' ٍّ',
  'ﱠ' => ' َّ',
  'ﱡ' => ' ُّ',
  'ﱢ' => ' ِّ',
  'ﱣ' => ' ّٰ',
  'ﱤ' => 'ئر',
  'ﱥ' => 'ئز',
  'ﱦ' => 'ئم',
  'ﱧ' => 'ئن',
  'ﱨ' => 'ئى',
  'ﱩ' => 'ئي',
  'ﱪ' => 'بر',
  'ﱫ' => 'بز',
  'ﱬ' => 'بم',
  'ﱭ' => 'بن',
  'ﱮ' => 'بى',
  'ﱯ' => 'بي',
  'ﱰ' => 'تر',
  'ﱱ' => 'تز',
  'ﱲ' => 'تم',
  'ﱳ' => 'تن',
  'ﱴ' => 'تى',
  'ﱵ' => 'تي',
  'ﱶ' => 'ثر',
  'ﱷ' => 'ثز',
  'ﱸ' => 'ثم',
  'ﱹ' => 'ثن',
  'ﱺ' => 'ثى',
  'ﱻ' => 'ثي',
  'ﱼ' => 'فى',
  'ﱽ' => 'في',
  'ﱾ' => 'قى',
  'ﱿ' => 'قي',
  'ﲀ' => 'كا',
  'ﲁ' => 'كل',
  'ﲂ' => 'كم',
  'ﲃ' => 'كى',
  'ﲄ' => 'كي',
  'ﲅ' => 'لم',
  'ﲆ' => 'لى',
  'ﲇ' => 'لي',
  'ﲈ' => 'ما',
  'ﲉ' => 'مم',
  'ﲊ' => 'نر',
  'ﲋ' => 'نز',
  'ﲌ' => 'نم',
  'ﲍ' => 'نن',
  'ﲎ' => 'نى',
  'ﲏ' => 'ني',
  'ﲐ' => 'ىٰ',
  'ﲑ' => 'ير',
  'ﲒ' => 'يز',
  'ﲓ' => 'يم',
  'ﲔ' => 'ين',
  'ﲕ' => 'يى',
  'ﲖ' => 'يي',
  'ﲗ' => 'ئج',
  'ﲘ' => 'ئح',
  'ﲙ' => 'ئخ',
  'ﲚ' => 'ئم',
  'ﲛ' => 'ئه',
  'ﲜ' => 'بج',
  'ﲝ' => 'بح',
  'ﲞ' => 'بخ',
  'ﲟ' => 'بم',
  'ﲠ' => 'به',
  'ﲡ' => 'تج',
  'ﲢ' => 'تح',
  'ﲣ' => 'تخ',
  'ﲤ' => 'تم',
  'ﲥ' => 'ته',
  'ﲦ' => 'ثم',
  'ﲧ' => 'جح',
  'ﲨ' => 'جم',
  'ﲩ' => 'حج',
  'ﲪ' => 'حم',
  'ﲫ' => 'خج',
  'ﲬ' => 'خم',
  'ﲭ' => 'سج',
  'ﲮ' => 'سح',
  'ﲯ' => 'سخ',
  'ﲰ' => 'سم',
  'ﲱ' => 'صح',
  'ﲲ' => 'صخ',
  'ﲳ' => 'صم',
  'ﲴ' => 'ضج',
  'ﲵ' => 'ضح',
  'ﲶ' => 'ضخ',
  'ﲷ' => 'ضم',
  'ﲸ' => 'طح',
  'ﲹ' => 'ظم',
  'ﲺ' => 'عج',
  'ﲻ' => 'عم',
  'ﲼ' => 'غج',
  'ﲽ' => 'غم',
  'ﲾ' => 'فج',
  'ﲿ' => 'فح',
  'ﳀ' => 'فخ',
  'ﳁ' => 'فم',
  'ﳂ' => 'قح',
  'ﳃ' => 'قم',
  'ﳄ' => 'كج',
  'ﳅ' => 'كح',
  'ﳆ' => 'كخ',
  'ﳇ' => 'كل',
  'ﳈ' => 'كم',
  'ﳉ' => 'لج',
  'ﳊ' => 'لح',
  'ﳋ' => 'لخ',
  'ﳌ' => 'لم',
  'ﳍ' => 'له',
  'ﳎ' => 'مج',
  'ﳏ' => 'مح',
  'ﳐ' => 'مخ',
  'ﳑ' => 'مم',
  'ﳒ' => 'نج',
  'ﳓ' => 'نح',
  'ﳔ' => 'نخ',
  'ﳕ' => 'نم',
  'ﳖ' => 'نه',
  'ﳗ' => 'هج',
  'ﳘ' => 'هم',
  'ﳙ' => 'هٰ',
  'ﳚ' => 'يج',
  'ﳛ' => 'يح',
  'ﳜ' => 'يخ',
  'ﳝ' => 'يم',
  'ﳞ' => 'يه',
  'ﳟ' => 'ئم',
  'ﳠ' => 'ئه',
  'ﳡ' => 'بم',
  'ﳢ' => 'به',
  'ﳣ' => 'تم',
  'ﳤ' => 'ته',
  'ﳥ' => 'ثم',
  'ﳦ' => 'ثه',
  'ﳧ' => 'سم',
  'ﳨ' => 'سه',
  'ﳩ' => 'شم',
  'ﳪ' => 'شه',
  'ﳫ' => 'كل',
  'ﳬ' => 'كم',
  'ﳭ' => 'لم',
  'ﳮ' => 'نم',
  'ﳯ' => 'نه',
  'ﳰ' => 'يم',
  'ﳱ' => 'يه',
  'ﳲ' => 'ـَّ',
  'ﳳ' => 'ـُّ',
  'ﳴ' => 'ـِّ',
  'ﳵ' => 'طى',
  'ﳶ' => 'طي',
  'ﳷ' => 'عى',
  'ﳸ' => 'عي',
  'ﳹ' => 'غى',
  'ﳺ' => 'غي',
  'ﳻ' => 'سى',
  'ﳼ' => 'سي',
  'ﳽ' => 'شى',
  'ﳾ' => 'شي',
  'ﳿ' => 'حى',
  'ﴀ' => 'حي',
  'ﴁ' => 'جى',
  'ﴂ' => 'جي',
  'ﴃ' => 'خى',
  'ﴄ' => 'خي',
  'ﴅ' => 'صى',
  'ﴆ' => 'صي',
  'ﴇ' => 'ضى',
  'ﴈ' => 'ضي',
  'ﴉ' => 'شج',
  'ﴊ' => 'شح',
  'ﴋ' => 'شخ',
  'ﴌ' => 'شم',
  'ﴍ' => 'شر',
  'ﴎ' => 'سر',
  'ﴏ' => 'صر',
  'ﴐ' => 'ضر',
  'ﴑ' => 'طى',
  'ﴒ' => 'طي',
  'ﴓ' => 'عى',
  'ﴔ' => 'عي',
  'ﴕ' => 'غى',
  'ﴖ' => 'غي',
  'ﴗ' => 'سى',
  'ﴘ' => 'سي',
  'ﴙ' => 'شى',
  'ﴚ' => 'شي',
  'ﴛ' => 'حى',
  'ﴜ' => 'حي',
  'ﴝ' => 'جى',
  'ﴞ' => 'جي',
  'ﴟ' => 'خى',
  'ﴠ' => 'خي',
  'ﴡ' => 'صى',
  'ﴢ' => 'صي',
  'ﴣ' => 'ضى',
  'ﴤ' => 'ضي',
  'ﴥ' => 'شج',
  'ﴦ' => 'شح',
  'ﴧ' => 'شخ',
  'ﴨ' => 'شم',
  'ﴩ' => 'شر',
  'ﴪ' => 'سر',
  'ﴫ' => 'صر',
  'ﴬ' => 'ضر',
  'ﴭ' => 'شج',
  'ﴮ' => 'شح',
  'ﴯ' => 'شخ',
  'ﴰ' => 'شم',
  'ﴱ' => 'سه',
  'ﴲ' => 'شه',
  'ﴳ' => 'طم',
  'ﴴ' => 'سج',
  'ﴵ' => 'سح',
  'ﴶ' => 'سخ',
  'ﴷ' => 'شج',
  'ﴸ' => 'شح',
  'ﴹ' => 'شخ',
  'ﴺ' => 'طم',
  'ﴻ' => 'ظم',
  'ﴼ' => 'اً',
  'ﴽ' => 'اً',
  'ﵐ' => 'تجم',
  'ﵑ' => 'تحج',
  'ﵒ' => 'تحج',
  'ﵓ' => 'تحم',
  'ﵔ' => 'تخم',
  'ﵕ' => 'تمج',
  'ﵖ' => 'تمح',
  'ﵗ' => 'تمخ',
  'ﵘ' => 'جمح',
  'ﵙ' => 'جمح',
  'ﵚ' => 'حمي',
  'ﵛ' => 'حمى',
  'ﵜ' => 'سحج',
  'ﵝ' => 'سجح',
  'ﵞ' => 'سجى',
  'ﵟ' => 'سمح',
  'ﵠ' => 'سمح',
  'ﵡ' => 'سمج',
  'ﵢ' => 'سمم',
  'ﵣ' => 'سمم',
  'ﵤ' => 'صحح',
  'ﵥ' => 'صحح',
  'ﵦ' => 'صمم',
  'ﵧ' => 'شحم',
  'ﵨ' => 'شحم',
  'ﵩ' => 'شجي',
  'ﵪ' => 'شمخ',
  'ﵫ' => 'شمخ',
  'ﵬ' => 'شمم',
  'ﵭ' => 'شمم',
  'ﵮ' => 'ضحى',
  'ﵯ' => 'ضخم',
  'ﵰ' => 'ضخم',
  'ﵱ' => 'طمح',
  'ﵲ' => 'طمح',
  'ﵳ' => 'طمم',
  'ﵴ' => 'طمي',
  'ﵵ' => 'عجم',
  'ﵶ' => 'عمم',
  'ﵷ' => 'عمم',
  'ﵸ' => 'عمى',
  'ﵹ' => 'غمم',
  'ﵺ' => 'غمي',
  'ﵻ' => 'غمى',
  'ﵼ' => 'فخم',
  'ﵽ' => 'فخم',
  'ﵾ' => 'قمح',
  'ﵿ' => 'قمم',
  'ﶀ' => 'لحم',
  'ﶁ' => 'لحي',
  'ﶂ' => 'لحى',
  'ﶃ' => 'لجج',
  'ﶄ' => 'لجج',
  'ﶅ' => 'لخم',
  'ﶆ' => 'لخم',
  'ﶇ' => 'لمح',
  'ﶈ' => 'لمح',
  'ﶉ' => 'محج',
  'ﶊ' => 'محم',
  'ﶋ' => 'محي',
  'ﶌ' => 'مجح',
  'ﶍ' => 'مجم',
  'ﶎ' => 'مخج',
  'ﶏ' => 'مخم',
  'ﶒ' => 'مجخ',
  'ﶓ' => 'همج',
  'ﶔ' => 'همم',
  'ﶕ' => 'نحم',
  'ﶖ' => 'نحى',
  'ﶗ' => 'نجم',
  'ﶘ' => 'نجم',
  'ﶙ' => 'نجى',
  'ﶚ' => 'نمي',
  'ﶛ' => 'نمى',
  'ﶜ' => 'يمم',
  'ﶝ' => 'يمم',
  'ﶞ' => 'بخي',
  'ﶟ' => 'تجي',
  'ﶠ' => 'تجى',
  'ﶡ' => 'تخي',
  'ﶢ' => 'تخى',
  'ﶣ' => 'تمي',
  'ﶤ' => 'تمى',
  'ﶥ' => 'جمي',
  'ﶦ' => 'جحى',
  'ﶧ' => 'جمى',
  'ﶨ' => 'سخى',
  'ﶩ' => 'صحي',
  'ﶪ' => 'شحي',
  'ﶫ' => 'ضحي',
  'ﶬ' => 'لجي',
  'ﶭ' => 'لمي',
  'ﶮ' => 'يحي',
  'ﶯ' => 'يجي',
  'ﶰ' => 'يمي',
  'ﶱ' => 'ممي',
  'ﶲ' => 'قمي',
  'ﶳ' => 'نحي',
  'ﶴ' => 'قمح',
  'ﶵ' => 'لحم',
  'ﶶ' => 'عمي',
  'ﶷ' => 'كمي',
  'ﶸ' => 'نجح',
  'ﶹ' => 'مخي',
  'ﶺ' => 'لجم',
  'ﶻ' => 'كمم',
  'ﶼ' => 'لجم',
  'ﶽ' => 'نجح',
  'ﶾ' => 'جحي',
  'ﶿ' => 'حجي',
  'ﷀ' => 'مجي',
  'ﷁ' => 'فمي',
  'ﷂ' => 'بحي',
  'ﷃ' => 'كمم',
  'ﷄ' => 'عجم',
  'ﷅ' => 'صمم',
  'ﷆ' => 'سخي',
  'ﷇ' => 'نجي',
  'ﷰ' => 'صلے',
  'ﷱ' => 'قلے',
  'ﷲ' => 'الله',
  'ﷳ' => 'اكبر',
  'ﷴ' => 'محمد',
  'ﷵ' => 'صلعم',
  'ﷶ' => 'رسول',
  'ﷷ' => 'عليه',
  'ﷸ' => 'وسلم',
  'ﷹ' => 'صلى',
  'ﷺ' => 'صلى الله عليه وسلم',
  'ﷻ' => 'جل جلاله',
  '﷼' => 'ریال',
  '︐' => ',',
  '︑' => '、',
  '︒' => '。',
  '︓' => ':',
  '︔' => ';',
  '︕' => '!',
  '︖' => '?',
  '︗' => '〖',
  '︘' => '〗',
  '︙' => '...',
  '︰' => '..',
  '︱' => '—',
  '︲' => '–',
  '︳' => '_',
  '︴' => '_',
  '︵' => '(',
  '︶' => ')',
  '︷' => '{',
  '︸' => '}',
  '︹' => '〔',
  '︺' => '〕',
  '︻' => '【',
  '︼' => '】',
  '︽' => '《',
  '︾' => '》',
  '︿' => '〈',
  '﹀' => '〉',
  '﹁' => '「',
  '﹂' => '」',
  '﹃' => '『',
  '﹄' => '』',
  '﹇' => '[',
  '﹈' => ']',
  '﹉' => ' ̅',
  '﹊' => ' ̅',
  '﹋' => ' ̅',
  '﹌' => ' ̅',
  '﹍' => '_',
  '﹎' => '_',
  '﹏' => '_',
  '﹐' => ',',
  '﹑' => '、',
  '﹒' => '.',
  '﹔' => ';',
  '﹕' => ':',
  '﹖' => '?',
  '﹗' => '!',
  '﹘' => '—',
  '﹙' => '(',
  '﹚' => ')',
  '﹛' => '{',
  '﹜' => '}',
  '﹝' => '〔',
  '﹞' => '〕',
  '﹟' => '#',
  '﹠' => '&',
  '﹡' => '*',
  '﹢' => '+',
  '﹣' => '-',
  '﹤' => '<',
  '﹥' => '>',
  '﹦' => '=',
  '﹨' => '\\',
  '﹩' => '$',
  '﹪' => '%',
  '﹫' => '@',
  'ﹰ' => ' ً',
  'ﹱ' => 'ـً',
  'ﹲ' => ' ٌ',
  'ﹴ' => ' ٍ',
  'ﹶ' => ' َ',
  'ﹷ' => 'ـَ',
  'ﹸ' => ' ُ',
  'ﹹ' => 'ـُ',
  'ﹺ' => ' ِ',
  'ﹻ' => 'ـِ',
  'ﹼ' => ' ّ',
  'ﹽ' => 'ـّ',
  'ﹾ' => ' ْ',
  'ﹿ' => 'ـْ',
  'ﺀ' => 'ء',
  'ﺁ' => 'آ',
  'ﺂ' => 'آ',
  'ﺃ' => 'أ',
  'ﺄ' => 'أ',
  'ﺅ' => 'ؤ',
  'ﺆ' => 'ؤ',
  'ﺇ' => 'إ',
  'ﺈ' => 'إ',
  'ﺉ' => 'ئ',
  'ﺊ' => 'ئ',
  'ﺋ' => 'ئ',
  'ﺌ' => 'ئ',
  'ﺍ' => 'ا',
  'ﺎ' => 'ا',
  'ﺏ' => 'ب',
  'ﺐ' => 'ب',
  'ﺑ' => 'ب',
  'ﺒ' => 'ب',
  'ﺓ' => 'ة',
  'ﺔ' => 'ة',
  'ﺕ' => 'ت',
  'ﺖ' => 'ت',
  'ﺗ' => 'ت',
  'ﺘ' => 'ت',
  'ﺙ' => 'ث',
  'ﺚ' => 'ث',
  'ﺛ' => 'ث',
  'ﺜ' => 'ث',
  'ﺝ' => 'ج',
  'ﺞ' => 'ج',
  'ﺟ' => 'ج',
  'ﺠ' => 'ج',
  'ﺡ' => 'ح',
  'ﺢ' => 'ح',
  'ﺣ' => 'ح',
  'ﺤ' => 'ح',
  'ﺥ' => 'خ',
  'ﺦ' => 'خ',
  'ﺧ' => 'خ',
  'ﺨ' => 'خ',
  'ﺩ' => 'د',
  'ﺪ' => 'د',
  'ﺫ' => 'ذ',
  'ﺬ' => 'ذ',
  'ﺭ' => 'ر',
  'ﺮ' => 'ر',
  'ﺯ' => 'ز',
  'ﺰ' => 'ز',
  'ﺱ' => 'س',
  'ﺲ' => 'س',
  'ﺳ' => 'س',
  'ﺴ' => 'س',
  'ﺵ' => 'ش',
  'ﺶ' => 'ش',
  'ﺷ' => 'ش',
  'ﺸ' => 'ش',
  'ﺹ' => 'ص',
  'ﺺ' => 'ص',
  'ﺻ' => 'ص',
  'ﺼ' => 'ص',
  'ﺽ' => 'ض',
  'ﺾ' => 'ض',
  'ﺿ' => 'ض',
  'ﻀ' => 'ض',
  'ﻁ' => 'ط',
  'ﻂ' => 'ط',
  'ﻃ' => 'ط',
  'ﻄ' => 'ط',
  'ﻅ' => 'ظ',
  'ﻆ' => 'ظ',
  'ﻇ' => 'ظ',
  'ﻈ' => 'ظ',
  'ﻉ' => 'ع',
  'ﻊ' => 'ع',
  'ﻋ' => 'ع',
  'ﻌ' => 'ع',
  'ﻍ' => 'غ',
  'ﻎ' => 'غ',
  'ﻏ' => 'غ',
  'ﻐ' => 'غ',
  'ﻑ' => 'ف',
  'ﻒ' => 'ف',
  'ﻓ' => 'ف',
  'ﻔ' => 'ف',
  'ﻕ' => 'ق',
  'ﻖ' => 'ق',
  'ﻗ' => 'ق',
  'ﻘ' => 'ق',
  'ﻙ' => 'ك',
  'ﻚ' => 'ك',
  'ﻛ' => 'ك',
  'ﻜ' => 'ك',
  'ﻝ' => 'ل',
  'ﻞ' => 'ل',
  'ﻟ' => 'ل',
  'ﻠ' => 'ل',
  'ﻡ' => 'م',
  'ﻢ' => 'م',
  'ﻣ' => 'م',
  'ﻤ' => 'م',
  'ﻥ' => 'ن',
  'ﻦ' => 'ن',
  'ﻧ' => 'ن',
  'ﻨ' => 'ن',
  'ﻩ' => 'ه',
  'ﻪ' => 'ه',
  'ﻫ' => 'ه',
  'ﻬ' => 'ه',
  'ﻭ' => 'و',
  'ﻮ' => 'و',
  'ﻯ' => 'ى',
  'ﻰ' => 'ى',
  'ﻱ' => 'ي',
  'ﻲ' => 'ي',
  'ﻳ' => 'ي',
  'ﻴ' => 'ي',
  'ﻵ' => 'لآ',
  'ﻶ' => 'لآ',
  'ﻷ' => 'لأ',
  'ﻸ' => 'لأ',
  'ﻹ' => 'لإ',
  'ﻺ' => 'لإ',
  'ﻻ' => 'لا',
  'ﻼ' => 'لا',
  '！' => '!',
  '＂' => '"',
  '＃' => '#',
  '＄' => '$',
  '％' => '%',
  '＆' => '&',
  '＇' => '\'',
  '（' => '(',
  '）' => ')',
  '＊' => '*',
  '＋' => '+',
  '，' => ',',
  '－' => '-',
  '．' => '.',
  '／' => '/',
  '０' => '0',
  '１' => '1',
  '２' => '2',
  '３' => '3',
  '４' => '4',
  '５' => '5',
  '６' => '6',
  '７' => '7',
  '８' => '8',
  '９' => '9',
  '：' => ':',
  '；' => ';',
  '＜' => '<',
  '＝' => '=',
  '＞' => '>',
  '？' => '?',
  '＠' => '@',
  'Ａ' => 'A',
  'Ｂ' => 'B',
  'Ｃ' => 'C',
  'Ｄ' => 'D',
  'Ｅ' => 'E',
  'Ｆ' => 'F',
  'Ｇ' => 'G',
  'Ｈ' => 'H',
  'Ｉ' => 'I',
  'Ｊ' => 'J',
  'Ｋ' => 'K',
  'Ｌ' => 'L',
  'Ｍ' => 'M',
  'Ｎ' => 'N',
  'Ｏ' => 'O',
  'Ｐ' => 'P',
  'Ｑ' => 'Q',
  'Ｒ' => 'R',
  'Ｓ' => 'S',
  'Ｔ' => 'T',
  'Ｕ' => 'U',
  'Ｖ' => 'V',
  'Ｗ' => 'W',
  'Ｘ' => 'X',
  'Ｙ' => 'Y',
  'Ｚ' => 'Z',
  '［' => '[',
  '＼' => '\\',
  '］' => ']',
  '＾' => '^',
  '＿' => '_',
  '｀' => '`',
  'ａ' => 'a',
  'ｂ' => 'b',
  'ｃ' => 'c',
  'ｄ' => 'd',
  'ｅ' => 'e',
  'ｆ' => 'f',
  'ｇ' => 'g',
  'ｈ' => 'h',
  'ｉ' => 'i',
  'ｊ' => 'j',
  'ｋ' => 'k',
  'ｌ' => 'l',
  'ｍ' => 'm',
  'ｎ' => 'n',
  'ｏ' => 'o',
  'ｐ' => 'p',
  'ｑ' => 'q',
  'ｒ' => 'r',
  'ｓ' => 's',
  'ｔ' => 't',
  'ｕ' => 'u',
  'ｖ' => 'v',
  'ｗ' => 'w',
  'ｘ' => 'x',
  'ｙ' => 'y',
  'ｚ' => 'z',
  '｛' => '{',
  '｜' => '|',
  '｝' => '}',
  '～' => '~',
  '｟' => '⦅',
  '｠' => '⦆',
  '｡' => '。',
  '｢' => '「',
  '｣' => '」',
  '､' => '、',
  '･' => '・',
  'ｦ' => 'ヲ',
  'ｧ' => 'ァ',
  'ｨ' => 'ィ',
  'ｩ' => 'ゥ',
  'ｪ' => 'ェ',
  'ｫ' => 'ォ',
  'ｬ' => 'ャ',
  'ｭ' => 'ュ',
  'ｮ' => 'ョ',
  'ｯ' => 'ッ',
  'ｰ' => 'ー',
  'ｱ' => 'ア',
  'ｲ' => 'イ',
  'ｳ' => 'ウ',
  'ｴ' => 'エ',
  'ｵ' => 'オ',
  'ｶ' => 'カ',
  'ｷ' => 'キ',
  'ｸ' => 'ク',
  'ｹ' => 'ケ',
  'ｺ' => 'コ',
  'ｻ' => 'サ',
  'ｼ' => 'シ',
  'ｽ' => 'ス',
  'ｾ' => 'セ',
  'ｿ' => 'ソ',
  'ﾀ' => 'タ',
  'ﾁ' => 'チ',
  'ﾂ' => 'ツ',
  'ﾃ' => 'テ',
  'ﾄ' => 'ト',
  'ﾅ' => 'ナ',
  'ﾆ' => 'ニ',
  'ﾇ' => 'ヌ',
  'ﾈ' => 'ネ',
  'ﾉ' => 'ノ',
  'ﾊ' => 'ハ',
  'ﾋ' => 'ヒ',
  'ﾌ' => 'フ',
  'ﾍ' => 'ヘ',
  'ﾎ' => 'ホ',
  'ﾏ' => 'マ',
  'ﾐ' => 'ミ',
  'ﾑ' => 'ム',
  'ﾒ' => 'メ',
  'ﾓ' => 'モ',
  'ﾔ' => 'ヤ',
  'ﾕ' => 'ユ',
  'ﾖ' => 'ヨ',
  'ﾗ' => 'ラ',
  'ﾘ' => 'リ',
  'ﾙ' => 'ル',
  'ﾚ' => 'レ',
  'ﾛ' => 'ロ',
  'ﾜ' => 'ワ',
  'ﾝ' => 'ン',
  'ﾞ' => '゙',
  'ﾟ' => '゚',
  'ﾠ' => 'ᅠ',
  'ﾡ' => 'ᄀ',
  'ﾢ' => 'ᄁ',
  'ﾣ' => 'ᆪ',
  'ﾤ' => 'ᄂ',
  'ﾥ' => 'ᆬ',
  'ﾦ' => 'ᆭ',
  'ﾧ' => 'ᄃ',
  'ﾨ' => 'ᄄ',
  'ﾩ' => 'ᄅ',
  'ﾪ' => 'ᆰ',
  'ﾫ' => 'ᆱ',
  'ﾬ' => 'ᆲ',
  'ﾭ' => 'ᆳ',
  'ﾮ' => 'ᆴ',
  'ﾯ' => 'ᆵ',
  'ﾰ' => 'ᄚ',
  'ﾱ' => 'ᄆ',
  'ﾲ' => 'ᄇ',
  'ﾳ' => 'ᄈ',
  'ﾴ' => 'ᄡ',
  'ﾵ' => 'ᄉ',
  'ﾶ' => 'ᄊ',
  'ﾷ' => 'ᄋ',
  'ﾸ' => 'ᄌ',
  'ﾹ' => 'ᄍ',
  'ﾺ' => 'ᄎ',
  'ﾻ' => 'ᄏ',
  'ﾼ' => 'ᄐ',
  'ﾽ' => 'ᄑ',
  'ﾾ' => 'ᄒ',
  'ￂ' => 'ᅡ',
  'ￃ' => 'ᅢ',
  'ￄ' => 'ᅣ',
  'ￅ' => 'ᅤ',
  'ￆ' => 'ᅥ',
  'ￇ' => 'ᅦ',
  'ￊ' => 'ᅧ',
  'ￋ' => 'ᅨ',
  'ￌ' => 'ᅩ',
  'ￍ' => 'ᅪ',
  'ￎ' => 'ᅫ',
  'ￏ' => 'ᅬ',
  'ￒ' => 'ᅭ',
  'ￓ' => 'ᅮ',
  'ￔ' => 'ᅯ',
  'ￕ' => 'ᅰ',
  'ￖ' => 'ᅱ',
  'ￗ' => 'ᅲ',
  'ￚ' => 'ᅳ',
  'ￛ' => 'ᅴ',
  'ￜ' => 'ᅵ',
  '￠' => '¢',
  '￡' => '£',
  '￢' => '¬',
  '￣' => ' ̄',
  '￤' => '¦',
  '￥' => '¥',
  '￦' => '₩',
  '￨' => '│',
  '￩' => '←',
  '￪' => '↑',
  '￫' => '→',
  '￬' => '↓',
  '￭' => '■',
  '￮' => '○',
  '𝐀' => 'A',
  '𝐁' => 'B',
  '𝐂' => 'C',
  '𝐃' => 'D',
  '𝐄' => 'E',
  '𝐅' => 'F',
  '𝐆' => 'G',
  '𝐇' => 'H',
  '𝐈' => 'I',
  '𝐉' => 'J',
  '𝐊' => 'K',
  '𝐋' => 'L',
  '𝐌' => 'M',
  '𝐍' => 'N',
  '𝐎' => 'O',
  '𝐏' => 'P',
  '𝐐' => 'Q',
  '𝐑' => 'R',
  '𝐒' => 'S',
  '𝐓' => 'T',
  '𝐔' => 'U',
  '𝐕' => 'V',
  '𝐖' => 'W',
  '𝐗' => 'X',
  '𝐘' => 'Y',
  '𝐙' => 'Z',
  '𝐚' => 'a',
  '𝐛' => 'b',
  '𝐜' => 'c',
  '𝐝' => 'd',
  '𝐞' => 'e',
  '𝐟' => 'f',
  '𝐠' => 'g',
  '𝐡' => 'h',
  '𝐢' => 'i',
  '𝐣' => 'j',
  '𝐤' => 'k',
  '𝐥' => 'l',
  '𝐦' => 'm',
  '𝐧' => 'n',
  '𝐨' => 'o',
  '𝐩' => 'p',
  '𝐪' => 'q',
  '𝐫' => 'r',
  '𝐬' => 's',
  '𝐭' => 't',
  '𝐮' => 'u',
  '𝐯' => 'v',
  '𝐰' => 'w',
  '𝐱' => 'x',
  '𝐲' => 'y',
  '𝐳' => 'z',
  '𝐴' => 'A',
  '𝐵' => 'B',
  '𝐶' => 'C',
  '𝐷' => 'D',
  '𝐸' => 'E',
  '𝐹' => 'F',
  '𝐺' => 'G',
  '𝐻' => 'H',
  '𝐼' => 'I',
  '𝐽' => 'J',
  '𝐾' => 'K',
  '𝐿' => 'L',
  '𝑀' => 'M',
  '𝑁' => 'N',
  '𝑂' => 'O',
  '𝑃' => 'P',
  '𝑄' => 'Q',
  '𝑅' => 'R',
  '𝑆' => 'S',
  '𝑇' => 'T',
  '𝑈' => 'U',
  '𝑉' => 'V',
  '𝑊' => 'W',
  '𝑋' => 'X',
  '𝑌' => 'Y',
  '𝑍' => 'Z',
  '𝑎' => 'a',
  '𝑏' => 'b',
  '𝑐' => 'c',
  '𝑑' => 'd',
  '𝑒' => 'e',
  '𝑓' => 'f',
  '𝑔' => 'g',
  '𝑖' => 'i',
  '𝑗' => 'j',
  '𝑘' => 'k',
  '𝑙' => 'l',
  '𝑚' => 'm',
  '𝑛' => 'n',
  '𝑜' => 'o',
  '𝑝' => 'p',
  '𝑞' => 'q',
  '𝑟' => 'r',
  '𝑠' => 's',
  '𝑡' => 't',
  '𝑢' => 'u',
  '𝑣' => 'v',
  '𝑤' => 'w',
  '𝑥' => 'x',
  '𝑦' => 'y',
  '𝑧' => 'z',
  '𝑨' => 'A',
  '𝑩' => 'B',
  '𝑪' => 'C',
  '𝑫' => 'D',
  '𝑬' => 'E',
  '𝑭' => 'F',
  '𝑮' => 'G',
  '𝑯' => 'H',
  '𝑰' => 'I',
  '𝑱' => 'J',
  '𝑲' => 'K',
  '𝑳' => 'L',
  '𝑴' => 'M',
  '𝑵' => 'N',
  '𝑶' => 'O',
  '𝑷' => 'P',
  '𝑸' => 'Q',
  '𝑹' => 'R',
  '𝑺' => 'S',
  '𝑻' => 'T',
  '𝑼' => 'U',
  '𝑽' => 'V',
  '𝑾' => 'W',
  '𝑿' => 'X',
  '𝒀' => 'Y',
  '𝒁' => 'Z',
  '𝒂' => 'a',
  '𝒃' => 'b',
  '𝒄' => 'c',
  '𝒅' => 'd',
  '𝒆' => 'e',
  '𝒇' => 'f',
  '𝒈' => 'g',
  '𝒉' => 'h',
  '𝒊' => 'i',
  '𝒋' => 'j',
  '𝒌' => 'k',
  '𝒍' => 'l',
  '𝒎' => 'm',
  '𝒏' => 'n',
  '𝒐' => 'o',
  '𝒑' => 'p',
  '𝒒' => 'q',
  '𝒓' => 'r',
  '𝒔' => 's',
  '𝒕' => 't',
  '𝒖' => 'u',
  '𝒗' => 'v',
  '𝒘' => 'w',
  '𝒙' => 'x',
  '𝒚' => 'y',
  '𝒛' => 'z',
  '𝒜' => 'A',
  '𝒞' => 'C',
  '𝒟' => 'D',
  '𝒢' => 'G',
  '𝒥' => 'J',
  '𝒦' => 'K',
  '𝒩' => 'N',
  '𝒪' => 'O',
  '𝒫' => 'P',
  '𝒬' => 'Q',
  '𝒮' => 'S',
  '𝒯' => 'T',
  '𝒰' => 'U',
  '𝒱' => 'V',
  '𝒲' => 'W',
  '𝒳' => 'X',
  '𝒴' => 'Y',
  '𝒵' => 'Z',
  '𝒶' => 'a',
  '𝒷' => 'b',
  '𝒸' => 'c',
  '𝒹' => 'd',
  '𝒻' => 'f',
  '𝒽' => 'h',
  '𝒾' => 'i',
  '𝒿' => 'j',
  '𝓀' => 'k',
  '𝓁' => 'l',
  '𝓂' => 'm',
  '𝓃' => 'n',
  '𝓅' => 'p',
  '𝓆' => 'q',
  '𝓇' => 'r',
  '𝓈' => 's',
  '𝓉' => 't',
  '𝓊' => 'u',
  '𝓋' => 'v',
  '𝓌' => 'w',
  '𝓍' => 'x',
  '𝓎' => 'y',
  '𝓏' => 'z',
  '𝓐' => 'A',
  '𝓑' => 'B',
  '𝓒' => 'C',
  '𝓓' => 'D',
  '𝓔' => 'E',
  '𝓕' => 'F',
  '𝓖' => 'G',
  '𝓗' => 'H',
  '𝓘' => 'I',
  '𝓙' => 'J',
  '𝓚' => 'K',
  '𝓛' => 'L',
  '𝓜' => 'M',
  '𝓝' => 'N',
  '𝓞' => 'O',
  '𝓟' => 'P',
  '𝓠' => 'Q',
  '𝓡' => 'R',
  '𝓢' => 'S',
  '𝓣' => 'T',
  '𝓤' => 'U',
  '𝓥' => 'V',
  '𝓦' => 'W',
  '𝓧' => 'X',
  '𝓨' => 'Y',
  '𝓩' => 'Z',
  '𝓪' => 'a',
  '𝓫' => 'b',
  '𝓬' => 'c',
  '𝓭' => 'd',
  '𝓮' => 'e',
  '𝓯' => 'f',
  '𝓰' => 'g',
  '𝓱' => 'h',
  '𝓲' => 'i',
  '𝓳' => 'j',
  '𝓴' => 'k',
  '𝓵' => 'l',
  '𝓶' => 'm',
  '𝓷' => 'n',
  '𝓸' => 'o',
  '𝓹' => 'p',
  '𝓺' => 'q',
  '𝓻' => 'r',
  '𝓼' => 's',
  '𝓽' => 't',
  '𝓾' => 'u',
  '𝓿' => 'v',
  '𝔀' => 'w',
  '𝔁' => 'x',
  '𝔂' => 'y',
  '𝔃' => 'z',
  '𝔄' => 'A',
  '𝔅' => 'B',
  '𝔇' => 'D',
  '𝔈' => 'E',
  '𝔉' => 'F',
  '𝔊' => 'G',
  '𝔍' => 'J',
  '𝔎' => 'K',
  '𝔏' => 'L',
  '𝔐' => 'M',
  '𝔑' => 'N',
  '𝔒' => 'O',
  '𝔓' => 'P',
  '𝔔' => 'Q',
  '𝔖' => 'S',
  '𝔗' => 'T',
  '𝔘' => 'U',
  '𝔙' => 'V',
  '𝔚' => 'W',
  '𝔛' => 'X',
  '𝔜' => 'Y',
  '𝔞' => 'a',
  '𝔟' => 'b',
  '𝔠' => 'c',
  '𝔡' => 'd',
  '𝔢' => 'e',
  '𝔣' => 'f',
  '𝔤' => 'g',
  '𝔥' => 'h',
  '𝔦' => 'i',
  '𝔧' => 'j',
  '𝔨' => 'k',
  '𝔩' => 'l',
  '𝔪' => 'm',
  '𝔫' => 'n',
  '𝔬' => 'o',
  '𝔭' => 'p',
  '𝔮' => 'q',
  '𝔯' => 'r',
  '𝔰' => 's',
  '𝔱' => 't',
  '𝔲' => 'u',
  '𝔳' => 'v',
  '𝔴' => 'w',
  '𝔵' => 'x',
  '𝔶' => 'y',
  '𝔷' => 'z',
  '𝔸' => 'A',
  '𝔹' => 'B',
  '𝔻' => 'D',
  '𝔼' => 'E',
  '𝔽' => 'F',
  '𝔾' => 'G',
  '𝕀' => 'I',
  '𝕁' => 'J',
  '𝕂' => 'K',
  '𝕃' => 'L',
  '𝕄' => 'M',
  '𝕆' => 'O',
  '𝕊' => 'S',
  '𝕋' => 'T',
  '𝕌' => 'U',
  '𝕍' => 'V',
  '𝕎' => 'W',
  '𝕏' => 'X',
  '𝕐' => 'Y',
  '𝕒' => 'a',
  '𝕓' => 'b',
  '𝕔' => 'c',
  '𝕕' => 'd',
  '𝕖' => 'e',
  '𝕗' => 'f',
  '𝕘' => 'g',
  '𝕙' => 'h',
  '𝕚' => 'i',
  '𝕛' => 'j',
  '𝕜' => 'k',
  '𝕝' => 'l',
  '𝕞' => 'm',
  '𝕟' => 'n',
  '𝕠' => 'o',
  '𝕡' => 'p',
  '𝕢' => 'q',
  '𝕣' => 'r',
  '𝕤' => 's',
  '𝕥' => 't',
  '𝕦' => 'u',
  '𝕧' => 'v',
  '𝕨' => 'w',
  '𝕩' => 'x',
  '𝕪' => 'y',
  '𝕫' => 'z',
  '𝕬' => 'A',
  '𝕭' => 'B',
  '𝕮' => 'C',
  '𝕯' => 'D',
  '𝕰' => 'E',
  '𝕱' => 'F',
  '𝕲' => 'G',
  '𝕳' => 'H',
  '𝕴' => 'I',
  '𝕵' => 'J',
  '𝕶' => 'K',
  '𝕷' => 'L',
  '𝕸' => 'M',
  '𝕹' => 'N',
  '𝕺' => 'O',
  '𝕻' => 'P',
  '𝕼' => 'Q',
  '𝕽' => 'R',
  '𝕾' => 'S',
  '𝕿' => 'T',
  '𝖀' => 'U',
  '𝖁' => 'V',
  '𝖂' => 'W',
  '𝖃' => 'X',
  '𝖄' => 'Y',
  '𝖅' => 'Z',
  '𝖆' => 'a',
  '𝖇' => 'b',
  '𝖈' => 'c',
  '𝖉' => 'd',
  '𝖊' => 'e',
  '𝖋' => 'f',
  '𝖌' => 'g',
  '𝖍' => 'h',
  '𝖎' => 'i',
  '𝖏' => 'j',
  '𝖐' => 'k',
  '𝖑' => 'l',
  '𝖒' => 'm',
  '𝖓' => 'n',
  '𝖔' => 'o',
  '𝖕' => 'p',
  '𝖖' => 'q',
  '𝖗' => 'r',
  '𝖘' => 's',
  '𝖙' => 't',
  '𝖚' => 'u',
  '𝖛' => 'v',
  '𝖜' => 'w',
  '𝖝' => 'x',
  '𝖞' => 'y',
  '𝖟' => 'z',
  '𝖠' => 'A',
  '𝖡' => 'B',
  '𝖢' => 'C',
  '𝖣' => 'D',
  '𝖤' => 'E',
  '𝖥' => 'F',
  '𝖦' => 'G',
  '𝖧' => 'H',
  '𝖨' => 'I',
  '𝖩' => 'J',
  '𝖪' => 'K',
  '𝖫' => 'L',
  '𝖬' => 'M',
  '𝖭' => 'N',
  '𝖮' => 'O',
  '𝖯' => 'P',
  '𝖰' => 'Q',
  '𝖱' => 'R',
  '𝖲' => 'S',
  '𝖳' => 'T',
  '𝖴' => 'U',
  '𝖵' => 'V',
  '𝖶' => 'W',
  '𝖷' => 'X',
  '𝖸' => 'Y',
  '𝖹' => 'Z',
  '𝖺' => 'a',
  '𝖻' => 'b',
  '𝖼' => 'c',
  '𝖽' => 'd',
  '𝖾' => 'e',
  '𝖿' => 'f',
  '𝗀' => 'g',
  '𝗁' => 'h',
  '𝗂' => 'i',
  '𝗃' => 'j',
  '𝗄' => 'k',
  '𝗅' => 'l',
  '𝗆' => 'm',
  '𝗇' => 'n',
  '𝗈' => 'o',
  '𝗉' => 'p',
  '𝗊' => 'q',
  '𝗋' => 'r',
  '𝗌' => 's',
  '𝗍' => 't',
  '𝗎' => 'u',
  '𝗏' => 'v',
  '𝗐' => 'w',
  '𝗑' => 'x',
  '𝗒' => 'y',
  '𝗓' => 'z',
  '𝗔' => 'A',
  '𝗕' => 'B',
  '𝗖' => 'C',
  '𝗗' => 'D',
  '𝗘' => 'E',
  '𝗙' => 'F',
  '𝗚' => 'G',
  '𝗛' => 'H',
  '𝗜' => 'I',
  '𝗝' => 'J',
  '𝗞' => 'K',
  '𝗟' => 'L',
  '𝗠' => 'M',
  '𝗡' => 'N',
  '𝗢' => 'O',
  '𝗣' => 'P',
  '𝗤' => 'Q',
  '𝗥' => 'R',
  '𝗦' => 'S',
  '𝗧' => 'T',
  '𝗨' => 'U',
  '𝗩' => 'V',
  '𝗪' => 'W',
  '𝗫' => 'X',
  '𝗬' => 'Y',
  '𝗭' => 'Z',
  '𝗮' => 'a',
  '𝗯' => 'b',
  '𝗰' => 'c',
  '𝗱' => 'd',
  '𝗲' => 'e',
  '𝗳' => 'f',
  '𝗴' => 'g',
  '𝗵' => 'h',
  '𝗶' => 'i',
  '𝗷' => 'j',
  '𝗸' => 'k',
  '𝗹' => 'l',
  '𝗺' => 'm',
  '𝗻' => 'n',
  '𝗼' => 'o',
  '𝗽' => 'p',
  '𝗾' => 'q',
  '𝗿' => 'r',
  '𝘀' => 's',
  '𝘁' => 't',
  '𝘂' => 'u',
  '𝘃' => 'v',
  '𝘄' => 'w',
  '𝘅' => 'x',
  '𝘆' => 'y',
  '𝘇' => 'z',
  '𝘈' => 'A',
  '𝘉' => 'B',
  '𝘊' => 'C',
  '𝘋' => 'D',
  '𝘌' => 'E',
  '𝘍' => 'F',
  '𝘎' => 'G',
  '𝘏' => 'H',
  '𝘐' => 'I',
  '𝘑' => 'J',
  '𝘒' => 'K',
  '𝘓' => 'L',
  '𝘔' => 'M',
  '𝘕' => 'N',
  '𝘖' => 'O',
  '𝘗' => 'P',
  '𝘘' => 'Q',
  '𝘙' => 'R',
  '𝘚' => 'S',
  '𝘛' => 'T',
  '𝘜' => 'U',
  '𝘝' => 'V',
  '𝘞' => 'W',
  '𝘟' => 'X',
  '𝘠' => 'Y',
  '𝘡' => 'Z',
  '𝘢' => 'a',
  '𝘣' => 'b',
  '𝘤' => 'c',
  '𝘥' => 'd',
  '𝘦' => 'e',
  '𝘧' => 'f',
  '𝘨' => 'g',
  '𝘩' => 'h',
  '𝘪' => 'i',
  '𝘫' => 'j',
  '𝘬' => 'k',
  '𝘭' => 'l',
  '𝘮' => 'm',
  '𝘯' => 'n',
  '𝘰' => 'o',
  '𝘱' => 'p',
  '𝘲' => 'q',
  '𝘳' => 'r',
  '𝘴' => 's',
  '𝘵' => 't',
  '𝘶' => 'u',
  '𝘷' => 'v',
  '𝘸' => 'w',
  '𝘹' => 'x',
  '𝘺' => 'y',
  '𝘻' => 'z',
  '𝘼' => 'A',
  '𝘽' => 'B',
  '𝘾' => 'C',
  '𝘿' => 'D',
  '𝙀' => 'E',
  '𝙁' => 'F',
  '𝙂' => 'G',
  '𝙃' => 'H',
  '𝙄' => 'I',
  '𝙅' => 'J',
  '𝙆' => 'K',
  '𝙇' => 'L',
  '𝙈' => 'M',
  '𝙉' => 'N',
  '𝙊' => 'O',
  '𝙋' => 'P',
  '𝙌' => 'Q',
  '𝙍' => 'R',
  '𝙎' => 'S',
  '𝙏' => 'T',
  '𝙐' => 'U',
  '𝙑' => 'V',
  '𝙒' => 'W',
  '𝙓' => 'X',
  '𝙔' => 'Y',
  '𝙕' => 'Z',
  '𝙖' => 'a',
  '𝙗' => 'b',
  '𝙘' => 'c',
  '𝙙' => 'd',
  '𝙚' => 'e',
  '𝙛' => 'f',
  '𝙜' => 'g',
  '𝙝' => 'h',
  '𝙞' => 'i',
  '𝙟' => 'j',
  '𝙠' => 'k',
  '𝙡' => 'l',
  '𝙢' => 'm',
  '𝙣' => 'n',
  '𝙤' => 'o',
  '𝙥' => 'p',
  '𝙦' => 'q',
  '𝙧' => 'r',
  '𝙨' => 's',
  '𝙩' => 't',
  '𝙪' => 'u',
  '𝙫' => 'v',
  '𝙬' => 'w',
  '𝙭' => 'x',
  '𝙮' => 'y',
  '𝙯' => 'z',
  '𝙰' => 'A',
  '𝙱' => 'B',
  '𝙲' => 'C',
  '𝙳' => 'D',
  '𝙴' => 'E',
  '𝙵' => 'F',
  '𝙶' => 'G',
  '𝙷' => 'H',
  '𝙸' => 'I',
  '𝙹' => 'J',
  '𝙺' => 'K',
  '𝙻' => 'L',
  '𝙼' => 'M',
  '𝙽' => 'N',
  '𝙾' => 'O',
  '𝙿' => 'P',
  '𝚀' => 'Q',
  '𝚁' => 'R',
  '𝚂' => 'S',
  '𝚃' => 'T',
  '𝚄' => 'U',
  '𝚅' => 'V',
  '𝚆' => 'W',
  '𝚇' => 'X',
  '𝚈' => 'Y',
  '𝚉' => 'Z',
  '𝚊' => 'a',
  '𝚋' => 'b',
  '𝚌' => 'c',
  '𝚍' => 'd',
  '𝚎' => 'e',
  '𝚏' => 'f',
  '𝚐' => 'g',
  '𝚑' => 'h',
  '𝚒' => 'i',
  '𝚓' => 'j',
  '𝚔' => 'k',
  '𝚕' => 'l',
  '𝚖' => 'm',
  '𝚗' => 'n',
  '𝚘' => 'o',
  '𝚙' => 'p',
  '𝚚' => 'q',
  '𝚛' => 'r',
  '𝚜' => 's',
  '𝚝' => 't',
  '𝚞' => 'u',
  '𝚟' => 'v',
  '𝚠' => 'w',
  '𝚡' => 'x',
  '𝚢' => 'y',
  '𝚣' => 'z',
  '𝚤' => 'ı',
  '𝚥' => 'ȷ',
  '𝚨' => 'Α',
  '𝚩' => 'Β',
  '𝚪' => 'Γ',
  '𝚫' => 'Δ',
  '𝚬' => 'Ε',
  '𝚭' => 'Ζ',
  '𝚮' => 'Η',
  '𝚯' => 'Θ',
  '𝚰' => 'Ι',
  '𝚱' => 'Κ',
  '𝚲' => 'Λ',
  '𝚳' => 'Μ',
  '𝚴' => 'Ν',
  '𝚵' => 'Ξ',
  '𝚶' => 'Ο',
  '𝚷' => 'Π',
  '𝚸' => 'Ρ',
  '𝚹' => 'Θ',
  '𝚺' => 'Σ',
  '𝚻' => 'Τ',
  '𝚼' => 'Υ',
  '𝚽' => 'Φ',
  '𝚾' => 'Χ',
  '𝚿' => 'Ψ',
  '𝛀' => 'Ω',
  '𝛁' => '∇',
  '𝛂' => 'α',
  '𝛃' => 'β',
  '𝛄' => 'γ',
  '𝛅' => 'δ',
  '𝛆' => 'ε',
  '𝛇' => 'ζ',
  '𝛈' => 'η',
  '𝛉' => 'θ',
  '𝛊' => 'ι',
  '𝛋' => 'κ',
  '𝛌' => 'λ',
  '𝛍' => 'μ',
  '𝛎' => 'ν',
  '𝛏' => 'ξ',
  '𝛐' => 'ο',
  '𝛑' => 'π',
  '𝛒' => 'ρ',
  '𝛓' => 'ς',
  '𝛔' => 'σ',
  '𝛕' => 'τ',
  '𝛖' => 'υ',
  '𝛗' => 'φ',
  '𝛘' => 'χ',
  '𝛙' => 'ψ',
  '𝛚' => 'ω',
  '𝛛' => '∂',
  '𝛜' => 'ε',
  '𝛝' => 'θ',
  '𝛞' => 'κ',
  '𝛟' => 'φ',
  '𝛠' => 'ρ',
  '𝛡' => 'π',
  '𝛢' => 'Α',
  '𝛣' => 'Β',
  '𝛤' => 'Γ',
  '𝛥' => 'Δ',
  '𝛦' => 'Ε',
  '𝛧' => 'Ζ',
  '𝛨' => 'Η',
  '𝛩' => 'Θ',
  '𝛪' => 'Ι',
  '𝛫' => 'Κ',
  '𝛬' => 'Λ',
  '𝛭' => 'Μ',
  '𝛮' => 'Ν',
  '𝛯' => 'Ξ',
  '𝛰' => 'Ο',
  '𝛱' => 'Π',
  '𝛲' => 'Ρ',
  '𝛳' => 'Θ',
  '𝛴' => 'Σ',
  '𝛵' => 'Τ',
  '𝛶' => 'Υ',
  '𝛷' => 'Φ',
  '𝛸' => 'Χ',
  '𝛹' => 'Ψ',
  '𝛺' => 'Ω',
  '𝛻' => '∇',
  '𝛼' => 'α',
  '𝛽' => 'β',
  '𝛾' => 'γ',
  '𝛿' => 'δ',
  '𝜀' => 'ε',
  '𝜁' => 'ζ',
  '𝜂' => 'η',
  '𝜃' => 'θ',
  '𝜄' => 'ι',
  '𝜅' => 'κ',
  '𝜆' => 'λ',
  '𝜇' => 'μ',
  '𝜈' => 'ν',
  '𝜉' => 'ξ',
  '𝜊' => 'ο',
  '𝜋' => 'π',
  '𝜌' => 'ρ',
  '𝜍' => 'ς',
  '𝜎' => 'σ',
  '𝜏' => 'τ',
  '𝜐' => 'υ',
  '𝜑' => 'φ',
  '𝜒' => 'χ',
  '𝜓' => 'ψ',
  '𝜔' => 'ω',
  '𝜕' => '∂',
  '𝜖' => 'ε',
  '𝜗' => 'θ',
  '𝜘' => 'κ',
  '𝜙' => 'φ',
  '𝜚' => 'ρ',
  '𝜛' => 'π',
  '𝜜' => 'Α',
  '𝜝' => 'Β',
  '𝜞' => 'Γ',
  '𝜟' => 'Δ',
  '𝜠' => 'Ε',
  '𝜡' => 'Ζ',
  '𝜢' => 'Η',
  '𝜣' => 'Θ',
  '𝜤' => 'Ι',
  '𝜥' => 'Κ',
  '𝜦' => 'Λ',
  '𝜧' => 'Μ',
  '𝜨' => 'Ν',
  '𝜩' => 'Ξ',
  '𝜪' => 'Ο',
  '𝜫' => 'Π',
  '𝜬' => 'Ρ',
  '𝜭' => 'Θ',
  '𝜮' => 'Σ',
  '𝜯' => 'Τ',
  '𝜰' => 'Υ',
  '𝜱' => 'Φ',
  '𝜲' => 'Χ',
  '𝜳' => 'Ψ',
  '𝜴' => 'Ω',
  '𝜵' => '∇',
  '𝜶' => 'α',
  '𝜷' => 'β',
  '𝜸' => 'γ',
  '𝜹' => 'δ',
  '𝜺' => 'ε',
  '𝜻' => 'ζ',
  '𝜼' => 'η',
  '𝜽' => 'θ',
  '𝜾' => 'ι',
  '𝜿' => 'κ',
  '𝝀' => 'λ',
  '𝝁' => 'μ',
  '𝝂' => 'ν',
  '𝝃' => 'ξ',
  '𝝄' => 'ο',
  '𝝅' => 'π',
  '𝝆' => 'ρ',
  '𝝇' => 'ς',
  '𝝈' => 'σ',
  '𝝉' => 'τ',
  '𝝊' => 'υ',
  '𝝋' => 'φ',
  '𝝌' => 'χ',
  '𝝍' => 'ψ',
  '𝝎' => 'ω',
  '𝝏' => '∂',
  '𝝐' => 'ε',
  '𝝑' => 'θ',
  '𝝒' => 'κ',
  '𝝓' => 'φ',
  '𝝔' => 'ρ',
  '𝝕' => 'π',
  '𝝖' => 'Α',
  '𝝗' => 'Β',
  '𝝘' => 'Γ',
  '𝝙' => 'Δ',
  '𝝚' => 'Ε',
  '𝝛' => 'Ζ',
  '𝝜' => 'Η',
  '𝝝' => 'Θ',
  '𝝞' => 'Ι',
  '𝝟' => 'Κ',
  '𝝠' => 'Λ',
  '𝝡' => 'Μ',
  '𝝢' => 'Ν',
  '𝝣' => 'Ξ',
  '𝝤' => 'Ο',
  '𝝥' => 'Π',
  '𝝦' => 'Ρ',
  '𝝧' => 'Θ',
  '𝝨' => 'Σ',
  '𝝩' => 'Τ',
  '𝝪' => 'Υ',
  '𝝫' => 'Φ',
  '𝝬' => 'Χ',
  '𝝭' => 'Ψ',
  '𝝮' => 'Ω',
  '𝝯' => '∇',
  '𝝰' => 'α',
  '𝝱' => 'β',
  '𝝲' => 'γ',
  '𝝳' => 'δ',
  '𝝴' => 'ε',
  '𝝵' => 'ζ',
  '𝝶' => 'η',
  '𝝷' => 'θ',
  '𝝸' => 'ι',
  '𝝹' => 'κ',
  '𝝺' => 'λ',
  '𝝻' => 'μ',
  '𝝼' => 'ν',
  '𝝽' => 'ξ',
  '𝝾' => 'ο',
  '𝝿' => 'π',
  '𝞀' => 'ρ',
  '𝞁' => 'ς',
  '𝞂' => 'σ',
  '𝞃' => 'τ',
  '𝞄' => 'υ',
  '𝞅' => 'φ',
  '𝞆' => 'χ',
  '𝞇' => 'ψ',
  '𝞈' => 'ω',
  '𝞉' => '∂',
  '𝞊' => 'ε',
  '𝞋' => 'θ',
  '𝞌' => 'κ',
  '𝞍' => 'φ',
  '𝞎' => 'ρ',
  '𝞏' => 'π',
  '𝞐' => 'Α',
  '𝞑' => 'Β',
  '𝞒' => 'Γ',
  '𝞓' => 'Δ',
  '𝞔' => 'Ε',
  '𝞕' => 'Ζ',
  '𝞖' => 'Η',
  '𝞗' => 'Θ',
  '𝞘' => 'Ι',
  '𝞙' => 'Κ',
  '𝞚' => 'Λ',
  '𝞛' => 'Μ',
  '𝞜' => 'Ν',
  '𝞝' => 'Ξ',
  '𝞞' => 'Ο',
  '𝞟' => 'Π',
  '𝞠' => 'Ρ',
  '𝞡' => 'Θ',
  '𝞢' => 'Σ',
  '𝞣' => 'Τ',
  '𝞤' => 'Υ',
  '𝞥' => 'Φ',
  '𝞦' => 'Χ',
  '𝞧' => 'Ψ',
  '𝞨' => 'Ω',
  '𝞩' => '∇',
  '𝞪' => 'α',
  '𝞫' => 'β',
  '𝞬' => 'γ',
  '𝞭' => 'δ',
  '𝞮' => 'ε',
  '𝞯' => 'ζ',
  '𝞰' => 'η',
  '𝞱' => 'θ',
  '𝞲' => 'ι',
  '𝞳' => 'κ',
  '𝞴' => 'λ',
  '𝞵' => 'μ',
  '𝞶' => 'ν',
  '𝞷' => 'ξ',
  '𝞸' => 'ο',
  '𝞹' => 'π',
  '𝞺' => 'ρ',
  '𝞻' => 'ς',
  '𝞼' => 'σ',
  '𝞽' => 'τ',
  '𝞾' => 'υ',
  '𝞿' => 'φ',
  '𝟀' => 'χ',
  '𝟁' => 'ψ',
  '𝟂' => 'ω',
  '𝟃' => '∂',
  '𝟄' => 'ε',
  '𝟅' => 'θ',
  '𝟆' => 'κ',
  '𝟇' => 'φ',
  '𝟈' => 'ρ',
  '𝟉' => 'π',
  '𝟊' => 'Ϝ',
  '𝟋' => 'ϝ',
  '𝟎' => '0',
  '𝟏' => '1',
  '𝟐' => '2',
  '𝟑' => '3',
  '𝟒' => '4',
  '𝟓' => '5',
  '𝟔' => '6',
  '𝟕' => '7',
  '𝟖' => '8',
  '𝟗' => '9',
  '𝟘' => '0',
  '𝟙' => '1',
  '𝟚' => '2',
  '𝟛' => '3',
  '𝟜' => '4',
  '𝟝' => '5',
  '𝟞' => '6',
  '𝟟' => '7',
  '𝟠' => '8',
  '𝟡' => '9',
  '𝟢' => '0',
  '𝟣' => '1',
  '𝟤' => '2',
  '𝟥' => '3',
  '𝟦' => '4',
  '𝟧' => '5',
  '𝟨' => '6',
  '𝟩' => '7',
  '𝟪' => '8',
  '𝟫' => '9',
  '𝟬' => '0',
  '𝟭' => '1',
  '𝟮' => '2',
  '𝟯' => '3',
  '𝟰' => '4',
  '𝟱' => '5',
  '𝟲' => '6',
  '𝟳' => '7',
  '𝟴' => '8',
  '𝟵' => '9',
  '𝟶' => '0',
  '𝟷' => '1',
  '𝟸' => '2',
  '𝟹' => '3',
  '𝟺' => '4',
  '𝟻' => '5',
  '𝟼' => '6',
  '𝟽' => '7',
  '𝟾' => '8',
  '𝟿' => '9',
  '𞸀' => 'ا',
  '𞸁' => 'ب',
  '𞸂' => 'ج',
  '𞸃' => 'د',
  '𞸅' => 'و',
  '𞸆' => 'ز',
  '𞸇' => 'ح',
  '𞸈' => 'ط',
  '𞸉' => 'ي',
  '𞸊' => 'ك',
  '𞸋' => 'ل',
  '𞸌' => 'م',
  '𞸍' => 'ن',
  '𞸎' => 'س',
  '𞸏' => 'ع',
  '𞸐' => 'ف',
  '𞸑' => 'ص',
  '𞸒' => 'ق',
  '𞸓' => 'ر',
  '𞸔' => 'ش',
  '𞸕' => 'ت',
  '𞸖' => 'ث',
  '𞸗' => 'خ',
  '𞸘' => 'ذ',
  '𞸙' => 'ض',
  '𞸚' => 'ظ',
  '𞸛' => 'غ',
  '𞸜' => 'ٮ',
  '𞸝' => 'ں',
  '𞸞' => 'ڡ',
  '𞸟' => 'ٯ',
  '𞸡' => 'ب',
  '𞸢' => 'ج',
  '𞸤' => 'ه',
  '𞸧' => 'ح',
  '𞸩' => 'ي',
  '𞸪' => 'ك',
  '𞸫' => 'ل',
  '𞸬' => 'م',
  '𞸭' => 'ن',
  '𞸮' => 'س',
  '𞸯' => 'ع',
  '𞸰' => 'ف',
  '𞸱' => 'ص',
  '𞸲' => 'ق',
  '𞸴' => 'ش',
  '𞸵' => 'ت',
  '𞸶' => 'ث',
  '𞸷' => 'خ',
  '𞸹' => 'ض',
  '𞸻' => 'غ',
  '𞹂' => 'ج',
  '𞹇' => 'ح',
  '𞹉' => 'ي',
  '𞹋' => 'ل',
  '𞹍' => 'ن',
  '𞹎' => 'س',
  '𞹏' => 'ع',
  '𞹑' => 'ص',
  '𞹒' => 'ق',
  '𞹔' => 'ش',
  '𞹗' => 'خ',
  '𞹙' => 'ض',
  '𞹛' => 'غ',
  '𞹝' => 'ں',
  '𞹟' => 'ٯ',
  '𞹡' => 'ب',
  '𞹢' => 'ج',
  '𞹤' => 'ه',
  '𞹧' => 'ح',
  '𞹨' => 'ط',
  '𞹩' => 'ي',
  '𞹪' => 'ك',
  '𞹬' => 'م',
  '𞹭' => 'ن',
  '𞹮' => 'س',
  '𞹯' => 'ع',
  '𞹰' => 'ف',
  '𞹱' => 'ص',
  '𞹲' => 'ق',
  '𞹴' => 'ش',
  '𞹵' => 'ت',
  '𞹶' => 'ث',
  '𞹷' => 'خ',
  '𞹹' => 'ض',
  '𞹺' => 'ظ',
  '𞹻' => 'غ',
  '𞹼' => 'ٮ',
  '𞹾' => 'ڡ',
  '𞺀' => 'ا',
  '𞺁' => 'ب',
  '𞺂' => 'ج',
  '𞺃' => 'د',
  '𞺄' => 'ه',
  '𞺅' => 'و',
  '𞺆' => 'ز',
  '𞺇' => 'ح',
  '𞺈' => 'ط',
  '𞺉' => 'ي',
  '𞺋' => 'ل',
  '𞺌' => 'م',
  '𞺍' => 'ن',
  '𞺎' => 'س',
  '𞺏' => 'ع',
  '𞺐' => 'ف',
  '𞺑' => 'ص',
  '𞺒' => 'ق',
  '𞺓' => 'ر',
  '𞺔' => 'ش',
  '𞺕' => 'ت',
  '𞺖' => 'ث',
  '𞺗' => 'خ',
  '𞺘' => 'ذ',
  '𞺙' => 'ض',
  '𞺚' => 'ظ',
  '𞺛' => 'غ',
  '𞺡' => 'ب',
  '𞺢' => 'ج',
  '𞺣' => 'د',
  '𞺥' => 'و',
  '𞺦' => 'ز',
  '𞺧' => 'ح',
  '𞺨' => 'ط',
  '𞺩' => 'ي',
  '𞺫' => 'ل',
  '𞺬' => 'م',
  '𞺭' => 'ن',
  '𞺮' => 'س',
  '𞺯' => 'ع',
  '𞺰' => 'ف',
  '𞺱' => 'ص',
  '𞺲' => 'ق',
  '𞺳' => 'ر',
  '𞺴' => 'ش',
  '𞺵' => 'ت',
  '𞺶' => 'ث',
  '𞺷' => 'خ',
  '𞺸' => 'ذ',
  '𞺹' => 'ض',
  '𞺺' => 'ظ',
  '𞺻' => 'غ',
  '🄀' => '0.',
  '🄁' => '0,',
  '🄂' => '1,',
  '🄃' => '2,',
  '🄄' => '3,',
  '🄅' => '4,',
  '🄆' => '5,',
  '🄇' => '6,',
  '🄈' => '7,',
  '🄉' => '8,',
  '🄊' => '9,',
  '🄐' => '(A)',
  '🄑' => '(B)',
  '🄒' => '(C)',
  '🄓' => '(D)',
  '🄔' => '(E)',
  '🄕' => '(F)',
  '🄖' => '(G)',
  '🄗' => '(H)',
  '🄘' => '(I)',
  '🄙' => '(J)',
  '🄚' => '(K)',
  '🄛' => '(L)',
  '🄜' => '(M)',
  '🄝' => '(N)',
  '🄞' => '(O)',
  '🄟' => '(P)',
  '🄠' => '(Q)',
  '🄡' => '(R)',
  '🄢' => '(S)',
  '🄣' => '(T)',
  '🄤' => '(U)',
  '🄥' => '(V)',
  '🄦' => '(W)',
  '🄧' => '(X)',
  '🄨' => '(Y)',
  '🄩' => '(Z)',
  '🄪' => '〔S〕',
  '🄫' => 'C',
  '🄬' => 'R',
  '🄭' => 'CD',
  '🄮' => 'WZ',
  '🄰' => 'A',
  '🄱' => 'B',
  '🄲' => 'C',
  '🄳' => 'D',
  '🄴' => 'E',
  '🄵' => 'F',
  '🄶' => 'G',
  '🄷' => 'H',
  '🄸' => 'I',
  '🄹' => 'J',
  '🄺' => 'K',
  '🄻' => 'L',
  '🄼' => 'M',
  '🄽' => 'N',
  '🄾' => 'O',
  '🄿' => 'P',
  '🅀' => 'Q',
  '🅁' => 'R',
  '🅂' => 'S',
  '🅃' => 'T',
  '🅄' => 'U',
  '🅅' => 'V',
  '🅆' => 'W',
  '🅇' => 'X',
  '🅈' => 'Y',
  '🅉' => 'Z',
  '🅊' => 'HV',
  '🅋' => 'MV',
  '🅌' => 'SD',
  '🅍' => 'SS',
  '🅎' => 'PPV',
  '🅏' => 'WC',
  '🅪' => 'MC',
  '🅫' => 'MD',
  '🅬' => 'MR',
  '🆐' => 'DJ',
  '🈀' => 'ほか',
  '🈁' => 'ココ',
  '🈂' => 'サ',
  '🈐' => '手',
  '🈑' => '字',
  '🈒' => '双',
  '🈓' => 'デ',
  '🈔' => '二',
  '🈕' => '多',
  '🈖' => '解',
  '🈗' => '天',
  '🈘' => '交',
  '🈙' => '映',
  '🈚' => '無',
  '🈛' => '料',
  '🈜' => '前',
  '🈝' => '後',
  '🈞' => '再',
  '🈟' => '新',
  '🈠' => '初',
  '🈡' => '終',
  '🈢' => '生',
  '🈣' => '販',
  '🈤' => '声',
  '🈥' => '吹',
  '🈦' => '演',
  '🈧' => '投',
  '🈨' => '捕',
  '🈩' => '一',
  '🈪' => '三',
  '🈫' => '遊',
  '🈬' => '左',
  '🈭' => '中',
  '🈮' => '右',
  '🈯' => '指',
  '🈰' => '走',
  '🈱' => '打',
  '🈲' => '禁',
  '🈳' => '空',
  '🈴' => '合',
  '🈵' => '満',
  '🈶' => '有',
  '🈷' => '月',
  '🈸' => '申',
  '🈹' => '割',
  '🈺' => '営',
  '🈻' => '配',
  '🉀' => '〔本〕',
  '🉁' => '〔三〕',
  '🉂' => '〔二〕',
  '🉃' => '〔安〕',
  '🉄' => '〔点〕',
  '🉅' => '〔打〕',
  '🉆' => '〔盗〕',
  '🉇' => '〔勝〕',
  '🉈' => '〔敗〕',
  '🉐' => '得',
  '🉑' => '可',
  '🯰' => '0',
  '🯱' => '1',
  '🯲' => '2',
  '🯳' => '3',
  '🯴' => '4',
  '🯵' => '5',
  '🯶' => '6',
  '🯷' => '7',
  '🯸' => '8',
  '🯹' => '9',
);
<?php

return array (
  '̀' => 230,
  '́' => 230,
  '̂' => 230,
  '̃' => 230,
  '̄' => 230,
  '̅' => 230,
  '̆' => 230,
  '̇' => 230,
  '̈' => 230,
  '̉' => 230,
  '̊' => 230,
  '̋' => 230,
  '̌' => 230,
  '̍' => 230,
  '̎' => 230,
  '̏' => 230,
  '̐' => 230,
  '̑' => 230,
  '̒' => 230,
  '̓' => 230,
  '̔' => 230,
  '̕' => 232,
  '̖' => 220,
  '̗' => 220,
  '̘' => 220,
  '̙' => 220,
  '̚' => 232,
  '̛' => 216,
  '̜' => 220,
  '̝' => 220,
  '̞' => 220,
  '̟' => 220,
  '̠' => 220,
  '̡' => 202,
  '̢' => 202,
  '̣' => 220,
  '̤' => 220,
  '̥' => 220,
  '̦' => 220,
  '̧' => 202,
  '̨' => 202,
  '̩' => 220,
  '̪' => 220,
  '̫' => 220,
  '̬' => 220,
  '̭' => 220,
  '̮' => 220,
  '̯' => 220,
  '̰' => 220,
  '̱' => 220,
  '̲' => 220,
  '̳' => 220,
  '̴' => 1,
  '̵' => 1,
  '̶' => 1,
  '̷' => 1,
  '̸' => 1,
  '̹' => 220,
  '̺' => 220,
  '̻' => 220,
  '̼' => 220,
  '̽' => 230,
  '̾' => 230,
  '̿' => 230,
  '̀' => 230,
  '́' => 230,
  '͂' => 230,
  '̓' => 230,
  '̈́' => 230,
  'ͅ' => 240,
  '͆' => 230,
  '͇' => 220,
  '͈' => 220,
  '͉' => 220,
  '͊' => 230,
  '͋' => 230,
  '͌' => 230,
  '͍' => 220,
  '͎' => 220,
  '͐' => 230,
  '͑' => 230,
  '͒' => 230,
  '͓' => 220,
  '͔' => 220,
  '͕' => 220,
  '͖' => 220,
  '͗' => 230,
  '͘' => 232,
  '͙' => 220,
  '͚' => 220,
  '͛' => 230,
  '͜' => 233,
  '͝' => 234,
  '͞' => 234,
  '͟' => 233,
  '͠' => 234,
  '͡' => 234,
  '͢' => 233,
  'ͣ' => 230,
  'ͤ' => 230,
  'ͥ' => 230,
  'ͦ' => 230,
  'ͧ' => 230,
  'ͨ' => 230,
  'ͩ' => 230,
  'ͪ' => 230,
  'ͫ' => 230,
  'ͬ' => 230,
  'ͭ' => 230,
  'ͮ' => 230,
  'ͯ' => 230,
  '҃' => 230,
  '҄' => 230,
  '҅' => 230,
  '҆' => 230,
  '҇' => 230,
  '֑' => 220,
  '֒' => 230,
  '֓' => 230,
  '֔' => 230,
  '֕' => 230,
  '֖' => 220,
  '֗' => 230,
  '֘' => 230,
  '֙' => 230,
  '֚' => 222,
  '֛' => 220,
  '֜' => 230,
  '֝' => 230,
  '֞' => 230,
  '֟' => 230,
  '֠' => 230,
  '֡' => 230,
  '֢' => 220,
  '֣' => 220,
  '֤' => 220,
  '֥' => 220,
  '֦' => 220,
  '֧' => 220,
  '֨' => 230,
  '֩' => 230,
  '֪' => 220,
  '֫' => 230,
  '֬' => 230,
  '֭' => 222,
  '֮' => 228,
  '֯' => 230,
  'ְ' => 10,
  'ֱ' => 11,
  'ֲ' => 12,
  'ֳ' => 13,
  'ִ' => 14,
  'ֵ' => 15,
  'ֶ' => 16,
  'ַ' => 17,
  'ָ' => 18,
  'ֹ' => 19,
  'ֺ' => 19,
  'ֻ' => 20,
  'ּ' => 21,
  'ֽ' => 22,
  'ֿ' => 23,
  'ׁ' => 24,
  'ׂ' => 25,
  'ׄ' => 230,
  'ׅ' => 220,
  'ׇ' => 18,
  'ؐ' => 230,
  'ؑ' => 230,
  'ؒ' => 230,
  'ؓ' => 230,
  'ؔ' => 230,
  'ؕ' => 230,
  'ؖ' => 230,
  'ؗ' => 230,
  'ؘ' => 30,
  'ؙ' => 31,
  'ؚ' => 32,
  'ً' => 27,
  'ٌ' => 28,
  'ٍ' => 29,
  'َ' => 30,
  'ُ' => 31,
  'ِ' => 32,
  'ّ' => 33,
  'ْ' => 34,
  'ٓ' => 230,
  'ٔ' => 230,
  'ٕ' => 220,
  'ٖ' => 220,
  'ٗ' => 230,
  '٘' => 230,
  'ٙ' => 230,
  'ٚ' => 230,
  'ٛ' => 230,
  'ٜ' => 220,
  'ٝ' => 230,
  'ٞ' => 230,
  'ٟ' => 220,
  'ٰ' => 35,
  'ۖ' => 230,
  'ۗ' => 230,
  'ۘ' => 230,
  'ۙ' => 230,
  'ۚ' => 230,
  'ۛ' => 230,
  'ۜ' => 230,
  '۟' => 230,
  '۠' => 230,
  'ۡ' => 230,
  'ۢ' => 230,
  'ۣ' => 220,
  'ۤ' => 230,
  'ۧ' => 230,
  'ۨ' => 230,
  '۪' => 220,
  '۫' => 230,
  '۬' => 230,
  'ۭ' => 220,
  'ܑ' => 36,
  'ܰ' => 230,
  'ܱ' => 220,
  'ܲ' => 230,
  'ܳ' => 230,
  'ܴ' => 220,
  'ܵ' => 230,
  'ܶ' => 230,
  'ܷ' => 220,
  'ܸ' => 220,
  'ܹ' => 220,
  'ܺ' => 230,
  'ܻ' => 220,
  'ܼ' => 220,
  'ܽ' => 230,
  'ܾ' => 220,
  'ܿ' => 230,
  '݀' => 230,
  '݁' => 230,
  '݂' => 220,
  '݃' => 230,
  '݄' => 220,
  '݅' => 230,
  '݆' => 220,
  '݇' => 230,
  '݈' => 220,
  '݉' => 230,
  '݊' => 230,
  '߫' => 230,
  '߬' => 230,
  '߭' => 230,
  '߮' => 230,
  '߯' => 230,
  '߰' => 230,
  '߱' => 230,
  '߲' => 220,
  '߳' => 230,
  '߽' => 220,
  'ࠖ' => 230,
  'ࠗ' => 230,
  '࠘' => 230,
  '࠙' => 230,
  'ࠛ' => 230,
  'ࠜ' => 230,
  'ࠝ' => 230,
  'ࠞ' => 230,
  'ࠟ' => 230,
  'ࠠ' => 230,
  'ࠡ' => 230,
  'ࠢ' => 230,
  'ࠣ' => 230,
  'ࠥ' => 230,
  'ࠦ' => 230,
  'ࠧ' => 230,
  'ࠩ' => 230,
  'ࠪ' => 230,
  'ࠫ' => 230,
  'ࠬ' => 230,
  '࠭' => 230,
  '࡙' => 220,
  '࡚' => 220,
  '࡛' => 220,
  '࣓' => 220,
  'ࣔ' => 230,
  'ࣕ' => 230,
  'ࣖ' => 230,
  'ࣗ' => 230,
  'ࣘ' => 230,
  'ࣙ' => 230,
  'ࣚ' => 230,
  'ࣛ' => 230,
  'ࣜ' => 230,
  'ࣝ' => 230,
  'ࣞ' => 230,
  'ࣟ' => 230,
  '࣠' => 230,
  '࣡' => 230,
  'ࣣ' => 220,
  'ࣤ' => 230,
  'ࣥ' => 230,
  'ࣦ' => 220,
  'ࣧ' => 230,
  'ࣨ' => 230,
  'ࣩ' => 220,
  '࣪' => 230,
  '࣫' => 230,
  '࣬' => 230,
  '࣭' => 220,
  '࣮' => 220,
  '࣯' => 220,
  'ࣰ' => 27,
  'ࣱ' => 28,
  'ࣲ' => 29,
  'ࣳ' => 230,
  'ࣴ' => 230,
  'ࣵ' => 230,
  'ࣶ' => 220,
  'ࣷ' => 230,
  'ࣸ' => 230,
  'ࣹ' => 220,
  'ࣺ' => 220,
  'ࣻ' => 230,
  'ࣼ' => 230,
  'ࣽ' => 230,
  'ࣾ' => 230,
  'ࣿ' => 230,
  '़' => 7,
  '्' => 9,
  '॑' => 230,
  '॒' => 220,
  '॓' => 230,
  '॔' => 230,
  '়' => 7,
  '্' => 9,
  '৾' => 230,
  '਼' => 7,
  '੍' => 9,
  '઼' => 7,
  '્' => 9,
  '଼' => 7,
  '୍' => 9,
  '்' => 9,
  '్' => 9,
  'ౕ' => 84,
  'ౖ' => 91,
  '಼' => 7,
  '್' => 9,
  '഻' => 9,
  '഼' => 9,
  '്' => 9,
  '්' => 9,
  'ุ' => 103,
  'ู' => 103,
  'ฺ' => 9,
  '่' => 107,
  '้' => 107,
  '๊' => 107,
  '๋' => 107,
  'ຸ' => 118,
  'ູ' => 118,
  '຺' => 9,
  '່' => 122,
  '້' => 122,
  '໊' => 122,
  '໋' => 122,
  '༘' => 220,
  '༙' => 220,
  '༵' => 220,
  '༷' => 220,
  '༹' => 216,
  'ཱ' => 129,
  'ི' => 130,
  'ུ' => 132,
  'ེ' => 130,
  'ཻ' => 130,
  'ོ' => 130,
  'ཽ' => 130,
  'ྀ' => 130,
  'ྂ' => 230,
  'ྃ' => 230,
  '྄' => 9,
  '྆' => 230,
  '྇' => 230,
  '࿆' => 220,
  '့' => 7,
  '္' => 9,
  '်' => 9,
  'ႍ' => 220,
  '፝' => 230,
  '፞' => 230,
  '፟' => 230,
  '᜔' => 9,
  '᜴' => 9,
  '្' => 9,
  '៝' => 230,
  'ᢩ' => 228,
  '᤹' => 222,
  '᤺' => 230,
  '᤻' => 220,
  'ᨗ' => 230,
  'ᨘ' => 220,
  '᩠' => 9,
  '᩵' => 230,
  '᩶' => 230,
  '᩷' => 230,
  '᩸' => 230,
  '᩹' => 230,
  '᩺' => 230,
  '᩻' => 230,
  '᩼' => 230,
  '᩿' => 220,
  '᪰' => 230,
  '᪱' => 230,
  '᪲' => 230,
  '᪳' => 230,
  '᪴' => 230,
  '᪵' => 220,
  '᪶' => 220,
  '᪷' => 220,
  '᪸' => 220,
  '᪹' => 220,
  '᪺' => 220,
  '᪻' => 230,
  '᪼' => 230,
  '᪽' => 220,
  'ᪿ' => 220,
  'ᫀ' => 220,
  '᬴' => 7,
  '᭄' => 9,
  '᭫' => 230,
  '᭬' => 220,
  '᭭' => 230,
  '᭮' => 230,
  '᭯' => 230,
  '᭰' => 230,
  '᭱' => 230,
  '᭲' => 230,
  '᭳' => 230,
  '᮪' => 9,
  '᮫' => 9,
  '᯦' => 7,
  '᯲' => 9,
  '᯳' => 9,
  '᰷' => 7,
  '᳐' => 230,
  '᳑' => 230,
  '᳒' => 230,
  '᳔' => 1,
  '᳕' => 220,
  '᳖' => 220,
  '᳗' => 220,
  '᳘' => 220,
  '᳙' => 220,
  '᳚' => 230,
  '᳛' => 230,
  '᳜' => 220,
  '᳝' => 220,
  '᳞' => 220,
  '᳟' => 220,
  '᳠' => 230,
  '᳢' => 1,
  '᳣' => 1,
  '᳤' => 1,
  '᳥' => 1,
  '᳦' => 1,
  '᳧' => 1,
  '᳨' => 1,
  '᳭' => 220,
  '᳴' => 230,
  '᳸' => 230,
  '᳹' => 230,
  '᷀' => 230,
  '᷁' => 230,
  '᷂' => 220,
  '᷃' => 230,
  '᷄' => 230,
  '᷅' => 230,
  '᷆' => 230,
  '᷇' => 230,
  '᷈' => 230,
  '᷉' => 230,
  '᷊' => 220,
  '᷋' => 230,
  '᷌' => 230,
  '᷍' => 234,
  '᷎' => 214,
  '᷏' => 220,
  '᷐' => 202,
  '᷑' => 230,
  '᷒' => 230,
  'ᷓ' => 230,
  'ᷔ' => 230,
  'ᷕ' => 230,
  'ᷖ' => 230,
  'ᷗ' => 230,
  'ᷘ' => 230,
  'ᷙ' => 230,
  'ᷚ' => 230,
  'ᷛ' => 230,
  'ᷜ' => 230,
  'ᷝ' => 230,
  'ᷞ' => 230,
  'ᷟ' => 230,
  'ᷠ' => 230,
  'ᷡ' => 230,
  'ᷢ' => 230,
  'ᷣ' => 230,
  'ᷤ' => 230,
  'ᷥ' => 230,
  'ᷦ' => 230,
  'ᷧ' => 230,
  'ᷨ' => 230,
  'ᷩ' => 230,
  'ᷪ' => 230,
  'ᷫ' => 230,
  'ᷬ' => 230,
  'ᷭ' => 230,
  'ᷮ' => 230,
  'ᷯ' => 230,
  'ᷰ' => 230,
  'ᷱ' => 230,
  'ᷲ' => 230,
  'ᷳ' => 230,
  'ᷴ' => 230,
  '᷵' => 230,
  '᷶' => 232,
  '᷷' => 228,
  '᷸' => 228,
  '᷹' => 220,
  '᷻' => 230,
  '᷼' => 233,
  '᷽' => 220,
  '᷾' => 230,
  '᷿' => 220,
  '⃐' => 230,
  '⃑' => 230,
  '⃒' => 1,
  '⃓' => 1,
  '⃔' => 230,
  '⃕' => 230,
  '⃖' => 230,
  '⃗' => 230,
  '⃘' => 1,
  '⃙' => 1,
  '⃚' => 1,
  '⃛' => 230,
  '⃜' => 230,
  '⃡' => 230,
  '⃥' => 1,
  '⃦' => 1,
  '⃧' => 230,
  '⃨' => 220,
  '⃩' => 230,
  '⃪' => 1,
  '⃫' => 1,
  '⃬' => 220,
  '⃭' => 220,
  '⃮' => 220,
  '⃯' => 220,
  '⃰' => 230,
  '⳯' => 230,
  '⳰' => 230,
  '⳱' => 230,
  '⵿' => 9,
  'ⷠ' => 230,
  'ⷡ' => 230,
  'ⷢ' => 230,
  'ⷣ' => 230,
  'ⷤ' => 230,
  'ⷥ' => 230,
  'ⷦ' => 230,
  'ⷧ' => 230,
  'ⷨ' => 230,
  'ⷩ' => 230,
  'ⷪ' => 230,
  'ⷫ' => 230,
  'ⷬ' => 230,
  'ⷭ' => 230,
  'ⷮ' => 230,
  'ⷯ' => 230,
  'ⷰ' => 230,
  'ⷱ' => 230,
  'ⷲ' => 230,
  'ⷳ' => 230,
  'ⷴ' => 230,
  'ⷵ' => 230,
  'ⷶ' => 230,
  'ⷷ' => 230,
  'ⷸ' => 230,
  'ⷹ' => 230,
  'ⷺ' => 230,
  'ⷻ' => 230,
  'ⷼ' => 230,
  'ⷽ' => 230,
  'ⷾ' => 230,
  'ⷿ' => 230,
  '〪' => 218,
  '〫' => 228,
  '〬' => 232,
  '〭' => 222,
  '〮' => 224,
  '〯' => 224,
  '゙' => 8,
  '゚' => 8,
  '꙯' => 230,
  'ꙴ' => 230,
  'ꙵ' => 230,
  'ꙶ' => 230,
  'ꙷ' => 230,
  'ꙸ' => 230,
  'ꙹ' => 230,
  'ꙺ' => 230,
  'ꙻ' => 230,
  '꙼' => 230,
  '꙽' => 230,
  'ꚞ' => 230,
  'ꚟ' => 230,
  '꛰' => 230,
  '꛱' => 230,
  '꠆' => 9,
  '꠬' => 9,
  '꣄' => 9,
  '꣠' => 230,
  '꣡' => 230,
  '꣢' => 230,
  '꣣' => 230,
  '꣤' => 230,
  '꣥' => 230,
  '꣦' => 230,
  '꣧' => 230,
  '꣨' => 230,
  '꣩' => 230,
  '꣪' => 230,
  '꣫' => 230,
  '꣬' => 230,
  '꣭' => 230,
  '꣮' => 230,
  '꣯' => 230,
  '꣰' => 230,
  '꣱' => 230,
  '꤫' => 220,
  '꤬' => 220,
  '꤭' => 220,
  '꥓' => 9,
  '꦳' => 7,
  '꧀' => 9,
  'ꪰ' => 230,
  'ꪲ' => 230,
  'ꪳ' => 230,
  'ꪴ' => 220,
  'ꪷ' => 230,
  'ꪸ' => 230,
  'ꪾ' => 230,
  '꪿' => 230,
  '꫁' => 230,
  '꫶' => 9,
  '꯭' => 9,
  'ﬞ' => 26,
  '︠' => 230,
  '︡' => 230,
  '︢' => 230,
  '︣' => 230,
  '︤' => 230,
  '︥' => 230,
  '︦' => 230,
  '︧' => 220,
  '︨' => 220,
  '︩' => 220,
  '︪' => 220,
  '︫' => 220,
  '︬' => 220,
  '︭' => 220,
  '︮' => 230,
  '︯' => 230,
  '𐇽' => 220,
  '𐋠' => 220,
  '𐍶' => 230,
  '𐍷' => 230,
  '𐍸' => 230,
  '𐍹' => 230,
  '𐍺' => 230,
  '𐨍' => 220,
  '𐨏' => 230,
  '𐨸' => 230,
  '𐨹' => 1,
  '𐨺' => 220,
  '𐨿' => 9,
  '𐫥' => 230,
  '𐫦' => 220,
  '𐴤' => 230,
  '𐴥' => 230,
  '𐴦' => 230,
  '𐴧' => 230,
  '𐺫' => 230,
  '𐺬' => 230,
  '𐽆' => 220,
  '𐽇' => 220,
  '𐽈' => 230,
  '𐽉' => 230,
  '𐽊' => 230,
  '𐽋' => 220,
  '𐽌' => 230,
  '𐽍' => 220,
  '𐽎' => 220,
  '𐽏' => 220,
  '𐽐' => 220,
  '𑁆' => 9,
  '𑁿' => 9,
  '𑂹' => 9,
  '𑂺' => 7,
  '𑄀' => 230,
  '𑄁' => 230,
  '𑄂' => 230,
  '𑄳' => 9,
  '𑄴' => 9,
  '𑅳' => 7,
  '𑇀' => 9,
  '𑇊' => 7,
  '𑈵' => 9,
  '𑈶' => 7,
  '𑋩' => 7,
  '𑋪' => 9,
  '𑌻' => 7,
  '𑌼' => 7,
  '𑍍' => 9,
  '𑍦' => 230,
  '𑍧' => 230,
  '𑍨' => 230,
  '𑍩' => 230,
  '𑍪' => 230,
  '𑍫' => 230,
  '𑍬' => 230,
  '𑍰' => 230,
  '𑍱' => 230,
  '𑍲' => 230,
  '𑍳' => 230,
  '𑍴' => 230,
  '𑑂' => 9,
  '𑑆' => 7,
  '𑑞' => 230,
  '𑓂' => 9,
  '𑓃' => 7,
  '𑖿' => 9,
  '𑗀' => 7,
  '𑘿' => 9,
  '𑚶' => 9,
  '𑚷' => 7,
  '𑜫' => 9,
  '𑠹' => 9,
  '𑠺' => 7,
  '𑤽' => 9,
  '𑤾' => 9,
  '𑥃' => 7,
  '𑧠' => 9,
  '𑨴' => 9,
  '𑩇' => 9,
  '𑪙' => 9,
  '𑰿' => 9,
  '𑵂' => 7,
  '𑵄' => 9,
  '𑵅' => 9,
  '𑶗' => 9,
  '𖫰' => 1,
  '𖫱' => 1,
  '𖫲' => 1,
  '𖫳' => 1,
  '𖫴' => 1,
  '𖬰' => 230,
  '𖬱' => 230,
  '𖬲' => 230,
  '𖬳' => 230,
  '𖬴' => 230,
  '𖬵' => 230,
  '𖬶' => 230,
  '𖿰' => 6,
  '𖿱' => 6,
  '𛲞' => 1,
  '𝅥' => 216,
  '𝅦' => 216,
  '𝅧' => 1,
  '𝅨' => 1,
  '𝅩' => 1,
  '𝅭' => 226,
  '𝅮' => 216,
  '𝅯' => 216,
  '𝅰' => 216,
  '𝅱' => 216,
  '𝅲' => 216,
  '𝅻' => 220,
  '𝅼' => 220,
  '𝅽' => 220,
  '𝅾' => 220,
  '𝅿' => 220,
  '𝆀' => 220,
  '𝆁' => 220,
  '𝆂' => 220,
  '𝆅' => 230,
  '𝆆' => 230,
  '𝆇' => 230,
  '𝆈' => 230,
  '𝆉' => 230,
  '𝆊' => 220,
  '𝆋' => 220,
  '𝆪' => 230,
  '𝆫' => 230,
  '𝆬' => 230,
  '𝆭' => 230,
  '𝉂' => 230,
  '𝉃' => 230,
  '𝉄' => 230,
  '𞀀' => 230,
  '𞀁' => 230,
  '𞀂' => 230,
  '𞀃' => 230,
  '𞀄' => 230,
  '𞀅' => 230,
  '𞀆' => 230,
  '𞀈' => 230,
  '𞀉' => 230,
  '𞀊' => 230,
  '𞀋' => 230,
  '𞀌' => 230,
  '𞀍' => 230,
  '𞀎' => 230,
  '𞀏' => 230,
  '𞀐' => 230,
  '𞀑' => 230,
  '𞀒' => 230,
  '𞀓' => 230,
  '𞀔' => 230,
  '𞀕' => 230,
  '𞀖' => 230,
  '𞀗' => 230,
  '𞀘' => 230,
  '𞀛' => 230,
  '𞀜' => 230,
  '𞀝' => 230,
  '𞀞' => 230,
  '𞀟' => 230,
  '𞀠' => 230,
  '𞀡' => 230,
  '𞀣' => 230,
  '𞀤' => 230,
  '𞀦' => 230,
  '𞀧' => 230,
  '𞀨' => 230,
  '𞀩' => 230,
  '𞀪' => 230,
  '𞄰' => 230,
  '𞄱' => 230,
  '𞄲' => 230,
  '𞄳' => 230,
  '𞄴' => 230,
  '𞄵' => 230,
  '𞄶' => 230,
  '𞋬' => 230,
  '𞋭' => 230,
  '𞋮' => 230,
  '𞋯' => 230,
  '𞣐' => 220,
  '𞣑' => 220,
  '𞣒' => 220,
  '𞣓' => 220,
  '𞣔' => 220,
  '𞣕' => 220,
  '𞣖' => 220,
  '𞥄' => 230,
  '𞥅' => 230,
  '𞥆' => 230,
  '𞥇' => 230,
  '𞥈' => 230,
  '𞥉' => 230,
  '𞥊' => 7,
);
<?php

return array (
  'À' => 'À',
  'Á' => 'Á',
  'Â' => 'Â',
  'Ã' => 'Ã',
  'Ä' => 'Ä',
  'Å' => 'Å',
  'Ç' => 'Ç',
  'È' => 'È',
  'É' => 'É',
  'Ê' => 'Ê',
  'Ë' => 'Ë',
  'Ì' => 'Ì',
  'Í' => 'Í',
  'Î' => 'Î',
  'Ï' => 'Ï',
  'Ñ' => 'Ñ',
  'Ò' => 'Ò',
  'Ó' => 'Ó',
  'Ô' => 'Ô',
  'Õ' => 'Õ',
  'Ö' => 'Ö',
  'Ù' => 'Ù',
  'Ú' => 'Ú',
  'Û' => 'Û',
  'Ü' => 'Ü',
  'Ý' => 'Ý',
  'à' => 'à',
  'á' => 'á',
  'â' => 'â',
  'ã' => 'ã',
  'ä' => 'ä',
  'å' => 'å',
  'ç' => 'ç',
  'è' => 'è',
  'é' => 'é',
  'ê' => 'ê',
  'ë' => 'ë',
  'ì' => 'ì',
  'í' => 'í',
  'î' => 'î',
  'ï' => 'ï',
  'ñ' => 'ñ',
  'ò' => 'ò',
  'ó' => 'ó',
  'ô' => 'ô',
  'õ' => 'õ',
  'ö' => 'ö',
  'ù' => 'ù',
  'ú' => 'ú',
  'û' => 'û',
  'ü' => 'ü',
  'ý' => 'ý',
  'ÿ' => 'ÿ',
  'Ā' => 'Ā',
  'ā' => 'ā',
  'Ă' => 'Ă',
  'ă' => 'ă',
  'Ą' => 'Ą',
  'ą' => 'ą',
  'Ć' => 'Ć',
  'ć' => 'ć',
  'Ĉ' => 'Ĉ',
  'ĉ' => 'ĉ',
  'Ċ' => 'Ċ',
  'ċ' => 'ċ',
  'Č' => 'Č',
  'č' => 'č',
  'Ď' => 'Ď',
  'ď' => 'ď',
  'Ē' => 'Ē',
  'ē' => 'ē',
  'Ĕ' => 'Ĕ',
  'ĕ' => 'ĕ',
  'Ė' => 'Ė',
  'ė' => 'ė',
  'Ę' => 'Ę',
  'ę' => 'ę',
  'Ě' => 'Ě',
  'ě' => 'ě',
  'Ĝ' => 'Ĝ',
  'ĝ' => 'ĝ',
  'Ğ' => 'Ğ',
  'ğ' => 'ğ',
  'Ġ' => 'Ġ',
  'ġ' => 'ġ',
  'Ģ' => 'Ģ',
  'ģ' => 'ģ',
  'Ĥ' => 'Ĥ',
  'ĥ' => 'ĥ',
  'Ĩ' => 'Ĩ',
  'ĩ' => 'ĩ',
  'Ī' => 'Ī',
  'ī' => 'ī',
  'Ĭ' => 'Ĭ',
  'ĭ' => 'ĭ',
  'Į' => 'Į',
  'į' => 'į',
  'İ' => 'İ',
  'Ĵ' => 'Ĵ',
  'ĵ' => 'ĵ',
  'Ķ' => 'Ķ',
  'ķ' => 'ķ',
  'Ĺ' => 'Ĺ',
  'ĺ' => 'ĺ',
  'Ļ' => 'Ļ',
  'ļ' => 'ļ',
  'Ľ' => 'Ľ',
  'ľ' => 'ľ',
  'Ń' => 'Ń',
  'ń' => 'ń',
  'Ņ' => 'Ņ',
  'ņ' => 'ņ',
  'Ň' => 'Ň',
  'ň' => 'ň',
  'Ō' => 'Ō',
  'ō' => 'ō',
  'Ŏ' => 'Ŏ',
  'ŏ' => 'ŏ',
  'Ő' => 'Ő',
  'ő' => 'ő',
  'Ŕ' => 'Ŕ',
  'ŕ' => 'ŕ',
  'Ŗ' => 'Ŗ',
  'ŗ' => 'ŗ',
  'Ř' => 'Ř',
  'ř' => 'ř',
  'Ś' => 'Ś',
  'ś' => 'ś',
  'Ŝ' => 'Ŝ',
  'ŝ' => 'ŝ',
  'Ş' => 'Ş',
  'ş' => 'ş',
  'Š' => 'Š',
  'š' => 'š',
  'Ţ' => 'Ţ',
  'ţ' => 'ţ',
  'Ť' => 'Ť',
  'ť' => 'ť',
  'Ũ' => 'Ũ',
  'ũ' => 'ũ',
  'Ū' => 'Ū',
  'ū' => 'ū',
  'Ŭ' => 'Ŭ',
  'ŭ' => 'ŭ',
  'Ů' => 'Ů',
  'ů' => 'ů',
  'Ű' => 'Ű',
  'ű' => 'ű',
  'Ų' => 'Ų',
  'ų' => 'ų',
  'Ŵ' => 'Ŵ',
  'ŵ' => 'ŵ',
  'Ŷ' => 'Ŷ',
  'ŷ' => 'ŷ',
  'Ÿ' => 'Ÿ',
  'Ź' => 'Ź',
  'ź' => 'ź',
  'Ż' => 'Ż',
  'ż' => 'ż',
  'Ž' => 'Ž',
  'ž' => 'ž',
  'Ơ' => 'Ơ',
  'ơ' => 'ơ',
  'Ư' => 'Ư',
  'ư' => 'ư',
  'Ǎ' => 'Ǎ',
  'ǎ' => 'ǎ',
  'Ǐ' => 'Ǐ',
  'ǐ' => 'ǐ',
  'Ǒ' => 'Ǒ',
  'ǒ' => 'ǒ',
  'Ǔ' => 'Ǔ',
  'ǔ' => 'ǔ',
  'Ǖ' => 'Ǖ',
  'ǖ' => 'ǖ',
  'Ǘ' => 'Ǘ',
  'ǘ' => 'ǘ',
  'Ǚ' => 'Ǚ',
  'ǚ' => 'ǚ',
  'Ǜ' => 'Ǜ',
  'ǜ' => 'ǜ',
  'Ǟ' => 'Ǟ',
  'ǟ' => 'ǟ',
  'Ǡ' => 'Ǡ',
  'ǡ' => 'ǡ',
  'Ǣ' => 'Ǣ',
  'ǣ' => 'ǣ',
  'Ǧ' => 'Ǧ',
  'ǧ' => 'ǧ',
  'Ǩ' => 'Ǩ',
  'ǩ' => 'ǩ',
  'Ǫ' => 'Ǫ',
  'ǫ' => 'ǫ',
  'Ǭ' => 'Ǭ',
  'ǭ' => 'ǭ',
  'Ǯ' => 'Ǯ',
  'ǯ' => 'ǯ',
  'ǰ' => 'ǰ',
  'Ǵ' => 'Ǵ',
  'ǵ' => 'ǵ',
  'Ǹ' => 'Ǹ',
  'ǹ' => 'ǹ',
  'Ǻ' => 'Ǻ',
  'ǻ' => 'ǻ',
  'Ǽ' => 'Ǽ',
  'ǽ' => 'ǽ',
  'Ǿ' => 'Ǿ',
  'ǿ' => 'ǿ',
  'Ȁ' => 'Ȁ',
  'ȁ' => 'ȁ',
  'Ȃ' => 'Ȃ',
  'ȃ' => 'ȃ',
  'Ȅ' => 'Ȅ',
  'ȅ' => 'ȅ',
  'Ȇ' => 'Ȇ',
  'ȇ' => 'ȇ',
  'Ȉ' => 'Ȉ',
  'ȉ' => 'ȉ',
  'Ȋ' => 'Ȋ',
  'ȋ' => 'ȋ',
  'Ȍ' => 'Ȍ',
  'ȍ' => 'ȍ',
  'Ȏ' => 'Ȏ',
  'ȏ' => 'ȏ',
  'Ȑ' => 'Ȑ',
  'ȑ' => 'ȑ',
  'Ȓ' => 'Ȓ',
  'ȓ' => 'ȓ',
  'Ȕ' => 'Ȕ',
  'ȕ' => 'ȕ',
  'Ȗ' => 'Ȗ',
  'ȗ' => 'ȗ',
  'Ș' => 'Ș',
  'ș' => 'ș',
  'Ț' => 'Ț',
  'ț' => 'ț',
  'Ȟ' => 'Ȟ',
  'ȟ' => 'ȟ',
  'Ȧ' => 'Ȧ',
  'ȧ' => 'ȧ',
  'Ȩ' => 'Ȩ',
  'ȩ' => 'ȩ',
  'Ȫ' => 'Ȫ',
  'ȫ' => 'ȫ',
  'Ȭ' => 'Ȭ',
  'ȭ' => 'ȭ',
  'Ȯ' => 'Ȯ',
  'ȯ' => 'ȯ',
  'Ȱ' => 'Ȱ',
  'ȱ' => 'ȱ',
  'Ȳ' => 'Ȳ',
  'ȳ' => 'ȳ',
  '΅' => '΅',
  'Ά' => 'Ά',
  'Έ' => 'Έ',
  'Ή' => 'Ή',
  'Ί' => 'Ί',
  'Ό' => 'Ό',
  'Ύ' => 'Ύ',
  'Ώ' => 'Ώ',
  'ΐ' => 'ΐ',
  'Ϊ' => 'Ϊ',
  'Ϋ' => 'Ϋ',
  'ά' => 'ά',
  'έ' => 'έ',
  'ή' => 'ή',
  'ί' => 'ί',
  'ΰ' => 'ΰ',
  'ϊ' => 'ϊ',
  'ϋ' => 'ϋ',
  'ό' => 'ό',
  'ύ' => 'ύ',
  'ώ' => 'ώ',
  'ϓ' => 'ϓ',
  'ϔ' => 'ϔ',
  'Ѐ' => 'Ѐ',
  'Ё' => 'Ё',
  'Ѓ' => 'Ѓ',
  'Ї' => 'Ї',
  'Ќ' => 'Ќ',
  'Ѝ' => 'Ѝ',
  'Ў' => 'Ў',
  'Й' => 'Й',
  'й' => 'й',
  'ѐ' => 'ѐ',
  'ё' => 'ё',
  'ѓ' => 'ѓ',
  'ї' => 'ї',
  'ќ' => 'ќ',
  'ѝ' => 'ѝ',
  'ў' => 'ў',
  'Ѷ' => 'Ѷ',
  'ѷ' => 'ѷ',
  'Ӂ' => 'Ӂ',
  'ӂ' => 'ӂ',
  'Ӑ' => 'Ӑ',
  'ӑ' => 'ӑ',
  'Ӓ' => 'Ӓ',
  'ӓ' => 'ӓ',
  'Ӗ' => 'Ӗ',
  'ӗ' => 'ӗ',
  'Ӛ' => 'Ӛ',
  'ӛ' => 'ӛ',
  'Ӝ' => 'Ӝ',
  'ӝ' => 'ӝ',
  'Ӟ' => 'Ӟ',
  'ӟ' => 'ӟ',
  'Ӣ' => 'Ӣ',
  'ӣ' => 'ӣ',
  'Ӥ' => 'Ӥ',
  'ӥ' => 'ӥ',
  'Ӧ' => 'Ӧ',
  'ӧ' => 'ӧ',
  'Ӫ' => 'Ӫ',
  'ӫ' => 'ӫ',
  'Ӭ' => 'Ӭ',
  'ӭ' => 'ӭ',
  'Ӯ' => 'Ӯ',
  'ӯ' => 'ӯ',
  'Ӱ' => 'Ӱ',
  'ӱ' => 'ӱ',
  'Ӳ' => 'Ӳ',
  'ӳ' => 'ӳ',
  'Ӵ' => 'Ӵ',
  'ӵ' => 'ӵ',
  'Ӹ' => 'Ӹ',
  'ӹ' => 'ӹ',
  'آ' => 'آ',
  'أ' => 'أ',
  'ؤ' => 'ؤ',
  'إ' => 'إ',
  'ئ' => 'ئ',
  'ۀ' => 'ۀ',
  'ۂ' => 'ۂ',
  'ۓ' => 'ۓ',
  'ऩ' => 'ऩ',
  'ऱ' => 'ऱ',
  'ऴ' => 'ऴ',
  'ো' => 'ো',
  'ৌ' => 'ৌ',
  'ୈ' => 'ୈ',
  'ୋ' => 'ୋ',
  'ୌ' => 'ୌ',
  'ஔ' => 'ஔ',
  'ொ' => 'ொ',
  'ோ' => 'ோ',
  'ௌ' => 'ௌ',
  'ై' => 'ై',
  'ೀ' => 'ೀ',
  'ೇ' => 'ೇ',
  'ೈ' => 'ೈ',
  'ೊ' => 'ೊ',
  'ೋ' => 'ೋ',
  'ൊ' => 'ൊ',
  'ോ' => 'ോ',
  'ൌ' => 'ൌ',
  'ේ' => 'ේ',
  'ො' => 'ො',
  'ෝ' => 'ෝ',
  'ෞ' => 'ෞ',
  'ဦ' => 'ဦ',
  'ᬆ' => 'ᬆ',
  'ᬈ' => 'ᬈ',
  'ᬊ' => 'ᬊ',
  'ᬌ' => 'ᬌ',
  'ᬎ' => 'ᬎ',
  'ᬒ' => 'ᬒ',
  'ᬻ' => 'ᬻ',
  'ᬽ' => 'ᬽ',
  'ᭀ' => 'ᭀ',
  'ᭁ' => 'ᭁ',
  'ᭃ' => 'ᭃ',
  'Ḁ' => 'Ḁ',
  'ḁ' => 'ḁ',
  'Ḃ' => 'Ḃ',
  'ḃ' => 'ḃ',
  'Ḅ' => 'Ḅ',
  'ḅ' => 'ḅ',
  'Ḇ' => 'Ḇ',
  'ḇ' => 'ḇ',
  'Ḉ' => 'Ḉ',
  'ḉ' => 'ḉ',
  'Ḋ' => 'Ḋ',
  'ḋ' => 'ḋ',
  'Ḍ' => 'Ḍ',
  'ḍ' => 'ḍ',
  'Ḏ' => 'Ḏ',
  'ḏ' => 'ḏ',
  'Ḑ' => 'Ḑ',
  'ḑ' => 'ḑ',
  'Ḓ' => 'Ḓ',
  'ḓ' => 'ḓ',
  'Ḕ' => 'Ḕ',
  'ḕ' => 'ḕ',
  'Ḗ' => 'Ḗ',
  'ḗ' => 'ḗ',
  'Ḙ' => 'Ḙ',
  'ḙ' => 'ḙ',
  'Ḛ' => 'Ḛ',
  'ḛ' => 'ḛ',
  'Ḝ' => 'Ḝ',
  'ḝ' => 'ḝ',
  'Ḟ' => 'Ḟ',
  'ḟ' => 'ḟ',
  'Ḡ' => 'Ḡ',
  'ḡ' => 'ḡ',
  'Ḣ' => 'Ḣ',
  'ḣ' => 'ḣ',
  'Ḥ' => 'Ḥ',
  'ḥ' => 'ḥ',
  'Ḧ' => 'Ḧ',
  'ḧ' => 'ḧ',
  'Ḩ' => 'Ḩ',
  'ḩ' => 'ḩ',
  'Ḫ' => 'Ḫ',
  'ḫ' => 'ḫ',
  'Ḭ' => 'Ḭ',
  'ḭ' => 'ḭ',
  'Ḯ' => 'Ḯ',
  'ḯ' => 'ḯ',
  'Ḱ' => 'Ḱ',
  'ḱ' => 'ḱ',
  'Ḳ' => 'Ḳ',
  'ḳ' => 'ḳ',
  'Ḵ' => 'Ḵ',
  'ḵ' => 'ḵ',
  'Ḷ' => 'Ḷ',
  'ḷ' => 'ḷ',
  'Ḹ' => 'Ḹ',
  'ḹ' => 'ḹ',
  'Ḻ' => 'Ḻ',
  'ḻ' => 'ḻ',
  'Ḽ' => 'Ḽ',
  'ḽ' => 'ḽ',
  'Ḿ' => 'Ḿ',
  'ḿ' => 'ḿ',
  'Ṁ' => 'Ṁ',
  'ṁ' => 'ṁ',
  'Ṃ' => 'Ṃ',
  'ṃ' => 'ṃ',
  'Ṅ' => 'Ṅ',
  'ṅ' => 'ṅ',
  'Ṇ' => 'Ṇ',
  'ṇ' => 'ṇ',
  'Ṉ' => 'Ṉ',
  'ṉ' => 'ṉ',
  'Ṋ' => 'Ṋ',
  'ṋ' => 'ṋ',
  'Ṍ' => 'Ṍ',
  'ṍ' => 'ṍ',
  'Ṏ' => 'Ṏ',
  'ṏ' => 'ṏ',
  'Ṑ' => 'Ṑ',
  'ṑ' => 'ṑ',
  'Ṓ' => 'Ṓ',
  'ṓ' => 'ṓ',
  'Ṕ' => 'Ṕ',
  'ṕ' => 'ṕ',
  'Ṗ' => 'Ṗ',
  'ṗ' => 'ṗ',
  'Ṙ' => 'Ṙ',
  'ṙ' => 'ṙ',
  'Ṛ' => 'Ṛ',
  'ṛ' => 'ṛ',
  'Ṝ' => 'Ṝ',
  'ṝ' => 'ṝ',
  'Ṟ' => 'Ṟ',
  'ṟ' => 'ṟ',
  'Ṡ' => 'Ṡ',
  'ṡ' => 'ṡ',
  'Ṣ' => 'Ṣ',
  'ṣ' => 'ṣ',
  'Ṥ' => 'Ṥ',
  'ṥ' => 'ṥ',
  'Ṧ' => 'Ṧ',
  'ṧ' => 'ṧ',
  'Ṩ' => 'Ṩ',
  'ṩ' => 'ṩ',
  'Ṫ' => 'Ṫ',
  'ṫ' => 'ṫ',
  'Ṭ' => 'Ṭ',
  'ṭ' => 'ṭ',
  'Ṯ' => 'Ṯ',
  'ṯ' => 'ṯ',
  'Ṱ' => 'Ṱ',
  'ṱ' => 'ṱ',
  'Ṳ' => 'Ṳ',
  'ṳ' => 'ṳ',
  'Ṵ' => 'Ṵ',
  'ṵ' => 'ṵ',
  'Ṷ' => 'Ṷ',
  'ṷ' => 'ṷ',
  'Ṹ' => 'Ṹ',
  'ṹ' => 'ṹ',
  'Ṻ' => 'Ṻ',
  'ṻ' => 'ṻ',
  'Ṽ' => 'Ṽ',
  'ṽ' => 'ṽ',
  'Ṿ' => 'Ṿ',
  'ṿ' => 'ṿ',
  'Ẁ' => 'Ẁ',
  'ẁ' => 'ẁ',
  'Ẃ' => 'Ẃ',
  'ẃ' => 'ẃ',
  'Ẅ' => 'Ẅ',
  'ẅ' => 'ẅ',
  'Ẇ' => 'Ẇ',
  'ẇ' => 'ẇ',
  'Ẉ' => 'Ẉ',
  'ẉ' => 'ẉ',
  'Ẋ' => 'Ẋ',
  'ẋ' => 'ẋ',
  'Ẍ' => 'Ẍ',
  'ẍ' => 'ẍ',
  'Ẏ' => 'Ẏ',
  'ẏ' => 'ẏ',
  'Ẑ' => 'Ẑ',
  'ẑ' => 'ẑ',
  'Ẓ' => 'Ẓ',
  'ẓ' => 'ẓ',
  'Ẕ' => 'Ẕ',
  'ẕ' => 'ẕ',
  'ẖ' => 'ẖ',
  'ẗ' => 'ẗ',
  'ẘ' => 'ẘ',
  'ẙ' => 'ẙ',
  'ẛ' => 'ẛ',
  'Ạ' => 'Ạ',
  'ạ' => 'ạ',
  'Ả' => 'Ả',
  'ả' => 'ả',
  'Ấ' => 'Ấ',
  'ấ' => 'ấ',
  'Ầ' => 'Ầ',
  'ầ' => 'ầ',
  'Ẩ' => 'Ẩ',
  'ẩ' => 'ẩ',
  'Ẫ' => 'Ẫ',
  'ẫ' => 'ẫ',
  'Ậ' => 'Ậ',
  'ậ' => 'ậ',
  'Ắ' => 'Ắ',
  'ắ' => 'ắ',
  'Ằ' => 'Ằ',
  'ằ' => 'ằ',
  'Ẳ' => 'Ẳ',
  'ẳ' => 'ẳ',
  'Ẵ' => 'Ẵ',
  'ẵ' => 'ẵ',
  'Ặ' => 'Ặ',
  'ặ' => 'ặ',
  'Ẹ' => 'Ẹ',
  'ẹ' => 'ẹ',
  'Ẻ' => 'Ẻ',
  'ẻ' => 'ẻ',
  'Ẽ' => 'Ẽ',
  'ẽ' => 'ẽ',
  'Ế' => 'Ế',
  'ế' => 'ế',
  'Ề' => 'Ề',
  'ề' => 'ề',
  'Ể' => 'Ể',
  'ể' => 'ể',
  'Ễ' => 'Ễ',
  'ễ' => 'ễ',
  'Ệ' => 'Ệ',
  'ệ' => 'ệ',
  'Ỉ' => 'Ỉ',
  'ỉ' => 'ỉ',
  'Ị' => 'Ị',
  'ị' => 'ị',
  'Ọ' => 'Ọ',
  'ọ' => 'ọ',
  'Ỏ' => 'Ỏ',
  'ỏ' => 'ỏ',
  'Ố' => 'Ố',
  'ố' => 'ố',
  'Ồ' => 'Ồ',
  'ồ' => 'ồ',
  'Ổ' => 'Ổ',
  'ổ' => 'ổ',
  'Ỗ' => 'Ỗ',
  'ỗ' => 'ỗ',
  'Ộ' => 'Ộ',
  'ộ' => 'ộ',
  'Ớ' => 'Ớ',
  'ớ' => 'ớ',
  'Ờ' => 'Ờ',
  'ờ' => 'ờ',
  'Ở' => 'Ở',
  'ở' => 'ở',
  'Ỡ' => 'Ỡ',
  'ỡ' => 'ỡ',
  'Ợ' => 'Ợ',
  'ợ' => 'ợ',
  'Ụ' => 'Ụ',
  'ụ' => 'ụ',
  'Ủ' => 'Ủ',
  'ủ' => 'ủ',
  'Ứ' => 'Ứ',
  'ứ' => 'ứ',
  'Ừ' => 'Ừ',
  'ừ' => 'ừ',
  'Ử' => 'Ử',
  'ử' => 'ử',
  'Ữ' => 'Ữ',
  'ữ' => 'ữ',
  'Ự' => 'Ự',
  'ự' => 'ự',
  'Ỳ' => 'Ỳ',
  'ỳ' => 'ỳ',
  'Ỵ' => 'Ỵ',
  'ỵ' => 'ỵ',
  'Ỷ' => 'Ỷ',
  'ỷ' => 'ỷ',
  'Ỹ' => 'Ỹ',
  'ỹ' => 'ỹ',
  'ἀ' => 'ἀ',
  'ἁ' => 'ἁ',
  'ἂ' => 'ἂ',
  'ἃ' => 'ἃ',
  'ἄ' => 'ἄ',
  'ἅ' => 'ἅ',
  'ἆ' => 'ἆ',
  'ἇ' => 'ἇ',
  'Ἀ' => 'Ἀ',
  'Ἁ' => 'Ἁ',
  'Ἂ' => 'Ἂ',
  'Ἃ' => 'Ἃ',
  'Ἄ' => 'Ἄ',
  'Ἅ' => 'Ἅ',
  'Ἆ' => 'Ἆ',
  'Ἇ' => 'Ἇ',
  'ἐ' => 'ἐ',
  'ἑ' => 'ἑ',
  'ἒ' => 'ἒ',
  'ἓ' => 'ἓ',
  'ἔ' => 'ἔ',
  'ἕ' => 'ἕ',
  'Ἐ' => 'Ἐ',
  'Ἑ' => 'Ἑ',
  'Ἒ' => 'Ἒ',
  'Ἓ' => 'Ἓ',
  'Ἔ' => 'Ἔ',
  'Ἕ' => 'Ἕ',
  'ἠ' => 'ἠ',
  'ἡ' => 'ἡ',
  'ἢ' => 'ἢ',
  'ἣ' => 'ἣ',
  'ἤ' => 'ἤ',
  'ἥ' => 'ἥ',
  'ἦ' => 'ἦ',
  'ἧ' => 'ἧ',
  'Ἠ' => 'Ἠ',
  'Ἡ' => 'Ἡ',
  'Ἢ' => 'Ἢ',
  'Ἣ' => 'Ἣ',
  'Ἤ' => 'Ἤ',
  'Ἥ' => 'Ἥ',
  'Ἦ' => 'Ἦ',
  'Ἧ' => 'Ἧ',
  'ἰ' => 'ἰ',
  'ἱ' => 'ἱ',
  'ἲ' => 'ἲ',
  'ἳ' => 'ἳ',
  'ἴ' => 'ἴ',
  'ἵ' => 'ἵ',
  'ἶ' => 'ἶ',
  'ἷ' => 'ἷ',
  'Ἰ' => 'Ἰ',
  'Ἱ' => 'Ἱ',
  'Ἲ' => 'Ἲ',
  'Ἳ' => 'Ἳ',
  'Ἴ' => 'Ἴ',
  'Ἵ' => 'Ἵ',
  'Ἶ' => 'Ἶ',
  'Ἷ' => 'Ἷ',
  'ὀ' => 'ὀ',
  'ὁ' => 'ὁ',
  'ὂ' => 'ὂ',
  'ὃ' => 'ὃ',
  'ὄ' => 'ὄ',
  'ὅ' => 'ὅ',
  'Ὀ' => 'Ὀ',
  'Ὁ' => 'Ὁ',
  'Ὂ' => 'Ὂ',
  'Ὃ' => 'Ὃ',
  'Ὄ' => 'Ὄ',
  'Ὅ' => 'Ὅ',
  'ὐ' => 'ὐ',
  'ὑ' => 'ὑ',
  'ὒ' => 'ὒ',
  'ὓ' => 'ὓ',
  'ὔ' => 'ὔ',
  'ὕ' => 'ὕ',
  'ὖ' => 'ὖ',
  'ὗ' => 'ὗ',
  'Ὑ' => 'Ὑ',
  'Ὓ' => 'Ὓ',
  'Ὕ' => 'Ὕ',
  'Ὗ' => 'Ὗ',
  'ὠ' => 'ὠ',
  'ὡ' => 'ὡ',
  'ὢ' => 'ὢ',
  'ὣ' => 'ὣ',
  'ὤ' => 'ὤ',
  'ὥ' => 'ὥ',
  'ὦ' => 'ὦ',
  'ὧ' => 'ὧ',
  'Ὠ' => 'Ὠ',
  'Ὡ' => 'Ὡ',
  'Ὢ' => 'Ὢ',
  'Ὣ' => 'Ὣ',
  'Ὤ' => 'Ὤ',
  'Ὥ' => 'Ὥ',
  'Ὦ' => 'Ὦ',
  'Ὧ' => 'Ὧ',
  'ὰ' => 'ὰ',
  'ὲ' => 'ὲ',
  'ὴ' => 'ὴ',
  'ὶ' => 'ὶ',
  'ὸ' => 'ὸ',
  'ὺ' => 'ὺ',
  'ὼ' => 'ὼ',
  'ᾀ' => 'ᾀ',
  'ᾁ' => 'ᾁ',
  'ᾂ' => 'ᾂ',
  'ᾃ' => 'ᾃ',
  'ᾄ' => 'ᾄ',
  'ᾅ' => 'ᾅ',
  'ᾆ' => 'ᾆ',
  'ᾇ' => 'ᾇ',
  'ᾈ' => 'ᾈ',
  'ᾉ' => 'ᾉ',
  'ᾊ' => 'ᾊ',
  'ᾋ' => 'ᾋ',
  'ᾌ' => 'ᾌ',
  'ᾍ' => 'ᾍ',
  'ᾎ' => 'ᾎ',
  'ᾏ' => 'ᾏ',
  'ᾐ' => 'ᾐ',
  'ᾑ' => 'ᾑ',
  'ᾒ' => 'ᾒ',
  'ᾓ' => 'ᾓ',
  'ᾔ' => 'ᾔ',
  'ᾕ' => 'ᾕ',
  'ᾖ' => 'ᾖ',
  'ᾗ' => 'ᾗ',
  'ᾘ' => 'ᾘ',
  'ᾙ' => 'ᾙ',
  'ᾚ' => 'ᾚ',
  'ᾛ' => 'ᾛ',
  'ᾜ' => 'ᾜ',
  'ᾝ' => 'ᾝ',
  'ᾞ' => 'ᾞ',
  'ᾟ' => 'ᾟ',
  'ᾠ' => 'ᾠ',
  'ᾡ' => 'ᾡ',
  'ᾢ' => 'ᾢ',
  'ᾣ' => 'ᾣ',
  'ᾤ' => 'ᾤ',
  'ᾥ' => 'ᾥ',
  'ᾦ' => 'ᾦ',
  'ᾧ' => 'ᾧ',
  'ᾨ' => 'ᾨ',
  'ᾩ' => 'ᾩ',
  'ᾪ' => 'ᾪ',
  'ᾫ' => 'ᾫ',
  'ᾬ' => 'ᾬ',
  'ᾭ' => 'ᾭ',
  'ᾮ' => 'ᾮ',
  'ᾯ' => 'ᾯ',
  'ᾰ' => 'ᾰ',
  'ᾱ' => 'ᾱ',
  'ᾲ' => 'ᾲ',
  'ᾳ' => 'ᾳ',
  'ᾴ' => 'ᾴ',
  'ᾶ' => 'ᾶ',
  'ᾷ' => 'ᾷ',
  'Ᾰ' => 'Ᾰ',
  'Ᾱ' => 'Ᾱ',
  'Ὰ' => 'Ὰ',
  'ᾼ' => 'ᾼ',
  '῁' => '῁',
  'ῂ' => 'ῂ',
  'ῃ' => 'ῃ',
  'ῄ' => 'ῄ',
  'ῆ' => 'ῆ',
  'ῇ' => 'ῇ',
  'Ὲ' => 'Ὲ',
  'Ὴ' => 'Ὴ',
  'ῌ' => 'ῌ',
  '῍' => '῍',
  '῎' => '῎',
  '῏' => '῏',
  'ῐ' => 'ῐ',
  'ῑ' => 'ῑ',
  'ῒ' => 'ῒ',
  'ῖ' => 'ῖ',
  'ῗ' => 'ῗ',
  'Ῐ' => 'Ῐ',
  'Ῑ' => 'Ῑ',
  'Ὶ' => 'Ὶ',
  '῝' => '῝',
  '῞' => '῞',
  '῟' => '῟',
  'ῠ' => 'ῠ',
  'ῡ' => 'ῡ',
  'ῢ' => 'ῢ',
  'ῤ' => 'ῤ',
  'ῥ' => 'ῥ',
  'ῦ' => 'ῦ',
  'ῧ' => 'ῧ',
  'Ῠ' => 'Ῠ',
  'Ῡ' => 'Ῡ',
  'Ὺ' => 'Ὺ',
  'Ῥ' => 'Ῥ',
  '῭' => '῭',
  'ῲ' => 'ῲ',
  'ῳ' => 'ῳ',
  'ῴ' => 'ῴ',
  'ῶ' => 'ῶ',
  'ῷ' => 'ῷ',
  'Ὸ' => 'Ὸ',
  'Ὼ' => 'Ὼ',
  'ῼ' => 'ῼ',
  '↚' => '↚',
  '↛' => '↛',
  '↮' => '↮',
  '⇍' => '⇍',
  '⇎' => '⇎',
  '⇏' => '⇏',
  '∄' => '∄',
  '∉' => '∉',
  '∌' => '∌',
  '∤' => '∤',
  '∦' => '∦',
  '≁' => '≁',
  '≄' => '≄',
  '≇' => '≇',
  '≉' => '≉',
  '≠' => '≠',
  '≢' => '≢',
  '≭' => '≭',
  '≮' => '≮',
  '≯' => '≯',
  '≰' => '≰',
  '≱' => '≱',
  '≴' => '≴',
  '≵' => '≵',
  '≸' => '≸',
  '≹' => '≹',
  '⊀' => '⊀',
  '⊁' => '⊁',
  '⊄' => '⊄',
  '⊅' => '⊅',
  '⊈' => '⊈',
  '⊉' => '⊉',
  '⊬' => '⊬',
  '⊭' => '⊭',
  '⊮' => '⊮',
  '⊯' => '⊯',
  '⋠' => '⋠',
  '⋡' => '⋡',
  '⋢' => '⋢',
  '⋣' => '⋣',
  '⋪' => '⋪',
  '⋫' => '⋫',
  '⋬' => '⋬',
  '⋭' => '⋭',
  'が' => 'が',
  'ぎ' => 'ぎ',
  'ぐ' => 'ぐ',
  'げ' => 'げ',
  'ご' => 'ご',
  'ざ' => 'ざ',
  'じ' => 'じ',
  'ず' => 'ず',
  'ぜ' => 'ぜ',
  'ぞ' => 'ぞ',
  'だ' => 'だ',
  'ぢ' => 'ぢ',
  'づ' => 'づ',
  'で' => 'で',
  'ど' => 'ど',
  'ば' => 'ば',
  'ぱ' => 'ぱ',
  'び' => 'び',
  'ぴ' => 'ぴ',
  'ぶ' => 'ぶ',
  'ぷ' => 'ぷ',
  'べ' => 'べ',
  'ぺ' => 'ぺ',
  'ぼ' => 'ぼ',
  'ぽ' => 'ぽ',
  'ゔ' => 'ゔ',
  'ゞ' => 'ゞ',
  'ガ' => 'ガ',
  'ギ' => 'ギ',
  'グ' => 'グ',
  'ゲ' => 'ゲ',
  'ゴ' => 'ゴ',
  'ザ' => 'ザ',
  'ジ' => 'ジ',
  'ズ' => 'ズ',
  'ゼ' => 'ゼ',
  'ゾ' => 'ゾ',
  'ダ' => 'ダ',
  'ヂ' => 'ヂ',
  'ヅ' => 'ヅ',
  'デ' => 'デ',
  'ド' => 'ド',
  'バ' => 'バ',
  'パ' => 'パ',
  'ビ' => 'ビ',
  'ピ' => 'ピ',
  'ブ' => 'ブ',
  'プ' => 'プ',
  'ベ' => 'ベ',
  'ペ' => 'ペ',
  'ボ' => 'ボ',
  'ポ' => 'ポ',
  'ヴ' => 'ヴ',
  'ヷ' => 'ヷ',
  'ヸ' => 'ヸ',
  'ヹ' => 'ヹ',
  'ヺ' => 'ヺ',
  'ヾ' => 'ヾ',
  '𑂚' => '𑂚',
  '𑂜' => '𑂜',
  '𑂫' => '𑂫',
  '𑄮' => '𑄮',
  '𑄯' => '𑄯',
  '𑍋' => '𑍋',
  '𑍌' => '𑍌',
  '𑒻' => '𑒻',
  '𑒼' => '𑒼',
  '𑒾' => '𑒾',
  '𑖺' => '𑖺',
  '𑖻' => '𑖻',
  '𑤸' => '𑤸',
);
<?php

return array (
  'À' => 'À',
  'Á' => 'Á',
  'Â' => 'Â',
  'Ã' => 'Ã',
  'Ä' => 'Ä',
  'Å' => 'Å',
  'Ç' => 'Ç',
  'È' => 'È',
  'É' => 'É',
  'Ê' => 'Ê',
  'Ë' => 'Ë',
  'Ì' => 'Ì',
  'Í' => 'Í',
  'Î' => 'Î',
  'Ï' => 'Ï',
  'Ñ' => 'Ñ',
  'Ò' => 'Ò',
  'Ó' => 'Ó',
  'Ô' => 'Ô',
  'Õ' => 'Õ',
  'Ö' => 'Ö',
  'Ù' => 'Ù',
  'Ú' => 'Ú',
  'Û' => 'Û',
  'Ü' => 'Ü',
  'Ý' => 'Ý',
  'à' => 'à',
  'á' => 'á',
  'â' => 'â',
  'ã' => 'ã',
  'ä' => 'ä',
  'å' => 'å',
  'ç' => 'ç',
  'è' => 'è',
  'é' => 'é',
  'ê' => 'ê',
  'ë' => 'ë',
  'ì' => 'ì',
  'í' => 'í',
  'î' => 'î',
  'ï' => 'ï',
  'ñ' => 'ñ',
  'ò' => 'ò',
  'ó' => 'ó',
  'ô' => 'ô',
  'õ' => 'õ',
  'ö' => 'ö',
  'ù' => 'ù',
  'ú' => 'ú',
  'û' => 'û',
  'ü' => 'ü',
  'ý' => 'ý',
  'ÿ' => 'ÿ',
  'Ā' => 'Ā',
  'ā' => 'ā',
  'Ă' => 'Ă',
  'ă' => 'ă',
  'Ą' => 'Ą',
  'ą' => 'ą',
  'Ć' => 'Ć',
  'ć' => 'ć',
  'Ĉ' => 'Ĉ',
  'ĉ' => 'ĉ',
  'Ċ' => 'Ċ',
  'ċ' => 'ċ',
  'Č' => 'Č',
  'č' => 'č',
  'Ď' => 'Ď',
  'ď' => 'ď',
  'Ē' => 'Ē',
  'ē' => 'ē',
  'Ĕ' => 'Ĕ',
  'ĕ' => 'ĕ',
  'Ė' => 'Ė',
  'ė' => 'ė',
  'Ę' => 'Ę',
  'ę' => 'ę',
  'Ě' => 'Ě',
  'ě' => 'ě',
  'Ĝ' => 'Ĝ',
  'ĝ' => 'ĝ',
  'Ğ' => 'Ğ',
  'ğ' => 'ğ',
  'Ġ' => 'Ġ',
  'ġ' => 'ġ',
  'Ģ' => 'Ģ',
  'ģ' => 'ģ',
  'Ĥ' => 'Ĥ',
  'ĥ' => 'ĥ',
  'Ĩ' => 'Ĩ',
  'ĩ' => 'ĩ',
  'Ī' => 'Ī',
  'ī' => 'ī',
  'Ĭ' => 'Ĭ',
  'ĭ' => 'ĭ',
  'Į' => 'Į',
  'į' => 'į',
  'İ' => 'İ',
  'Ĵ' => 'Ĵ',
  'ĵ' => 'ĵ',
  'Ķ' => 'Ķ',
  'ķ' => 'ķ',
  'Ĺ' => 'Ĺ',
  'ĺ' => 'ĺ',
  'Ļ' => 'Ļ',
  'ļ' => 'ļ',
  'Ľ' => 'Ľ',
  'ľ' => 'ľ',
  'Ń' => 'Ń',
  'ń' => 'ń',
  'Ņ' => 'Ņ',
  'ņ' => 'ņ',
  'Ň' => 'Ň',
  'ň' => 'ň',
  'Ō' => 'Ō',
  'ō' => 'ō',
  'Ŏ' => 'Ŏ',
  'ŏ' => 'ŏ',
  'Ő' => 'Ő',
  'ő' => 'ő',
  'Ŕ' => 'Ŕ',
  'ŕ' => 'ŕ',
  'Ŗ' => 'Ŗ',
  'ŗ' => 'ŗ',
  'Ř' => 'Ř',
  'ř' => 'ř',
  'Ś' => 'Ś',
  'ś' => 'ś',
  'Ŝ' => 'Ŝ',
  'ŝ' => 'ŝ',
  'Ş' => 'Ş',
  'ş' => 'ş',
  'Š' => 'Š',
  'š' => 'š',
  'Ţ' => 'Ţ',
  'ţ' => 'ţ',
  'Ť' => 'Ť',
  'ť' => 'ť',
  'Ũ' => 'Ũ',
  'ũ' => 'ũ',
  'Ū' => 'Ū',
  'ū' => 'ū',
  'Ŭ' => 'Ŭ',
  'ŭ' => 'ŭ',
  'Ů' => 'Ů',
  'ů' => 'ů',
  'Ű' => 'Ű',
  'ű' => 'ű',
  'Ų' => 'Ų',
  'ų' => 'ų',
  'Ŵ' => 'Ŵ',
  'ŵ' => 'ŵ',
  'Ŷ' => 'Ŷ',
  'ŷ' => 'ŷ',
  'Ÿ' => 'Ÿ',
  'Ź' => 'Ź',
  'ź' => 'ź',
  'Ż' => 'Ż',
  'ż' => 'ż',
  'Ž' => 'Ž',
  'ž' => 'ž',
  'Ơ' => 'Ơ',
  'ơ' => 'ơ',
  'Ư' => 'Ư',
  'ư' => 'ư',
  'Ǎ' => 'Ǎ',
  'ǎ' => 'ǎ',
  'Ǐ' => 'Ǐ',
  'ǐ' => 'ǐ',
  'Ǒ' => 'Ǒ',
  'ǒ' => 'ǒ',
  'Ǔ' => 'Ǔ',
  'ǔ' => 'ǔ',
  'Ǖ' => 'Ǖ',
  'ǖ' => 'ǖ',
  'Ǘ' => 'Ǘ',
  'ǘ' => 'ǘ',
  'Ǚ' => 'Ǚ',
  'ǚ' => 'ǚ',
  'Ǜ' => 'Ǜ',
  'ǜ' => 'ǜ',
  'Ǟ' => 'Ǟ',
  'ǟ' => 'ǟ',
  'Ǡ' => 'Ǡ',
  'ǡ' => 'ǡ',
  'Ǣ' => 'Ǣ',
  'ǣ' => 'ǣ',
  'Ǧ' => 'Ǧ',
  'ǧ' => 'ǧ',
  'Ǩ' => 'Ǩ',
  'ǩ' => 'ǩ',
  'Ǫ' => 'Ǫ',
  'ǫ' => 'ǫ',
  'Ǭ' => 'Ǭ',
  'ǭ' => 'ǭ',
  'Ǯ' => 'Ǯ',
  'ǯ' => 'ǯ',
  'ǰ' => 'ǰ',
  'Ǵ' => 'Ǵ',
  'ǵ' => 'ǵ',
  'Ǹ' => 'Ǹ',
  'ǹ' => 'ǹ',
  'Ǻ' => 'Ǻ',
  'ǻ' => 'ǻ',
  'Ǽ' => 'Ǽ',
  'ǽ' => 'ǽ',
  'Ǿ' => 'Ǿ',
  'ǿ' => 'ǿ',
  'Ȁ' => 'Ȁ',
  'ȁ' => 'ȁ',
  'Ȃ' => 'Ȃ',
  'ȃ' => 'ȃ',
  'Ȅ' => 'Ȅ',
  'ȅ' => 'ȅ',
  'Ȇ' => 'Ȇ',
  'ȇ' => 'ȇ',
  'Ȉ' => 'Ȉ',
  'ȉ' => 'ȉ',
  'Ȋ' => 'Ȋ',
  'ȋ' => 'ȋ',
  'Ȍ' => 'Ȍ',
  'ȍ' => 'ȍ',
  'Ȏ' => 'Ȏ',
  'ȏ' => 'ȏ',
  'Ȑ' => 'Ȑ',
  'ȑ' => 'ȑ',
  'Ȓ' => 'Ȓ',
  'ȓ' => 'ȓ',
  'Ȕ' => 'Ȕ',
  'ȕ' => 'ȕ',
  'Ȗ' => 'Ȗ',
  'ȗ' => 'ȗ',
  'Ș' => 'Ș',
  'ș' => 'ș',
  'Ț' => 'Ț',
  'ț' => 'ț',
  'Ȟ' => 'Ȟ',
  'ȟ' => 'ȟ',
  'Ȧ' => 'Ȧ',
  'ȧ' => 'ȧ',
  'Ȩ' => 'Ȩ',
  'ȩ' => 'ȩ',
  'Ȫ' => 'Ȫ',
  'ȫ' => 'ȫ',
  'Ȭ' => 'Ȭ',
  'ȭ' => 'ȭ',
  'Ȯ' => 'Ȯ',
  'ȯ' => 'ȯ',
  'Ȱ' => 'Ȱ',
  'ȱ' => 'ȱ',
  'Ȳ' => 'Ȳ',
  'ȳ' => 'ȳ',
  '̀' => '̀',
  '́' => '́',
  '̓' => '̓',
  '̈́' => '̈́',
  'ʹ' => 'ʹ',
  ';' => ';',
  '΅' => '΅',
  'Ά' => 'Ά',
  '·' => '·',
  'Έ' => 'Έ',
  'Ή' => 'Ή',
  'Ί' => 'Ί',
  'Ό' => 'Ό',
  'Ύ' => 'Ύ',
  'Ώ' => 'Ώ',
  'ΐ' => 'ΐ',
  'Ϊ' => 'Ϊ',
  'Ϋ' => 'Ϋ',
  'ά' => 'ά',
  'έ' => 'έ',
  'ή' => 'ή',
  'ί' => 'ί',
  'ΰ' => 'ΰ',
  'ϊ' => 'ϊ',
  'ϋ' => 'ϋ',
  'ό' => 'ό',
  'ύ' => 'ύ',
  'ώ' => 'ώ',
  'ϓ' => 'ϓ',
  'ϔ' => 'ϔ',
  'Ѐ' => 'Ѐ',
  'Ё' => 'Ё',
  'Ѓ' => 'Ѓ',
  'Ї' => 'Ї',
  'Ќ' => 'Ќ',
  'Ѝ' => 'Ѝ',
  'Ў' => 'Ў',
  'Й' => 'Й',
  'й' => 'й',
  'ѐ' => 'ѐ',
  'ё' => 'ё',
  'ѓ' => 'ѓ',
  'ї' => 'ї',
  'ќ' => 'ќ',
  'ѝ' => 'ѝ',
  'ў' => 'ў',
  'Ѷ' => 'Ѷ',
  'ѷ' => 'ѷ',
  'Ӂ' => 'Ӂ',
  'ӂ' => 'ӂ',
  'Ӑ' => 'Ӑ',
  'ӑ' => 'ӑ',
  'Ӓ' => 'Ӓ',
  'ӓ' => 'ӓ',
  'Ӗ' => 'Ӗ',
  'ӗ' => 'ӗ',
  'Ӛ' => 'Ӛ',
  'ӛ' => 'ӛ',
  'Ӝ' => 'Ӝ',
  'ӝ' => 'ӝ',
  'Ӟ' => 'Ӟ',
  'ӟ' => 'ӟ',
  'Ӣ' => 'Ӣ',
  'ӣ' => 'ӣ',
  'Ӥ' => 'Ӥ',
  'ӥ' => 'ӥ',
  'Ӧ' => 'Ӧ',
  'ӧ' => 'ӧ',
  'Ӫ' => 'Ӫ',
  'ӫ' => 'ӫ',
  'Ӭ' => 'Ӭ',
  'ӭ' => 'ӭ',
  'Ӯ' => 'Ӯ',
  'ӯ' => 'ӯ',
  'Ӱ' => 'Ӱ',
  'ӱ' => 'ӱ',
  'Ӳ' => 'Ӳ',
  'ӳ' => 'ӳ',
  'Ӵ' => 'Ӵ',
  'ӵ' => 'ӵ',
  'Ӹ' => 'Ӹ',
  'ӹ' => 'ӹ',
  'آ' => 'آ',
  'أ' => 'أ',
  'ؤ' => 'ؤ',
  'إ' => 'إ',
  'ئ' => 'ئ',
  'ۀ' => 'ۀ',
  'ۂ' => 'ۂ',
  'ۓ' => 'ۓ',
  'ऩ' => 'ऩ',
  'ऱ' => 'ऱ',
  'ऴ' => 'ऴ',
  'क़' => 'क़',
  'ख़' => 'ख़',
  'ग़' => 'ग़',
  'ज़' => 'ज़',
  'ड़' => 'ड़',
  'ढ़' => 'ढ़',
  'फ़' => 'फ़',
  'य़' => 'य़',
  'ো' => 'ো',
  'ৌ' => 'ৌ',
  'ড়' => 'ড়',
  'ঢ়' => 'ঢ়',
  'য়' => 'য়',
  'ਲ਼' => 'ਲ਼',
  'ਸ਼' => 'ਸ਼',
  'ਖ਼' => 'ਖ਼',
  'ਗ਼' => 'ਗ਼',
  'ਜ਼' => 'ਜ਼',
  'ਫ਼' => 'ਫ਼',
  'ୈ' => 'ୈ',
  'ୋ' => 'ୋ',
  'ୌ' => 'ୌ',
  'ଡ଼' => 'ଡ଼',
  'ଢ଼' => 'ଢ଼',
  'ஔ' => 'ஔ',
  'ொ' => 'ொ',
  'ோ' => 'ோ',
  'ௌ' => 'ௌ',
  'ై' => 'ై',
  'ೀ' => 'ೀ',
  'ೇ' => 'ೇ',
  'ೈ' => 'ೈ',
  'ೊ' => 'ೊ',
  'ೋ' => 'ೋ',
  'ൊ' => 'ൊ',
  'ോ' => 'ോ',
  'ൌ' => 'ൌ',
  'ේ' => 'ේ',
  'ො' => 'ො',
  'ෝ' => 'ෝ',
  'ෞ' => 'ෞ',
  'གྷ' => 'གྷ',
  'ཌྷ' => 'ཌྷ',
  'དྷ' => 'དྷ',
  'བྷ' => 'བྷ',
  'ཛྷ' => 'ཛྷ',
  'ཀྵ' => 'ཀྵ',
  'ཱི' => 'ཱི',
  'ཱུ' => 'ཱུ',
  'ྲྀ' => 'ྲྀ',
  'ླྀ' => 'ླྀ',
  'ཱྀ' => 'ཱྀ',
  'ྒྷ' => 'ྒྷ',
  'ྜྷ' => 'ྜྷ',
  'ྡྷ' => 'ྡྷ',
  'ྦྷ' => 'ྦྷ',
  'ྫྷ' => 'ྫྷ',
  'ྐྵ' => 'ྐྵ',
  'ဦ' => 'ဦ',
  'ᬆ' => 'ᬆ',
  'ᬈ' => 'ᬈ',
  'ᬊ' => 'ᬊ',
  'ᬌ' => 'ᬌ',
  'ᬎ' => 'ᬎ',
  'ᬒ' => 'ᬒ',
  'ᬻ' => 'ᬻ',
  'ᬽ' => 'ᬽ',
  'ᭀ' => 'ᭀ',
  'ᭁ' => 'ᭁ',
  'ᭃ' => 'ᭃ',
  'Ḁ' => 'Ḁ',
  'ḁ' => 'ḁ',
  'Ḃ' => 'Ḃ',
  'ḃ' => 'ḃ',
  'Ḅ' => 'Ḅ',
  'ḅ' => 'ḅ',
  'Ḇ' => 'Ḇ',
  'ḇ' => 'ḇ',
  'Ḉ' => 'Ḉ',
  'ḉ' => 'ḉ',
  'Ḋ' => 'Ḋ',
  'ḋ' => 'ḋ',
  'Ḍ' => 'Ḍ',
  'ḍ' => 'ḍ',
  'Ḏ' => 'Ḏ',
  'ḏ' => 'ḏ',
  'Ḑ' => 'Ḑ',
  'ḑ' => 'ḑ',
  'Ḓ' => 'Ḓ',
  'ḓ' => 'ḓ',
  'Ḕ' => 'Ḕ',
  'ḕ' => 'ḕ',
  'Ḗ' => 'Ḗ',
  'ḗ' => 'ḗ',
  'Ḙ' => 'Ḙ',
  'ḙ' => 'ḙ',
  'Ḛ' => 'Ḛ',
  'ḛ' => 'ḛ',
  'Ḝ' => 'Ḝ',
  'ḝ' => 'ḝ',
  'Ḟ' => 'Ḟ',
  'ḟ' => 'ḟ',
  'Ḡ' => 'Ḡ',
  'ḡ' => 'ḡ',
  'Ḣ' => 'Ḣ',
  'ḣ' => 'ḣ',
  'Ḥ' => 'Ḥ',
  'ḥ' => 'ḥ',
  'Ḧ' => 'Ḧ',
  'ḧ' => 'ḧ',
  'Ḩ' => 'Ḩ',
  'ḩ' => 'ḩ',
  'Ḫ' => 'Ḫ',
  'ḫ' => 'ḫ',
  'Ḭ' => 'Ḭ',
  'ḭ' => 'ḭ',
  'Ḯ' => 'Ḯ',
  'ḯ' => 'ḯ',
  'Ḱ' => 'Ḱ',
  'ḱ' => 'ḱ',
  'Ḳ' => 'Ḳ',
  'ḳ' => 'ḳ',
  'Ḵ' => 'Ḵ',
  'ḵ' => 'ḵ',
  'Ḷ' => 'Ḷ',
  'ḷ' => 'ḷ',
  'Ḹ' => 'Ḹ',
  'ḹ' => 'ḹ',
  'Ḻ' => 'Ḻ',
  'ḻ' => 'ḻ',
  'Ḽ' => 'Ḽ',
  'ḽ' => 'ḽ',
  'Ḿ' => 'Ḿ',
  'ḿ' => 'ḿ',
  'Ṁ' => 'Ṁ',
  'ṁ' => 'ṁ',
  'Ṃ' => 'Ṃ',
  'ṃ' => 'ṃ',
  'Ṅ' => 'Ṅ',
  'ṅ' => 'ṅ',
  'Ṇ' => 'Ṇ',
  'ṇ' => 'ṇ',
  'Ṉ' => 'Ṉ',
  'ṉ' => 'ṉ',
  'Ṋ' => 'Ṋ',
  'ṋ' => 'ṋ',
  'Ṍ' => 'Ṍ',
  'ṍ' => 'ṍ',
  'Ṏ' => 'Ṏ',
  'ṏ' => 'ṏ',
  'Ṑ' => 'Ṑ',
  'ṑ' => 'ṑ',
  'Ṓ' => 'Ṓ',
  'ṓ' => 'ṓ',
  'Ṕ' => 'Ṕ',
  'ṕ' => 'ṕ',
  'Ṗ' => 'Ṗ',
  'ṗ' => 'ṗ',
  'Ṙ' => 'Ṙ',
  'ṙ' => 'ṙ',
  'Ṛ' => 'Ṛ',
  'ṛ' => 'ṛ',
  'Ṝ' => 'Ṝ',
  'ṝ' => 'ṝ',
  'Ṟ' => 'Ṟ',
  'ṟ' => 'ṟ',
  'Ṡ' => 'Ṡ',
  'ṡ' => 'ṡ',
  'Ṣ' => 'Ṣ',
  'ṣ' => 'ṣ',
  'Ṥ' => 'Ṥ',
  'ṥ' => 'ṥ',
  'Ṧ' => 'Ṧ',
  'ṧ' => 'ṧ',
  'Ṩ' => 'Ṩ',
  'ṩ' => 'ṩ',
  'Ṫ' => 'Ṫ',
  'ṫ' => 'ṫ',
  'Ṭ' => 'Ṭ',
  'ṭ' => 'ṭ',
  'Ṯ' => 'Ṯ',
  'ṯ' => 'ṯ',
  'Ṱ' => 'Ṱ',
  'ṱ' => 'ṱ',
  'Ṳ' => 'Ṳ',
  'ṳ' => 'ṳ',
  'Ṵ' => 'Ṵ',
  'ṵ' => 'ṵ',
  'Ṷ' => 'Ṷ',
  'ṷ' => 'ṷ',
  'Ṹ' => 'Ṹ',
  'ṹ' => 'ṹ',
  'Ṻ' => 'Ṻ',
  'ṻ' => 'ṻ',
  'Ṽ' => 'Ṽ',
  'ṽ' => 'ṽ',
  'Ṿ' => 'Ṿ',
  'ṿ' => 'ṿ',
  'Ẁ' => 'Ẁ',
  'ẁ' => 'ẁ',
  'Ẃ' => 'Ẃ',
  'ẃ' => 'ẃ',
  'Ẅ' => 'Ẅ',
  'ẅ' => 'ẅ',
  'Ẇ' => 'Ẇ',
  'ẇ' => 'ẇ',
  'Ẉ' => 'Ẉ',
  'ẉ' => 'ẉ',
  'Ẋ' => 'Ẋ',
  'ẋ' => 'ẋ',
  'Ẍ' => 'Ẍ',
  'ẍ' => 'ẍ',
  'Ẏ' => 'Ẏ',
  'ẏ' => 'ẏ',
  'Ẑ' => 'Ẑ',
  'ẑ' => 'ẑ',
  'Ẓ' => 'Ẓ',
  'ẓ' => 'ẓ',
  'Ẕ' => 'Ẕ',
  'ẕ' => 'ẕ',
  'ẖ' => 'ẖ',
  'ẗ' => 'ẗ',
  'ẘ' => 'ẘ',
  'ẙ' => 'ẙ',
  'ẛ' => 'ẛ',
  'Ạ' => 'Ạ',
  'ạ' => 'ạ',
  'Ả' => 'Ả',
  'ả' => 'ả',
  'Ấ' => 'Ấ',
  'ấ' => 'ấ',
  'Ầ' => 'Ầ',
  'ầ' => 'ầ',
  'Ẩ' => 'Ẩ',
  'ẩ' => 'ẩ',
  'Ẫ' => 'Ẫ',
  'ẫ' => 'ẫ',
  'Ậ' => 'Ậ',
  'ậ' => 'ậ',
  'Ắ' => 'Ắ',
  'ắ' => 'ắ',
  'Ằ' => 'Ằ',
  'ằ' => 'ằ',
  'Ẳ' => 'Ẳ',
  'ẳ' => 'ẳ',
  'Ẵ' => 'Ẵ',
  'ẵ' => 'ẵ',
  'Ặ' => 'Ặ',
  'ặ' => 'ặ',
  'Ẹ' => 'Ẹ',
  'ẹ' => 'ẹ',
  'Ẻ' => 'Ẻ',
  'ẻ' => 'ẻ',
  'Ẽ' => 'Ẽ',
  'ẽ' => 'ẽ',
  'Ế' => 'Ế',
  'ế' => 'ế',
  'Ề' => 'Ề',
  'ề' => 'ề',
  'Ể' => 'Ể',
  'ể' => 'ể',
  'Ễ' => 'Ễ',
  'ễ' => 'ễ',
  'Ệ' => 'Ệ',
  'ệ' => 'ệ',
  'Ỉ' => 'Ỉ',
  'ỉ' => 'ỉ',
  'Ị' => 'Ị',
  'ị' => 'ị',
  'Ọ' => 'Ọ',
  'ọ' => 'ọ',
  'Ỏ' => 'Ỏ',
  'ỏ' => 'ỏ',
  'Ố' => 'Ố',
  'ố' => 'ố',
  'Ồ' => 'Ồ',
  'ồ' => 'ồ',
  'Ổ' => 'Ổ',
  'ổ' => 'ổ',
  'Ỗ' => 'Ỗ',
  'ỗ' => 'ỗ',
  'Ộ' => 'Ộ',
  'ộ' => 'ộ',
  'Ớ' => 'Ớ',
  'ớ' => 'ớ',
  'Ờ' => 'Ờ',
  'ờ' => 'ờ',
  'Ở' => 'Ở',
  'ở' => 'ở',
  'Ỡ' => 'Ỡ',
  'ỡ' => 'ỡ',
  'Ợ' => 'Ợ',
  'ợ' => 'ợ',
  'Ụ' => 'Ụ',
  'ụ' => 'ụ',
  'Ủ' => 'Ủ',
  'ủ' => 'ủ',
  'Ứ' => 'Ứ',
  'ứ' => 'ứ',
  'Ừ' => 'Ừ',
  'ừ' => 'ừ',
  'Ử' => 'Ử',
  'ử' => 'ử',
  'Ữ' => 'Ữ',
  'ữ' => 'ữ',
  'Ự' => 'Ự',
  'ự' => 'ự',
  'Ỳ' => 'Ỳ',
  'ỳ' => 'ỳ',
  'Ỵ' => 'Ỵ',
  'ỵ' => 'ỵ',
  'Ỷ' => 'Ỷ',
  'ỷ' => 'ỷ',
  'Ỹ' => 'Ỹ',
  'ỹ' => 'ỹ',
  'ἀ' => 'ἀ',
  'ἁ' => 'ἁ',
  'ἂ' => 'ἂ',
  'ἃ' => 'ἃ',
  'ἄ' => 'ἄ',
  'ἅ' => 'ἅ',
  'ἆ' => 'ἆ',
  'ἇ' => 'ἇ',
  'Ἀ' => 'Ἀ',
  'Ἁ' => 'Ἁ',
  'Ἂ' => 'Ἂ',
  'Ἃ' => 'Ἃ',
  'Ἄ' => 'Ἄ',
  'Ἅ' => 'Ἅ',
  'Ἆ' => 'Ἆ',
  'Ἇ' => 'Ἇ',
  'ἐ' => 'ἐ',
  'ἑ' => 'ἑ',
  'ἒ' => 'ἒ',
  'ἓ' => 'ἓ',
  'ἔ' => 'ἔ',
  'ἕ' => 'ἕ',
  'Ἐ' => 'Ἐ',
  'Ἑ' => 'Ἑ',
  'Ἒ' => 'Ἒ',
  'Ἓ' => 'Ἓ',
  'Ἔ' => 'Ἔ',
  'Ἕ' => 'Ἕ',
  'ἠ' => 'ἠ',
  'ἡ' => 'ἡ',
  'ἢ' => 'ἢ',
  'ἣ' => 'ἣ',
  'ἤ' => 'ἤ',
  'ἥ' => 'ἥ',
  'ἦ' => 'ἦ',
  'ἧ' => 'ἧ',
  'Ἠ' => 'Ἠ',
  'Ἡ' => 'Ἡ',
  'Ἢ' => 'Ἢ',
  'Ἣ' => 'Ἣ',
  'Ἤ' => 'Ἤ',
  'Ἥ' => 'Ἥ',
  'Ἦ' => 'Ἦ',
  'Ἧ' => 'Ἧ',
  'ἰ' => 'ἰ',
  'ἱ' => 'ἱ',
  'ἲ' => 'ἲ',
  'ἳ' => 'ἳ',
  'ἴ' => 'ἴ',
  'ἵ' => 'ἵ',
  'ἶ' => 'ἶ',
  'ἷ' => 'ἷ',
  'Ἰ' => 'Ἰ',
  'Ἱ' => 'Ἱ',
  'Ἲ' => 'Ἲ',
  'Ἳ' => 'Ἳ',
  'Ἴ' => 'Ἴ',
  'Ἵ' => 'Ἵ',
  'Ἶ' => 'Ἶ',
  'Ἷ' => 'Ἷ',
  'ὀ' => 'ὀ',
  'ὁ' => 'ὁ',
  'ὂ' => 'ὂ',
  'ὃ' => 'ὃ',
  'ὄ' => 'ὄ',
  'ὅ' => 'ὅ',
  'Ὀ' => 'Ὀ',
  'Ὁ' => 'Ὁ',
  'Ὂ' => 'Ὂ',
  'Ὃ' => 'Ὃ',
  'Ὄ' => 'Ὄ',
  'Ὅ' => 'Ὅ',
  'ὐ' => 'ὐ',
  'ὑ' => 'ὑ',
  'ὒ' => 'ὒ',
  'ὓ' => 'ὓ',
  'ὔ' => 'ὔ',
  'ὕ' => 'ὕ',
  'ὖ' => 'ὖ',
  'ὗ' => 'ὗ',
  'Ὑ' => 'Ὑ',
  'Ὓ' => 'Ὓ',
  'Ὕ' => 'Ὕ',
  'Ὗ' => 'Ὗ',
  'ὠ' => 'ὠ',
  'ὡ' => 'ὡ',
  'ὢ' => 'ὢ',
  'ὣ' => 'ὣ',
  'ὤ' => 'ὤ',
  'ὥ' => 'ὥ',
  'ὦ' => 'ὦ',
  'ὧ' => 'ὧ',
  'Ὠ' => 'Ὠ',
  'Ὡ' => 'Ὡ',
  'Ὢ' => 'Ὢ',
  'Ὣ' => 'Ὣ',
  'Ὤ' => 'Ὤ',
  'Ὥ' => 'Ὥ',
  'Ὦ' => 'Ὦ',
  'Ὧ' => 'Ὧ',
  'ὰ' => 'ὰ',
  'ά' => 'ά',
  'ὲ' => 'ὲ',
  'έ' => 'έ',
  'ὴ' => 'ὴ',
  'ή' => 'ή',
  'ὶ' => 'ὶ',
  'ί' => 'ί',
  'ὸ' => 'ὸ',
  'ό' => 'ό',
  'ὺ' => 'ὺ',
  'ύ' => 'ύ',
  'ὼ' => 'ὼ',
  'ώ' => 'ώ',
  'ᾀ' => 'ᾀ',
  'ᾁ' => 'ᾁ',
  'ᾂ' => 'ᾂ',
  'ᾃ' => 'ᾃ',
  'ᾄ' => 'ᾄ',
  'ᾅ' => 'ᾅ',
  'ᾆ' => 'ᾆ',
  'ᾇ' => 'ᾇ',
  'ᾈ' => 'ᾈ',
  'ᾉ' => 'ᾉ',
  'ᾊ' => 'ᾊ',
  'ᾋ' => 'ᾋ',
  'ᾌ' => 'ᾌ',
  'ᾍ' => 'ᾍ',
  'ᾎ' => 'ᾎ',
  'ᾏ' => 'ᾏ',
  'ᾐ' => 'ᾐ',
  'ᾑ' => 'ᾑ',
  'ᾒ' => 'ᾒ',
  'ᾓ' => 'ᾓ',
  'ᾔ' => 'ᾔ',
  'ᾕ' => 'ᾕ',
  'ᾖ' => 'ᾖ',
  'ᾗ' => 'ᾗ',
  'ᾘ' => 'ᾘ',
  'ᾙ' => 'ᾙ',
  'ᾚ' => 'ᾚ',
  'ᾛ' => 'ᾛ',
  'ᾜ' => 'ᾜ',
  'ᾝ' => 'ᾝ',
  'ᾞ' => 'ᾞ',
  'ᾟ' => 'ᾟ',
  'ᾠ' => 'ᾠ',
  'ᾡ' => 'ᾡ',
  'ᾢ' => 'ᾢ',
  'ᾣ' => 'ᾣ',
  'ᾤ' => 'ᾤ',
  'ᾥ' => 'ᾥ',
  'ᾦ' => 'ᾦ',
  'ᾧ' => 'ᾧ',
  'ᾨ' => 'ᾨ',
  'ᾩ' => 'ᾩ',
  'ᾪ' => 'ᾪ',
  'ᾫ' => 'ᾫ',
  'ᾬ' => 'ᾬ',
  'ᾭ' => 'ᾭ',
  'ᾮ' => 'ᾮ',
  'ᾯ' => 'ᾯ',
  'ᾰ' => 'ᾰ',
  'ᾱ' => 'ᾱ',
  'ᾲ' => 'ᾲ',
  'ᾳ' => 'ᾳ',
  'ᾴ' => 'ᾴ',
  'ᾶ' => 'ᾶ',
  'ᾷ' => 'ᾷ',
  'Ᾰ' => 'Ᾰ',
  'Ᾱ' => 'Ᾱ',
  'Ὰ' => 'Ὰ',
  'Ά' => 'Ά',
  'ᾼ' => 'ᾼ',
  'ι' => 'ι',
  '῁' => '῁',
  'ῂ' => 'ῂ',
  'ῃ' => 'ῃ',
  'ῄ' => 'ῄ',
  'ῆ' => 'ῆ',
  'ῇ' => 'ῇ',
  'Ὲ' => 'Ὲ',
  'Έ' => 'Έ',
  'Ὴ' => 'Ὴ',
  'Ή' => 'Ή',
  'ῌ' => 'ῌ',
  '῍' => '῍',
  '῎' => '῎',
  '῏' => '῏',
  'ῐ' => 'ῐ',
  'ῑ' => 'ῑ',
  'ῒ' => 'ῒ',
  'ΐ' => 'ΐ',
  'ῖ' => 'ῖ',
  'ῗ' => 'ῗ',
  'Ῐ' => 'Ῐ',
  'Ῑ' => 'Ῑ',
  'Ὶ' => 'Ὶ',
  'Ί' => 'Ί',
  '῝' => '῝',
  '῞' => '῞',
  '῟' => '῟',
  'ῠ' => 'ῠ',
  'ῡ' => 'ῡ',
  'ῢ' => 'ῢ',
  'ΰ' => 'ΰ',
  'ῤ' => 'ῤ',
  'ῥ' => 'ῥ',
  'ῦ' => 'ῦ',
  'ῧ' => 'ῧ',
  'Ῠ' => 'Ῠ',
  'Ῡ' => 'Ῡ',
  'Ὺ' => 'Ὺ',
  'Ύ' => 'Ύ',
  'Ῥ' => 'Ῥ',
  '῭' => '῭',
  '΅' => '΅',
  '`' => '`',
  'ῲ' => 'ῲ',
  'ῳ' => 'ῳ',
  'ῴ' => 'ῴ',
  'ῶ' => 'ῶ',
  'ῷ' => 'ῷ',
  'Ὸ' => 'Ὸ',
  'Ό' => 'Ό',
  'Ὼ' => 'Ὼ',
  'Ώ' => 'Ώ',
  'ῼ' => 'ῼ',
  '´' => '´',
  ' ' => ' ',
  ' ' => ' ',
  'Ω' => 'Ω',
  'K' => 'K',
  'Å' => 'Å',
  '↚' => '↚',
  '↛' => '↛',
  '↮' => '↮',
  '⇍' => '⇍',
  '⇎' => '⇎',
  '⇏' => '⇏',
  '∄' => '∄',
  '∉' => '∉',
  '∌' => '∌',
  '∤' => '∤',
  '∦' => '∦',
  '≁' => '≁',
  '≄' => '≄',
  '≇' => '≇',
  '≉' => '≉',
  '≠' => '≠',
  '≢' => '≢',
  '≭' => '≭',
  '≮' => '≮',
  '≯' => '≯',
  '≰' => '≰',
  '≱' => '≱',
  '≴' => '≴',
  '≵' => '≵',
  '≸' => '≸',
  '≹' => '≹',
  '⊀' => '⊀',
  '⊁' => '⊁',
  '⊄' => '⊄',
  '⊅' => '⊅',
  '⊈' => '⊈',
  '⊉' => '⊉',
  '⊬' => '⊬',
  '⊭' => '⊭',
  '⊮' => '⊮',
  '⊯' => '⊯',
  '⋠' => '⋠',
  '⋡' => '⋡',
  '⋢' => '⋢',
  '⋣' => '⋣',
  '⋪' => '⋪',
  '⋫' => '⋫',
  '⋬' => '⋬',
  '⋭' => '⋭',
  '〈' => '〈',
  '〉' => '〉',
  '⫝̸' => '⫝̸',
  'が' => 'が',
  'ぎ' => 'ぎ',
  'ぐ' => 'ぐ',
  'げ' => 'げ',
  'ご' => 'ご',
  'ざ' => 'ざ',
  'じ' => 'じ',
  'ず' => 'ず',
  'ぜ' => 'ぜ',
  'ぞ' => 'ぞ',
  'だ' => 'だ',
  'ぢ' => 'ぢ',
  'づ' => 'づ',
  'で' => 'で',
  'ど' => 'ど',
  'ば' => 'ば',
  'ぱ' => 'ぱ',
  'び' => 'び',
  'ぴ' => 'ぴ',
  'ぶ' => 'ぶ',
  'ぷ' => 'ぷ',
  'べ' => 'べ',
  'ぺ' => 'ぺ',
  'ぼ' => 'ぼ',
  'ぽ' => 'ぽ',
  'ゔ' => 'ゔ',
  'ゞ' => 'ゞ',
  'ガ' => 'ガ',
  'ギ' => 'ギ',
  'グ' => 'グ',
  'ゲ' => 'ゲ',
  'ゴ' => 'ゴ',
  'ザ' => 'ザ',
  'ジ' => 'ジ',
  'ズ' => 'ズ',
  'ゼ' => 'ゼ',
  'ゾ' => 'ゾ',
  'ダ' => 'ダ',
  'ヂ' => 'ヂ',
  'ヅ' => 'ヅ',
  'デ' => 'デ',
  'ド' => 'ド',
  'バ' => 'バ',
  'パ' => 'パ',
  'ビ' => 'ビ',
  'ピ' => 'ピ',
  'ブ' => 'ブ',
  'プ' => 'プ',
  'ベ' => 'ベ',
  'ペ' => 'ペ',
  'ボ' => 'ボ',
  'ポ' => 'ポ',
  'ヴ' => 'ヴ',
  'ヷ' => 'ヷ',
  'ヸ' => 'ヸ',
  'ヹ' => 'ヹ',
  'ヺ' => 'ヺ',
  'ヾ' => 'ヾ',
  '豈' => '豈',
  '更' => '更',
  '車' => '車',
  '賈' => '賈',
  '滑' => '滑',
  '串' => '串',
  '句' => '句',
  '龜' => '龜',
  '龜' => '龜',
  '契' => '契',
  '金' => '金',
  '喇' => '喇',
  '奈' => '奈',
  '懶' => '懶',
  '癩' => '癩',
  '羅' => '羅',
  '蘿' => '蘿',
  '螺' => '螺',
  '裸' => '裸',
  '邏' => '邏',
  '樂' => '樂',
  '洛' => '洛',
  '烙' => '烙',
  '珞' => '珞',
  '落' => '落',
  '酪' => '酪',
  '駱' => '駱',
  '亂' => '亂',
  '卵' => '卵',
  '欄' => '欄',
  '爛' => '爛',
  '蘭' => '蘭',
  '鸞' => '鸞',
  '嵐' => '嵐',
  '濫' => '濫',
  '藍' => '藍',
  '襤' => '襤',
  '拉' => '拉',
  '臘' => '臘',
  '蠟' => '蠟',
  '廊' => '廊',
  '朗' => '朗',
  '浪' => '浪',
  '狼' => '狼',
  '郎' => '郎',
  '來' => '來',
  '冷' => '冷',
  '勞' => '勞',
  '擄' => '擄',
  '櫓' => '櫓',
  '爐' => '爐',
  '盧' => '盧',
  '老' => '老',
  '蘆' => '蘆',
  '虜' => '虜',
  '路' => '路',
  '露' => '露',
  '魯' => '魯',
  '鷺' => '鷺',
  '碌' => '碌',
  '祿' => '祿',
  '綠' => '綠',
  '菉' => '菉',
  '錄' => '錄',
  '鹿' => '鹿',
  '論' => '論',
  '壟' => '壟',
  '弄' => '弄',
  '籠' => '籠',
  '聾' => '聾',
  '牢' => '牢',
  '磊' => '磊',
  '賂' => '賂',
  '雷' => '雷',
  '壘' => '壘',
  '屢' => '屢',
  '樓' => '樓',
  '淚' => '淚',
  '漏' => '漏',
  '累' => '累',
  '縷' => '縷',
  '陋' => '陋',
  '勒' => '勒',
  '肋' => '肋',
  '凜' => '凜',
  '凌' => '凌',
  '稜' => '稜',
  '綾' => '綾',
  '菱' => '菱',
  '陵' => '陵',
  '讀' => '讀',
  '拏' => '拏',
  '樂' => '樂',
  '諾' => '諾',
  '丹' => '丹',
  '寧' => '寧',
  '怒' => '怒',
  '率' => '率',
  '異' => '異',
  '北' => '北',
  '磻' => '磻',
  '便' => '便',
  '復' => '復',
  '不' => '不',
  '泌' => '泌',
  '數' => '數',
  '索' => '索',
  '參' => '參',
  '塞' => '塞',
  '省' => '省',
  '葉' => '葉',
  '說' => '說',
  '殺' => '殺',
  '辰' => '辰',
  '沈' => '沈',
  '拾' => '拾',
  '若' => '若',
  '掠' => '掠',
  '略' => '略',
  '亮' => '亮',
  '兩' => '兩',
  '凉' => '凉',
  '梁' => '梁',
  '糧' => '糧',
  '良' => '良',
  '諒' => '諒',
  '量' => '量',
  '勵' => '勵',
  '呂' => '呂',
  '女' => '女',
  '廬' => '廬',
  '旅' => '旅',
  '濾' => '濾',
  '礪' => '礪',
  '閭' => '閭',
  '驪' => '驪',
  '麗' => '麗',
  '黎' => '黎',
  '力' => '力',
  '曆' => '曆',
  '歷' => '歷',
  '轢' => '轢',
  '年' => '年',
  '憐' => '憐',
  '戀' => '戀',
  '撚' => '撚',
  '漣' => '漣',
  '煉' => '煉',
  '璉' => '璉',
  '秊' => '秊',
  '練' => '練',
  '聯' => '聯',
  '輦' => '輦',
  '蓮' => '蓮',
  '連' => '連',
  '鍊' => '鍊',
  '列' => '列',
  '劣' => '劣',
  '咽' => '咽',
  '烈' => '烈',
  '裂' => '裂',
  '說' => '說',
  '廉' => '廉',
  '念' => '念',
  '捻' => '捻',
  '殮' => '殮',
  '簾' => '簾',
  '獵' => '獵',
  '令' => '令',
  '囹' => '囹',
  '寧' => '寧',
  '嶺' => '嶺',
  '怜' => '怜',
  '玲' => '玲',
  '瑩' => '瑩',
  '羚' => '羚',
  '聆' => '聆',
  '鈴' => '鈴',
  '零' => '零',
  '靈' => '靈',
  '領' => '領',
  '例' => '例',
  '禮' => '禮',
  '醴' => '醴',
  '隸' => '隸',
  '惡' => '惡',
  '了' => '了',
  '僚' => '僚',
  '寮' => '寮',
  '尿' => '尿',
  '料' => '料',
  '樂' => '樂',
  '燎' => '燎',
  '療' => '療',
  '蓼' => '蓼',
  '遼' => '遼',
  '龍' => '龍',
  '暈' => '暈',
  '阮' => '阮',
  '劉' => '劉',
  '杻' => '杻',
  '柳' => '柳',
  '流' => '流',
  '溜' => '溜',
  '琉' => '琉',
  '留' => '留',
  '硫' => '硫',
  '紐' => '紐',
  '類' => '類',
  '六' => '六',
  '戮' => '戮',
  '陸' => '陸',
  '倫' => '倫',
  '崙' => '崙',
  '淪' => '淪',
  '輪' => '輪',
  '律' => '律',
  '慄' => '慄',
  '栗' => '栗',
  '率' => '率',
  '隆' => '隆',
  '利' => '利',
  '吏' => '吏',
  '履' => '履',
  '易' => '易',
  '李' => '李',
  '梨' => '梨',
  '泥' => '泥',
  '理' => '理',
  '痢' => '痢',
  '罹' => '罹',
  '裏' => '裏',
  '裡' => '裡',
  '里' => '里',
  '離' => '離',
  '匿' => '匿',
  '溺' => '溺',
  '吝' => '吝',
  '燐' => '燐',
  '璘' => '璘',
  '藺' => '藺',
  '隣' => '隣',
  '鱗' => '鱗',
  '麟' => '麟',
  '林' => '林',
  '淋' => '淋',
  '臨' => '臨',
  '立' => '立',
  '笠' => '笠',
  '粒' => '粒',
  '狀' => '狀',
  '炙' => '炙',
  '識' => '識',
  '什' => '什',
  '茶' => '茶',
  '刺' => '刺',
  '切' => '切',
  '度' => '度',
  '拓' => '拓',
  '糖' => '糖',
  '宅' => '宅',
  '洞' => '洞',
  '暴' => '暴',
  '輻' => '輻',
  '行' => '行',
  '降' => '降',
  '見' => '見',
  '廓' => '廓',
  '兀' => '兀',
  '嗀' => '嗀',
  '塚' => '塚',
  '晴' => '晴',
  '凞' => '凞',
  '猪' => '猪',
  '益' => '益',
  '礼' => '礼',
  '神' => '神',
  '祥' => '祥',
  '福' => '福',
  '靖' => '靖',
  '精' => '精',
  '羽' => '羽',
  '蘒' => '蘒',
  '諸' => '諸',
  '逸' => '逸',
  '都' => '都',
  '飯' => '飯',
  '飼' => '飼',
  '館' => '館',
  '鶴' => '鶴',
  '郞' => '郞',
  '隷' => '隷',
  '侮' => '侮',
  '僧' => '僧',
  '免' => '免',
  '勉' => '勉',
  '勤' => '勤',
  '卑' => '卑',
  '喝' => '喝',
  '嘆' => '嘆',
  '器' => '器',
  '塀' => '塀',
  '墨' => '墨',
  '層' => '層',
  '屮' => '屮',
  '悔' => '悔',
  '慨' => '慨',
  '憎' => '憎',
  '懲' => '懲',
  '敏' => '敏',
  '既' => '既',
  '暑' => '暑',
  '梅' => '梅',
  '海' => '海',
  '渚' => '渚',
  '漢' => '漢',
  '煮' => '煮',
  '爫' => '爫',
  '琢' => '琢',
  '碑' => '碑',
  '社' => '社',
  '祉' => '祉',
  '祈' => '祈',
  '祐' => '祐',
  '祖' => '祖',
  '祝' => '祝',
  '禍' => '禍',
  '禎' => '禎',
  '穀' => '穀',
  '突' => '突',
  '節' => '節',
  '練' => '練',
  '縉' => '縉',
  '繁' => '繁',
  '署' => '署',
  '者' => '者',
  '臭' => '臭',
  '艹' => '艹',
  '艹' => '艹',
  '著' => '著',
  '褐' => '褐',
  '視' => '視',
  '謁' => '謁',
  '謹' => '謹',
  '賓' => '賓',
  '贈' => '贈',
  '辶' => '辶',
  '逸' => '逸',
  '難' => '難',
  '響' => '響',
  '頻' => '頻',
  '恵' => '恵',
  '𤋮' => '𤋮',
  '舘' => '舘',
  '並' => '並',
  '况' => '况',
  '全' => '全',
  '侀' => '侀',
  '充' => '充',
  '冀' => '冀',
  '勇' => '勇',
  '勺' => '勺',
  '喝' => '喝',
  '啕' => '啕',
  '喙' => '喙',
  '嗢' => '嗢',
  '塚' => '塚',
  '墳' => '墳',
  '奄' => '奄',
  '奔' => '奔',
  '婢' => '婢',
  '嬨' => '嬨',
  '廒' => '廒',
  '廙' => '廙',
  '彩' => '彩',
  '徭' => '徭',
  '惘' => '惘',
  '慎' => '慎',
  '愈' => '愈',
  '憎' => '憎',
  '慠' => '慠',
  '懲' => '懲',
  '戴' => '戴',
  '揄' => '揄',
  '搜' => '搜',
  '摒' => '摒',
  '敖' => '敖',
  '晴' => '晴',
  '朗' => '朗',
  '望' => '望',
  '杖' => '杖',
  '歹' => '歹',
  '殺' => '殺',
  '流' => '流',
  '滛' => '滛',
  '滋' => '滋',
  '漢' => '漢',
  '瀞' => '瀞',
  '煮' => '煮',
  '瞧' => '瞧',
  '爵' => '爵',
  '犯' => '犯',
  '猪' => '猪',
  '瑱' => '瑱',
  '甆' => '甆',
  '画' => '画',
  '瘝' => '瘝',
  '瘟' => '瘟',
  '益' => '益',
  '盛' => '盛',
  '直' => '直',
  '睊' => '睊',
  '着' => '着',
  '磌' => '磌',
  '窱' => '窱',
  '節' => '節',
  '类' => '类',
  '絛' => '絛',
  '練' => '練',
  '缾' => '缾',
  '者' => '者',
  '荒' => '荒',
  '華' => '華',
  '蝹' => '蝹',
  '襁' => '襁',
  '覆' => '覆',
  '視' => '視',
  '調' => '調',
  '諸' => '諸',
  '請' => '請',
  '謁' => '謁',
  '諾' => '諾',
  '諭' => '諭',
  '謹' => '謹',
  '變' => '變',
  '贈' => '贈',
  '輸' => '輸',
  '遲' => '遲',
  '醙' => '醙',
  '鉶' => '鉶',
  '陼' => '陼',
  '難' => '難',
  '靖' => '靖',
  '韛' => '韛',
  '響' => '響',
  '頋' => '頋',
  '頻' => '頻',
  '鬒' => '鬒',
  '龜' => '龜',
  '𢡊' => '𢡊',
  '𢡄' => '𢡄',
  '𣏕' => '𣏕',
  '㮝' => '㮝',
  '䀘' => '䀘',
  '䀹' => '䀹',
  '𥉉' => '𥉉',
  '𥳐' => '𥳐',
  '𧻓' => '𧻓',
  '齃' => '齃',
  '龎' => '龎',
  'יִ' => 'יִ',
  'ײַ' => 'ײַ',
  'שׁ' => 'שׁ',
  'שׂ' => 'שׂ',
  'שּׁ' => 'שּׁ',
  'שּׂ' => 'שּׂ',
  'אַ' => 'אַ',
  'אָ' => 'אָ',
  'אּ' => 'אּ',
  'בּ' => 'בּ',
  'גּ' => 'גּ',
  'דּ' => 'דּ',
  'הּ' => 'הּ',
  'וּ' => 'וּ',
  'זּ' => 'זּ',
  'טּ' => 'טּ',
  'יּ' => 'יּ',
  'ךּ' => 'ךּ',
  'כּ' => 'כּ',
  'לּ' => 'לּ',
  'מּ' => 'מּ',
  'נּ' => 'נּ',
  'סּ' => 'סּ',
  'ףּ' => 'ףּ',
  'פּ' => 'פּ',
  'צּ' => 'צּ',
  'קּ' => 'קּ',
  'רּ' => 'רּ',
  'שּ' => 'שּ',
  'תּ' => 'תּ',
  'וֹ' => 'וֹ',
  'בֿ' => 'בֿ',
  'כֿ' => 'כֿ',
  'פֿ' => 'פֿ',
  '𑂚' => '𑂚',
  '𑂜' => '𑂜',
  '𑂫' => '𑂫',
  '𑄮' => '𑄮',
  '𑄯' => '𑄯',
  '𑍋' => '𑍋',
  '𑍌' => '𑍌',
  '𑒻' => '𑒻',
  '𑒼' => '𑒼',
  '𑒾' => '𑒾',
  '𑖺' => '𑖺',
  '𑖻' => '𑖻',
  '𑤸' => '𑤸',
  '𝅗𝅥' => '𝅗𝅥',
  '𝅘𝅥' => '𝅘𝅥',
  '𝅘𝅥𝅮' => '𝅘𝅥𝅮',
  '𝅘𝅥𝅯' => '𝅘𝅥𝅯',
  '𝅘𝅥𝅰' => '𝅘𝅥𝅰',
  '𝅘𝅥𝅱' => '𝅘𝅥𝅱',
  '𝅘𝅥𝅲' => '𝅘𝅥𝅲',
  '𝆹𝅥' => '𝆹𝅥',
  '𝆺𝅥' => '𝆺𝅥',
  '𝆹𝅥𝅮' => '𝆹𝅥𝅮',
  '𝆺𝅥𝅮' => '𝆺𝅥𝅮',
  '𝆹𝅥𝅯' => '𝆹𝅥𝅯',
  '𝆺𝅥𝅯' => '𝆺𝅥𝅯',
  '丽' => '丽',
  '丸' => '丸',
  '乁' => '乁',
  '𠄢' => '𠄢',
  '你' => '你',
  '侮' => '侮',
  '侻' => '侻',
  '倂' => '倂',
  '偺' => '偺',
  '備' => '備',
  '僧' => '僧',
  '像' => '像',
  '㒞' => '㒞',
  '𠘺' => '𠘺',
  '免' => '免',
  '兔' => '兔',
  '兤' => '兤',
  '具' => '具',
  '𠔜' => '𠔜',
  '㒹' => '㒹',
  '內' => '內',
  '再' => '再',
  '𠕋' => '𠕋',
  '冗' => '冗',
  '冤' => '冤',
  '仌' => '仌',
  '冬' => '冬',
  '况' => '况',
  '𩇟' => '𩇟',
  '凵' => '凵',
  '刃' => '刃',
  '㓟' => '㓟',
  '刻' => '刻',
  '剆' => '剆',
  '割' => '割',
  '剷' => '剷',
  '㔕' => '㔕',
  '勇' => '勇',
  '勉' => '勉',
  '勤' => '勤',
  '勺' => '勺',
  '包' => '包',
  '匆' => '匆',
  '北' => '北',
  '卉' => '卉',
  '卑' => '卑',
  '博' => '博',
  '即' => '即',
  '卽' => '卽',
  '卿' => '卿',
  '卿' => '卿',
  '卿' => '卿',
  '𠨬' => '𠨬',
  '灰' => '灰',
  '及' => '及',
  '叟' => '叟',
  '𠭣' => '𠭣',
  '叫' => '叫',
  '叱' => '叱',
  '吆' => '吆',
  '咞' => '咞',
  '吸' => '吸',
  '呈' => '呈',
  '周' => '周',
  '咢' => '咢',
  '哶' => '哶',
  '唐' => '唐',
  '啓' => '啓',
  '啣' => '啣',
  '善' => '善',
  '善' => '善',
  '喙' => '喙',
  '喫' => '喫',
  '喳' => '喳',
  '嗂' => '嗂',
  '圖' => '圖',
  '嘆' => '嘆',
  '圗' => '圗',
  '噑' => '噑',
  '噴' => '噴',
  '切' => '切',
  '壮' => '壮',
  '城' => '城',
  '埴' => '埴',
  '堍' => '堍',
  '型' => '型',
  '堲' => '堲',
  '報' => '報',
  '墬' => '墬',
  '𡓤' => '𡓤',
  '売' => '売',
  '壷' => '壷',
  '夆' => '夆',
  '多' => '多',
  '夢' => '夢',
  '奢' => '奢',
  '𡚨' => '𡚨',
  '𡛪' => '𡛪',
  '姬' => '姬',
  '娛' => '娛',
  '娧' => '娧',
  '姘' => '姘',
  '婦' => '婦',
  '㛮' => '㛮',
  '㛼' => '㛼',
  '嬈' => '嬈',
  '嬾' => '嬾',
  '嬾' => '嬾',
  '𡧈' => '𡧈',
  '寃' => '寃',
  '寘' => '寘',
  '寧' => '寧',
  '寳' => '寳',
  '𡬘' => '𡬘',
  '寿' => '寿',
  '将' => '将',
  '当' => '当',
  '尢' => '尢',
  '㞁' => '㞁',
  '屠' => '屠',
  '屮' => '屮',
  '峀' => '峀',
  '岍' => '岍',
  '𡷤' => '𡷤',
  '嵃' => '嵃',
  '𡷦' => '𡷦',
  '嵮' => '嵮',
  '嵫' => '嵫',
  '嵼' => '嵼',
  '巡' => '巡',
  '巢' => '巢',
  '㠯' => '㠯',
  '巽' => '巽',
  '帨' => '帨',
  '帽' => '帽',
  '幩' => '幩',
  '㡢' => '㡢',
  '𢆃' => '𢆃',
  '㡼' => '㡼',
  '庰' => '庰',
  '庳' => '庳',
  '庶' => '庶',
  '廊' => '廊',
  '𪎒' => '𪎒',
  '廾' => '廾',
  '𢌱' => '𢌱',
  '𢌱' => '𢌱',
  '舁' => '舁',
  '弢' => '弢',
  '弢' => '弢',
  '㣇' => '㣇',
  '𣊸' => '𣊸',
  '𦇚' => '𦇚',
  '形' => '形',
  '彫' => '彫',
  '㣣' => '㣣',
  '徚' => '徚',
  '忍' => '忍',
  '志' => '志',
  '忹' => '忹',
  '悁' => '悁',
  '㤺' => '㤺',
  '㤜' => '㤜',
  '悔' => '悔',
  '𢛔' => '𢛔',
  '惇' => '惇',
  '慈' => '慈',
  '慌' => '慌',
  '慎' => '慎',
  '慌' => '慌',
  '慺' => '慺',
  '憎' => '憎',
  '憲' => '憲',
  '憤' => '憤',
  '憯' => '憯',
  '懞' => '懞',
  '懲' => '懲',
  '懶' => '懶',
  '成' => '成',
  '戛' => '戛',
  '扝' => '扝',
  '抱' => '抱',
  '拔' => '拔',
  '捐' => '捐',
  '𢬌' => '𢬌',
  '挽' => '挽',
  '拼' => '拼',
  '捨' => '捨',
  '掃' => '掃',
  '揤' => '揤',
  '𢯱' => '𢯱',
  '搢' => '搢',
  '揅' => '揅',
  '掩' => '掩',
  '㨮' => '㨮',
  '摩' => '摩',
  '摾' => '摾',
  '撝' => '撝',
  '摷' => '摷',
  '㩬' => '㩬',
  '敏' => '敏',
  '敬' => '敬',
  '𣀊' => '𣀊',
  '旣' => '旣',
  '書' => '書',
  '晉' => '晉',
  '㬙' => '㬙',
  '暑' => '暑',
  '㬈' => '㬈',
  '㫤' => '㫤',
  '冒' => '冒',
  '冕' => '冕',
  '最' => '最',
  '暜' => '暜',
  '肭' => '肭',
  '䏙' => '䏙',
  '朗' => '朗',
  '望' => '望',
  '朡' => '朡',
  '杞' => '杞',
  '杓' => '杓',
  '𣏃' => '𣏃',
  '㭉' => '㭉',
  '柺' => '柺',
  '枅' => '枅',
  '桒' => '桒',
  '梅' => '梅',
  '𣑭' => '𣑭',
  '梎' => '梎',
  '栟' => '栟',
  '椔' => '椔',
  '㮝' => '㮝',
  '楂' => '楂',
  '榣' => '榣',
  '槪' => '槪',
  '檨' => '檨',
  '𣚣' => '𣚣',
  '櫛' => '櫛',
  '㰘' => '㰘',
  '次' => '次',
  '𣢧' => '𣢧',
  '歔' => '歔',
  '㱎' => '㱎',
  '歲' => '歲',
  '殟' => '殟',
  '殺' => '殺',
  '殻' => '殻',
  '𣪍' => '𣪍',
  '𡴋' => '𡴋',
  '𣫺' => '𣫺',
  '汎' => '汎',
  '𣲼' => '𣲼',
  '沿' => '沿',
  '泍' => '泍',
  '汧' => '汧',
  '洖' => '洖',
  '派' => '派',
  '海' => '海',
  '流' => '流',
  '浩' => '浩',
  '浸' => '浸',
  '涅' => '涅',
  '𣴞' => '𣴞',
  '洴' => '洴',
  '港' => '港',
  '湮' => '湮',
  '㴳' => '㴳',
  '滋' => '滋',
  '滇' => '滇',
  '𣻑' => '𣻑',
  '淹' => '淹',
  '潮' => '潮',
  '𣽞' => '𣽞',
  '𣾎' => '𣾎',
  '濆' => '濆',
  '瀹' => '瀹',
  '瀞' => '瀞',
  '瀛' => '瀛',
  '㶖' => '㶖',
  '灊' => '灊',
  '災' => '災',
  '灷' => '灷',
  '炭' => '炭',
  '𠔥' => '𠔥',
  '煅' => '煅',
  '𤉣' => '𤉣',
  '熜' => '熜',
  '𤎫' => '𤎫',
  '爨' => '爨',
  '爵' => '爵',
  '牐' => '牐',
  '𤘈' => '𤘈',
  '犀' => '犀',
  '犕' => '犕',
  '𤜵' => '𤜵',
  '𤠔' => '𤠔',
  '獺' => '獺',
  '王' => '王',
  '㺬' => '㺬',
  '玥' => '玥',
  '㺸' => '㺸',
  '㺸' => '㺸',
  '瑇' => '瑇',
  '瑜' => '瑜',
  '瑱' => '瑱',
  '璅' => '璅',
  '瓊' => '瓊',
  '㼛' => '㼛',
  '甤' => '甤',
  '𤰶' => '𤰶',
  '甾' => '甾',
  '𤲒' => '𤲒',
  '異' => '異',
  '𢆟' => '𢆟',
  '瘐' => '瘐',
  '𤾡' => '𤾡',
  '𤾸' => '𤾸',
  '𥁄' => '𥁄',
  '㿼' => '㿼',
  '䀈' => '䀈',
  '直' => '直',
  '𥃳' => '𥃳',
  '𥃲' => '𥃲',
  '𥄙' => '𥄙',
  '𥄳' => '𥄳',
  '眞' => '眞',
  '真' => '真',
  '真' => '真',
  '睊' => '睊',
  '䀹' => '䀹',
  '瞋' => '瞋',
  '䁆' => '䁆',
  '䂖' => '䂖',
  '𥐝' => '𥐝',
  '硎' => '硎',
  '碌' => '碌',
  '磌' => '磌',
  '䃣' => '䃣',
  '𥘦' => '𥘦',
  '祖' => '祖',
  '𥚚' => '𥚚',
  '𥛅' => '𥛅',
  '福' => '福',
  '秫' => '秫',
  '䄯' => '䄯',
  '穀' => '穀',
  '穊' => '穊',
  '穏' => '穏',
  '𥥼' => '𥥼',
  '𥪧' => '𥪧',
  '𥪧' => '𥪧',
  '竮' => '竮',
  '䈂' => '䈂',
  '𥮫' => '𥮫',
  '篆' => '篆',
  '築' => '築',
  '䈧' => '䈧',
  '𥲀' => '𥲀',
  '糒' => '糒',
  '䊠' => '䊠',
  '糨' => '糨',
  '糣' => '糣',
  '紀' => '紀',
  '𥾆' => '𥾆',
  '絣' => '絣',
  '䌁' => '䌁',
  '緇' => '緇',
  '縂' => '縂',
  '繅' => '繅',
  '䌴' => '䌴',
  '𦈨' => '𦈨',
  '𦉇' => '𦉇',
  '䍙' => '䍙',
  '𦋙' => '𦋙',
  '罺' => '罺',
  '𦌾' => '𦌾',
  '羕' => '羕',
  '翺' => '翺',
  '者' => '者',
  '𦓚' => '𦓚',
  '𦔣' => '𦔣',
  '聠' => '聠',
  '𦖨' => '𦖨',
  '聰' => '聰',
  '𣍟' => '𣍟',
  '䏕' => '䏕',
  '育' => '育',
  '脃' => '脃',
  '䐋' => '䐋',
  '脾' => '脾',
  '媵' => '媵',
  '𦞧' => '𦞧',
  '𦞵' => '𦞵',
  '𣎓' => '𣎓',
  '𣎜' => '𣎜',
  '舁' => '舁',
  '舄' => '舄',
  '辞' => '辞',
  '䑫' => '䑫',
  '芑' => '芑',
  '芋' => '芋',
  '芝' => '芝',
  '劳' => '劳',
  '花' => '花',
  '芳' => '芳',
  '芽' => '芽',
  '苦' => '苦',
  '𦬼' => '𦬼',
  '若' => '若',
  '茝' => '茝',
  '荣' => '荣',
  '莭' => '莭',
  '茣' => '茣',
  '莽' => '莽',
  '菧' => '菧',
  '著' => '著',
  '荓' => '荓',
  '菊' => '菊',
  '菌' => '菌',
  '菜' => '菜',
  '𦰶' => '𦰶',
  '𦵫' => '𦵫',
  '𦳕' => '𦳕',
  '䔫' => '䔫',
  '蓱' => '蓱',
  '蓳' => '蓳',
  '蔖' => '蔖',
  '𧏊' => '𧏊',
  '蕤' => '蕤',
  '𦼬' => '𦼬',
  '䕝' => '䕝',
  '䕡' => '䕡',
  '𦾱' => '𦾱',
  '𧃒' => '𧃒',
  '䕫' => '䕫',
  '虐' => '虐',
  '虜' => '虜',
  '虧' => '虧',
  '虩' => '虩',
  '蚩' => '蚩',
  '蚈' => '蚈',
  '蜎' => '蜎',
  '蛢' => '蛢',
  '蝹' => '蝹',
  '蜨' => '蜨',
  '蝫' => '蝫',
  '螆' => '螆',
  '䗗' => '䗗',
  '蟡' => '蟡',
  '蠁' => '蠁',
  '䗹' => '䗹',
  '衠' => '衠',
  '衣' => '衣',
  '𧙧' => '𧙧',
  '裗' => '裗',
  '裞' => '裞',
  '䘵' => '䘵',
  '裺' => '裺',
  '㒻' => '㒻',
  '𧢮' => '𧢮',
  '𧥦' => '𧥦',
  '䚾' => '䚾',
  '䛇' => '䛇',
  '誠' => '誠',
  '諭' => '諭',
  '變' => '變',
  '豕' => '豕',
  '𧲨' => '𧲨',
  '貫' => '貫',
  '賁' => '賁',
  '贛' => '贛',
  '起' => '起',
  '𧼯' => '𧼯',
  '𠠄' => '𠠄',
  '跋' => '跋',
  '趼' => '趼',
  '跰' => '跰',
  '𠣞' => '𠣞',
  '軔' => '軔',
  '輸' => '輸',
  '𨗒' => '𨗒',
  '𨗭' => '𨗭',
  '邔' => '邔',
  '郱' => '郱',
  '鄑' => '鄑',
  '𨜮' => '𨜮',
  '鄛' => '鄛',
  '鈸' => '鈸',
  '鋗' => '鋗',
  '鋘' => '鋘',
  '鉼' => '鉼',
  '鏹' => '鏹',
  '鐕' => '鐕',
  '𨯺' => '𨯺',
  '開' => '開',
  '䦕' => '䦕',
  '閷' => '閷',
  '𨵷' => '𨵷',
  '䧦' => '䧦',
  '雃' => '雃',
  '嶲' => '嶲',
  '霣' => '霣',
  '𩅅' => '𩅅',
  '𩈚' => '𩈚',
  '䩮' => '䩮',
  '䩶' => '䩶',
  '韠' => '韠',
  '𩐊' => '𩐊',
  '䪲' => '䪲',
  '𩒖' => '𩒖',
  '頋' => '頋',
  '頋' => '頋',
  '頩' => '頩',
  '𩖶' => '𩖶',
  '飢' => '飢',
  '䬳' => '䬳',
  '餩' => '餩',
  '馧' => '馧',
  '駂' => '駂',
  '駾' => '駾',
  '䯎' => '䯎',
  '𩬰' => '𩬰',
  '鬒' => '鬒',
  '鱀' => '鱀',
  '鳽' => '鳽',
  '䳎' => '䳎',
  '䳭' => '䳭',
  '鵧' => '鵧',
  '𪃎' => '𪃎',
  '䳸' => '䳸',
  '𪄅' => '𪄅',
  '𪈎' => '𪈎',
  '𪊑' => '𪊑',
  '麻' => '麻',
  '䵖' => '䵖',
  '黹' => '黹',
  '黾' => '黾',
  '鼅' => '鼅',
  '鼏' => '鼏',
  '鼖' => '鼖',
  '鼻' => '鼻',
  '𪘀' => '𪘀',
);
<?php

class Normalizer extends Symfony\Polyfill\Intl\Normalizer\Normalizer
{
    /**
     * @deprecated since ICU 56 and removed in PHP 8
     */
    public const NONE = 2;
    public const FORM_D = 4;
    public const FORM_KD = 8;
    public const FORM_C = 16;
    public const FORM_KC = 32;
    public const NFD = 4;
    public const NFKD = 8;
    public const NFC = 16;
    public const NFKC = 32;
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\VarExporter\Exception;

class LogicException extends \LogicException implements ExceptionInterface
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\VarExporter\Exception;

class ClassNotFoundException extends \Exception implements ExceptionInterface
{
    public function __construct(string $class, ?\Throwable $previous = null)
    {
        parent::__construct(\sprintf('Class "%s" not found.', $class), 0, $previous);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\VarExporter\Exception;

interface ExceptionInterface extends \Throwable
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\VarExporter\Exception;

class NotInstantiableTypeException extends \Exception implements ExceptionInterface
{
    public function __construct(string $type, ?\Throwable $previous = null)
    {
        parent::__construct(\sprintf('Type "%s" is not instantiable.', $type), 0, $previous);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\VarExporter\Internal;

use Symfony\Component\Serializer\Attribute\Ignore;

if (\PHP_VERSION_ID >= 80300) {
    /**
     * @internal
     */
    trait LazyObjectTrait
    {
        #[Ignore]
        private readonly LazyObjectState $lazyObjectState;
    }
} else {
    /**
     * @internal
     */
    trait LazyObjectTrait
    {
        #[Ignore]
        private LazyObjectState $lazyObjectState;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\VarExporter\Internal;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @internal
 */
class Values
{
    public function __construct(
        public readonly array $values,
    ) {
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\VarExporter\Internal;

use Symfony\Component\VarExporter\Exception\NotInstantiableTypeException;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @internal
 */
class Exporter
{
    /**
     * Prepares an array of values for VarExporter.
     *
     * For performance this method is public and has no type-hints.
     *
     * @param array             &$values
     * @param \SplObjectStorage $objectsPool
     * @param array             &$refsPool
     * @param int               &$objectsCount
     * @param bool              &$valuesAreStatic
     *
     * @return array
     *
     * @throws NotInstantiableTypeException When a value cannot be serialized
     */
    public static function prepare($values, $objectsPool, &$refsPool, &$objectsCount, &$valuesAreStatic)
    {
        $refs = $values;
        foreach ($values as $k => $value) {
            if (\is_resource($value)) {
                throw new NotInstantiableTypeException(get_resource_type($value).' resource');
            }
            $refs[$k] = $objectsPool;

            if ($isRef = !$valueIsStatic = $values[$k] !== $objectsPool) {
                $values[$k] = &$value; // Break hard references to make $values completely
                unset($value);         // independent from the original structure
                $refs[$k] = $value = $values[$k];
                if ($value instanceof Reference && 0 > $value->id) {
                    $valuesAreStatic = false;
                    ++$value->count;
                    continue;
                }
                $refsPool[] = [&$refs[$k], $value, &$value];
                $refs[$k] = $values[$k] = new Reference(-\count($refsPool), $value);
            }

            if (\is_array($value)) {
                if ($value) {
                    $value = self::prepare($value, $objectsPool, $refsPool, $objectsCount, $valueIsStatic);
                }
                goto handle_value;
            } elseif (!\is_object($value) || $value instanceof \UnitEnum) {
                goto handle_value;
            }

            $valueIsStatic = false;
            if (isset($objectsPool[$value])) {
                ++$objectsCount;
                $value = new Reference($objectsPool[$value][0]);
                goto handle_value;
            }

            if ($value instanceof \Closure && !($r = new \ReflectionFunction($value))->isAnonymous()) {
                $callable = [$r->getClosureThis() ?? $r->getClosureCalledClass()?->name, $r->name];
                $r = $callable[0] ? new \ReflectionMethod(...$callable) : null;
                $value = new NamedClosure(self::prepare($callable, $objectsPool, $refsPool, $objectsCount, $valueIsStatic), $r);

                goto handle_value;
            }

            $class = $value::class;
            $reflector = Registry::$reflectors[$class] ??= Registry::getClassReflector($class);
            $properties = [];
            $sleep = null;
            $proto = Registry::$prototypes[$class];

            if (null === $proto && !$value instanceof \Serializable && method_exists($class, '__unserialize')) {
                // The class cannot be instantiated empty; let serialize()/unserialize()
                // deal with reconstructing the whole value.
                ++$objectsCount;
                $objectsPool[$value] = [$id = \count($objectsPool), serialize($value), [], 0];
                $value = new Reference($id);
                goto handle_value;
            }

            if ($reflector->hasMethod('__serialize')) {
                if (!$reflector->getMethod('__serialize')->isPublic()) {
                    throw new \Error(\sprintf('Call to %s method "%s::__serialize()".', $reflector->getMethod('__serialize')->isProtected() ? 'protected' : 'private', $class));
                }

                if (!\is_array($arrayValue = $value->__serialize())) {
                    throw new \TypeError($class.'::__serialize() must return an array');
                }

                if ($reflector->hasMethod('__unserialize')) {
                    $properties = $arrayValue;
                    goto prepare_value;
                }
            } elseif (($value instanceof \ArrayIterator || $value instanceof \ArrayObject) && null !== $proto) {
                // ArrayIterator and ArrayObject need special care because their "flags"
                // option changes the behavior of the (array) casting operator.
                [$arrayValue, $properties] = self::getArrayObjectProperties($value, $proto);

                // populates Registry::$prototypes[$class] with a new instance
                Registry::getClassReflector($class, Registry::$instantiableWithoutConstructor[$class], Registry::$cloneable[$class]);
            } elseif ($value instanceof \SplObjectStorage && Registry::$cloneable[$class] && null !== $proto) {
                // By implementing Serializable, SplObjectStorage breaks
                // internal references; let's deal with it on our own.
                foreach (clone $value as $v) {
                    $properties[] = $v;
                    $properties[] = $value[$v];
                }
                $properties = ['SplObjectStorage' => ["\0" => $properties]];
                $arrayValue = (array) $value;
            } elseif ($value instanceof \Serializable || $value instanceof \__PHP_Incomplete_Class) {
                ++$objectsCount;
                $objectsPool[$value] = [$id = \count($objectsPool), serialize($value), [], 0];
                $value = new Reference($id);
                goto handle_value;
            } else {
                if (method_exists($class, '__sleep')) {
                    if (!\is_array($sleep = $value->__sleep())) {
                        trigger_error('serialize(): __sleep should return an array only containing the names of instance-variables to serialize', \E_USER_NOTICE);
                        $value = null;
                        goto handle_value;
                    }
                    $sleep = array_flip($sleep);
                }

                $arrayValue = (array) $value;
            }

            $proto = (array) $proto;

            foreach ($arrayValue as $name => $v) {
                $i = 0;
                $n = (string) $name;
                if ('' === $n || "\0" !== $n[0]) {
                    $parent = $reflector;
                    do {
                        $p = $parent->hasProperty($n) ? $parent->getProperty($n) : null;
                    } while (!$p && $parent = $parent->getParentClass());

                    $c = $p && (!$p->isPublic() || (\PHP_VERSION_ID >= 80400 ? $p->isProtectedSet() || $p->isPrivateSet() : $p->isReadOnly())) ? $p->class : 'stdClass';
                } elseif ('*' === $n[1]) {
                    $n = substr($n, 3);
                    $c = $reflector->getProperty($n)->class;
                } else {
                    $i = strpos($n, "\0", 2);
                    $c = substr($n, 1, $i - 1);
                    $n = substr($n, 1 + $i);
                }
                if (null !== $sleep) {
                    if (!isset($sleep[$name]) && (!isset($sleep[$n]) || ($i && $c !== $class))) {
                        unset($arrayValue[$name]);
                        continue;
                    }
                    unset($sleep[$name], $sleep[$n]);
                }
                if ("\x00Error\x00trace" === $name || "\x00Exception\x00trace" === $name) {
                    $properties[$c][$n] = $v;
                } elseif (!\array_key_exists($name, $proto) || $proto[$name] !== $v) {
                    $properties[match ($c) {
                        'Error' => 'TypeError',
                        'Exception' => 'ErrorException',
                        default => $c,
                    }][$n] = $v;
                }
            }
            if ($sleep) {
                foreach ($sleep as $n => $v) {
                    if (\is_string($n) && $reflector->hasProperty($n)) {
                        continue;
                    }
                    trigger_error(\sprintf('serialize(): "%s" returned as member variable from __sleep() but does not exist', $n), \E_USER_NOTICE);
                }
            }
            if (method_exists($class, '__unserialize')) {
                $properties = $arrayValue;
            }

            prepare_value:
            $objectsPool[$value] = [$id = \count($objectsPool)];
            $properties = self::prepare($properties, $objectsPool, $refsPool, $objectsCount, $valueIsStatic);
            ++$objectsCount;
            $objectsPool[$value] = [$id, $class, $properties, method_exists($class, '__unserialize') ? -$objectsCount : (method_exists($class, '__wakeup') ? $objectsCount : 0)];

            $value = new Reference($id);

            handle_value:
            if ($isRef) {
                unset($value); // Break the hard reference created above
            } elseif (!$valueIsStatic) {
                $values[$k] = $value;
            }
            $valuesAreStatic = $valueIsStatic && $valuesAreStatic;
        }

        return $values;
    }

    public static function export($value, $indent = '')
    {
        switch (true) {
            case \is_int($value) || \is_float($value): return var_export($value, true);
            case [] === $value: return '[]';
            case false === $value: return 'false';
            case true === $value: return 'true';
            case null === $value: return 'null';
            case '' === $value: return "''";
            case $value instanceof \UnitEnum: return '\\'.ltrim(var_export($value, true), '\\');
        }

        if ($value instanceof Reference) {
            if (0 <= $value->id) {
                return '$o['.$value->id.']';
            }
            if (!$value->count) {
                return self::export($value->value, $indent);
            }
            $value = -$value->id;

            return '&$r['.$value.']';
        }
        $subIndent = $indent.'    ';

        if ($value instanceof NamedClosure) {
            if ($value->method?->isPublic() ?? true) {
                return match (true) {
                    null === $value->callable[0] => '\\'.$value->callable[1],
                    \is_string($value->callable[0]) => '\\'.$value->callable[0].'::'.$value->callable[1],
                    \is_object($value->callable[0]) => self::export($value->callable[0], $subIndent).'->'.$value->callable[1],
                }.'(...)';
            }

            return 'new \ReflectionMethod(\\'.$value->method->class.'::class, '.self::export($value->callable[1]).')'
                .'->getClosure('.(\is_object($value->callable[0]) ? self::export($value->callable[0]) : '').')';
        }

        if (\is_string($value)) {
            $code = \sprintf("'%s'", addcslashes($value, "'\\"));

            $code = preg_replace_callback("/((?:[\\0\\r\\n]|\u{202A}|\u{202B}|\u{202D}|\u{202E}|\u{2066}|\u{2067}|\u{2068}|\u{202C}|\u{2069})++)(.)/", static function ($m) use ($subIndent) {
                $m[1] = \sprintf('\'."%s".\'', str_replace(
                    ["\0", "\r", "\n", "\u{202A}", "\u{202B}", "\u{202D}", "\u{202E}", "\u{2066}", "\u{2067}", "\u{2068}", "\u{202C}", "\u{2069}", '\n\\'],
                    ['\0', '\r', '\n', '\u{202A}', '\u{202B}', '\u{202D}', '\u{202E}', '\u{2066}', '\u{2067}', '\u{2068}', '\u{202C}', '\u{2069}', '\n"'."\n".$subIndent.'."\\'],
                    $m[1]
                ));

                if ("'" === $m[2]) {
                    return substr($m[1], 0, -2);
                }

                if (str_ends_with($m[1], 'n".\'')) {
                    return substr_replace($m[1], "\n".$subIndent.".'".$m[2], -2);
                }

                return $m[1].$m[2];
            }, $code, -1, $count);

            if ($count && str_starts_with($code, "''.")) {
                $code = substr($code, 3);
            }

            return $code;
        }

        if (\is_array($value)) {
            $j = -1;
            $code = '';
            foreach ($value as $k => $v) {
                $code .= $subIndent;
                if (!\is_int($k) || 1 !== $k - $j) {
                    $code .= self::export($k, $subIndent).' => ';
                }
                if (\is_int($k) && $k > $j) {
                    $j = $k;
                }
                $code .= self::export($v, $subIndent).",\n";
            }

            return "[\n".$code.$indent.']';
        }

        if ($value instanceof Values) {
            $code = $subIndent."\$r = [],\n";
            foreach ($value->values as $k => $v) {
                $code .= $subIndent.'$r['.$k.'] = '.self::export($v, $subIndent).",\n";
            }

            return "[\n".$code.$indent.']';
        }

        if ($value instanceof Registry) {
            return self::exportRegistry($value, $indent, $subIndent);
        }

        if ($value instanceof Hydrator) {
            return self::exportHydrator($value, $indent, $subIndent);
        }

        throw new \UnexpectedValueException(\sprintf('Cannot export value of type "%s".', get_debug_type($value)));
    }

    private static function exportRegistry(Registry $value, string $indent, string $subIndent): string
    {
        $code = '';
        $serializables = [];
        $seen = [];
        $prototypesAccess = 0;
        $factoriesAccess = 0;
        $r = '\\'.Registry::class;
        $j = -1;

        foreach ($value->classes as $k => $class) {
            if (':' === ($class[1] ?? null)) {
                $serializables[$k] = $class;
                continue;
            }
            if (!Registry::$instantiableWithoutConstructor[$class]) {
                if (is_subclass_of($class, 'Serializable') && !method_exists($class, '__unserialize')) {
                    $serializables[$k] = 'C:'.\strlen($class).':"'.$class.'":0:{}';
                } else {
                    $serializables[$k] = 'O:'.\strlen($class).':"'.$class.'":0:{}';
                }
                if (is_subclass_of($class, 'Throwable')) {
                    $eol = is_subclass_of($class, 'Error') ? "\0Error\0" : "\0Exception\0";
                    $serializables[$k] = substr_replace($serializables[$k], '1:{s:'.(5 + \strlen($eol)).':"'.$eol.'trace";a:0:{}}', -4);
                }
                continue;
            }
            $code .= $subIndent.(1 !== $k - $j ? $k.' => ' : '');
            $j = $k;
            $eol = ",\n";
            $c = '['.self::export($class).']';

            if ($seen[$class] ?? false) {
                if (Registry::$cloneable[$class]) {
                    ++$prototypesAccess;
                    $code .= 'clone $p'.$c;
                } else {
                    ++$factoriesAccess;
                    $code .= '$f'.$c.'()';
                }
            } else {
                $seen[$class] = true;
                if (Registry::$cloneable[$class]) {
                    $code .= 'clone ('.($prototypesAccess++ ? '$p' : '($p = &'.$r.'::$prototypes)').$c.' ?? '.$r.'::p';
                } else {
                    $code .= '('.($factoriesAccess++ ? '$f' : '($f = &'.$r.'::$factories)').$c.' ?? '.$r.'::f';
                    $eol = '()'.$eol;
                }
                $code .= '('.substr($c, 1, -1).'))';
            }
            $code .= $eol;
        }

        if (1 === $prototypesAccess) {
            $code = str_replace('($p = &'.$r.'::$prototypes)', $r.'::$prototypes', $code);
        }
        if (1 === $factoriesAccess) {
            $code = str_replace('($f = &'.$r.'::$factories)', $r.'::$factories', $code);
        }
        if ('' !== $code) {
            $code = "\n".$code.$indent;
        }

        if ($serializables) {
            $code = $r.'::unserialize(['.$code.'], '.self::export($serializables, $indent).')';
        } else {
            $code = '['.$code.']';
        }

        return '$o = '.$code;
    }

    private static function exportHydrator(Hydrator $value, string $indent, string $subIndent): string
    {
        $code = '';
        foreach ($value->properties as $class => $properties) {
            $code .= $subIndent.'    '.self::export($class).' => '.self::export($properties, $subIndent.'    ').",\n";
        }

        $code = [
            self::export($value->registry, $subIndent),
            self::export($value->values, $subIndent),
            '' !== $code ? "[\n".$code.$subIndent.']' : '[]',
            self::export($value->value, $subIndent),
            self::export($value->wakeups, $subIndent),
        ];

        return '\\'.$value::class."::hydrate(\n".$subIndent.implode(",\n".$subIndent, $code)."\n".$indent.')';
    }

    /**
     * @param \ArrayIterator|\ArrayObject $value
     * @param \ArrayIterator|\ArrayObject $proto
     */
    private static function getArrayObjectProperties($value, $proto): array
    {
        $reflector = $value instanceof \ArrayIterator ? 'ArrayIterator' : 'ArrayObject';
        $reflector = Registry::$reflectors[$reflector] ??= Registry::getClassReflector($reflector);

        $properties = [
            $arrayValue = (array) $value,
            $reflector->getMethod('getFlags')->invoke($value),
            $value instanceof \ArrayObject ? $reflector->getMethod('getIteratorClass')->invoke($value) : 'ArrayIterator',
        ];

        $reflector = $reflector->getMethod('setFlags');
        $reflector->invoke($proto, \ArrayObject::STD_PROP_LIST);

        if ($properties[1] & \ArrayObject::STD_PROP_LIST) {
            $reflector->invoke($value, 0);
            $properties[0] = (array) $value;
        } else {
            $reflector->invoke($value, \ArrayObject::STD_PROP_LIST);
            $arrayValue = (array) $value;
        }
        $reflector->invoke($value, $properties[1]);

        if ([[], 0, 'ArrayIterator'] === $properties) {
            $properties = [];
        } else {
            if ('ArrayIterator' === $properties[2]) {
                unset($properties[2]);
            }
            $properties = [$reflector->class => ["\0" => $properties]];
        }

        return [$arrayValue, $properties];
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\VarExporter\Internal;

use Symfony\Component\VarExporter\Exception\ClassNotFoundException;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @internal
 */
class Hydrator
{
    public const PROPERTY_HAS_HOOKS = 1;
    public const PROPERTY_NOT_BY_REF = 2;

    public static array $hydrators = [];
    public static array $simpleHydrators = [];
    public static array $propertyScopes = [];

    public function __construct(
        public readonly Registry $registry,
        public readonly ?Values $values,
        public readonly array $properties,
        public readonly mixed $value,
        public readonly array $wakeups,
    ) {
    }

    public static function hydrate($objects, $values, $properties, $value, $wakeups)
    {
        foreach ($properties as $class => $vars) {
            (self::$hydrators[$class] ??= self::getHydrator($class))($vars, $objects);
        }
        foreach ($wakeups as $k => $v) {
            if (\is_array($v)) {
                $objects[-$k]->__unserialize($v);
            } else {
                $objects[$v]->__wakeup();
            }
        }

        return $value;
    }

    public static function getHydrator($class)
    {
        $baseHydrator = self::$hydrators['stdClass'] ??= static function ($properties, $objects) {
            foreach ($properties as $name => $values) {
                foreach ($values as $i => $v) {
                    $objects[$i]->$name = $v;
                }
            }
        };

        switch ($class) {
            case 'stdClass':
                return $baseHydrator;

            case 'ErrorException':
                return $baseHydrator->bindTo(null, new class extends \ErrorException {
                });

            case 'TypeError':
                return $baseHydrator->bindTo(null, new class extends \Error {
                });

            case 'SplObjectStorage':
                return static function ($properties, $objects) {
                    foreach ($properties as $name => $values) {
                        if ("\0" === $name) {
                            foreach ($values as $i => $v) {
                                for ($j = 0; $j < \count($v); ++$j) {
                                    $objects[$i][$v[$j]] = $v[++$j];
                                }
                            }
                            continue;
                        }
                        foreach ($values as $i => $v) {
                            $objects[$i]->$name = $v;
                        }
                    }
                };
        }

        if (!class_exists($class) && !interface_exists($class, false) && !trait_exists($class, false)) {
            throw new ClassNotFoundException($class);
        }
        $classReflector = new \ReflectionClass($class);

        switch ($class) {
            case 'ArrayIterator':
            case 'ArrayObject':
                $constructor = $classReflector->getConstructor()->invokeArgs(...);

                return static function ($properties, $objects) use ($constructor) {
                    foreach ($properties as $name => $values) {
                        if ("\0" !== $name) {
                            foreach ($values as $i => $v) {
                                $objects[$i]->$name = $v;
                            }
                        }
                    }
                    foreach ($properties["\0"] ?? [] as $i => $v) {
                        $constructor($objects[$i], $v);
                    }
                };
        }

        if (!$classReflector->isInternal()) {
            return $baseHydrator->bindTo(null, $class);
        }

        if ($classReflector->name !== $class) {
            return self::$hydrators[$classReflector->name] ??= self::getHydrator($classReflector->name);
        }

        $propertySetters = [];
        foreach ($classReflector->getProperties() as $propertyReflector) {
            if (!$propertyReflector->isStatic()) {
                $propertySetters[$propertyReflector->name] = $propertyReflector->setValue(...);
            }
        }

        if (!$propertySetters) {
            return $baseHydrator;
        }

        return static function ($properties, $objects) use ($propertySetters) {
            foreach ($properties as $name => $values) {
                if ($setValue = $propertySetters[$name] ?? null) {
                    foreach ($values as $i => $v) {
                        $setValue($objects[$i], $v);
                    }
                    continue;
                }
                foreach ($values as $i => $v) {
                    $objects[$i]->$name = $v;
                }
            }
        };
    }

    public static function getSimpleHydrator($class)
    {
        $baseHydrator = self::$simpleHydrators['stdClass'] ??= (function ($properties, $object) {
            $notByRef = (array) $this;

            foreach ($properties as $name => &$value) {
                if (!$noRef = $notByRef[$name] ?? false) {
                    $object->$name = $value;
                    $object->$name = &$value;
                } elseif (true !== $noRef) {
                    $noRef($object, $value);
                } else {
                    $object->$name = $value;
                }
            }
        })->bindTo(new \stdClass());

        switch ($class) {
            case 'stdClass':
                return $baseHydrator;

            case 'ErrorException':
                return $baseHydrator->bindTo(new \stdClass(), new class extends \ErrorException {
                });

            case 'TypeError':
                return $baseHydrator->bindTo(new \stdClass(), new class extends \Error {
                });

            case 'SplObjectStorage':
                return static function ($properties, $object) {
                    foreach ($properties as $name => &$value) {
                        if ("\0" !== $name) {
                            $object->$name = $value;
                            $object->$name = &$value;
                            continue;
                        }
                        for ($i = 0; $i < \count($value); ++$i) {
                            $object[$value[$i]] = $value[++$i];
                        }
                    }
                };
        }

        if (!class_exists($class) && !interface_exists($class, false) && !trait_exists($class, false)) {
            throw new ClassNotFoundException($class);
        }
        $classReflector = new \ReflectionClass($class);

        switch ($class) {
            case 'ArrayIterator':
            case 'ArrayObject':
                $constructor = $classReflector->getConstructor()->invokeArgs(...);

                return static function ($properties, $object) use ($constructor) {
                    foreach ($properties as $name => &$value) {
                        if ("\0" === $name) {
                            $constructor($object, $value);
                        } else {
                            $object->$name = $value;
                            $object->$name = &$value;
                        }
                    }
                };
        }

        if (!$classReflector->isInternal()) {
            $notByRef = new \stdClass();
            foreach ($classReflector->getProperties() as $propertyReflector) {
                if ($propertyReflector->isStatic()) {
                    continue;
                }
                if (\PHP_VERSION_ID >= 80400 && !$propertyReflector->isAbstract() && $propertyReflector->getHooks()) {
                    $notByRef->{$propertyReflector->name} = $propertyReflector->setRawValue(...);
                } elseif ($propertyReflector->isReadOnly()) {
                    $notByRef->{$propertyReflector->name} = static function ($object, $value) use ($propertyReflector) {
                        if (!$propertyReflector->isInitialized($object)) {
                            $propertyReflector->setValue($object, $value);
                        }
                    };
                }
            }

            return $baseHydrator->bindTo($notByRef, $class);
        }

        if ($classReflector->name !== $class) {
            return self::$simpleHydrators[$classReflector->name] ??= self::getSimpleHydrator($classReflector->name);
        }

        $propertySetters = [];
        foreach ($classReflector->getProperties() as $propertyReflector) {
            if (!$propertyReflector->isStatic()) {
                $propertySetters[$propertyReflector->name] = $propertyReflector->setValue(...);
            }
        }

        if (!$propertySetters) {
            return $baseHydrator;
        }

        return static function ($properties, $object) use ($propertySetters) {
            foreach ($properties as $name => &$value) {
                if ($setValue = $propertySetters[$name] ?? null) {
                    $setValue($object, $value);
                } else {
                    $object->$name = $value;
                    $object->$name = &$value;
                }
            }
        };
    }

    public static function getPropertyScopes($class): array
    {
        $propertyScopes = [];
        $r = new \ReflectionClass($class);

        foreach ($r->getProperties() as $property) {
            $flags = $property->getModifiers();

            if (\ReflectionProperty::IS_STATIC & $flags) {
                continue;
            }
            $name = $property->name;
            $access = ($flags << 2) | ($flags & \ReflectionProperty::IS_READONLY ? self::PROPERTY_NOT_BY_REF : 0);

            if (\PHP_VERSION_ID >= 80400 && !$property->isAbstract() && $h = $property->getHooks()) {
                $access |= self::PROPERTY_HAS_HOOKS | (isset($h['get']) && !$h['get']->returnsReference() ? self::PROPERTY_NOT_BY_REF : 0);
            }

            if (\ReflectionProperty::IS_PRIVATE & $flags) {
                $propertyScopes["\0$class\0$name"] = $propertyScopes[$name] = [$class, $name, null, $access, $property];

                continue;
            }

            $propertyScopes[$name] = [$class, $name, null, $access, $property];

            if ($flags & (\PHP_VERSION_ID >= 80400 ? \ReflectionProperty::IS_PRIVATE_SET : \ReflectionProperty::IS_READONLY)) {
                $propertyScopes[$name][2] = $property->class;
            }

            if (\ReflectionProperty::IS_PROTECTED & $flags) {
                $propertyScopes["\0*\0$name"] = $propertyScopes[$name];
            }
        }

        while ($r = $r->getParentClass()) {
            $class = $r->name;

            foreach ($r->getProperties(\ReflectionProperty::IS_PRIVATE) as $property) {
                $flags = $property->getModifiers();

                if (\ReflectionProperty::IS_STATIC & $flags) {
                    continue;
                }
                $name = $property->name;
                $access = ($flags << 2) | ($flags & \ReflectionProperty::IS_READONLY ? self::PROPERTY_NOT_BY_REF : 0);

                if (\PHP_VERSION_ID >= 80400 && $h = $property->getHooks()) {
                    $access |= self::PROPERTY_HAS_HOOKS | (isset($h['get']) && !$h['get']->returnsReference() ? self::PROPERTY_NOT_BY_REF : 0);
                }

                $propertyScopes["\0$class\0$name"] = [$class, $name, null, $access, $property];
                $propertyScopes[$name] ??= $propertyScopes["\0$class\0$name"];
            }
        }

        return $propertyScopes;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\VarExporter\Internal;

use Symfony\Component\Serializer\Attribute\Ignore;
use Symfony\Component\VarExporter\Internal\LazyObjectRegistry as Registry;

/**
 * @internal
 */
trait LazyDecoratorTrait
{
    #[Ignore]
    private readonly LazyObjectState $lazyObjectState;

    /**
     * Creates a lazy-loading decorator.
     *
     * @param \Closure():object $initializer Returns the proxied object
     * @param static|null       $instance
     */
    public static function createLazyProxy(\Closure $initializer, ?object $instance = null): static
    {
        $class = $instance ? $instance::class : static::class;

        if (self::class === $class && \defined($class.'::LAZY_OBJECT_PROPERTY_SCOPES')) {
            Hydrator::$propertyScopes[$class] ??= $class::LAZY_OBJECT_PROPERTY_SCOPES;
        }

        $instance ??= (Registry::$classReflectors[$class] ??= ($r = new \ReflectionClass($class))->hasProperty('lazyObjectState')
            ? $r
            : throw new \LogicException('Cannot create a lazy proxy for a non-decorator object.')
        )->newInstanceWithoutConstructor();

        $state = $instance->lazyObjectState ??= new LazyObjectState();
        $state->initializer = null;
        unset($state->realInstance);

        foreach (Registry::$classResetters[$class] ??= Registry::getClassResetters($class) as $reset) {
            $reset($instance, []);
        }
        $state->initializer = $initializer;

        return $instance;
    }

    public function __construct(...$args)
    {
        self::createLazyProxy(static fn () => new parent(...$args), $this);
    }

    public function __destruct()
    {
    }

    #[Ignore]
    public function isLazyObjectInitialized(bool $partial = false): bool
    {
        return isset($this->lazyObjectState->realInstance);
    }

    public function initializeLazyObject(): parent
    {
        return $this->lazyObjectState->realInstance;
    }

    public function resetLazyObject(): bool
    {
        if (!isset($this->lazyObjectState->initializer)) {
            return false;
        }
        unset($this->lazyObjectState->realInstance);

        return true;
    }

    public function &__get($name): mixed
    {
        $instance = $this->lazyObjectState->realInstance;
        $class = $this::class;

        $propertyScopes = Hydrator::$propertyScopes[$class] ??= Hydrator::getPropertyScopes($class);
        $notByRef = 0;

        if ([, , , $access] = $propertyScopes[$name] ?? null) {
            $notByRef = $access & Hydrator::PROPERTY_NOT_BY_REF || ($access >> 2) & \ReflectionProperty::IS_PRIVATE_SET;
        }

        if ($notByRef || 2 !== ((Registry::$parentMethods[$class] ??= Registry::getParentMethods($class))['get'] ?: 2)) {
            $value = $instance->$name;

            return $value;
        }

        try {
            return $instance->$name;
        } catch (\Error $e) {
            if (\Error::class !== $e::class || !str_starts_with($e->getMessage(), 'Cannot access uninitialized non-nullable property')) {
                throw $e;
            }

            try {
                $instance->$name = [];

                return $instance->$name;
            } catch (\Error) {
                if (preg_match('/^Cannot access uninitialized non-nullable property ([^ ]++) by reference$/', $e->getMessage(), $matches)) {
                    throw new \Error('Typed property '.$matches[1].' must not be accessed before initialization', $e->getCode(), $e->getPrevious());
                }

                throw $e;
            }
        }
    }

    public function __set($name, $value): void
    {
        $this->lazyObjectState->realInstance->$name = $value;
    }

    public function __isset($name): bool
    {
        return isset($this->lazyObjectState->realInstance->$name);
    }

    public function __unset($name): void
    {
        if ($this->lazyObjectState->initializer) {
            unset($this->lazyObjectState->realInstance->$name);
        }
    }

    public function __serialize(): array
    {
        return [$this->lazyObjectState->realInstance];
    }

    public function __unserialize($data): void
    {
        $this->lazyObjectState = new LazyObjectState();
        $this->lazyObjectState->realInstance = $data[0];
    }

    public function __clone(): void
    {
        $this->lazyObjectState->realInstance; // initialize lazy object
        $this->lazyObjectState = clone $this->lazyObjectState;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\VarExporter\Internal;

/**
 * Stores the state of lazy objects and caches related reflection information.
 *
 * As a micro-optimization, this class uses no type declarations.
 *
 * @internal
 */
class LazyObjectRegistry
{
    /**
     * @var array<class-string, \ReflectionClass>
     */
    public static array $classReflectors = [];

    /**
     * @var array<class-string, array<string, mixed>>
     */
    public static array $defaultProperties = [];

    /**
     * @var array<class-string, list<\Closure>>
     */
    public static array $classResetters = [];

    /**
     * @var array<class-string, array{get: \Closure, set: \Closure, isset: \Closure, unset: \Closure}>
     */
    public static array $classAccessors = [];

    /**
     * @var array<class-string, array{set: bool, isset: bool, unset: bool, clone: bool, serialize: bool, unserialize: bool, sleep: bool, wakeup: bool, destruct: bool, get: int}>
     */
    public static array $parentMethods = [];

    public static ?\Closure $noInitializerState = null;

    public static function getClassResetters($class)
    {
        $classProperties = [];
        $hookedProperties = [];

        if ((self::$classReflectors[$class] ??= new \ReflectionClass($class))->isInternal()) {
            $propertyScopes = [];
        } else {
            $propertyScopes = Hydrator::$propertyScopes[$class] ??= Hydrator::getPropertyScopes($class);
        }

        foreach ($propertyScopes as $key => [$scope, $name, $writeScope, $access]) {
            $propertyScopes[$k = "\0$scope\0$name"] ?? $propertyScopes[$k = "\0*\0$name"] ?? $k = $name;

            if ($k !== $key || "\0$class\0lazyObjectState" === $k) {
                continue;
            }

            if ($access & Hydrator::PROPERTY_HAS_HOOKS) {
                $hookedProperties[$k] = true;
            } else {
                $classProperties[$writeScope ?? $scope][$name] = $key;
            }
        }

        $resetters = [];
        foreach ($classProperties as $scope => $properties) {
            $resetters[] = \Closure::bind(static function ($instance, $skippedProperties) use ($properties) {
                foreach ($properties as $name => $key) {
                    if (!\array_key_exists($key, $skippedProperties)) {
                        unset($instance->$name);
                    }
                }
            }, null, $scope);
        }

        return $resetters;
    }

    public static function getClassAccessors($class)
    {
        return \Closure::bind(static fn () => [
            'get' => static function &($instance, $name, $notByRef) {
                if (!$notByRef) {
                    return $instance->$name;
                }
                $value = $instance->$name;

                return $value;
            },
            'set' => static function ($instance, $name, $value) {
                $instance->$name = $value;
            },
            'isset' => static fn ($instance, $name) => isset($instance->$name),
            'unset' => static function ($instance, $name) {
                unset($instance->$name);
            },
        ], null, \Closure::class === $class ? null : $class)();
    }

    public static function getParentMethods($class)
    {
        $parent = get_parent_class($class);
        $methods = [];

        foreach (['set', 'isset', 'unset', 'clone', 'serialize', 'unserialize', 'sleep', 'wakeup', 'destruct', 'get'] as $method) {
            if (!$parent || !method_exists($parent, '__'.$method)) {
                $methods[$method] = false;
            } else {
                $m = new \ReflectionMethod($parent, '__'.$method);
                $methods[$method] = !$m->isAbstract() && !$m->isPrivate();
            }
        }

        $methods['get'] = $methods['get'] ? ($m->returnsReference() ? 2 : 1) : 0;

        return $methods;
    }

    public static function getScopeForRead($propertyScopes, $class, $property)
    {
        if (!isset($propertyScopes[$k = "\0$class\0$property"]) && !isset($propertyScopes[$k = "\0*\0$property"])) {
            return null;
        }
        $frame = debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT | \DEBUG_BACKTRACE_IGNORE_ARGS, 3)[2];

        if (\ReflectionProperty::class === $scope = $frame['class'] ?? \Closure::class) {
            $scope = $frame['object']->class;
        }
        if ('*' === $k[1] && ($class === $scope || (is_subclass_of($class, $scope) && !isset($propertyScopes["\0$scope\0$property"])))) {
            return null;
        }

        return $scope;
    }

    public static function getScopeForWrite($propertyScopes, $class, $property, $flags)
    {
        if (!($flags & (\ReflectionProperty::IS_PRIVATE | \ReflectionProperty::IS_PROTECTED | \ReflectionProperty::IS_READONLY | (\PHP_VERSION_ID >= 80400 ? \ReflectionProperty::IS_PRIVATE_SET | \ReflectionProperty::IS_PROTECTED_SET : 0)))) {
            return null;
        }
        $frame = debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT | \DEBUG_BACKTRACE_IGNORE_ARGS, 3)[2];

        if (\ReflectionProperty::class === $scope = $frame['class'] ?? \Closure::class) {
            $scope = $frame['object']->class;
        }
        if ($flags & (\ReflectionProperty::IS_PRIVATE | (\PHP_VERSION_ID >= 80400 ? \ReflectionProperty::IS_PRIVATE_SET : \ReflectionProperty::IS_READONLY))) {
            return $scope;
        }
        if ($flags & (\ReflectionProperty::IS_PROTECTED | (\PHP_VERSION_ID >= 80400 ? \ReflectionProperty::IS_PROTECTED_SET : 0)) && ($class === $scope || (is_subclass_of($class, $scope) && !isset($propertyScopes["\0$scope\0$property"])))) {
            return null;
        }

        return $scope;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\VarExporter\Internal;

use Symfony\Component\VarExporter\Hydrator as PublicHydrator;

/**
 * Keeps the state of lazy objects.
 *
 * As a micro-optimization, this class uses no type declarations.
 *
 * @internal
 */
class LazyObjectState
{
    public const STATUS_UNINITIALIZED_FULL = 1;
    public const STATUS_UNINITIALIZED_PARTIAL = 2;
    public const STATUS_INITIALIZED_FULL = 3;
    public const STATUS_INITIALIZED_PARTIAL = 4;

    /**
     * @var self::STATUS_*
     */
    public int $status = self::STATUS_UNINITIALIZED_FULL;

    public object $realInstance;
    public object $cloneInstance;

    /**
     * @param array<string, true> $skippedProperties
     */
    public function __construct(
        public ?\Closure $initializer = null,
        public array $skippedProperties = [],
    ) {
    }

    public function initialize($instance, $propertyName, $writeScope)
    {
        if (self::STATUS_UNINITIALIZED_FULL !== $this->status) {
            return $this->status;
        }

        $this->status = self::STATUS_INITIALIZED_PARTIAL;

        try {
            if ($defaultProperties = array_diff_key(LazyObjectRegistry::$defaultProperties[$instance::class], $this->skippedProperties)) {
                PublicHydrator::hydrate($instance, $defaultProperties);
            }

            ($this->initializer)($instance);
        } catch (\Throwable $e) {
            $this->status = self::STATUS_UNINITIALIZED_FULL;
            $this->reset($instance);

            throw $e;
        }

        return $this->status = self::STATUS_INITIALIZED_FULL;
    }

    public function reset($instance): void
    {
        $class = $instance::class;
        $propertyScopes = Hydrator::$propertyScopes[$class] ??= Hydrator::getPropertyScopes($class);
        $skippedProperties = $this->skippedProperties;
        $properties = (array) $instance;

        foreach ($propertyScopes as $key => [$scope, $name, , $access]) {
            $propertyScopes[$k = "\0$scope\0$name"] ?? $propertyScopes[$k = "\0*\0$name"] ?? $k = $name;

            if ($k === $key && ($access & Hydrator::PROPERTY_HAS_HOOKS || ($access >> 2) & \ReflectionProperty::IS_READONLY || !\array_key_exists($k, $properties))) {
                $skippedProperties[$k] = true;
            }
        }

        foreach (LazyObjectRegistry::$classResetters[$class] as $reset) {
            $reset($instance, $skippedProperties);
        }

        foreach ((array) $instance as $name => $value) {
            if ("\0" !== ($name[0] ?? '') && !\array_key_exists($name, $skippedProperties)) {
                unset($instance->$name);
            }
        }

        $this->status = self::STATUS_UNINITIALIZED_FULL;
    }

    public function __clone()
    {
        if (isset($this->cloneInstance)) {
            try {
                $this->realInstance = $this->cloneInstance;
            } finally {
                unset($this->cloneInstance);
            }
        } elseif (isset($this->realInstance)) {
            $this->realInstance = clone $this->realInstance;
        }
    }

    public function __get($name)
    {
        if ('realInstance' !== $name) {
            throw new \BadMethodCallException(\sprintf('No such property "%s::$%s"', self::class, $name));
        }

        return $this->realInstance = ($this->initializer)();
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\VarExporter\Internal;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @internal
 */
class NamedClosure
{
    public function __construct(
        public readonly array $callable,
        public readonly ?\ReflectionMethod $method = null,
    ) {
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\VarExporter\Internal;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @internal
 */
class Reference
{
    public int $count = 0;

    public function __construct(
        public readonly int $id,
        public readonly mixed $value = null,
    ) {
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\VarExporter\Internal;

use Symfony\Component\VarExporter\Exception\ClassNotFoundException;
use Symfony\Component\VarExporter\Exception\NotInstantiableTypeException;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @internal
 */
class Registry
{
    public static array $reflectors = [];
    public static array $prototypes = [];
    public static array $factories = [];
    public static array $cloneable = [];
    public static array $instantiableWithoutConstructor = [];

    public function __construct(
        public readonly array $classes,
    ) {
    }

    public static function unserialize($objects, $serializables)
    {
        $unserializeCallback = ini_set('unserialize_callback_func', __CLASS__.'::getClassReflector');

        try {
            foreach ($serializables as $k => $v) {
                $objects[$k] = unserialize($v, ['allowed_classes' => true]);
            }
        } finally {
            ini_set('unserialize_callback_func', $unserializeCallback);
        }

        return $objects;
    }

    public static function p($class)
    {
        self::getClassReflector($class, true, true);

        return self::$prototypes[$class];
    }

    public static function f($class)
    {
        $reflector = self::$reflectors[$class] ??= self::getClassReflector($class, true, false);

        return self::$factories[$class] = $reflector->newInstanceWithoutConstructor(...);
    }

    public static function getClassReflector($class, $instantiableWithoutConstructor = false, $cloneable = null)
    {
        if (!($isClass = class_exists($class)) && !interface_exists($class, false) && !trait_exists($class, false)) {
            throw new ClassNotFoundException($class);
        }
        $reflector = new \ReflectionClass($class);

        if ($instantiableWithoutConstructor) {
            $proto = $reflector->newInstanceWithoutConstructor();
        } elseif (!$isClass || $reflector->isAbstract()) {
            throw new NotInstantiableTypeException($class);
        } elseif ($reflector->name !== $class) {
            $reflector = self::$reflectors[$name = $reflector->name] ??= self::getClassReflector($name, false, $cloneable);
            self::$cloneable[$class] = self::$cloneable[$name];
            self::$instantiableWithoutConstructor[$class] = self::$instantiableWithoutConstructor[$name];
            self::$prototypes[$class] = self::$prototypes[$name];

            return $reflector;
        } else {
            try {
                $proto = $reflector->newInstanceWithoutConstructor();
                $instantiableWithoutConstructor = true;
            } catch (\ReflectionException) {
                $proto = $reflector->implementsInterface('Serializable') && !method_exists($class, '__unserialize') ? 'C:' : 'O:';
                if ('C:' === $proto && !$reflector->getMethod('unserialize')->isInternal()) {
                    $proto = null;
                } else {
                    try {
                        $proto = @unserialize($proto.\strlen($class).':"'.$class.'":0:{}', ['allowed_classes' => true]);
                    } catch (\Exception $e) {
                        if (method_exists($class, '__unserialize')) {
                            // The class cannot be instantiated empty but defines __serialize()/__unserialize();
                            // it'll be reconstructed by serializing the whole value.
                            $proto = null;
                        } elseif (__FILE__ !== $e->getFile()) {
                            throw $e;
                        } else {
                            throw new NotInstantiableTypeException($class, $e);
                        }
                    }
                    if (false === $proto) {
                        if (!method_exists($class, '__unserialize')) {
                            throw new NotInstantiableTypeException($class);
                        }
                        $proto = null;
                    }
                }
            }
            if (null !== $proto && !$proto instanceof \Throwable && !$proto instanceof \Serializable && !method_exists($class, '__sleep') && !method_exists($class, '__serialize')) {
                try {
                    serialize($proto);
                } catch (\Exception $e) {
                    throw new NotInstantiableTypeException($class, $e);
                }
            }
        }

        if (null === $cloneable) {
            if (($proto instanceof \Reflector || $proto instanceof \ReflectionGenerator || $proto instanceof \ReflectionType || $proto instanceof \IteratorIterator || $proto instanceof \RecursiveIteratorIterator) && (!$proto instanceof \Serializable && !method_exists($proto, '__wakeup') && !method_exists($class, '__unserialize'))) {
                throw new NotInstantiableTypeException($class);
            }

            $cloneable = $reflector->isCloneable() && !$reflector->hasMethod('__clone');
        }

        self::$cloneable[$class] = $cloneable;
        self::$instantiableWithoutConstructor[$class] = $instantiableWithoutConstructor;
        self::$prototypes[$class] = $proto;

        if ($proto instanceof \Throwable) {
            static $setTrace;

            if (null === $setTrace) {
                $setTrace = [
                    new \ReflectionProperty(\Error::class, 'trace'),
                    new \ReflectionProperty(\Exception::class, 'trace'),
                ];
                $setTrace[0] = $setTrace[0]->setValue(...);
                $setTrace[1] = $setTrace[1]->setValue(...);
            }

            $setTrace[$proto instanceof \Exception]($proto, []);
        }

        return $reflector;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\VarExporter;

use Symfony\Component\VarExporter\Internal\Hydrator as InternalHydrator;

/**
 * Utility class to hydrate the properties of an object.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
final class Hydrator
{
    /**
     * Sets the properties of an object, including private and protected ones.
     *
     * For example:
     *
     *     // Sets the public or protected $object->propertyName property
     *     Hydrator::hydrate($object, ['propertyName' => $propertyValue]);
     *
     *     // Sets a private property defined on its parent Bar class:
     *     Hydrator::hydrate($object, ["\0Bar\0privateBarProperty" => $propertyValue]);
     *
     *     // Alternative way to set the private $object->privateBarProperty property
     *     Hydrator::hydrate($object, [], [
     *         Bar::class => ['privateBarProperty' => $propertyValue],
     *     ]);
     *
     * Instances of ArrayObject, ArrayIterator and SplObjectStorage can be hydrated
     * by using the special "\0" property name to define their internal value:
     *
     *     // Hydrates an SplObjectStorage where $info1 is attached to $obj1, etc.
     *     Hydrator::hydrate($object, ["\0" => [$obj1, $info1, $obj2, $info2...]]);
     *
     *     // Hydrates an ArrayObject populated with $inputArray
     *     Hydrator::hydrate($object, ["\0" => [$inputArray]]);
     *
     * @template T of object
     *
     * @param T                                         $instance         The object to hydrate
     * @param array<string, mixed>                      $properties       The properties to set on the instance
     * @param array<class-string, array<string, mixed>> $scopedProperties The properties to set on the instance,
     *                                                                    keyed by their declaring class
     *
     * @return T
     */
    public static function hydrate(object $instance, array $properties = [], array $scopedProperties = []): object
    {
        if ($properties) {
            $class = $instance::class;
            $propertyScopes = InternalHydrator::$propertyScopes[$class] ??= InternalHydrator::getPropertyScopes($class);

            foreach ($properties as $name => &$value) {
                [$scope, $name, $writeScope] = $propertyScopes[$name] ?? [$class, $name, $class];
                $scopedProperties[$writeScope ?? $scope][$name] = &$value;
            }
            unset($value);
        }

        foreach ($scopedProperties as $scope => $properties) {
            if ($properties) {
                (InternalHydrator::$simpleHydrators[$scope] ??= InternalHydrator::getSimpleHydrator($scope))($properties, $instance);
            }
        }

        return $instance;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\VarExporter;

use Symfony\Component\VarExporter\Exception\ExceptionInterface;
use Symfony\Component\VarExporter\Internal\Exporter;
use Symfony\Component\VarExporter\Internal\Hydrator;
use Symfony\Component\VarExporter\Internal\Registry;
use Symfony\Component\VarExporter\Internal\Values;

/**
 * Exports serializable PHP values to PHP code.
 *
 * VarExporter allows serializing PHP data structures to plain PHP code (like var_export())
 * while preserving all the semantics associated with serialize() (unlike var_export()).
 *
 * By leveraging OPcache, the generated PHP code is faster than doing the same with unserialize().
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
final class VarExporter
{
    /**
     * Exports a serializable PHP value to PHP code.
     *
     * @param bool                              &$isStaticValue Set to true after execution if the provided value is static, false otherwise
     * @param array<class-string, class-string> &$foundClasses  Classes found in the value are added to this list as both keys and values
     *
     * @throws ExceptionInterface When the provided value cannot be serialized
     */
    public static function export(mixed $value, ?bool &$isStaticValue = null, array &$foundClasses = []): string
    {
        $isStaticValue = true;

        if (!\is_object($value) && !(\is_array($value) && $value) && !\is_resource($value) || $value instanceof \UnitEnum) {
            return Exporter::export($value);
        }

        $objectsPool = new \SplObjectStorage();
        $refsPool = [];
        $objectsCount = 0;

        try {
            $value = Exporter::prepare([$value], $objectsPool, $refsPool, $objectsCount, $isStaticValue)[0];
        } finally {
            $references = [];
            foreach ($refsPool as $i => $v) {
                if ($v[0]->count) {
                    $references[1 + $i] = $v[2];
                }
                $v[0] = $v[1];
            }
        }

        if ($isStaticValue) {
            return Exporter::export($value);
        }

        $classes = [];
        $values = [];
        $states = [];
        foreach ($objectsPool as $i => $v) {
            [, $class, $values[], $wakeup] = $objectsPool[$v];
            $foundClasses[$class] = $classes[] = $class;

            if (0 < $wakeup) {
                $states[$wakeup] = $i;
            } elseif (0 > $wakeup) {
                $states[-$wakeup] = [$i, array_pop($values)];
                $values[] = [];
            }
        }
        ksort($states);

        $wakeups = [null];
        foreach ($states as $v) {
            if (\is_array($v)) {
                $wakeups[-$v[0]] = $v[1];
            } else {
                $wakeups[] = $v;
            }
        }

        if (null === $wakeups[0]) {
            unset($wakeups[0]);
        }

        $properties = [];
        foreach ($values as $i => $vars) {
            foreach ($vars as $class => $values) {
                foreach ($values as $name => $v) {
                    $properties[$class][$name][$i] = $v;
                }
            }
        }

        if ($classes || $references) {
            $value = new Hydrator(new Registry($classes), $references ? new Values($references) : null, $properties, $value, $wakeups);
        } else {
            $isStaticValue = true;
        }

        return Exporter::export($value);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\VarExporter;

use Symfony\Component\Serializer\Attribute\Ignore;
use Symfony\Component\VarExporter\Hydrator as PublicHydrator;
use Symfony\Component\VarExporter\Internal\Hydrator;
use Symfony\Component\VarExporter\Internal\LazyObjectRegistry as Registry;
use Symfony\Component\VarExporter\Internal\LazyObjectState;
use Symfony\Component\VarExporter\Internal\LazyObjectTrait;

if (\PHP_VERSION_ID >= 80400) {
    trigger_deprecation('symfony/var-exporter', '7.3', 'The "%s" trait is deprecated, use native lazy objects instead.', LazyProxyTrait::class);
}

/**
 * @deprecated since Symfony 7.3, use native lazy objects instead
 */
trait LazyProxyTrait
{
    use LazyObjectTrait;

    /**
     * Creates a lazy-loading virtual proxy.
     *
     * @param \Closure():object $initializer Returns the proxied object
     * @param static|null       $instance
     */
    public static function createLazyProxy(\Closure $initializer, ?object $instance = null): static
    {
        if (self::class !== $class = $instance ? $instance::class : static::class) {
            $skippedProperties = ["\0".self::class."\0lazyObjectState" => true];
        }

        if (!isset(Registry::$defaultProperties[$class])) {
            Registry::$classReflectors[$class] ??= new \ReflectionClass($class);
            $instance ??= Registry::$classReflectors[$class]->newInstanceWithoutConstructor();
            Registry::$defaultProperties[$class] ??= (array) $instance;

            if (self::class === $class && \defined($class.'::LAZY_OBJECT_PROPERTY_SCOPES')) {
                Hydrator::$propertyScopes[$class] ??= $class::LAZY_OBJECT_PROPERTY_SCOPES;
            }

            Registry::$classResetters[$class] ??= Registry::getClassResetters($class);
        } else {
            $instance ??= Registry::$classReflectors[$class]->newInstanceWithoutConstructor();
        }

        if (isset($instance->lazyObjectState)) {
            $instance->lazyObjectState->initializer = $initializer;
            unset($instance->lazyObjectState->realInstance);

            return $instance;
        }

        $instance->lazyObjectState = new LazyObjectState($initializer);

        foreach (Registry::$classResetters[$class] as $reset) {
            $reset($instance, $skippedProperties ??= []);
        }

        return $instance;
    }

    /**
     * Returns whether the object is initialized.
     *
     * @param bool $partial Whether partially initialized objects should be considered as initialized
     */
    #[Ignore]
    public function isLazyObjectInitialized(bool $partial = false): bool
    {
        return !isset($this->lazyObjectState) || isset($this->lazyObjectState->realInstance) || Registry::$noInitializerState === $this->lazyObjectState->initializer;
    }

    /**
     * Forces initialization of a lazy object and returns it.
     */
    public function initializeLazyObject(): parent
    {
        if ($state = $this->lazyObjectState ?? null) {
            return $state->realInstance ??= ($state->initializer)();
        }

        return $this;
    }

    /**
     * @return bool Returns false when the object cannot be reset, ie when it's not a lazy object
     */
    public function resetLazyObject(): bool
    {
        if (!isset($this->lazyObjectState) || Registry::$noInitializerState === $this->lazyObjectState->initializer) {
            return false;
        }

        unset($this->lazyObjectState->realInstance);

        return true;
    }

    public function &__get($name): mixed
    {
        $propertyScopes = Hydrator::$propertyScopes[$this::class] ??= Hydrator::getPropertyScopes($this::class);
        $scope = null;
        $instance = $this;
        $notByRef = 0;

        if ([$class, , $writeScope, $access] = $propertyScopes[$name] ?? null) {
            $notByRef = $access & Hydrator::PROPERTY_NOT_BY_REF;
            $scope = Registry::getScopeForRead($propertyScopes, $class, $name);

            if (null === $scope || isset($propertyScopes["\0$scope\0$name"])) {
                if ($state = $this->lazyObjectState ?? null) {
                    $instance = $state->realInstance ??= ($state->initializer)();
                }
                if (\PHP_VERSION_ID >= 80400 && !$notByRef && ($access >> 2) & \ReflectionProperty::IS_PRIVATE_SET) {
                    $scope ??= $writeScope;
                }
                $parent = 2;
                goto get_in_scope;
            }
        }
        $parent = (Registry::$parentMethods[self::class] ??= Registry::getParentMethods(self::class))['get'];

        if ($state = $this->lazyObjectState ?? null) {
            $instance = $state->realInstance ??= ($state->initializer)();
        } else {
            if (2 === $parent) {
                return parent::__get($name);
            }
            $value = parent::__get($name);

            return $value;
        }

        if (!$parent && null === $class && !\array_key_exists($name, (array) $instance)) {
            $frame = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0];
            trigger_error(\sprintf('Undefined property: %s::$%s in %s on line %s', $instance::class, $name, $frame['file'], $frame['line']), \E_USER_NOTICE);
        }

        get_in_scope:
        $notByRef = $notByRef || 1 === $parent;

        try {
            if (null === $scope) {
                if (!$notByRef) {
                    return $instance->$name;
                }
                $value = $instance->$name;

                return $value;
            }
            $accessor = Registry::$classAccessors[$scope] ??= Registry::getClassAccessors($scope);

            return $accessor['get']($instance, $name, $notByRef);
        } catch (\Error $e) {
            if (\Error::class !== $e::class || !str_starts_with($e->getMessage(), 'Cannot access uninitialized non-nullable property')) {
                throw $e;
            }

            try {
                if (null === $scope) {
                    $instance->$name = [];

                    return $instance->$name;
                }

                $accessor['set']($instance, $name, []);

                return $accessor['get']($instance, $name, $notByRef);
            } catch (\Error) {
                throw $e;
            }
        }
    }

    public function __set($name, $value): void
    {
        $propertyScopes = Hydrator::$propertyScopes[$this::class] ??= Hydrator::getPropertyScopes($this::class);
        $scope = null;
        $instance = $this;

        if ([$class, , $writeScope, $access] = $propertyScopes[$name] ?? null) {
            $scope = Registry::getScopeForWrite($propertyScopes, $class, $name, $access >> 2);

            if ($writeScope === $scope || isset($propertyScopes["\0$scope\0$name"])) {
                if ($state = $this->lazyObjectState ?? null) {
                    $instance = $state->realInstance ??= ($state->initializer)();
                }
                goto set_in_scope;
            }
        }

        if ($state = $this->lazyObjectState ?? null) {
            $instance = $state->realInstance ??= ($state->initializer)();
        } elseif ((Registry::$parentMethods[self::class] ??= Registry::getParentMethods(self::class))['set']) {
            parent::__set($name, $value);

            return;
        }

        set_in_scope:

        if (null === $scope) {
            $instance->$name = $value;
        } else {
            $accessor = Registry::$classAccessors[$scope] ??= Registry::getClassAccessors($scope);
            $accessor['set']($instance, $name, $value);
        }
    }

    public function __isset($name): bool
    {
        $propertyScopes = Hydrator::$propertyScopes[$this::class] ??= Hydrator::getPropertyScopes($this::class);
        $scope = null;
        $instance = $this;

        if ([$class] = $propertyScopes[$name] ?? null) {
            $scope = Registry::getScopeForRead($propertyScopes, $class, $name);

            if (null === $scope || isset($propertyScopes["\0$scope\0$name"])) {
                if ($state = $this->lazyObjectState ?? null) {
                    $instance = $state->realInstance ??= ($state->initializer)();
                }
                goto isset_in_scope;
            }
        }

        if ($state = $this->lazyObjectState ?? null) {
            $instance = $state->realInstance ??= ($state->initializer)();
        } elseif ((Registry::$parentMethods[self::class] ??= Registry::getParentMethods(self::class))['isset']) {
            return parent::__isset($name);
        }

        isset_in_scope:

        if (null === $scope) {
            return isset($instance->$name);
        }
        $accessor = Registry::$classAccessors[$scope] ??= Registry::getClassAccessors($scope);

        return $accessor['isset']($instance, $name);
    }

    public function __unset($name): void
    {
        $propertyScopes = Hydrator::$propertyScopes[$this::class] ??= Hydrator::getPropertyScopes($this::class);
        $scope = null;
        $instance = $this;

        if ([$class, , $writeScope, $access] = $propertyScopes[$name] ?? null) {
            $scope = Registry::getScopeForWrite($propertyScopes, $class, $name, $access >> 2);

            if ($writeScope === $scope || isset($propertyScopes["\0$scope\0$name"])) {
                if ($state = $this->lazyObjectState ?? null) {
                    $instance = $state->realInstance ??= ($state->initializer)();
                }
                goto unset_in_scope;
            }
        }

        if ($state = $this->lazyObjectState ?? null) {
            $instance = $state->realInstance ??= ($state->initializer)();
        } elseif ((Registry::$parentMethods[self::class] ??= Registry::getParentMethods(self::class))['unset']) {
            parent::__unset($name);

            return;
        }

        unset_in_scope:

        if (null === $scope) {
            unset($instance->$name);
        } else {
            $accessor = Registry::$classAccessors[$scope] ??= Registry::getClassAccessors($scope);
            $accessor['unset']($instance, $name);
        }
    }

    public function __clone(): void
    {
        if (!isset($this->lazyObjectState)) {
            if ((Registry::$parentMethods[self::class] ??= Registry::getParentMethods(self::class))['clone']) {
                parent::__clone();
            }

            return;
        }

        $this->lazyObjectState = clone $this->lazyObjectState;
    }

    public function __serialize(): array
    {
        $class = self::class;
        $state = $this->lazyObjectState ?? null;

        if (!$state && (Registry::$parentMethods[$class] ??= Registry::getParentMethods($class))['serialize']) {
            $properties = parent::__serialize();
        } else {
            $properties = (array) $this;

            if ($state) {
                unset($properties["\0$class\0lazyObjectState"]);
                $properties["\0$class\0lazyObjectReal"] = $state->realInstance ??= ($state->initializer)();
            }
        }

        if ($state || Registry::$parentMethods[$class]['serialize'] || !Registry::$parentMethods[$class]['sleep']) {
            return $properties;
        }

        $scope = get_parent_class($class);
        $data = [];

        foreach (parent::__sleep() as $name) {
            $value = $properties[$k = $name] ?? $properties[$k = "\0*\0$name"] ?? $properties[$k = "\0$class\0$name"] ?? $properties[$k = "\0$scope\0$name"] ?? $k = null;

            if (null === $k) {
                trigger_error(\sprintf('serialize(): "%s" returned as member variable from __sleep() but does not exist', $name), \E_USER_NOTICE);
            } else {
                $data[$k] = $value;
            }
        }

        return $data;
    }

    public function __unserialize(array $data): void
    {
        $class = self::class;

        if ($instance = $data["\0$class\0lazyObjectReal"] ?? null) {
            unset($data["\0$class\0lazyObjectReal"]);

            foreach (Registry::$classResetters[$class] ??= Registry::getClassResetters($class) as $reset) {
                $reset($this, $data);
            }

            if ($data) {
                PublicHydrator::hydrate($this, $data);
            }
            $this->lazyObjectState = new LazyObjectState(Registry::$noInitializerState ??= static fn () => throw new \LogicException('Lazy proxy has no initializer.'));
            $this->lazyObjectState->realInstance = $instance;
        } elseif ((Registry::$parentMethods[$class] ??= Registry::getParentMethods($class))['unserialize']) {
            parent::__unserialize($data);
        } else {
            PublicHydrator::hydrate($this, $data);

            if (Registry::$parentMethods[$class]['wakeup']) {
                parent::__wakeup();
            }
        }
    }

    public function __destruct()
    {
        if (isset($this->lazyObjectState)) {
            return;
        }

        if ((Registry::$parentMethods[self::class] ??= Registry::getParentMethods(self::class))['destruct']) {
            parent::__destruct();
        }
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\VarExporter;

interface LazyObjectInterface
{
    /**
     * Returns whether the object is initialized.
     *
     * @param bool $partial Whether partially initialized objects should be considered as initialized
     */
    public function isLazyObjectInitialized(bool $partial = false): bool;

    /**
     * Forces initialization of a lazy object and returns it.
     */
    public function initializeLazyObject(): object;

    /**
     * @return bool Returns false when the object cannot be reset, ie when it's not a lazy object
     */
    public function resetLazyObject(): bool;
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\VarExporter;

use Symfony\Component\VarExporter\Exception\LogicException;
use Symfony\Component\VarExporter\Internal\Hydrator;
use Symfony\Component\VarExporter\Internal\LazyDecoratorTrait;
use Symfony\Component\VarExporter\Internal\LazyObjectRegistry;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 */
final class ProxyHelper
{
    /**
     * Helps generate lazy-loading ghost objects.
     *
     * @deprecated since Symfony 7.3, use native lazy objects instead
     *
     * @throws LogicException When the class is incompatible with ghost objects
     */
    public static function generateLazyGhost(\ReflectionClass $class): string
    {
        if (\PHP_VERSION_ID >= 80400) {
            trigger_deprecation('symfony/var-exporter', '7.3', 'Using ProxyHelper::generateLazyGhost() is deprecated, use native lazy objects instead.');
        }
        if (\PHP_VERSION_ID < 80300 && $class->isReadOnly()) {
            throw new LogicException(\sprintf('Cannot generate lazy ghost with PHP < 8.3: class "%s" is readonly.', $class->name));
        }
        if ($class->isFinal()) {
            throw new LogicException(\sprintf('Cannot generate lazy ghost: class "%s" is final.', $class->name));
        }
        if ($class->isInterface() || $class->isAbstract() || $class->isTrait()) {
            throw new LogicException(\sprintf('Cannot generate lazy ghost: "%s" is not a concrete class.', $class->name));
        }
        if (\stdClass::class !== $class->name && $class->isInternal()) {
            throw new LogicException(\sprintf('Cannot generate lazy ghost: class "%s" is internal.', $class->name));
        }
        if ($class->hasMethod('__get') && 'mixed' !== (self::exportType($class->getMethod('__get')) ?? 'mixed')) {
            throw new LogicException(\sprintf('Cannot generate lazy ghost: return type of method "%s::__get()" should be "mixed".', $class->name));
        }

        static $traitMethods;
        $traitMethods ??= (new \ReflectionClass(LazyGhostTrait::class))->getMethods();

        foreach ($traitMethods as $method) {
            if ($class->hasMethod($method->name) && $class->getMethod($method->name)->isFinal()) {
                throw new LogicException(\sprintf('Cannot generate lazy ghost: method "%s::%s()" is final.', $class->name, $method->name));
            }
        }

        $parent = $class;
        while ($parent = $parent->getParentClass()) {
            if (\stdClass::class !== $parent->name && $parent->isInternal()) {
                throw new LogicException(\sprintf('Cannot generate lazy ghost: class "%s" extends "%s" which is internal.', $class->name, $parent->name));
            }
        }

        $hooks = '';
        $propertyScopes = Hydrator::$propertyScopes[$class->name] ??= Hydrator::getPropertyScopes($class->name);
        foreach ($propertyScopes as $key => [$scope, $name, , $access]) {
            $propertyScopes[$k = "\0$scope\0$name"] ?? $propertyScopes[$k = "\0*\0$name"] ?? $k = $name;
            $flags = $access >> 2;

            if ($k !== $key || !($access & Hydrator::PROPERTY_HAS_HOOKS) || $flags & \ReflectionProperty::IS_VIRTUAL) {
                continue;
            }

            if ($flags & (\ReflectionProperty::IS_FINAL | \ReflectionProperty::IS_PRIVATE)) {
                throw new LogicException(\sprintf('Cannot generate lazy ghost: property "%s::$%s" is final or private(set).', $class->name, $name));
            }

            $p = $propertyScopes[$k][4] ?? Hydrator::$propertyScopes[$class->name][$k][4] = new \ReflectionProperty($scope, $name);

            $type = self::exportType($p);
            $hooks .= "\n    "
                .($p->isProtected() ? 'protected' : 'public')
                .($p->isProtectedSet() ? ' protected(set)' : '')
                ." {$type} \${$name}"
                .($p->hasDefaultValue() ? ' = '.VarExporter::export($p->getDefaultValue()) : '')
                ." {\n";

            foreach ($p->getHooks() as $hook => $method) {
                if ('get' === $hook) {
                    $ref = ($method->returnsReference() ? '&' : '');
                    $hooks .= "        {$ref}get { \$this->initializeLazyObject(); return parent::\${$name}::get(); }\n";
                } elseif ('set' === $hook) {
                    $parameters = self::exportParameters($method, true);
                    $arg = '$'.$method->getParameters()[0]->name;
                    $hooks .= "        set({$parameters}) { \$this->initializeLazyObject(); parent::\${$name}::set({$arg}); }\n";
                } else {
                    throw new LogicException(\sprintf('Cannot generate lazy ghost: hook "%s::%s()" is not supported.', $class->name, $method->name));
                }
            }

            $hooks .= "    }\n";
        }

        $propertyScopes = self::exportPropertyScopes($class->name, $propertyScopes);

        return <<<EOPHP
             extends \\{$class->name} implements \Symfony\Component\VarExporter\LazyObjectInterface
            {
                use \Symfony\Component\VarExporter\LazyGhostTrait;

                private const LAZY_OBJECT_PROPERTY_SCOPES = {$propertyScopes};
            {$hooks}}

            // Help opcache.preload discover always-needed symbols
            class_exists(\Symfony\Component\VarExporter\Internal\Hydrator::class);
            class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectRegistry::class);
            class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectState::class);

            EOPHP;
    }

    /**
     * Helps generate lazy-loading decorators.
     *
     * @param \ReflectionClass[] $interfaces
     *
     * @throws LogicException When the class is incompatible with virtual proxies
     */
    public static function generateLazyProxy(?\ReflectionClass $class, array $interfaces = []): string
    {
        if (!class_exists($class?->name ?? \stdClass::class, false)) {
            throw new LogicException(\sprintf('Cannot generate lazy proxy: "%s" is not a class.', $class->name));
        }
        if ($class?->isFinal()) {
            throw new LogicException(\sprintf('Cannot generate lazy proxy: class "%s" is final.', $class->name));
        }
        if (\PHP_VERSION_ID < 80400) {
            return self::generateLegacyLazyProxy($class, $interfaces);
        }

        if ($class && !$class->isAbstract()) {
            $parent = $class;
            do {
                $extendsInternalClass = $parent->isInternal();
            } while (!$extendsInternalClass && $parent = $parent->getParentClass());

            if (!$extendsInternalClass) {
                trigger_deprecation('symfony/var-exporter', '7.3', 'Generating lazy proxy for class "%s" is deprecated; leverage native lazy objects instead.', $class->name);
                // throw new LogicException(\sprintf('Cannot generate lazy proxy: leverage native lazy objects instead for class "%s".', $class->name));
            }
        }

        $propertyScopes = $class ? Hydrator::$propertyScopes[$class->name] ??= Hydrator::getPropertyScopes($class->name) : [];
        $abstractProperties = [];
        $hookedProperties = [];
        foreach ($propertyScopes as $key => [$scope, $name, , $access]) {
            $propertyScopes[$k = "\0$scope\0$name"] ?? $propertyScopes[$k = "\0*\0$name"] ?? $k = $name;
            $flags = $access >> 2;

            if ($k !== $key || $flags & \ReflectionProperty::IS_PRIVATE) {
                continue;
            }

            if ($flags & \ReflectionProperty::IS_ABSTRACT) {
                $abstractProperties[$name] = $propertyScopes[$k][4] ?? Hydrator::$propertyScopes[$class->name][$k][4] = new \ReflectionProperty($scope, $name);
                continue;
            }
            $abstractProperties[$name] = false;

            if (!($access & Hydrator::PROPERTY_HAS_HOOKS)) {
                continue;
            }

            if ($flags & \ReflectionProperty::IS_FINAL) {
                throw new LogicException(\sprintf('Cannot generate lazy proxy: property "%s::$%s" is final.', $class->name, $name));
            }

            $p = $propertyScopes[$k][4] ?? Hydrator::$propertyScopes[$class->name][$k][4] = new \ReflectionProperty($scope, $name);
            $hookedProperties[$name] = [$p, $p->getHooks()];
        }

        $methodReflectors = [$class?->getMethods(\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED) ?? []];
        foreach ($interfaces as $interface) {
            if (!$interface->isInterface()) {
                throw new LogicException(\sprintf('Cannot generate lazy proxy: "%s" is not an interface.', $interface->name));
            }
            $methodReflectors[] = $interface->getMethods();

            foreach ($interface->getProperties() as $p) {
                $abstractProperties[$p->name] ??= $p;
                $hookedProperties[$p->name] ??= [$p, []];
                $hookedProperties[$p->name][1] += $p->getHooks();
            }
        }

        $hooks = '';

        foreach (array_filter($abstractProperties) as $name => $p) {
            $type = self::exportType($p);
            $hooks .= "\n    "
                .($p->isProtected() ? 'protected' : 'public')
                .($p->isProtectedSet() ? ' protected(set)' : '')
                ." {$type} \${$name};\n";
        }

        foreach ($hookedProperties as $name => [$p, $methods]) {
            if ($abstractProperties[$p->name] ?? false) {
                continue;
            }
            $type = self::exportType($p);
            $hooks .= "\n    "
                .($p->isProtected() ? 'protected' : 'public')
                .($p->isProtectedSet() ? ' protected(set)' : '')
                ." {$type} \${$name} {\n";

            foreach ($methods as $hook => $method) {
                if ('get' === $hook) {
                    $ref = ($method->returnsReference() ? '&' : '');
                    $hooks .= <<<EOPHP
                                {$ref}get {
                                    return \$this->lazyObjectState->realInstance->{$p->name};
                                }

                        EOPHP;
                } elseif ('set' === $hook) {
                    $parameters = self::exportParameters($method, true);
                    $arg = '$'.$method->getParameters()[0]->name;
                    $hooks .= <<<EOPHP
                                set({$parameters}) {
                                    \$this->lazyObjectState->realInstance->{$p->name} = {$arg};
                                }

                        EOPHP;
                } else {
                    throw new LogicException(\sprintf('Cannot generate lazy proxy: hook "%s::%s()" is not supported.', $class->name, $method->name));
                }
            }

            $hooks .= "    }\n";
        }

        $methods = [];
        $methodReflectors = array_merge(...$methodReflectors);

        foreach ($methodReflectors as $method) {
            if ('__get' !== strtolower($method->name) || 'mixed' === ($type = self::exportType($method) ?? 'mixed')) {
                continue;
            }
            $trait = new \ReflectionMethod(LazyDecoratorTrait::class, '__get');
            $body = \array_slice(file($trait->getFileName()), $trait->getStartLine() - 1, $trait->getEndLine() - $trait->getStartLine());
            $body[0] = str_replace('): mixed', '): '.$type, $body[0]);
            $methods['__get'] = strtr(implode('', $body).'    }', [
                'Hydrator' => '\\'.Hydrator::class,
                'Registry' => '\\'.LazyObjectRegistry::class,
            ]);
            break;
        }

        foreach ($methodReflectors as $method) {
            if (($method->isStatic() && !$method->isAbstract()) || isset($methods[$lcName = strtolower($method->name)])) {
                continue;
            }
            if ($method->isFinal()) {
                throw new LogicException(\sprintf('Cannot generate lazy proxy: method "%s::%s()" is final.', $class->name, $method->name));
            }
            if (method_exists(LazyDecoratorTrait::class, $method->name)) {
                continue;
            }

            $signature = self::exportSignature($method, true, $args);

            if ($method->isStatic()) {
                $body = "        throw new \BadMethodCallException('Cannot forward abstract method \"{$method->class}::{$method->name}()\".');";
            } elseif (str_ends_with($signature, '): never') || str_ends_with($signature, '): void')) {
                $body = <<<EOPHP
                            \$this->lazyObjectState->realInstance->{$method->name}({$args});
                    EOPHP;
            } else {
                $mayReturnThis = false;
                foreach (preg_split('/[()|&]++/', self::exportType($method) ?? 'static') as $type) {
                    if (\in_array($type = ltrim($type, '?'), ['static', 'object'], true)) {
                        $mayReturnThis = true;
                        break;
                    }
                    foreach ([$class, ...$interfaces] as $r) {
                        if ($r && is_a($r->name, $type, true)) {
                            $mayReturnThis = true;
                            break 2;
                        }
                    }
                }

                if ($method->returnsReference() || !$mayReturnThis) {
                    $body = <<<EOPHP
                                return \$this->lazyObjectState->realInstance->{$method->name}({$args});
                        EOPHP;
                } else {
                    $body = <<<EOPHP
                                \${0} = \$this->lazyObjectState->realInstance;
                                \${1} = \${0}->{$method->name}({$args});

                                return match (true) {
                                    \${1} === \${0} => \$this,
                                    !\${1} instanceof \${0} || !\${0} instanceof \${1} => \${1},
                                    null !== \$this->lazyObjectState->cloneInstance =& \${1} => clone \$this,
                                };
                        EOPHP;
                }
            }
            $methods[$lcName] = "    {$signature}\n    {\n{$body}\n    }";
        }

        $types = $interfaces = array_unique(array_column($interfaces, 'name'));
        $interfaces[] = LazyObjectInterface::class;
        $interfaces = implode(', \\', $interfaces);
        $parent = $class ? ' extends \\'.$class->name : '';
        array_unshift($types, $class ? 'parent' : '');
        $type = ltrim(implode('&\\', $types), '&');

        if (!$class) {
            $trait = new \ReflectionMethod(LazyDecoratorTrait::class, 'initializeLazyObject');
            $body = \array_slice(file($trait->getFileName()), $trait->getStartLine() - 1, $trait->getEndLine() - $trait->getStartLine());
            $body[0] = str_replace('): parent', '): '.$type, $body[0]);
            $methods = ['initializeLazyObject' => implode('', $body).'    }'] + $methods;
        }
        $body = $methods ? "\n".implode("\n\n", $methods)."\n" : '';
        $propertyScopes = $class ? self::exportPropertyScopes($class->name, $propertyScopes) : '[]';
        $lazyProxyTraitStatement = [];

        if (
            $class?->hasMethod('__unserialize')
            && !$class->getMethod('__unserialize')->getParameters()[0]->getType()
        ) {
            // fix contravariance type problem when $class declares a `__unserialize()` method without typehint.
            $lazyProxyTraitStatement[] = '__unserialize as private __doUnserialize;';

            $body .= <<<EOPHP

                    public function __unserialize(\$data): void
                    {
                        \$this->__doUnserialize(\$data);
                    }

                EOPHP;
        }

        if ($lazyProxyTraitStatement) {
            $lazyProxyTraitStatement = implode("\n        ", $lazyProxyTraitStatement);
            $lazyProxyTraitStatement = <<<EOPHP
                use \Symfony\Component\VarExporter\Internal\LazyDecoratorTrait {
                        {$lazyProxyTraitStatement}
                    }
                EOPHP;
        } else {
            $lazyProxyTraitStatement = <<<EOPHP
                use \Symfony\Component\VarExporter\Internal\LazyDecoratorTrait;
                EOPHP;
        }

        return <<<EOPHP
            {$parent} implements \\{$interfaces}
            {
                {$lazyProxyTraitStatement}

                private const LAZY_OBJECT_PROPERTY_SCOPES = {$propertyScopes};
            {$hooks}{$body}}

            // Help opcache.preload discover always-needed symbols
            class_exists(\Symfony\Component\VarExporter\Internal\Hydrator::class);
            class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectRegistry::class);

            EOPHP;
    }

    private static function generateLegacyLazyProxy(?\ReflectionClass $class, array $interfaces): string
    {
        if (\PHP_VERSION_ID < 80300 && $class?->isReadOnly()) {
            throw new LogicException(\sprintf('Cannot generate lazy proxy with PHP < 8.3: class "%s" is readonly.', $class->name));
        }

        $propertyScopes = $class ? Hydrator::$propertyScopes[$class->name] ??= Hydrator::getPropertyScopes($class->name) : [];
        $methodReflectors = [$class?->getMethods(\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED) ?? []];
        foreach ($interfaces as $interface) {
            if (!$interface->isInterface()) {
                throw new LogicException(\sprintf('Cannot generate lazy proxy: "%s" is not an interface.', $interface->name));
            }
            $methodReflectors[] = $interface->getMethods();
        }

        $extendsInternalClass = false;
        if ($parent = $class) {
            do {
                $extendsInternalClass = \stdClass::class !== $parent->name && $parent->isInternal();
            } while (!$extendsInternalClass && $parent = $parent->getParentClass());
        }
        $methodsHaveToBeProxied = $extendsInternalClass;
        $methods = [];
        $methodReflectors = array_merge(...$methodReflectors);

        foreach ($methodReflectors as $method) {
            if ('__get' !== strtolower($method->name) || 'mixed' === ($type = self::exportType($method) ?? 'mixed')) {
                continue;
            }
            $methodsHaveToBeProxied = true;
            $trait = new \ReflectionMethod(LazyProxyTrait::class, '__get');
            $body = \array_slice(file($trait->getFileName()), $trait->getStartLine() - 1, $trait->getEndLine() - $trait->getStartLine());
            $body[0] = str_replace('): mixed', '): '.$type, $body[0]);
            $methods['__get'] = strtr(implode('', $body).'    }', [
                'Hydrator' => '\\'.Hydrator::class,
                'Registry' => '\\'.LazyObjectRegistry::class,
            ]);
            break;
        }

        foreach ($methodReflectors as $method) {
            if (($method->isStatic() && !$method->isAbstract()) || isset($methods[$lcName = strtolower($method->name)])) {
                continue;
            }
            if ($method->isFinal()) {
                if ($extendsInternalClass || $methodsHaveToBeProxied || method_exists(LazyProxyTrait::class, $method->name)) {
                    throw new LogicException(\sprintf('Cannot generate lazy proxy: method "%s::%s()" is final.', $class->name, $method->name));
                }
                continue;
            }
            if (method_exists(LazyProxyTrait::class, $method->name) || ($method->isProtected() && !$method->isAbstract())) {
                continue;
            }

            $signature = self::exportSignature($method, true, $args);
            $parentCall = $method->isAbstract() ? "throw new \BadMethodCallException('Cannot forward abstract method \"{$method->class}::{$method->name}()\".')" : "parent::{$method->name}({$args})";

            if ($method->isStatic()) {
                $body = "        $parentCall;";
            } elseif (str_ends_with($signature, '): never') || str_ends_with($signature, '): void')) {
                $body = <<<EOPHP
                            if (isset(\$this->lazyObjectState)) {
                                (\$this->lazyObjectState->realInstance ??= (\$this->lazyObjectState->initializer)())->{$method->name}({$args});
                            } else {
                                {$parentCall};
                            }
                    EOPHP;
            } else {
                if (!$methodsHaveToBeProxied && !$method->isAbstract()) {
                    // Skip proxying methods that might return $this
                    foreach (preg_split('/[()|&]++/', self::exportType($method) ?? 'static') as $type) {
                        if (\in_array($type = ltrim($type, '?'), ['static', 'object'], true)) {
                            continue 2;
                        }
                        foreach ([$class, ...$interfaces] as $r) {
                            if ($r && is_a($r->name, $type, true)) {
                                continue 3;
                            }
                        }
                    }
                }

                $body = <<<EOPHP
                            if (isset(\$this->lazyObjectState)) {
                                return (\$this->lazyObjectState->realInstance ??= (\$this->lazyObjectState->initializer)())->{$method->name}({$args});
                            }

                            return {$parentCall};
                    EOPHP;
            }
            $methods[$lcName] = "    {$signature}\n    {\n{$body}\n    }";
        }

        $types = $interfaces = array_unique(array_column($interfaces, 'name'));
        $interfaces[] = LazyObjectInterface::class;
        $interfaces = implode(', \\', $interfaces);
        $parent = $class ? ' extends \\'.$class->name : '';
        array_unshift($types, $class ? 'parent' : '');
        $type = ltrim(implode('&\\', $types), '&');

        if (!$class) {
            $trait = new \ReflectionMethod(LazyProxyTrait::class, 'initializeLazyObject');
            $body = \array_slice(file($trait->getFileName()), $trait->getStartLine() - 1, $trait->getEndLine() - $trait->getStartLine());
            $body[0] = str_replace('): parent', '): '.$type, $body[0]);
            $methods = ['initializeLazyObject' => implode('', $body).'    }'] + $methods;
        }
        $body = $methods ? "\n".implode("\n\n", $methods)."\n" : '';
        $propertyScopes = $class ? self::exportPropertyScopes($class->name, $propertyScopes) : '[]';

        if (
            $class?->hasMethod('__unserialize')
            && !$class->getMethod('__unserialize')->getParameters()[0]->getType()
        ) {
            // fix contravariance type problem when $class declares a `__unserialize()` method without typehint.
            $lazyProxyTraitStatement = <<<EOPHP
                use \Symfony\Component\VarExporter\LazyProxyTrait {
                        __unserialize as private __doUnserialize;
                    }
                EOPHP;

            $body .= <<<EOPHP

                    public function __unserialize(\$data): void
                    {
                        \$this->__doUnserialize(\$data);
                    }

                EOPHP;
        } else {
            $lazyProxyTraitStatement = <<<EOPHP
                use \Symfony\Component\VarExporter\LazyProxyTrait;
                EOPHP;
        }

        return <<<EOPHP
            {$parent} implements \\{$interfaces}
            {
                {$lazyProxyTraitStatement}

                private const LAZY_OBJECT_PROPERTY_SCOPES = {$propertyScopes};
            {$body}}

            // Help opcache.preload discover always-needed symbols
            class_exists(\Symfony\Component\VarExporter\Internal\Hydrator::class);
            class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectRegistry::class);
            class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectState::class);

            EOPHP;
    }

    public static function exportParameters(\ReflectionFunctionAbstract $function, bool $withParameterTypes = true, ?string &$args = null): string
    {
        $byRefIndex = 0;
        $args = '';
        $param = null;
        $parameters = [];
        $namespace = $function instanceof \ReflectionMethod ? $function->class : $function->getNamespaceName().'\\';
        $namespace = substr($namespace, 0, strrpos($namespace, '\\') ?: 0);
        foreach ($function->getParameters() as $param) {
            $parameters[] = ($param->getAttributes(\SensitiveParameter::class) ? '#[\SensitiveParameter] ' : '')
                .($withParameterTypes && $param->hasType() ? self::exportType($param).' ' : '')
                .($param->isPassedByReference() ? '&' : '')
                .($param->isVariadic() ? '...' : '').'$'.$param->name
                .($param->isOptional() && !$param->isVariadic() ? ' = '.self::exportDefault($param, $namespace) : '');
            if ($param->isPassedByReference()) {
                $byRefIndex = 1 + $param->getPosition();
            }
            $args .= ($param->isVariadic() ? '...$' : '$').$param->name.', ';
        }

        if (!$param || !$byRefIndex) {
            $args = '...\func_get_args()';
        } elseif ($param->isVariadic()) {
            $args = substr($args, 0, -2);
        } else {
            $args = explode(', ', $args, 1 + $byRefIndex);
            $args[$byRefIndex] = \sprintf('...\array_slice(\func_get_args(), %d)', $byRefIndex);
            $args = implode(', ', $args);
        }

        return implode(', ', $parameters);
    }

    public static function exportSignature(\ReflectionFunctionAbstract $function, bool $withParameterTypes = true, ?string &$args = null): string
    {
        $parameters = self::exportParameters($function, $withParameterTypes, $args);

        $signature = 'function '.($function->returnsReference() ? '&' : '')
            .($function->isClosure() ? '' : $function->name).'('.$parameters.')';

        if ($function instanceof \ReflectionMethod) {
            $signature = ($function->isPublic() ? 'public ' : ($function->isProtected() ? 'protected ' : 'private '))
                .($function->isStatic() ? 'static ' : '').$signature;
        }
        if ($function->hasReturnType()) {
            $signature .= ': '.self::exportType($function);
        }

        static $getPrototype;
        $getPrototype ??= (new \ReflectionMethod(\ReflectionMethod::class, 'getPrototype'))->invoke(...);

        while ($function) {
            if ($function->hasTentativeReturnType()) {
                return '#[\ReturnTypeWillChange] '.$signature;
            }

            try {
                $function = $function instanceof \ReflectionMethod && $function->isAbstract() ? false : $getPrototype($function);
            } catch (\ReflectionException) {
                break;
            }
        }

        return $signature;
    }

    public static function exportType(\ReflectionFunctionAbstract|\ReflectionProperty|\ReflectionParameter $owner, bool $noBuiltin = false, ?\ReflectionType $type = null): ?string
    {
        if (!$type ??= $owner instanceof \ReflectionFunctionAbstract ? $owner->getReturnType() : $owner->getType()) {
            return null;
        }
        $class = null;
        $types = [];
        if ($type instanceof \ReflectionUnionType) {
            $reflectionTypes = $type->getTypes();
            $glue = '|';
        } elseif ($type instanceof \ReflectionIntersectionType) {
            $reflectionTypes = $type->getTypes();
            $glue = '&';
        } else {
            $reflectionTypes = [$type];
            $glue = null;
        }

        foreach ($reflectionTypes as $type) {
            if ($type instanceof \ReflectionIntersectionType) {
                if ('' !== $name = '('.self::exportType($owner, $noBuiltin, $type).')') {
                    $types[] = $name;
                }
                continue;
            }
            $name = $type->getName();

            if ($noBuiltin && $type->isBuiltin()) {
                continue;
            }
            if (\in_array($name, ['parent', 'self'], true) && $class ??= $owner->getDeclaringClass()) {
                $name = 'parent' === $name ? ($class->getParentClass() ?: null)?->name ?? 'parent' : $class->name;
            }

            $types[] = ($noBuiltin || $type->isBuiltin() || 'static' === $name ? '' : '\\').$name;
        }

        if (!$types) {
            return '';
        }
        if (null === $glue) {
            $defaultNull = $owner instanceof \ReflectionParameter && 'NULL' === rtrim(substr(explode('$'.$owner->name.' = ', (string) $owner, 2)[1] ?? '', 0, -2));

            return (!$noBuiltin && ($type->allowsNull() || $defaultNull) && !\in_array($name, ['mixed', 'null'], true) ? '?' : '').$types[0];
        }
        sort($types);

        return implode($glue, $types);
    }

    private static function exportPropertyScopes(string $parent, array $propertyScopes): string
    {
        uksort($propertyScopes, 'strnatcmp');
        foreach ($propertyScopes as $k => $v) {
            unset($propertyScopes[$k][4]);
        }
        $propertyScopes = VarExporter::export($propertyScopes);
        $propertyScopes = str_replace(VarExporter::export($parent), 'parent::class', $propertyScopes);
        $propertyScopes = preg_replace("/(?|(,)\n( )       |\n        |,\n    (\]))/", '$1$2', $propertyScopes);

        return str_replace("\n", "\n    ", $propertyScopes);
    }

    private static function exportDefault(\ReflectionParameter $param, $namespace): string
    {
        $default = rtrim(substr(explode('$'.$param->name.' = ', (string) $param, 2)[1] ?? '', 0, -2));

        if (\in_array($default, ['<default>', 'NULL'], true)) {
            return 'null';
        }
        if (str_ends_with($default, "...'") && preg_match("/^'(?:[^'\\\\]*+(?:\\\\.)*+)*+'$/", $default)) {
            return VarExporter::export($param->getDefaultValue());
        }

        $regexp = "/(\"(?:[^\"\\\\]*+(?:\\\\.)*+)*+\"|'(?:[^'\\\\]*+(?:\\\\.)*+)*+')/";
        $parts = preg_split($regexp, $default, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY);

        $regexp = '/([\[\( ]|^)([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\\\\[a-zA-Z0-9_\x7f-\xff]++)*+)(\(?)(?!: )/';
        $callback = (false !== strpbrk($default, "\\:('") && $class = $param->getDeclaringClass())
            ? static fn ($m) => $m[1].match ($m[2]) {
                'new', 'false', 'true', 'null' => $m[2],
                'NULL' => 'null',
                'self' => '\\'.$class->name,
                'namespace\\parent',
                'parent' => ($parent = $class->getParentClass()) ? '\\'.$parent->name : 'parent',
                default => self::exportSymbol($m[2], '(' !== $m[3], $namespace),
            }.$m[3]
            : static fn ($m) => $m[1].match ($m[2]) {
                'new', 'false', 'true', 'null', 'self', 'parent' => $m[2],
                'NULL' => 'null',
                default => self::exportSymbol($m[2], '(' !== $m[3], $namespace),
            }.$m[3];

        return implode('', array_map(static fn ($part) => match ($part[0]) {
            '"' => $part, // for internal classes only
            "'" => false !== strpbrk($part, "\\\0\r\n") ? '"'.substr(str_replace(['$', "\0", "\r", "\n"], ['\$', '\0', '\r', '\n'], $part), 1, -1).'"' : $part,
            default => preg_replace_callback($regexp, $callback, $part),
        }, $parts));
    }

    private static function exportSymbol(string $symbol, bool $mightBeRootConst, string $namespace): string
    {
        if (!$mightBeRootConst
            || false === ($ns = strrpos($symbol, '\\'))
            || substr($symbol, 0, $ns) !== $namespace
            || \defined($symbol)
            || !\defined(substr($symbol, $ns + 1))
        ) {
            return '\\'.$symbol;
        }

        return '\\'.substr($symbol, $ns + 1);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\VarExporter;

use Symfony\Component\VarExporter\Exception\ExceptionInterface;
use Symfony\Component\VarExporter\Exception\NotInstantiableTypeException;
use Symfony\Component\VarExporter\Internal\Registry;

/**
 * A utility class to create objects without calling their constructor.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
final class Instantiator
{
    /**
     * Creates an object and sets its properties without calling its constructor nor any other methods.
     *
     * @see Hydrator::hydrate() for examples
     *
     * @template T of object
     *
     * @param class-string<T>                           $class            The class of the instance to create
     * @param array<string, mixed>                      $properties       The properties to set on the instance
     * @param array<class-string, array<string, mixed>> $scopedProperties The properties to set on the instance,
     *                                                                    keyed by their declaring class
     *
     * @return T
     *
     * @throws ExceptionInterface When the instance cannot be created
     */
    public static function instantiate(string $class, array $properties = [], array $scopedProperties = []): object
    {
        $reflector = Registry::$reflectors[$class] ??= Registry::getClassReflector($class);

        if (Registry::$cloneable[$class]) {
            $instance = clone Registry::$prototypes[$class];
        } elseif (Registry::$instantiableWithoutConstructor[$class]) {
            $instance = $reflector->newInstanceWithoutConstructor();
        } elseif (null === Registry::$prototypes[$class]) {
            throw new NotInstantiableTypeException($class);
        } elseif ($reflector->implementsInterface('Serializable') && !method_exists($class, '__unserialize')) {
            $instance = unserialize('C:'.\strlen($class).':"'.$class.'":0:{}', ['allowed_classes' => true]);
        } else {
            $instance = unserialize('O:'.\strlen($class).':"'.$class.'":0:{}', ['allowed_classes' => true]);
        }

        return $properties || $scopedProperties ? Hydrator::hydrate($instance, $properties, $scopedProperties) : $instance;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\VarExporter;

use Symfony\Component\Serializer\Attribute\Ignore;
use Symfony\Component\VarExporter\Internal\Hydrator;
use Symfony\Component\VarExporter\Internal\LazyObjectRegistry as Registry;
use Symfony\Component\VarExporter\Internal\LazyObjectState;
use Symfony\Component\VarExporter\Internal\LazyObjectTrait;

if (\PHP_VERSION_ID >= 80400) {
    trigger_deprecation('symfony/var-exporter', '7.3', 'The "%s" trait is deprecated, use native lazy objects instead.', LazyGhostTrait::class);
}

/**
 * @deprecated since Symfony 7.3, use native lazy objects instead
 */
trait LazyGhostTrait
{
    use LazyObjectTrait;

    /**
     * Creates a lazy-loading ghost instance.
     *
     * Skipped properties should be indexed by their array-cast identifier, see
     * https://php.net/manual/language.types.array#language.types.array.casting
     *
     * @param \Closure(static):void    $initializer       The closure should initialize the object it receives as argument
     * @param array<string, true>|null $skippedProperties An array indexed by the properties to skip, a.k.a. the ones
     *                                                    that the initializer doesn't initialize, if any
     * @param static|null              $instance
     */
    public static function createLazyGhost(\Closure $initializer, ?array $skippedProperties = null, ?object $instance = null): static
    {
        if (self::class !== $class = $instance ? $instance::class : static::class) {
            $skippedProperties["\0".self::class."\0lazyObjectState"] = true;
        }

        if (!isset(Registry::$defaultProperties[$class])) {
            Registry::$classReflectors[$class] ??= new \ReflectionClass($class);
            $instance ??= Registry::$classReflectors[$class]->newInstanceWithoutConstructor();
            Registry::$defaultProperties[$class] ??= (array) $instance;
            Registry::$classResetters[$class] ??= Registry::getClassResetters($class);

            if (self::class === $class && \defined($class.'::LAZY_OBJECT_PROPERTY_SCOPES')) {
                Hydrator::$propertyScopes[$class] ??= $class::LAZY_OBJECT_PROPERTY_SCOPES;
            }
        } else {
            $instance ??= Registry::$classReflectors[$class]->newInstanceWithoutConstructor();
        }

        if (isset($instance->lazyObjectState)) {
            $instance->lazyObjectState->initializer = $initializer;
            $instance->lazyObjectState->skippedProperties = $skippedProperties ??= [];

            if (LazyObjectState::STATUS_UNINITIALIZED_FULL !== $instance->lazyObjectState->status) {
                $instance->lazyObjectState->reset($instance);
            }

            return $instance;
        }

        $instance->lazyObjectState = new LazyObjectState($initializer, $skippedProperties ??= []);

        foreach (Registry::$classResetters[$class] as $reset) {
            $reset($instance, $skippedProperties);
        }

        return $instance;
    }

    /**
     * Returns whether the object is initialized.
     *
     * @param bool $partial Whether partially initialized objects should be considered as initialized
     */
    #[Ignore]
    public function isLazyObjectInitialized(bool $partial = false): bool
    {
        if (!$state = $this->lazyObjectState ?? null) {
            return true;
        }

        return LazyObjectState::STATUS_INITIALIZED_FULL === $state->status;
    }

    /**
     * Forces initialization of a lazy object and returns it.
     */
    public function initializeLazyObject(): static
    {
        if (!$state = $this->lazyObjectState ?? null) {
            return $this;
        }

        if (LazyObjectState::STATUS_UNINITIALIZED_FULL === $state->status) {
            $state->initialize($this, '', null);
        }

        return $this;
    }

    /**
     * @return bool Returns false when the object cannot be reset, ie when it's not a lazy object
     */
    public function resetLazyObject(): bool
    {
        if (!$state = $this->lazyObjectState ?? null) {
            return false;
        }

        if (LazyObjectState::STATUS_UNINITIALIZED_FULL !== $state->status) {
            $state->reset($this);
        }

        return true;
    }

    public function &__get($name): mixed
    {
        $propertyScopes = Hydrator::$propertyScopes[$this::class] ??= Hydrator::getPropertyScopes($this::class);
        $scope = null;
        $notByRef = 0;

        if ([$class, , $writeScope, $access] = $propertyScopes[$name] ?? null) {
            $scope = Registry::getScopeForRead($propertyScopes, $class, $name);
            $state = $this->lazyObjectState ?? null;

            if ($state && (null === $scope || isset($propertyScopes["\0$scope\0$name"]))) {
                $notByRef = $access & Hydrator::PROPERTY_NOT_BY_REF;

                if (LazyObjectState::STATUS_INITIALIZED_FULL === $state->status) {
                    // Work around php/php-src#12695
                    $property = null === $scope ? $name : "\0$scope\0$name";
                    $property = $propertyScopes[$property][4]
                        ?? Hydrator::$propertyScopes[$this::class][$property][4] = new \ReflectionProperty($scope ?? $class, $name);
                } else {
                    $property = null;
                }
                if (\PHP_VERSION_ID >= 80400 && !$notByRef && ($access >> 2) & \ReflectionProperty::IS_PRIVATE_SET) {
                    $scope ??= $writeScope;
                }

                if ($property?->isInitialized($this) ?? LazyObjectState::STATUS_UNINITIALIZED_PARTIAL !== $state->initialize($this, $name, $writeScope ?? $scope)) {
                    goto get_in_scope;
                }
            }
        }

        if ($parent = (Registry::$parentMethods[self::class] ??= Registry::getParentMethods(self::class))['get']) {
            if (2 === $parent) {
                return parent::__get($name);
            }
            $value = parent::__get($name);

            return $value;
        }

        if (null === $class) {
            $frame = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0];
            trigger_error(\sprintf('Undefined property: %s::$%s in %s on line %s', $this::class, $name, $frame['file'], $frame['line']), \E_USER_NOTICE);
        }

        get_in_scope:

        try {
            if (null === $scope) {
                if (!$notByRef) {
                    return $this->$name;
                }
                $value = $this->$name;

                return $value;
            }
            $accessor = Registry::$classAccessors[$scope] ??= Registry::getClassAccessors($scope);

            return $accessor['get']($this, $name, $notByRef);
        } catch (\Error $e) {
            if (\Error::class !== $e::class || !str_starts_with($e->getMessage(), 'Cannot access uninitialized non-nullable property')) {
                throw $e;
            }

            try {
                if (null === $scope) {
                    $this->$name = [];

                    return $this->$name;
                }

                $accessor['set']($this, $name, []);

                return $accessor['get']($this, $name, $notByRef);
            } catch (\Error) {
                if (preg_match('/^Cannot access uninitialized non-nullable property ([^ ]++) by reference$/', $e->getMessage(), $matches)) {
                    throw new \Error('Typed property '.$matches[1].' must not be accessed before initialization', $e->getCode(), $e->getPrevious());
                }

                throw $e;
            }
        }
    }

    public function __set($name, $value): void
    {
        $propertyScopes = Hydrator::$propertyScopes[$this::class] ??= Hydrator::getPropertyScopes($this::class);
        $scope = null;

        if ([$class, , $writeScope, $access] = $propertyScopes[$name] ?? null) {
            $scope = Registry::getScopeForWrite($propertyScopes, $class, $name, $access >> 2);
            $state = $this->lazyObjectState ?? null;

            if ($state && ($writeScope === $scope || isset($propertyScopes["\0$scope\0$name"]))
                && LazyObjectState::STATUS_INITIALIZED_FULL !== $state->status
            ) {
                if (LazyObjectState::STATUS_UNINITIALIZED_FULL === $state->status) {
                    $state->initialize($this, $name, $writeScope ?? $scope);
                }
                goto set_in_scope;
            }
        }

        if ((Registry::$parentMethods[self::class] ??= Registry::getParentMethods(self::class))['set']) {
            parent::__set($name, $value);

            return;
        }

        set_in_scope:

        if (null === $scope) {
            $this->$name = $value;
        } else {
            $accessor = Registry::$classAccessors[$scope] ??= Registry::getClassAccessors($scope);
            $accessor['set']($this, $name, $value);
        }
    }

    public function __isset($name): bool
    {
        $propertyScopes = Hydrator::$propertyScopes[$this::class] ??= Hydrator::getPropertyScopes($this::class);
        $scope = null;

        if ([$class, , $writeScope] = $propertyScopes[$name] ?? null) {
            $scope = Registry::getScopeForRead($propertyScopes, $class, $name);
            $state = $this->lazyObjectState ?? null;

            if ($state && (null === $scope || isset($propertyScopes["\0$scope\0$name"]))
                && LazyObjectState::STATUS_INITIALIZED_FULL !== $state->status
                && LazyObjectState::STATUS_UNINITIALIZED_PARTIAL !== $state->initialize($this, $name, $writeScope ?? $scope)
            ) {
                goto isset_in_scope;
            }
        }

        if ((Registry::$parentMethods[self::class] ??= Registry::getParentMethods(self::class))['isset']) {
            return parent::__isset($name);
        }

        isset_in_scope:

        if (null === $scope) {
            return isset($this->$name);
        }
        $accessor = Registry::$classAccessors[$scope] ??= Registry::getClassAccessors($scope);

        return $accessor['isset']($this, $name);
    }

    public function __unset($name): void
    {
        $propertyScopes = Hydrator::$propertyScopes[$this::class] ??= Hydrator::getPropertyScopes($this::class);
        $scope = null;

        if ([$class, , $writeScope, $access] = $propertyScopes[$name] ?? null) {
            $scope = Registry::getScopeForWrite($propertyScopes, $class, $name, $access >> 2);
            $state = $this->lazyObjectState ?? null;

            if ($state && ($writeScope === $scope || isset($propertyScopes["\0$scope\0$name"]))
                && LazyObjectState::STATUS_INITIALIZED_FULL !== $state->status
            ) {
                if (LazyObjectState::STATUS_UNINITIALIZED_FULL === $state->status) {
                    $state->initialize($this, $name, $writeScope ?? $scope);
                }
                goto unset_in_scope;
            }
        }

        if ((Registry::$parentMethods[self::class] ??= Registry::getParentMethods(self::class))['unset']) {
            parent::__unset($name);

            return;
        }

        unset_in_scope:

        if (null === $scope) {
            unset($this->$name);
        } else {
            $accessor = Registry::$classAccessors[$scope] ??= Registry::getClassAccessors($scope);
            $accessor['unset']($this, $name);
        }
    }

    public function __clone(): void
    {
        if ($state = $this->lazyObjectState ?? null) {
            $this->lazyObjectState = clone $state;
        }

        if ((Registry::$parentMethods[self::class] ??= Registry::getParentMethods(self::class))['clone']) {
            parent::__clone();
        }
    }

    public function __serialize(): array
    {
        $class = self::class;

        if ((Registry::$parentMethods[$class] ??= Registry::getParentMethods($class))['serialize']) {
            $properties = parent::__serialize();
        } else {
            $this->initializeLazyObject();
            $properties = (array) $this;
        }
        unset($properties["\0$class\0lazyObjectState"]);

        if (Registry::$parentMethods[$class]['serialize'] || !Registry::$parentMethods[$class]['sleep']) {
            return $properties;
        }

        $scope = get_parent_class($class);
        $data = [];

        foreach (parent::__sleep() as $name) {
            $value = $properties[$k = $name] ?? $properties[$k = "\0*\0$name"] ?? $properties[$k = "\0$class\0$name"] ?? $properties[$k = "\0$scope\0$name"] ?? $k = null;

            if (null === $k) {
                trigger_error(\sprintf('serialize(): "%s" returned as member variable from __sleep() but does not exist', $name), \E_USER_NOTICE);
            } else {
                $data[$k] = $value;
            }
        }

        return $data;
    }

    public function __destruct()
    {
        $state = $this->lazyObjectState ?? null;

        if (LazyObjectState::STATUS_UNINITIALIZED_FULL === $state?->status) {
            return;
        }

        if ((Registry::$parentMethods[self::class] ??= Registry::getParentMethods(self::class))['destruct']) {
            parent::__destruct();
        }
    }

    #[Ignore]
    private function setLazyObjectAsInitialized(bool $initialized): void
    {
        if ($state = $this->lazyObjectState ?? null) {
            $state->status = $initialized ? LazyObjectState::STATUS_INITIALIZED_FULL : LazyObjectState::STATUS_UNINITIALIZED_FULL;
        }
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\String;

use Random\Randomizer;
use Symfony\Component\String\Exception\ExceptionInterface;
use Symfony\Component\String\Exception\InvalidArgumentException;
use Symfony\Component\String\Exception\RuntimeException;

/**
 * Represents a binary-safe string of bytes.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 * @author Hugo Hamon <hugohamon@neuf.fr>
 *
 * @throws ExceptionInterface
 */
class ByteString extends AbstractString
{
    private const ALPHABET_ALPHANUMERIC = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';

    public function __construct(string $string = '')
    {
        $this->string = $string;
    }

    /*
     * The following method was derived from code of the Hack Standard Library (v4.40 - 2020-05-03)
     *
     * https://github.com/hhvm/hsl/blob/80a42c02f036f72a42f0415e80d6b847f4bf62d5/src/random/private.php#L16
     *
     * Code subject to the MIT license (https://github.com/hhvm/hsl/blob/master/LICENSE).
     *
     * Copyright (c) 2004-2020, Facebook, Inc. (https://www.facebook.com/)
     */

    public static function fromRandom(int $length = 16, ?string $alphabet = null): self
    {
        if ($length <= 0) {
            throw new InvalidArgumentException(\sprintf('A strictly positive length is expected, "%d" given.', $length));
        }

        $alphabet ??= self::ALPHABET_ALPHANUMERIC;
        $alphabetSize = \strlen($alphabet);
        $bits = (int) ceil(log($alphabetSize, 2.0));
        if ($bits <= 0 || $bits > 56) {
            throw new InvalidArgumentException('The length of the alphabet must in the [2^1, 2^56] range.');
        }

        if (\PHP_VERSION_ID >= 80300) {
            return new static((new Randomizer())->getBytesFromString($alphabet, $length));
        }

        $ret = '';
        while ($length > 0) {
            $urandomLength = (int) ceil(2 * $length * $bits / 8.0);
            $data = random_bytes($urandomLength);
            $unpackedData = 0;
            $unpackedBits = 0;
            for ($i = 0; $i < $urandomLength && $length > 0; ++$i) {
                // Unpack 8 bits
                $unpackedData = ($unpackedData << 8) | \ord($data[$i]);
                $unpackedBits += 8;

                // While we have enough bits to select a character from the alphabet, keep
                // consuming the random data
                for (; $unpackedBits >= $bits && $length > 0; $unpackedBits -= $bits) {
                    $index = ($unpackedData & ((1 << $bits) - 1));
                    $unpackedData >>= $bits;
                    // Unfortunately, the alphabet size is not necessarily a power of two.
                    // Worst case, it is 2^k + 1, which means we need (k+1) bits and we
                    // have around a 50% chance of missing as k gets larger
                    if ($index < $alphabetSize) {
                        $ret .= $alphabet[$index];
                        --$length;
                    }
                }
            }
        }

        return new static($ret);
    }

    public function bytesAt(int $offset): array
    {
        $str = $this->string[$offset] ?? '';

        return '' === $str ? [] : [\ord($str)];
    }

    public function append(string ...$suffix): static
    {
        $str = clone $this;
        $str->string .= 1 >= \count($suffix) ? ($suffix[0] ?? '') : implode('', $suffix);

        return $str;
    }

    public function camel(): static
    {
        $str = clone $this;

        $parts = explode(' ', trim(ucwords(preg_replace('/[^a-zA-Z0-9\x7f-\xff]++/', ' ', $this->string))));
        $parts[0] = 1 !== \strlen($parts[0]) && ctype_upper($parts[0]) ? $parts[0] : lcfirst($parts[0]);
        $str->string = implode('', $parts);

        return $str;
    }

    public function chunk(int $length = 1): array
    {
        if (1 > $length) {
            throw new InvalidArgumentException('The chunk length must be greater than zero.');
        }

        if ('' === $this->string) {
            return [];
        }

        $str = clone $this;
        $chunks = [];

        foreach (str_split($this->string, $length) as $chunk) {
            $str->string = $chunk;
            $chunks[] = clone $str;
        }

        return $chunks;
    }

    public function endsWith(string|iterable|AbstractString $suffix): bool
    {
        if ($suffix instanceof AbstractString) {
            $suffix = $suffix->string;
        } elseif (!\is_string($suffix)) {
            return parent::endsWith($suffix);
        }

        return '' !== $suffix && \strlen($this->string) >= \strlen($suffix) && 0 === substr_compare($this->string, $suffix, -\strlen($suffix), null, $this->ignoreCase);
    }

    public function equalsTo(string|iterable|AbstractString $string): bool
    {
        if ($string instanceof AbstractString) {
            $string = $string->string;
        } elseif (!\is_string($string)) {
            return parent::equalsTo($string);
        }

        if ('' !== $string && $this->ignoreCase) {
            return 0 === strcasecmp($string, $this->string);
        }

        return $string === $this->string;
    }

    public function folded(): static
    {
        $str = clone $this;
        $str->string = strtolower($str->string);

        return $str;
    }

    public function indexOf(string|iterable|AbstractString $needle, int $offset = 0): ?int
    {
        if ($needle instanceof AbstractString) {
            $needle = $needle->string;
        } elseif (!\is_string($needle)) {
            return parent::indexOf($needle, $offset);
        }

        if ('' === $needle) {
            return null;
        }

        try {
            $i = $this->ignoreCase ? stripos($this->string, $needle, $offset) : strpos($this->string, $needle, $offset);
        } catch (\ValueError) {
            return null;
        }

        return false === $i ? null : $i;
    }

    public function indexOfLast(string|iterable|AbstractString $needle, int $offset = 0): ?int
    {
        if ($needle instanceof AbstractString) {
            $needle = $needle->string;
        } elseif (!\is_string($needle)) {
            return parent::indexOfLast($needle, $offset);
        }

        if ('' === $needle) {
            return null;
        }

        try {
            $i = $this->ignoreCase ? strripos($this->string, $needle, $offset) : strrpos($this->string, $needle, $offset);
        } catch (\ValueError) {
            return null;
        }

        return false === $i ? null : $i;
    }

    public function isUtf8(): bool
    {
        return '' === $this->string || preg_match('//u', $this->string);
    }

    public function join(array $strings, ?string $lastGlue = null): static
    {
        $str = clone $this;

        $tail = null !== $lastGlue && 1 < \count($strings) ? $lastGlue.array_pop($strings) : '';
        $str->string = implode($this->string, $strings).$tail;

        return $str;
    }

    public function length(): int
    {
        return \strlen($this->string);
    }

    public function lower(): static
    {
        $str = clone $this;
        $str->string = strtolower($str->string);

        return $str;
    }

    public function match(string $regexp, int $flags = 0, int $offset = 0): array
    {
        $match = ((\PREG_PATTERN_ORDER | \PREG_SET_ORDER) & $flags) ? 'preg_match_all' : 'preg_match';

        if ($this->ignoreCase) {
            $regexp .= 'i';
        }

        set_error_handler(static fn ($t, $m) => throw new InvalidArgumentException($m));

        try {
            if (false === $match($regexp, $this->string, $matches, $flags | \PREG_UNMATCHED_AS_NULL, $offset)) {
                throw new RuntimeException('Matching failed with error: '.preg_last_error_msg());
            }
        } finally {
            restore_error_handler();
        }

        return $matches;
    }

    public function padBoth(int $length, string $padStr = ' '): static
    {
        $str = clone $this;
        $str->string = str_pad($this->string, $length, $padStr, \STR_PAD_BOTH);

        return $str;
    }

    public function padEnd(int $length, string $padStr = ' '): static
    {
        $str = clone $this;
        $str->string = str_pad($this->string, $length, $padStr, \STR_PAD_RIGHT);

        return $str;
    }

    public function padStart(int $length, string $padStr = ' '): static
    {
        $str = clone $this;
        $str->string = str_pad($this->string, $length, $padStr, \STR_PAD_LEFT);

        return $str;
    }

    public function prepend(string ...$prefix): static
    {
        $str = clone $this;
        $str->string = (1 >= \count($prefix) ? ($prefix[0] ?? '') : implode('', $prefix)).$str->string;

        return $str;
    }

    public function replace(string $from, string $to): static
    {
        $str = clone $this;

        if ('' !== $from) {
            $str->string = $this->ignoreCase ? str_ireplace($from, $to, $this->string) : str_replace($from, $to, $this->string);
        }

        return $str;
    }

    public function replaceMatches(string $fromRegexp, string|callable $to): static
    {
        if ($this->ignoreCase) {
            $fromRegexp .= 'i';
        }

        $replace = \is_array($to) || $to instanceof \Closure ? 'preg_replace_callback' : 'preg_replace';

        set_error_handler(static fn ($t, $m) => throw new InvalidArgumentException($m));

        try {
            if (null === $string = $replace($fromRegexp, $to, $this->string)) {
                $lastError = preg_last_error();

                foreach (get_defined_constants(true)['pcre'] as $k => $v) {
                    if ($lastError === $v && str_ends_with($k, '_ERROR')) {
                        throw new RuntimeException('Matching failed with '.$k.'.');
                    }
                }

                throw new RuntimeException('Matching failed with unknown error code.');
            }
        } finally {
            restore_error_handler();
        }

        $str = clone $this;
        $str->string = $string;

        return $str;
    }

    public function reverse(): static
    {
        $str = clone $this;
        $str->string = strrev($str->string);

        return $str;
    }

    public function slice(int $start = 0, ?int $length = null): static
    {
        $str = clone $this;
        $str->string = substr($this->string, $start, $length ?? \PHP_INT_MAX);

        return $str;
    }

    public function snake(): static
    {
        $str = $this->camel();
        $str->string = strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], '\1_\2', $str->string));

        return $str;
    }

    public function splice(string $replacement, int $start = 0, ?int $length = null): static
    {
        $str = clone $this;
        $str->string = substr_replace($this->string, $replacement, $start, $length ?? \PHP_INT_MAX);

        return $str;
    }

    public function split(string $delimiter, ?int $limit = null, ?int $flags = null): array
    {
        if (1 > $limit ??= \PHP_INT_MAX) {
            throw new InvalidArgumentException('Split limit must be a positive integer.');
        }

        if ('' === $delimiter) {
            throw new InvalidArgumentException('Split delimiter is empty.');
        }

        if (null !== $flags) {
            return parent::split($delimiter, $limit, $flags);
        }

        $str = clone $this;
        $chunks = $this->ignoreCase
            ? preg_split('{'.preg_quote($delimiter).'}iD', $this->string, $limit)
            : explode($delimiter, $this->string, $limit);

        foreach ($chunks as &$chunk) {
            $str->string = $chunk;
            $chunk = clone $str;
        }

        return $chunks;
    }

    public function startsWith(string|iterable|AbstractString $prefix): bool
    {
        if ($prefix instanceof AbstractString) {
            $prefix = $prefix->string;
        } elseif (!\is_string($prefix)) {
            return parent::startsWith($prefix);
        }

        return '' !== $prefix && 0 === ($this->ignoreCase ? strncasecmp($this->string, $prefix, \strlen($prefix)) : strncmp($this->string, $prefix, \strlen($prefix)));
    }

    public function title(bool $allWords = false): static
    {
        $str = clone $this;
        $str->string = $allWords ? ucwords($str->string) : ucfirst($str->string);

        return $str;
    }

    public function toUnicodeString(?string $fromEncoding = null): UnicodeString
    {
        return new UnicodeString($this->toCodePointString($fromEncoding)->string);
    }

    public function toCodePointString(?string $fromEncoding = null): CodePointString
    {
        $u = new CodePointString();

        if (\in_array($fromEncoding, [null, 'utf8', 'utf-8', 'UTF8', 'UTF-8'], true) && preg_match('//u', $this->string)) {
            $u->string = $this->string;

            return $u;
        }

        set_error_handler(static fn ($t, $m) => throw new InvalidArgumentException($m));

        try {
            try {
                $validEncoding = false !== mb_detect_encoding($this->string, $fromEncoding ?? 'Windows-1252', true);
            } catch (InvalidArgumentException $e) {
                if (!\function_exists('iconv')) {
                    throw $e;
                }

                $u->string = iconv($fromEncoding ?? 'Windows-1252', 'UTF-8', $this->string);

                return $u;
            }
        } finally {
            restore_error_handler();
        }

        if (!$validEncoding) {
            throw new InvalidArgumentException(\sprintf('Invalid "%s" string.', $fromEncoding ?? 'Windows-1252'));
        }

        $u->string = mb_convert_encoding($this->string, 'UTF-8', $fromEncoding ?? 'Windows-1252');

        return $u;
    }

    public function trim(string $chars = " \t\n\r\0\x0B\x0C"): static
    {
        $str = clone $this;
        $str->string = trim($str->string, $chars);

        return $str;
    }

    public function trimEnd(string $chars = " \t\n\r\0\x0B\x0C"): static
    {
        $str = clone $this;
        $str->string = rtrim($str->string, $chars);

        return $str;
    }

    public function trimStart(string $chars = " \t\n\r\0\x0B\x0C"): static
    {
        $str = clone $this;
        $str->string = ltrim($str->string, $chars);

        return $str;
    }

    public function upper(): static
    {
        $str = clone $this;
        $str->string = strtoupper($str->string);

        return $str;
    }

    public function width(bool $ignoreAnsiDecoration = true): int
    {
        $string = preg_match('//u', $this->string) ? $this->string : preg_replace('/[\x80-\xFF]/', '?', $this->string);

        return (new CodePointString($string))->width($ignoreAnsiDecoration);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\String\Inflector;

/**
 * French inflector.
 *
 * This class does only inflect nouns; not adjectives nor composed words like "soixante-dix".
 */
final class FrenchInflector implements InflectorInterface
{
    /**
     * A list of all rules for pluralise.
     *
     * @see https://la-conjugaison.nouvelobs.com/regles/grammaire/le-pluriel-des-noms-121.php
     */
    private const PLURALIZE_REGEXP = [
        // First entry: regexp
        // Second entry: replacement

        // Words finishing with "s", "x" or "z" are invariables
        // Les mots finissant par "s", "x" ou "z" sont invariables
        ['/(s|x|z)$/i', '\1'],

        // Words finishing with "eau" are pluralized with a "x"
        // Les mots finissant par "eau" prennent tous un "x" au pluriel
        ['/(eau)$/i', '\1x'],

        // Words finishing with "au" are pluralized with a "x" excepted "landau"
        // Les mots finissant par "au" prennent un "x" au pluriel sauf "landau"
        ['/^(landau)$/i', '\1s'],
        ['/(au)$/i', '\1x'],

        // Words finishing with "eu" are pluralized with a "x" excepted "pneu", "bleu", "émeu"
        // Les mots finissant en "eu" prennent un "x" au pluriel sauf "pneu", "bleu", "émeu"
        ['/^(pneu|bleu|émeu)$/i', '\1s'],
        ['/(eu)$/i', '\1x'],

        // Words finishing with "al" are pluralized with a "aux" excepted
        // Les mots finissant en "al" se terminent en "aux" sauf
        ['/^(bal|carnaval|caracal|chacal|choral|corral|étal|festival|récital|val)$/i', '\1s'],
        ['/al$/i', '\1aux'],

        // Aspirail, bail, corail, émail, fermail, soupirail, travail, vantail et vitrail font leur pluriel en -aux
        ['/^(aspir|b|cor|ém|ferm|soupir|trav|vant|vitr)ail$/i', '\1aux'],

        // Bijou, caillou, chou, genou, hibou, joujou et pou qui prennent un x au pluriel
        ['/^(bij|caill|ch|gen|hib|jouj|p)ou$/i', '\1oux'],

        // Invariable words
        ['/^(cinquante|soixante|mille)$/i', '\1'],

        // French titles
        ['/^(mon|ma)(sieur|dame|demoiselle|seigneur)$/', 'mes\2s'],
        ['/^(Mon|Ma)(sieur|dame|demoiselle|seigneur)$/', 'Mes\2s'],
    ];

    /**
     * A list of all rules for singularize.
     */
    private const SINGULARIZE_REGEXP = [
        // First entry: regexp
        // Second entry: replacement

        // Aspirail, bail, corail, émail, fermail, soupirail, travail, vantail et vitrail font leur pluriel en -aux
        ['/((aspir|b|cor|ém|ferm|soupir|trav|vant|vitr))aux$/i', '\1ail'],

        // Words finishing with "eau" are pluralized with a "x"
        // Les mots finissant par "eau" prennent tous un "x" au pluriel
        ['/(eau)x$/i', '\1'],

        // Words finishing with "al" are pluralized with a "aux" expected
        // Les mots finissant en "al" se terminent en "aux" sauf
        ['/(amir|anim|arsen|boc|can|capit|capor|chev|crist|génér|hopit|hôpit|idé|journ|littor|loc|m|mét|minér|princip|radic|termin)aux$/i', '\1al'],

        // Words finishing with "au" are pluralized with a "x" excepted "landau"
        // Les mots finissant par "au" prennent un "x" au pluriel sauf "landau"
        ['/(au)x$/i', '\1'],

        // Words finishing with "eu" are pluralized with a "x" excepted "pneu", "bleu", "émeu"
        // Les mots finissant en "eu" prennent un "x" au pluriel sauf "pneu", "bleu", "émeu"
        ['/(eu)x$/i', '\1'],

        //  Words finishing with "ou" are pluralized with a "s" excepted bijou, caillou, chou, genou, hibou, joujou, pou
        // Les mots finissant par "ou" prennent un "s" sauf bijou, caillou, chou, genou, hibou, joujou, pou
        ['/(bij|caill|ch|gen|hib|jouj|p)oux$/i', '\1ou'],

        // French titles
        ['/^mes(dame|demoiselle)s$/', 'ma\1'],
        ['/^Mes(dame|demoiselle)s$/', 'Ma\1'],
        ['/^mes(sieur|seigneur)s$/', 'mon\1'],
        ['/^Mes(sieur|seigneur)s$/', 'Mon\1'],

        // Default rule
        ['/s$/i', ''],
    ];

    /**
     * A list of words which should not be inflected.
     * This list is only used by singularize.
     */
    private const UNINFLECTED = '/^(abcès|accès|abus|albatros|anchois|anglais|autobus|bois|brebis|carquois|cas|chas|colis|concours|corps|cours|cyprès|décès|devis|discours|dos|embarras|engrais|entrelacs|excès|fils|fois|gâchis|gars|glas|héros|intrus|jars|jus|kermès|lacis|legs|lilas|marais|mars|matelas|mépris|mets|mois|mors|obus|os|palais|paradis|parcours|pardessus|pays|plusieurs|poids|pois|pouls|printemps|processus|progrès|puits|pus|rabais|radis|recors|recours|refus|relais|remords|remous|rictus|rhinocéros|repas|rubis|sans|sas|secours|sens|souris|succès|talus|tapis|tas|taudis|temps|tiers|univers|velours|verglas|vernis|virus)$/i';

    public function singularize(string $plural): array
    {
        if ($this->isInflectedWord($plural)) {
            return [$plural];
        }

        foreach (self::SINGULARIZE_REGEXP as $rule) {
            [$regexp, $replace] = $rule;

            if (1 === preg_match($regexp, $plural)) {
                return [preg_replace($regexp, $replace, $plural)];
            }
        }

        return [$plural];
    }

    public function pluralize(string $singular): array
    {
        if ($this->isInflectedWord($singular)) {
            return [$singular];
        }

        foreach (self::PLURALIZE_REGEXP as $rule) {
            [$regexp, $replace] = $rule;

            if (1 === preg_match($regexp, $singular)) {
                return [preg_replace($regexp, $replace, $singular)];
            }
        }

        return [$singular.'s'];
    }

    private function isInflectedWord(string $word): bool
    {
        return 1 === preg_match(self::UNINFLECTED, $word);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\String\Inflector;

final class SpanishInflector implements InflectorInterface
{
    /**
     * A list of all rules for pluralise.
     *
     * @see https://www.spanishdict.com/guide/spanish-plural-noun-forms
     * @see https://www.rae.es/gram%C3%A1tica/morfolog%C3%ADa/la-formaci%C3%B3n-del-plural-plurales-en-s-y-plurales-en-es-reglas-generales
     */
    // First entry: regex
    // Second entry: replacement
    private const PLURALIZE_REGEXP = [
        // Specials sí, no
        ['/(sí|no)$/i', '\1es'],

        // Words ending with vowel must use -s (RAE 3.2a, 3.2c)
        ['/(a|e|i|o|u|á|é|í|ó|ú)$/i', '\1s'],

        // Word ending in s or x and the previous letter is accented (RAE 3.2n)
        ['/ás$/i', 'ases'],
        ['/és$/i', 'eses'],
        ['/ís$/i', 'ises'],
        ['/ós$/i', 'oses'],
        ['/ús$/i', 'uses'],

        // Words ending in -ión must changed to -iones
        ['/ión$/i', '\1iones'],

        // Words ending in some consonants must use -es (RAE 3.2k)
        ['/(l|r|n|d|j|s|x|ch|y)$/i', '\1es'],

        // Word ending in z, must changed to ces
        ['/(z)$/i', 'ces'],
    ];

    /**
     * A list of all rules for singularize.
     */
    private const SINGULARIZE_REGEXP = [
        // Specials sí, no
        ['/(sí|no)es$/i', '\1'],

        // Words ending in -ión must changed to -iones
        ['/iones$/i', '\1ión'],

        // Word ending in z, must changed to ces
        ['/ces$/i', 'z'],

        // Word ending in s or x and the previous letter is accented (RAE 3.2n)
        ['/(\w)ases$/i', '\1ás'],
        ['/eses$/i', 'és'],
        ['/ises$/i', 'ís'],
        ['/(\w{2,})oses$/i', '\1ós'],
        ['/(\w)uses$/i', '\1ús'],

        // Words ending in some consonants and -es, must be the consonants
        ['/(l|r|n|d|j|s|x|ch|y)e?s$/i', '\1'],

        // Words ended with vowel and s, must be vowel
        ['/(a|e|i|o|u|á|é|ó|í|ú)s$/i', '\1'],
    ];

    private const UNINFLECTED_RULES = [
        // Words ending with pies (RAE 3.2n)
        '/.*(piés)$/i',
    ];

    private const UNINFLECTED = '/^(lunes|martes|miércoles|jueves|viernes|análisis|torax|yo|pies)$/i';

    public function singularize(string $plural): array
    {
        if ($this->isInflectedWord($plural)) {
            return [$plural];
        }

        foreach (self::SINGULARIZE_REGEXP as $rule) {
            [$regexp, $replace] = $rule;

            if (1 === preg_match($regexp, $plural)) {
                return [preg_replace($regexp, $replace, $plural)];
            }
        }

        return [$plural];
    }

    public function pluralize(string $singular): array
    {
        if ($this->isInflectedWord($singular)) {
            return [$singular];
        }

        foreach (self::PLURALIZE_REGEXP as $rule) {
            [$regexp, $replace] = $rule;

            if (1 === preg_match($regexp, $singular)) {
                return [preg_replace($regexp, $replace, $singular)];
            }
        }

        return [$singular.'s'];
    }

    private function isInflectedWord(string $word): bool
    {
        foreach (self::UNINFLECTED_RULES as $rule) {
            if (1 === preg_match($rule, $word)) {
                return true;
            }
        }

        return 1 === preg_match(self::UNINFLECTED, $word);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\String\Inflector;

interface InflectorInterface
{
    /**
     * Returns the singular forms of a string.
     *
     * If the method can't determine the form with certainty, several possible singulars are returned.
     *
     * @return string[]
     */
    public function singularize(string $plural): array;

    /**
     * Returns the plural forms of a string.
     *
     * If the method can't determine the form with certainty, several possible plurals are returned.
     *
     * @return string[]
     */
    public function pluralize(string $singular): array;
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\String\Inflector;

final class EnglishInflector implements InflectorInterface
{
    /**
     * Map English plural to singular suffixes.
     *
     * @see http://english-zone.com/spelling/plurals.html
     */
    private const PLURAL_MAP = [
        // First entry: plural suffix, reversed
        // Second entry: length of plural suffix
        // Third entry: Whether the suffix may succeed a vowel
        // Fourth entry: Whether the suffix may succeed a consonant
        // Fifth entry: singular suffix, normal

        // insignias (insigne), insignia (insigne)
        ['saingisni', 9, true, true, 'insigne'],
        ['aingisni', 8, true, true, 'insigne'],

        // passersby (passerby)
        ['ybsressap', 9, true, true, 'passerby'],

        // nodes (node)
        ['sedon', 5, true, true, 'node'],

        // bacteria (bacterium)
        ['airetcab', 8, true, true, 'bacterium'],

        // issues (issue)
        ['seussi', 6, true, true, 'issue'],

        // corpora (corpus)
        ['aroproc', 7, true, true, 'corpus'],

        // criteria (criterion)
        ['airetirc', 8, true, true, 'criterion'],

        // curricula (curriculum)
        ['alucirruc', 9, true, true, 'curriculum'],

        // quora (quorum)
        ['arouq', 5, true, true, 'quorum'],

        // genera (genus)
        ['areneg', 6, true, true, 'genus'],

        // media (medium)
        ['aidem', 5, true, true, 'medium'],

        // memoranda (memorandum)
        ['adnaromem', 9, true, true, 'memorandum'],

        // phenomena (phenomenon)
        ['anemonehp', 9, true, true, 'phenomenon'],

        // strata (stratum)
        ['atarts', 6, true, true, 'stratum'],

        // nebulae (nebula)
        ['ea', 2, true, true, 'a'],

        // services (service)
        ['secivres', 8, true, true, 'service'],

        // mice (mouse), lice (louse)
        ['eci', 3, false, true, 'ouse'],

        // geese (goose)
        ['esee', 4, false, true, 'oose'],

        // fungi (fungus), alumni (alumnus), syllabi (syllabus), radii (radius)
        ['i', 1, true, true, 'us'],

        // men (man), women (woman)
        ['nem', 3, true, true, 'man'],

        // children (child)
        ['nerdlihc', 8, true, true, 'child'],

        // oxen (ox)
        ['nexo', 4, false, false, 'ox'],

        // indices (index), appendices (appendix), prices (price)
        ['seci', 4, false, true, ['ex', 'ix', 'ice']],

        // codes (code)
        ['sedoc', 5, false, true, 'code'],

        // selfies (selfie)
        ['seifles', 7, true, true, 'selfie'],

        // zombies (zombie)
        ['seibmoz', 7, true, true, 'zombie'],

        // movies (movie)
        ['seivom', 6, true, true, 'movie'],

        // names (name)
        ['seman', 5, true, false, 'name'],

        // conspectuses (conspectus), prospectuses (prospectus)
        ['sesutcep', 8, true, true, 'pectus'],

        // feet (foot)
        ['teef', 4, true, true, 'foot'],

        // geese (goose)
        ['eseeg', 5, true, true, 'goose'],

        // teeth (tooth)
        ['hteet', 5, true, true, 'tooth'],

        // news (news)
        ['swen', 4, true, true, 'news'],

        // series (series)
        ['seires', 6, true, true, 'series'],

        // babies (baby)
        ['sei', 3, false, true, 'y'],

        // accesses (access), addresses (address), kisses (kiss)
        ['sess', 4, true, false, 'ss'],

        // statuses (status)
        ['sesutats', 8, true, true, 'status'],

        // article (articles), ancle (ancles)
        ['sel', 3, true, true, 'le'],

        // analyses (analysis), ellipses (ellipsis), fungi (fungus),
        // neuroses (neurosis), theses (thesis), emphases (emphasis),
        // oases (oasis), crises (crisis), houses (house), bases (base),
        // atlases (atlas)
        ['ses', 3, true, true, ['s', 'se', 'sis']],

        // objectives (objective), alternative (alternatives)
        ['sevit', 5, true, true, 'tive'],

        // drives (drive)
        ['sevird', 6, false, true, 'drive'],

        // lives (life), wives (wife)
        ['sevi', 4, false, true, 'ife'],

        // moves (move)
        ['sevom', 5, true, true, 'move'],

        // hooves (hoof), dwarves (dwarf), elves (elf), leaves (leaf), caves (cave), staves (staff)
        ['sev', 3, true, true, ['f', 've', 'ff']],

        // axes (axis), axes (ax), axes (axe)
        ['sexa', 4, false, false, ['ax', 'axe', 'axis']],

        // indexes (index), matrixes (matrix)
        ['sex', 3, true, false, 'x'],

        // quizzes (quiz)
        ['sezz', 4, true, false, 'z'],

        // bureaus (bureau)
        ['suae', 4, false, true, 'eau'],

        // fees (fee), trees (tree), employees (employee)
        ['see', 3, true, true, 'ee'],

        // edges (edge)
        ['segd', 4, true, true, 'dge'],

        // outages (outage) - specific fix to avoid 'outag'
        ['segatuo', 7, true, true, 'outage'],

        // traces (trace), faces (face), places (place), pieces (piece)
        ['sec', 3, true, true, 'ce'],

        // roses (rose), garages (garage), cassettes (cassette),
        // waltzes (waltz), heroes (hero), bushes (bush), arches (arch),
        // shoes (shoe)
        ['se', 2, true, true, ['', 'e']],

        // status (status)
        ['sutats', 6, true, true, 'status'],

        // tags (tag)
        ['s', 1, true, true, ''],

        // chateaux (chateau)
        ['xuae', 4, false, true, 'eau'],

        // people (person)
        ['elpoep', 6, true, true, 'person'],
    ];

    /**
     * Map English singular to plural suffixes.
     *
     * @see http://english-zone.com/spelling/plurals.html
     */
    private const SINGULAR_MAP = [
        // First entry: singular suffix, reversed
        // Second entry: length of singular suffix
        // Third entry: Whether the suffix may succeed a vowel
        // Fourth entry: Whether the suffix may succeed a consonant
        // Fifth entry: plural suffix, normal

        // passerby (passersby)
        ['ybressap', 8, true, true, 'passersby'],

        // insigne (insignia, insignias)
        ['engisni', 7, true, true, ['insignia', 'insignias']],

        // nodes (node)
        ['edon', 4, true, true, 'nodes'],

        // axes (axis)
        ['sixa', 4, false, false, 'axes'],

        // criterion (criteria)
        ['airetirc', 8, false, false, 'criterion'],

        // nebulae (nebula)
        ['aluben', 6, false, false, 'nebulae'],

        // children (child)
        ['dlihc', 5, true, true, 'children'],

        // prices (price)
        ['eci', 3, false, true, 'ices'],

        // services (service)
        ['ecivres', 7, true, true, 'services'],

        // lives (life), wives (wife)
        ['efi', 3, false, true, 'ives'],

        // selfies (selfie)
        ['eifles', 6, true, true, 'selfies'],

        // movies (movie)
        ['eivom', 5, true, true, 'movies'],

        // lice (louse)
        ['esuol', 5, false, true, 'lice'],

        // mice (mouse)
        ['esuom', 5, false, true, 'mice'],

        // geese (goose)
        ['esoo', 4, false, true, 'eese'],

        // houses (house), bases (base)
        ['es', 2, true, true, 'ses'],

        // geese (goose)
        ['esoog', 5, true, true, 'geese'],

        // caves (cave)
        ['ev', 2, true, true, 'ves'],

        // drives (drive)
        ['evird', 5, false, true, 'drives'],

        // objectives (objective), alternative (alternatives)
        ['evit', 4, true, true, 'tives'],

        // moves (move)
        ['evom', 4, true, true, 'moves'],

        // staves (staff)
        ['ffats', 5, true, true, 'staves'],

        // hooves (hoof), dwarves (dwarf), elves (elf), leaves (leaf)
        ['ff', 2, true, true, 'ffs'],

        // hooves (hoof), dwarves (dwarf), elves (elf), leaves (leaf)
        ['f', 1, true, true, ['fs', 'ves']],

        // arches (arch)
        ['hc', 2, true, true, 'ches'],

        // bushes (bush)
        ['hs', 2, true, true, 'shes'],

        // teeth (tooth)
        ['htoot', 5, true, true, 'teeth'],

        // albums (album)
        ['mubla', 5, true, true, 'albums'],

        // quorums (quorum)
        ['murouq', 6, true, true, ['quora', 'quorums']],

        // bacteria (bacterium), curricula (curriculum), media (medium), memoranda (memorandum), phenomena (phenomenon), strata (stratum)
        ['mu', 2, true, true, 'a'],

        // men (man), women (woman)
        ['nam', 3, true, true, 'men'],

        // people (person)
        ['nosrep', 6, true, true, ['persons', 'people']],

        // criteria (criterion)
        ['noiretirc', 9, true, true, 'criteria'],

        // phenomena (phenomenon)
        ['nonemonehp', 10, true, true, 'phenomena'],

        // echoes (echo)
        ['ohce', 4, true, true, 'echoes'],

        // heroes (hero)
        ['oreh', 4, true, true, 'heroes'],

        // atlases (atlas)
        ['salta', 5, true, true, 'atlases'],

        // aliases (alias)
        ['saila', 5, true, true, 'aliases'],

        // irises (iris)
        ['siri', 4, true, true, 'irises'],

        // analyses (analysis), ellipses (ellipsis), neuroses (neurosis)
        // theses (thesis), emphases (emphasis), oases (oasis),
        // crises (crisis)
        ['sis', 3, true, true, 'ses'],

        // accesses (access), addresses (address), kisses (kiss)
        ['ss', 2, true, false, 'sses'],

        // syllabi (syllabus)
        ['suballys', 8, true, true, 'syllabi'],

        // buses (bus)
        ['sub', 3, true, true, 'buses'],

        // circuses (circus)
        ['suc', 3, true, true, 'cuses'],

        // hippocampi (hippocampus)
        ['supmacoppih', 11, false, false, 'hippocampi'],

        // campuses (campus)
        ['sup', 3, true, true, 'puses'],

        // status (status)
        ['sutats', 6, true, true, ['status', 'statuses']],

        // conspectuses (conspectus), prospectuses (prospectus)
        ['sutcep', 6, true, true, 'pectuses'],

        // nexuses (nexus)
        ['suxen', 5, false, false, 'nexuses'],

        // fungi (fungus), alumni (alumnus), syllabi (syllabus), radii (radius)
        ['su', 2, true, true, 'i'],

        // news (news)
        ['swen', 4, true, true, 'news'],

        // feet (foot)
        ['toof', 4, true, true, 'feet'],

        // chateaux (chateau), bureaus (bureau)
        ['uae', 3, false, true, ['eaus', 'eaux']],

        // oxen (ox)
        ['xo', 2, false, false, 'oxen'],

        // hoaxes (hoax)
        ['xaoh', 4, true, false, 'hoaxes'],

        // indices (index)
        ['xedni', 5, false, true, ['indicies', 'indexes']],

        // fax (faxes, faxxes)
        ['xaf', 3, true, true, ['faxes', 'faxxes']],

        // boxes (box)
        ['xo', 2, false, true, 'oxes'],

        // indexes (index), matrixes (matrix), appendices (appendix)
        ['x', 1, true, false, ['ces', 'xes']],

        // babies (baby)
        ['y', 1, false, true, 'ies'],

        // quizzes (quiz)
        ['ziuq', 4, true, false, 'quizzes'],

        // waltzes (waltz)
        ['z', 1, true, true, 'zes'],
    ];

    /**
     * A list of words which should not be inflected, reversed.
     */
    private const UNINFLECTED = [
        '',

        // data
        'atad',

        // deer
        'reed',

        // equipment
        'tnempiuqe',

        // feedback
        'kcabdeef',

        // fish
        'hsif',

        // health
        'htlaeh',

        // history
        'yrotsih',

        // info
        'ofni',

        // information
        'noitamrofni',

        // money
        'yenom',

        // moose
        'esoom',

        // series
        'seires',

        // sheep
        'peehs',

        // species
        'seiceps',

        // traffic
        'ciffart',

        // aircraft
        'tfarcria',

        // hardware
        'erawdrah',
    ];

    public function singularize(string $plural): array
    {
        $pluralRev = strrev($plural);
        $lowerPluralRev = strtolower($pluralRev);
        $pluralLength = \strlen($lowerPluralRev);

        // Check if the word is one which is not inflected, return early if so
        if (\in_array($lowerPluralRev, self::UNINFLECTED, true)) {
            return [$plural];
        }

        // The outer loop iterates over the entries of the plural table
        // The inner loop $j iterates over the characters of the plural suffix
        // in the plural table to compare them with the characters of the actual
        // given plural suffix
        foreach (self::PLURAL_MAP as $map) {
            $suffix = $map[0];
            $suffixLength = $map[1];
            $j = 0;

            // Compare characters in the plural table and of the suffix of the
            // given plural one by one
            while ($suffix[$j] === $lowerPluralRev[$j]) {
                // Let $j point to the next character
                ++$j;

                // Successfully compared the last character
                // Add an entry with the singular suffix to the singular array
                if ($j === $suffixLength) {
                    // Is there any character preceding the suffix in the plural string?
                    if ($j < $pluralLength) {
                        $nextIsVowel = str_contains('aeiou', $lowerPluralRev[$j]);

                        if (!$map[2] && $nextIsVowel) {
                            // suffix may not succeed a vowel but next char is one
                            break;
                        }

                        if (!$map[3] && !$nextIsVowel) {
                            // suffix may not succeed a consonant but next char is one
                            break;
                        }
                    }

                    $newBase = substr($plural, 0, $pluralLength - $suffixLength);
                    $newSuffix = $map[4];

                    // Check whether the first character in the plural suffix
                    // is uppercased. If yes, uppercase the first character in
                    // the singular suffix too
                    $firstUpper = ctype_upper($pluralRev[$j - 1]);

                    if (\is_array($newSuffix)) {
                        $singulars = [];

                        foreach ($newSuffix as $newSuffixEntry) {
                            $singulars[] = $newBase.($firstUpper ? ucfirst($newSuffixEntry) : $newSuffixEntry);
                        }

                        return $singulars;
                    }

                    return [$newBase.($firstUpper ? ucfirst($newSuffix) : $newSuffix)];
                }

                // Suffix is longer than word
                if ($j === $pluralLength) {
                    break;
                }
            }
        }

        // Assume that plural and singular is identical
        return [$plural];
    }

    public function pluralize(string $singular): array
    {
        $singularRev = strrev($singular);
        $lowerSingularRev = strtolower($singularRev);
        $singularLength = \strlen($lowerSingularRev);

        // Check if the word is one which is not inflected, return early if so
        if (\in_array($lowerSingularRev, self::UNINFLECTED, true)) {
            return [$singular];
        }

        // The outer loop iterates over the entries of the singular table
        // The inner loop $j iterates over the characters of the singular suffix
        // in the singular table to compare them with the characters of the actual
        // given singular suffix
        foreach (self::SINGULAR_MAP as $map) {
            $suffix = $map[0];
            $suffixLength = $map[1];
            $j = 0;

            // Compare characters in the singular table and of the suffix of the
            // given plural one by one

            while ($suffix[$j] === $lowerSingularRev[$j]) {
                // Let $j point to the next character
                ++$j;

                // Successfully compared the last character
                // Add an entry with the plural suffix to the plural array
                if ($j === $suffixLength) {
                    // Is there any character preceding the suffix in the plural string?
                    if ($j < $singularLength) {
                        $nextIsVowel = str_contains('aeiou', $lowerSingularRev[$j]);

                        if (!$map[2] && $nextIsVowel) {
                            // suffix may not succeed a vowel but next char is one
                            break;
                        }

                        if (!$map[3] && !$nextIsVowel) {
                            // suffix may not succeed a consonant but next char is one
                            break;
                        }
                    }

                    $newBase = substr($singular, 0, $singularLength - $suffixLength);
                    $newSuffix = $map[4];

                    // Check whether the first character in the singular suffix
                    // is uppercased. If yes, uppercase the first character in
                    // the singular suffix too
                    $firstUpper = ctype_upper($singularRev[$j - 1]);

                    if (\is_array($newSuffix)) {
                        $plurals = [];

                        foreach ($newSuffix as $newSuffixEntry) {
                            $plurals[] = $newBase.($firstUpper ? ucfirst($newSuffixEntry) : $newSuffixEntry);
                        }

                        return $plurals;
                    }

                    return [$newBase.($firstUpper ? ucfirst($newSuffix) : $newSuffix)];
                }

                // Suffix is longer than word
                if ($j === $singularLength) {
                    break;
                }
            }
        }

        // Assume that plural is singular with a trailing `s`
        return [$singular.'s'];
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\String\Exception;

class RuntimeException extends \RuntimeException implements ExceptionInterface
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\String\Exception;

interface ExceptionInterface extends \Throwable
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\String\Exception;

class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\String;

use Symfony\Component\String\Exception\ExceptionInterface;
use Symfony\Component\String\Exception\InvalidArgumentException;
use Symfony\Component\String\Exception\RuntimeException;

/**
 * Represents a string of abstract Unicode characters.
 *
 * Unicode defines 3 types of "characters" (bytes, code points and grapheme clusters).
 * This class is the abstract type to use as a type-hint when the logic you want to
 * implement is Unicode-aware but doesn't care about code points vs grapheme clusters.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @throws ExceptionInterface
 */
abstract class AbstractUnicodeString extends AbstractString
{
    public const NFC = \Normalizer::NFC;
    public const NFD = \Normalizer::NFD;
    public const NFKC = \Normalizer::NFKC;
    public const NFKD = \Normalizer::NFKD;

    // all ASCII letters sorted by typical frequency of occurrence
    private const ASCII = "\x20\x65\x69\x61\x73\x6E\x74\x72\x6F\x6C\x75\x64\x5D\x5B\x63\x6D\x70\x27\x0A\x67\x7C\x68\x76\x2E\x66\x62\x2C\x3A\x3D\x2D\x71\x31\x30\x43\x32\x2A\x79\x78\x29\x28\x4C\x39\x41\x53\x2F\x50\x22\x45\x6A\x4D\x49\x6B\x33\x3E\x35\x54\x3C\x44\x34\x7D\x42\x7B\x38\x46\x77\x52\x36\x37\x55\x47\x4E\x3B\x4A\x7A\x56\x23\x48\x4F\x57\x5F\x26\x21\x4B\x3F\x58\x51\x25\x59\x5C\x09\x5A\x2B\x7E\x5E\x24\x40\x60\x7F\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F";

    // the subset of folded case mappings that is not in lower case mappings
    private const FOLD_FROM = ['İ', 'µ', 'ſ', "\xCD\x85", 'ς', 'ϐ', 'ϑ', 'ϕ', 'ϖ', 'ϰ', 'ϱ', 'ϵ', 'ẛ', "\xE1\xBE\xBE", 'ß', 'ŉ', 'ǰ', 'ΐ', 'ΰ', 'և', 'ẖ', 'ẗ', 'ẘ', 'ẙ', 'ẚ', 'ẞ', 'ὐ', 'ὒ', 'ὔ', 'ὖ', 'ᾀ', 'ᾁ', 'ᾂ', 'ᾃ', 'ᾄ', 'ᾅ', 'ᾆ', 'ᾇ', 'ᾈ', 'ᾉ', 'ᾊ', 'ᾋ', 'ᾌ', 'ᾍ', 'ᾎ', 'ᾏ', 'ᾐ', 'ᾑ', 'ᾒ', 'ᾓ', 'ᾔ', 'ᾕ', 'ᾖ', 'ᾗ', 'ᾘ', 'ᾙ', 'ᾚ', 'ᾛ', 'ᾜ', 'ᾝ', 'ᾞ', 'ᾟ', 'ᾠ', 'ᾡ', 'ᾢ', 'ᾣ', 'ᾤ', 'ᾥ', 'ᾦ', 'ᾧ', 'ᾨ', 'ᾩ', 'ᾪ', 'ᾫ', 'ᾬ', 'ᾭ', 'ᾮ', 'ᾯ', 'ᾲ', 'ᾳ', 'ᾴ', 'ᾶ', 'ᾷ', 'ᾼ', 'ῂ', 'ῃ', 'ῄ', 'ῆ', 'ῇ', 'ῌ', 'ῒ', 'ῖ', 'ῗ', 'ῢ', 'ῤ', 'ῦ', 'ῧ', 'ῲ', 'ῳ', 'ῴ', 'ῶ', 'ῷ', 'ῼ', 'ﬀ', 'ﬁ', 'ﬂ', 'ﬃ', 'ﬄ', 'ﬅ', 'ﬆ', 'ﬓ', 'ﬔ', 'ﬕ', 'ﬖ', 'ﬗ'];
    private const FOLD_TO = ['i̇', 'μ', 's', 'ι', 'σ', 'β', 'θ', 'φ', 'π', 'κ', 'ρ', 'ε', 'ṡ', 'ι', 'ss', 'ʼn', 'ǰ', 'ΐ', 'ΰ', 'եւ', 'ẖ', 'ẗ', 'ẘ', 'ẙ', 'aʾ', 'ss', 'ὐ', 'ὒ', 'ὔ', 'ὖ', 'ἀι', 'ἁι', 'ἂι', 'ἃι', 'ἄι', 'ἅι', 'ἆι', 'ἇι', 'ἀι', 'ἁι', 'ἂι', 'ἃι', 'ἄι', 'ἅι', 'ἆι', 'ἇι', 'ἠι', 'ἡι', 'ἢι', 'ἣι', 'ἤι', 'ἥι', 'ἦι', 'ἧι', 'ἠι', 'ἡι', 'ἢι', 'ἣι', 'ἤι', 'ἥι', 'ἦι', 'ἧι', 'ὠι', 'ὡι', 'ὢι', 'ὣι', 'ὤι', 'ὥι', 'ὦι', 'ὧι', 'ὠι', 'ὡι', 'ὢι', 'ὣι', 'ὤι', 'ὥι', 'ὦι', 'ὧι', 'ὰι', 'αι', 'άι', 'ᾶ', 'ᾶι', 'αι', 'ὴι', 'ηι', 'ήι', 'ῆ', 'ῆι', 'ηι', 'ῒ', 'ῖ', 'ῗ', 'ῢ', 'ῤ', 'ῦ', 'ῧ', 'ὼι', 'ωι', 'ώι', 'ῶ', 'ῶι', 'ωι', 'ff', 'fi', 'fl', 'ffi', 'ffl', 'st', 'st', 'մն', 'մե', 'մի', 'վն', 'մխ'];

    // the subset of https://github.com/unicode-org/cldr/blob/master/common/transforms/Latin-ASCII.xml that is not in NFKD
    private const TRANSLIT_FROM = ['Æ', 'Ð', 'Ø', 'Þ', 'ß', 'æ', 'ð', 'ø', 'þ', 'Đ', 'đ', 'Ħ', 'ħ', 'ı', 'ĸ', 'Ŀ', 'ŀ', 'Ł', 'ł', 'ŉ', 'Ŋ', 'ŋ', 'Œ', 'œ', 'Ŧ', 'ŧ', 'ƀ', 'Ɓ', 'Ƃ', 'ƃ', 'Ƈ', 'ƈ', 'Ɖ', 'Ɗ', 'Ƌ', 'ƌ', 'Ɛ', 'Ƒ', 'ƒ', 'Ɠ', 'ƕ', 'Ɩ', 'Ɨ', 'Ƙ', 'ƙ', 'ƚ', 'Ɲ', 'ƞ', 'Ƣ', 'ƣ', 'Ƥ', 'ƥ', 'ƫ', 'Ƭ', 'ƭ', 'Ʈ', 'Ʋ', 'Ƴ', 'ƴ', 'Ƶ', 'ƶ', 'Ǆ', 'ǅ', 'ǆ', 'Ǥ', 'ǥ', 'ȡ', 'Ȥ', 'ȥ', 'ȴ', 'ȵ', 'ȶ', 'ȷ', 'ȸ', 'ȹ', 'Ⱥ', 'Ȼ', 'ȼ', 'Ƚ', 'Ⱦ', 'ȿ', 'ɀ', 'Ƀ', 'Ʉ', 'Ɇ', 'ɇ', 'Ɉ', 'ɉ', 'Ɍ', 'ɍ', 'Ɏ', 'ɏ', 'ɓ', 'ɕ', 'ɖ', 'ɗ', 'ɛ', 'ɟ', 'ɠ', 'ɡ', 'ɢ', 'ɦ', 'ɧ', 'ɨ', 'ɪ', 'ɫ', 'ɬ', 'ɭ', 'ɱ', 'ɲ', 'ɳ', 'ɴ', 'ɶ', 'ɼ', 'ɽ', 'ɾ', 'ʀ', 'ʂ', 'ʈ', 'ʉ', 'ʋ', 'ʏ', 'ʐ', 'ʑ', 'ʙ', 'ʛ', 'ʜ', 'ʝ', 'ʟ', 'ʠ', 'ʣ', 'ʥ', 'ʦ', 'ʪ', 'ʫ', 'ᴀ', 'ᴁ', 'ᴃ', 'ᴄ', 'ᴅ', 'ᴆ', 'ᴇ', 'ᴊ', 'ᴋ', 'ᴌ', 'ᴍ', 'ᴏ', 'ᴘ', 'ᴛ', 'ᴜ', 'ᴠ', 'ᴡ', 'ᴢ', 'ᵫ', 'ᵬ', 'ᵭ', 'ᵮ', 'ᵯ', 'ᵰ', 'ᵱ', 'ᵲ', 'ᵳ', 'ᵴ', 'ᵵ', 'ᵶ', 'ᵺ', 'ᵻ', 'ᵽ', 'ᵾ', 'ᶀ', 'ᶁ', 'ᶂ', 'ᶃ', 'ᶄ', 'ᶅ', 'ᶆ', 'ᶇ', 'ᶈ', 'ᶉ', 'ᶊ', 'ᶌ', 'ᶍ', 'ᶎ', 'ᶏ', 'ᶑ', 'ᶒ', 'ᶓ', 'ᶖ', 'ᶙ', 'ẚ', 'ẜ', 'ẝ', 'ẞ', 'Ỻ', 'ỻ', 'Ỽ', 'ỽ', 'Ỿ', 'ỿ', '©', '®', '₠', '₢', '₣', '₤', '₧', '₺', '₹', 'ℌ', '℞', '㎧', '㎮', '㏆', '㏗', '㏞', '㏟', '¼', '½', '¾', '⅓', '⅔', '⅕', '⅖', '⅗', '⅘', '⅙', '⅚', '⅛', '⅜', '⅝', '⅞', '⅟', '〇', '‘', '’', '‚', '‛', '“', '”', '„', '‟', '′', '″', '〝', '〞', '«', '»', '‹', '›', '‐', '‑', '‒', '–', '—', '―', '︱', '︲', '﹘', '‖', '⁄', '⁅', '⁆', '⁎', '、', '。', '〈', '〉', '《', '》', '〔', '〕', '〘', '〙', '〚', '〛', '︑', '︒', '︹', '︺', '︽', '︾', '︿', '﹀', '﹑', '﹝', '﹞', '｟', '｠', '｡', '､', '×', '÷', '−', '∕', '∖', '∣', '∥', '≪', '≫', '⦅', '⦆'];
    private const TRANSLIT_TO = ['AE', 'D', 'O', 'TH', 'ss', 'ae', 'd', 'o', 'th', 'D', 'd', 'H', 'h', 'i', 'q', 'L', 'l', 'L', 'l', '\'n', 'N', 'n', 'OE', 'oe', 'T', 't', 'b', 'B', 'B', 'b', 'C', 'c', 'D', 'D', 'D', 'd', 'E', 'F', 'f', 'G', 'hv', 'I', 'I', 'K', 'k', 'l', 'N', 'n', 'OI', 'oi', 'P', 'p', 't', 'T', 't', 'T', 'V', 'Y', 'y', 'Z', 'z', 'DZ', 'Dz', 'dz', 'G', 'g', 'd', 'Z', 'z', 'l', 'n', 't', 'j', 'db', 'qp', 'A', 'C', 'c', 'L', 'T', 's', 'z', 'B', 'U', 'E', 'e', 'J', 'j', 'R', 'r', 'Y', 'y', 'b', 'c', 'd', 'd', 'e', 'j', 'g', 'g', 'G', 'h', 'h', 'i', 'I', 'l', 'l', 'l', 'm', 'n', 'n', 'N', 'OE', 'r', 'r', 'r', 'R', 's', 't', 'u', 'v', 'Y', 'z', 'z', 'B', 'G', 'H', 'j', 'L', 'q', 'dz', 'dz', 'ts', 'ls', 'lz', 'A', 'AE', 'B', 'C', 'D', 'D', 'E', 'J', 'K', 'L', 'M', 'O', 'P', 'T', 'U', 'V', 'W', 'Z', 'ue', 'b', 'd', 'f', 'm', 'n', 'p', 'r', 'r', 's', 't', 'z', 'th', 'I', 'p', 'U', 'b', 'd', 'f', 'g', 'k', 'l', 'm', 'n', 'p', 'r', 's', 'v', 'x', 'z', 'a', 'd', 'e', 'e', 'i', 'u', 'a', 's', 's', 'SS', 'LL', 'll', 'V', 'v', 'Y', 'y', '(C)', '(R)', 'CE', 'Cr', 'Fr.', 'L.', 'Pts', 'TL', 'Rs', 'x', 'Rx', 'm/s', 'rad/s', 'C/kg', 'pH', 'V/m', 'A/m', ' 1/4', ' 1/2', ' 3/4', ' 1/3', ' 2/3', ' 1/5', ' 2/5', ' 3/5', ' 4/5', ' 1/6', ' 5/6', ' 1/8', ' 3/8', ' 5/8', ' 7/8', ' 1/', '0', '\'', '\'', ',', '\'', '"', '"', ',,', '"', '\'', '"', '"', '"', '<<', '>>', '<', '>', '-', '-', '-', '-', '-', '-', '-', '-', '-', '||', '/', '[', ']', '*', ',', '.', '<', '>', '<<', '>>', '[', ']', '[', ']', '[', ']', ',', '.', '[', ']', '<<', '>>', '<', '>', ',', '[', ']', '((', '))', '.', ',', '*', '/', '-', '/', '\\', '|', '||', '<<', '>>', '((', '))'];

    private static array $transliterators = [];
    private static array $tableZero;
    private static array $tableWide;

    public static function fromCodePoints(int ...$codes): static
    {
        $string = '';

        foreach ($codes as $code) {
            if (0x80 > $code %= 0x200000) {
                $string .= \chr($code);
            } elseif (0x800 > $code) {
                $string .= \chr(0xC0 | $code >> 6).\chr(0x80 | $code & 0x3F);
            } elseif (0x10000 > $code) {
                $string .= \chr(0xE0 | $code >> 12).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F);
            } else {
                $string .= \chr(0xF0 | $code >> 18).\chr(0x80 | $code >> 12 & 0x3F).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F);
            }
        }

        return new static($string);
    }

    /**
     * Generic UTF-8 to ASCII transliteration.
     *
     * Install the intl extension for best results.
     *
     * @param string[]|\Transliterator[]|\Closure[] $rules See "*-Latin" rules from Transliterator::listIDs()
     */
    public function ascii(array $rules = []): self
    {
        $str = clone $this;
        $s = $str->string;
        $str->string = '';

        array_unshift($rules, 'nfd');
        $rules[] = 'latin-ascii';

        if (\function_exists('transliterator_transliterate')) {
            $rules[] = 'any-latin/bgn';
        }

        $rules[] = 'nfkd';
        $rules[] = '[:nonspacing mark:] remove';

        while (\strlen($s) - 1 > $i = strspn($s, self::ASCII)) {
            if (0 < --$i) {
                $str->string .= substr($s, 0, $i);
                $s = substr($s, $i);
            }

            if (!$rule = array_shift($rules)) {
                $rules = []; // An empty rule interrupts the next ones
            }

            if ($rule instanceof \Transliterator) {
                $s = $rule->transliterate($s);
            } elseif ($rule instanceof \Closure) {
                $s = $rule($s);
            } elseif ($rule) {
                if ('nfd' === $rule = strtolower($rule)) {
                    normalizer_is_normalized($s, self::NFD) ?: $s = normalizer_normalize($s, self::NFD);
                } elseif ('nfkd' === $rule) {
                    normalizer_is_normalized($s, self::NFKD) ?: $s = normalizer_normalize($s, self::NFKD);
                } elseif ('[:nonspacing mark:] remove' === $rule) {
                    $s = preg_replace('/\p{Mn}++/u', '', $s);
                } elseif ('latin-ascii' === $rule) {
                    $s = str_replace(self::TRANSLIT_FROM, self::TRANSLIT_TO, $s);
                } elseif ('de-ascii' === $rule) {
                    $s = preg_replace("/([AUO])\u{0308}(?=\p{Ll})/u", '$1e', $s);
                    $s = str_replace(["a\u{0308}", "o\u{0308}", "u\u{0308}", "A\u{0308}", "O\u{0308}", "U\u{0308}"], ['ae', 'oe', 'ue', 'AE', 'OE', 'UE'], $s);
                } elseif (\function_exists('transliterator_transliterate')) {
                    if (null === $transliterator = self::$transliterators[$rule] ??= \Transliterator::create($rule)) {
                        if ('any-latin/bgn' === $rule) {
                            $rule = 'any-latin';
                            $transliterator = self::$transliterators[$rule] ??= \Transliterator::create($rule);
                        }

                        if (null === $transliterator) {
                            throw new InvalidArgumentException(\sprintf('Unknown transliteration rule "%s".', $rule));
                        }

                        self::$transliterators['any-latin/bgn'] = $transliterator;
                    }

                    $s = $transliterator->transliterate($s);
                }
            } elseif (!\function_exists('iconv')) {
                $s = preg_replace('/[^\x00-\x7F]/u', '?', $s);
            } else {
                $previousLocale = setlocale(\LC_CTYPE, 0);
                try {
                    setlocale(\LC_CTYPE, 'C');
                    $s = @preg_replace_callback('/[^\x00-\x7F]/u', static function ($c) {
                        $c = (string) iconv('UTF-8', 'ASCII//TRANSLIT', $c[0]);

                        if ('' === $c && '' === iconv('UTF-8', 'ASCII//TRANSLIT', '²')) {
                            throw new \LogicException(\sprintf('"%s" requires a translit-able iconv implementation, try installing "gnu-libiconv" if you\'re using Alpine Linux.', static::class));
                        }

                        return 1 < \strlen($c) ? ltrim($c, '\'`"^~') : ('' !== $c ? $c : '?');
                    }, $s);
                } finally {
                    setlocale(\LC_CTYPE, $previousLocale);
                }
            }
        }

        $str->string .= $s;

        return $str;
    }

    public function camel(): static
    {
        $str = clone $this;
        $str->string = str_replace(' ', '', preg_replace_callback('/\b.(?!\p{Lu})/u', static function ($m) {
            static $i = 0;

            return 1 === ++$i ? ('İ' === $m[0] ? 'i̇' : mb_strtolower($m[0], 'UTF-8')) : mb_convert_case($m[0], \MB_CASE_TITLE, 'UTF-8');
        }, preg_replace('/[^\pL0-9]++/u', ' ', $this->string)));

        return $str;
    }

    /**
     * @return int[]
     */
    public function codePointsAt(int $offset): array
    {
        $str = $this->slice($offset, 1);

        if ('' === $str->string) {
            return [];
        }

        $codePoints = [];

        foreach (preg_split('//u', $str->string, -1, \PREG_SPLIT_NO_EMPTY) as $c) {
            $codePoints[] = mb_ord($c, 'UTF-8');
        }

        return $codePoints;
    }

    public function folded(bool $compat = true): static
    {
        $str = clone $this;

        if (!$compat || !\defined('Normalizer::NFKC_CF')) {
            $str->string = normalizer_normalize($str->string, $compat ? \Normalizer::NFKC : \Normalizer::NFC);
            $str->string = mb_strtolower(str_replace(self::FOLD_FROM, self::FOLD_TO, $str->string), 'UTF-8');
        } else {
            $str->string = normalizer_normalize($str->string, \Normalizer::NFKC_CF);
        }

        return $str;
    }

    public function join(array $strings, ?string $lastGlue = null): static
    {
        $str = clone $this;

        $tail = null !== $lastGlue && 1 < \count($strings) ? $lastGlue.array_pop($strings) : '';
        $str->string = implode($this->string, $strings).$tail;

        if (!preg_match('//u', $str->string)) {
            throw new InvalidArgumentException('Invalid UTF-8 string.');
        }

        return $str;
    }

    public function lower(): static
    {
        $str = clone $this;
        $str->string = mb_strtolower(str_replace('İ', 'i̇', $str->string), 'UTF-8');

        return $str;
    }

    /**
     * @param string $locale In the format language_region (e.g. tr_TR)
     */
    public function localeLower(string $locale): static
    {
        if (null !== $transliterator = $this->getLocaleTransliterator($locale, 'Lower')) {
            $str = clone $this;
            $str->string = $transliterator->transliterate($str->string);

            return $str;
        }

        return $this->lower();
    }

    public function match(string $regexp, int $flags = 0, int $offset = 0): array
    {
        $match = ((\PREG_PATTERN_ORDER | \PREG_SET_ORDER) & $flags) ? 'preg_match_all' : 'preg_match';

        if ($this->ignoreCase) {
            $regexp .= 'i';
        }

        set_error_handler(static fn ($t, $m) => throw new InvalidArgumentException($m));

        try {
            if (false === $match($regexp.'u', $this->string, $matches, $flags | \PREG_UNMATCHED_AS_NULL, $offset)) {
                throw new RuntimeException('Matching failed with error: '.preg_last_error_msg());
            }
        } finally {
            restore_error_handler();
        }

        return $matches;
    }

    public function normalize(int $form = self::NFC): static
    {
        if (!\in_array($form, [self::NFC, self::NFD, self::NFKC, self::NFKD], true)) {
            throw new InvalidArgumentException('Unsupported normalization form.');
        }

        $str = clone $this;
        normalizer_is_normalized($str->string, $form) ?: $str->string = normalizer_normalize($str->string, $form);

        return $str;
    }

    public function padBoth(int $length, string $padStr = ' '): static
    {
        if ('' === $padStr || !preg_match('//u', $padStr)) {
            throw new InvalidArgumentException('Invalid UTF-8 string.');
        }

        $pad = clone $this;
        $pad->string = $padStr;

        return $this->pad($length, $pad, \STR_PAD_BOTH);
    }

    public function padEnd(int $length, string $padStr = ' '): static
    {
        if ('' === $padStr || !preg_match('//u', $padStr)) {
            throw new InvalidArgumentException('Invalid UTF-8 string.');
        }

        $pad = clone $this;
        $pad->string = $padStr;

        return $this->pad($length, $pad, \STR_PAD_RIGHT);
    }

    public function padStart(int $length, string $padStr = ' '): static
    {
        if ('' === $padStr || !preg_match('//u', $padStr)) {
            throw new InvalidArgumentException('Invalid UTF-8 string.');
        }

        $pad = clone $this;
        $pad->string = $padStr;

        return $this->pad($length, $pad, \STR_PAD_LEFT);
    }

    public function replaceMatches(string $fromRegexp, string|callable $to): static
    {
        if ($this->ignoreCase) {
            $fromRegexp .= 'i';
        }

        if (\is_array($to) || $to instanceof \Closure) {
            $replace = 'preg_replace_callback';
            $to = static function (array $m) use ($to): string {
                $to = $to($m);

                if ('' !== $to && (!\is_string($to) || !preg_match('//u', $to))) {
                    throw new InvalidArgumentException('Replace callback must return a valid UTF-8 string.');
                }

                return $to;
            };
        } elseif ('' !== $to && !preg_match('//u', $to)) {
            throw new InvalidArgumentException('Invalid UTF-8 string.');
        } else {
            $replace = 'preg_replace';
        }

        set_error_handler(static fn ($t, $m) => throw new InvalidArgumentException($m));

        try {
            if (null === $string = $replace($fromRegexp.'u', $to, $this->string)) {
                $lastError = preg_last_error();

                foreach (get_defined_constants(true)['pcre'] as $k => $v) {
                    if ($lastError === $v && str_ends_with($k, '_ERROR')) {
                        throw new RuntimeException('Matching failed with '.$k.'.');
                    }
                }

                throw new RuntimeException('Matching failed with unknown error code.');
            }
        } finally {
            restore_error_handler();
        }

        $str = clone $this;
        $str->string = $string;

        return $str;
    }

    public function reverse(): static
    {
        $str = clone $this;
        $str->string = implode('', array_reverse(grapheme_str_split($str->string)));

        return $str;
    }

    public function snake(): static
    {
        $str = $this->camel();
        $str->string = mb_strtolower(preg_replace(['/(\p{Lu}+)(\p{Lu}\p{Ll})/u', '/([\p{Ll}0-9])(\p{Lu})/u'], '\1_\2', $str->string), 'UTF-8');

        return $str;
    }

    public function title(bool $allWords = false): static
    {
        $str = clone $this;

        $limit = $allWords ? -1 : 1;

        $str->string = preg_replace_callback('/\b./u', static fn (array $m): string => mb_convert_case($m[0], \MB_CASE_TITLE, 'UTF-8'), $str->string, $limit);

        return $str;
    }

    /**
     * @param string $locale In the format language_region (e.g. tr_TR)
     */
    public function localeTitle(string $locale): static
    {
        if (null !== $transliterator = $this->getLocaleTransliterator($locale, 'Title')) {
            $str = clone $this;
            $str->string = $transliterator->transliterate($str->string);

            return $str;
        }

        return $this->title();
    }

    public function trim(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): static
    {
        if (" \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}" !== $chars && !preg_match('//u', $chars)) {
            throw new InvalidArgumentException('Invalid UTF-8 chars.');
        }
        $chars = preg_quote($chars);

        $str = clone $this;
        $str->string = preg_replace("{^[$chars]++|[$chars]++$}uD", '', $str->string);

        return $str;
    }

    public function trimEnd(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): static
    {
        if (" \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}" !== $chars && !preg_match('//u', $chars)) {
            throw new InvalidArgumentException('Invalid UTF-8 chars.');
        }
        $chars = preg_quote($chars);

        $str = clone $this;
        $str->string = preg_replace("{[$chars]++$}uD", '', $str->string);

        return $str;
    }

    public function trimPrefix($prefix): static
    {
        if (!$this->ignoreCase) {
            return parent::trimPrefix($prefix);
        }

        $str = clone $this;

        if ($prefix instanceof \Traversable) {
            $prefix = iterator_to_array($prefix, false);
        } elseif ($prefix instanceof parent) {
            $prefix = $prefix->string;
        }

        $prefix = implode('|', array_map('preg_quote', (array) $prefix));
        $str->string = preg_replace("{^(?:$prefix)}iuD", '', $this->string);

        return $str;
    }

    public function trimStart(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): static
    {
        if (" \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}" !== $chars && !preg_match('//u', $chars)) {
            throw new InvalidArgumentException('Invalid UTF-8 chars.');
        }
        $chars = preg_quote($chars);

        $str = clone $this;
        $str->string = preg_replace("{^[$chars]++}uD", '', $str->string);

        return $str;
    }

    public function trimSuffix($suffix): static
    {
        if (!$this->ignoreCase) {
            return parent::trimSuffix($suffix);
        }

        $str = clone $this;

        if ($suffix instanceof \Traversable) {
            $suffix = iterator_to_array($suffix, false);
        } elseif ($suffix instanceof parent) {
            $suffix = $suffix->string;
        }

        $suffix = implode('|', array_map('preg_quote', (array) $suffix));
        $str->string = preg_replace("{(?:$suffix)$}iuD", '', $this->string);

        return $str;
    }

    public function upper(): static
    {
        $str = clone $this;
        $str->string = mb_strtoupper($str->string, 'UTF-8');

        return $str;
    }

    /**
     * @param string $locale In the format language_region (e.g. tr_TR)
     */
    public function localeUpper(string $locale): static
    {
        if (null !== $transliterator = $this->getLocaleTransliterator($locale, 'Upper')) {
            $str = clone $this;
            $str->string = $transliterator->transliterate($str->string);

            return $str;
        }

        return $this->upper();
    }

    public function width(bool $ignoreAnsiDecoration = true): int
    {
        $width = 0;
        $s = str_replace(["\x00", "\x05", "\x07"], '', $this->string);

        if (str_contains($s, "\r")) {
            $s = str_replace(["\r\n", "\r"], "\n", $s);
        }

        if (!$ignoreAnsiDecoration) {
            $s = preg_replace('/[\p{Cc}\x7F]++/u', '', $s);
        }

        foreach (explode("\n", $s) as $s) {
            if ($ignoreAnsiDecoration) {
                $s = preg_replace('/(?:\x1B(?:
                    \[ [\x30-\x3F]*+ [\x20-\x2F]*+ [\x40-\x7E]
                    | [P\]X^_] .*? \x1B\\\\
                    | [\x41-\x7E]
                )|[\p{Cc}\x7F]++)/xu', '', $s);
            }

            $lineWidth = $this->wcswidth($s);

            if ($lineWidth > $width) {
                $width = $lineWidth;
            }
        }

        return $width;
    }

    private function pad(int $len, self $pad, int $type): static
    {
        $sLen = $this->length();

        if ($len <= $sLen) {
            return clone $this;
        }

        $padLen = $pad->length();
        $freeLen = $len - $sLen;
        $len = $freeLen % $padLen;

        switch ($type) {
            case \STR_PAD_RIGHT:
                return $this->append(str_repeat($pad->string, intdiv($freeLen, $padLen)).($len ? $pad->slice(0, $len) : ''));

            case \STR_PAD_LEFT:
                return $this->prepend(str_repeat($pad->string, intdiv($freeLen, $padLen)).($len ? $pad->slice(0, $len) : ''));

            case \STR_PAD_BOTH:
                $freeLen /= 2;

                $rightLen = ceil($freeLen);
                $len = $rightLen % $padLen;
                $str = $this->append(str_repeat($pad->string, intdiv($rightLen, $padLen)).($len ? $pad->slice(0, $len) : ''));

                $leftLen = floor($freeLen);
                $len = $leftLen % $padLen;

                return $str->prepend(str_repeat($pad->string, intdiv($leftLen, $padLen)).($len ? $pad->slice(0, $len) : ''));

            default:
                throw new InvalidArgumentException('Invalid padding type.');
        }
    }

    /**
     * Based on https://github.com/jquast/wcwidth, a Python implementation of https://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c.
     */
    private function wcswidth(string $string): int
    {
        $width = 0;
        $lastChar = null;
        $lastWidth = null;

        foreach (preg_split('//u', $string, -1, \PREG_SPLIT_NO_EMPTY) as $c) {
            $codePoint = mb_ord($c, 'UTF-8');

            if (0 === $codePoint // NULL
                || 0x034F === $codePoint // COMBINING GRAPHEME JOINER
                || (0x200B <= $codePoint && 0x200F >= $codePoint) // ZERO WIDTH SPACE to RIGHT-TO-LEFT MARK
                || 0x2028 === $codePoint // LINE SEPARATOR
                || 0x2029 === $codePoint // PARAGRAPH SEPARATOR
                || (0x202A <= $codePoint && 0x202E >= $codePoint) // LEFT-TO-RIGHT EMBEDDING to RIGHT-TO-LEFT OVERRIDE
                || (0x2060 <= $codePoint && 0x2063 >= $codePoint) // WORD JOINER to INVISIBLE SEPARATOR
            ) {
                continue;
            }

            // Non printable characters
            if (32 > $codePoint // C0 control characters
                || (0x07F <= $codePoint && 0x0A0 > $codePoint) // C1 control characters and DEL
            ) {
                return -1;
            }

            if (0xFE0F === $codePoint) {
                if (\PCRE_VERSION_MAJOR < 10 || \PCRE_VERSION_MAJOR === 10 && \PCRE_VERSION_MINOR < 40) {
                    $regex = '/\p{So}/u';
                } else {
                    $regex = '/\p{Emoji}/u';
                }
                if (null !== $lastChar && 1 === $lastWidth && preg_match($regex, $lastChar)) {
                    ++$width;
                    $lastWidth = 2;
                }

                continue;
            }

            self::$tableZero ??= require __DIR__.'/Resources/data/wcswidth_table_zero.php';

            if ($codePoint >= self::$tableZero[0][0] && $codePoint <= self::$tableZero[$ubound = \count(self::$tableZero) - 1][1]) {
                $lbound = 0;
                while ($ubound >= $lbound) {
                    $mid = floor(($lbound + $ubound) / 2);

                    if ($codePoint > self::$tableZero[$mid][1]) {
                        $lbound = $mid + 1;
                    } elseif ($codePoint < self::$tableZero[$mid][0]) {
                        $ubound = $mid - 1;
                    } else {
                        continue 2;
                    }
                }
            }

            self::$tableWide ??= require __DIR__.'/Resources/data/wcswidth_table_wide.php';

            if ($codePoint >= self::$tableWide[0][0] && $codePoint <= self::$tableWide[$ubound = \count(self::$tableWide) - 1][1]) {
                $lbound = 0;
                while ($ubound >= $lbound) {
                    $mid = floor(($lbound + $ubound) / 2);

                    if ($codePoint > self::$tableWide[$mid][1]) {
                        $lbound = $mid + 1;
                    } elseif ($codePoint < self::$tableWide[$mid][0]) {
                        $ubound = $mid - 1;
                    } else {
                        $width += 2;
                        $lastChar = $c;
                        $lastWidth = 2;

                        continue 2;
                    }
                }
            }

            ++$width;
            $lastChar = $c;
            $lastWidth = 1;
        }

        return $width;
    }

    private function getLocaleTransliterator(string $locale, string $id): ?\Transliterator
    {
        $rule = $locale.'-'.$id;
        if (\array_key_exists($rule, self::$transliterators)) {
            return self::$transliterators[$rule];
        }

        if (null !== $transliterator = self::$transliterators[$rule] = \Transliterator::create($rule)) {
            return $transliterator;
        }

        // Try to find a parent locale (nl_BE -> nl)
        if (false === $i = strpos($locale, '_')) {
            return null;
        }

        $parentRule = substr_replace($locale, '-'.$id, $i);

        // Parent locale was already cached, return and store as current locale
        if (\array_key_exists($parentRule, self::$transliterators)) {
            return self::$transliterators[$rule] = self::$transliterators[$parentRule];
        }

        // Create transliterator based on parent locale and cache the result on both initial and parent locale values
        $transliterator = \Transliterator::create($parentRule);

        return self::$transliterators[$rule] = self::$transliterators[$parentRule] = $transliterator;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\String;

use Symfony\Component\String\Exception\ExceptionInterface;
use Symfony\Component\String\Exception\InvalidArgumentException;

/**
 * Represents a string of Unicode grapheme clusters encoded as UTF-8.
 *
 * A letter followed by combining characters (accents typically) form what Unicode defines
 * as a grapheme cluster: a character as humans mean it in written texts. This class knows
 * about the concept and won't split a letter apart from its combining accents. It also
 * ensures all string comparisons happen on their canonically-composed representation,
 * ignoring e.g. the order in which accents are listed when a letter has many of them.
 *
 * @see https://unicode.org/reports/tr15/
 *
 * @author Nicolas Grekas <p@tchwork.com>
 * @author Hugo Hamon <hugohamon@neuf.fr>
 *
 * @throws ExceptionInterface
 */
class UnicodeString extends AbstractUnicodeString
{
    public function __construct(string $string = '')
    {
        if ('' === $string || normalizer_is_normalized($this->string = $string)) {
            return;
        }

        if (false === $string = normalizer_normalize($string)) {
            throw new InvalidArgumentException('Invalid UTF-8 string.');
        }

        $this->string = $string;
    }

    public function append(string ...$suffix): static
    {
        $str = clone $this;
        $str->string = $this->string.(1 >= \count($suffix) ? ($suffix[0] ?? '') : implode('', $suffix));

        if (normalizer_is_normalized($str->string)) {
            return $str;
        }

        if (false === $string = normalizer_normalize($str->string)) {
            throw new InvalidArgumentException('Invalid UTF-8 string.');
        }

        $str->string = $string;

        return $str;
    }

    public function chunk(int $length = 1): array
    {
        if (1 > $length) {
            throw new InvalidArgumentException('The chunk length must be greater than zero.');
        }

        if ('' === $this->string) {
            return [];
        }

        $rx = '/(';
        while (65535 < $length) {
            $rx .= '\X{65535}';
            $length -= 65535;
        }
        $rx .= '\X{'.$length.'})/u';

        $str = clone $this;
        $chunks = [];

        foreach (preg_split($rx, $this->string, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY) as $chunk) {
            $str->string = $chunk;
            $chunks[] = clone $str;
        }

        return $chunks;
    }

    public function endsWith(string|iterable|AbstractString $suffix): bool
    {
        if ($suffix instanceof AbstractString) {
            $suffix = $suffix->string;
        } elseif (!\is_string($suffix)) {
            return parent::endsWith($suffix);
        }

        $form = null === $this->ignoreCase ? \Normalizer::NFD : \Normalizer::NFC;
        normalizer_is_normalized($suffix, $form) ?: $suffix = normalizer_normalize($suffix, $form);

        if ('' === $suffix || false === $suffix) {
            return false;
        }

        if (false === $grapheme = grapheme_extract($this->string, \strlen($suffix), \GRAPHEME_EXTR_MAXBYTES, \strlen($this->string) - \strlen($suffix))) {
            $grapheme = '';
        }

        if ($this->ignoreCase) {
            return 0 === mb_stripos($grapheme, $suffix, 0, 'UTF-8');
        }

        return $suffix === $grapheme;
    }

    public function equalsTo(string|iterable|AbstractString $string): bool
    {
        if ($string instanceof AbstractString) {
            $string = $string->string;
        } elseif (!\is_string($string)) {
            return parent::equalsTo($string);
        }

        $form = null === $this->ignoreCase ? \Normalizer::NFD : \Normalizer::NFC;
        normalizer_is_normalized($string, $form) ?: $string = normalizer_normalize($string, $form);

        if ('' !== $string && false !== $string && $this->ignoreCase) {
            return \strlen($string) === \strlen($this->string) && 0 === mb_stripos($this->string, $string, 0, 'UTF-8');
        }

        return $string === $this->string;
    }

    public function indexOf(string|iterable|AbstractString $needle, int $offset = 0): ?int
    {
        if ($needle instanceof AbstractString) {
            $needle = $needle->string;
        } elseif (!\is_string($needle)) {
            return parent::indexOf($needle, $offset);
        }

        $form = null === $this->ignoreCase ? \Normalizer::NFD : \Normalizer::NFC;
        normalizer_is_normalized($needle, $form) ?: $needle = normalizer_normalize($needle, $form);

        if ('' === $needle || false === $needle) {
            return null;
        }

        try {
            $i = $this->ignoreCase ? grapheme_stripos($this->string, $needle, $offset) : grapheme_strpos($this->string, $needle, $offset);
        } catch (\ValueError) {
            return null;
        }

        return false === $i ? null : $i;
    }

    public function indexOfLast(string|iterable|AbstractString $needle, int $offset = 0): ?int
    {
        if ($needle instanceof AbstractString) {
            $needle = $needle->string;
        } elseif (!\is_string($needle)) {
            return parent::indexOfLast($needle, $offset);
        }

        $form = null === $this->ignoreCase ? \Normalizer::NFD : \Normalizer::NFC;
        normalizer_is_normalized($needle, $form) ?: $needle = normalizer_normalize($needle, $form);

        if ('' === $needle || false === $needle) {
            return null;
        }

        $string = $this->string;

        if (0 > $offset) {
            // workaround https://bugs.php.net/74264
            if (0 > $offset += grapheme_strlen($needle)) {
                $string = grapheme_substr($string, 0, $offset);
            }
            $offset = 0;
        }

        try {
            $i = $this->ignoreCase ? grapheme_strripos($string, $needle, $offset) : grapheme_strrpos($string, $needle, $offset);
        } catch (\ValueError) {
            return null;
        }

        return false === $i ? null : $i;
    }

    public function join(array $strings, ?string $lastGlue = null): static
    {
        $str = parent::join($strings, $lastGlue);
        normalizer_is_normalized($str->string) ?: $str->string = normalizer_normalize($str->string);

        return $str;
    }

    public function length(): int
    {
        return grapheme_strlen($this->string);
    }

    public function normalize(int $form = self::NFC): static
    {
        $str = clone $this;

        if (\in_array($form, [self::NFC, self::NFKC], true)) {
            normalizer_is_normalized($str->string, $form) ?: $str->string = normalizer_normalize($str->string, $form);
        } elseif (!\in_array($form, [self::NFD, self::NFKD], true)) {
            throw new InvalidArgumentException('Unsupported normalization form.');
        } elseif (!normalizer_is_normalized($str->string, $form)) {
            $str->string = normalizer_normalize($str->string, $form);
            $str->ignoreCase = null;
        }

        return $str;
    }

    public function prepend(string ...$prefix): static
    {
        $str = clone $this;
        $str->string = (1 >= \count($prefix) ? ($prefix[0] ?? '') : implode('', $prefix)).$this->string;

        if (normalizer_is_normalized($str->string)) {
            return $str;
        }

        if (false === $string = normalizer_normalize($str->string)) {
            throw new InvalidArgumentException('Invalid UTF-8 string.');
        }

        $str->string = $string;

        return $str;
    }

    public function replace(string $from, string $to): static
    {
        $str = clone $this;
        normalizer_is_normalized($from) ?: $from = normalizer_normalize($from);

        if ('' !== $from && false !== $from) {
            $tail = $str->string;
            $result = '';
            $indexOf = $this->ignoreCase ? 'grapheme_stripos' : 'grapheme_strpos';

            while ('' !== $tail && false !== $i = $indexOf($tail, $from)) {
                $slice = grapheme_substr($tail, 0, $i);
                $result .= $slice.$to;
                $tail = substr($tail, \strlen($slice) + \strlen($from));
            }

            $str->string = $result.$tail;

            if (normalizer_is_normalized($str->string)) {
                return $str;
            }

            if (false === $string = normalizer_normalize($str->string)) {
                throw new InvalidArgumentException('Invalid UTF-8 string.');
            }

            $str->string = $string;
        }

        return $str;
    }

    public function replaceMatches(string $fromRegexp, string|callable $to): static
    {
        $str = parent::replaceMatches($fromRegexp, $to);
        normalizer_is_normalized($str->string) ?: $str->string = normalizer_normalize($str->string);

        return $str;
    }

    public function slice(int $start = 0, ?int $length = null): static
    {
        $str = clone $this;

        $str->string = (string) grapheme_substr($this->string, $start, $length ?? 2147483647);

        return $str;
    }

    public function splice(string $replacement, int $start = 0, ?int $length = null): static
    {
        $str = clone $this;

        $start = $start ? \strlen(grapheme_substr($this->string, 0, $start)) : 0;
        $length = $length ? \strlen(grapheme_substr(substr($this->string, $start), 0, $length)) : $length;
        $str->string = substr_replace($this->string, $replacement, $start, $length ?? 2147483647);

        if (normalizer_is_normalized($str->string)) {
            return $str;
        }

        if (false === $string = normalizer_normalize($str->string)) {
            throw new InvalidArgumentException('Invalid UTF-8 string.');
        }

        $str->string = $string;

        return $str;
    }

    public function split(string $delimiter, ?int $limit = null, ?int $flags = null): array
    {
        if (1 > $limit ??= 2147483647) {
            throw new InvalidArgumentException('Split limit must be a positive integer.');
        }

        if ('' === $delimiter) {
            throw new InvalidArgumentException('Split delimiter is empty.');
        }

        if (null !== $flags) {
            return parent::split($delimiter.'u', $limit, $flags);
        }

        normalizer_is_normalized($delimiter) ?: $delimiter = normalizer_normalize($delimiter);

        if (false === $delimiter) {
            throw new InvalidArgumentException('Split delimiter is not a valid UTF-8 string.');
        }

        $str = clone $this;
        $tail = $this->string;
        $chunks = [];
        $indexOf = $this->ignoreCase ? 'grapheme_stripos' : 'grapheme_strpos';

        while (1 < $limit && false !== $i = $indexOf($tail, $delimiter)) {
            $str->string = grapheme_substr($tail, 0, $i);
            $chunks[] = clone $str;
            $tail = substr($tail, \strlen($str->string) + \strlen($delimiter));
            --$limit;
        }

        $str->string = $tail;
        $chunks[] = clone $str;

        return $chunks;
    }

    public function startsWith(string|iterable|AbstractString $prefix): bool
    {
        if ($prefix instanceof AbstractString) {
            $prefix = $prefix->string;
        } elseif (!\is_string($prefix)) {
            return parent::startsWith($prefix);
        }

        $form = null === $this->ignoreCase ? \Normalizer::NFD : \Normalizer::NFC;
        normalizer_is_normalized($prefix, $form) ?: $prefix = normalizer_normalize($prefix, $form);

        if ('' === $prefix || false === $prefix) {
            return false;
        }

        if (false === $grapheme = grapheme_extract($this->string, \strlen($prefix), \GRAPHEME_EXTR_MAXBYTES)) {
            $grapheme = '';
        }

        if ($this->ignoreCase) {
            return 0 === mb_stripos($grapheme, $prefix, 0, 'UTF-8');
        }

        return $prefix === $grapheme;
    }

    public function trimPrefix($prefix): static
    {
        if (\is_array($prefix) || $prefix instanceof \Traversable) {
            return parent::trimPrefix($prefix);
        }

        if ($prefix instanceof AbstractString) {
            $prefix = $prefix->string;
        } else {
            $prefix = (string) $prefix;
        }

        if (!normalizer_is_normalized($prefix, \Normalizer::NFC)) {
            $prefix = normalizer_normalize($prefix, \Normalizer::NFC);
        }

        return parent::trimPrefix($prefix);
    }

    public function trimSuffix($suffix): static
    {
        if (\is_array($suffix) || $suffix instanceof \Traversable) {
            return parent::trimSuffix($suffix);
        }

        if ($suffix instanceof AbstractString) {
            $suffix = $suffix->string;
        } else {
            $suffix = (string) $suffix;
        }

        if (!normalizer_is_normalized($suffix, \Normalizer::NFC)) {
            $suffix = normalizer_normalize($suffix, \Normalizer::NFC);
        }

        return parent::trimSuffix($suffix);
    }

    public function __unserialize(array $data): void
    {
        if ($wakeup = self::class !== (new \ReflectionMethod($this, '__wakeup'))->class && self::class === (new \ReflectionMethod($this, '__unserialize'))->class) {
            trigger_deprecation('symfony/string', '7.4', 'Implementing "%s::__wakeup()" is deprecated, use "__unserialize()" instead.', get_debug_type($this));
        }

        if (($data['string'] ?? null) instanceof \Stringable || ($data["\0*\0string"] ?? null) instanceof \Stringable) {
            throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
        }

        try {
            if (\in_array(array_keys($data), [['string'], ["\0*\0string"]], true)) {
                $this->string = $data['string'] ?? $data["\0*\0string"];

                if ($wakeup) {
                    $this->__wakeup();
                }

                return;
            }

            trigger_deprecation('symfony/string', '7.4', 'Passing more than just key "string" to "%s::__unserialize()" is deprecated, populate properties in "%s::__unserialize()" instead.', self::class, get_debug_type($this));

            \Closure::bind(function ($data) use ($wakeup) {
                foreach ($data as $key => $value) {
                    $this->{("\0" === $key[0] ?? '') ? substr($key, 1 + strrpos($key, "\0")) : $key} = $value;
                }

                if ($wakeup) {
                    $this->__wakeup();
                }
            }, $this, static::class)($data);
        } finally {
            if (!$wakeup) {
                normalizer_is_normalized($this->string) ?: $this->string = normalizer_normalize($this->string);
            }
        }
    }

    /**
     * @deprecated since Symfony 7.4, will be replaced by `__unserialize()` in 8.0
     */
    public function __wakeup(): void
    {
        trigger_deprecation('symfony/string', '7.4', 'Calling "%s::__wakeup()" is deprecated, use "__unserialize()" instead.', get_debug_type($this));

        if (!\is_string($this->string)) {
            throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
        }

        normalizer_is_normalized($this->string) ?: $this->string = normalizer_normalize($this->string);
    }

    public function __clone()
    {
        if (null === $this->ignoreCase) {
            normalizer_is_normalized($this->string) ?: $this->string = normalizer_normalize($this->string);
        }

        $this->ignoreCase = false;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\String;

use Symfony\Component\String\Exception\ExceptionInterface;
use Symfony\Component\String\Exception\InvalidArgumentException;
use Symfony\Component\String\Exception\RuntimeException;

/**
 * Represents a string of abstract characters.
 *
 * Unicode defines 3 types of "characters" (bytes, code points and grapheme clusters).
 * This class is the abstract type to use as a type-hint when the logic you want to
 * implement doesn't care about the exact variant it deals with.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 * @author Hugo Hamon <hugohamon@neuf.fr>
 *
 * @throws ExceptionInterface
 */
abstract class AbstractString implements \Stringable, \JsonSerializable
{
    public const PREG_PATTERN_ORDER = \PREG_PATTERN_ORDER;
    public const PREG_SET_ORDER = \PREG_SET_ORDER;
    public const PREG_OFFSET_CAPTURE = \PREG_OFFSET_CAPTURE;
    public const PREG_UNMATCHED_AS_NULL = \PREG_UNMATCHED_AS_NULL;

    public const PREG_SPLIT = 0;
    public const PREG_SPLIT_NO_EMPTY = \PREG_SPLIT_NO_EMPTY;
    public const PREG_SPLIT_DELIM_CAPTURE = \PREG_SPLIT_DELIM_CAPTURE;
    public const PREG_SPLIT_OFFSET_CAPTURE = \PREG_SPLIT_OFFSET_CAPTURE;

    protected string $string = '';
    protected ?bool $ignoreCase = false;

    abstract public function __construct(string $string = '');

    /**
     * Unwraps instances of AbstractString back to strings.
     *
     * @return string[]|array
     */
    public static function unwrap(array $values): array
    {
        foreach ($values as $k => $v) {
            if ($v instanceof self) {
                $values[$k] = $v->__toString();
            } elseif (\is_array($v) && $values[$k] !== $v = static::unwrap($v)) {
                $values[$k] = $v;
            }
        }

        return $values;
    }

    /**
     * Wraps (and normalizes) strings in instances of AbstractString.
     *
     * @return static[]|array
     */
    public static function wrap(array $values): array
    {
        $i = 0;
        $keys = null;

        foreach ($values as $k => $v) {
            if (\is_string($k) && '' !== $k && $k !== $j = (string) new static($k)) {
                $keys ??= array_keys($values);
                $keys[$i] = $j;
            }

            if (\is_string($v)) {
                $values[$k] = new static($v);
            } elseif (\is_array($v) && $values[$k] !== $v = static::wrap($v)) {
                $values[$k] = $v;
            }

            ++$i;
        }

        return null !== $keys ? array_combine($keys, $values) : $values;
    }

    /**
     * @param string|string[] $needle
     */
    public function after(string|iterable $needle, bool $includeNeedle = false, int $offset = 0): static
    {
        $str = clone $this;
        $i = \PHP_INT_MAX;

        if (\is_string($needle)) {
            $needle = [$needle];
        }

        foreach ($needle as $n) {
            $n = (string) $n;
            $j = $this->indexOf($n, $offset);

            if (null !== $j && $j < $i) {
                $i = $j;
                $str->string = $n;
            }
        }

        if (\PHP_INT_MAX === $i) {
            return $str;
        }

        if (!$includeNeedle) {
            $i += $str->length();
        }

        return $this->slice($i);
    }

    /**
     * @param string|string[] $needle
     */
    public function afterLast(string|iterable $needle, bool $includeNeedle = false, int $offset = 0): static
    {
        $str = clone $this;
        $i = null;

        if (\is_string($needle)) {
            $needle = [$needle];
        }

        foreach ($needle as $n) {
            $n = (string) $n;
            $j = $this->indexOfLast($n, $offset);

            if (null !== $j && $j >= $i) {
                $i = $offset = $j;
                $str->string = $n;
            }
        }

        if (null === $i) {
            return $str;
        }

        if (!$includeNeedle) {
            $i += $str->length();
        }

        return $this->slice($i);
    }

    abstract public function append(string ...$suffix): static;

    /**
     * @param string|string[] $needle
     */
    public function before(string|iterable $needle, bool $includeNeedle = false, int $offset = 0): static
    {
        $str = clone $this;
        $i = \PHP_INT_MAX;

        if (\is_string($needle)) {
            $needle = [$needle];
        }

        foreach ($needle as $n) {
            $n = (string) $n;
            $j = $this->indexOf($n, $offset);

            if (null !== $j && $j < $i) {
                $i = $j;
                $str->string = $n;
            }
        }

        if (\PHP_INT_MAX === $i) {
            return $str;
        }

        if ($includeNeedle) {
            $i += $str->length();
        }

        return $this->slice(0, $i);
    }

    /**
     * @param string|string[] $needle
     */
    public function beforeLast(string|iterable $needle, bool $includeNeedle = false, int $offset = 0): static
    {
        $str = clone $this;
        $i = null;

        if (\is_string($needle)) {
            $needle = [$needle];
        }

        foreach ($needle as $n) {
            $n = (string) $n;
            $j = $this->indexOfLast($n, $offset);

            if (null !== $j && $j >= $i) {
                $i = $offset = $j;
                $str->string = $n;
            }
        }

        if (null === $i) {
            return $str;
        }

        if ($includeNeedle) {
            $i += $str->length();
        }

        return $this->slice(0, $i);
    }

    /**
     * @return int[]
     */
    public function bytesAt(int $offset): array
    {
        $str = $this->slice($offset, 1);

        return '' === $str->string ? [] : array_values(unpack('C*', $str->string));
    }

    abstract public function camel(): static;

    /**
     * @return static[]
     */
    abstract public function chunk(int $length = 1): array;

    public function collapseWhitespace(): static
    {
        $str = clone $this;
        $str->string = trim(preg_replace("/(?:[ \n\r\t\x0C]{2,}+|[\n\r\t\x0C])/", ' ', $str->string), " \n\r\t\x0C");

        return $str;
    }

    /**
     * @param string|string[] $needle
     */
    public function containsAny(string|iterable $needle): bool
    {
        return null !== $this->indexOf($needle);
    }

    /**
     * @param string|string[] $suffix
     */
    public function endsWith(string|iterable $suffix): bool
    {
        if (\is_string($suffix)) {
            throw new \TypeError(\sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class));
        }

        foreach ($suffix as $s) {
            if ($this->endsWith((string) $s)) {
                return true;
            }
        }

        return false;
    }

    public function ensureEnd(string $suffix): static
    {
        if (!$this->endsWith($suffix)) {
            return $this->append($suffix);
        }

        $suffix = preg_quote($suffix);
        $regex = '{('.$suffix.')(?:'.$suffix.')++$}D';

        return $this->replaceMatches($regex.($this->ignoreCase ? 'i' : ''), '$1');
    }

    public function ensureStart(string $prefix): static
    {
        $prefix = new static($prefix);

        if (!$this->startsWith($prefix)) {
            return $this->prepend($prefix);
        }

        $str = clone $this;
        $i = $prefixLen = $prefix->length();

        while ($this->indexOf($prefix, $i) === $i) {
            $str = $str->slice($prefixLen);
            $i += $prefixLen;
        }

        return $str;
    }

    /**
     * @param string|string[] $string
     */
    public function equalsTo(string|iterable $string): bool
    {
        if (\is_string($string)) {
            throw new \TypeError(\sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class));
        }

        foreach ($string as $s) {
            if ($this->equalsTo((string) $s)) {
                return true;
            }
        }

        return false;
    }

    abstract public function folded(): static;

    public function ignoreCase(): static
    {
        $str = clone $this;
        $str->ignoreCase = true;

        return $str;
    }

    /**
     * @param string|string[] $needle
     */
    public function indexOf(string|iterable $needle, int $offset = 0): ?int
    {
        if (\is_string($needle)) {
            throw new \TypeError(\sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class));
        }

        $i = \PHP_INT_MAX;

        foreach ($needle as $n) {
            $j = $this->indexOf((string) $n, $offset);

            if (null !== $j && $j < $i) {
                $i = $j;
            }
        }

        return \PHP_INT_MAX === $i ? null : $i;
    }

    /**
     * @param string|string[] $needle
     */
    public function indexOfLast(string|iterable $needle, int $offset = 0): ?int
    {
        if (\is_string($needle)) {
            throw new \TypeError(\sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class));
        }

        $i = null;

        foreach ($needle as $n) {
            $j = $this->indexOfLast((string) $n, $offset);

            if (null !== $j && $j >= $i) {
                $i = $offset = $j;
            }
        }

        return $i;
    }

    public function isEmpty(): bool
    {
        return '' === $this->string;
    }

    abstract public function join(array $strings, ?string $lastGlue = null): static;

    public function jsonSerialize(): string
    {
        return $this->string;
    }

    abstract public function length(): int;

    abstract public function lower(): static;

    /**
     * Matches the string using a regular expression.
     *
     * Pass PREG_PATTERN_ORDER or PREG_SET_ORDER as $flags to get all occurrences matching the regular expression.
     *
     * @return array All matches in a multi-dimensional array ordered according to flags
     */
    abstract public function match(string $regexp, int $flags = 0, int $offset = 0): array;

    abstract public function padBoth(int $length, string $padStr = ' '): static;

    abstract public function padEnd(int $length, string $padStr = ' '): static;

    abstract public function padStart(int $length, string $padStr = ' '): static;

    abstract public function prepend(string ...$prefix): static;

    public function repeat(int $multiplier): static
    {
        if (0 > $multiplier) {
            throw new InvalidArgumentException(\sprintf('Multiplier must be positive, %d given.', $multiplier));
        }

        $str = clone $this;
        $str->string = str_repeat($str->string, $multiplier);

        return $str;
    }

    abstract public function replace(string $from, string $to): static;

    abstract public function replaceMatches(string $fromRegexp, string|callable $to): static;

    abstract public function reverse(): static;

    abstract public function slice(int $start = 0, ?int $length = null): static;

    abstract public function snake(): static;

    public function kebab(): static
    {
        return $this->snake()->replace('_', '-');
    }

    public function pascal(): static
    {
        return $this->camel()->title();
    }

    abstract public function splice(string $replacement, int $start = 0, ?int $length = null): static;

    /**
     * @return static[]
     */
    public function split(string $delimiter, ?int $limit = null, ?int $flags = null): array
    {
        if (null === $flags) {
            throw new \TypeError('Split behavior when $flags is null must be implemented by child classes.');
        }

        if ($this->ignoreCase) {
            $delimiter .= 'i';
        }

        set_error_handler(static fn ($t, $m) => throw new InvalidArgumentException($m));

        try {
            if (false === $chunks = preg_split($delimiter, $this->string, $limit, $flags)) {
                throw new RuntimeException('Splitting failed with error: '.preg_last_error_msg());
            }
        } finally {
            restore_error_handler();
        }

        $str = clone $this;

        if (self::PREG_SPLIT_OFFSET_CAPTURE & $flags) {
            foreach ($chunks as &$chunk) {
                $str->string = $chunk[0];
                $chunk[0] = clone $str;
            }
        } else {
            foreach ($chunks as &$chunk) {
                $str->string = $chunk;
                $chunk = clone $str;
            }
        }

        return $chunks;
    }

    /**
     * @param string|string[] $prefix
     */
    public function startsWith(string|iterable $prefix): bool
    {
        if (\is_string($prefix)) {
            throw new \TypeError(\sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class));
        }

        foreach ($prefix as $prefix) {
            if ($this->startsWith((string) $prefix)) {
                return true;
            }
        }

        return false;
    }

    abstract public function title(bool $allWords = false): static;

    public function toByteString(?string $toEncoding = null): ByteString
    {
        $b = new ByteString();

        $toEncoding = \in_array($toEncoding, ['utf8', 'utf-8', 'UTF8'], true) ? 'UTF-8' : $toEncoding;

        if (null === $toEncoding || $toEncoding === $fromEncoding = $this instanceof AbstractUnicodeString || preg_match('//u', $b->string) ? 'UTF-8' : 'Windows-1252') {
            $b->string = $this->string;

            return $b;
        }

        try {
            $b->string = mb_convert_encoding($this->string, $toEncoding, 'UTF-8');
        } catch (\ValueError $e) {
            if (!\function_exists('iconv')) {
                throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
            }

            $b->string = iconv('UTF-8', $toEncoding, $this->string);
        }

        return $b;
    }

    public function toCodePointString(): CodePointString
    {
        return new CodePointString($this->string);
    }

    public function toString(): string
    {
        return $this->string;
    }

    public function toUnicodeString(): UnicodeString
    {
        return new UnicodeString($this->string);
    }

    abstract public function trim(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): static;

    abstract public function trimEnd(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): static;

    /**
     * @param string|string[] $prefix
     */
    public function trimPrefix($prefix): static
    {
        if (\is_array($prefix) || $prefix instanceof \Traversable) { // don't use is_iterable(), it's slow
            foreach ($prefix as $s) {
                $t = $this->trimPrefix($s);

                if ($t->string !== $this->string) {
                    return $t;
                }
            }

            return clone $this;
        }

        $str = clone $this;

        if ($prefix instanceof self) {
            $prefix = $prefix->string;
        } else {
            $prefix = (string) $prefix;
        }

        if ('' !== $prefix && \strlen($this->string) >= \strlen($prefix) && 0 === substr_compare($this->string, $prefix, 0, \strlen($prefix), $this->ignoreCase)) {
            $str->string = substr($this->string, \strlen($prefix));
        }

        return $str;
    }

    abstract public function trimStart(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): static;

    /**
     * @param string|string[] $suffix
     */
    public function trimSuffix($suffix): static
    {
        if (\is_array($suffix) || $suffix instanceof \Traversable) { // don't use is_iterable(), it's slow
            foreach ($suffix as $s) {
                $t = $this->trimSuffix($s);

                if ($t->string !== $this->string) {
                    return $t;
                }
            }

            return clone $this;
        }

        $str = clone $this;

        if ($suffix instanceof self) {
            $suffix = $suffix->string;
        } else {
            $suffix = (string) $suffix;
        }

        if ('' !== $suffix && \strlen($this->string) >= \strlen($suffix) && 0 === substr_compare($this->string, $suffix, -\strlen($suffix), null, $this->ignoreCase)) {
            $str->string = substr($this->string, 0, -\strlen($suffix));
        }

        return $str;
    }

    public function truncate(int $length, string $ellipsis = '', bool|TruncateMode $cut = TruncateMode::Char): static
    {
        $stringLength = $this->length();

        if ($stringLength <= $length) {
            return clone $this;
        }

        $ellipsisLength = '' !== $ellipsis ? (new static($ellipsis))->length() : 0;

        if ($length < $ellipsisLength) {
            $ellipsisLength = 0;
        }

        $desiredLength = $length;
        if (TruncateMode::WordAfter === $cut || !$cut) {
            if (null === $length = $this->indexOf([' ', "\r", "\n", "\t"], ($length ?: 1) - 1)) {
                return clone $this;
            }

            $length += $ellipsisLength;
        } elseif (TruncateMode::WordBefore === $cut && null !== $this->indexOf([' ', "\r", "\n", "\t"], ($length ?: 1) - 1)) {
            $length += $ellipsisLength;
        }

        $str = $this->slice(0, $length - $ellipsisLength);

        if (TruncateMode::WordBefore === $cut) {
            if (0 === $ellipsisLength && $desiredLength === $this->indexOf([' ', "\r", "\n", "\t"], $length)) {
                return $str;
            }

            $str = $str->beforeLast([' ', "\r", "\n", "\t"]);
        }

        return $ellipsisLength ? $str->trimEnd()->append($ellipsis) : $str;
    }

    abstract public function upper(): static;

    /**
     * Returns the printable length on a terminal.
     */
    abstract public function width(bool $ignoreAnsiDecoration = true): int;

    public function wordwrap(int $width = 75, string $break = "\n", bool $cut = false): static
    {
        $lines = '' !== $break ? $this->split($break) : [clone $this];
        $chars = [];
        $mask = '';

        if (1 === \count($lines) && '' === $lines[0]->string) {
            return $lines[0];
        }

        foreach ($lines as $i => $line) {
            if ($i) {
                $chars[] = $break;
                $mask .= '#';
            }

            foreach ($line->chunk() as $char) {
                $chars[] = $char->string;
                $mask .= ' ' === $char->string ? ' ' : '?';
            }
        }

        $string = '';
        $j = 0;
        $b = $i = -1;
        $mask = wordwrap($mask, $width, '#', $cut);

        while (false !== $b = strpos($mask, '#', $b + 1)) {
            for (++$i; $i < $b; ++$i) {
                $string .= $chars[$j];
                unset($chars[$j++]);
            }

            if ($break === $chars[$j] || ' ' === $chars[$j]) {
                unset($chars[$j++]);
            }

            $string .= $break;
        }

        $str = clone $this;
        $str->string = $string.implode('', $chars);

        return $str;
    }

    public function __serialize(): array
    {
        if (self::class === (new \ReflectionMethod($this, '__sleep'))->class || self::class !== (new \ReflectionMethod($this, '__serialize'))->class) {
            return ['string' => $this->string];
        }

        trigger_deprecation('symfony/string', '7.4', 'Implementing "%s::__sleep()" is deprecated, use "__serialize()" instead.', get_debug_type($this));

        $data = [];
        foreach ($this->__sleep() as $key) {
            try {
                if (($r = new \ReflectionProperty($this, $key))->isInitialized($this)) {
                    $data[$key] = $r->getValue($this);
                }
            } catch (\ReflectionException) {
                $data[$key] = $this->$key;
            }
        }

        return $data;
    }

    /**
     * @deprecated since Symfony 7.4, will be replaced by `__unserialize()` in 8.0
     */
    public function __sleep(): array
    {
        trigger_deprecation('symfony/string', '7.4', 'Calling "%s::__sleep()" is deprecated, use "__serialize()" instead.', get_debug_type($this));

        return ['string'];
    }

    public function __clone()
    {
        $this->ignoreCase = false;
    }

    public function __toString(): string
    {
        return $this->string;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\String;

enum TruncateMode
{
    /**
     * Will cut exactly at given length.
     *
     * Length: 14
     * Source: Lorem ipsum dolor sit amet
     * Output: Lorem ipsum do
     */
    case Char;

    /**
     * Returns the string up to the last complete word containing the specified length.
     *
     * Length: 14
     * Source: Lorem ipsum dolor sit amet
     * Output: Lorem ipsum
     */
    case WordBefore;

    /**
     * Returns the string up to the complete word after or at the given length.
     *
     * Length: 14
     * Source: Lorem ipsum dolor sit amet
     * Output: Lorem ipsum dolor
     */
    case WordAfter;
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\String;

use Symfony\Component\String\Exception\ExceptionInterface;
use Symfony\Component\String\Exception\InvalidArgumentException;

/**
 * Represents a string of Unicode code points encoded as UTF-8.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 * @author Hugo Hamon <hugohamon@neuf.fr>
 *
 * @throws ExceptionInterface
 */
class CodePointString extends AbstractUnicodeString
{
    public function __construct(string $string = '')
    {
        if ('' !== $string && !preg_match('//u', $string)) {
            throw new InvalidArgumentException('Invalid UTF-8 string.');
        }

        $this->string = $string;
    }

    public function append(string ...$suffix): static
    {
        $str = clone $this;
        $str->string .= 1 >= \count($suffix) ? ($suffix[0] ?? '') : implode('', $suffix);

        if (!preg_match('//u', $str->string)) {
            throw new InvalidArgumentException('Invalid UTF-8 string.');
        }

        return $str;
    }

    public function chunk(int $length = 1): array
    {
        if (1 > $length) {
            throw new InvalidArgumentException('The chunk length must be greater than zero.');
        }

        if ('' === $this->string) {
            return [];
        }

        $rx = '/(';
        while (65535 < $length) {
            $rx .= '.{65535}';
            $length -= 65535;
        }
        $rx .= '.{'.$length.'})/us';

        $str = clone $this;
        $chunks = [];

        foreach (preg_split($rx, $this->string, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY) as $chunk) {
            $str->string = $chunk;
            $chunks[] = clone $str;
        }

        return $chunks;
    }

    public function codePointsAt(int $offset): array
    {
        $str = $offset ? $this->slice($offset, 1) : $this;

        return '' === $str->string ? [] : [mb_ord($str->string, 'UTF-8')];
    }

    public function endsWith(string|iterable|AbstractString $suffix): bool
    {
        if ($suffix instanceof AbstractString) {
            $suffix = $suffix->string;
        } elseif (!\is_string($suffix)) {
            return parent::endsWith($suffix);
        }

        if ('' === $suffix || !preg_match('//u', $suffix)) {
            return false;
        }

        if ($this->ignoreCase) {
            return preg_match('{'.preg_quote($suffix).'$}iuD', $this->string);
        }

        return \strlen($this->string) >= \strlen($suffix) && 0 === substr_compare($this->string, $suffix, -\strlen($suffix));
    }

    public function equalsTo(string|iterable|AbstractString $string): bool
    {
        if ($string instanceof AbstractString) {
            $string = $string->string;
        } elseif (!\is_string($string)) {
            return parent::equalsTo($string);
        }

        if ('' !== $string && $this->ignoreCase) {
            return \strlen($string) === \strlen($this->string) && 0 === mb_stripos($this->string, $string, 0, 'UTF-8');
        }

        return $string === $this->string;
    }

    public function indexOf(string|iterable|AbstractString $needle, int $offset = 0): ?int
    {
        if ($needle instanceof AbstractString) {
            $needle = $needle->string;
        } elseif (!\is_string($needle)) {
            return parent::indexOf($needle, $offset);
        }

        if ('' === $needle) {
            return null;
        }

        try {
            $i = $this->ignoreCase ? mb_stripos($this->string, $needle, $offset, 'UTF-8') : mb_strpos($this->string, $needle, $offset, 'UTF-8');
        } catch (\ValueError) {
            return null;
        }

        return false === $i ? null : $i;
    }

    public function indexOfLast(string|iterable|AbstractString $needle, int $offset = 0): ?int
    {
        if ($needle instanceof AbstractString) {
            $needle = $needle->string;
        } elseif (!\is_string($needle)) {
            return parent::indexOfLast($needle, $offset);
        }

        if ('' === $needle) {
            return null;
        }

        try {
            $i = $this->ignoreCase ? mb_strripos($this->string, $needle, $offset, 'UTF-8') : mb_strrpos($this->string, $needle, $offset, 'UTF-8');
        } catch (\ValueError) {
            return null;
        }

        return false === $i ? null : $i;
    }

    public function length(): int
    {
        return mb_strlen($this->string, 'UTF-8');
    }

    public function prepend(string ...$prefix): static
    {
        $str = clone $this;
        $str->string = (1 >= \count($prefix) ? ($prefix[0] ?? '') : implode('', $prefix)).$this->string;

        if (!preg_match('//u', $str->string)) {
            throw new InvalidArgumentException('Invalid UTF-8 string.');
        }

        return $str;
    }

    public function replace(string $from, string $to): static
    {
        $str = clone $this;

        if ('' === $from || !preg_match('//u', $from)) {
            return $str;
        }

        if ('' !== $to && !preg_match('//u', $to)) {
            throw new InvalidArgumentException('Invalid UTF-8 string.');
        }

        if ($this->ignoreCase) {
            $str->string = implode($to, preg_split('{'.preg_quote($from).'}iuD', $this->string));
        } else {
            $str->string = str_replace($from, $to, $this->string);
        }

        return $str;
    }

    public function slice(int $start = 0, ?int $length = null): static
    {
        $str = clone $this;
        $str->string = mb_substr($this->string, $start, $length, 'UTF-8');

        return $str;
    }

    public function splice(string $replacement, int $start = 0, ?int $length = null): static
    {
        if (!preg_match('//u', $replacement)) {
            throw new InvalidArgumentException('Invalid UTF-8 string.');
        }

        $str = clone $this;
        $start = $start ? \strlen(mb_substr($this->string, 0, $start, 'UTF-8')) : 0;
        $length = $length ? \strlen(mb_substr(substr($this->string, $start), 0, $length, 'UTF-8')) : $length;
        $str->string = substr_replace($this->string, $replacement, $start, $length ?? \PHP_INT_MAX);

        return $str;
    }

    public function split(string $delimiter, ?int $limit = null, ?int $flags = null): array
    {
        if (1 > $limit ??= \PHP_INT_MAX) {
            throw new InvalidArgumentException('Split limit must be a positive integer.');
        }

        if ('' === $delimiter) {
            throw new InvalidArgumentException('Split delimiter is empty.');
        }

        if (null !== $flags) {
            return parent::split($delimiter.'u', $limit, $flags);
        }

        if (!preg_match('//u', $delimiter)) {
            throw new InvalidArgumentException('Split delimiter is not a valid UTF-8 string.');
        }

        $str = clone $this;
        $chunks = $this->ignoreCase
            ? preg_split('{'.preg_quote($delimiter).'}iuD', $this->string, $limit)
            : explode($delimiter, $this->string, $limit);

        foreach ($chunks as &$chunk) {
            $str->string = $chunk;
            $chunk = clone $str;
        }

        return $chunks;
    }

    public function startsWith(string|iterable|AbstractString $prefix): bool
    {
        if ($prefix instanceof AbstractString) {
            $prefix = $prefix->string;
        } elseif (!\is_string($prefix)) {
            return parent::startsWith($prefix);
        }

        if ('' === $prefix || !preg_match('//u', $prefix)) {
            return false;
        }

        if ($this->ignoreCase) {
            return 0 === mb_stripos($this->string, $prefix, 0, 'UTF-8');
        }

        return 0 === strncmp($this->string, $prefix, \strlen($prefix));
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\String;

/**
 * A string whose value is computed lazily by a callback.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
class LazyString implements \Stringable, \JsonSerializable
{
    private \Closure|string $value;

    /**
     * @param callable|array $callback A callable or a [Closure, method] lazy-callable
     */
    public static function fromCallable(callable|array $callback, mixed ...$arguments): static
    {
        if (\is_array($callback) && !\is_callable($callback) && !(($callback[0] ?? null) instanceof \Closure || 2 < \count($callback))) {
            throw new \TypeError(\sprintf('Argument 1 passed to "%s()" must be a callable or a [Closure, method] lazy-callable, "%s" given.', __METHOD__, '['.implode(', ', array_map('get_debug_type', $callback)).']'));
        }

        $lazyString = new static();
        $lazyString->value = static function () use (&$callback, &$arguments): string {
            static $value;

            if (null !== $arguments) {
                if (!\is_callable($callback)) {
                    $callback[0] = $callback[0]();
                    $callback[1] ??= '__invoke';
                }
                $value = $callback(...$arguments);
                $callback = !\is_scalar($value) && !$value instanceof \Stringable ? self::getPrettyName($callback) : 'callable';
                $arguments = null;
            }

            return $value ?? '';
        };

        return $lazyString;
    }

    public static function fromStringable(string|int|float|bool|\Stringable $value): static
    {
        if (\is_object($value)) {
            return static::fromCallable($value->__toString(...));
        }

        $lazyString = new static();
        $lazyString->value = (string) $value;

        return $lazyString;
    }

    /**
     * Tells whether the provided value can be cast to string.
     */
    final public static function isStringable(mixed $value): bool
    {
        return \is_string($value) || $value instanceof \Stringable || \is_scalar($value);
    }

    /**
     * Casts scalars and stringable objects to strings.
     *
     * @throws \TypeError When the provided value is not stringable
     */
    final public static function resolve(\Stringable|string|int|float|bool $value): string
    {
        return $value;
    }

    public function __toString(): string
    {
        if (\is_string($this->value)) {
            return $this->value;
        }

        try {
            return $this->value = ($this->value)();
        } catch (\Throwable $e) {
            if (\TypeError::class === $e::class && __FILE__ === $e->getFile()) {
                $type = explode(', ', $e->getMessage());
                $type = substr(array_pop($type), 0, -\strlen(' returned'));
                $r = new \ReflectionFunction($this->value);
                $callback = $r->getStaticVariables()['callback'];

                $e = new \TypeError(\sprintf('Return value of %s() passed to %s::fromCallable() must be of the type string, %s returned.', $callback, static::class, $type));
            }

            throw $e;
        }
    }

    public function __serialize(): array
    {
        if (self::class === (new \ReflectionMethod($this, '__sleep'))->class || self::class !== (new \ReflectionMethod($this, '__serialize'))->class) {
            $this->__toString();

            return ['value' => $this->value];
        }

        trigger_deprecation('symfony/string', '7.4', 'Implementing "%s::__sleep()" is deprecated, use "__serialize()" instead.', get_debug_type($this));

        $data = [];
        foreach ($this->__sleep() as $key) {
            try {
                if (($r = new \ReflectionProperty($this, $key))->isInitialized($this)) {
                    $data[$key] = $r->getValue($this);
                }
            } catch (\ReflectionException) {
                $data[$key] = $this->$key;
            }
        }

        return $data;
    }

    /**
     * @deprecated since Symfony 7.4, will be replaced by `__serialize()` in 8.0
     */
    public function __sleep(): array
    {
        trigger_deprecation('symfony/string', '7.4', 'Calling "%s::__sleep()" is deprecated, use "__serialize()" instead.', get_debug_type($this));

        $this->__toString();

        return ['value'];
    }

    public function jsonSerialize(): string
    {
        return $this->__toString();
    }

    private function __construct()
    {
    }

    private static function getPrettyName(callable $callback): string
    {
        if (\is_string($callback)) {
            return $callback;
        }

        if (\is_array($callback)) {
            $class = \is_object($callback[0]) ? get_debug_type($callback[0]) : $callback[0];
            $method = $callback[1];
        } elseif ($callback instanceof \Closure) {
            $r = new \ReflectionFunction($callback);

            if ($r->isAnonymous() || !$class = $r->getClosureCalledClass()) {
                return $r->name;
            }

            $class = $class->name;
            $method = $r->name;
        } else {
            $class = get_debug_type($callback);
            $method = '__invoke';
        }

        return $class.'::'.$method;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\String\Slugger;

use Symfony\Component\String\AbstractUnicodeString;

/**
 * Creates a URL-friendly slug from a given string.
 *
 * @author Titouan Galopin <galopintitouan@gmail.com>
 */
interface SluggerInterface
{
    /**
     * Creates a slug for the given string and locale, using appropriate transliteration when needed.
     */
    public function slug(string $string, string $separator = '-', ?string $locale = null): AbstractUnicodeString;
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\String\Slugger;

use Symfony\Component\Emoji\EmojiTransliterator;
use Symfony\Component\String\AbstractUnicodeString;
use Symfony\Component\String\UnicodeString;
use Symfony\Contracts\Translation\LocaleAwareInterface;

if (!interface_exists(LocaleAwareInterface::class)) {
    throw new \LogicException('You cannot use the "Symfony\Component\String\Slugger\AsciiSlugger" as the "symfony/translation-contracts" package is not installed. Try running "composer require symfony/translation-contracts".');
}

/**
 * @author Titouan Galopin <galopintitouan@gmail.com>
 */
class AsciiSlugger implements SluggerInterface, LocaleAwareInterface
{
    private const LOCALE_TO_TRANSLITERATOR_ID = [
        'am' => 'Amharic-Latin',
        'ar' => 'Arabic-Latin',
        'az' => 'Azerbaijani-Latin',
        'be' => 'Belarusian-Latin',
        'bg' => 'Bulgarian-Latin',
        'bn' => 'Bengali-Latin',
        'de' => 'de-ASCII',
        'el' => 'Greek-Latin',
        'fa' => 'Persian-Latin',
        'he' => 'Hebrew-Latin',
        'hy' => 'Armenian-Latin',
        'ka' => 'Georgian-Latin',
        'kk' => 'Kazakh-Latin',
        'ky' => 'Kirghiz-Latin',
        'ko' => 'Korean-Latin',
        'mk' => 'Macedonian-Latin',
        'mn' => 'Mongolian-Latin',
        'or' => 'Oriya-Latin',
        'ps' => 'Pashto-Latin',
        'ru' => 'Russian-Latin',
        'sr' => 'Serbian-Latin',
        'sr_Cyrl' => 'Serbian-Latin',
        'th' => 'Thai-Latin',
        'tk' => 'Turkmen-Latin',
        'uk' => 'Ukrainian-Latin',
        'uz' => 'Uzbek-Latin',
        'zh' => 'Han-Latin',
    ];

    private \Closure|array $symbolsMap = [
        'en' => ['@' => 'at', '&' => 'and'],
    ];
    private bool|string $emoji = false;

    /**
     * Cache of transliterators per locale.
     *
     * @var \Transliterator[]
     */
    private array $transliterators = [];

    public function __construct(
        private ?string $defaultLocale = null,
        array|\Closure|null $symbolsMap = null,
    ) {
        $this->symbolsMap = $symbolsMap ?? $this->symbolsMap;
    }

    public function setLocale(string $locale): void
    {
        $this->defaultLocale = $locale;
    }

    public function getLocale(): string
    {
        return $this->defaultLocale ?? '';
    }

    /**
     * @param bool|string $emoji true will use the same locale,
     *                           false will disable emoji,
     *                           and a string to use a specific locale
     */
    public function withEmoji(bool|string $emoji = true): static
    {
        if (false !== $emoji && !class_exists(EmojiTransliterator::class)) {
            throw new \LogicException(\sprintf('You cannot use the "%s()" method as the "symfony/emoji" package is not installed. Try running "composer require symfony/emoji".', __METHOD__));
        }

        $new = clone $this;
        $new->emoji = $emoji;

        return $new;
    }

    public function slug(string $string, string $separator = '-', ?string $locale = null): AbstractUnicodeString
    {
        $locale ??= $this->defaultLocale;

        $transliterator = [];
        if ($locale && ('de' === $locale || str_starts_with($locale, 'de_'))) {
            // Use the shortcut for German in UnicodeString::ascii() if possible (faster and no requirement on intl)
            $transliterator = ['de-ASCII'];
        } elseif (\function_exists('transliterator_transliterate') && $locale) {
            $transliterator = (array) $this->createTransliterator($locale);
        }

        if ($emojiTransliterator = $this->createEmojiTransliterator($locale)) {
            $transliterator[] = $emojiTransliterator;
        }

        if ($this->symbolsMap instanceof \Closure) {
            // If the symbols map is passed as a closure, there is no need to fallback to the parent locale
            // as the closure can just provide substitutions for all locales of interest.
            $symbolsMap = $this->symbolsMap;
            array_unshift($transliterator, static fn ($s) => $symbolsMap($s, $locale));
        }

        $unicodeString = (new UnicodeString($string))->ascii($transliterator);

        if (\is_array($this->symbolsMap)) {
            $map = null;
            if (isset($this->symbolsMap[$locale ?? ''])) {
                $map = $this->symbolsMap[$locale ?? ''];
            } else {
                $parent = self::getParentLocale($locale);
                if ($parent && isset($this->symbolsMap[$parent])) {
                    $map = $this->symbolsMap[$parent];
                }
            }
            if ($map) {
                foreach ($map as $char => $replace) {
                    $unicodeString = $unicodeString->replace($char, ' '.$replace.' ');
                }
            }
        }

        return $unicodeString
            ->replaceMatches('/[^A-Za-z0-9]++/', $separator)
            ->trim($separator)
        ;
    }

    private function createTransliterator(string $locale): ?\Transliterator
    {
        if (\array_key_exists($locale, $this->transliterators)) {
            return $this->transliterators[$locale];
        }

        // Exact locale supported, cache and return
        if ($id = self::LOCALE_TO_TRANSLITERATOR_ID[$locale] ?? null) {
            return $this->transliterators[$locale] = \Transliterator::create($id.'/BGN') ?? \Transliterator::create($id);
        }

        // Locale not supported and no parent, fallback to any-latin
        if (!$parent = self::getParentLocale($locale)) {
            return $this->transliterators[$locale] = null;
        }

        // Try to use the parent locale (ie. try "de" for "de_AT") and cache both locales
        if ($id = self::LOCALE_TO_TRANSLITERATOR_ID[$parent] ?? null) {
            $transliterator = \Transliterator::create($id.'/BGN') ?? \Transliterator::create($id);
        }

        return $this->transliterators[$locale] = $this->transliterators[$parent] = $transliterator ?? null;
    }

    private function createEmojiTransliterator(?string $locale): ?EmojiTransliterator
    {
        if (\is_string($this->emoji)) {
            $locale = $this->emoji;
        } elseif (!$this->emoji) {
            return null;
        }

        while (null !== $locale) {
            try {
                return EmojiTransliterator::create("emoji-$locale");
            } catch (\IntlException) {
                $locale = self::getParentLocale($locale);
            }
        }

        return null;
    }

    private static function getParentLocale(?string $locale): ?string
    {
        if (!$locale) {
            return null;
        }
        if (false === $str = strrchr($locale, '_')) {
            // no parent locale
            return null;
        }

        return substr($locale, 0, -\strlen($str));
    }
}
<?php

/*
 * This file has been auto-generated by the Symfony String Component for internal use.
 *
 * Unicode version: 16.0.0
 * Date: 2024-09-11T08:21:22+00:00
 */

return [
    [
        768,
        879,
    ],
    [
        1155,
        1159,
    ],
    [
        1160,
        1161,
    ],
    [
        1425,
        1469,
    ],
    [
        1471,
        1471,
    ],
    [
        1473,
        1474,
    ],
    [
        1476,
        1477,
    ],
    [
        1479,
        1479,
    ],
    [
        1552,
        1562,
    ],
    [
        1611,
        1631,
    ],
    [
        1648,
        1648,
    ],
    [
        1750,
        1756,
    ],
    [
        1759,
        1764,
    ],
    [
        1767,
        1768,
    ],
    [
        1770,
        1773,
    ],
    [
        1809,
        1809,
    ],
    [
        1840,
        1866,
    ],
    [
        1958,
        1968,
    ],
    [
        2027,
        2035,
    ],
    [
        2045,
        2045,
    ],
    [
        2070,
        2073,
    ],
    [
        2075,
        2083,
    ],
    [
        2085,
        2087,
    ],
    [
        2089,
        2093,
    ],
    [
        2137,
        2139,
    ],
    [
        2199,
        2207,
    ],
    [
        2250,
        2273,
    ],
    [
        2275,
        2306,
    ],
    [
        2362,
        2362,
    ],
    [
        2364,
        2364,
    ],
    [
        2369,
        2376,
    ],
    [
        2381,
        2381,
    ],
    [
        2385,
        2391,
    ],
    [
        2402,
        2403,
    ],
    [
        2433,
        2433,
    ],
    [
        2492,
        2492,
    ],
    [
        2497,
        2500,
    ],
    [
        2509,
        2509,
    ],
    [
        2530,
        2531,
    ],
    [
        2558,
        2558,
    ],
    [
        2561,
        2562,
    ],
    [
        2620,
        2620,
    ],
    [
        2625,
        2626,
    ],
    [
        2631,
        2632,
    ],
    [
        2635,
        2637,
    ],
    [
        2641,
        2641,
    ],
    [
        2672,
        2673,
    ],
    [
        2677,
        2677,
    ],
    [
        2689,
        2690,
    ],
    [
        2748,
        2748,
    ],
    [
        2753,
        2757,
    ],
    [
        2759,
        2760,
    ],
    [
        2765,
        2765,
    ],
    [
        2786,
        2787,
    ],
    [
        2810,
        2815,
    ],
    [
        2817,
        2817,
    ],
    [
        2876,
        2876,
    ],
    [
        2879,
        2879,
    ],
    [
        2881,
        2884,
    ],
    [
        2893,
        2893,
    ],
    [
        2901,
        2902,
    ],
    [
        2914,
        2915,
    ],
    [
        2946,
        2946,
    ],
    [
        3008,
        3008,
    ],
    [
        3021,
        3021,
    ],
    [
        3072,
        3072,
    ],
    [
        3076,
        3076,
    ],
    [
        3132,
        3132,
    ],
    [
        3134,
        3136,
    ],
    [
        3142,
        3144,
    ],
    [
        3146,
        3149,
    ],
    [
        3157,
        3158,
    ],
    [
        3170,
        3171,
    ],
    [
        3201,
        3201,
    ],
    [
        3260,
        3260,
    ],
    [
        3263,
        3263,
    ],
    [
        3270,
        3270,
    ],
    [
        3276,
        3277,
    ],
    [
        3298,
        3299,
    ],
    [
        3328,
        3329,
    ],
    [
        3387,
        3388,
    ],
    [
        3393,
        3396,
    ],
    [
        3405,
        3405,
    ],
    [
        3426,
        3427,
    ],
    [
        3457,
        3457,
    ],
    [
        3530,
        3530,
    ],
    [
        3538,
        3540,
    ],
    [
        3542,
        3542,
    ],
    [
        3633,
        3633,
    ],
    [
        3636,
        3642,
    ],
    [
        3655,
        3662,
    ],
    [
        3761,
        3761,
    ],
    [
        3764,
        3772,
    ],
    [
        3784,
        3790,
    ],
    [
        3864,
        3865,
    ],
    [
        3893,
        3893,
    ],
    [
        3895,
        3895,
    ],
    [
        3897,
        3897,
    ],
    [
        3953,
        3966,
    ],
    [
        3968,
        3972,
    ],
    [
        3974,
        3975,
    ],
    [
        3981,
        3991,
    ],
    [
        3993,
        4028,
    ],
    [
        4038,
        4038,
    ],
    [
        4141,
        4144,
    ],
    [
        4146,
        4151,
    ],
    [
        4153,
        4154,
    ],
    [
        4157,
        4158,
    ],
    [
        4184,
        4185,
    ],
    [
        4190,
        4192,
    ],
    [
        4209,
        4212,
    ],
    [
        4226,
        4226,
    ],
    [
        4229,
        4230,
    ],
    [
        4237,
        4237,
    ],
    [
        4253,
        4253,
    ],
    [
        4957,
        4959,
    ],
    [
        5906,
        5908,
    ],
    [
        5938,
        5939,
    ],
    [
        5970,
        5971,
    ],
    [
        6002,
        6003,
    ],
    [
        6068,
        6069,
    ],
    [
        6071,
        6077,
    ],
    [
        6086,
        6086,
    ],
    [
        6089,
        6099,
    ],
    [
        6109,
        6109,
    ],
    [
        6155,
        6157,
    ],
    [
        6159,
        6159,
    ],
    [
        6277,
        6278,
    ],
    [
        6313,
        6313,
    ],
    [
        6432,
        6434,
    ],
    [
        6439,
        6440,
    ],
    [
        6450,
        6450,
    ],
    [
        6457,
        6459,
    ],
    [
        6679,
        6680,
    ],
    [
        6683,
        6683,
    ],
    [
        6742,
        6742,
    ],
    [
        6744,
        6750,
    ],
    [
        6752,
        6752,
    ],
    [
        6754,
        6754,
    ],
    [
        6757,
        6764,
    ],
    [
        6771,
        6780,
    ],
    [
        6783,
        6783,
    ],
    [
        6832,
        6845,
    ],
    [
        6846,
        6846,
    ],
    [
        6847,
        6862,
    ],
    [
        6912,
        6915,
    ],
    [
        6964,
        6964,
    ],
    [
        6966,
        6970,
    ],
    [
        6972,
        6972,
    ],
    [
        6978,
        6978,
    ],
    [
        7019,
        7027,
    ],
    [
        7040,
        7041,
    ],
    [
        7074,
        7077,
    ],
    [
        7080,
        7081,
    ],
    [
        7083,
        7085,
    ],
    [
        7142,
        7142,
    ],
    [
        7144,
        7145,
    ],
    [
        7149,
        7149,
    ],
    [
        7151,
        7153,
    ],
    [
        7212,
        7219,
    ],
    [
        7222,
        7223,
    ],
    [
        7376,
        7378,
    ],
    [
        7380,
        7392,
    ],
    [
        7394,
        7400,
    ],
    [
        7405,
        7405,
    ],
    [
        7412,
        7412,
    ],
    [
        7416,
        7417,
    ],
    [
        7616,
        7679,
    ],
    [
        8400,
        8412,
    ],
    [
        8413,
        8416,
    ],
    [
        8417,
        8417,
    ],
    [
        8418,
        8420,
    ],
    [
        8421,
        8432,
    ],
    [
        11503,
        11505,
    ],
    [
        11647,
        11647,
    ],
    [
        11744,
        11775,
    ],
    [
        12330,
        12333,
    ],
    [
        12441,
        12442,
    ],
    [
        42607,
        42607,
    ],
    [
        42608,
        42610,
    ],
    [
        42612,
        42621,
    ],
    [
        42654,
        42655,
    ],
    [
        42736,
        42737,
    ],
    [
        43010,
        43010,
    ],
    [
        43014,
        43014,
    ],
    [
        43019,
        43019,
    ],
    [
        43045,
        43046,
    ],
    [
        43052,
        43052,
    ],
    [
        43204,
        43205,
    ],
    [
        43232,
        43249,
    ],
    [
        43263,
        43263,
    ],
    [
        43302,
        43309,
    ],
    [
        43335,
        43345,
    ],
    [
        43392,
        43394,
    ],
    [
        43443,
        43443,
    ],
    [
        43446,
        43449,
    ],
    [
        43452,
        43453,
    ],
    [
        43493,
        43493,
    ],
    [
        43561,
        43566,
    ],
    [
        43569,
        43570,
    ],
    [
        43573,
        43574,
    ],
    [
        43587,
        43587,
    ],
    [
        43596,
        43596,
    ],
    [
        43644,
        43644,
    ],
    [
        43696,
        43696,
    ],
    [
        43698,
        43700,
    ],
    [
        43703,
        43704,
    ],
    [
        43710,
        43711,
    ],
    [
        43713,
        43713,
    ],
    [
        43756,
        43757,
    ],
    [
        43766,
        43766,
    ],
    [
        44005,
        44005,
    ],
    [
        44008,
        44008,
    ],
    [
        44013,
        44013,
    ],
    [
        64286,
        64286,
    ],
    [
        65024,
        65039,
    ],
    [
        65056,
        65071,
    ],
    [
        66045,
        66045,
    ],
    [
        66272,
        66272,
    ],
    [
        66422,
        66426,
    ],
    [
        68097,
        68099,
    ],
    [
        68101,
        68102,
    ],
    [
        68108,
        68111,
    ],
    [
        68152,
        68154,
    ],
    [
        68159,
        68159,
    ],
    [
        68325,
        68326,
    ],
    [
        68900,
        68903,
    ],
    [
        68969,
        68973,
    ],
    [
        69291,
        69292,
    ],
    [
        69372,
        69375,
    ],
    [
        69446,
        69456,
    ],
    [
        69506,
        69509,
    ],
    [
        69633,
        69633,
    ],
    [
        69688,
        69702,
    ],
    [
        69744,
        69744,
    ],
    [
        69747,
        69748,
    ],
    [
        69759,
        69761,
    ],
    [
        69811,
        69814,
    ],
    [
        69817,
        69818,
    ],
    [
        69826,
        69826,
    ],
    [
        69888,
        69890,
    ],
    [
        69927,
        69931,
    ],
    [
        69933,
        69940,
    ],
    [
        70003,
        70003,
    ],
    [
        70016,
        70017,
    ],
    [
        70070,
        70078,
    ],
    [
        70089,
        70092,
    ],
    [
        70095,
        70095,
    ],
    [
        70191,
        70193,
    ],
    [
        70196,
        70196,
    ],
    [
        70198,
        70199,
    ],
    [
        70206,
        70206,
    ],
    [
        70209,
        70209,
    ],
    [
        70367,
        70367,
    ],
    [
        70371,
        70378,
    ],
    [
        70400,
        70401,
    ],
    [
        70459,
        70460,
    ],
    [
        70464,
        70464,
    ],
    [
        70502,
        70508,
    ],
    [
        70512,
        70516,
    ],
    [
        70587,
        70592,
    ],
    [
        70606,
        70606,
    ],
    [
        70608,
        70608,
    ],
    [
        70610,
        70610,
    ],
    [
        70625,
        70626,
    ],
    [
        70712,
        70719,
    ],
    [
        70722,
        70724,
    ],
    [
        70726,
        70726,
    ],
    [
        70750,
        70750,
    ],
    [
        70835,
        70840,
    ],
    [
        70842,
        70842,
    ],
    [
        70847,
        70848,
    ],
    [
        70850,
        70851,
    ],
    [
        71090,
        71093,
    ],
    [
        71100,
        71101,
    ],
    [
        71103,
        71104,
    ],
    [
        71132,
        71133,
    ],
    [
        71219,
        71226,
    ],
    [
        71229,
        71229,
    ],
    [
        71231,
        71232,
    ],
    [
        71339,
        71339,
    ],
    [
        71341,
        71341,
    ],
    [
        71344,
        71349,
    ],
    [
        71351,
        71351,
    ],
    [
        71453,
        71453,
    ],
    [
        71455,
        71455,
    ],
    [
        71458,
        71461,
    ],
    [
        71463,
        71467,
    ],
    [
        71727,
        71735,
    ],
    [
        71737,
        71738,
    ],
    [
        71995,
        71996,
    ],
    [
        71998,
        71998,
    ],
    [
        72003,
        72003,
    ],
    [
        72148,
        72151,
    ],
    [
        72154,
        72155,
    ],
    [
        72160,
        72160,
    ],
    [
        72193,
        72202,
    ],
    [
        72243,
        72248,
    ],
    [
        72251,
        72254,
    ],
    [
        72263,
        72263,
    ],
    [
        72273,
        72278,
    ],
    [
        72281,
        72283,
    ],
    [
        72330,
        72342,
    ],
    [
        72344,
        72345,
    ],
    [
        72752,
        72758,
    ],
    [
        72760,
        72765,
    ],
    [
        72767,
        72767,
    ],
    [
        72850,
        72871,
    ],
    [
        72874,
        72880,
    ],
    [
        72882,
        72883,
    ],
    [
        72885,
        72886,
    ],
    [
        73009,
        73014,
    ],
    [
        73018,
        73018,
    ],
    [
        73020,
        73021,
    ],
    [
        73023,
        73029,
    ],
    [
        73031,
        73031,
    ],
    [
        73104,
        73105,
    ],
    [
        73109,
        73109,
    ],
    [
        73111,
        73111,
    ],
    [
        73459,
        73460,
    ],
    [
        73472,
        73473,
    ],
    [
        73526,
        73530,
    ],
    [
        73536,
        73536,
    ],
    [
        73538,
        73538,
    ],
    [
        73562,
        73562,
    ],
    [
        78912,
        78912,
    ],
    [
        78919,
        78933,
    ],
    [
        90398,
        90409,
    ],
    [
        90413,
        90415,
    ],
    [
        92912,
        92916,
    ],
    [
        92976,
        92982,
    ],
    [
        94031,
        94031,
    ],
    [
        94095,
        94098,
    ],
    [
        94180,
        94180,
    ],
    [
        113821,
        113822,
    ],
    [
        118528,
        118573,
    ],
    [
        118576,
        118598,
    ],
    [
        119143,
        119145,
    ],
    [
        119163,
        119170,
    ],
    [
        119173,
        119179,
    ],
    [
        119210,
        119213,
    ],
    [
        119362,
        119364,
    ],
    [
        121344,
        121398,
    ],
    [
        121403,
        121452,
    ],
    [
        121461,
        121461,
    ],
    [
        121476,
        121476,
    ],
    [
        121499,
        121503,
    ],
    [
        121505,
        121519,
    ],
    [
        122880,
        122886,
    ],
    [
        122888,
        122904,
    ],
    [
        122907,
        122913,
    ],
    [
        122915,
        122916,
    ],
    [
        122918,
        122922,
    ],
    [
        123023,
        123023,
    ],
    [
        123184,
        123190,
    ],
    [
        123566,
        123566,
    ],
    [
        123628,
        123631,
    ],
    [
        124140,
        124143,
    ],
    [
        124398,
        124399,
    ],
    [
        125136,
        125142,
    ],
    [
        125252,
        125258,
    ],
    [
        917760,
        917999,
    ],
];
<?php

/*
 * This file has been auto-generated by the Symfony String Component for internal use.
 *
 * Unicode version: 16.0.0
 * Date: 2024-09-11T08:21:22+00:00
 */

return [
    [
        4352,
        4447,
    ],
    [
        8986,
        8987,
    ],
    [
        9001,
        9001,
    ],
    [
        9002,
        9002,
    ],
    [
        9193,
        9196,
    ],
    [
        9200,
        9200,
    ],
    [
        9203,
        9203,
    ],
    [
        9725,
        9726,
    ],
    [
        9748,
        9749,
    ],
    [
        9776,
        9783,
    ],
    [
        9800,
        9811,
    ],
    [
        9855,
        9855,
    ],
    [
        9866,
        9871,
    ],
    [
        9875,
        9875,
    ],
    [
        9889,
        9889,
    ],
    [
        9898,
        9899,
    ],
    [
        9917,
        9918,
    ],
    [
        9924,
        9925,
    ],
    [
        9934,
        9934,
    ],
    [
        9940,
        9940,
    ],
    [
        9962,
        9962,
    ],
    [
        9970,
        9971,
    ],
    [
        9973,
        9973,
    ],
    [
        9978,
        9978,
    ],
    [
        9981,
        9981,
    ],
    [
        9989,
        9989,
    ],
    [
        9994,
        9995,
    ],
    [
        10024,
        10024,
    ],
    [
        10060,
        10060,
    ],
    [
        10062,
        10062,
    ],
    [
        10067,
        10069,
    ],
    [
        10071,
        10071,
    ],
    [
        10133,
        10135,
    ],
    [
        10160,
        10160,
    ],
    [
        10175,
        10175,
    ],
    [
        11035,
        11036,
    ],
    [
        11088,
        11088,
    ],
    [
        11093,
        11093,
    ],
    [
        11904,
        11929,
    ],
    [
        11931,
        12019,
    ],
    [
        12032,
        12245,
    ],
    [
        12272,
        12287,
    ],
    [
        12288,
        12288,
    ],
    [
        12289,
        12291,
    ],
    [
        12292,
        12292,
    ],
    [
        12293,
        12293,
    ],
    [
        12294,
        12294,
    ],
    [
        12295,
        12295,
    ],
    [
        12296,
        12296,
    ],
    [
        12297,
        12297,
    ],
    [
        12298,
        12298,
    ],
    [
        12299,
        12299,
    ],
    [
        12300,
        12300,
    ],
    [
        12301,
        12301,
    ],
    [
        12302,
        12302,
    ],
    [
        12303,
        12303,
    ],
    [
        12304,
        12304,
    ],
    [
        12305,
        12305,
    ],
    [
        12306,
        12307,
    ],
    [
        12308,
        12308,
    ],
    [
        12309,
        12309,
    ],
    [
        12310,
        12310,
    ],
    [
        12311,
        12311,
    ],
    [
        12312,
        12312,
    ],
    [
        12313,
        12313,
    ],
    [
        12314,
        12314,
    ],
    [
        12315,
        12315,
    ],
    [
        12316,
        12316,
    ],
    [
        12317,
        12317,
    ],
    [
        12318,
        12319,
    ],
    [
        12320,
        12320,
    ],
    [
        12321,
        12329,
    ],
    [
        12330,
        12333,
    ],
    [
        12334,
        12335,
    ],
    [
        12336,
        12336,
    ],
    [
        12337,
        12341,
    ],
    [
        12342,
        12343,
    ],
    [
        12344,
        12346,
    ],
    [
        12347,
        12347,
    ],
    [
        12348,
        12348,
    ],
    [
        12349,
        12349,
    ],
    [
        12350,
        12350,
    ],
    [
        12353,
        12438,
    ],
    [
        12441,
        12442,
    ],
    [
        12443,
        12444,
    ],
    [
        12445,
        12446,
    ],
    [
        12447,
        12447,
    ],
    [
        12448,
        12448,
    ],
    [
        12449,
        12538,
    ],
    [
        12539,
        12539,
    ],
    [
        12540,
        12542,
    ],
    [
        12543,
        12543,
    ],
    [
        12549,
        12591,
    ],
    [
        12593,
        12686,
    ],
    [
        12688,
        12689,
    ],
    [
        12690,
        12693,
    ],
    [
        12694,
        12703,
    ],
    [
        12704,
        12735,
    ],
    [
        12736,
        12773,
    ],
    [
        12783,
        12783,
    ],
    [
        12784,
        12799,
    ],
    [
        12800,
        12830,
    ],
    [
        12832,
        12841,
    ],
    [
        12842,
        12871,
    ],
    [
        12880,
        12880,
    ],
    [
        12881,
        12895,
    ],
    [
        12896,
        12927,
    ],
    [
        12928,
        12937,
    ],
    [
        12938,
        12976,
    ],
    [
        12977,
        12991,
    ],
    [
        12992,
        13055,
    ],
    [
        13056,
        13311,
    ],
    [
        13312,
        19903,
    ],
    [
        19904,
        19967,
    ],
    [
        19968,
        40959,
    ],
    [
        40960,
        40980,
    ],
    [
        40981,
        40981,
    ],
    [
        40982,
        42124,
    ],
    [
        42128,
        42182,
    ],
    [
        43360,
        43388,
    ],
    [
        44032,
        55203,
    ],
    [
        63744,
        64109,
    ],
    [
        64110,
        64111,
    ],
    [
        64112,
        64217,
    ],
    [
        64218,
        64255,
    ],
    [
        65040,
        65046,
    ],
    [
        65047,
        65047,
    ],
    [
        65048,
        65048,
    ],
    [
        65049,
        65049,
    ],
    [
        65072,
        65072,
    ],
    [
        65073,
        65074,
    ],
    [
        65075,
        65076,
    ],
    [
        65077,
        65077,
    ],
    [
        65078,
        65078,
    ],
    [
        65079,
        65079,
    ],
    [
        65080,
        65080,
    ],
    [
        65081,
        65081,
    ],
    [
        65082,
        65082,
    ],
    [
        65083,
        65083,
    ],
    [
        65084,
        65084,
    ],
    [
        65085,
        65085,
    ],
    [
        65086,
        65086,
    ],
    [
        65087,
        65087,
    ],
    [
        65088,
        65088,
    ],
    [
        65089,
        65089,
    ],
    [
        65090,
        65090,
    ],
    [
        65091,
        65091,
    ],
    [
        65092,
        65092,
    ],
    [
        65093,
        65094,
    ],
    [
        65095,
        65095,
    ],
    [
        65096,
        65096,
    ],
    [
        65097,
        65100,
    ],
    [
        65101,
        65103,
    ],
    [
        65104,
        65106,
    ],
    [
        65108,
        65111,
    ],
    [
        65112,
        65112,
    ],
    [
        65113,
        65113,
    ],
    [
        65114,
        65114,
    ],
    [
        65115,
        65115,
    ],
    [
        65116,
        65116,
    ],
    [
        65117,
        65117,
    ],
    [
        65118,
        65118,
    ],
    [
        65119,
        65121,
    ],
    [
        65122,
        65122,
    ],
    [
        65123,
        65123,
    ],
    [
        65124,
        65126,
    ],
    [
        65128,
        65128,
    ],
    [
        65129,
        65129,
    ],
    [
        65130,
        65131,
    ],
    [
        65281,
        65283,
    ],
    [
        65284,
        65284,
    ],
    [
        65285,
        65287,
    ],
    [
        65288,
        65288,
    ],
    [
        65289,
        65289,
    ],
    [
        65290,
        65290,
    ],
    [
        65291,
        65291,
    ],
    [
        65292,
        65292,
    ],
    [
        65293,
        65293,
    ],
    [
        65294,
        65295,
    ],
    [
        65296,
        65305,
    ],
    [
        65306,
        65307,
    ],
    [
        65308,
        65310,
    ],
    [
        65311,
        65312,
    ],
    [
        65313,
        65338,
    ],
    [
        65339,
        65339,
    ],
    [
        65340,
        65340,
    ],
    [
        65341,
        65341,
    ],
    [
        65342,
        65342,
    ],
    [
        65343,
        65343,
    ],
    [
        65344,
        65344,
    ],
    [
        65345,
        65370,
    ],
    [
        65371,
        65371,
    ],
    [
        65372,
        65372,
    ],
    [
        65373,
        65373,
    ],
    [
        65374,
        65374,
    ],
    [
        65375,
        65375,
    ],
    [
        65376,
        65376,
    ],
    [
        65504,
        65505,
    ],
    [
        65506,
        65506,
    ],
    [
        65507,
        65507,
    ],
    [
        65508,
        65508,
    ],
    [
        65509,
        65510,
    ],
    [
        94176,
        94177,
    ],
    [
        94178,
        94178,
    ],
    [
        94179,
        94179,
    ],
    [
        94180,
        94180,
    ],
    [
        94192,
        94193,
    ],
    [
        94208,
        100343,
    ],
    [
        100352,
        101119,
    ],
    [
        101120,
        101589,
    ],
    [
        101631,
        101631,
    ],
    [
        101632,
        101640,
    ],
    [
        110576,
        110579,
    ],
    [
        110581,
        110587,
    ],
    [
        110589,
        110590,
    ],
    [
        110592,
        110847,
    ],
    [
        110848,
        110882,
    ],
    [
        110898,
        110898,
    ],
    [
        110928,
        110930,
    ],
    [
        110933,
        110933,
    ],
    [
        110948,
        110951,
    ],
    [
        110960,
        111355,
    ],
    [
        119552,
        119638,
    ],
    [
        119648,
        119670,
    ],
    [
        126980,
        126980,
    ],
    [
        127183,
        127183,
    ],
    [
        127374,
        127374,
    ],
    [
        127377,
        127386,
    ],
    [
        127488,
        127490,
    ],
    [
        127504,
        127547,
    ],
    [
        127552,
        127560,
    ],
    [
        127568,
        127569,
    ],
    [
        127584,
        127589,
    ],
    [
        127744,
        127776,
    ],
    [
        127789,
        127797,
    ],
    [
        127799,
        127868,
    ],
    [
        127870,
        127891,
    ],
    [
        127904,
        127946,
    ],
    [
        127951,
        127955,
    ],
    [
        127968,
        127984,
    ],
    [
        127988,
        127988,
    ],
    [
        127992,
        127994,
    ],
    [
        127995,
        127999,
    ],
    [
        128000,
        128062,
    ],
    [
        128064,
        128064,
    ],
    [
        128066,
        128252,
    ],
    [
        128255,
        128317,
    ],
    [
        128331,
        128334,
    ],
    [
        128336,
        128359,
    ],
    [
        128378,
        128378,
    ],
    [
        128405,
        128406,
    ],
    [
        128420,
        128420,
    ],
    [
        128507,
        128511,
    ],
    [
        128512,
        128591,
    ],
    [
        128640,
        128709,
    ],
    [
        128716,
        128716,
    ],
    [
        128720,
        128722,
    ],
    [
        128725,
        128727,
    ],
    [
        128732,
        128735,
    ],
    [
        128747,
        128748,
    ],
    [
        128756,
        128764,
    ],
    [
        128992,
        129003,
    ],
    [
        129008,
        129008,
    ],
    [
        129292,
        129338,
    ],
    [
        129340,
        129349,
    ],
    [
        129351,
        129535,
    ],
    [
        129648,
        129660,
    ],
    [
        129664,
        129673,
    ],
    [
        129679,
        129734,
    ],
    [
        129742,
        129756,
    ],
    [
        129759,
        129769,
    ],
    [
        129776,
        129784,
    ],
    [
        131072,
        173791,
    ],
    [
        173792,
        173823,
    ],
    [
        173824,
        177977,
    ],
    [
        177978,
        177983,
    ],
    [
        177984,
        178205,
    ],
    [
        178206,
        178207,
    ],
    [
        178208,
        183969,
    ],
    [
        183970,
        183983,
    ],
    [
        183984,
        191456,
    ],
    [
        191457,
        191471,
    ],
    [
        191472,
        192093,
    ],
    [
        192094,
        194559,
    ],
    [
        194560,
        195101,
    ],
    [
        195102,
        195103,
    ],
    [
        195104,
        196605,
    ],
    [
        196608,
        201546,
    ],
    [
        201547,
        201551,
    ],
    [
        201552,
        205743,
    ],
    [
        205744,
        262141,
    ],
];
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\String;

if (!\function_exists(u::class)) {
    function u(?string $string = ''): UnicodeString
    {
        return new UnicodeString($string ?? '');
    }
}

if (!\function_exists(b::class)) {
    function b(?string $string = ''): ByteString
    {
        return new ByteString($string ?? '');
    }
}

if (!\function_exists(s::class)) {
    /**
     * @return UnicodeString|ByteString
     */
    function s(?string $string = ''): AbstractString
    {
        $string ??= '';

        return preg_match('//u', $string) ? new UnicodeString($string) : new ByteString($string);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Filesystem\Exception;

/**
 * IOException interface for file and input/output stream related exceptions thrown by the component.
 *
 * @author Christian Gärtner <christiangaertner.film@googlemail.com>
 */
interface IOExceptionInterface extends ExceptionInterface
{
    /**
     * Returns the associated path for the exception.
     */
    public function getPath(): ?string;
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Filesystem\Exception;

/**
 * Exception class thrown when a filesystem operation failure happens.
 *
 * @author Romain Neutron <imprec@gmail.com>
 * @author Christian Gärtner <christiangaertner.film@googlemail.com>
 * @author Fabien Potencier <fabien@symfony.com>
 */
class IOException extends \RuntimeException implements IOExceptionInterface
{
    private ?string $path;

    public function __construct(string $message, int $code = 0, ?\Throwable $previous = null, ?string $path = null)
    {
        $this->path = $path;

        parent::__construct($message, $code, $previous);
    }

    public function getPath(): ?string
    {
        return $this->path;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Filesystem\Exception;

/**
 * @author Théo Fidry <theo.fidry@gmail.com>
 */
class RuntimeException extends \RuntimeException implements ExceptionInterface
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Filesystem\Exception;

/**
 * Exception class thrown when a file couldn't be found.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 * @author Christian Gärtner <christiangaertner.film@googlemail.com>
 */
class FileNotFoundException extends IOException
{
    public function __construct(?string $message = null, int $code = 0, ?\Throwable $previous = null, ?string $path = null)
    {
        if (null === $message) {
            if (null === $path) {
                $message = 'File could not be found.';
            } else {
                $message = \sprintf('File "%s" could not be found.', $path);
            }
        }

        parent::__construct($message, $code, $previous, $path);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Filesystem\Exception;

/**
 * Exception interface for all exceptions thrown by the component.
 *
 * @author Romain Neutron <imprec@gmail.com>
 */
interface ExceptionInterface extends \Throwable
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Filesystem\Exception;

/**
 * @author Christian Flothmann <christian.flothmann@sensiolabs.de>
 */
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Filesystem;

use Symfony\Component\Filesystem\Exception\FileNotFoundException;
use Symfony\Component\Filesystem\Exception\InvalidArgumentException;
use Symfony\Component\Filesystem\Exception\IOException;

/**
 * Provides basic utility to manipulate the file system.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class Filesystem
{
    private static ?string $lastError = null;

    /**
     * Copies a file.
     *
     * If the target file is older than the origin file, it's always overwritten.
     * If the target file is newer, it is overwritten only when the
     * $overwriteNewerFiles option is set to true.
     *
     * @return void
     *
     * @throws FileNotFoundException When originFile doesn't exist
     * @throws IOException           When copy fails
     */
    public function copy(string $originFile, string $targetFile, bool $overwriteNewerFiles = false)
    {
        $originIsLocal = stream_is_local($originFile) || 0 === stripos($originFile, 'file://');
        if ($originIsLocal && !is_file($originFile)) {
            throw new FileNotFoundException(\sprintf('Failed to copy "%s" because file does not exist.', $originFile), 0, null, $originFile);
        }

        $this->mkdir(\dirname($targetFile));

        $doCopy = true;
        if (!$overwriteNewerFiles && !parse_url($originFile, \PHP_URL_HOST) && is_file($targetFile)) {
            $doCopy = filemtime($originFile) > filemtime($targetFile);
        }

        if ($doCopy) {
            // https://bugs.php.net/64634
            if (!$source = self::box('fopen', $originFile, 'r')) {
                throw new IOException(\sprintf('Failed to copy "%s" to "%s" because source file could not be opened for reading: ', $originFile, $targetFile).self::$lastError, 0, null, $originFile);
            }

            // Stream context created to allow files overwrite when using FTP stream wrapper - disabled by default
            if (!$target = self::box('fopen', $targetFile, 'w', false, stream_context_create(['ftp' => ['overwrite' => true]]))) {
                throw new IOException(\sprintf('Failed to copy "%s" to "%s" because target file could not be opened for writing: ', $originFile, $targetFile).self::$lastError, 0, null, $originFile);
            }

            $bytesCopied = stream_copy_to_stream($source, $target);
            fclose($source);
            fclose($target);
            unset($source, $target);

            if (!is_file($targetFile)) {
                throw new IOException(\sprintf('Failed to copy "%s" to "%s".', $originFile, $targetFile), 0, null, $originFile);
            }

            if ($originIsLocal) {
                // Like `cp`, preserve the source mode masked by the umask
                self::box('chmod', $targetFile, fileperms($originFile) & 0o777 & ~umask());

                // Like `cp`, preserve the file modification time
                self::box('touch', $targetFile, filemtime($originFile));

                if ($bytesCopied !== $bytesOrigin = filesize($originFile)) {
                    throw new IOException(\sprintf('Failed to copy the whole content of "%s" to "%s" (%g of %g bytes copied).', $originFile, $targetFile, $bytesCopied, $bytesOrigin), 0, null, $originFile);
                }
            }
        }
    }

    /**
     * Creates a directory recursively.
     *
     * @return void
     *
     * @throws IOException On any directory creation failure
     */
    public function mkdir(string|iterable $dirs, int $mode = 0o777)
    {
        foreach ($this->toIterable($dirs) as $dir) {
            if (is_dir($dir)) {
                continue;
            }

            if (!self::box('mkdir', $dir, $mode, true) && !is_dir($dir)) {
                throw new IOException(\sprintf('Failed to create "%s": ', $dir).self::$lastError, 0, null, $dir);
            }
        }
    }

    /**
     * Checks the existence of files or directories.
     */
    public function exists(string|iterable $files): bool
    {
        $maxPathLength = \PHP_MAXPATHLEN - 2;

        foreach ($this->toIterable($files) as $file) {
            if (\strlen($file) > $maxPathLength) {
                throw new IOException(\sprintf('Could not check if file exist because path length exceeds %d characters.', $maxPathLength), 0, null, $file);
            }

            if (!file_exists($file)) {
                return false;
            }
        }

        return true;
    }

    /**
     * Sets access and modification time of file.
     *
     * @param int|null $time  The touch time as a Unix timestamp, if not supplied the current system time is used
     * @param int|null $atime The access time as a Unix timestamp, if not supplied the current system time is used
     *
     * @return void
     *
     * @throws IOException When touch fails
     */
    public function touch(string|iterable $files, ?int $time = null, ?int $atime = null)
    {
        foreach ($this->toIterable($files) as $file) {
            if (!($time ? self::box('touch', $file, $time, $atime) : self::box('touch', $file))) {
                throw new IOException(\sprintf('Failed to touch "%s": ', $file).self::$lastError, 0, null, $file);
            }
        }
    }

    /**
     * Removes files or directories.
     *
     * @return void
     *
     * @throws IOException When removal fails
     */
    public function remove(string|iterable $files)
    {
        if ($files instanceof \Traversable) {
            $files = iterator_to_array($files, false);
        } elseif (!\is_array($files)) {
            $files = [$files];
        }

        self::doRemove($files, false);
    }

    private static function doRemove(array $files, bool $isRecursive): void
    {
        $files = array_reverse($files);
        foreach ($files as $file) {
            if (is_link($file)) {
                // See https://bugs.php.net/52176
                if (!(self::box('unlink', $file) || '\\' !== \DIRECTORY_SEPARATOR || self::box('rmdir', $file)) && file_exists($file)) {
                    throw new IOException(\sprintf('Failed to remove symlink "%s": ', $file).self::$lastError);
                }
            } elseif (is_dir($file)) {
                if (!$isRecursive) {
                    $tmpName = \dirname(realpath($file)).'/.!'.strrev(strtr(base64_encode(random_bytes(2)), '/=', '-!'));

                    if (file_exists($tmpName)) {
                        try {
                            self::doRemove([$tmpName], true);
                        } catch (IOException) {
                        }
                    }

                    if (!file_exists($tmpName) && self::box('rename', $file, $tmpName)) {
                        $origFile = $file;
                        $file = $tmpName;
                    } else {
                        $origFile = null;
                    }
                }

                $filesystemIterator = new \FilesystemIterator($file, \FilesystemIterator::CURRENT_AS_PATHNAME | \FilesystemIterator::SKIP_DOTS);
                self::doRemove(iterator_to_array($filesystemIterator, true), true);

                if (!self::box('rmdir', $file) && file_exists($file) && !$isRecursive) {
                    $lastError = self::$lastError;

                    if (null !== $origFile && self::box('rename', $file, $origFile)) {
                        $file = $origFile;
                    }

                    throw new IOException(\sprintf('Failed to remove directory "%s": ', $file).$lastError);
                }
            } elseif (!self::box('unlink', $file) && ((self::$lastError && str_contains(self::$lastError, 'Permission denied')) || file_exists($file))) {
                throw new IOException(\sprintf('Failed to remove file "%s": ', $file).self::$lastError);
            }
        }
    }

    /**
     * Change mode for an array of files or directories.
     *
     * @param int  $mode      The new mode (octal)
     * @param int  $umask     The mode mask (octal)
     * @param bool $recursive Whether change the mod recursively or not
     *
     * @return void
     *
     * @throws IOException When the change fails
     */
    public function chmod(string|iterable $files, int $mode, int $umask = 0o000, bool $recursive = false)
    {
        foreach ($this->toIterable($files) as $file) {
            if (!self::box('chmod', $file, $mode & ~$umask)) {
                throw new IOException(\sprintf('Failed to chmod file "%s": ', $file).self::$lastError, 0, null, $file);
            }
            if ($recursive && is_dir($file) && !is_link($file)) {
                $this->chmod(new \FilesystemIterator($file), $mode, $umask, true);
            }
        }
    }

    /**
     * Change the owner of an array of files or directories.
     *
     * This method always throws on Windows, as the underlying PHP function is not supported.
     *
     * @see https://php.net/chown
     *
     * @param string|int $user      A user name or number
     * @param bool       $recursive Whether change the owner recursively or not
     *
     * @return void
     *
     * @throws IOException When the change fails
     */
    public function chown(string|iterable $files, string|int $user, bool $recursive = false)
    {
        foreach ($this->toIterable($files) as $file) {
            if ($recursive && is_dir($file) && !is_link($file)) {
                $this->chown(new \FilesystemIterator($file), $user, true);
            }
            if (is_link($file) && \function_exists('lchown')) {
                if (!self::box('lchown', $file, $user)) {
                    throw new IOException(\sprintf('Failed to chown file "%s": ', $file).self::$lastError, 0, null, $file);
                }
            } else {
                if (!self::box('chown', $file, $user)) {
                    throw new IOException(\sprintf('Failed to chown file "%s": ', $file).self::$lastError, 0, null, $file);
                }
            }
        }
    }

    /**
     * Change the group of an array of files or directories.
     *
     * This method always throws on Windows, as the underlying PHP function is not supported.
     *
     * @see https://php.net/chgrp
     *
     * @param string|int $group     A group name or number
     * @param bool       $recursive Whether change the group recursively or not
     *
     * @return void
     *
     * @throws IOException When the change fails
     */
    public function chgrp(string|iterable $files, string|int $group, bool $recursive = false)
    {
        foreach ($this->toIterable($files) as $file) {
            if ($recursive && is_dir($file) && !is_link($file)) {
                $this->chgrp(new \FilesystemIterator($file), $group, true);
            }
            if (is_link($file) && \function_exists('lchgrp')) {
                if (!self::box('lchgrp', $file, $group)) {
                    throw new IOException(\sprintf('Failed to chgrp file "%s": ', $file).self::$lastError, 0, null, $file);
                }
            } else {
                if (!self::box('chgrp', $file, $group)) {
                    throw new IOException(\sprintf('Failed to chgrp file "%s": ', $file).self::$lastError, 0, null, $file);
                }
            }
        }
    }

    /**
     * Renames a file or a directory.
     *
     * @return void
     *
     * @throws IOException When target file or directory already exists
     * @throws IOException When origin cannot be renamed
     */
    public function rename(string $origin, string $target, bool $overwrite = false)
    {
        // we check that target does not exist
        if (!$overwrite && $this->isReadable($target)) {
            throw new IOException(\sprintf('Cannot rename because the target "%s" already exists.', $target), 0, null, $target);
        }

        if (!self::box('rename', $origin, $target)) {
            if (is_dir($origin)) {
                // See https://bugs.php.net/54097 & https://php.net/rename#113943
                $this->mirror($origin, $target, null, ['override' => $overwrite, 'delete' => $overwrite]);
                $this->remove($origin);

                return;
            }
            throw new IOException(\sprintf('Cannot rename "%s" to "%s": ', $origin, $target).self::$lastError, 0, null, $target);
        }
    }

    /**
     * Tells whether a file exists and is readable.
     *
     * @throws IOException When windows path is longer than 258 characters
     */
    private function isReadable(string $filename): bool
    {
        $maxPathLength = \PHP_MAXPATHLEN - 2;

        if (\strlen($filename) > $maxPathLength) {
            throw new IOException(\sprintf('Could not check if file is readable because path length exceeds %d characters.', $maxPathLength), 0, null, $filename);
        }

        return is_readable($filename);
    }

    /**
     * Creates a symbolic link or copy a directory.
     *
     * @return void
     *
     * @throws IOException When symlink fails
     */
    public function symlink(string $originDir, string $targetDir, bool $copyOnWindows = false)
    {
        self::assertFunctionExists('symlink');

        if ('\\' === \DIRECTORY_SEPARATOR) {
            $originDir = strtr($originDir, '/', '\\');
            $targetDir = strtr($targetDir, '/', '\\');

            if ($copyOnWindows) {
                $this->mirror($originDir, $targetDir);

                return;
            }
        }

        $this->mkdir(\dirname($targetDir));

        if (is_link($targetDir)) {
            if (readlink($targetDir) === $originDir) {
                return;
            }
            $this->remove($targetDir);
        }

        if (!self::box('symlink', $originDir, $targetDir)) {
            $this->linkException($originDir, $targetDir, 'symbolic');
        }
    }

    /**
     * Creates a hard link, or several hard links to a file.
     *
     * @param string|string[] $targetFiles The target file(s)
     *
     * @return void
     *
     * @throws FileNotFoundException When original file is missing or not a file
     * @throws IOException           When link fails, including if link already exists
     */
    public function hardlink(string $originFile, string|iterable $targetFiles)
    {
        self::assertFunctionExists('link');

        if (!$this->exists($originFile)) {
            throw new FileNotFoundException(null, 0, null, $originFile);
        }

        if (!is_file($originFile)) {
            throw new FileNotFoundException(\sprintf('Origin file "%s" is not a file.', $originFile));
        }

        foreach ($this->toIterable($targetFiles) as $targetFile) {
            if (is_file($targetFile)) {
                if (fileinode($originFile) === fileinode($targetFile)) {
                    continue;
                }
                $this->remove($targetFile);
            }

            if (!self::box('link', $originFile, $targetFile)) {
                $this->linkException($originFile, $targetFile, 'hard');
            }
        }
    }

    /**
     * @param string $linkType Name of the link type, typically 'symbolic' or 'hard'
     */
    private function linkException(string $origin, string $target, string $linkType): never
    {
        if (self::$lastError) {
            if ('\\' === \DIRECTORY_SEPARATOR && str_contains(self::$lastError, 'error code(1314)')) {
                throw new IOException(\sprintf('Unable to create "%s" link due to error code 1314: \'A required privilege is not held by the client\'. Do you have the required Administrator-rights?', $linkType), 0, null, $target);
            }
        }
        throw new IOException(\sprintf('Failed to create "%s" link from "%s" to "%s": ', $linkType, $origin, $target).self::$lastError, 0, null, $target);
    }

    /**
     * Resolves links in paths.
     *
     * With $canonicalize = false (default)
     *      - if $path does not exist or is not a link, returns null
     *      - if $path is a link, returns the next direct target of the link without considering the existence of the target
     *
     * With $canonicalize = true
     *      - if $path does not exist, returns null
     *      - if $path exists, returns its absolute fully resolved final version
     */
    public function readlink(string $path, bool $canonicalize = false): ?string
    {
        if (!$canonicalize && !is_link($path)) {
            return null;
        }

        if ($canonicalize) {
            if (!$this->exists($path)) {
                return null;
            }

            return realpath($path);
        }

        return readlink($path);
    }

    /**
     * Given an existing path, convert it to a path relative to a given starting path.
     */
    public function makePathRelative(string $endPath, string $startPath): string
    {
        if (!$this->isAbsolutePath($startPath)) {
            throw new InvalidArgumentException(\sprintf('The start path "%s" is not absolute.', $startPath));
        }

        if (!$this->isAbsolutePath($endPath)) {
            throw new InvalidArgumentException(\sprintf('The end path "%s" is not absolute.', $endPath));
        }

        $originalEndPath = $endPath;

        // Normalize separators on Windows
        if ('\\' === \DIRECTORY_SEPARATOR) {
            $endPath = str_replace('\\', '/', $endPath);
            $startPath = str_replace('\\', '/', $startPath);
        }

        $splitDriveLetter = static fn ($path) => (\strlen($path) > 2 && ':' === $path[1] && '/' === $path[2] && ctype_alpha($path[0]))
            ? [substr($path, 2), strtoupper($path[0])]
            : [$path, null];

        $splitPath = static function ($path) {
            $result = [];

            foreach (explode('/', trim($path, '/')) as $segment) {
                if ('..' === $segment) {
                    array_pop($result);
                } elseif ('.' !== $segment && '' !== $segment) {
                    $result[] = $segment;
                }
            }

            return $result;
        };

        [$endPath, $endDriveLetter] = $splitDriveLetter($endPath);
        [$startPath, $startDriveLetter] = $splitDriveLetter($startPath);

        $startPathArr = $splitPath($startPath);
        $endPathArr = $splitPath($endPath);

        if ($endDriveLetter && $startDriveLetter && $endDriveLetter != $startDriveLetter) {
            // End path is on another drive, so no relative path exists
            return $endDriveLetter.':/'.($endPathArr ? implode('/', $endPathArr).'/' : '');
        }

        // Find for which directory the common path stops
        $index = 0;
        while (isset($startPathArr[$index]) && isset($endPathArr[$index]) && $startPathArr[$index] === $endPathArr[$index]) {
            ++$index;
        }

        // Determine how deep the start path is relative to the common path (ie, "web/bundles" = 2 levels)
        if (1 === \count($startPathArr) && '' === $startPathArr[0]) {
            $depth = 0;
        } else {
            $depth = \count($startPathArr) - $index;
        }

        // Repeated "../" for each level need to reach the common path
        $traverser = str_repeat('../', $depth);

        $endPathRemainder = implode('/', \array_slice($endPathArr, $index));

        // Construct $endPath from traversing to the common path, then to the remaining $endPath
        $relativePath = $traverser.('' !== $endPathRemainder ? $endPathRemainder.'/' : '');

        // Remove ending "/" if $endPath points to an existing file
        if (str_ends_with($relativePath, '/') && is_file($originalEndPath)) {
            $relativePath = substr($relativePath, 0, -1);
        }

        return '' === $relativePath ? './' : $relativePath;
    }

    /**
     * Mirrors a directory to another.
     *
     * Copies files and directories from the origin directory into the target directory. By default:
     *
     *  - existing files in the target directory will be overwritten, except if they are newer (see the `override` option)
     *  - files in the target directory that do not exist in the source directory will not be deleted (see the `delete` option)
     *
     * @param \Traversable|null $iterator Iterator that filters which files and directories to copy, if null a recursive iterator is created
     * @param array             $options  An array of boolean options
     *                                    Valid options are:
     *                                    - $options['override'] If true, target files newer than origin files are overwritten (see copy(), defaults to false)
     *                                    - $options['copy_on_windows'] Whether to copy files instead of links on Windows (see symlink(), defaults to false)
     *                                    - $options['delete'] Whether to delete files that are not in the source directory (defaults to false)
     *
     * @return void
     *
     * @throws IOException When file type is unknown
     */
    public function mirror(string $originDir, string $targetDir, ?\Traversable $iterator = null, array $options = [])
    {
        $targetDir = rtrim($targetDir, '/\\');
        $originDir = rtrim($originDir, '/\\');
        $originDirLen = \strlen($originDir);

        if (!$this->exists($originDir)) {
            throw new IOException(\sprintf('The origin directory specified "%s" was not found.', $originDir), 0, null, $originDir);
        }

        // Iterate in destination folder to remove obsolete entries
        if ($this->exists($targetDir) && isset($options['delete']) && $options['delete']) {
            $deleteIterator = $iterator;
            if (null === $deleteIterator) {
                $flags = \FilesystemIterator::SKIP_DOTS;
                $deleteIterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($targetDir, $flags), \RecursiveIteratorIterator::CHILD_FIRST);
            }
            $targetDirLen = \strlen($targetDir);
            foreach ($deleteIterator as $file) {
                $origin = $originDir.substr($file->getPathname(), $targetDirLen);
                if (!$this->exists($origin)) {
                    $this->remove($file);
                }
            }
        }

        $copyOnWindows = $options['copy_on_windows'] ?? false;

        if (null === $iterator) {
            $flags = $copyOnWindows ? \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS : \FilesystemIterator::SKIP_DOTS;
            $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($originDir, $flags), \RecursiveIteratorIterator::SELF_FIRST);
        }

        $this->mkdir($targetDir);
        $filesCreatedWhileMirroring = [];

        foreach ($iterator as $file) {
            if ($file->getPathname() === $targetDir || $file->getRealPath() === $targetDir || isset($filesCreatedWhileMirroring[$file->getRealPath()])) {
                continue;
            }

            $target = $targetDir.substr($file->getPathname(), $originDirLen);
            $filesCreatedWhileMirroring[$target] = true;

            if (!$copyOnWindows && is_link($file)) {
                $this->symlink($file->getLinkTarget(), $target);
            } elseif (is_dir($file)) {
                $this->mkdir($target);
            } elseif (is_file($file)) {
                $this->copy($file, $target, $options['override'] ?? false);
            } else {
                throw new IOException(\sprintf('Unable to guess "%s" file type.', $file), 0, null, $file);
            }
        }
    }

    /**
     * Returns whether the file path is an absolute path.
     */
    public function isAbsolutePath(string $file): bool
    {
        return '' !== $file && (strspn($file, '/\\', 0, 1)
            || (\strlen($file) > 3 && ctype_alpha($file[0])
                && ':' === $file[1]
                && strspn($file, '/\\', 2, 1)
            )
            || null !== parse_url($file, \PHP_URL_SCHEME)
        );
    }

    /**
     * Creates a temporary file with support for custom stream wrappers.
     *
     * @param string $prefix The prefix of the generated temporary filename
     *                       Note: Windows uses only the first three characters of prefix
     * @param string $suffix The suffix of the generated temporary filename
     *
     * @return string The new temporary filename (with path), or throw an exception on failure
     */
    public function tempnam(string $dir, string $prefix, string $suffix = ''): string
    {
        [$scheme, $hierarchy] = $this->getSchemeAndHierarchy($dir);

        // If no scheme or scheme is "file" or "gs" (Google Cloud) create temp file in local filesystem
        if ((null === $scheme || 'file' === $scheme || 'gs' === $scheme) && '' === $suffix) {
            // PHP's tempnam() truncates the prefix to 63 characters; trim trailing whitespace
            // from the truncated value, as a trailing space makes the file creation fail on Windows
            // If tempnam failed or no scheme return the filename otherwise prepend the scheme
            if ($tmpFile = self::box('tempnam', $hierarchy, rtrim(substr($prefix, 0, 63)))) {
                if (null !== $scheme && 'gs' !== $scheme) {
                    return $scheme.'://'.$tmpFile;
                }

                return $tmpFile;
            }

            throw new IOException('A temporary file could not be created: '.self::$lastError);
        }

        // Loop until we create a valid temp file or have reached 10 attempts
        for ($i = 0; $i < 10; ++$i) {
            // Create a unique filename
            $tmpFile = $dir.'/'.$prefix.uniqid(mt_rand(), true).$suffix;

            // Use fopen instead of file_exists as some streams do not support stat
            // Use mode 'x+' to atomically check existence and create to avoid a TOCTOU vulnerability
            if (!$handle = self::box('fopen', $tmpFile, 'x+')) {
                continue;
            }

            // Close the file if it was successfully opened
            self::box('fclose', $handle);

            return $tmpFile;
        }

        throw new IOException('A temporary file could not be created: '.self::$lastError);
    }

    /**
     * Atomically dumps content into a file.
     *
     * @param string|resource $content The data to write into the file
     *
     * @return void
     *
     * @throws IOException if the file cannot be written to
     */
    public function dumpFile(string $filename, $content)
    {
        if (\is_array($content)) {
            throw new \TypeError(\sprintf('Argument 2 passed to "%s()" must be string or resource, array given.', __METHOD__));
        }

        $dir = \dirname($filename);

        if (is_link($filename) && $linkTarget = $this->readlink($filename)) {
            $this->dumpFile(Path::makeAbsolute($linkTarget, $dir), $content);

            return;
        }

        if (!is_dir($dir)) {
            $this->mkdir($dir);
        }

        // Will create a temp file with 0600 access rights
        // when the filesystem supports chmod.
        $tmpFile = $this->tempnam($dir, basename($filename));

        try {
            if (false === self::box('file_put_contents', $tmpFile, $content)) {
                throw new IOException(\sprintf('Failed to write file "%s": ', $filename).self::$lastError, 0, null, $filename);
            }

            self::box('chmod', $tmpFile, self::box('fileperms', $filename) ?: 0o666 & ~umask());

            $this->rename($tmpFile, $filename, true);
        } finally {
            if (file_exists($tmpFile)) {
                if ('\\' === \DIRECTORY_SEPARATOR && !is_writable($tmpFile)) {
                    self::box('chmod', $tmpFile, self::box('fileperms', $tmpFile) | 0o200);
                }

                self::box('unlink', $tmpFile);
            }
        }
    }

    /**
     * Appends content to an existing file.
     *
     * @param string|resource $content The content to append
     * @param bool            $lock    Whether the file should be locked when writing to it
     *
     * @return void
     *
     * @throws IOException If the file is not writable
     */
    public function appendToFile(string $filename, $content/* , bool $lock = false */)
    {
        if (\is_array($content)) {
            throw new \TypeError(\sprintf('Argument 2 passed to "%s()" must be string or resource, array given.', __METHOD__));
        }

        $dir = \dirname($filename);

        if (!is_dir($dir)) {
            $this->mkdir($dir);
        }

        $lock = \func_num_args() > 2 && func_get_arg(2);

        if (false === self::box('file_put_contents', $filename, $content, \FILE_APPEND | ($lock ? \LOCK_EX : 0))) {
            throw new IOException(\sprintf('Failed to write file "%s": ', $filename).self::$lastError, 0, null, $filename);
        }
    }

    private function toIterable(string|iterable $files): iterable
    {
        return is_iterable($files) ? $files : [$files];
    }

    /**
     * Gets a 2-tuple of scheme (may be null) and hierarchical part of a filename (e.g. file:///tmp -> [file, tmp]).
     */
    private function getSchemeAndHierarchy(string $filename): array
    {
        $components = explode('://', $filename, 2);

        return 2 === \count($components) ? [$components[0], $components[1]] : [null, $components[0]];
    }

    private static function assertFunctionExists(string $func): void
    {
        if (!\function_exists($func)) {
            throw new IOException(\sprintf('Unable to perform filesystem operation because the "%s()" function has been disabled.', $func));
        }
    }

    private static function box(string $func, mixed ...$args): mixed
    {
        self::assertFunctionExists($func);

        self::$lastError = null;
        set_error_handler(self::handleError(...));
        try {
            return $func(...$args);
        } finally {
            restore_error_handler();
        }
    }

    /**
     * @internal
     */
    public static function handleError(int $type, string $msg): void
    {
        self::$lastError = $msg;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Filesystem;

use Symfony\Component\Filesystem\Exception\InvalidArgumentException;
use Symfony\Component\Filesystem\Exception\RuntimeException;

/**
 * Contains utility methods for handling path strings.
 *
 * The methods in this class are able to deal with both UNIX and Windows paths.
 * On Windows, backward slashes are normalized to forward slashes. On UNIX,
 * backward slashes are treated as valid filename characters and are not replaced.
 * All methods return normalized parts with no excess "." and ".." segments.
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 * @author Thomas Schulz <mail@king2500.net>
 * @author Théo Fidry <theo.fidry@gmail.com>
 */
final class Path
{
    /**
     * The number of buffer entries that triggers a cleanup operation.
     */
    private const CLEANUP_THRESHOLD = 1250;

    /**
     * The buffer size after the cleanup operation.
     */
    private const CLEANUP_SIZE = 1000;

    /**
     * Buffers input/output of {@link canonicalize()}.
     *
     * @var array<string, string>
     */
    private static array $buffer = [];

    private static int $bufferSize = 0;

    /**
     * Canonicalizes the given path.
     *
     * During normalization, all "." and ".." segments are removed as far as
     * possible. ".." segments at the beginning of relative paths are not removed.
     * On Windows, backward slashes are replaced by forward slashes ("/").
     *
     * ```php
     * echo Path::canonicalize("../css/./style.css");
     * // => ../css/style.css
     * ```
     *
     * This method is able to deal with both UNIX and Windows paths.
     */
    public static function canonicalize(string $path): string
    {
        if ('' === $path) {
            return '';
        }

        // This method is called by many other methods in this class. Buffer
        // the canonicalized paths to make up for the severe performance
        // decrease.
        if (isset(self::$buffer[$path])) {
            return self::$buffer[$path];
        }

        // Replace "~" with user's home directory.
        if ('~' === $path[0]) {
            $path = self::getHomeDirectory().substr($path, 1);
        }

        $path = self::normalize($path);

        [$root, $pathWithoutRoot] = self::split($path);

        $canonicalParts = self::findCanonicalParts($root, $pathWithoutRoot);

        // Add the root directory again
        self::$buffer[$path] = $canonicalPath = $root.implode('/', $canonicalParts);
        ++self::$bufferSize;

        // Clean up regularly to prevent memory leaks
        if (self::$bufferSize > self::CLEANUP_THRESHOLD) {
            self::$buffer = \array_slice(self::$buffer, -self::CLEANUP_SIZE, null, true);
            self::$bufferSize = self::CLEANUP_SIZE;
        }

        return $canonicalPath;
    }

    /**
     * Normalizes the given path.
     *
     * On Windows, backward slashes are replaced by forward slashes ("/").
     * On UNIX, backward slashes are preserved as they are valid filename characters.
     * Contrary to {@link canonicalize()}, this method does not remove invalid
     * or dot path segments. Consequently, it is much more efficient and should
     * be used whenever the given path is known to be a valid, absolute system
     * path.
     */
    public static function normalize(string $path): string
    {
        return '\\' === \DIRECTORY_SEPARATOR ? str_replace('\\', '/', $path) : $path;
    }

    /**
     * Returns the directory part of the path.
     *
     * This method is similar to PHP's dirname(), but handles various cases
     * where dirname() returns a weird result:
     *
     *  - dirname() does not accept backslashes on UNIX
     *  - dirname("C:/symfony") returns "C:", not "C:/"
     *  - dirname("C:/") returns ".", not "C:/"
     *  - dirname("C:") returns ".", not "C:/"
     *  - dirname("symfony") returns ".", not ""
     *  - dirname() does not canonicalize the result
     *
     * This method fixes these shortcomings and behaves like dirname()
     * otherwise.
     *
     * The result is a canonical path.
     *
     * @return string The canonical directory part. Returns the root directory
     *                if the root directory is passed. Returns an empty string
     *                if a relative path is passed that contains no slashes.
     *                Returns an empty string if an empty string is passed.
     */
    public static function getDirectory(string $path): string
    {
        if ('' === $path) {
            return '';
        }

        $path = self::canonicalize($path);

        // Maintain scheme
        if (false !== $schemeSeparatorPosition = strpos($path, '://')) {
            $scheme = substr($path, 0, $schemeSeparatorPosition + 3);
            $path = substr($path, $schemeSeparatorPosition + 3);
        } else {
            $scheme = '';
        }

        if (false === $dirSeparatorPosition = strrpos($path, '/')) {
            return '';
        }

        // Directory equals root directory "/"
        if (0 === $dirSeparatorPosition) {
            return $scheme.'/';
        }

        // Directory equals Windows root "C:/"
        if (2 === $dirSeparatorPosition && ctype_alpha($path[0]) && ':' === $path[1]) {
            return $scheme.substr($path, 0, 3);
        }

        return $scheme.substr($path, 0, $dirSeparatorPosition);
    }

    /**
     * Returns canonical path of the user's home directory.
     *
     * Supported operating systems:
     *
     *  - UNIX
     *  - Windows8 and upper
     *
     * If your operating system or environment isn't supported, an exception is thrown.
     *
     * The result is a canonical path.
     *
     * @throws RuntimeException If your operating system or environment isn't supported
     */
    public static function getHomeDirectory(): string
    {
        // For UNIX support
        if (getenv('HOME')) {
            return self::canonicalize(getenv('HOME'));
        }

        // For >= Windows8 support
        if (getenv('HOMEDRIVE') && getenv('HOMEPATH')) {
            return self::canonicalize(getenv('HOMEDRIVE').getenv('HOMEPATH'));
        }

        throw new RuntimeException("Cannot find the home directory path: Your environment or operating system isn't supported.");
    }

    /**
     * Returns the root directory of a path.
     *
     * The result is a canonical path.
     *
     * @return string The canonical root directory. Returns an empty string if
     *                the given path is relative or empty.
     */
    public static function getRoot(string $path): string
    {
        if ('' === $path) {
            return '';
        }

        // Maintain scheme
        if (false !== $schemeSeparatorPosition = strpos($path, '://')) {
            $scheme = substr($path, 0, $schemeSeparatorPosition + 3);
            $path = substr($path, $schemeSeparatorPosition + 3);
        } else {
            $scheme = '';
        }

        $firstCharacter = $path[0];

        if ('/' === $firstCharacter) {
            return $scheme.'/';
        }

        if ('\\' !== \DIRECTORY_SEPARATOR) {
            return '';
        }

        if ('\\' === $firstCharacter) {
            return $scheme.'/';
        }

        $length = \strlen($path);

        // Windows root
        if ($length > 1 && ':' === $path[1] && ctype_alpha($firstCharacter)) {
            // Special case: "C:"
            if (2 === $length) {
                return $scheme.$path.'/';
            }

            // Normal case: "C:/" or "C:\"
            if ('/' === $path[2] || '\\' === $path[2]) {
                return $scheme.$firstCharacter.$path[1].'/';
            }
        }

        return '';
    }

    /**
     * Returns the file name without the extension from a file path.
     *
     * @param string|null $extension if specified, only that extension is cut
     *                               off (may contain leading dot)
     */
    public static function getFilenameWithoutExtension(string $path, ?string $extension = null): string
    {
        if ('' === $path) {
            return '';
        }

        if (null !== $extension) {
            // remove extension and trailing dot
            return rtrim(basename($path, $extension), '.');
        }

        return pathinfo($path, \PATHINFO_FILENAME);
    }

    /**
     * Returns the extension from a file path (without leading dot).
     *
     * @param bool $forceLowerCase forces the extension to be lower-case
     */
    public static function getExtension(string $path, bool $forceLowerCase = false): string
    {
        if ('' === $path) {
            return '';
        }

        $extension = pathinfo($path, \PATHINFO_EXTENSION);

        if ($forceLowerCase) {
            $extension = self::toLower($extension);
        }

        return $extension;
    }

    /**
     * Returns whether the path has an (or the specified) extension.
     *
     * @param string               $path       the path string
     * @param string|string[]|null $extensions if null or not provided, checks if
     *                                         an extension exists, otherwise
     *                                         checks for the specified extension
     *                                         or array of extensions (with or
     *                                         without leading dot)
     * @param bool                 $ignoreCase whether to ignore case-sensitivity
     */
    public static function hasExtension(string $path, $extensions = null, bool $ignoreCase = false): bool
    {
        if ('' === $path) {
            return false;
        }

        $actualExtension = self::getExtension($path, $ignoreCase);

        // Only check if path has any extension
        if ([] === $extensions || null === $extensions) {
            return '' !== $actualExtension;
        }

        if (\is_string($extensions)) {
            $extensions = [$extensions];
        }

        foreach ($extensions as $key => $extension) {
            if ($ignoreCase) {
                $extension = self::toLower($extension);
            }

            // remove leading '.' in extensions array
            $extensions[$key] = ltrim($extension, '.');
        }

        return \in_array($actualExtension, $extensions, true);
    }

    /**
     * Changes the extension of a path string.
     *
     * @param string $path      The path string with filename.ext to change.
     * @param string $extension new extension (with or without leading dot)
     *
     * @return string the path string with new file extension
     */
    public static function changeExtension(string $path, string $extension): string
    {
        if ('' === $path) {
            return '';
        }

        $actualExtension = self::getExtension($path);
        $extension = ltrim($extension, '.');

        // No extension for paths
        if (str_ends_with($path, '/')) {
            return $path;
        }

        // No actual extension in path
        if (!$actualExtension) {
            return $path.(str_ends_with($path, '.') ? '' : '.').$extension;
        }

        return substr($path, 0, -\strlen($actualExtension)).$extension;
    }

    /**
     * Returns whether the given path is absolute.
     */
    public static function isAbsolute(string $path): bool
    {
        if ('' === $path) {
            return false;
        }

        // URLs and stream wrappers are considered absolute
        if (str_contains($path, '://') && null !== parse_url($path, \PHP_URL_SCHEME)) {
            return true;
        }

        if ('/' === $path[0]) {
            return true;
        }

        if ('\\' !== \DIRECTORY_SEPARATOR) {
            return false;
        }

        if ('\\' === $path[0]) {
            return true;
        }

        // Windows root
        if (\strlen($path) > 1 && ctype_alpha($path[0]) && ':' === $path[1]) {
            // Special case: "C:"
            if (2 === \strlen($path)) {
                return true;
            }

            // Normal case: "C:/" or "C:\"
            if ('/' === $path[2] || '\\' === $path[2]) {
                return true;
            }
        }

        return false;
    }

    public static function isRelative(string $path): bool
    {
        return !self::isAbsolute($path);
    }

    /**
     * Turns a relative path into an absolute path in canonical form.
     *
     * Usually, the relative path is appended to the given base path. Dot
     * segments ("." and "..") are removed/collapsed and all slashes turned
     * into forward slashes.
     *
     * ```php
     * echo Path::makeAbsolute("../style.css", "/symfony/puli/css");
     * // => /symfony/puli/style.css
     * ```
     *
     * If an absolute path is passed, that path is returned unless its root
     * directory is different than the one of the base path. In that case, an
     * exception is thrown.
     *
     * ```php
     * Path::makeAbsolute("/style.css", "/symfony/puli/css");
     * // => /style.css
     *
     * Path::makeAbsolute("C:/style.css", "C:/symfony/puli/css");
     * // => C:/style.css
     *
     * Path::makeAbsolute("C:/style.css", "/symfony/puli/css");
     * // InvalidArgumentException
     * ```
     *
     * If the base path is not an absolute path, an exception is thrown.
     *
     * The result is a canonical path.
     *
     * @param string $basePath an absolute base path
     *
     * @throws InvalidArgumentException if the base path is not absolute or if
     *                                  the given path is an absolute path with
     *                                  a different root than the base path
     */
    public static function makeAbsolute(string $path, string $basePath): string
    {
        if ('' === $basePath) {
            throw new InvalidArgumentException(\sprintf('The base path must be a non-empty string. Got: "%s".', $basePath));
        }

        if (!self::isAbsolute($basePath)) {
            throw new InvalidArgumentException(\sprintf('The base path "%s" is not an absolute path.', $basePath));
        }

        if (self::isAbsolute($path)) {
            return self::canonicalize($path);
        }

        if (false !== $schemeSeparatorPosition = strpos($basePath, '://')) {
            $scheme = substr($basePath, 0, $schemeSeparatorPosition + 3);
            $basePath = substr($basePath, $schemeSeparatorPosition + 3);
        } else {
            $scheme = '';
        }

        return $scheme.self::canonicalize(rtrim($basePath, '/'.\DIRECTORY_SEPARATOR).'/'.$path);
    }

    /**
     * Turns a path into a relative path.
     *
     * The relative path is created relative to the given base path:
     *
     * ```php
     * echo Path::makeRelative("/symfony/style.css", "/symfony/puli");
     * // => ../style.css
     * ```
     *
     * If a relative path is passed and the base path is absolute, the relative
     * path is returned unchanged:
     *
     * ```php
     * Path::makeRelative("style.css", "/symfony/puli/css");
     * // => style.css
     * ```
     *
     * If both paths are relative, the relative path is created with the
     * assumption that both paths are relative to the same directory:
     *
     * ```php
     * Path::makeRelative("style.css", "symfony/puli/css");
     * // => ../../../style.css
     * ```
     *
     * If both paths are absolute, their root directory must be the same,
     * otherwise an exception is thrown:
     *
     * ```php
     * Path::makeRelative("C:/symfony/style.css", "/symfony/puli");
     * // InvalidArgumentException
     * ```
     *
     * If the passed path is absolute, but the base path is not, an exception
     * is thrown as well:
     *
     * ```php
     * Path::makeRelative("/symfony/style.css", "symfony/puli");
     * // InvalidArgumentException
     * ```
     *
     * If the base path is not an absolute path, an exception is thrown.
     *
     * The result is a canonical path.
     *
     * @throws InvalidArgumentException if the base path is not absolute or if
     *                                  the given path has a different root
     *                                  than the base path
     */
    public static function makeRelative(string $path, string $basePath): string
    {
        $path = self::canonicalize($path);
        $basePath = self::canonicalize($basePath);

        [$root, $relativePath] = self::split($path);
        [$baseRoot, $relativeBasePath] = self::split($basePath);

        // If the base path is given as absolute path and the path is already
        // relative, consider it to be relative to the given absolute path
        // already
        if ('' === $root && '' !== $baseRoot) {
            // If base path is already in its root
            if ('' === $relativeBasePath) {
                $relativePath = ltrim($relativePath, './'.\DIRECTORY_SEPARATOR);
            }

            return $relativePath;
        }

        // If the passed path is absolute, but the base path is not, we
        // cannot generate a relative path
        if ('' !== $root && '' === $baseRoot) {
            throw new InvalidArgumentException(\sprintf('The absolute path "%s" cannot be made relative to the relative path "%s". You should provide an absolute base path instead.', $path, $basePath));
        }

        // Fail if the roots of the two paths are different
        if ($baseRoot && $root !== $baseRoot) {
            throw new InvalidArgumentException(\sprintf('The path "%s" cannot be made relative to "%s", because they have different roots ("%s" and "%s").', $path, $basePath, $root, $baseRoot));
        }

        if ('' === $relativeBasePath) {
            return $relativePath;
        }

        // Build a "../../" prefix with as many "../" parts as necessary
        $parts = explode('/', $relativePath);
        $baseParts = explode('/', $relativeBasePath);
        $dotDotPrefix = '';

        // Once we found a non-matching part in the prefix, we need to add
        // "../" parts for all remaining parts
        $match = true;

        foreach ($baseParts as $index => $basePart) {
            if ($match && isset($parts[$index]) && $basePart === $parts[$index]) {
                unset($parts[$index]);

                continue;
            }

            $match = false;
            $dotDotPrefix .= '../';
        }

        return rtrim($dotDotPrefix.implode('/', $parts), '/');
    }

    /**
     * Returns whether the given path is on the local filesystem.
     */
    public static function isLocal(string $path): bool
    {
        return '' !== $path && !str_contains($path, '://');
    }

    /**
     * Returns the longest common base path in canonical form of a set of paths or
     * `null` if the paths are on different Windows partitions.
     *
     * Dot segments ("." and "..") are removed/collapsed and all slashes turned
     * into forward slashes.
     *
     * ```php
     * $basePath = Path::getLongestCommonBasePath(
     *     '/symfony/css/style.css',
     *     '/symfony/css/..'
     * );
     * // => /symfony
     * ```
     *
     * The root is returned if no common base path can be found:
     *
     * ```php
     * $basePath = Path::getLongestCommonBasePath(
     *     '/symfony/css/style.css',
     *     '/puli/css/..'
     * );
     * // => /
     * ```
     *
     * If the paths are located on different Windows partitions, `null` is
     * returned.
     *
     * ```php
     * $basePath = Path::getLongestCommonBasePath(
     *     'C:/symfony/css/style.css',
     *     'D:/symfony/css/..'
     * );
     * // => null
     * ```
     */
    public static function getLongestCommonBasePath(string ...$paths): ?string
    {
        [$bpRoot, $basePath] = self::split(self::canonicalize(reset($paths)));

        for (next($paths); null !== key($paths) && '' !== $basePath; next($paths)) {
            [$root, $path] = self::split(self::canonicalize(current($paths)));

            // If we deal with different roots (e.g. C:/ vs. D:/), it's time
            // to quit
            if ($root !== $bpRoot) {
                return null;
            }

            // Make the base path shorter until it fits into path
            while (true) {
                if ('.' === $basePath) {
                    // No more base paths
                    $basePath = '';

                    // next path
                    continue 2;
                }

                // Prevent false positives for common prefixes
                // see isBasePath()
                if (str_starts_with($path.'/', $basePath.'/')) {
                    // next path
                    continue 2;
                }

                $basePath = \dirname($basePath);
            }
        }

        return $bpRoot.$basePath;
    }

    /**
     * Joins two or more path strings into a canonical path.
     */
    public static function join(string ...$paths): string
    {
        $finalPath = null;
        $wasScheme = false;

        foreach ($paths as $path) {
            if ('' === $path) {
                continue;
            }

            if (null === $finalPath) {
                // For first part we keep slashes, like '/top', 'C:\' or 'phar://'
                $finalPath = $path;
                $wasScheme = str_contains($path, '://');
                continue;
            }

            // Only add slash if previous part didn't end with '/' or '\' (Windows)
            if ('/' !== substr($finalPath, -1) && \DIRECTORY_SEPARATOR !== substr($finalPath, -1)) {
                $finalPath .= '/';
            }

            // If first part included a scheme like 'phar://' we allow \current part to start with '/', otherwise trim
            $finalPath .= $wasScheme ? $path : ltrim($path, '/');
            $wasScheme = false;
        }

        if (null === $finalPath) {
            return '';
        }

        return self::canonicalize($finalPath);
    }

    /**
     * Returns whether a path is a base path of another path.
     *
     * Dot segments ("." and "..") are removed/collapsed and all slashes turned
     * into forward slashes.
     *
     * ```php
     * Path::isBasePath('/symfony', '/symfony/css');
     * // => true
     *
     * Path::isBasePath('/symfony', '/symfony');
     * // => true
     *
     * Path::isBasePath('/symfony', '/symfony/..');
     * // => false
     *
     * Path::isBasePath('/symfony', '/puli');
     * // => false
     * ```
     */
    public static function isBasePath(string $basePath, string $ofPath): bool
    {
        $basePath = self::canonicalize($basePath);
        $ofPath = self::canonicalize($ofPath);

        // Append slashes to prevent false positives when two paths have
        // a common prefix, for example /base/foo and /base/foobar.
        // Don't append a slash for the root "/", because then that root
        // won't be discovered as common prefix ("//" is not a prefix of
        // "/foobar/").
        return str_starts_with($ofPath.'/', rtrim($basePath, '/').'/');
    }

    /**
     * @return string[]
     */
    private static function findCanonicalParts(string $root, string $pathWithoutRoot): array
    {
        $parts = explode('/', $pathWithoutRoot);

        $canonicalParts = [];

        // Collapse "." and "..", if possible
        foreach ($parts as $part) {
            if ('.' === $part || '' === $part) {
                continue;
            }

            // Collapse ".." with the previous part, if one exists
            // Don't collapse ".." if the previous part is also ".."
            if ('..' === $part && \count($canonicalParts) > 0 && '..' !== $canonicalParts[\count($canonicalParts) - 1]) {
                array_pop($canonicalParts);

                continue;
            }

            // Only add ".." prefixes for relative paths
            if ('..' !== $part || '' === $root) {
                $canonicalParts[] = $part;
            }
        }

        return $canonicalParts;
    }

    /**
     * Splits a canonical path into its root directory and the remainder.
     *
     * If the path has no root directory, an empty root directory will be
     * returned.
     *
     * If the root directory is a Windows style partition, the resulting root
     * will always contain a trailing slash.
     *
     * list ($root, $path) = Path::split("C:/symfony")
     * // => ["C:/", "symfony"]
     *
     * list ($root, $path) = Path::split("C:")
     * // => ["C:/", ""]
     *
     * @return array{string, string} an array with the root directory and the remaining relative path
     */
    private static function split(string $path): array
    {
        if ('' === $path) {
            return ['', ''];
        }

        // Remember scheme as part of the root, if any
        if (false !== $schemeSeparatorPosition = strpos($path, '://')) {
            $root = substr($path, 0, $schemeSeparatorPosition + 3);
            $path = substr($path, $schemeSeparatorPosition + 3);
        } else {
            $root = '';
        }

        $length = \strlen($path);

        // Remove and remember root directory
        if (str_starts_with($path, '/')) {
            $root .= '/';
            $path = $length > 1 ? substr($path, 1) : '';
        } elseif ('\\' === \DIRECTORY_SEPARATOR && $length > 1 && ctype_alpha($path[0]) && ':' === $path[1]) {
            if (2 === $length) {
                // Windows special case: "C:"
                $root .= $path.'/';
                $path = '';
            } elseif ('/' === $path[2]) {
                // Windows normal case: "C:/"..
                $root .= substr($path, 0, 3);
                $path = $length > 3 ? substr($path, 3) : '';
            }
        }

        return [$root, $path];
    }

    private static function toLower(string $string): string
    {
        if (false !== $encoding = mb_detect_encoding($string, null, true)) {
            return mb_strtolower($string, $encoding);
        }

        return strtolower($string);
    }

    private function __construct()
    {
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

if (!function_exists('trigger_deprecation')) {
    /**
     * Triggers a silenced deprecation notice.
     *
     * @param string $package The name of the Composer package that is triggering the deprecation
     * @param string $version The version of the package that introduced the deprecation
     * @param string $message The message of the deprecation
     * @param mixed  ...$args Values to insert in the message using printf() formatting
     *
     * @author Nicolas Grekas <p@tchwork.com>
     */
    function trigger_deprecation(string $package, string $version, string $message, mixed ...$args): void
    {
        @trigger_error(($package || $version ? "Since $package $version: " : '').($args ? vsprintf($message, $args) : $message), \E_USER_DEPRECATED);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Yaml\Exception;

/**
 * Exception class thrown when an error occurs during dumping.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class DumpException extends RuntimeException
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Yaml\Exception;

/**
 * Exception class thrown when an error occurs during parsing.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class ParseException extends RuntimeException
{
    private ?string $parsedFile;
    private int $parsedLine;
    private ?string $snippet;
    private string $rawMessage;

    /**
     * @param string      $message    The error message
     * @param int         $parsedLine The line where the error occurred
     * @param string|null $snippet    The snippet of code near the problem
     * @param string|null $parsedFile The file name where the error occurred
     */
    public function __construct(string $message, int $parsedLine = -1, ?string $snippet = null, ?string $parsedFile = null, ?\Throwable $previous = null)
    {
        $this->parsedFile = $parsedFile;
        $this->parsedLine = $parsedLine;
        $this->snippet = $snippet;
        $this->rawMessage = $message;

        $this->updateRepr();

        parent::__construct($this->message, 0, $previous);
    }

    /**
     * Gets the snippet of code near the error.
     */
    public function getSnippet(): string
    {
        return $this->snippet;
    }

    /**
     * Sets the snippet of code near the error.
     *
     * @return void
     */
    public function setSnippet(string $snippet)
    {
        $this->snippet = $snippet;

        $this->updateRepr();
    }

    /**
     * Gets the filename where the error occurred.
     *
     * This method returns null if a string is parsed.
     */
    public function getParsedFile(): string
    {
        return $this->parsedFile;
    }

    /**
     * Sets the filename where the error occurred.
     *
     * @return void
     */
    public function setParsedFile(string $parsedFile)
    {
        $this->parsedFile = $parsedFile;

        $this->updateRepr();
    }

    /**
     * Gets the line where the error occurred.
     */
    public function getParsedLine(): int
    {
        return $this->parsedLine;
    }

    /**
     * Sets the line where the error occurred.
     *
     * @return void
     */
    public function setParsedLine(int $parsedLine)
    {
        $this->parsedLine = $parsedLine;

        $this->updateRepr();
    }

    private function updateRepr(): void
    {
        $this->message = $this->rawMessage;

        $dot = false;
        if (str_ends_with($this->message, '.')) {
            $this->message = substr($this->message, 0, -1);
            $dot = true;
        }

        if (null !== $this->parsedFile) {
            $this->message .= \sprintf(' in %s', json_encode($this->parsedFile, \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE));
        }

        if ($this->parsedLine >= 0) {
            $this->message .= \sprintf(' at line %d', $this->parsedLine);
        }

        if ($this->snippet) {
            $this->message .= \sprintf(' (near "%s")', $this->snippet);
        }

        if ($dot) {
            $this->message .= '.';
        }
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Yaml\Exception;

/**
 * Exception class thrown when an error occurs during parsing.
 *
 * @author Romain Neutron <imprec@gmail.com>
 */
class RuntimeException extends \RuntimeException implements ExceptionInterface
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Yaml\Exception;

/**
 * Exception interface for all exceptions thrown by the component.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
interface ExceptionInterface extends \Throwable
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Yaml;

use Symfony\Component\Yaml\Tag\TaggedValue;

/**
 * Dumper dumps PHP variables to YAML strings.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 *
 * @final
 */
class Dumper
{
    /**
     * The amount of spaces to use for indentation of nested nodes.
     */
    private int $indentation;

    public function __construct(int $indentation = 4)
    {
        if ($indentation < 1) {
            throw new \InvalidArgumentException('The indentation must be greater than zero.');
        }

        $this->indentation = $indentation;
    }

    /**
     * Dumps a PHP value to YAML.
     *
     * @param mixed $input  The PHP value
     * @param int   $inline The level where you switch to inline YAML
     * @param int   $indent The level of indentation (used internally)
     * @param int   $flags  A bit field of Yaml::DUMP_* constants to customize the dumped YAML string
     */
    public function dump(mixed $input, int $inline = 0, int $indent = 0, int $flags = 0): string
    {
        $output = '';
        $prefix = $indent ? str_repeat(' ', $indent) : '';
        $dumpObjectAsInlineMap = true;

        if (Yaml::DUMP_OBJECT_AS_MAP & $flags && ($input instanceof \ArrayObject || $input instanceof \stdClass)) {
            $dumpObjectAsInlineMap = empty((array) $input);
        }

        if ($inline <= 0 || (!\is_array($input) && !$input instanceof TaggedValue && $dumpObjectAsInlineMap) || empty($input)) {
            $output .= $prefix.Inline::dump($input, $flags);
        } elseif ($input instanceof TaggedValue) {
            $output .= $this->dumpTaggedValue($input, $inline, $indent, $flags, $prefix);
        } else {
            $dumpAsMap = Inline::isHash($input);

            foreach ($input as $key => $value) {
                if ('' !== $output && "\n" !== $output[-1]) {
                    $output .= "\n";
                }

                if (\is_int($key) && Yaml::DUMP_NUMERIC_KEY_AS_STRING & $flags) {
                    $key = (string) $key;
                }

                if (Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK & $flags && \is_string($value) && str_contains($value, "\n") && !str_contains($value, "\r")) {
                    $blockIndentationIndicator = $this->getBlockIndentationIndicator($value);

                    if (isset($value[-2]) && "\n" === $value[-2] && "\n" === $value[-1]) {
                        $blockChompingIndicator = '+';
                    } elseif ("\n" === $value[-1]) {
                        $blockChompingIndicator = '';
                    } else {
                        $blockChompingIndicator = '-';
                    }

                    $output .= \sprintf('%s%s%s |%s%s', $prefix, $dumpAsMap ? Inline::dump($key, $flags).':' : '-', '', $blockIndentationIndicator, $blockChompingIndicator);

                    foreach (explode("\n", $value) as $row) {
                        if ('' === $row) {
                            $output .= "\n";
                        } else {
                            $output .= \sprintf("\n%s%s%s", $prefix, str_repeat(' ', $this->indentation), $row);
                        }
                    }

                    continue;
                }

                if ($value instanceof TaggedValue) {
                    $output .= \sprintf('%s%s !%s', $prefix, $dumpAsMap ? Inline::dump($key, $flags).':' : '-', $value->getTag());

                    if (Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK & $flags && \is_string($value->getValue()) && str_contains($value->getValue(), "\n") && !str_contains($value->getValue(), "\r\n")) {
                        $blockIndentationIndicator = $this->getBlockIndentationIndicator($value->getValue());
                        $output .= \sprintf(' |%s', $blockIndentationIndicator);

                        foreach (explode("\n", $value->getValue()) as $row) {
                            $output .= \sprintf("\n%s%s%s", $prefix, str_repeat(' ', $this->indentation), $row);
                        }

                        continue;
                    }

                    if ($inline - 1 <= 0 || null === $value->getValue() || \is_scalar($value->getValue())) {
                        $output .= ' '.$this->dump($value->getValue(), $inline - 1, 0, $flags)."\n";
                    } else {
                        $output .= "\n";
                        $output .= $this->dump($value->getValue(), $inline - 1, $dumpAsMap ? $indent + $this->indentation : $indent + 2, $flags);
                    }

                    continue;
                }

                $dumpObjectAsInlineMap = true;

                if (Yaml::DUMP_OBJECT_AS_MAP & $flags && ($value instanceof \ArrayObject || $value instanceof \stdClass)) {
                    $dumpObjectAsInlineMap = empty((array) $value);
                }

                $willBeInlined = $inline - 1 <= 0 || !\is_array($value) && $dumpObjectAsInlineMap || empty($value);

                $output .= \sprintf('%s%s%s%s',
                    $prefix,
                    $dumpAsMap ? Inline::dump($key, $flags).':' : '-',
                    $willBeInlined ? ' ' : "\n",
                    $this->dump($value, $inline - 1, $willBeInlined ? 0 : $indent + $this->indentation, $flags)
                ).($willBeInlined ? "\n" : '');
            }
        }

        return $output;
    }

    private function dumpTaggedValue(TaggedValue $value, int $inline, int $indent, int $flags, string $prefix): string
    {
        $output = \sprintf('%s!%s', $prefix ? $prefix.' ' : '', $value->getTag());

        if (Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK & $flags && \is_string($value->getValue()) && str_contains($value->getValue(), "\n") && !str_contains($value->getValue(), "\r\n")) {
            $blockIndentationIndicator = $this->getBlockIndentationIndicator($value->getValue());
            $output .= \sprintf(' |%s', $blockIndentationIndicator);

            foreach (explode("\n", $value->getValue()) as $row) {
                $output .= \sprintf("\n%s%s%s", $prefix, str_repeat(' ', $this->indentation), $row);
            }

            return $output;
        }

        if ($inline - 1 <= 0 || null === $value->getValue() || \is_scalar($value->getValue())) {
            return $output.' '.$this->dump($value->getValue(), $inline - 1, 0, $flags)."\n";
        }

        return $output."\n".$this->dump($value->getValue(), $inline - 1, $indent, $flags);
    }

    private function getBlockIndentationIndicator(string $value): string
    {
        $lines = explode("\n", $value);

        // If the first line (that is neither empty nor contains only spaces)
        // starts with a space character, the spec requires a block indentation indicator
        // http://www.yaml.org/spec/1.2/spec.html#id2793979
        foreach ($lines as $line) {
            if ('' !== trim($line, ' ')) {
                return (' ' === substr($line, 0, 1)) ? (string) $this->indentation : '';
            }
        }

        return '';
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Yaml;

use Symfony\Component\Yaml\Exception\ParseException;

/**
 * Yaml offers convenience methods to load and dump YAML.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 *
 * @final
 */
class Yaml
{
    public const DUMP_OBJECT = 1;
    public const PARSE_EXCEPTION_ON_INVALID_TYPE = 2;
    public const PARSE_OBJECT = 4;
    public const PARSE_OBJECT_FOR_MAP = 8;
    public const DUMP_EXCEPTION_ON_INVALID_TYPE = 16;
    public const PARSE_DATETIME = 32;
    public const DUMP_OBJECT_AS_MAP = 64;
    public const DUMP_MULTI_LINE_LITERAL_BLOCK = 128;
    public const PARSE_CONSTANT = 256;
    public const PARSE_CUSTOM_TAGS = 512;
    public const DUMP_EMPTY_ARRAY_AS_SEQUENCE = 1024;
    public const DUMP_NULL_AS_TILDE = 2048;
    public const DUMP_NUMERIC_KEY_AS_STRING = 4096;
    public const PARSE_EXCEPTION_ON_ALIAS = 8192;

    /**
     * Parses a YAML file into a PHP value.
     *
     * Usage:
     *
     *     $array = Yaml::parseFile('config.yml');
     *     print_r($array);
     *
     * @param string $filename                 The path to the YAML file to be parsed
     * @param int    $flags                    A bit field of PARSE_* constants to customize the YAML parser behavior
     * @param int    $maxNestingLevel          The maximum nesting depth for nested YAML blocks
     * @param int    $maxAliasesForCollections The maximum number of collection aliases to resolve
     *
     * @throws ParseException If the file could not be read or the YAML is not valid
     */
    public static function parseFile(string $filename, int $flags = 0, int $maxNestingLevel = Parser::DEFAULT_MAX_NESTING_LEVEL, int $maxAliasesForCollections = Parser::DEFAULT_MAX_ALIASES_FOR_COLLECTIONS): mixed
    {
        $yaml = new Parser($maxNestingLevel, $maxAliasesForCollections);

        return $yaml->parseFile($filename, $flags);
    }

    /**
     * Parses YAML into a PHP value.
     *
     *  Usage:
     *  <code>
     *   $array = Yaml::parse(file_get_contents('config.yml'));
     *   print_r($array);
     *  </code>
     *
     * @param string $input                    A string containing YAML
     * @param int    $flags                    A bit field of PARSE_* constants to customize the YAML parser behavior
     * @param int    $maxNestingLevel          The maximum nesting depth for nested YAML blocks
     * @param int    $maxAliasesForCollections The maximum number of collection aliases to resolve
     *
     * @throws ParseException If the YAML is not valid
     */
    public static function parse(string $input, int $flags = 0, int $maxNestingLevel = Parser::DEFAULT_MAX_NESTING_LEVEL, int $maxAliasesForCollections = Parser::DEFAULT_MAX_ALIASES_FOR_COLLECTIONS): mixed
    {
        $yaml = new Parser($maxNestingLevel, $maxAliasesForCollections);

        return $yaml->parse($input, $flags);
    }

    /**
     * Dumps a PHP value to a YAML string.
     *
     * The dump method, when supplied with an array, will do its best
     * to convert the array into friendly YAML.
     *
     * @param mixed $input  The PHP value
     * @param int   $inline The level where you switch to inline YAML
     * @param int   $indent The amount of spaces to use for indentation of nested nodes
     * @param int   $flags  A bit field of DUMP_* constants to customize the dumped YAML string
     */
    public static function dump(mixed $input, int $inline = 2, int $indent = 4, int $flags = 0): string
    {
        $yaml = new Dumper($indent);

        return $yaml->dump($input, $inline, 0, $flags);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Yaml\Command;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\CI\GithubActionReporter;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Parser;
use Symfony\Component\Yaml\Yaml;

/**
 * Validates YAML files syntax and outputs encountered errors.
 *
 * @author Grégoire Pineau <lyrixx@lyrixx.info>
 * @author Robin Chalas <robin.chalas@gmail.com>
 */
#[AsCommand(name: 'lint:yaml', description: 'Lint a YAML file and outputs encountered errors')]
class LintCommand extends Command
{
    private Parser $parser;
    private ?string $format = null;
    private bool $displayCorrectFiles;
    private ?\Closure $directoryIteratorProvider;
    private ?\Closure $isReadableProvider;

    public function __construct(?string $name = null, ?callable $directoryIteratorProvider = null, ?callable $isReadableProvider = null)
    {
        parent::__construct($name);

        $this->directoryIteratorProvider = null === $directoryIteratorProvider ? null : $directoryIteratorProvider(...);
        $this->isReadableProvider = null === $isReadableProvider ? null : $isReadableProvider(...);
    }

    /**
     * @return void
     */
    protected function configure()
    {
        $this
            ->addArgument('filename', InputArgument::IS_ARRAY, 'A file, a directory or "-" for reading from STDIN')
            ->addOption('format', null, InputOption::VALUE_REQUIRED, \sprintf('The output format ("%s")', implode('", "', $this->getAvailableFormatOptions())))
            ->addOption('exclude', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Path(s) to exclude')
            ->addOption('parse-tags', null, InputOption::VALUE_NEGATABLE, 'Parse custom tags', null)
            ->setHelp(<<<EOF
                The <info>%command.name%</info> command lints a YAML file and outputs to STDOUT
                the first encountered syntax error.

                You can validates YAML contents passed from STDIN:

                  <info>cat filename | php %command.full_name% -</info>

                You can also validate the syntax of a file:

                  <info>php %command.full_name% filename</info>

                Or of a whole directory:

                  <info>php %command.full_name% dirname</info>
                  <info>php %command.full_name% dirname --format=json</info>

                You can also exclude one or more specific files:

                  <info>php %command.full_name% dirname --exclude="dirname/foo.yaml" --exclude="dirname/bar.yaml"</info>

                EOF
            )
        ;
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io = new SymfonyStyle($input, $output);
        $filenames = (array) $input->getArgument('filename');
        $excludes = $input->getOption('exclude');
        $this->format = $input->getOption('format');
        $flags = $input->getOption('parse-tags');

        if (null === $this->format) {
            // Autodetect format according to CI environment
            $this->format = class_exists(GithubActionReporter::class) && GithubActionReporter::isGithubActionEnvironment() ? 'github' : 'txt';
        }

        $flags = $flags ? Yaml::PARSE_CUSTOM_TAGS : 0;

        $this->displayCorrectFiles = $output->isVerbose();

        if (['-'] === $filenames) {
            return $this->display($io, [$this->validate(file_get_contents('php://stdin'), $flags)]);
        }

        if (!$filenames) {
            throw new RuntimeException('Please provide a filename or pipe file content to STDIN.');
        }

        $filesInfo = [];
        foreach ($filenames as $filename) {
            if (!$this->isReadable($filename)) {
                throw new RuntimeException(\sprintf('File or directory "%s" is not readable.', $filename));
            }

            foreach ($this->getFiles($filename) as $file) {
                if (!\in_array($file->getPathname(), $excludes, true)) {
                    $filesInfo[] = $this->validate(file_get_contents($file), $flags, $file);
                }
            }
        }

        return $this->display($io, $filesInfo);
    }

    private function validate(string $content, int $flags, ?string $file = null): array
    {
        $prevErrorHandler = set_error_handler(function ($level, $message, $file, $line) use (&$prevErrorHandler) {
            if (\E_USER_DEPRECATED === $level) {
                throw new ParseException($message, $this->getParser()->getRealCurrentLineNb() + 1);
            }

            return $prevErrorHandler ? $prevErrorHandler($level, $message, $file, $line) : false;
        });

        try {
            $this->getParser()->parse($content, Yaml::PARSE_CONSTANT | $flags);
        } catch (ParseException $e) {
            return ['file' => $file, 'line' => $e->getParsedLine(), 'valid' => false, 'message' => $e->getMessage()];
        } finally {
            restore_error_handler();
        }

        return ['file' => $file, 'valid' => true];
    }

    private function display(SymfonyStyle $io, array $files): int
    {
        return match ($this->format) {
            'txt' => $this->displayTxt($io, $files),
            'json' => $this->displayJson($io, $files),
            'github' => $this->displayTxt($io, $files, true),
            default => throw new InvalidArgumentException(\sprintf('Supported formats are "%s".', implode('", "', $this->getAvailableFormatOptions()))),
        };
    }

    private function displayTxt(SymfonyStyle $io, array $filesInfo, bool $errorAsGithubAnnotations = false): int
    {
        $countFiles = \count($filesInfo);
        $erroredFiles = 0;
        $suggestTagOption = false;

        if ($errorAsGithubAnnotations) {
            $githubReporter = new GithubActionReporter($io);
        }

        foreach ($filesInfo as $info) {
            if ($info['valid'] && $this->displayCorrectFiles) {
                $io->comment('<info>OK</info>'.($info['file'] ? \sprintf(' in %s', $info['file']) : ''));
            } elseif (!$info['valid']) {
                ++$erroredFiles;
                $io->text('<error> ERROR </error>'.($info['file'] ? \sprintf(' in %s', $info['file']) : ''));
                $io->text(\sprintf('<error> >> %s</error>', $info['message']));

                if (str_contains($info['message'], 'PARSE_CUSTOM_TAGS')) {
                    $suggestTagOption = true;
                }

                if ($errorAsGithubAnnotations) {
                    $githubReporter->error($info['message'], $info['file'] ?? 'php://stdin', $info['line']);
                }
            }
        }

        if (0 === $erroredFiles) {
            $io->success(\sprintf('All %d YAML files contain valid syntax.', $countFiles));
        } else {
            $io->warning(\sprintf('%d YAML files have valid syntax and %d contain errors.%s', $countFiles - $erroredFiles, $erroredFiles, $suggestTagOption ? ' Use the --parse-tags option if you want parse custom tags.' : ''));
        }

        return min($erroredFiles, 1);
    }

    private function displayJson(SymfonyStyle $io, array $filesInfo): int
    {
        $errors = 0;

        array_walk($filesInfo, static function (&$v) use (&$errors) {
            $v['file'] = (string) $v['file'];
            if (!$v['valid']) {
                ++$errors;
            }

            if (isset($v['message']) && str_contains($v['message'], 'PARSE_CUSTOM_TAGS')) {
                $v['message'] .= ' Use the --parse-tags option if you want parse custom tags.';
            }
        });

        $io->writeln(json_encode($filesInfo, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES));

        return min($errors, 1);
    }

    private function getFiles(string $fileOrDirectory): iterable
    {
        if (is_file($fileOrDirectory)) {
            yield new \SplFileInfo($fileOrDirectory);

            return;
        }

        foreach ($this->getDirectoryIterator($fileOrDirectory) as $file) {
            if (!\in_array($file->getExtension(), ['yml', 'yaml'])) {
                continue;
            }

            yield $file;
        }
    }

    private function getParser(): Parser
    {
        return $this->parser ??= new Parser();
    }

    private function getDirectoryIterator(string $directory): iterable
    {
        $default = static fn ($directory) => new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS),
            \RecursiveIteratorIterator::LEAVES_ONLY
        );

        if (null !== $this->directoryIteratorProvider) {
            return ($this->directoryIteratorProvider)($directory, $default);
        }

        return $default($directory);
    }

    private function isReadable(string $fileOrDirectory): bool
    {
        $default = is_readable(...);

        if (null !== $this->isReadableProvider) {
            return ($this->isReadableProvider)($fileOrDirectory, $default);
        }

        return $default($fileOrDirectory);
    }

    public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
    {
        if ($input->mustSuggestOptionValuesFor('format')) {
            $suggestions->suggestValues($this->getAvailableFormatOptions());
        }
    }

    private function getAvailableFormatOptions(): array
    {
        return ['txt', 'json', 'github'];
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Yaml;

use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Tag\TaggedValue;

/**
 * @internal
 */
final class ParserState
{
    public int $maxNestingLevel = Parser::DEFAULT_MAX_NESTING_LEVEL;
    public int $currentNestingLevel = 0;
    public int $maxAliasesForCollections = Parser::DEFAULT_MAX_ALIASES_FOR_COLLECTIONS;
    public int $collectionAliasCount = 0;
    public bool $aliasesEnabled = true;

    public function reset(): void
    {
        $this->currentNestingLevel = 0;
        $this->collectionAliasCount = 0;
        $this->aliasesEnabled = true;
    }

    public function enterNestingLevel(int $line, ?string $snippet, ?string $filename): void
    {
        if (++$this->currentNestingLevel > $this->maxNestingLevel) {
            --$this->currentNestingLevel;

            throw new ParseException(\sprintf('Maximum nesting depth of %d exceeded.', $this->maxNestingLevel), $line, $snippet, $filename);
        }
    }

    public function leaveNestingLevel(): void
    {
        if ($this->currentNestingLevel > 0) {
            --$this->currentNestingLevel;
        }
    }

    public function countAlias(mixed $refValue, int $line, ?string $snippet, ?string $filename): void
    {
        if (!$this->aliasesEnabled) {
            throw new ParseException('Aliases are disabled.', $line, $snippet, $filename);
        }

        if ($refValue instanceof TaggedValue) {
            $refValue = $refValue->getValue();
        }

        if (!\is_array($refValue) && !$refValue instanceof \stdClass) {
            return;
        }

        if (++$this->collectionAliasCount > $this->maxAliasesForCollections) {
            throw new ParseException(\sprintf('Maximum number of collection aliases (%d) exceeded. This limit can be increased via the Parser constructor.', $this->maxAliasesForCollections), $line, $snippet, $filename);
        }
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Yaml;

use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Tag\TaggedValue;

/**
 * Parser parses YAML strings to convert them to PHP arrays.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 *
 * @final
 */
class Parser
{
    public const TAG_PATTERN = '(?P<tag>![\w!.\/:-]+)';
    public const BLOCK_SCALAR_HEADER_PATTERN = '(?P<separator>\||>)(?P<modifiers>\+|\-|\d+|\+\d+|\-\d+|\d+\+|\d+\-)?(?P<comments> +#.*)?';
    public const REFERENCE_PATTERN = '#^&(?P<ref>[^ ]++) *+(?P<value>.*)#u';
    public const DEFAULT_MAX_NESTING_LEVEL = 128;
    public const DEFAULT_MAX_ALIASES_FOR_COLLECTIONS = 128;

    private ?string $filename = null;
    private int $offset = 0;
    private int $numberOfParsedLines = 0;
    private ?int $totalNumberOfLines = null;
    private array $lines = [];
    private int $currentLineNb = -1;
    private string $currentLine = '';
    private array $refs = [];
    private array $skippedLineNumbers = [];
    private array $locallySkippedLineNumbers = [];
    private array $refsBeingParsed = [];
    private ?ParserState $state = null;

    public function __construct(int $maxNestingLevel = self::DEFAULT_MAX_NESTING_LEVEL, int $maxAliasesForCollections = self::DEFAULT_MAX_ALIASES_FOR_COLLECTIONS)
    {
        if ($maxNestingLevel < 1) {
            throw new \InvalidArgumentException('The maximum nesting depth must be greater than 0.');
        }

        if ($maxAliasesForCollections < 0) {
            throw new \InvalidArgumentException('The maximum number of collection aliases must be greater than or equal to 0.');
        }

        $this->getState()->maxNestingLevel = $maxNestingLevel;
        $this->getState()->maxAliasesForCollections = $maxAliasesForCollections;
    }

    /**
     * Parses a YAML file into a PHP value.
     *
     * @param string $filename The path to the YAML file to be parsed
     * @param int    $flags    A bit field of Yaml::PARSE_* constants to customize the YAML parser behavior
     *
     * @throws ParseException If the file could not be read or the YAML is not valid
     */
    public function parseFile(string $filename, int $flags = 0): mixed
    {
        if (!is_file($filename)) {
            throw new ParseException(\sprintf('File "%s" does not exist.', $filename));
        }

        if (!is_readable($filename)) {
            throw new ParseException(\sprintf('File "%s" cannot be read.', $filename));
        }

        $this->filename = $filename;

        try {
            return $this->parse(file_get_contents($filename), $flags);
        } finally {
            $this->filename = null;
        }
    }

    /**
     * Parses a YAML string to a PHP value.
     *
     * @param string $value A YAML string
     * @param int    $flags A bit field of Yaml::PARSE_* constants to customize the YAML parser behavior
     *
     * @throws ParseException If the YAML is not valid
     */
    public function parse(string $value, int $flags = 0): mixed
    {
        if (false === preg_match('//u', $value)) {
            throw new ParseException('The YAML value does not appear to be valid UTF-8.', -1, null, $this->filename);
        }

        $this->refs = [];
        $state = $this->getState();
        $state->reset();
        $state->aliasesEnabled = 0 === (Yaml::PARSE_EXCEPTION_ON_ALIAS & $flags);

        try {
            $data = $this->doParse($value, $flags);
        } finally {
            $this->refsBeingParsed = [];
            $this->offset = 0;
            $this->lines = [];
            $this->currentLine = '';
            $this->numberOfParsedLines = 0;
            $this->refs = [];
            $this->skippedLineNumbers = [];
            $this->locallySkippedLineNumbers = [];
            $this->totalNumberOfLines = null;
            $state->reset();
        }

        return $data;
    }

    private function getState(): ParserState
    {
        return $this->state ??= new ParserState();
    }

    private function doParse(string $value, int $flags): mixed
    {
        $this->currentLineNb = -1;
        $this->currentLine = '';
        $value = $this->cleanup($value);
        $this->lines = explode("\n", $value);
        $this->numberOfParsedLines = \count($this->lines);
        $this->locallySkippedLineNumbers = [];
        $this->totalNumberOfLines ??= $this->numberOfParsedLines;

        if (!$this->moveToNextLine()) {
            return null;
        }

        $data = [];
        $context = null;
        $allowOverwrite = false;

        while ($this->isCurrentLineEmpty()) {
            if (!$this->moveToNextLine()) {
                return null;
            }
        }

        // Resolves the tag and returns if end of the document
        if (null !== ($tag = $this->getLineTag($this->currentLine, $flags, false)) && !$this->moveToNextLine()) {
            return new TaggedValue($tag, '');
        }

        do {
            if ($this->isCurrentLineEmpty()) {
                continue;
            }

            // tab?
            if ("\t" === $this->currentLine[0]) {
                throw new ParseException('A YAML file cannot contain tabs as indentation.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
            }

            Inline::initialize($flags, $this->getRealCurrentLineNb(), $this->filename);

            $isRef = $mergeNode = false;
            if ('-' === $this->currentLine[0] && self::preg_match('#^\-((?P<leadspaces>\s+)(?P<value>.+))?$#u', rtrim($this->currentLine), $values)) {
                if ($context && 'mapping' == $context) {
                    throw new ParseException('You cannot define a sequence item when in a mapping.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
                }
                $context = 'sequence';

                if (isset($values['value']) && '&' === $values['value'][0] && self::preg_match(self::REFERENCE_PATTERN, $values['value'], $matches)) {
                    $isRef = $matches['ref'];
                    $this->refsBeingParsed[] = $isRef;
                    $values['value'] = $matches['value'];
                }

                if (isset($values['value'][1]) && '?' === $values['value'][0] && ' ' === $values['value'][1]) {
                    throw new ParseException('Complex mappings are not supported.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
                }

                // array
                if (isset($values['value']) && str_starts_with(ltrim($values['value'], ' '), '-')) {
                    // Inline first child
                    $currentLineNumber = $this->getRealCurrentLineNb();

                    $sequenceIndentation = \strlen($values['leadspaces']) + 1;
                    $sequenceYaml = substr($this->currentLine, $sequenceIndentation);
                    $sequenceYaml .= "\n".$this->getNextEmbedBlock($sequenceIndentation, true);

                    $data[] = $this->parseBlock($currentLineNumber, rtrim($sequenceYaml), $flags);
                } elseif (!isset($values['value']) || '' == trim($values['value'], ' ') || str_starts_with(ltrim($values['value'], ' '), '#')) {
                    $data[] = $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(null, true) ?? '', $flags);
                } elseif (null !== $subTag = $this->getLineTag(ltrim($values['value'], ' '), $flags)) {
                    $data[] = new TaggedValue(
                        $subTag,
                        $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(null, true), $flags)
                    );
                } else {
                    if (
                        isset($values['leadspaces'])
                        && (
                            '!' === $values['value'][0]
                            || self::preg_match('#^(?P<key>'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\{\[].*?) *\:(\s+(?P<value>.+?))?\s*$#u', $this->trimTag($values['value']), $matches)
                        )
                    ) {
                        $block = $values['value'];
                        if ($this->isNextLineIndented() || isset($matches['value']) && '>-' === $matches['value']) {
                            $block .= "\n".$this->getNextEmbedBlock($this->getCurrentLineIndentation() + \strlen($values['leadspaces']) + 1);
                        }

                        $data[] = $this->parseBlock($this->getRealCurrentLineNb(), $block, $flags);
                    } else {
                        $data[] = $this->parseValue($values['value'], $flags, $context);
                    }
                }
                if ($isRef) {
                    $this->refs[$isRef] = end($data);
                    array_pop($this->refsBeingParsed);
                }
            } elseif (
                // @todo in 7.0 remove legacy "(?:!?!php/const:)?"
                self::preg_match('#^(?P<key>(?:![^\s]++\s++)?(?:'.Inline::REGEX_QUOTED_STRING.'|(?:!?!php/const:)?[^ \'"\[\{!].*?)) *\:(( |\t)++(?P<value>.+))?$#u', rtrim($this->currentLine), $values)
                && (!str_contains($values['key'], ' #') || \in_array($values['key'][0], ['"', "'"]))
            ) {
                if (str_starts_with($values['key'], '!php/const:')) {
                    trigger_deprecation('symfony/yaml', '6.2', 'YAML syntax for key "%s" is deprecated and replaced by "!php/const %s".', $values['key'], substr($values['key'], 11));
                }

                if ($context && 'sequence' == $context) {
                    throw new ParseException('You cannot define a mapping item when in a sequence.', $this->currentLineNb + 1, $this->currentLine, $this->filename);
                }
                $context = 'mapping';

                try {
                    $key = Inline::parseScalar($values['key']);
                } catch (ParseException $e) {
                    $e->setParsedLine($this->getRealCurrentLineNb() + 1);
                    $e->setSnippet($this->currentLine);

                    throw $e;
                }

                if (!\is_string($key) && !\is_int($key)) {
                    throw new ParseException((is_numeric($key) ? 'Numeric' : 'Non-string').' keys are not supported. Quote your evaluable mapping keys instead.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
                }

                // Convert float keys to strings, to avoid being converted to integers by PHP
                if (\is_float($key)) {
                    $key = (string) $key;
                }

                if ('<<' === $key && (!isset($values['value']) || '&' !== $values['value'][0] || !self::preg_match('#^&(?P<ref>[^ ]+)#u', $values['value'], $refMatches))) {
                    $mergeNode = true;
                    $allowOverwrite = true;
                    if (isset($values['value'][0]) && '*' === $values['value'][0]) {
                        $refName = substr(rtrim($values['value']), 1);
                        if (!\array_key_exists($refName, $this->refs)) {
                            if (false !== $pos = array_search($refName, $this->refsBeingParsed, true)) {
                                throw new ParseException(\sprintf('Circular reference [%s] detected for reference "%s".', implode(', ', array_merge(\array_slice($this->refsBeingParsed, $pos), [$refName])), $refName), $this->currentLineNb + 1, $this->currentLine, $this->filename);
                            }

                            throw new ParseException(\sprintf('Reference "%s" does not exist.', $refName), $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
                        }

                        $refValue = $this->refs[$refName];

                        $this->getState()->countAlias($refValue, $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);

                        if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && $refValue instanceof \stdClass) {
                            $refValue = (array) $refValue;
                        }

                        if (!\is_array($refValue)) {
                            throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
                        }

                        $data += $refValue; // array union
                    } else {
                        if (isset($values['value']) && '' !== $values['value']) {
                            $value = $values['value'];
                        } else {
                            $value = $this->getNextEmbedBlock();
                        }
                        $parsed = $this->parseBlock($this->getRealCurrentLineNb() + 1, $value, $flags);

                        if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && $parsed instanceof \stdClass) {
                            $parsed = (array) $parsed;
                        }

                        if (!\is_array($parsed)) {
                            throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
                        }

                        if (isset($parsed[0])) {
                            // If the value associated with the merge key is a sequence, then this sequence is expected to contain mapping nodes
                            // and each of these nodes is merged in turn according to its order in the sequence. Keys in mapping nodes earlier
                            // in the sequence override keys specified in later mapping nodes.
                            foreach ($parsed as $parsedItem) {
                                if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && $parsedItem instanceof \stdClass) {
                                    $parsedItem = (array) $parsedItem;
                                }

                                if (!\is_array($parsedItem)) {
                                    throw new ParseException('Merge items must be arrays.', $this->getRealCurrentLineNb() + 1, $parsedItem, $this->filename);
                                }

                                $data += $parsedItem; // array union
                            }
                        } else {
                            // If the value associated with the key is a single mapping node, each of its key/value pairs is inserted into the
                            // current mapping, unless the key already exists in it.
                            $data += $parsed; // array union
                        }
                    }
                } elseif ('<<' !== $key && isset($values['value']) && '&' === $values['value'][0] && self::preg_match(self::REFERENCE_PATTERN, $values['value'], $matches)) {
                    $isRef = $matches['ref'];
                    $this->refsBeingParsed[] = $isRef;
                    $values['value'] = $matches['value'];
                }

                $subTag = null;
                if ($mergeNode) {
                    // Merge keys
                } elseif (!isset($values['value']) || '' === $values['value'] || str_starts_with($values['value'], '#') || (null !== $subTag = $this->getLineTag($values['value'], $flags)) || '<<' === $key) {
                    // hash
                    // if next line is less indented or equal, then it means that the current value is null
                    if (!$this->isNextLineIndented() && !$this->isNextLineUnIndentedCollection()) {
                        // Spec: Keys MUST be unique; first one wins.
                        // But overwriting is allowed when a merge node is used in current block.
                        if ($allowOverwrite || !isset($data[$key])) {
                            if (null !== $subTag) {
                                $data[$key] = new TaggedValue($subTag, '');
                            } else {
                                $data[$key] = null;
                            }
                        } else {
                            throw new ParseException(\sprintf('Duplicate key "%s" detected.', $key), $this->getRealCurrentLineNb() + 1, $this->currentLine);
                        }
                    } else {
                        // remember the parsed line number here in case we need it to provide some contexts in error messages below
                        $realCurrentLineNbKey = $this->getRealCurrentLineNb();
                        $value = $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(), $flags);
                        if ('<<' === $key) {
                            $this->refs[$refMatches['ref']] = $value;

                            if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && $value instanceof \stdClass) {
                                $value = (array) $value;
                            }

                            $data += $value;
                        } elseif ($allowOverwrite || !isset($data[$key])) {
                            // Spec: Keys MUST be unique; first one wins.
                            // But overwriting is allowed when a merge node is used in current block.
                            if (null !== $subTag) {
                                $data[$key] = new TaggedValue($subTag, $value);
                            } else {
                                $data[$key] = $value;
                            }
                        } else {
                            throw new ParseException(\sprintf('Duplicate key "%s" detected.', $key), $realCurrentLineNbKey + 1, $this->currentLine);
                        }
                    }
                } else {
                    $value = $this->parseValue(rtrim($values['value']), $flags, $context);
                    // Spec: Keys MUST be unique; first one wins.
                    // But overwriting is allowed when a merge node is used in current block.
                    if ($allowOverwrite || !isset($data[$key])) {
                        $data[$key] = $value;
                    } else {
                        throw new ParseException(\sprintf('Duplicate key "%s" detected.', $key), $this->getRealCurrentLineNb() + 1, $this->currentLine);
                    }
                }
                if ($isRef) {
                    $this->refs[$isRef] = $data[$key];
                    array_pop($this->refsBeingParsed);
                }
            } elseif ('"' === $this->currentLine[0] || "'" === $this->currentLine[0]) {
                if (null !== $context) {
                    throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
                }

                try {
                    return Inline::parse($this->lexInlineQuotedString(), $flags, $this->refs, $this->state);
                } catch (ParseException $e) {
                    $e->setParsedLine($this->getRealCurrentLineNb() + 1);
                    $e->setSnippet($this->currentLine);

                    throw $e;
                }
            } elseif ('{' === $this->currentLine[0]) {
                if (null !== $context) {
                    throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
                }

                try {
                    $parsedMapping = Inline::parse($this->lexInlineMapping(), $flags, $this->refs, $this->state);

                    while ($this->moveToNextLine()) {
                        if (!$this->isCurrentLineEmpty()) {
                            throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
                        }
                    }

                    return $parsedMapping;
                } catch (ParseException $e) {
                    $e->setParsedLine($this->getRealCurrentLineNb() + 1);
                    $e->setSnippet($this->currentLine);

                    throw $e;
                }
            } elseif ('[' === $this->currentLine[0]) {
                if (null !== $context) {
                    throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
                }

                try {
                    $parsedSequence = Inline::parse($this->lexInlineSequence(), $flags, $this->refs, $this->state);

                    while ($this->moveToNextLine()) {
                        if (!$this->isCurrentLineEmpty()) {
                            throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
                        }
                    }

                    return $parsedSequence;
                } catch (ParseException $e) {
                    $e->setParsedLine($this->getRealCurrentLineNb() + 1);
                    $e->setSnippet($this->currentLine);

                    throw $e;
                }
            } else {
                // multiple documents are not supported
                if ('---' === $this->currentLine) {
                    throw new ParseException('Multiple documents are not supported.', $this->currentLineNb + 1, $this->currentLine, $this->filename);
                }

                if ($deprecatedUsage = (isset($this->currentLine[1]) && '?' === $this->currentLine[0] && ' ' === $this->currentLine[1])) {
                    throw new ParseException('Complex mappings are not supported.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
                }

                // 1-liner optionally followed by newline(s)
                if (\is_string($value) && $this->lines[0] === trim($value)) {
                    try {
                        $value = Inline::parse($this->lines[0], $flags, $this->refs, $this->state);
                    } catch (ParseException $e) {
                        $e->setParsedLine($this->getRealCurrentLineNb() + 1);
                        $e->setSnippet($this->currentLine);

                        throw $e;
                    }

                    return $value;
                }

                // try to parse the value as a multi-line string as a last resort
                if (0 === $this->currentLineNb) {
                    $previousLineWasNewline = false;
                    $previousLineWasTerminatedWithBackslash = false;
                    $value = '';

                    foreach ($this->lines as $line) {
                        $trimmedLine = trim($line);
                        if ('#' === ($trimmedLine[0] ?? '')) {
                            continue;
                        }
                        // If the indentation is not consistent at offset 0, it is to be considered as a ParseError
                        if (0 === $this->offset && !$deprecatedUsage && isset($line[0]) && ' ' === $line[0]) {
                            throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
                        }

                        if (str_contains($line, ': ')) {
                            throw new ParseException('Mapping values are not allowed in multi-line blocks.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
                        }

                        if ('' === $trimmedLine) {
                            $value .= "\n";
                        } elseif (!$previousLineWasNewline && !$previousLineWasTerminatedWithBackslash) {
                            $value .= ' ';
                        }

                        if ('' !== $trimmedLine && str_ends_with($line, '\\')) {
                            $value .= ltrim(substr($line, 0, -1));
                        } elseif ('' !== $trimmedLine) {
                            $value .= $trimmedLine;
                        }

                        if ('' === $trimmedLine) {
                            $previousLineWasNewline = true;
                            $previousLineWasTerminatedWithBackslash = false;
                        } elseif (str_ends_with($line, '\\')) {
                            $previousLineWasNewline = false;
                            $previousLineWasTerminatedWithBackslash = true;
                        } else {
                            $previousLineWasNewline = false;
                            $previousLineWasTerminatedWithBackslash = false;
                        }
                    }

                    try {
                        return Inline::parse(trim($value), 0, $this->refs, $this->state);
                    } catch (ParseException) {
                        // fall-through to the ParseException thrown below
                    }
                }

                throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
            }
        } while ($this->moveToNextLine());

        if (null !== $tag) {
            $data = new TaggedValue($tag, $data);
        }

        if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && 'mapping' === $context && !\is_object($data)) {
            $object = new \stdClass();

            foreach ($data as $key => $value) {
                $object->$key = $value;
            }

            $data = $object;
        }

        return empty($data) ? null : $data;
    }

    private function parseBlock(int $offset, string $yaml, int $flags): mixed
    {
        $skippedLineNumbers = $this->skippedLineNumbers;

        foreach ($this->locallySkippedLineNumbers as $lineNumber) {
            if ($lineNumber < $offset) {
                continue;
            }

            $skippedLineNumbers[] = $lineNumber;
        }

        $parser = new self();
        $parser->offset = $offset;
        $parser->totalNumberOfLines = $this->totalNumberOfLines;
        $parser->skippedLineNumbers = $skippedLineNumbers;
        $parser->refs = &$this->refs;
        $parser->refsBeingParsed = $this->refsBeingParsed;
        $parser->state = $this->state;

        $this->getState()->enterNestingLevel($offset + 1, $this->currentLine, $this->filename);

        try {
            return $parser->doParse($yaml, $flags);
        } finally {
            $this->getState()->leaveNestingLevel();
        }
    }

    /**
     * Returns the current line number (takes the offset into account).
     *
     * @internal
     */
    public function getRealCurrentLineNb(): int
    {
        $realCurrentLineNumber = $this->currentLineNb + $this->offset;

        foreach ($this->skippedLineNumbers as $skippedLineNumber) {
            if ($skippedLineNumber > $realCurrentLineNumber) {
                break;
            }

            ++$realCurrentLineNumber;
        }

        return $realCurrentLineNumber;
    }

    private function getCurrentLineIndentation(): int
    {
        if (' ' !== ($this->currentLine[0] ?? '')) {
            return 0;
        }

        return \strlen($this->currentLine) - \strlen(ltrim($this->currentLine, ' '));
    }

    /**
     * Returns the next embed block of YAML.
     *
     * @param int|null $indentation The indent level at which the block is to be read, or null for default
     * @param bool     $inSequence  True if the enclosing data structure is a sequence
     *
     * @throws ParseException When indentation problem are detected
     */
    private function getNextEmbedBlock(?int $indentation = null, bool $inSequence = false): string
    {
        $oldLineIndentation = $this->getCurrentLineIndentation();

        if (!$this->moveToNextLine()) {
            return '';
        }

        if (null === $indentation) {
            $newIndent = null;
            $movements = 0;

            do {
                $EOF = false;

                // empty and comment-like lines do not influence the indentation depth
                if ($this->isCurrentLineEmpty() || $this->isCurrentLineComment()) {
                    $EOF = !$this->moveToNextLine();

                    if (!$EOF) {
                        ++$movements;
                    }
                } else {
                    $newIndent = $this->getCurrentLineIndentation();
                }
            } while (!$EOF && null === $newIndent);

            for ($i = 0; $i < $movements; ++$i) {
                $this->moveToPreviousLine();
            }

            $unindentedEmbedBlock = $this->isStringUnIndentedCollectionItem();

            if (!$this->isCurrentLineEmpty() && 0 === $newIndent && !$unindentedEmbedBlock) {
                throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
            }
        } else {
            $newIndent = $indentation;
        }

        $data = [];

        if ($this->getCurrentLineIndentation() >= $newIndent) {
            $data[] = substr($this->currentLine, $newIndent ?? 0);
        } elseif ($this->isCurrentLineEmpty() || $this->isCurrentLineComment()) {
            $data[] = $this->currentLine;
        } else {
            $this->moveToPreviousLine();

            return '';
        }

        if ($inSequence && $oldLineIndentation === $newIndent && isset($data[0][0]) && '-' === $data[0][0]) {
            // the previous line contained a dash but no item content, this line is a sequence item with the same indentation
            // and therefore no nested list or mapping
            $this->moveToPreviousLine();

            return '';
        }

        $isItUnindentedCollection = $this->isStringUnIndentedCollectionItem();
        $isItComment = $this->isCurrentLineComment();

        while ($this->moveToNextLine()) {
            if ($isItComment && !$isItUnindentedCollection) {
                $isItUnindentedCollection = $this->isStringUnIndentedCollectionItem();
                $isItComment = $this->isCurrentLineComment();
            }

            $indent = $this->getCurrentLineIndentation();

            if ($isItUnindentedCollection && !$this->isCurrentLineEmpty() && !$this->isStringUnIndentedCollectionItem() && $newIndent === $indent) {
                $this->moveToPreviousLine();
                break;
            }

            if ($this->isCurrentLineBlank()) {
                $data[] = substr($this->currentLine, $newIndent ?? 0);
                continue;
            }

            if ($indent >= $newIndent) {
                $data[] = substr($this->currentLine, $newIndent ?? 0);
            } elseif ($this->isCurrentLineComment()) {
                $data[] = $this->currentLine;
            } elseif (0 == $indent) {
                $this->moveToPreviousLine();

                break;
            } else {
                throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
            }
        }

        return implode("\n", $data);
    }

    private function hasMoreLines(): bool
    {
        return (\count($this->lines) - 1) > $this->currentLineNb;
    }

    /**
     * Moves the parser to the next line.
     */
    private function moveToNextLine(): bool
    {
        if ($this->currentLineNb >= $this->numberOfParsedLines - 1) {
            return false;
        }

        $this->currentLine = $this->lines[++$this->currentLineNb];

        return true;
    }

    /**
     * Moves the parser to the previous line.
     */
    private function moveToPreviousLine(): bool
    {
        if ($this->currentLineNb < 1) {
            return false;
        }

        $this->currentLine = $this->lines[--$this->currentLineNb];

        return true;
    }

    /**
     * Parses a YAML value.
     *
     * @param string $value   A YAML value
     * @param int    $flags   A bit field of Yaml::PARSE_* constants to customize the YAML parser behavior
     * @param string $context The parser context (either sequence or mapping)
     *
     * @throws ParseException When reference does not exist
     */
    private function parseValue(string $value, int $flags, string $context): mixed
    {
        if (str_starts_with($value, '*')) {
            if (false !== $pos = strpos($value, '#')) {
                $value = substr($value, 1, $pos - 2);
            } else {
                $value = substr($value, 1);
            }

            if (!\array_key_exists($value, $this->refs)) {
                if (false !== $pos = array_search($value, $this->refsBeingParsed, true)) {
                    throw new ParseException(\sprintf('Circular reference [%s] detected for reference "%s".', implode(', ', array_merge(\array_slice($this->refsBeingParsed, $pos), [$value])), $value), $this->currentLineNb + 1, $this->currentLine, $this->filename);
                }

                throw new ParseException(\sprintf('Reference "%s" does not exist.', $value), $this->currentLineNb + 1, $this->currentLine, $this->filename);
            }

            $this->getState()->countAlias($this->refs[$value], $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);

            return $this->refs[$value];
        }

        if (\in_array($value[0], ['!', '|', '>'], true) && self::preg_match('/^(?:'.self::TAG_PATTERN.' +)?'.self::BLOCK_SCALAR_HEADER_PATTERN.'$/', $value, $matches)) {
            $modifiers = $matches['modifiers'] ?? '';

            $data = $this->parseBlockScalar($matches['separator'], preg_replace('#\d+#', '', $modifiers), abs((int) $modifiers));

            if ('' !== $matches['tag'] && '!' !== $matches['tag']) {
                if ('!!binary' === $matches['tag']) {
                    return Inline::evaluateBinaryScalar($data);
                }

                return new TaggedValue(substr($matches['tag'], 1), $data);
            }

            return $data;
        }

        try {
            if ('' !== $value && '{' === $value[0]) {
                $cursor = \strlen(rtrim($this->currentLine)) - \strlen(rtrim($value));

                return Inline::parse($this->lexInlineMapping($cursor), $flags, $this->refs, $this->state);
            } elseif ('' !== $value && '[' === $value[0]) {
                $cursor = \strlen(rtrim($this->currentLine)) - \strlen(rtrim($value));

                return Inline::parse($this->lexInlineSequence($cursor), $flags, $this->refs, $this->state);
            }

            switch ($value[0] ?? '') {
                case '"':
                case "'":
                    $cursor = \strlen(rtrim($this->currentLine)) - \strlen(rtrim($value));
                    $parsedValue = Inline::parse($this->lexInlineQuotedString($cursor), $flags, $this->refs, $this->state);

                    if (isset($this->currentLine[$cursor]) && preg_replace('/\s*(#.*)?$/A', '', substr($this->currentLine, $cursor))) {
                        throw new ParseException(\sprintf('Unexpected characters near "%s".', substr($this->currentLine, $cursor)));
                    }

                    return $parsedValue;
                default:
                    $lines = [];

                    while ($this->moveToNextLine()) {
                        // unquoted strings end before the first unindented line
                        if (0 === $this->getCurrentLineIndentation()) {
                            $this->moveToPreviousLine();

                            break;
                        }

                        if ($this->isCurrentLineComment()) {
                            break;
                        }

                        if ('mapping' === $context && str_contains($this->currentLine, ': ') && !$this->isCurrentLineComment()) {
                            throw new ParseException('A colon cannot be used in an unquoted mapping value.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
                        }

                        $lines[] = trim($this->currentLine);
                    }

                    for ($i = 0, $linesCount = \count($lines), $previousLineBlank = false; $i < $linesCount; ++$i) {
                        if ('' === $lines[$i]) {
                            $value .= "\n";
                            $previousLineBlank = true;
                        } elseif ($previousLineBlank) {
                            $value .= $lines[$i];
                            $previousLineBlank = false;
                        } else {
                            $value .= ' '.$lines[$i];
                            $previousLineBlank = false;
                        }
                    }

                    Inline::$parsedLineNumber = $this->getRealCurrentLineNb();

                    $parsedValue = Inline::parse($value, $flags, $this->refs, $this->state);

                    if ('mapping' === $context && \is_string($parsedValue) && '"' !== $value[0] && "'" !== $value[0] && '[' !== $value[0] && '{' !== $value[0] && '!' !== $value[0] && str_contains($parsedValue, ': ')) {
                        throw new ParseException('A colon cannot be used in an unquoted mapping value.', $this->getRealCurrentLineNb() + 1, $value, $this->filename);
                    }

                    return $parsedValue;
            }
        } catch (ParseException $e) {
            $e->setParsedLine($this->getRealCurrentLineNb() + 1);
            $e->setSnippet($this->currentLine);

            throw $e;
        }
    }

    /**
     * Parses a block scalar.
     *
     * @param string $style       The style indicator that was used to begin this block scalar (| or >)
     * @param string $chomping    The chomping indicator that was used to begin this block scalar (+ or -)
     * @param int    $indentation The indentation indicator that was used to begin this block scalar
     */
    private function parseBlockScalar(string $style, string $chomping = '', int $indentation = 0): string
    {
        $notEOF = $this->moveToNextLine();
        if (!$notEOF) {
            return '';
        }

        $isCurrentLineBlank = $this->isCurrentLineBlank();
        $blockLines = [];

        // leading blank lines are consumed before determining indentation
        while ($notEOF && $isCurrentLineBlank) {
            // newline only if not EOF
            if ($notEOF = $this->moveToNextLine()) {
                $blockLines[] = '';
                $isCurrentLineBlank = $this->isCurrentLineBlank();
            }
        }

        // determine indentation if not specified
        if (0 === $indentation) {
            $currentLineLength = \strlen($this->currentLine);

            for ($i = 0; $i < $currentLineLength && ' ' === $this->currentLine[$i]; ++$i) {
                ++$indentation;
            }
        }

        if ($indentation > 0) {
            $pattern = \sprintf('/^ {%d}(.*)$/', $indentation);

            while (
                $notEOF && (
                    $isCurrentLineBlank
                    || self::preg_match($pattern, $this->currentLine, $matches)
                )
            ) {
                if ($isCurrentLineBlank && \strlen($this->currentLine) > $indentation) {
                    $blockLines[] = substr($this->currentLine, $indentation);
                } elseif ($isCurrentLineBlank) {
                    $blockLines[] = '';
                } else {
                    $blockLines[] = $matches[1];
                }

                // newline only if not EOF
                if ($notEOF = $this->moveToNextLine()) {
                    $isCurrentLineBlank = $this->isCurrentLineBlank();
                }
            }
        } elseif ($notEOF) {
            $blockLines[] = '';
        }

        if ($notEOF) {
            $blockLines[] = '';
            $this->moveToPreviousLine();
        } elseif (!$notEOF && !$this->isCurrentLineLastLineInDocument()) {
            $blockLines[] = '';
        }

        // folded style
        if ('>' === $style) {
            $text = '';
            $previousLineIndented = false;
            $previousLineBlank = false;

            for ($i = 0, $blockLinesCount = \count($blockLines); $i < $blockLinesCount; ++$i) {
                if ('' === $blockLines[$i]) {
                    $text .= "\n";
                    $previousLineIndented = false;
                    $previousLineBlank = true;
                } elseif (' ' === $blockLines[$i][0]) {
                    $text .= "\n".$blockLines[$i];
                    $previousLineIndented = true;
                    $previousLineBlank = false;
                } elseif ($previousLineIndented) {
                    $text .= "\n".$blockLines[$i];
                    $previousLineIndented = false;
                    $previousLineBlank = false;
                } elseif ($previousLineBlank || 0 === $i) {
                    $text .= $blockLines[$i];
                    $previousLineIndented = false;
                    $previousLineBlank = false;
                } else {
                    $text .= ' '.$blockLines[$i];
                    $previousLineIndented = false;
                    $previousLineBlank = false;
                }
            }
        } else {
            $text = implode("\n", $blockLines);
        }

        // deal with trailing newlines
        if ('' === $chomping) {
            $text = preg_replace('/\n+$/', "\n", $text);
        } elseif ('-' === $chomping) {
            $text = preg_replace('/\n+$/', '', $text);
        }

        return $text;
    }

    /**
     * Returns true if the next line is indented.
     */
    private function isNextLineIndented(): bool
    {
        $currentIndentation = $this->getCurrentLineIndentation();
        $movements = 0;

        do {
            $EOF = !$this->moveToNextLine();

            if (!$EOF) {
                ++$movements;
            }
        } while (!$EOF && ($this->isCurrentLineEmpty() || $this->isCurrentLineComment()));

        if ($EOF) {
            for ($i = 0; $i < $movements; ++$i) {
                $this->moveToPreviousLine();
            }

            return false;
        }

        $ret = $this->getCurrentLineIndentation() > $currentIndentation;

        for ($i = 0; $i < $movements; ++$i) {
            $this->moveToPreviousLine();
        }

        return $ret;
    }

    private function isCurrentLineEmpty(): bool
    {
        return $this->isCurrentLineBlank() || $this->isCurrentLineComment();
    }

    private function isCurrentLineBlank(): bool
    {
        return '' === $this->currentLine || '' === trim($this->currentLine, ' ');
    }

    private function isCurrentLineComment(): bool
    {
        // checking explicitly the first char of the trim is faster than loops or strpos
        $ltrimmedLine = '' !== $this->currentLine && ' ' === $this->currentLine[0] ? ltrim($this->currentLine, ' ') : $this->currentLine;

        return '' !== $ltrimmedLine && '#' === $ltrimmedLine[0];
    }

    private function isCurrentLineLastLineInDocument(): bool
    {
        return ($this->offset + $this->currentLineNb) >= ($this->totalNumberOfLines - 1);
    }

    private function cleanup(string $value): string
    {
        $value = str_replace(["\r\n", "\r"], "\n", $value);

        // strip YAML header
        $count = 0;
        $value = preg_replace('#^%YAML[: ][\d.]++[^\n]*+\n#u', '', $value, -1, $count);
        $this->offset += $count;

        // remove leading comments
        $trimmedValue = preg_replace('#^(?:\#[^\n]*+\n)++#', '', $value, -1, $count);
        if (1 === $count) {
            // items have been removed, update the offset
            $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n");
            $value = $trimmedValue;
        }

        // remove start of the document marker (---)
        $trimmedValue = preg_replace('#^---[^\n]*+\n#', '', $value, -1, $count);
        if (1 === $count) {
            // items have been removed, update the offset
            $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n");
            $value = $trimmedValue;

            // remove end of the document marker (...)
            $value = preg_replace('#\.\.\.\s*+$#', '', $value);
        }

        return $value;
    }

    private function isNextLineUnIndentedCollection(): bool
    {
        $currentIndentation = $this->getCurrentLineIndentation();
        $movements = 0;

        do {
            $EOF = !$this->moveToNextLine();

            if (!$EOF) {
                ++$movements;
            }
        } while (!$EOF && ($this->isCurrentLineEmpty() || $this->isCurrentLineComment()));

        if ($EOF) {
            return false;
        }

        $ret = $this->getCurrentLineIndentation() === $currentIndentation && $this->isStringUnIndentedCollectionItem();

        for ($i = 0; $i < $movements; ++$i) {
            $this->moveToPreviousLine();
        }

        return $ret;
    }

    private function isStringUnIndentedCollectionItem(): bool
    {
        return '-' === rtrim($this->currentLine) || str_starts_with($this->currentLine, '- ');
    }

    /**
     * A local wrapper for "preg_match" which will throw a ParseException if there
     * is an internal error in the PCRE engine.
     *
     * This avoids us needing to check for "false" every time PCRE is used
     * in the YAML engine
     *
     * @throws ParseException on a PCRE internal error
     *
     * @internal
     */
    public static function preg_match(string $pattern, string $subject, ?array &$matches = null, int $flags = 0, int $offset = 0): int
    {
        if (false === $ret = preg_match($pattern, $subject, $matches, $flags, $offset)) {
            throw new ParseException(preg_last_error_msg());
        }

        return $ret;
    }

    /**
     * Trim the tag on top of the value.
     *
     * Prevent values such as "!foo {quz: bar}" to be considered as
     * a mapping block.
     */
    private function trimTag(string $value): string
    {
        if ('!' === $value[0]) {
            return ltrim(substr($value, 1, strcspn($value, " \r\n", 1)), ' ');
        }

        return $value;
    }

    private function getLineTag(string $value, int $flags, bool $nextLineCheck = true): ?string
    {
        if ('' === $value || '!' !== $value[0] || 1 !== self::preg_match('/^'.self::TAG_PATTERN.' *( +#.*)?$/', $value, $matches)) {
            return null;
        }

        if ($nextLineCheck && !$this->isNextLineIndented()) {
            return null;
        }

        $tag = substr($matches['tag'], 1);

        // Built-in tags
        if ($tag && '!' === $tag[0]) {
            throw new ParseException(\sprintf('The built-in tag "!%s" is not implemented.', $tag), $this->getRealCurrentLineNb() + 1, $value, $this->filename);
        }

        if (Yaml::PARSE_CUSTOM_TAGS & $flags) {
            return $tag;
        }

        throw new ParseException(\sprintf('Tags support is not enabled. You must use the flag "Yaml::PARSE_CUSTOM_TAGS" to use "%s".', $matches['tag']), $this->getRealCurrentLineNb() + 1, $value, $this->filename);
    }

    private function lexInlineQuotedString(int &$cursor = 0): string
    {
        $quotation = $this->currentLine[$cursor];
        $value = $quotation;
        ++$cursor;

        $previousLineWasNewline = true;
        $previousLineWasTerminatedWithBackslash = false;
        $lineNumber = 0;

        do {
            if (++$lineNumber > 1) {
                $cursor += strspn($this->currentLine, ' ', $cursor);
            }

            if ($this->isCurrentLineBlank()) {
                $value .= "\n";
            } elseif (!$previousLineWasNewline && !$previousLineWasTerminatedWithBackslash) {
                $value .= ' ';
            }

            for (; \strlen($this->currentLine) > $cursor; ++$cursor) {
                switch ($this->currentLine[$cursor]) {
                    case '\\':
                        if ("'" === $quotation) {
                            $value .= '\\';
                        } elseif (isset($this->currentLine[++$cursor])) {
                            $value .= '\\'.$this->currentLine[$cursor];
                        }

                        break;
                    case $quotation:
                        ++$cursor;

                        if ("'" === $quotation && isset($this->currentLine[$cursor]) && "'" === $this->currentLine[$cursor]) {
                            $value .= "''";
                            break;
                        }

                        return $value.$quotation;
                    default:
                        $value .= $this->currentLine[$cursor];
                }
            }

            if ($this->isCurrentLineBlank()) {
                $previousLineWasNewline = true;
                $previousLineWasTerminatedWithBackslash = false;
            } elseif ('"' === $quotation && 1 === (\strlen($this->currentLine) - \strlen(rtrim($this->currentLine, '\\'))) % 2) {
                $previousLineWasNewline = false;
                $previousLineWasTerminatedWithBackslash = true;
            } else {
                $previousLineWasNewline = false;
                $previousLineWasTerminatedWithBackslash = false;
            }

            if ($this->hasMoreLines()) {
                $cursor = 0;
            }
        } while ($this->moveToNextLine());

        throw new ParseException('Malformed inline YAML string.');
    }

    private function lexUnquotedString(int &$cursor): string
    {
        $offset = $cursor;

        while ($cursor < \strlen($this->currentLine)) {
            if (\in_array($this->currentLine[$cursor], ['[', ']', '{', '}', ',', ':'], true)) {
                break;
            }

            if (\in_array($this->currentLine[$cursor], [' ', "\t"], true) && '#' === ($this->currentLine[$cursor + 1] ?? '')) {
                break;
            }

            ++$cursor;
        }

        if ($cursor === $offset) {
            throw new ParseException('Malformed unquoted YAML string.');
        }

        return substr($this->currentLine, $offset, $cursor - $offset);
    }

    private function lexInlineAnchorOrAlias(int &$cursor): string
    {
        $offset = $cursor;
        ++$cursor;

        while ($cursor < \strlen($this->currentLine) && !\in_array($this->currentLine[$cursor], [' ', "\t", ',', '[', ']', '{', '}'], true)) {
            ++$cursor;
        }

        return substr($this->currentLine, $offset, $cursor - $offset);
    }

    private function lexInlineMapping(int &$cursor = 0, bool $consumeUntilEol = true): string
    {
        return $this->lexInlineStructure($cursor, '}', $consumeUntilEol);
    }

    private function lexInlineSequence(int &$cursor = 0, bool $consumeUntilEol = true): string
    {
        return $this->lexInlineStructure($cursor, ']', $consumeUntilEol);
    }

    private function lexInlineStructure(int &$cursor, string $closingTag, bool $consumeUntilEol = true): string
    {
        $value = $this->currentLine[$cursor];
        ++$cursor;

        do {
            $this->consumeWhitespaces($cursor);

            while (isset($this->currentLine[$cursor])) {
                switch ($this->currentLine[$cursor]) {
                    case '"':
                    case "'":
                        $value .= $this->lexInlineQuotedString($cursor);
                        break;
                    case ':':
                    case ',':
                        $value .= $this->currentLine[$cursor];
                        ++$cursor;
                        break;
                    case '{':
                        $value .= $this->lexInlineMapping($cursor, false);
                        break;
                    case '[':
                        $value .= $this->lexInlineSequence($cursor, false);
                        break;
                    case '&':
                    case '*':
                        $value .= $this->lexInlineAnchorOrAlias($cursor);
                        break;
                    case $closingTag:
                        $value .= $this->currentLine[$cursor];
                        ++$cursor;

                        if ($consumeUntilEol && isset($this->currentLine[$cursor]) && ($whitespaces = strspn($this->currentLine, ' ', $cursor) + $cursor) < \strlen($this->currentLine) && '#' !== $this->currentLine[$whitespaces]) {
                            throw new ParseException(\sprintf('Unexpected token "%s".', trim(substr($this->currentLine, $cursor))));
                        }

                        return $value;
                    case '#':
                        break 2;
                    default:
                        $value .= $this->lexUnquotedString($cursor);
                }

                if ($this->consumeWhitespaces($cursor)) {
                    $value .= ' ';
                }
            }

            if ($this->hasMoreLines()) {
                $cursor = 0;
            }
        } while ($this->moveToNextLine());

        throw new ParseException('Malformed inline YAML string.');
    }

    private function consumeWhitespaces(int &$cursor): bool
    {
        $whitespacesConsumed = 0;

        do {
            $whitespaceOnlyTokenLength = strspn($this->currentLine, " \t", $cursor);
            $whitespacesConsumed += $whitespaceOnlyTokenLength;
            $cursor += $whitespaceOnlyTokenLength;

            if (isset($this->currentLine[$cursor])) {
                return 0 < $whitespacesConsumed;
            }

            if ($this->hasMoreLines()) {
                $cursor = 0;
            }
        } while ($this->moveToNextLine());

        return 0 < $whitespacesConsumed;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Yaml;

use Symfony\Component\Yaml\Exception\ParseException;

/**
 * Unescaper encapsulates unescaping rules for single and double-quoted
 * YAML strings.
 *
 * @author Matthew Lewinski <matthew@lewinski.org>
 *
 * @internal
 */
class Unescaper
{
    /**
     * Regex fragment that matches an escaped character in a double quoted string.
     */
    public const REGEX_ESCAPED_CHARACTER = '\\\\(x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4}|U[0-9a-fA-F]{8}|.)';

    /**
     * Unescapes a single quoted string.
     *
     * @param string $value A single quoted string
     */
    public function unescapeSingleQuotedString(string $value): string
    {
        return str_replace('\'\'', '\'', $value);
    }

    /**
     * Unescapes a double quoted string.
     *
     * @param string $value A double quoted string
     */
    public function unescapeDoubleQuotedString(string $value): string
    {
        $callback = fn ($match) => $this->unescapeCharacter($match[0]);

        // evaluate the string
        return preg_replace_callback('/'.self::REGEX_ESCAPED_CHARACTER.'/u', $callback, $value);
    }

    /**
     * Unescapes a character that was found in a double-quoted string.
     *
     * @param string $value An escaped character
     */
    private function unescapeCharacter(string $value): string
    {
        return match ($value[1]) {
            '0' => "\x0",
            'a' => "\x7",
            'b' => "\x8",
            't' => "\t",
            "\t" => "\t",
            'n' => "\n",
            'v' => "\xB",
            'f' => "\xC",
            'r' => "\r",
            'e' => "\x1B",
            ' ' => ' ',
            '"' => '"',
            '/' => '/',
            '\\' => '\\',
            // U+0085 NEXT LINE
            'N' => "\xC2\x85",
            // U+00A0 NO-BREAK SPACE
            '_' => "\xC2\xA0",
            // U+2028 LINE SEPARATOR
            'L' => "\xE2\x80\xA8",
            // U+2029 PARAGRAPH SEPARATOR
            'P' => "\xE2\x80\xA9",
            'x' => self::utf8chr(hexdec(substr($value, 2, 2))),
            'u' => self::utf8chr(hexdec(substr($value, 2, 4))),
            'U' => self::utf8chr(hexdec(substr($value, 2, 8))),
            default => throw new ParseException(\sprintf('Found unknown escape character "%s".', $value)),
        };
    }

    /**
     * Get the UTF-8 character for the given code point.
     */
    private static function utf8chr(int $c): string
    {
        if (0x80 > $c %= 0x200000) {
            return \chr($c);
        }
        if (0x800 > $c) {
            return \chr(0xC0 | $c >> 6).\chr(0x80 | $c & 0x3F);
        }
        if (0x10000 > $c) {
            return \chr(0xE0 | $c >> 12).\chr(0x80 | $c >> 6 & 0x3F).\chr(0x80 | $c & 0x3F);
        }

        return \chr(0xF0 | $c >> 18).\chr(0x80 | $c >> 12 & 0x3F).\chr(0x80 | $c >> 6 & 0x3F).\chr(0x80 | $c & 0x3F);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Yaml\Tag;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 * @author Guilhem N. <egetick@gmail.com>
 */
final class TaggedValue
{
    private string $tag;
    private mixed $value;

    public function __construct(string $tag, mixed $value)
    {
        $this->tag = $tag;
        $this->value = $value;
    }

    public function getTag(): string
    {
        return $this->tag;
    }

    public function getValue(): mixed
    {
        return $this->value;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Yaml;

use Symfony\Component\Yaml\Exception\DumpException;
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Tag\TaggedValue;

/**
 * Inline implements a YAML parser/dumper for the YAML inline syntax.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 *
 * @internal
 */
class Inline
{
    public const REGEX_QUOTED_STRING = '(?:"([^"\\\\]*+(?:\\\\.[^"\\\\]*+)*+)"|\'([^\']*+(?:\'\'[^\']*+)*+)\')';

    public static int $parsedLineNumber = -1;
    public static ?string $parsedFilename = null;

    private static bool $exceptionOnInvalidType = false;
    private static bool $objectSupport = false;
    private static bool $objectForMap = false;
    private static bool $constantSupport = false;

    public static function initialize(int $flags, ?int $parsedLineNumber = null, ?string $parsedFilename = null): void
    {
        self::$exceptionOnInvalidType = (bool) (Yaml::PARSE_EXCEPTION_ON_INVALID_TYPE & $flags);
        self::$objectSupport = (bool) (Yaml::PARSE_OBJECT & $flags);
        self::$objectForMap = (bool) (Yaml::PARSE_OBJECT_FOR_MAP & $flags);
        self::$constantSupport = (bool) (Yaml::PARSE_CONSTANT & $flags);
        self::$parsedFilename = $parsedFilename;

        if (null !== $parsedLineNumber) {
            self::$parsedLineNumber = $parsedLineNumber;
        }
    }

    /**
     * Converts a YAML string to a PHP value.
     *
     * @param int   $flags      A bit field of Yaml::PARSE_* constants to customize the YAML parser behavior
     * @param array $references Mapping of variable names to values
     *
     * @throws ParseException
     */
    public static function parse(string $value, int $flags = 0, array &$references = [], ?ParserState $state = null): mixed
    {
        self::initialize($flags);
        $state ??= new ParserState();

        $value = trim($value);

        if ('' === $value) {
            return '';
        }

        $i = 0;
        $isQuoted = null;
        $tag = self::parseTag($value, $i, $flags);
        switch ($value[$i]) {
            case '[':
                $result = self::parseSequence($state, $value, $flags, $i, $references);
                ++$i;
                break;
            case '{':
                $result = self::parseMapping($state, $value, $flags, $i, $references);
                ++$i;
                break;
            default:
                $result = self::parseScalar($value, $flags, null, $i, true, $references, $isQuoted, $state);
        }

        // some comments are allowed at the end
        if (preg_replace('/\s*#.*$/A', '', substr($value, $i))) {
            throw new ParseException(\sprintf('Unexpected characters near "%s".', substr($value, $i)), self::$parsedLineNumber + 1, $value, self::$parsedFilename);
        }

        if (null !== $tag && '' !== $tag) {
            return new TaggedValue($tag, $result);
        }

        return $result;
    }

    /**
     * Dumps a given PHP variable to a YAML string.
     *
     * @param mixed $value The PHP variable to convert
     * @param int   $flags A bit field of Yaml::DUMP_* constants to customize the dumped YAML string
     *
     * @throws DumpException When trying to dump PHP resource
     */
    public static function dump(mixed $value, int $flags = 0): string
    {
        switch (true) {
            case \is_resource($value):
                if (Yaml::DUMP_EXCEPTION_ON_INVALID_TYPE & $flags) {
                    throw new DumpException(\sprintf('Unable to dump PHP resources in a YAML file ("%s").', get_resource_type($value)));
                }

                return self::dumpNull($flags);
            case $value instanceof \DateTimeInterface:
                return $value->format(match (true) {
                    !$length = \strlen(rtrim($value->format('u'), '0')) => 'c',
                    $length < 4 => 'Y-m-d\TH:i:s.vP',
                    default => 'Y-m-d\TH:i:s.uP',
                });
            case $value instanceof \UnitEnum:
                return \sprintf('!php/const %s::%s', $value::class, $value->name);
            case \is_object($value):
                if ($value instanceof TaggedValue) {
                    return '!'.$value->getTag().' '.self::dump($value->getValue(), $flags);
                }

                if (Yaml::DUMP_OBJECT & $flags) {
                    return '!php/object '.self::dump(serialize($value));
                }

                if (Yaml::DUMP_OBJECT_AS_MAP & $flags && ($value instanceof \stdClass || $value instanceof \ArrayObject)) {
                    return self::dumpHashArray($value, $flags);
                }

                if (Yaml::DUMP_EXCEPTION_ON_INVALID_TYPE & $flags) {
                    throw new DumpException('Object support when dumping a YAML file has been disabled.');
                }

                return self::dumpNull($flags);
            case \is_array($value):
                return self::dumpArray($value, $flags);
            case null === $value:
                return self::dumpNull($flags);
            case true === $value:
                return 'true';
            case false === $value:
                return 'false';
            case \is_int($value):
                return $value;
            case \is_float($value) && is_nan($value):
                return '.NaN';
            case is_numeric($value) && false === strpbrk($value, "\f\n\r\t\v"):
                $locale = setlocale(\LC_NUMERIC, 0);
                if (false !== $locale) {
                    setlocale(\LC_NUMERIC, 'C');
                }
                if (\is_float($value)) {
                    $repr = (string) $value;
                    if (is_infinite($value)) {
                        $repr = str_ireplace('INF', '.Inf', $repr);
                    } elseif (floor($value) == $value && $repr == $value) {
                        // Preserve float data type since storing a whole number will result in integer value.
                        if (!str_contains($repr, 'E')) {
                            $repr .= '.0';
                        }
                    }
                } else {
                    $repr = \is_string($value) ? "'$value'" : (string) $value;
                }
                if (false !== $locale) {
                    setlocale(\LC_NUMERIC, $locale);
                }

                return $repr;
            case '' == $value:
                return "''";
            case self::isBinaryString($value):
                return '!!binary '.base64_encode($value);
            case Escaper::requiresDoubleQuoting($value):
                return Escaper::escapeWithDoubleQuotes($value);
            case Escaper::requiresSingleQuoting($value):
                $singleQuoted = Escaper::escapeWithSingleQuotes($value);
                if (!str_contains($value, "'")) {
                    return $singleQuoted;
                }
                // Attempt double-quoting the string instead to see if it's more efficient.
                $doubleQuoted = Escaper::escapeWithDoubleQuotes($value);

                return \strlen($doubleQuoted) < \strlen($singleQuoted) ? $doubleQuoted : $singleQuoted;
            case Parser::preg_match('{^[0-9]+[_0-9]*$}', $value):
            case Parser::preg_match(self::getHexRegex(), $value):
            case Parser::preg_match(self::getTimestampRegex(), $value):
                return Escaper::escapeWithSingleQuotes($value);
            default:
                return $value;
        }
    }

    /**
     * Check if given array is hash or just normal indexed array.
     */
    public static function isHash(array|\ArrayObject|\stdClass $value): bool
    {
        if ($value instanceof \stdClass || $value instanceof \ArrayObject) {
            return true;
        }

        $expectedKey = 0;

        foreach ($value as $key => $val) {
            if ($key !== $expectedKey++) {
                return true;
            }
        }

        return false;
    }

    /**
     * Dumps a PHP array to a YAML string.
     *
     * @param array $value The PHP array to dump
     * @param int   $flags A bit field of Yaml::DUMP_* constants to customize the dumped YAML string
     */
    private static function dumpArray(array $value, int $flags): string
    {
        // array
        if (($value || Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE & $flags) && !self::isHash($value)) {
            $output = [];
            foreach ($value as $val) {
                $output[] = self::dump($val, $flags);
            }

            return \sprintf('[%s]', implode(', ', $output));
        }

        return self::dumpHashArray($value, $flags);
    }

    /**
     * Dumps hash array to a YAML string.
     *
     * @param array|\ArrayObject|\stdClass $value The hash array to dump
     * @param int                          $flags A bit field of Yaml::DUMP_* constants to customize the dumped YAML string
     */
    private static function dumpHashArray(array|\ArrayObject|\stdClass $value, int $flags): string
    {
        $output = [];
        foreach ($value as $key => $val) {
            if (\is_int($key) && Yaml::DUMP_NUMERIC_KEY_AS_STRING & $flags) {
                $key = (string) $key;
            }

            $output[] = \sprintf('%s: %s', self::dump($key, $flags), self::dump($val, $flags));
        }

        return \sprintf('{ %s }', implode(', ', $output));
    }

    private static function dumpNull(int $flags): string
    {
        if (Yaml::DUMP_NULL_AS_TILDE & $flags) {
            return '~';
        }

        return 'null';
    }

    /**
     * Parses a YAML scalar.
     *
     * @throws ParseException When malformed inline YAML string is parsed
     */
    public static function parseScalar(string $scalar, int $flags = 0, ?array $delimiters = null, int &$i = 0, bool $evaluate = true, array &$references = [], ?bool &$isQuoted = null, ?ParserState $state = null): mixed
    {
        if (\in_array($scalar[$i], ['"', "'"], true)) {
            // quoted scalar
            $isQuoted = true;
            $output = self::parseQuotedScalar($scalar, $i);

            if (null !== $delimiters) {
                $tmp = ltrim(substr($scalar, $i), " \n");
                if ('' === $tmp) {
                    throw new ParseException(\sprintf('Unexpected end of line, expected one of "%s".', implode('', $delimiters)), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename);
                }
                if (!\in_array($tmp[0], $delimiters)) {
                    throw new ParseException(\sprintf('Unexpected characters (%s).', substr($scalar, $i)), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename);
                }
            }
        } else {
            // "normal" string
            $isQuoted = false;

            if (!$delimiters) {
                $output = substr($scalar, $i);
                $i += \strlen($output);

                // remove comments
                if (Parser::preg_match('/[ \t]+#/', $output, $match, \PREG_OFFSET_CAPTURE)) {
                    $output = substr($output, 0, $match[0][1]);
                }
            } elseif (Parser::preg_match('/^(.*?)('.implode('|', $delimiters).')/', substr($scalar, $i), $match)) {
                $output = $match[1];
                $i += \strlen($output);
                $output = trim($output);
            } else {
                throw new ParseException(\sprintf('Malformed inline YAML string: "%s".', $scalar), self::$parsedLineNumber + 1, null, self::$parsedFilename);
            }

            // a non-quoted string cannot start with @ or ` (reserved) nor with a scalar indicator (| or >)
            if ($output && ('@' === $output[0] || '`' === $output[0] || '|' === $output[0] || '>' === $output[0] || '%' === $output[0])) {
                throw new ParseException(\sprintf('The reserved indicator "%s" cannot start a plain scalar; you need to quote the scalar.', $output[0]), self::$parsedLineNumber + 1, $output, self::$parsedFilename);
            }

            if ($evaluate) {
                $state ??= new ParserState();
                $output = self::evaluateScalar($state, $output, $flags, $references, $isQuoted);
            }
        }

        return $output;
    }

    /**
     * Parses a YAML quoted scalar.
     *
     * @throws ParseException When malformed inline YAML string is parsed
     */
    private static function parseQuotedScalar(string $scalar, int &$i = 0): string
    {
        if (!Parser::preg_match('/'.self::REGEX_QUOTED_STRING.'/Au', substr($scalar, $i), $match)) {
            throw new ParseException(\sprintf('Malformed inline YAML string: "%s".', substr($scalar, $i)), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename);
        }

        $output = substr($match[0], 1, -1);

        $unescaper = new Unescaper();
        if ('"' == $scalar[$i]) {
            $output = $unescaper->unescapeDoubleQuotedString($output);
        } else {
            $output = $unescaper->unescapeSingleQuotedString($output);
        }

        $i += \strlen($match[0]);

        return $output;
    }

    /**
     * Parses a YAML sequence.
     *
     * @throws ParseException When malformed inline YAML string is parsed
     */
    private static function parseSequence(ParserState $state, string $sequence, int $flags, int &$i = 0, array &$references = []): array
    {
        $state->enterNestingLevel(self::$parsedLineNumber + 1, null, self::$parsedFilename);

        $output = [];
        $len = \strlen($sequence);
        ++$i;

        try {
            // [foo, bar, ...]
            $lastToken = null;
            while ($i < $len) {
                if (']' === $sequence[$i]) {
                    return $output;
                }
                if (',' === $sequence[$i] || ' ' === $sequence[$i]) {
                    if (',' === $sequence[$i] && (null === $lastToken || 'separator' === $lastToken)) {
                        $output[] = null;
                    } elseif (',' === $sequence[$i]) {
                        $lastToken = 'separator';
                    }

                    ++$i;

                    continue;
                }

                $tag = self::parseTag($sequence, $i, $flags);
                $anchorRef = self::parseAnchor($sequence, $i);
                if (null !== $anchorRef && null === $tag) {
                    $tag = self::parseTag($sequence, $i, $flags);
                }

                if (null !== $anchorRef && (!isset($sequence[$i]) || ',' === $sequence[$i] || ']' === $sequence[$i])) {
                    $value = null;
                    $references[$anchorRef] = $value;
                    if (null !== $tag && '' !== $tag) {
                        $value = new TaggedValue($tag, $value);
                    }
                    $output[] = $value;
                    $lastToken = 'value';
                    continue;
                }

                switch ($sequence[$i]) {
                    case '[':
                        // nested sequence
                        $value = self::parseSequence($state, $sequence, $flags, $i, $references);
                        break;
                    case '{':
                        // nested mapping
                        $value = self::parseMapping($state, $sequence, $flags, $i, $references);
                        break;
                    default:
                        $value = self::parseScalar($sequence, $flags, [',', ']'], $i, null === $tag, $references, $isQuoted, $state);

                        // the value can be an array if a reference has been resolved to an array var
                        if (\is_string($value) && !$isQuoted && str_contains($value, ': ')) {
                            // embedded mapping?
                            $j = $i;
                            $mappingValue = $value;
                            $mappingException = null;
                            do {
                                try {
                                    $pos = 0;
                                    $value = self::parseMapping($state, '{'.$mappingValue.'}', $flags, $pos, $references);
                                    $i = $j;
                                    $mappingException = null;
                                    break;
                                } catch (ParseException $exception) {
                                    $mappingException = $exception;
                                    if ($j >= $len) {
                                        break;
                                    }

                                    $mappingValue .= $sequence[$j++];
                                    if ($j >= $len) {
                                        break;
                                    }

                                    $mappingValue .= self::parseScalar($sequence, $flags, [',', ']'], $j, null === $tag, $references);
                                }
                            } while ($j < $len);

                            if ($mappingException) {
                                throw $mappingException;
                            }
                        }

                        --$i;
                }

                if (null !== $anchorRef) {
                    $references[$anchorRef] = $value;
                }

                if (null !== $tag && '' !== $tag) {
                    $value = new TaggedValue($tag, $value);
                }

                $output[] = $value;

                $lastToken = 'value';
                ++$i;
            }

            throw new ParseException(\sprintf('Malformed inline YAML string: "%s".', $sequence), self::$parsedLineNumber + 1, null, self::$parsedFilename);
        } finally {
            $state->leaveNestingLevel();
        }
    }

    /**
     * Parses a YAML mapping.
     *
     * @throws ParseException When malformed inline YAML string is parsed
     */
    private static function parseMapping(ParserState $state, string $mapping, int $flags, int &$i = 0, array &$references = []): array|\stdClass
    {
        $state->enterNestingLevel(self::$parsedLineNumber + 1, null, self::$parsedFilename);

        $output = [];
        $len = \strlen($mapping);
        ++$i;
        $allowOverwrite = false;

        try {
            // {foo: bar, bar:foo, ...}
            while ($i < $len) {
                switch ($mapping[$i]) {
                    case ' ':
                    case ',':
                    case "\n":
                        ++$i;
                        continue 2;
                    case '}':
                        if (self::$objectForMap) {
                            return (object) $output;
                        }

                        return $output;
                }

                // key
                $offsetBeforeKeyParsing = $i;
                $isKeyQuoted = \in_array($mapping[$i], ['"', "'"], true);
                $key = self::parseScalar($mapping, $flags, [':', ' '], $i, false);

                if ($offsetBeforeKeyParsing === $i) {
                    throw new ParseException('Missing mapping key.', self::$parsedLineNumber + 1, $mapping);
                }

                if ('!php/const' === $key || '!php/enum' === $key) {
                    $key .= ' '.self::parseScalar($mapping, $flags, ['(?<!:):(?!:)'], $i, false);
                    $key = self::evaluateScalar($state, $key, $flags);
                }

                if (false === $i = strpos($mapping, ':', $i)) {
                    break;
                }

                if (!$isKeyQuoted) {
                    $evaluatedKey = self::evaluateScalar($state, $key, $flags, $references);

                    if ('' !== $key && $evaluatedKey !== $key && !\is_string($evaluatedKey) && !\is_int($evaluatedKey)) {
                        throw new ParseException('Implicit casting of incompatible mapping keys to strings is not supported. Quote your evaluable mapping keys instead.', self::$parsedLineNumber + 1, $mapping);
                    }
                }

                if (!$isKeyQuoted && (!isset($mapping[$i + 1]) || !\in_array($mapping[$i + 1], [' ', ',', '[', ']', '{', '}', "\n"], true))) {
                    throw new ParseException('Colons must be followed by a space or an indication character (i.e. " ", ",", "[", "]", "{", "}").', self::$parsedLineNumber + 1, $mapping);
                }

                if ('<<' === $key) {
                    $allowOverwrite = true;
                }

                while ($i < $len) {
                    if (':' === $mapping[$i] || ' ' === $mapping[$i] || "\n" === $mapping[$i]) {
                        ++$i;

                        continue;
                    }

                    $tag = self::parseTag($mapping, $i, $flags);
                    $anchorRef = self::parseAnchor($mapping, $i);
                    if (null !== $anchorRef && null === $tag) {
                        $tag = self::parseTag($mapping, $i, $flags);
                    }

                    if (null !== $anchorRef && (!isset($mapping[$i]) || \in_array($mapping[$i], [',', '}', "\n"], true))) {
                        $value = null;
                        $references[$anchorRef] = $value;
                        if ('<<' !== $key) {
                            if ($allowOverwrite || !isset($output[$key])) {
                                $output[$key] = null !== $tag ? new TaggedValue($tag, $value) : $value;
                            } elseif (isset($output[$key])) {
                                throw new ParseException(\sprintf('Duplicate key "%s" detected.', $key), self::$parsedLineNumber + 1, $mapping);
                            }
                        }
                        continue 2;
                    }

                    switch ($mapping[$i]) {
                        case '[':
                            // nested sequence
                            $value = self::parseSequence($state, $mapping, $flags, $i, $references);
                            if (null !== $anchorRef) {
                                $references[$anchorRef] = $value;
                            }
                            // Spec: Keys MUST be unique; first one wins.
                            // Parser cannot abort this mapping earlier, since lines
                            // are processed sequentially.
                            // But overwriting is allowed when a merge node is used in current block.
                            if ('<<' === $key) {
                                foreach ($value as $parsedValue) {
                                    $output += $parsedValue;
                                }
                            } elseif ($allowOverwrite || !isset($output[$key])) {
                                if (null !== $tag) {
                                    $output[$key] = new TaggedValue($tag, $value);
                                } else {
                                    $output[$key] = $value;
                                }
                            } elseif (isset($output[$key])) {
                                throw new ParseException(\sprintf('Duplicate key "%s" detected.', $key), self::$parsedLineNumber + 1, $mapping);
                            }
                            break;
                        case '{':
                            // nested mapping
                            $value = self::parseMapping($state, $mapping, $flags, $i, $references);
                            if (null !== $anchorRef) {
                                $references[$anchorRef] = $value;
                            }
                            // Spec: Keys MUST be unique; first one wins.
                            // Parser cannot abort this mapping earlier, since lines
                            // are processed sequentially.
                            // But overwriting is allowed when a merge node is used in current block.
                            if ('<<' === $key) {
                                $output += $value;
                            } elseif ($allowOverwrite || !isset($output[$key])) {
                                if (null !== $tag) {
                                    $output[$key] = new TaggedValue($tag, $value);
                                } else {
                                    $output[$key] = $value;
                                }
                            } elseif (isset($output[$key])) {
                                throw new ParseException(\sprintf('Duplicate key "%s" detected.', $key), self::$parsedLineNumber + 1, $mapping);
                            }
                            break;
                        default:
                            $value = self::parseScalar($mapping, $flags, [',', '}', "\n"], $i, null === $tag, $references, $isValueQuoted, $state);
                            if (null !== $anchorRef) {
                                $references[$anchorRef] = $value;
                            }
                            // Spec: Keys MUST be unique; first one wins.
                            // Parser cannot abort this mapping earlier, since lines
                            // are processed sequentially.
                            // But overwriting is allowed when a merge node is used in current block.
                            if ('<<' === $key) {
                                $output += $value;
                            } elseif ($allowOverwrite || !isset($output[$key])) {
                                if (null !== $tag) {
                                    $output[$key] = new TaggedValue($tag, $value);
                                } else {
                                    $output[$key] = $value;
                                }
                            } elseif (isset($output[$key])) {
                                throw new ParseException(\sprintf('Duplicate key "%s" detected.', $key), self::$parsedLineNumber + 1, $mapping);
                            }
                            --$i;
                    }
                    ++$i;

                    continue 2;
                }
            }

            throw new ParseException(\sprintf('Malformed inline YAML string: "%s".', $mapping), self::$parsedLineNumber + 1, null, self::$parsedFilename);
        } finally {
            $state->leaveNestingLevel();
        }
    }

    /**
     * Evaluates scalars and replaces magic values.
     *
     * @throws ParseException when object parsing support was disabled and the parser detected a PHP object or when a reference could not be resolved
     */
    private static function evaluateScalar(ParserState $state, string $scalar, int $flags, array &$references = [], ?bool &$isQuotedString = null): mixed
    {
        $isQuotedString = false;
        $scalar = trim($scalar);

        if (str_starts_with($scalar, '*')) {
            if (false !== $pos = strpos($scalar, '#')) {
                $value = substr($scalar, 1, $pos - 2);
            } else {
                $value = substr($scalar, 1);
            }

            // an unquoted *
            if (false === $value || '' === $value) {
                throw new ParseException('A reference must contain at least one character.', self::$parsedLineNumber + 1, $value, self::$parsedFilename);
            }

            if (!\array_key_exists($value, $references)) {
                throw new ParseException(\sprintf('Reference "%s" does not exist.', $value), self::$parsedLineNumber + 1, $value, self::$parsedFilename);
            }

            $state->countAlias($references[$value], self::$parsedLineNumber + 1, null, self::$parsedFilename);

            return $references[$value];
        }

        $scalarLower = strtolower($scalar);

        switch (true) {
            case 'null' === $scalarLower:
            case '' === $scalar:
            case '~' === $scalar:
                return null;
            case 'true' === $scalarLower:
                return true;
            case 'false' === $scalarLower:
                return false;
            case '!' === $scalar[0]:
                switch (true) {
                    case str_starts_with($scalar, '!!str '):
                        $s = (string) substr($scalar, 6);

                        if (\in_array($s[0] ?? '', ['"', "'"], true)) {
                            $isQuotedString = true;
                            $s = self::parseQuotedScalar($s);
                        }

                        return $s;
                    case str_starts_with($scalar, '! '):
                        return substr($scalar, 2);
                    case str_starts_with($scalar, '!php/object'):
                        if (self::$objectSupport) {
                            if (!isset($scalar[12])) {
                                throw new ParseException('Missing value for tag "!php/object".', self::$parsedLineNumber + 1, $scalar, self::$parsedFilename);
                            }

                            return unserialize(self::parseScalar(substr($scalar, 12)), ['allowed_classes' => true]);
                        }

                        if (self::$exceptionOnInvalidType) {
                            throw new ParseException('Object support when parsing a YAML file has been disabled.', self::$parsedLineNumber + 1, $scalar, self::$parsedFilename);
                        }

                        return null;
                    case str_starts_with($scalar, '!php/const'):
                        if (self::$constantSupport) {
                            if (!isset($scalar[11])) {
                                throw new ParseException('Missing value for tag "!php/const".', self::$parsedLineNumber + 1, $scalar, self::$parsedFilename);
                            }

                            $i = 0;
                            if (\defined($const = self::parseScalar(substr($scalar, 11), 0, null, $i, false))) {
                                return \constant($const);
                            }

                            throw new ParseException(\sprintf('The constant "%s" is not defined.', $const), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename);
                        }
                        if (self::$exceptionOnInvalidType) {
                            throw new ParseException(\sprintf('The string "%s" could not be parsed as a constant. Did you forget to pass the "Yaml::PARSE_CONSTANT" flag to the parser?', $scalar), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename);
                        }

                        return null;
                    case str_starts_with($scalar, '!php/enum'):
                        if (self::$constantSupport) {
                            if (!isset($scalar[11])) {
                                throw new ParseException('Missing value for tag "!php/enum".', self::$parsedLineNumber + 1, $scalar, self::$parsedFilename);
                            }

                            $i = 0;
                            $enum = self::parseScalar(substr($scalar, 10), 0, null, $i, false);
                            if ($useValue = str_ends_with($enum, '->value')) {
                                $enum = substr($enum, 0, -7);
                            }
                            if (!\defined($enum)) {
                                throw new ParseException(\sprintf('The enum "%s" is not defined.', $enum), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename);
                            }

                            $value = \constant($enum);

                            if (!$value instanceof \UnitEnum) {
                                throw new ParseException(\sprintf('The string "%s" is not the name of a valid enum.', $enum), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename);
                            }
                            if (!$useValue) {
                                return $value;
                            }
                            if (!$value instanceof \BackedEnum) {
                                throw new ParseException(\sprintf('The enum "%s" defines no value next to its name.', $enum), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename);
                            }

                            return $value->value;
                        }
                        if (self::$exceptionOnInvalidType) {
                            throw new ParseException(\sprintf('The string "%s" could not be parsed as an enum. Did you forget to pass the "Yaml::PARSE_CONSTANT" flag to the parser?', $scalar), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename);
                        }

                        return null;
                    case str_starts_with($scalar, '!!float '):
                        return (float) substr($scalar, 8);
                    case str_starts_with($scalar, '!!binary '):
                        return self::evaluateBinaryScalar(substr($scalar, 9));
                }

                throw new ParseException(\sprintf('The string "%s" could not be parsed as it uses an unsupported built-in tag.', $scalar), self::$parsedLineNumber, $scalar, self::$parsedFilename);
            case preg_match('/^(?:\+|-)?0o(?P<value>[0-7_]++)$/', $scalar, $matches):
                $value = str_replace('_', '', $matches['value']);

                if ('-' === $scalar[0]) {
                    return -octdec($value);
                }

                return octdec($value);
            case \in_array($scalar[0], ['+', '-', '.'], true) || is_numeric($scalar[0]):
                if (Parser::preg_match('{^[+-]?[0-9][0-9_]*$}', $scalar)) {
                    $scalar = str_replace('_', '', $scalar);

                    if ('+' === $scalar[0]) {
                        $scalar = substr($scalar, 1);
                    }
                }

                switch (true) {
                    case ctype_digit($scalar):
                    case '-' === $scalar[0] && ctype_digit(substr($scalar, 1)):
                        if ($scalar < \PHP_INT_MIN || \PHP_INT_MAX < $scalar) {
                            return $scalar;
                        }

                        $cast = (int) $scalar;

                        return ($scalar === (string) $cast) ? $cast : $scalar;
                    case is_numeric($scalar):
                    case Parser::preg_match(self::getHexRegex(), $scalar):
                        $scalar = str_replace('_', '', $scalar);

                        return '0x' === $scalar[0].$scalar[1] ? hexdec($scalar) : (float) $scalar;
                    case '.inf' === $scalarLower:
                        return -log(0);
                    case '.nan' === $scalarLower:
                        return \NAN;
                    case '-.inf' === $scalarLower:
                        return log(0);
                    case Parser::preg_match('/^(-|\+)?[0-9][0-9_]*(\.[0-9_]+)?$/', $scalar):
                        return (float) str_replace('_', '', $scalar);
                    case Parser::preg_match(self::getTimestampRegex(), $scalar):
                        try {
                            // When no timezone is provided in the parsed date, YAML spec says we must assume UTC.
                            $time = new \DateTimeImmutable($scalar, new \DateTimeZone('UTC'));
                        } catch (\Exception $e) {
                            // Some dates accepted by the regex are not valid dates.
                            throw new ParseException(\sprintf('The date "%s" could not be parsed as it is an invalid date.', $scalar), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename, $e);
                        }

                        if (Yaml::PARSE_DATETIME & $flags) {
                            return $time;
                        }

                        if ('' !== rtrim($time->format('u'), '0')) {
                            return (float) $time->format('U.u');
                        }

                        try {
                            if (false !== $scalar = $time->getTimestamp()) {
                                return $scalar;
                            }
                        } catch (\DateRangeError|\ValueError) {
                            // no-op
                        }

                        return $time->format('U');
                }
        }

        return (string) $scalar;
    }

    private static function parseAnchor(string $value, int &$i): ?string
    {
        if (!isset($value[$i]) || '&' !== $value[$i]) {
            return null;
        }

        if (!Parser::preg_match('/^&(?P<ref>[^ \t,\[\]\{\}\n]++)\s*+/A', substr($value, $i), $matches)) {
            return null;
        }

        $i += \strlen($matches[0]);

        return $matches['ref'];
    }

    private static function parseTag(string $value, int &$i, int $flags): ?string
    {
        if ('!' !== $value[$i]) {
            return null;
        }

        $tagLength = strcspn($value, " \t\n[]{},", $i + 1);
        $tag = substr($value, $i + 1, $tagLength);

        $nextOffset = $i + $tagLength + 1;
        $nextOffset += strspn($value, ' ', $nextOffset);

        if ('' === $tag && (!isset($value[$nextOffset]) || \in_array($value[$nextOffset], [']', '}', ','], true))) {
            throw new ParseException('Using the unquoted scalar value "!" is not supported. You must quote it.', self::$parsedLineNumber + 1, $value, self::$parsedFilename);
        }

        // Is followed by a scalar and is a built-in tag
        if ('' !== $tag && (!isset($value[$nextOffset]) || !\in_array($value[$nextOffset], ['[', '{'], true)) && ('!' === $tag[0] || \in_array($tag, ['str', 'php/const', 'php/enum', 'php/object'], true))) {
            // Manage in {@link self::evaluateScalar()}
            return null;
        }

        $i = $nextOffset;

        // Built-in tags
        if ('' !== $tag && '!' === $tag[0]) {
            throw new ParseException(\sprintf('The built-in tag "!%s" is not implemented.', $tag), self::$parsedLineNumber + 1, $value, self::$parsedFilename);
        }

        if ('' !== $tag && !isset($value[$i])) {
            throw new ParseException(\sprintf('Missing value for tag "%s".', $tag), self::$parsedLineNumber + 1, $value, self::$parsedFilename);
        }

        if ('' === $tag || Yaml::PARSE_CUSTOM_TAGS & $flags) {
            return $tag;
        }

        throw new ParseException(\sprintf('Tags support is not enabled. Enable the "Yaml::PARSE_CUSTOM_TAGS" flag to use "!%s".', $tag), self::$parsedLineNumber + 1, $value, self::$parsedFilename);
    }

    public static function evaluateBinaryScalar(string $scalar): string
    {
        $parsedBinaryData = self::parseScalar(preg_replace('/\s/', '', $scalar));

        if (!\is_scalar($parsedBinaryData ?? '') && !$parsedBinaryData instanceof \Stringable) {
            throw new ParseException(\sprintf('The "!!binary" tag only supports a base64 encoded string, got "%s".', get_debug_type($parsedBinaryData)), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename);
        }

        $parsedBinaryData = (string) $parsedBinaryData;

        if (0 !== (\strlen($parsedBinaryData) % 4)) {
            throw new ParseException(\sprintf('The normalized base64 encoded data (data without whitespace characters) length must be a multiple of four (%d bytes given).', \strlen($parsedBinaryData)), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename);
        }

        if (!Parser::preg_match('#^[A-Z0-9+/]+={0,2}$#i', $parsedBinaryData)) {
            throw new ParseException(\sprintf('The base64 encoded data (%s) contains invalid characters.', $parsedBinaryData), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename);
        }

        return base64_decode($parsedBinaryData, true);
    }

    private static function isBinaryString(string $value): bool
    {
        return !preg_match('//u', $value) || preg_match('/[^\x00\x07-\x0d\x1B\x20-\xff]/', $value);
    }

    /**
     * Gets a regex that matches a YAML date.
     *
     * @see http://www.yaml.org/spec/1.2/spec.html#id2761573
     */
    private static function getTimestampRegex(): string
    {
        return <<<EOF
                    ~^
                    (?P<year>[0-9][0-9][0-9][0-9])
                    -(?P<month>[0-9][0-9]?)
                    -(?P<day>[0-9][0-9]?)
                    (?:(?:[Tt]|[ \t]+)
                    (?P<hour>[0-9][0-9]?)
                    :(?P<minute>[0-9][0-9])
                    :(?P<second>[0-9][0-9])
                    (?:\.(?P<fraction>[0-9]*))?
                    (?:[ \t]*(?P<tz>Z|(?P<tz_sign>[-+])(?P<tz_hour>[0-9][0-9]?)
                    (?::(?P<tz_minute>[0-9][0-9]))?))?)?
                    $~x
            EOF;
    }

    /**
     * Gets a regex that matches a YAML number in hexadecimal notation.
     */
    private static function getHexRegex(): string
    {
        return '~^0x[0-9a-f_]++$~i';
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Yaml;

/**
 * Escaper encapsulates escaping rules for single and double-quoted
 * YAML strings.
 *
 * @author Matthew Lewinski <matthew@lewinski.org>
 *
 * @internal
 */
class Escaper
{
    // Characters that would cause a dumped string to require double quoting.
    public const REGEX_CHARACTER_TO_ESCAPE = "[\\x00-\\x1f]|\x7f|\xc2\x85|\xc2\xa0|\xe2\x80\xa8|\xe2\x80\xa9";

    // Mapping arrays for escaping a double quoted string. The backslash is
    // first to ensure proper escaping because str_replace operates iteratively
    // on the input arrays. This ordering of the characters avoids the use of strtr,
    // which performs more slowly.
    private const ESCAPEES = ['\\', '\\\\', '\\"', '"',
        "\x00",  "\x01",  "\x02",  "\x03",  "\x04",  "\x05",  "\x06",  "\x07",
        "\x08",  "\x09",  "\x0a",  "\x0b",  "\x0c",  "\x0d",  "\x0e",  "\x0f",
        "\x10",  "\x11",  "\x12",  "\x13",  "\x14",  "\x15",  "\x16",  "\x17",
        "\x18",  "\x19",  "\x1a",  "\x1b",  "\x1c",  "\x1d",  "\x1e",  "\x1f",
        "\x7f",
        "\xc2\x85", "\xc2\xa0", "\xe2\x80\xa8", "\xe2\x80\xa9",
    ];
    private const ESCAPED = ['\\\\', '\\"', '\\\\', '\\"',
        '\\0',   '\\x01', '\\x02', '\\x03', '\\x04', '\\x05', '\\x06', '\\a',
        '\\b',   '\\t',   '\\n',   '\\v',   '\\f',   '\\r',   '\\x0e', '\\x0f',
        '\\x10', '\\x11', '\\x12', '\\x13', '\\x14', '\\x15', '\\x16', '\\x17',
        '\\x18', '\\x19', '\\x1a', '\\e',   '\\x1c', '\\x1d', '\\x1e', '\\x1f',
        '\\x7f',
        '\\N', '\\_', '\\L', '\\P',
    ];

    /**
     * Determines if a PHP value would require double quoting in YAML.
     *
     * @param string $value A PHP value
     */
    public static function requiresDoubleQuoting(string $value): bool
    {
        return 0 < preg_match('/'.self::REGEX_CHARACTER_TO_ESCAPE.'/u', $value);
    }

    /**
     * Escapes and surrounds a PHP value with double quotes.
     *
     * @param string $value A PHP value
     */
    public static function escapeWithDoubleQuotes(string $value): string
    {
        return \sprintf('"%s"', str_replace(self::ESCAPEES, self::ESCAPED, $value));
    }

    /**
     * Determines if a PHP value would require single quoting in YAML.
     *
     * @param string $value A PHP value
     */
    public static function requiresSingleQuoting(string $value): bool
    {
        // Determines if a PHP value is entirely composed of a value that would
        // require single quoting in YAML.
        if (\in_array(strtolower($value), ['null', '~', 'true', 'false', 'y', 'n', 'yes', 'no', 'on', 'off'])) {
            return true;
        }

        // Determines if the PHP value contains any single characters that would
        // cause it to require single quoting in YAML.
        return 0 < preg_match('/[ \s \' " \: \{ \} \[ \] , & \* \# \?] | \A[ \- ? | < > = ! % @ ` \p{Zs}]/xu', $value);
    }

    /**
     * Escapes and surrounds a PHP value with single quotes.
     *
     * @param string $value A PHP value
     */
    public static function escapeWithSingleQuotes(string $value): string
    {
        return \sprintf("'%s'", str_replace('\'', '\'\'', $value));
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Contracts\Service;

use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;

// Help opcache.preload discover always-needed symbols
class_exists(ContainerExceptionInterface::class);
class_exists(NotFoundExceptionInterface::class);

/**
 * A trait to help implement ServiceProviderInterface.
 *
 * @author Robin Chalas <robin.chalas@gmail.com>
 * @author Nicolas Grekas <p@tchwork.com>
 */
trait ServiceLocatorTrait
{
    private array $loading = [];
    private array $providedTypes;

    /**
     * @param array<string, callable> $factories
     */
    public function __construct(
        private array $factories,
    ) {
    }

    public function has(string $id): bool
    {
        return isset($this->factories[$id]);
    }

    public function get(string $id): mixed
    {
        if (!isset($this->factories[$id])) {
            throw $this->createNotFoundException($id);
        }

        if (isset($this->loading[$id])) {
            $ids = array_values($this->loading);
            $ids = \array_slice($this->loading, array_search($id, $ids));
            $ids[] = $id;

            throw $this->createCircularReferenceException($id, $ids);
        }

        $this->loading[$id] = $id;
        try {
            return $this->factories[$id]($this);
        } finally {
            unset($this->loading[$id]);
        }
    }

    public function getProvidedServices(): array
    {
        if (!isset($this->providedTypes)) {
            $this->providedTypes = [];

            foreach ($this->factories as $name => $factory) {
                if (!\is_callable($factory)) {
                    $this->providedTypes[$name] = '?';
                } else {
                    $type = (new \ReflectionFunction($factory))->getReturnType();

                    $this->providedTypes[$name] = $type ? ($type->allowsNull() ? '?' : '').($type instanceof \ReflectionNamedType ? $type->getName() : $type) : '?';
                }
            }
        }

        return $this->providedTypes;
    }

    private function createNotFoundException(string $id): NotFoundExceptionInterface
    {
        if (!$alternatives = array_keys($this->factories)) {
            $message = 'is empty...';
        } else {
            $last = array_pop($alternatives);
            if ($alternatives) {
                $message = \sprintf('only knows about the "%s" and "%s" services.', implode('", "', $alternatives), $last);
            } else {
                $message = \sprintf('only knows about the "%s" service.', $last);
            }
        }

        if ($this->loading) {
            $message = \sprintf('The service "%s" has a dependency on a non-existent service "%s". This locator %s', end($this->loading), $id, $message);
        } else {
            $message = \sprintf('Service "%s" not found: the current service locator %s', $id, $message);
        }

        return new class($message) extends \InvalidArgumentException implements NotFoundExceptionInterface {
        };
    }

    private function createCircularReferenceException(string $id, array $path): ContainerExceptionInterface
    {
        return new class(\sprintf('Circular reference detected for service "%s", path: "%s".', $id, implode(' -> ', $path))) extends \RuntimeException implements ContainerExceptionInterface {
        };
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Contracts\Service;

use Psr\Container\ContainerInterface;

/**
 * A ServiceProviderInterface exposes the identifiers and the types of services provided by a container.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 * @author Mateusz Sip <mateusz.sip@gmail.com>
 *
 * @template-covariant T of mixed
 */
interface ServiceProviderInterface extends ContainerInterface
{
    /**
     * @return T
     */
    public function get(string $id): mixed;

    public function has(string $id): bool;

    /**
     * Returns an associative array of service types keyed by the identifiers provided by the current container.
     *
     * Examples:
     *
     *  * ['logger' => 'Psr\Log\LoggerInterface'] means the object provides a service named "logger" that implements Psr\Log\LoggerInterface
     *  * ['foo' => '?'] means the container provides service name "foo" of unspecified type
     *  * ['bar' => '?Bar\Baz'] means the container provides a service "bar" of type Bar\Baz|null
     *
     * @return array<string, string> The provided service types, keyed by service names
     */
    public function getProvidedServices(): array;
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Contracts\Service;

/**
 * @deprecated use ContainerProviderInterface instead
 */
interface ContainerAwareInterface extends ContainerProviderInterface
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Contracts\Service\Attribute;

/**
 * A required dependency.
 *
 * This attribute indicates that a property holds a required dependency. The annotated property or method should be
 * considered during the instantiation process of the containing class.
 *
 * @author Alexander M. Turek <me@derrabus.de>
 */
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)]
final class Required
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Contracts\Service\Attribute;

use Symfony\Contracts\Service\ServiceMethodsSubscriberTrait;
use Symfony\Contracts\Service\ServiceSubscriberInterface;

/**
 * For use as the return value for {@see ServiceSubscriberInterface}.
 *
 * @example new SubscribedService('http_client', HttpClientInterface::class, false, new Target('githubApi'))
 *
 * Use with {@see ServiceMethodsSubscriberTrait} to mark a method's return type
 * as a subscribed service.
 *
 * @author Kevin Bond <kevinbond@gmail.com>
 */
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)]
final class SubscribedService
{
    /** @var object[] */
    public array $attributes;

    /**
     * @param string|null     $key        The key to use for the service
     * @param string|null     $type       The service type (a class name, "string", "iterable", etc.)
     * @param bool            $nullable   Whether the service is optional
     * @param object|object[] $attributes One or more dependency injection attributes to use
     */
    public function __construct(
        public ?string $key = null,
        public ?string $type = null,
        public bool $nullable = false,
        array|object $attributes = [],
    ) {
        $this->attributes = \is_array($attributes) ? $attributes : [$attributes];
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Contracts\Service;

use Psr\Container\ContainerInterface;

/**
 * Implemented by objects that expose a service container.
 */
interface ContainerProviderInterface
{
    public function getContainer(): ContainerInterface;
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Contracts\Service;

use Psr\Container\ContainerInterface;
use Symfony\Contracts\Service\Attribute\Required;
use Symfony\Contracts\Service\Attribute\SubscribedService;

/**
 * Implementation of ServiceSubscriberInterface that determines subscribed services
 * from methods that have the #[SubscribedService] attribute.
 *
 * Service ids are available as "ClassName::methodName" so that the implementation
 * of subscriber methods can be just `return $this->container->get(__METHOD__);`.
 *
 * @author Kevin Bond <kevinbond@gmail.com>
 */
trait ServiceMethodsSubscriberTrait
{
    protected ContainerInterface $container;

    public static function getSubscribedServices(): array
    {
        $services = method_exists(get_parent_class(self::class) ?: '', __FUNCTION__) ? parent::getSubscribedServices() : [];

        $reflectors = [
            ...(new \ReflectionClass(self::class))->getMethods(),
            ...(new \ReflectionClass(self::class))->getProperties(),
        ];

        foreach ($reflectors as $reflector) {
            if (self::class !== $reflector->class) {
                continue;
            }

            if (!$reflectionAttribute = $reflector->getAttributes(SubscribedService::class)[0] ?? null) {
                continue;
            }

            if ($reflector instanceof \ReflectionMethod) {
                if ($reflector->isStatic() || $reflector->isAbstract() || $reflector->isGenerator() || $reflector->isInternal() || $reflector->getNumberOfRequiredParameters()) {
                    throw new \LogicException(\sprintf('Cannot use "%s" on method "%s::%s()" (can only be used on non-static, non-abstract methods with no parameters).', SubscribedService::class, self::class, $reflector->name));
                }

                if (!$autowireType = $reflector->getReturnType()) {
                    throw new \LogicException(\sprintf('Cannot use "%s" on methods without a return type in "%s::%s()".', SubscribedService::class, $reflector->name, self::class));
                }

                $defaultKey = self::class.'::'.$reflector->name;
            } elseif ($reflector instanceof \ReflectionProperty) {
                if (\PHP_VERSION_ID < 80400 || !$reflector->hasHook(\PropertyHookType::Get)) {
                    throw new \LogicException(\sprintf('Cannot use "%s" on property "%s::$%s" (can only be used on properties with a get hook).', SubscribedService::class, self::class, $reflector->name));
                }

                if (!$autowireType = $reflector->getType()) {
                    throw new \LogicException(\sprintf('Cannot use "%s" on properties without a type in "%s::$%s".', SubscribedService::class, $reflector->name, self::class));
                }

                $defaultKey = self::class.'::$'.$reflector->name.'::get';
            } else {
                throw new \LogicException('Unexpected reflector: '.get_debug_type($reflector));
            }

            /* @var SubscribedService $attribute */
            $attribute = $reflectionAttribute->newInstance();
            $attribute->key ??= $defaultKey;
            $attribute->type ??= $autowireType instanceof \ReflectionNamedType ? $autowireType->getName() : (string) $autowireType;
            $attribute->nullable = $attribute->nullable ?: $autowireType->allowsNull();

            if ($attribute->attributes) {
                $services[] = $attribute;
            } else {
                $services[$attribute->key] = ($attribute->nullable ? '?' : '').$attribute->type;
            }
        }

        return $services;
    }

    #[Required]
    public function setContainer(ContainerInterface $container): ?ContainerInterface
    {
        $ret = null;
        if (method_exists(get_parent_class(self::class) ?: '', __FUNCTION__)) {
            $ret = parent::setContainer($container);
        }

        $this->container = $container;

        return $ret;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Contracts\Service;

use Symfony\Contracts\Service\Attribute\SubscribedService;

/**
 * A ServiceSubscriber exposes its dependencies via the static {@link getSubscribedServices} method.
 *
 * The getSubscribedServices method returns an array of service types required by such instances,
 * optionally keyed by the service names used internally. Service types that start with an interrogation
 * mark "?" are optional, while the other ones are mandatory service dependencies.
 *
 * The injected service locators SHOULD NOT allow access to any other services not specified by the method.
 *
 * It is expected that ServiceSubscriber instances consume PSR-11-based service locators internally.
 * This interface does not dictate any injection method for these service locators, although constructor
 * injection is recommended.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
interface ServiceSubscriberInterface
{
    /**
     * Returns an array of service types (or {@see SubscribedService} objects) required
     * by such instances, optionally keyed by the service names used internally.
     *
     * For mandatory dependencies:
     *
     *  * ['logger' => 'Psr\Log\LoggerInterface'] means the objects use the "logger" name
     *    internally to fetch a service which must implement Psr\Log\LoggerInterface.
     *  * ['loggers' => 'Psr\Log\LoggerInterface[]'] means the objects use the "loggers" name
     *    internally to fetch an iterable of Psr\Log\LoggerInterface instances.
     *  * ['Psr\Log\LoggerInterface'] is a shortcut for
     *  * ['Psr\Log\LoggerInterface' => 'Psr\Log\LoggerInterface']
     *
     * otherwise:
     *
     *  * ['logger' => '?Psr\Log\LoggerInterface'] denotes an optional dependency
     *  * ['loggers' => '?Psr\Log\LoggerInterface[]'] denotes an optional iterable dependency
     *  * ['?Psr\Log\LoggerInterface'] is a shortcut for
     *  * ['Psr\Log\LoggerInterface' => '?Psr\Log\LoggerInterface']
     *
     * additionally, an array of {@see SubscribedService}'s can be returned:
     *
     *  * [new SubscribedService('logger', Psr\Log\LoggerInterface::class)]
     *  * [new SubscribedService(type: Psr\Log\LoggerInterface::class, nullable: true)]
     *  * [new SubscribedService('http_client', HttpClientInterface::class, attributes: new Target('githubApi'))]
     *
     * @return string[]|SubscribedService[] The required service types, optionally keyed by service names
     */
    public static function getSubscribedServices(): array;
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Contracts\Service;

/**
 * A ServiceProviderInterface that is also countable and iterable.
 *
 * @author Kevin Bond <kevinbond@gmail.com>
 *
 * @template-covariant T of mixed
 *
 * @extends ServiceProviderInterface<T>
 * @extends \IteratorAggregate<string, T>
 */
interface ServiceCollectionInterface extends ServiceProviderInterface, \Countable, \IteratorAggregate
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Contracts\Service\Test;

use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;
use Symfony\Contracts\Service\ServiceLocatorTrait;

abstract class ServiceLocatorTestCase extends TestCase
{
    /**
     * @param array<string, callable> $factories
     */
    protected function getServiceLocator(array $factories): ContainerInterface
    {
        return new class($factories) implements ContainerInterface {
            use ServiceLocatorTrait;
        };
    }

    public function testHas()
    {
        $locator = $this->getServiceLocator([
            'foo' => static fn () => 'bar',
            'bar' => static fn () => 'baz',
            static fn () => 'dummy',
        ]);

        $this->assertTrue($locator->has('foo'));
        $this->assertTrue($locator->has('bar'));
        $this->assertFalse($locator->has('dummy'));
    }

    public function testGet()
    {
        $locator = $this->getServiceLocator([
            'foo' => static fn () => 'bar',
            'bar' => static fn () => 'baz',
        ]);

        $this->assertSame('bar', $locator->get('foo'));
        $this->assertSame('baz', $locator->get('bar'));
    }

    public function testGetDoesNotMemoize()
    {
        $i = 0;
        $locator = $this->getServiceLocator([
            'foo' => static function () use (&$i) {
                ++$i;

                return 'bar';
            },
        ]);

        $this->assertSame('bar', $locator->get('foo'));
        $this->assertSame('bar', $locator->get('foo'));
        $this->assertSame(2, $i);
    }

    public function testThrowsOnUndefinedInternalService()
    {
        $locator = $this->getServiceLocator([
            'foo' => static function () use (&$locator) { return $locator->get('bar'); },
        ]);

        $this->expectException(NotFoundExceptionInterface::class);
        $this->expectExceptionMessage('The service "foo" has a dependency on a non-existent service "bar". This locator only knows about the "foo" service.');

        $locator->get('foo');
    }

    public function testThrowsOnCircularReference()
    {
        $locator = $this->getServiceLocator([
            'foo' => static function () use (&$locator) { return $locator->get('bar'); },
            'bar' => static function () use (&$locator) { return $locator->get('baz'); },
            'baz' => static function () use (&$locator) { return $locator->get('bar'); },
        ]);

        $this->expectException(ContainerExceptionInterface::class);
        $this->expectExceptionMessage('Circular reference detected for service "bar", path: "bar -> baz -> bar".');

        $locator->get('foo');
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Contracts\Service\Test;

/**
 * @deprecated since PHPUnit 9.6
 */
class ServiceLocatorTest extends ServiceLocatorTestCase
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Contracts\Service;

use Psr\Container\ContainerInterface;
use Symfony\Contracts\Service\Attribute\Required;
use Symfony\Contracts\Service\Attribute\SubscribedService;

trigger_deprecation('symfony/contracts', 'v3.5', '"%s" is deprecated, use "ServiceMethodsSubscriberTrait" instead.', ServiceSubscriberTrait::class);

/**
 * Implementation of ServiceSubscriberInterface that determines subscribed services
 * from methods that have the #[SubscribedService] attribute.
 *
 * Service ids are available as "ClassName::methodName" so that the implementation
 * of subscriber methods can be just `return $this->container->get(__METHOD__);`.
 *
 * @property ContainerInterface $container
 *
 * @author Kevin Bond <kevinbond@gmail.com>
 *
 * @deprecated since symfony/contracts v3.5, use ServiceMethodsSubscriberTrait instead
 */
trait ServiceSubscriberTrait
{
    public static function getSubscribedServices(): array
    {
        $services = method_exists(get_parent_class(self::class) ?: '', __FUNCTION__) ? parent::getSubscribedServices() : [];

        foreach ((new \ReflectionClass(self::class))->getMethods() as $method) {
            if (self::class !== $method->class) {
                continue;
            }

            if (!$attribute = $method->getAttributes(SubscribedService::class)[0] ?? null) {
                continue;
            }

            if ($method->isStatic() || $method->isAbstract() || $method->isGenerator() || $method->isInternal() || $method->getNumberOfRequiredParameters()) {
                throw new \LogicException(\sprintf('Cannot use "%s" on method "%s::%s()" (can only be used on non-static, non-abstract methods with no parameters).', SubscribedService::class, self::class, $method->name));
            }

            if (!$returnType = $method->getReturnType()) {
                throw new \LogicException(\sprintf('Cannot use "%s" on methods without a return type in "%s::%s()".', SubscribedService::class, $method->name, self::class));
            }

            /** @var SubscribedService $attribute */
            $attribute = $attribute->newInstance();
            $attribute->key ??= self::class.'::'.$method->name;
            $attribute->type ??= $returnType instanceof \ReflectionNamedType ? $returnType->getName() : (string) $returnType;
            $attribute->nullable = $attribute->nullable ?: $returnType->allowsNull();

            if ($attribute->attributes) {
                $services[] = $attribute;
            } else {
                $services[$attribute->key] = ($attribute->nullable ? '?' : '').$attribute->type;
            }
        }

        return $services;
    }

    #[Required]
    public function setContainer(ContainerInterface $container): ?ContainerInterface
    {
        $ret = null;
        if (method_exists(get_parent_class(self::class) ?: '', __FUNCTION__)) {
            $ret = parent::setContainer($container);
        }

        $this->container = $container;

        return $ret;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Contracts\Service;

/**
 * Provides a way to reset an object to its initial state.
 *
 * When calling the "reset()" method on an object, it should be put back to its
 * initial state. This usually means clearing any internal buffers and forwarding
 * the call to internal dependencies. All properties of the object should be put
 * back to the same state it had when it was first ready to use.
 *
 * This method could be called, for example, to recycle objects that are used as
 * services, so that they can be used to handle several requests in the same
 * process loop (note that we advise making your services stateless instead of
 * implementing this interface when possible.)
 */
interface ResetInterface
{
    /**
     * @return void
     */
    public function reset();
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Contracts\EventDispatcher;

use Psr\EventDispatcher\EventDispatcherInterface as PsrEventDispatcherInterface;

/**
 * Allows providing hooks on domain-specific lifecycles by dispatching events.
 */
interface EventDispatcherInterface extends PsrEventDispatcherInterface
{
    /**
     * Dispatches an event to all registered listeners.
     *
     * @template T of object
     *
     * @param T           $event     The event to pass to the event handlers/listeners
     * @param string|null $eventName The name of the event to dispatch. If not supplied,
     *                               the class of $event should be used instead.
     *
     * @return T The passed $event MUST be returned
     */
    public function dispatch(object $event, ?string $eventName = null): object;
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Contracts\EventDispatcher;

use Psr\EventDispatcher\StoppableEventInterface;

/**
 * Event 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.
 *
 * You can call the method stopPropagation() to abort the execution of
 * further listeners in your event listener.
 *
 * @author Guilherme Blanco <guilhermeblanco@hotmail.com>
 * @author Jonathan Wage <jonwage@gmail.com>
 * @author Roman Borschel <roman@code-factory.org>
 * @author Bernhard Schussek <bschussek@gmail.com>
 * @author Nicolas Grekas <p@tchwork.com>
 */
class Event implements StoppableEventInterface
{
    private bool $propagationStopped = false;

    public function isPropagationStopped(): bool
    {
        return $this->propagationStopped;
    }

    /**
     * Stops the propagation of the event to further event listeners.
     *
     * If multiple event listeners are connected to the same event, no
     * further event listener will be triggered once any trigger calls
     * stopPropagation().
     */
    public function stopPropagation(): void
    {
        $this->propagationStopped = true;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

use Symfony\Polyfill\Mbstring as p;

if (!function_exists('mb_convert_encoding')) {
    function mb_convert_encoding(array|string|null $string, ?string $to_encoding, array|string|null $from_encoding = null): array|string|false { return p\Mbstring::mb_convert_encoding($string ?? '', (string) $to_encoding, $from_encoding); }
}
if (!function_exists('mb_decode_mimeheader')) {
    function mb_decode_mimeheader(?string $string): string { return p\Mbstring::mb_decode_mimeheader((string) $string); }
}
if (!function_exists('mb_encode_mimeheader')) {
    function mb_encode_mimeheader(?string $string, ?string $charset = null, ?string $transfer_encoding = null, ?string $newline = "\r\n", ?int $indent = 0): string { return p\Mbstring::mb_encode_mimeheader((string) $string, $charset, $transfer_encoding, (string) $newline, (int) $indent); }
}
if (!function_exists('mb_decode_numericentity')) {
    function mb_decode_numericentity(?string $string, array $map, ?string $encoding = null): string { return p\Mbstring::mb_decode_numericentity((string) $string, $map, $encoding); }
}
if (!function_exists('mb_encode_numericentity')) {
    function mb_encode_numericentity(?string $string, array $map, ?string $encoding = null, ?bool $hex = false): string { return p\Mbstring::mb_encode_numericentity((string) $string, $map, $encoding, (bool) $hex); }
}
if (!function_exists('mb_convert_case')) {
    function mb_convert_case(?string $string, ?int $mode, ?string $encoding = null): string { return p\Mbstring::mb_convert_case((string) $string, (int) $mode, $encoding); }
}
if (!function_exists('mb_internal_encoding')) {
    function mb_internal_encoding(?string $encoding = null): string|bool { return p\Mbstring::mb_internal_encoding($encoding); }
}
if (!function_exists('mb_language')) {
    function mb_language(?string $language = null): string|bool { return p\Mbstring::mb_language($language); }
}
if (!function_exists('mb_list_encodings')) {
    function mb_list_encodings(): array { return p\Mbstring::mb_list_encodings(); }
}
if (!function_exists('mb_encoding_aliases')) {
    function mb_encoding_aliases(?string $encoding): array { return p\Mbstring::mb_encoding_aliases((string) $encoding); }
}
if (!function_exists('mb_check_encoding')) {
    function mb_check_encoding(array|string|null $value = null, ?string $encoding = null): bool { return p\Mbstring::mb_check_encoding($value, $encoding); }
}
if (!function_exists('mb_detect_encoding')) {
    function mb_detect_encoding(?string $string, array|string|null $encodings = null, ?bool $strict = false): string|false { return p\Mbstring::mb_detect_encoding((string) $string, $encodings, (bool) $strict); }
}
if (!function_exists('mb_detect_order')) {
    function mb_detect_order(array|string|null $encoding = null): array|bool { return p\Mbstring::mb_detect_order($encoding); }
}
if (!function_exists('mb_parse_str')) {
    function mb_parse_str(?string $string, &$result = []): bool { parse_str((string) $string, $result); return (bool) $result; }
}
if (!function_exists('mb_strlen')) {
    function mb_strlen(?string $string, ?string $encoding = null): int { return p\Mbstring::mb_strlen((string) $string, $encoding); }
}
if (!function_exists('mb_strpos')) {
    function mb_strpos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $encoding = null): int|false { return p\Mbstring::mb_strpos((string) $haystack, (string) $needle, (int) $offset, $encoding); }
}
if (!function_exists('mb_strtolower')) {
    function mb_strtolower(?string $string, ?string $encoding = null): string { return p\Mbstring::mb_strtolower((string) $string, $encoding); }
}
if (!function_exists('mb_strtoupper')) {
    function mb_strtoupper(?string $string, ?string $encoding = null): string { return p\Mbstring::mb_strtoupper((string) $string, $encoding); }
}
if (!function_exists('mb_substitute_character')) {
    function mb_substitute_character(string|int|null $substitute_character = null): string|int|bool { return p\Mbstring::mb_substitute_character($substitute_character); }
}
if (!function_exists('mb_substr')) {
    function mb_substr(?string $string, ?int $start, ?int $length = null, ?string $encoding = null): string { return p\Mbstring::mb_substr((string) $string, (int) $start, $length, $encoding); }
}
if (!function_exists('mb_stripos')) {
    function mb_stripos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $encoding = null): int|false { return p\Mbstring::mb_stripos((string) $haystack, (string) $needle, (int) $offset, $encoding); }
}
if (!function_exists('mb_stristr')) {
    function mb_stristr(?string $haystack, ?string $needle, ?bool $before_needle = false, ?string $encoding = null): string|false { return p\Mbstring::mb_stristr((string) $haystack, (string) $needle, (bool) $before_needle, $encoding); }
}
if (!function_exists('mb_strrchr')) {
    function mb_strrchr(?string $haystack, ?string $needle, ?bool $before_needle = false, ?string $encoding = null): string|false { return p\Mbstring::mb_strrchr((string) $haystack, (string) $needle, (bool) $before_needle, $encoding); }
}
if (!function_exists('mb_strrichr')) {
    function mb_strrichr(?string $haystack, ?string $needle, ?bool $before_needle = false, ?string $encoding = null): string|false { return p\Mbstring::mb_strrichr((string) $haystack, (string) $needle, (bool) $before_needle, $encoding); }
}
if (!function_exists('mb_strripos')) {
    function mb_strripos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $encoding = null): int|false { return p\Mbstring::mb_strripos((string) $haystack, (string) $needle, (int) $offset, $encoding); }
}
if (!function_exists('mb_strrpos')) {
    function mb_strrpos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $encoding = null): int|false { return p\Mbstring::mb_strrpos((string) $haystack, (string) $needle, (int) $offset, $encoding); }
}
if (!function_exists('mb_strstr')) {
    function mb_strstr(?string $haystack, ?string $needle, ?bool $before_needle = false, ?string $encoding = null): string|false { return p\Mbstring::mb_strstr((string) $haystack, (string) $needle, (bool) $before_needle, $encoding); }
}
if (!function_exists('mb_get_info')) {
    function mb_get_info(?string $type = 'all'): array|string|int|false|null { return p\Mbstring::mb_get_info((string) $type); }
}
if (!function_exists('mb_http_output')) {
    function mb_http_output(?string $encoding = null): string|bool { return p\Mbstring::mb_http_output($encoding); }
}
if (!function_exists('mb_strwidth')) {
    function mb_strwidth(?string $string, ?string $encoding = null): int { return p\Mbstring::mb_strwidth((string) $string, $encoding); }
}
if (!function_exists('mb_substr_count')) {
    function mb_substr_count(?string $haystack, ?string $needle, ?string $encoding = null): int { return p\Mbstring::mb_substr_count((string) $haystack, (string) $needle, $encoding); }
}
if (!function_exists('mb_output_handler')) {
    function mb_output_handler(?string $string, ?int $status): string { return p\Mbstring::mb_output_handler((string) $string, (int) $status); }
}
if (!function_exists('mb_http_input')) {
    function mb_http_input(?string $type = null): array|string|false { return p\Mbstring::mb_http_input($type); }
}

if (!function_exists('mb_convert_variables')) {
    function mb_convert_variables(?string $to_encoding, array|string|null $from_encoding, mixed &$var, mixed &...$vars): string|false { return p\Mbstring::mb_convert_variables((string) $to_encoding, $from_encoding ?? '', $var, ...$vars); }
}

if (!function_exists('mb_ord')) {
    function mb_ord(?string $string, ?string $encoding = null): int|false { return p\Mbstring::mb_ord((string) $string, $encoding); }
}
if (!function_exists('mb_chr')) {
    function mb_chr(?int $codepoint, ?string $encoding = null): string|false { return p\Mbstring::mb_chr((int) $codepoint, $encoding); }
}
if (!function_exists('mb_scrub')) {
    function mb_scrub(?string $string, ?string $encoding = null): string { return p\Mbstring::mb_scrub($string, $encoding); }
}
if (!function_exists('mb_str_split')) {
    function mb_str_split(?string $string, ?int $length = 1, ?string $encoding = null): array { return p\Mbstring::mb_str_split((string) $string, (int) $length, $encoding); }
}

if (!function_exists('mb_str_pad')) {
    function mb_str_pad(?string $string, ?int $length, ?string $pad_string = ' ', ?int $pad_type = STR_PAD_RIGHT, ?string $encoding = null): string { return p\Mbstring::mb_str_pad((string) $string, (int) $length, (string) $pad_string, (int) $pad_type, $encoding); }
}

if (!function_exists('mb_ucfirst')) {
    function mb_ucfirst(?string $string, ?string $encoding = null): string { return p\Mbstring::mb_ucfirst((string) $string, $encoding); }
}

if (!function_exists('mb_lcfirst')) {
    function mb_lcfirst(?string $string, ?string $encoding = null): string { return p\Mbstring::mb_lcfirst((string) $string, $encoding); }
}

if (!function_exists('mb_trim')) {
    function mb_trim(?string $string, ?string $characters = null, ?string $encoding = null): string { return p\Mbstring::mb_trim((string) $string, $characters, $encoding); }
}

if (!function_exists('mb_ltrim')) {
    function mb_ltrim(?string $string, ?string $characters = null, ?string $encoding = null): string { return p\Mbstring::mb_ltrim((string) $string, $characters, $encoding); }
}

if (!function_exists('mb_rtrim')) {
    function mb_rtrim(?string $string, ?string $characters = null, ?string $encoding = null): string { return p\Mbstring::mb_rtrim((string) $string, $characters, $encoding); }
}

if (extension_loaded('mbstring')) {
    return;
}

if (!defined('MB_CASE_UPPER')) {
    define('MB_CASE_UPPER', 0);
}
if (!defined('MB_CASE_LOWER')) {
    define('MB_CASE_LOWER', 1);
}
if (!defined('MB_CASE_TITLE')) {
    define('MB_CASE_TITLE', 2);
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

if (\PHP_VERSION_ID >= 80000) {
    return require __DIR__.'/bootstrap80.php';
}

return require __DIR__.'/bootstrap72.php';
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

use Symfony\Polyfill\Mbstring as p;

if (!function_exists('mb_convert_encoding')) {
    function mb_convert_encoding($string, $to_encoding, $from_encoding = null) { return p\Mbstring::mb_convert_encoding($string, $to_encoding, $from_encoding); }
}
if (!function_exists('mb_decode_mimeheader')) {
    function mb_decode_mimeheader($string) { return p\Mbstring::mb_decode_mimeheader($string); }
}
if (!function_exists('mb_encode_mimeheader')) {
    function mb_encode_mimeheader($string, $charset = null, $transfer_encoding = null, $newline = "\r\n", $indent = 0) { return p\Mbstring::mb_encode_mimeheader($string, $charset, $transfer_encoding, $newline, $indent); }
}
if (!function_exists('mb_decode_numericentity')) {
    function mb_decode_numericentity($string, $map, $encoding = null) { return p\Mbstring::mb_decode_numericentity($string, $map, $encoding); }
}
if (!function_exists('mb_encode_numericentity')) {
    function mb_encode_numericentity($string, $map, $encoding = null, $hex = false) { return p\Mbstring::mb_encode_numericentity($string, $map, $encoding, $hex); }
}
if (!function_exists('mb_convert_case')) {
    function mb_convert_case($string, $mode, $encoding = null) { return p\Mbstring::mb_convert_case($string, $mode, $encoding); }
}
if (!function_exists('mb_internal_encoding')) {
    function mb_internal_encoding($encoding = null) { return p\Mbstring::mb_internal_encoding($encoding); }
}
if (!function_exists('mb_language')) {
    function mb_language($language = null) { return p\Mbstring::mb_language($language); }
}
if (!function_exists('mb_list_encodings')) {
    function mb_list_encodings() { return p\Mbstring::mb_list_encodings(); }
}
if (!function_exists('mb_encoding_aliases')) {
    function mb_encoding_aliases($encoding) { return p\Mbstring::mb_encoding_aliases($encoding); }
}
if (!function_exists('mb_check_encoding')) {
    function mb_check_encoding($value = null, $encoding = null) { return p\Mbstring::mb_check_encoding($value, $encoding); }
}
if (!function_exists('mb_detect_encoding')) {
    function mb_detect_encoding($string, $encodings = null, $strict = false) { return p\Mbstring::mb_detect_encoding($string, $encodings, $strict); }
}
if (!function_exists('mb_detect_order')) {
    function mb_detect_order($encoding = null) { return p\Mbstring::mb_detect_order($encoding); }
}
if (!function_exists('mb_parse_str')) {
    function mb_parse_str($string, &$result = []) { parse_str($string, $result); return (bool) $result; }
}
if (!function_exists('mb_strlen')) {
    function mb_strlen($string, $encoding = null) { return p\Mbstring::mb_strlen($string, $encoding); }
}
if (!function_exists('mb_strpos')) {
    function mb_strpos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strpos($haystack, $needle, $offset, $encoding); }
}
if (!function_exists('mb_strtolower')) {
    function mb_strtolower($string, $encoding = null) { return p\Mbstring::mb_strtolower($string, $encoding); }
}
if (!function_exists('mb_strtoupper')) {
    function mb_strtoupper($string, $encoding = null) { return p\Mbstring::mb_strtoupper($string, $encoding); }
}
if (!function_exists('mb_substitute_character')) {
    function mb_substitute_character($substitute_character = null) { return p\Mbstring::mb_substitute_character($substitute_character); }
}
if (!function_exists('mb_substr')) {
    function mb_substr($string, $start, $length = 2147483647, $encoding = null) { return p\Mbstring::mb_substr($string, $start, $length, $encoding); }
}
if (!function_exists('mb_stripos')) {
    function mb_stripos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_stripos($haystack, $needle, $offset, $encoding); }
}
if (!function_exists('mb_stristr')) {
    function mb_stristr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_stristr($haystack, $needle, $before_needle, $encoding); }
}
if (!function_exists('mb_strrchr')) {
    function mb_strrchr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strrchr($haystack, $needle, $before_needle, $encoding); }
}
if (!function_exists('mb_strrichr')) {
    function mb_strrichr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strrichr($haystack, $needle, $before_needle, $encoding); }
}
if (!function_exists('mb_strripos')) {
    function mb_strripos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strripos($haystack, $needle, $offset, $encoding); }
}
if (!function_exists('mb_strrpos')) {
    function mb_strrpos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strrpos($haystack, $needle, $offset, $encoding); }
}
if (!function_exists('mb_strstr')) {
    function mb_strstr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strstr($haystack, $needle, $before_needle, $encoding); }
}
if (!function_exists('mb_get_info')) {
    function mb_get_info($type = 'all') { return p\Mbstring::mb_get_info($type); }
}
if (!function_exists('mb_http_output')) {
    function mb_http_output($encoding = null) { return p\Mbstring::mb_http_output($encoding); }
}
if (!function_exists('mb_strwidth')) {
    function mb_strwidth($string, $encoding = null) { return p\Mbstring::mb_strwidth($string, $encoding); }
}
if (!function_exists('mb_substr_count')) {
    function mb_substr_count($haystack, $needle, $encoding = null) { return p\Mbstring::mb_substr_count($haystack, $needle, $encoding); }
}
if (!function_exists('mb_output_handler')) {
    function mb_output_handler($string, $status) { return p\Mbstring::mb_output_handler($string, $status); }
}
if (!function_exists('mb_http_input')) {
    function mb_http_input($type = null) { return p\Mbstring::mb_http_input($type); }
}

if (!function_exists('mb_convert_variables')) {
    function mb_convert_variables($to_encoding, $from_encoding, &...$vars) { return p\Mbstring::mb_convert_variables($to_encoding, $from_encoding, ...$vars); }
}

if (!function_exists('mb_ord')) {
    function mb_ord($string, $encoding = null) { return p\Mbstring::mb_ord($string, $encoding); }
}
if (!function_exists('mb_chr')) {
    function mb_chr($codepoint, $encoding = null) { return p\Mbstring::mb_chr($codepoint, $encoding); }
}
if (!function_exists('mb_scrub')) {
    function mb_scrub($string, $encoding = null) { return p\Mbstring::mb_scrub($string, $encoding); }
}
if (!function_exists('mb_str_split')) {
    function mb_str_split($string, $length = 1, $encoding = null) { return p\Mbstring::mb_str_split($string, $length, $encoding); }
}

if (!function_exists('mb_str_pad')) {
    /** @return string|false */
    function mb_str_pad(?string $string, ?int $length, ?string $pad_string = ' ', ?int $pad_type = STR_PAD_RIGHT, ?string $encoding = null) { return p\Mbstring::mb_str_pad((string) $string, (int) $length, (string) $pad_string, (int) $pad_type, $encoding); }
}

if (!function_exists('mb_ucfirst')) {
    /** @return string|false */
    function mb_ucfirst(?string $string, ?string $encoding = null) { return p\Mbstring::mb_ucfirst((string) $string, $encoding); }
}

if (!function_exists('mb_lcfirst')) {
    /** @return string|false */
    function mb_lcfirst(?string $string, ?string $encoding = null) { return p\Mbstring::mb_lcfirst((string) $string, $encoding); }
}

if (!function_exists('mb_trim')) {
    /** @return string|false */
    function mb_trim(?string $string, ?string $characters = null, ?string $encoding = null) { return p\Mbstring::mb_trim((string) $string, $characters, $encoding); }
}

if (!function_exists('mb_ltrim')) {
    /** @return string|false */
    function mb_ltrim(?string $string, ?string $characters = null, ?string $encoding = null) { return p\Mbstring::mb_ltrim((string) $string, $characters, $encoding); }
}

if (!function_exists('mb_rtrim')) {
    /** @return string|false */
    function mb_rtrim(?string $string, ?string $characters = null, ?string $encoding = null) { return p\Mbstring::mb_rtrim((string) $string, $characters, $encoding); }
}

if (extension_loaded('mbstring')) {
    return;
}

if (!defined('MB_CASE_UPPER')) {
    define('MB_CASE_UPPER', 0);
}
if (!defined('MB_CASE_LOWER')) {
    define('MB_CASE_LOWER', 1);
}
if (!defined('MB_CASE_TITLE')) {
    define('MB_CASE_TITLE', 2);
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Polyfill\Mbstring;

/**
 * Partial mbstring implementation in PHP, iconv based, UTF-8 centric.
 *
 * Implemented:
 * - mb_chr                  - Returns a specific character from its Unicode code point
 * - mb_convert_encoding     - Convert character encoding
 * - mb_convert_variables    - Convert character code in variable(s)
 * - mb_decode_mimeheader    - Decode string in MIME header field
 * - mb_encode_mimeheader    - Encode string for MIME header XXX NATIVE IMPLEMENTATION IS REALLY BUGGED
 * - mb_decode_numericentity - Decode HTML numeric string reference to character
 * - mb_encode_numericentity - Encode character to HTML numeric string reference
 * - mb_convert_case         - Perform case folding on a string
 * - mb_detect_encoding      - Detect character encoding
 * - mb_get_info             - Get internal settings of mbstring
 * - mb_http_input           - Detect HTTP input character encoding
 * - mb_http_output          - Set/Get HTTP output character encoding
 * - mb_internal_encoding    - Set/Get internal character encoding
 * - mb_list_encodings       - Returns an array of all supported encodings
 * - mb_ord                  - Returns the Unicode code point of a character
 * - mb_output_handler       - Callback function converts character encoding in output buffer
 * - mb_scrub                - Replaces ill-formed byte sequences with substitute characters
 * - mb_strlen               - Get string length
 * - mb_strpos               - Find position of first occurrence of string in a string
 * - mb_strrpos              - Find position of last occurrence of a string in a string
 * - mb_str_split            - Convert a string to an array
 * - mb_strtolower           - Make a string lowercase
 * - mb_strtoupper           - Make a string uppercase
 * - mb_substitute_character - Set/Get substitution character
 * - mb_substr               - Get part of string
 * - mb_stripos              - Finds position of first occurrence of a string within another, case insensitive
 * - mb_stristr              - Finds first occurrence of a string within another, case insensitive
 * - mb_strrchr              - Finds the last occurrence of a character in a string within another
 * - mb_strrichr             - Finds the last occurrence of a character in a string within another, case insensitive
 * - mb_strripos             - Finds position of last occurrence of a string within another, case insensitive
 * - mb_strstr               - Finds first occurrence of a string within another
 * - mb_strwidth             - Return width of string
 * - mb_substr_count         - Count the number of substring occurrences
 * - mb_ucfirst              - Make a string's first character uppercase
 * - mb_lcfirst              - Make a string's first character lowercase
 * - mb_trim                 - Strip whitespace (or other characters) from the beginning and end of a string
 * - mb_ltrim                - Strip whitespace (or other characters) from the beginning of a string
 * - mb_rtrim                - Strip whitespace (or other characters) from the end of a string
 *
 * Not implemented:
 * - mb_convert_kana         - Convert "kana" one from another ("zen-kaku", "han-kaku" and more)
 * - mb_ereg_*               - Regular expression with multibyte support
 * - mb_parse_str            - Parse GET/POST/COOKIE data and set global variable
 * - mb_preferred_mime_name  - Get MIME charset string
 * - mb_regex_encoding       - Returns current encoding for multibyte regex as string
 * - mb_regex_set_options    - Set/Get the default options for mbregex functions
 * - mb_send_mail            - Send encoded mail
 * - mb_split                - Split multibyte string using regular expression
 * - mb_strcut               - Get part of string
 * - mb_strimwidth           - Get truncated string with specified width
 *
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @internal
 */
final class Mbstring
{
    public const MB_CASE_FOLD = \PHP_INT_MAX;

    private const SIMPLE_CASE_FOLD = [
        ['µ', 'ſ', "\xCD\x85", 'ς', "\xCF\x90", "\xCF\x91", "\xCF\x95", "\xCF\x96", "\xCF\xB0", "\xCF\xB1", "\xCF\xB5", "\xE1\xBA\x9B", "\xE1\xBE\xBE"],
        ['μ', 's', 'ι',        'σ', 'β',        'θ',        'φ',        'π',        'κ',        'ρ',        'ε',        "\xE1\xB9\xA1", 'ι'],
    ];

    private static $encodingList = ['ASCII', 'UTF-8'];
    private static $language = 'neutral';
    private static $internalEncoding = 'UTF-8';
    private static $iconvSupportsIgnore;

    public static function mb_convert_encoding($s, $toEncoding, $fromEncoding = null)
    {
        if (\is_array($s)) {
            $r = [];
            foreach ($s as $str) {
                $r[] = self::mb_convert_encoding($str, $toEncoding, $fromEncoding);
            }

            return $r;
        }

        if (\is_array($fromEncoding) || (null !== $fromEncoding && false !== strpos($fromEncoding, ','))) {
            $fromEncoding = self::mb_detect_encoding($s, $fromEncoding);
        } else {
            $fromEncoding = self::getEncoding($fromEncoding);
        }

        $toEncoding = self::getEncoding($toEncoding);

        if ('BASE64' === $fromEncoding) {
            $s = base64_decode($s);
            $fromEncoding = $toEncoding;
        }

        if ('BASE64' === $toEncoding) {
            return base64_encode($s);
        }

        if ('HTML-ENTITIES' === $toEncoding || 'HTML' === $toEncoding) {
            if ('HTML-ENTITIES' === $fromEncoding || 'HTML' === $fromEncoding) {
                $fromEncoding = 'Windows-1252';
            }
            if ('UTF-8' !== $fromEncoding) {
                $s = self::iconv($fromEncoding, 'UTF-8', $s);
            }

            return preg_replace_callback('/[\x80-\xFF]+/', [__CLASS__, 'html_encoding_callback'], $s);
        }

        if ('HTML-ENTITIES' === $fromEncoding) {
            $decodeControlChars = static function ($m) {
                $code = '' !== ($m[2] ?? '') ? hexdec($m[2]) : (int) $m[1];

                if ($code < 32 || 127 === $code) {
                    return \chr($code);
                }
                if (128 <= $code && $code <= 159) {
                    return "\xC2".\chr(0x80 | ($code & 0x3F));
                }

                return $m[0];
            };

            if (\PHP_VERSION_ID >= 70400) {
                $s = html_entity_decode($s, \ENT_QUOTES, 'UTF-8');
                // html_entity_decode() leaves numeric entities for C0/C1 control
                // characters as-is (HTML spec), but mb_convert_encoding() decodes
                // them. Catch what html_entity_decode() missed.
                if (false !== strpos($s, '&#')) {
                    $s = preg_replace_callback('/&#(?:0*([0-9]++)|[xX]0*([0-9a-fA-F]++));/', $decodeControlChars, $s);
                }
            } else {
                // PHP < 7.4: html_entity_decode() truncates strings at NUL bytes,
                // so decode the control character entities first then call
                // html_entity_decode() on each NUL-delimited chunk independently.
                $s = preg_replace_callback('/&#(?:0*([0-9]++)|[xX]0*([0-9a-fA-F]++));/', $decodeControlChars, $s);
                $s = implode("\0", array_map(static function ($chunk) {
                    return html_entity_decode($chunk, \ENT_QUOTES, 'UTF-8');
                }, explode("\0", $s)));
            }
            $fromEncoding = 'UTF-8';
        }

        return self::iconv($fromEncoding, $toEncoding, $s);
    }

    public static function mb_convert_variables($toEncoding, $fromEncoding, &...$vars)
    {
        $ok = true;
        array_walk_recursive($vars, static function (&$v) use (&$ok, $toEncoding, $fromEncoding) {
            if (false === $v = self::mb_convert_encoding($v, $toEncoding, $fromEncoding)) {
                $ok = false;
            }
        });

        return $ok ? $fromEncoding : false;
    }

    public static function mb_decode_mimeheader($s)
    {
        return iconv_mime_decode($s, 2, self::$internalEncoding);
    }

    public static function mb_encode_mimeheader($s, $charset = null, $transferEncoding = null, $linefeed = null, $indent = null)
    {
        trigger_error('mb_encode_mimeheader() is bugged. Please use iconv_mime_encode() instead', \E_USER_WARNING);
    }

    public static function mb_decode_numericentity($s, $convmap, $encoding = null)
    {
        if (null !== $s && !\is_scalar($s) && !(\is_object($s) && method_exists($s, '__toString'))) {
            trigger_error('mb_decode_numericentity() expects parameter 1 to be string, '.\gettype($s).' given', \E_USER_WARNING);

            return null;
        }

        if (!\is_array($convmap) || (80000 > \PHP_VERSION_ID && !$convmap)) {
            return false;
        }

        if (null !== $encoding && !\is_scalar($encoding)) {
            trigger_error('mb_decode_numericentity() expects parameter 3 to be string, '.\gettype($s).' given', \E_USER_WARNING);

            return '';  // Instead of null (cf. mb_encode_numericentity).
        }

        $s = (string) $s;
        if ('' === $s) {
            return '';
        }

        $encoding = self::getEncoding($encoding);

        if ('UTF-8' === $encoding) {
            $encoding = null;
            if (!preg_match('//u', $s)) {
                $s = @self::iconv('UTF-8', 'UTF-8', $s);
            }
        } else {
            $s = self::iconv($encoding, 'UTF-8', $s);
        }

        $cnt = floor(\count($convmap) / 4) * 4;

        for ($i = 0; $i < $cnt; $i += 4) {
            // collector_decode_htmlnumericentity ignores $convmap[$i + 3]
            $convmap[$i] += $convmap[$i + 2];
            $convmap[$i + 1] += $convmap[$i + 2];
        }

        $s = preg_replace_callback('/&#(?:0*([0-9]+)|x0*([0-9a-fA-F]+))'.(\PHP_VERSION_ID >= 80200 ? '' : '(?!&)').';?/', static function (array $m) use ($cnt, $convmap) {
            $c = isset($m[2]) ? (int) hexdec($m[2]) : $m[1];
            for ($i = 0; $i < $cnt; $i += 4) {
                if ($c >= $convmap[$i] && $c <= $convmap[$i + 1]) {
                    return self::mb_chr($c - $convmap[$i + 2]);
                }
            }

            return $m[0];
        }, $s);

        if (null === $encoding) {
            return $s;
        }

        return self::iconv('UTF-8', $encoding, $s);
    }

    public static function mb_encode_numericentity($s, $convmap, $encoding = null, $is_hex = false)
    {
        if (null !== $s && !\is_scalar($s) && !(\is_object($s) && method_exists($s, '__toString'))) {
            trigger_error('mb_encode_numericentity() expects parameter 1 to be string, '.\gettype($s).' given', \E_USER_WARNING);

            return null;
        }

        if (!\is_array($convmap) || (80000 > \PHP_VERSION_ID && !$convmap)) {
            return false;
        }

        if (null !== $encoding && !\is_scalar($encoding)) {
            trigger_error('mb_encode_numericentity() expects parameter 3 to be string, '.\gettype($s).' given', \E_USER_WARNING);

            return null;  // Instead of '' (cf. mb_decode_numericentity).
        }

        if (null !== $is_hex && !\is_scalar($is_hex)) {
            trigger_error('mb_encode_numericentity() expects parameter 4 to be boolean, '.\gettype($s).' given', \E_USER_WARNING);

            return null;
        }

        $s = (string) $s;
        if ('' === $s) {
            return '';
        }

        $encoding = self::getEncoding($encoding);

        if ('UTF-8' === $encoding) {
            $encoding = null;
            if (!preg_match('//u', $s)) {
                $s = @self::iconv('UTF-8', 'UTF-8', $s);
            }
        } else {
            $s = self::iconv($encoding, 'UTF-8', $s);
        }

        static $ulenMask = ["\xC0" => 2, "\xD0" => 2, "\xE0" => 3, "\xF0" => 4];

        $cnt = floor(\count($convmap) / 4) * 4;
        $i = 0;
        $len = \strlen($s);
        $result = '';

        while ($i < $len) {
            $ulen = $s[$i] < "\x80" ? 1 : $ulenMask[$s[$i] & "\xF0"];
            $uchr = substr($s, $i, $ulen);
            $i += $ulen;
            $c = self::mb_ord($uchr);

            for ($j = 0; $j < $cnt; $j += 4) {
                if ($c >= $convmap[$j] && $c <= $convmap[$j + 1]) {
                    $cOffset = ($c + $convmap[$j + 2]) & $convmap[$j + 3];
                    $result .= $is_hex ? \sprintf('&#x%X;', $cOffset) : '&#'.$cOffset.';';
                    continue 2;
                }
            }
            $result .= $uchr;
        }

        if (null === $encoding) {
            return $result;
        }

        return self::iconv('UTF-8', $encoding, $result);
    }

    public static function mb_convert_case($s, $mode, $encoding = null)
    {
        $s = (string) $s;
        if ('' === $s) {
            return '';
        }

        $encoding = self::getEncoding($encoding);

        if ('UTF-8' === $encoding) {
            $encoding = null;
            if (!preg_match('//u', $s)) {
                $s = @self::iconv('UTF-8', 'UTF-8', $s);
            }
        } else {
            $s = self::iconv($encoding, 'UTF-8', $s);
        }

        if (\MB_CASE_TITLE == $mode) {
            static $titleRegexp = null;
            if (null === $titleRegexp) {
                $titleRegexp = self::getData('titleCaseRegexp');
            }
            $s = preg_replace_callback($titleRegexp, [__CLASS__, 'title_case'], $s);
        } else {
            if (\MB_CASE_UPPER == $mode) {
                static $upper = null;
                if (null === $upper) {
                    $upper = self::getData('upperCase');
                }
                $map = $upper;
            } else {
                if (self::MB_CASE_FOLD === $mode) {
                    static $caseFolding = null;
                    if (null === $caseFolding) {
                        $caseFolding = self::getData('caseFolding');
                    }
                    $s = strtr($s, $caseFolding);
                }

                static $lower = null;
                if (null === $lower) {
                    $lower = self::getData('lowerCase');
                }
                $map = $lower;
            }

            static $ulenMask = ["\xC0" => 2, "\xD0" => 2, "\xE0" => 3, "\xF0" => 4];

            $i = 0;
            $len = \strlen($s);

            while ($i < $len) {
                $ulen = $s[$i] < "\x80" ? 1 : $ulenMask[$s[$i] & "\xF0"];
                $uchr = substr($s, $i, $ulen);
                $i += $ulen;

                if (isset($map[$uchr])) {
                    $uchr = $map[$uchr];
                    $nlen = \strlen($uchr);

                    if ($nlen == $ulen) {
                        $nlen = $i;
                        do {
                            $s[--$nlen] = $uchr[--$ulen];
                        } while ($ulen);
                    } else {
                        $s = substr_replace($s, $uchr, $i - $ulen, $ulen);
                        $len += $nlen - $ulen;
                        $i += $nlen - $ulen;
                    }
                }
            }
        }

        if (null === $encoding) {
            return $s;
        }

        return self::iconv('UTF-8', $encoding, $s);
    }

    public static function mb_internal_encoding($encoding = null)
    {
        if (null === $encoding) {
            return self::$internalEncoding;
        }

        $normalizedEncoding = self::getEncoding($encoding);

        if ('UTF-8' === $normalizedEncoding || false !== @iconv($normalizedEncoding, $normalizedEncoding, ' ')) {
            self::$internalEncoding = $normalizedEncoding;

            return true;
        }

        if (80000 > \PHP_VERSION_ID) {
            return false;
        }

        throw new \ValueError(\sprintf('Argument #1 ($encoding) must be a valid encoding, "%s" given', $encoding));
    }

    public static function mb_language($lang = null)
    {
        if (null === $lang) {
            return self::$language;
        }

        switch ($normalizedLang = strtolower($lang)) {
            case 'uni':
            case 'neutral':
                self::$language = $normalizedLang;

                return true;
        }

        if (80000 > \PHP_VERSION_ID) {
            return false;
        }

        throw new \ValueError(\sprintf('Argument #1 ($language) must be a valid language, "%s" given', $lang));
    }

    public static function mb_list_encodings()
    {
        return ['UTF-8'];
    }

    public static function mb_encoding_aliases($encoding)
    {
        switch (strtoupper($encoding)) {
            case 'UTF8':
            case 'UTF-8':
                return ['utf8'];
        }

        return false;
    }

    public static function mb_check_encoding($var = null, $encoding = null)
    {
        if (null === $encoding) {
            if (null === $var) {
                return false;
            }
            $encoding = self::$internalEncoding;
        }

        if (!\is_array($var)) {
            return self::mb_detect_encoding($var, [$encoding]) || false !== @iconv($encoding, $encoding, $var);
        }

        foreach ($var as $key => $value) {
            if (!self::mb_check_encoding($key, $encoding)) {
                return false;
            }
            if (!self::mb_check_encoding($value, $encoding)) {
                return false;
            }
        }

        return true;
    }

    public static function mb_detect_encoding($str, $encodingList = null, $strict = false)
    {
        if (null === $encodingList) {
            $encodingList = self::$encodingList;
        } else {
            if (!\is_array($encodingList)) {
                $encodingList = array_map('trim', explode(',', $encodingList));
            }
            $encodingList = array_map('strtoupper', $encodingList);
        }

        foreach ($encodingList as $enc) {
            switch ($enc) {
                case 'ASCII':
                    if (!preg_match('/[\x80-\xFF]/', $str)) {
                        return $enc;
                    }
                    break;

                case 'UTF8':
                case 'UTF-8':
                    if (preg_match('//u', $str)) {
                        return 'UTF-8';
                    }
                    break;

                default:
                    if (0 === strncmp($enc, 'ISO-8859-', 9)) {
                        return $enc;
                    }
            }
        }

        return false;
    }

    public static function mb_detect_order($encodingList = null)
    {
        if (null === $encodingList) {
            return self::$encodingList;
        }

        if (!\is_array($encodingList)) {
            $encodingList = array_map('trim', explode(',', $encodingList));
        }
        $encodingList = array_map('strtoupper', $encodingList);

        foreach ($encodingList as $enc) {
            switch ($enc) {
                default:
                    if (strncmp($enc, 'ISO-8859-', 9)) {
                        return false;
                    }
                    // no break
                case 'ASCII':
                case 'UTF8':
                case 'UTF-8':
            }
        }

        self::$encodingList = $encodingList;

        return true;
    }

    public static function mb_strlen($s, $encoding = null)
    {
        $encoding = self::getEncoding($encoding);
        if ('CP850' === $encoding || 'ASCII' === $encoding) {
            return \strlen($s);
        }

        if (false !== $len = @iconv_strlen($s, $encoding)) {
            return $len;
        }

        if ('UTF-8' !== $encoding) {
            return $len;
        }

        return preg_match_all('/[\x00-\x7F]|[\xC0-\xDF][\x80-\xBF]?|[\xE0-\xEF][\x80-\xBF]{0,2}|[\xF0-\xF7][\x80-\xBF]{0,3}|[\xF8-\xFB][\x80-\xBF]{0,4}|[\xFC-\xFD][\x80-\xBF]{0,5}|[\x80-\xBF\xFE\xFF]/s', $s);
    }

    public static function mb_strpos($haystack, $needle, $offset = 0, $encoding = null)
    {
        $encoding = self::getEncoding($encoding);
        if ('CP850' === $encoding || 'ASCII' === $encoding) {
            return strpos($haystack, $needle, $offset);
        }

        $needle = (string) $needle;
        if ('' === $needle) {
            if (80000 > \PHP_VERSION_ID) {
                trigger_error(__METHOD__.': Empty delimiter', \E_USER_WARNING);

                return false;
            }

            return 0;
        }

        return iconv_strpos($haystack, $needle, $offset, $encoding);
    }

    public static function mb_strrpos($haystack, $needle, $offset = 0, $encoding = null)
    {
        $encoding = self::getEncoding($encoding);
        if ('CP850' === $encoding || 'ASCII' === $encoding) {
            return strrpos($haystack, $needle, $offset);
        }

        if ($offset != (int) $offset) {
            $offset = 0;
        } elseif ($offset = (int) $offset) {
            if ($offset < 0) {
                if (0 > $offset += self::mb_strlen($needle)) {
                    $haystack = self::mb_substr($haystack, 0, $offset, $encoding);
                }
                $offset = 0;
            } else {
                $haystack = self::mb_substr($haystack, $offset, 2147483647, $encoding);
            }
        }

        $pos = '' !== $needle || 80000 > \PHP_VERSION_ID
            ? iconv_strrpos($haystack, $needle, $encoding)
            : self::mb_strlen($haystack, $encoding);

        return false !== $pos ? $offset + $pos : false;
    }

    public static function mb_str_split($string, $split_length = 1, $encoding = null)
    {
        if (null !== $string && !\is_scalar($string) && !(\is_object($string) && method_exists($string, '__toString'))) {
            trigger_error('mb_str_split() expects parameter 1 to be string, '.\gettype($string).' given', \E_USER_WARNING);

            return null;
        }

        if (1 > $split_length = (int) $split_length) {
            if (80000 > \PHP_VERSION_ID) {
                trigger_error('The length of each segment must be greater than zero', \E_USER_WARNING);

                return false;
            }

            throw new \ValueError('Argument #2 ($length) must be greater than 0');
        }

        if (null === $encoding) {
            $encoding = mb_internal_encoding();
        }

        if ('UTF-8' === $encoding = self::getEncoding($encoding)) {
            $rx = '/(';
            while (65535 < $split_length) {
                $rx .= '.{65535}';
                $split_length -= 65535;
            }
            $rx .= '.{'.$split_length.'})/us';

            return preg_split($rx, $string, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY);
        }

        $result = [];
        $length = mb_strlen($string, $encoding);

        for ($i = 0; $i < $length; $i += $split_length) {
            $result[] = mb_substr($string, $i, $split_length, $encoding);
        }

        return $result;
    }

    public static function mb_strtolower($s, $encoding = null)
    {
        return self::mb_convert_case($s, \MB_CASE_LOWER, $encoding);
    }

    public static function mb_strtoupper($s, $encoding = null)
    {
        return self::mb_convert_case($s, \MB_CASE_UPPER, $encoding);
    }

    public static function mb_substitute_character($c = null)
    {
        if (null === $c) {
            return 'none';
        }
        if (0 === strcasecmp($c, 'none')) {
            return true;
        }
        if (80000 > \PHP_VERSION_ID) {
            return false;
        }
        if (\is_int($c) || 'long' === $c || 'entity' === $c) {
            return false;
        }

        throw new \ValueError('Argument #1 ($substitute_character) must be "none", "long", "entity" or a valid codepoint');
    }

    public static function mb_substr($s, $start, $length = null, $encoding = null)
    {
        $encoding = self::getEncoding($encoding);
        if ('CP850' === $encoding || 'ASCII' === $encoding) {
            return (string) substr($s, $start, null === $length ? 2147483647 : $length);
        }

        if ($start < 0) {
            $start = iconv_strlen($s, $encoding) + $start;
            if ($start < 0) {
                $start = 0;
            }
        }

        if (null === $length) {
            $length = 2147483647;
        } elseif ($length < 0) {
            $length = iconv_strlen($s, $encoding) + $length - $start;
            if ($length < 0) {
                return '';
            }
        }

        return (string) iconv_substr($s, $start, $length, $encoding);
    }

    public static function mb_stripos($haystack, $needle, $offset = 0, $encoding = null)
    {
        [$haystack, $needle] = str_replace(self::SIMPLE_CASE_FOLD[0], self::SIMPLE_CASE_FOLD[1], [
            self::mb_convert_case($haystack, \MB_CASE_LOWER, $encoding),
            self::mb_convert_case($needle, \MB_CASE_LOWER, $encoding),
        ]);

        return self::mb_strpos($haystack, $needle, $offset, $encoding);
    }

    public static function mb_stristr($haystack, $needle, $part = false, $encoding = null)
    {
        $pos = self::mb_stripos($haystack, $needle, 0, $encoding);

        return self::getSubpart($pos, $part, $haystack, $encoding);
    }

    public static function mb_strrchr($haystack, $needle, $part = false, $encoding = null)
    {
        $encoding = self::getEncoding($encoding);
        if ('CP850' === $encoding || 'ASCII' === $encoding) {
            $pos = strrpos($haystack, $needle);
        } else {
            $needle = self::mb_substr($needle, 0, 1, $encoding);
            $pos = iconv_strrpos($haystack, $needle, $encoding);
        }

        return self::getSubpart($pos, $part, $haystack, $encoding);
    }

    public static function mb_strrichr($haystack, $needle, $part = false, $encoding = null)
    {
        $needle = self::mb_substr($needle, 0, 1, $encoding);
        $pos = self::mb_strripos($haystack, $needle, $encoding);

        return self::getSubpart($pos, $part, $haystack, $encoding);
    }

    public static function mb_strripos($haystack, $needle, $offset = 0, $encoding = null)
    {
        $haystack = self::mb_convert_case($haystack, \MB_CASE_LOWER, $encoding);
        $needle = self::mb_convert_case($needle, \MB_CASE_LOWER, $encoding);

        $haystack = str_replace(self::SIMPLE_CASE_FOLD[0], self::SIMPLE_CASE_FOLD[1], $haystack);
        $needle = str_replace(self::SIMPLE_CASE_FOLD[0], self::SIMPLE_CASE_FOLD[1], $needle);

        return self::mb_strrpos($haystack, $needle, $offset, $encoding);
    }

    public static function mb_strstr($haystack, $needle, $part = false, $encoding = null)
    {
        $pos = strpos($haystack, $needle);
        if (false === $pos) {
            return false;
        }
        if ($part) {
            return substr($haystack, 0, $pos);
        }

        return substr($haystack, $pos);
    }

    public static function mb_get_info($type = 'all')
    {
        $info = [
            'internal_encoding' => self::$internalEncoding,
            'http_output' => 'pass',
            'http_output_conv_mimetypes' => '^(text/|application/xhtml\+xml)',
            'func_overload' => 0,
            'func_overload_list' => 'no overload',
            'mail_charset' => 'UTF-8',
            'mail_header_encoding' => 'BASE64',
            'mail_body_encoding' => 'BASE64',
            'illegal_chars' => 0,
            'encoding_translation' => 'Off',
            'language' => self::$language,
            'detect_order' => self::$encodingList,
            'substitute_character' => 'none',
            'strict_detection' => 'Off',
        ];

        if ('all' === $type) {
            return $info;
        }
        if (isset($info[$type])) {
            return $info[$type];
        }

        return false;
    }

    public static function mb_http_input($type = '')
    {
        return false;
    }

    public static function mb_http_output($encoding = null)
    {
        return null !== $encoding ? 'pass' === $encoding : 'pass';
    }

    public static function mb_strwidth($s, $encoding = null)
    {
        $encoding = self::getEncoding($encoding);

        if ('UTF-8' !== $encoding) {
            $s = self::iconv($encoding, 'UTF-8', $s);
        }

        $s = preg_replace('/[\x{1100}-\x{115F}\x{2329}\x{232A}\x{2E80}-\x{303E}\x{3040}-\x{A4CF}\x{AC00}-\x{D7A3}\x{F900}-\x{FAFF}\x{FE10}-\x{FE19}\x{FE30}-\x{FE6F}\x{FF00}-\x{FF60}\x{FFE0}-\x{FFE6}\x{20000}-\x{2FFFD}\x{30000}-\x{3FFFD}]/u', '', $s, -1, $wide);

        return ($wide << 1) + iconv_strlen($s, 'UTF-8');
    }

    public static function mb_substr_count($haystack, $needle, $encoding = null)
    {
        return substr_count($haystack, $needle);
    }

    public static function mb_output_handler($contents, $status)
    {
        return $contents;
    }

    public static function mb_chr($code, $encoding = null)
    {
        if (0x80 > $code %= 0x200000) {
            $s = \chr($code);
        } elseif (0x800 > $code) {
            $s = \chr(0xC0 | $code >> 6).\chr(0x80 | $code & 0x3F);
        } elseif (0x10000 > $code) {
            $s = \chr(0xE0 | $code >> 12).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F);
        } else {
            $s = \chr(0xF0 | $code >> 18).\chr(0x80 | $code >> 12 & 0x3F).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F);
        }

        if ('UTF-8' !== $encoding = self::getEncoding($encoding)) {
            $s = mb_convert_encoding($s, $encoding, 'UTF-8');
        }

        return $s;
    }

    public static function mb_ord($s, $encoding = null)
    {
        if ('UTF-8' !== $encoding = self::getEncoding($encoding)) {
            $s = mb_convert_encoding($s, 'UTF-8', $encoding);
        }

        if (1 === \strlen($s)) {
            return \ord($s);
        }

        $code = ($s = unpack('C*', substr($s, 0, 4))) ? $s[1] : 0;
        if (0xF0 <= $code) {
            return (($code - 0xF0) << 18) + (($s[2] - 0x80) << 12) + (($s[3] - 0x80) << 6) + $s[4] - 0x80;
        }
        if (0xE0 <= $code) {
            return (($code - 0xE0) << 12) + (($s[2] - 0x80) << 6) + $s[3] - 0x80;
        }
        if (0xC0 <= $code) {
            return (($code - 0xC0) << 6) + $s[2] - 0x80;
        }

        return $code;
    }

    /** @return string|false */
    public static function mb_scrub(?string $string, ?string $encoding = null): string
    {
        if (null === $encoding) {
            $encoding = self::mb_internal_encoding();
        } elseif (!self::assertEncoding($encoding, 'mb_scrub(): Argument #2 ($encoding) must be a valid encoding, "%s" given')) {
            return false;
        }

        return self::mb_convert_encoding((string) $string, $encoding, $encoding);
    }

    /** @return string|false */
    public static function mb_str_pad(string $string, int $length, string $pad_string = ' ', int $pad_type = \STR_PAD_RIGHT, ?string $encoding = null)
    {
        if (null === $encoding) {
            $encoding = self::mb_internal_encoding();
        } elseif (!self::assertEncoding($encoding, 'mb_str_pad(): Argument #5 ($encoding) must be a valid encoding, "%s" given')) {
            return false;
        }

        if (self::mb_strlen($pad_string, $encoding) <= 0) {
            if (\PHP_VERSION_ID < 80000) {
                trigger_error('mb_str_pad(): Argument #3 ($pad_string) must be a non-empty string', \E_USER_WARNING);

                return false;
            }

            throw new \ValueError('mb_str_pad(): Argument #3 ($pad_string) must be a non-empty string');
        }

        if (!\in_array($pad_type, [\STR_PAD_RIGHT, \STR_PAD_LEFT, \STR_PAD_BOTH], true)) {
            if (\PHP_VERSION_ID < 80000) {
                trigger_error('mb_str_pad(): Argument #4 ($pad_type) must be STR_PAD_LEFT, STR_PAD_RIGHT, or STR_PAD_BOTH', \E_USER_WARNING);

                return false;
            }

            throw new \ValueError('mb_str_pad(): Argument #4 ($pad_type) must be STR_PAD_LEFT, STR_PAD_RIGHT, or STR_PAD_BOTH');
        }

        $paddingRequired = $length - self::mb_strlen($string, $encoding);

        if ($paddingRequired < 1) {
            return $string;
        }

        switch ($pad_type) {
            case \STR_PAD_LEFT:
                return self::mb_substr(str_repeat($pad_string, $paddingRequired), 0, $paddingRequired, $encoding).$string;
            case \STR_PAD_RIGHT:
                return $string.self::mb_substr(str_repeat($pad_string, $paddingRequired), 0, $paddingRequired, $encoding);
            default:
                $leftPaddingLength = floor($paddingRequired / 2);
                $rightPaddingLength = $paddingRequired - $leftPaddingLength;

                return self::mb_substr(str_repeat($pad_string, $leftPaddingLength), 0, $leftPaddingLength, $encoding).$string.self::mb_substr(str_repeat($pad_string, $rightPaddingLength), 0, $rightPaddingLength, $encoding);
        }
    }

    /** @return string|false */
    public static function mb_ucfirst(string $string, ?string $encoding = null)
    {
        if (null === $encoding) {
            $encoding = self::mb_internal_encoding();
        } elseif (!self::assertEncoding($encoding, 'mb_ucfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given')) {
            return false;
        }

        $firstChar = mb_substr($string, 0, 1, $encoding);
        $firstChar = mb_convert_case($firstChar, \MB_CASE_TITLE, $encoding);

        return $firstChar.mb_substr($string, 1, null, $encoding);
    }

    /** @return string|false */
    public static function mb_lcfirst(string $string, ?string $encoding = null)
    {
        if (null === $encoding) {
            $encoding = self::mb_internal_encoding();
        } elseif (!self::assertEncoding($encoding, 'mb_lcfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given')) {
            return false;
        }

        $firstChar = mb_substr($string, 0, 1, $encoding);
        $firstChar = mb_convert_case($firstChar, \MB_CASE_LOWER, $encoding);

        return $firstChar.mb_substr($string, 1, null, $encoding);
    }

    /** @return string|false */
    public static function mb_trim(string $string, ?string $characters = null, ?string $encoding = null)
    {
        return self::mb_internal_trim('{^[%s]+|[%1$s]+$}Du', $string, $characters, $encoding, __FUNCTION__);
    }

    /** @return string|false */
    public static function mb_ltrim(string $string, ?string $characters = null, ?string $encoding = null)
    {
        return self::mb_internal_trim('{^[%s]+}Du', $string, $characters, $encoding, __FUNCTION__);
    }

    /** @return string|false */
    public static function mb_rtrim(string $string, ?string $characters = null, ?string $encoding = null)
    {
        return self::mb_internal_trim('{[%s]+$}Du', $string, $characters, $encoding, __FUNCTION__);
    }

    private static function getSubpart($pos, $part, $haystack, $encoding)
    {
        if (false === $pos) {
            return false;
        }
        if ($part) {
            return self::mb_substr($haystack, 0, $pos, $encoding);
        }

        return self::mb_substr($haystack, $pos, null, $encoding);
    }

    private static function html_encoding_callback(array $m)
    {
        $i = 1;
        $entities = '';
        $m = unpack('C*', htmlentities($m[0], \ENT_COMPAT, 'UTF-8'));

        while (isset($m[$i])) {
            if (0x80 > $m[$i]) {
                $entities .= \chr($m[$i++]);
                continue;
            }
            if (0xF0 <= $m[$i]) {
                $c = (($m[$i++] - 0xF0) << 18) + (($m[$i++] - 0x80) << 12) + (($m[$i++] - 0x80) << 6) + $m[$i++] - 0x80;
            } elseif (0xE0 <= $m[$i]) {
                $c = (($m[$i++] - 0xE0) << 12) + (($m[$i++] - 0x80) << 6) + $m[$i++] - 0x80;
            } else {
                $c = (($m[$i++] - 0xC0) << 6) + $m[$i++] - 0x80;
            }

            $entities .= '&#'.$c.';';
        }

        return $entities;
    }

    private static function title_case(array $s)
    {
        return self::mb_convert_case($s[1], \MB_CASE_UPPER, 'UTF-8').self::mb_convert_case($s[2], \MB_CASE_LOWER, 'UTF-8');
    }

    private static function getData($file)
    {
        if (file_exists($file = __DIR__.'/Resources/unidata/'.$file.'.php')) {
            return require $file;
        }

        return false;
    }

    private static function getEncoding($encoding)
    {
        if (null === $encoding) {
            return self::$internalEncoding;
        }

        if ('UTF-8' === $encoding) {
            return 'UTF-8';
        }

        $encoding = strtoupper($encoding);

        if ('8BIT' === $encoding || 'BINARY' === $encoding) {
            return 'CP850';
        }

        if ('UTF8' === $encoding) {
            return 'UTF-8';
        }

        if ('UTF-32' === $encoding) {
            return 'UTF-32BE';
        }

        if ('UTF-16' === $encoding) {
            return 'UTF-16BE';
        }

        return $encoding;
    }

    private static function iconv($fromEncoding, $toEncoding, $s)
    {
        if (null === self::$iconvSupportsIgnore) {
            self::$iconvSupportsIgnore = false !== @iconv('UTF-8', 'UTF-8//IGNORE', '');
        }

        return self::$iconvSupportsIgnore
            ? iconv($fromEncoding, $toEncoding.'//IGNORE', $s)
            : iconv($fromEncoding, $toEncoding, $s);
    }

    /** @return string|false */
    private static function mb_internal_trim(string $regex, string $string, ?string $characters, ?string $encoding, string $function)
    {
        if (null === $encoding) {
            $encoding = self::mb_internal_encoding();
        } elseif (!self::assertEncoding($encoding, $function.'(): Argument #3 ($encoding) must be a valid encoding, "%s" given')) {
            return false;
        }

        if ('' === $characters) {
            return null === $encoding ? $string : self::mb_convert_encoding($string, $encoding);
        }

        if ('UTF-8' === $encoding) {
            $encoding = null;
            if (!preg_match('//u', $string)) {
                $string = @self::iconv('UTF-8', 'UTF-8', $string);
            }
            if (null !== $characters && !preg_match('//u', $characters)) {
                $characters = @self::iconv('UTF-8', 'UTF-8', $characters);
            }
        } else {
            $string = self::iconv($encoding, 'UTF-8', $string);

            if (null !== $characters) {
                $characters = self::iconv($encoding, 'UTF-8', $characters);
            }
        }

        if (null === $characters) {
            $characters = "\\0 \f\n\r\t\v\u{00A0}\u{1680}\u{2000}\u{2001}\u{2002}\u{2003}\u{2004}\u{2005}\u{2006}\u{2007}\u{2008}\u{2009}\u{200A}\u{2028}\u{2029}\u{202F}\u{205F}\u{3000}\u{0085}\u{180E}";
        } else {
            $characters = preg_quote($characters);
        }

        $string = preg_replace(\sprintf($regex, $characters), '', $string);

        if (null === $encoding) {
            return $string;
        }

        return self::iconv('UTF-8', $encoding, $string);
    }

    private static function assertEncoding(string $encoding, string $errorFormat): bool
    {
        try {
            $validEncoding = @self::mb_check_encoding('', $encoding);
        } catch (\ValueError $e) {
            throw new \ValueError(\sprintf($errorFormat, $encoding));
        }

        if (!$validEncoding) {
            if (80000 > \PHP_VERSION_ID) {
                trigger_error(\sprintf($errorFormat, $encoding), \E_USER_WARNING);
            } else {
                throw new \ValueError(\sprintf($errorFormat, $encoding));
            }
        }

        return $validEncoding;
    }
}
<?php

return array (
  'a' => 'A',
  'b' => 'B',
  'c' => 'C',
  'd' => 'D',
  'e' => 'E',
  'f' => 'F',
  'g' => 'G',
  'h' => 'H',
  'i' => 'I',
  'j' => 'J',
  'k' => 'K',
  'l' => 'L',
  'm' => 'M',
  'n' => 'N',
  'o' => 'O',
  'p' => 'P',
  'q' => 'Q',
  'r' => 'R',
  's' => 'S',
  't' => 'T',
  'u' => 'U',
  'v' => 'V',
  'w' => 'W',
  'x' => 'X',
  'y' => 'Y',
  'z' => 'Z',
  'µ' => 'Μ',
  'à' => 'À',
  'á' => 'Á',
  'â' => 'Â',
  'ã' => 'Ã',
  'ä' => 'Ä',
  'å' => 'Å',
  'æ' => 'Æ',
  'ç' => 'Ç',
  'è' => 'È',
  'é' => 'É',
  'ê' => 'Ê',
  'ë' => 'Ë',
  'ì' => 'Ì',
  'í' => 'Í',
  'î' => 'Î',
  'ï' => 'Ï',
  'ð' => 'Ð',
  'ñ' => 'Ñ',
  'ò' => 'Ò',
  'ó' => 'Ó',
  'ô' => 'Ô',
  'õ' => 'Õ',
  'ö' => 'Ö',
  'ø' => 'Ø',
  'ù' => 'Ù',
  'ú' => 'Ú',
  'û' => 'Û',
  'ü' => 'Ü',
  'ý' => 'Ý',
  'þ' => 'Þ',
  'ÿ' => 'Ÿ',
  'ā' => 'Ā',
  'ă' => 'Ă',
  'ą' => 'Ą',
  'ć' => 'Ć',
  'ĉ' => 'Ĉ',
  'ċ' => 'Ċ',
  'č' => 'Č',
  'ď' => 'Ď',
  'đ' => 'Đ',
  'ē' => 'Ē',
  'ĕ' => 'Ĕ',
  'ė' => 'Ė',
  'ę' => 'Ę',
  'ě' => 'Ě',
  'ĝ' => 'Ĝ',
  'ğ' => 'Ğ',
  'ġ' => 'Ġ',
  'ģ' => 'Ģ',
  'ĥ' => 'Ĥ',
  'ħ' => 'Ħ',
  'ĩ' => 'Ĩ',
  'ī' => 'Ī',
  'ĭ' => 'Ĭ',
  'į' => 'Į',
  'ı' => 'I',
  'ĳ' => 'Ĳ',
  'ĵ' => 'Ĵ',
  'ķ' => 'Ķ',
  'ĺ' => 'Ĺ',
  'ļ' => 'Ļ',
  'ľ' => 'Ľ',
  'ŀ' => 'Ŀ',
  'ł' => 'Ł',
  'ń' => 'Ń',
  'ņ' => 'Ņ',
  'ň' => 'Ň',
  'ŋ' => 'Ŋ',
  'ō' => 'Ō',
  'ŏ' => 'Ŏ',
  'ő' => 'Ő',
  'œ' => 'Œ',
  'ŕ' => 'Ŕ',
  'ŗ' => 'Ŗ',
  'ř' => 'Ř',
  'ś' => 'Ś',
  'ŝ' => 'Ŝ',
  'ş' => 'Ş',
  'š' => 'Š',
  'ţ' => 'Ţ',
  'ť' => 'Ť',
  'ŧ' => 'Ŧ',
  'ũ' => 'Ũ',
  'ū' => 'Ū',
  'ŭ' => 'Ŭ',
  'ů' => 'Ů',
  'ű' => 'Ű',
  'ų' => 'Ų',
  'ŵ' => 'Ŵ',
  'ŷ' => 'Ŷ',
  'ź' => 'Ź',
  'ż' => 'Ż',
  'ž' => 'Ž',
  'ſ' => 'S',
  'ƀ' => 'Ƀ',
  'ƃ' => 'Ƃ',
  'ƅ' => 'Ƅ',
  'ƈ' => 'Ƈ',
  'ƌ' => 'Ƌ',
  'ƒ' => 'Ƒ',
  'ƕ' => 'Ƕ',
  'ƙ' => 'Ƙ',
  'ƚ' => 'Ƚ',
  'ƞ' => 'Ƞ',
  'ơ' => 'Ơ',
  'ƣ' => 'Ƣ',
  'ƥ' => 'Ƥ',
  'ƨ' => 'Ƨ',
  'ƭ' => 'Ƭ',
  'ư' => 'Ư',
  'ƴ' => 'Ƴ',
  'ƶ' => 'Ƶ',
  'ƹ' => 'Ƹ',
  'ƽ' => 'Ƽ',
  'ƿ' => 'Ƿ',
  'ǅ' => 'Ǆ',
  'ǆ' => 'Ǆ',
  'ǈ' => 'Ǉ',
  'ǉ' => 'Ǉ',
  'ǋ' => 'Ǌ',
  'ǌ' => 'Ǌ',
  'ǎ' => 'Ǎ',
  'ǐ' => 'Ǐ',
  'ǒ' => 'Ǒ',
  'ǔ' => 'Ǔ',
  'ǖ' => 'Ǖ',
  'ǘ' => 'Ǘ',
  'ǚ' => 'Ǚ',
  'ǜ' => 'Ǜ',
  'ǝ' => 'Ǝ',
  'ǟ' => 'Ǟ',
  'ǡ' => 'Ǡ',
  'ǣ' => 'Ǣ',
  'ǥ' => 'Ǥ',
  'ǧ' => 'Ǧ',
  'ǩ' => 'Ǩ',
  'ǫ' => 'Ǫ',
  'ǭ' => 'Ǭ',
  'ǯ' => 'Ǯ',
  'ǲ' => 'Ǳ',
  'ǳ' => 'Ǳ',
  'ǵ' => 'Ǵ',
  'ǹ' => 'Ǹ',
  'ǻ' => 'Ǻ',
  'ǽ' => 'Ǽ',
  'ǿ' => 'Ǿ',
  'ȁ' => 'Ȁ',
  'ȃ' => 'Ȃ',
  'ȅ' => 'Ȅ',
  'ȇ' => 'Ȇ',
  'ȉ' => 'Ȉ',
  'ȋ' => 'Ȋ',
  'ȍ' => 'Ȍ',
  'ȏ' => 'Ȏ',
  'ȑ' => 'Ȑ',
  'ȓ' => 'Ȓ',
  'ȕ' => 'Ȕ',
  'ȗ' => 'Ȗ',
  'ș' => 'Ș',
  'ț' => 'Ț',
  'ȝ' => 'Ȝ',
  'ȟ' => 'Ȟ',
  'ȣ' => 'Ȣ',
  'ȥ' => 'Ȥ',
  'ȧ' => 'Ȧ',
  'ȩ' => 'Ȩ',
  'ȫ' => 'Ȫ',
  'ȭ' => 'Ȭ',
  'ȯ' => 'Ȯ',
  'ȱ' => 'Ȱ',
  'ȳ' => 'Ȳ',
  'ȼ' => 'Ȼ',
  'ȿ' => 'Ȿ',
  'ɀ' => 'Ɀ',
  'ɂ' => 'Ɂ',
  'ɇ' => 'Ɇ',
  'ɉ' => 'Ɉ',
  'ɋ' => 'Ɋ',
  'ɍ' => 'Ɍ',
  'ɏ' => 'Ɏ',
  'ɐ' => 'Ɐ',
  'ɑ' => 'Ɑ',
  'ɒ' => 'Ɒ',
  'ɓ' => 'Ɓ',
  'ɔ' => 'Ɔ',
  'ɖ' => 'Ɖ',
  'ɗ' => 'Ɗ',
  'ə' => 'Ə',
  'ɛ' => 'Ɛ',
  'ɜ' => 'Ɜ',
  'ɠ' => 'Ɠ',
  'ɡ' => 'Ɡ',
  'ɣ' => 'Ɣ',
  'ɥ' => 'Ɥ',
  'ɦ' => 'Ɦ',
  'ɨ' => 'Ɨ',
  'ɩ' => 'Ɩ',
  'ɪ' => 'Ɪ',
  'ɫ' => 'Ɫ',
  'ɬ' => 'Ɬ',
  'ɯ' => 'Ɯ',
  'ɱ' => 'Ɱ',
  'ɲ' => 'Ɲ',
  'ɵ' => 'Ɵ',
  'ɽ' => 'Ɽ',
  'ʀ' => 'Ʀ',
  'ʂ' => 'Ʂ',
  'ʃ' => 'Ʃ',
  'ʇ' => 'Ʇ',
  'ʈ' => 'Ʈ',
  'ʉ' => 'Ʉ',
  'ʊ' => 'Ʊ',
  'ʋ' => 'Ʋ',
  'ʌ' => 'Ʌ',
  'ʒ' => 'Ʒ',
  'ʝ' => 'Ʝ',
  'ʞ' => 'Ʞ',
  'ͅ' => 'Ι',
  'ͱ' => 'Ͱ',
  'ͳ' => 'Ͳ',
  'ͷ' => 'Ͷ',
  'ͻ' => 'Ͻ',
  'ͼ' => 'Ͼ',
  'ͽ' => 'Ͽ',
  'ά' => 'Ά',
  'έ' => 'Έ',
  'ή' => 'Ή',
  'ί' => 'Ί',
  'α' => 'Α',
  'β' => 'Β',
  'γ' => 'Γ',
  'δ' => 'Δ',
  'ε' => 'Ε',
  'ζ' => 'Ζ',
  'η' => 'Η',
  'θ' => 'Θ',
  'ι' => 'Ι',
  'κ' => 'Κ',
  'λ' => 'Λ',
  'μ' => 'Μ',
  'ν' => 'Ν',
  'ξ' => 'Ξ',
  'ο' => 'Ο',
  'π' => 'Π',
  'ρ' => 'Ρ',
  'ς' => 'Σ',
  'σ' => 'Σ',
  'τ' => 'Τ',
  'υ' => 'Υ',
  'φ' => 'Φ',
  'χ' => 'Χ',
  'ψ' => 'Ψ',
  'ω' => 'Ω',
  'ϊ' => 'Ϊ',
  'ϋ' => 'Ϋ',
  'ό' => 'Ό',
  'ύ' => 'Ύ',
  'ώ' => 'Ώ',
  'ϐ' => 'Β',
  'ϑ' => 'Θ',
  'ϕ' => 'Φ',
  'ϖ' => 'Π',
  'ϗ' => 'Ϗ',
  'ϙ' => 'Ϙ',
  'ϛ' => 'Ϛ',
  'ϝ' => 'Ϝ',
  'ϟ' => 'Ϟ',
  'ϡ' => 'Ϡ',
  'ϣ' => 'Ϣ',
  'ϥ' => 'Ϥ',
  'ϧ' => 'Ϧ',
  'ϩ' => 'Ϩ',
  'ϫ' => 'Ϫ',
  'ϭ' => 'Ϭ',
  'ϯ' => 'Ϯ',
  'ϰ' => 'Κ',
  'ϱ' => 'Ρ',
  'ϲ' => 'Ϲ',
  'ϳ' => 'Ϳ',
  'ϵ' => 'Ε',
  'ϸ' => 'Ϸ',
  'ϻ' => 'Ϻ',
  'а' => 'А',
  'б' => 'Б',
  'в' => 'В',
  'г' => 'Г',
  'д' => 'Д',
  'е' => 'Е',
  'ж' => 'Ж',
  'з' => 'З',
  'и' => 'И',
  'й' => 'Й',
  'к' => 'К',
  'л' => 'Л',
  'м' => 'М',
  'н' => 'Н',
  'о' => 'О',
  'п' => 'П',
  'р' => 'Р',
  'с' => 'С',
  'т' => 'Т',
  'у' => 'У',
  'ф' => 'Ф',
  'х' => 'Х',
  'ц' => 'Ц',
  'ч' => 'Ч',
  'ш' => 'Ш',
  'щ' => 'Щ',
  'ъ' => 'Ъ',
  'ы' => 'Ы',
  'ь' => 'Ь',
  'э' => 'Э',
  'ю' => 'Ю',
  'я' => 'Я',
  'ѐ' => 'Ѐ',
  'ё' => 'Ё',
  'ђ' => 'Ђ',
  'ѓ' => 'Ѓ',
  'є' => 'Є',
  'ѕ' => 'Ѕ',
  'і' => 'І',
  'ї' => 'Ї',
  'ј' => 'Ј',
  'љ' => 'Љ',
  'њ' => 'Њ',
  'ћ' => 'Ћ',
  'ќ' => 'Ќ',
  'ѝ' => 'Ѝ',
  'ў' => 'Ў',
  'џ' => 'Џ',
  'ѡ' => 'Ѡ',
  'ѣ' => 'Ѣ',
  'ѥ' => 'Ѥ',
  'ѧ' => 'Ѧ',
  'ѩ' => 'Ѩ',
  'ѫ' => 'Ѫ',
  'ѭ' => 'Ѭ',
  'ѯ' => 'Ѯ',
  'ѱ' => 'Ѱ',
  'ѳ' => 'Ѳ',
  'ѵ' => 'Ѵ',
  'ѷ' => 'Ѷ',
  'ѹ' => 'Ѹ',
  'ѻ' => 'Ѻ',
  'ѽ' => 'Ѽ',
  'ѿ' => 'Ѿ',
  'ҁ' => 'Ҁ',
  'ҋ' => 'Ҋ',
  'ҍ' => 'Ҍ',
  'ҏ' => 'Ҏ',
  'ґ' => 'Ґ',
  'ғ' => 'Ғ',
  'ҕ' => 'Ҕ',
  'җ' => 'Җ',
  'ҙ' => 'Ҙ',
  'қ' => 'Қ',
  'ҝ' => 'Ҝ',
  'ҟ' => 'Ҟ',
  'ҡ' => 'Ҡ',
  'ң' => 'Ң',
  'ҥ' => 'Ҥ',
  'ҧ' => 'Ҧ',
  'ҩ' => 'Ҩ',
  'ҫ' => 'Ҫ',
  'ҭ' => 'Ҭ',
  'ү' => 'Ү',
  'ұ' => 'Ұ',
  'ҳ' => 'Ҳ',
  'ҵ' => 'Ҵ',
  'ҷ' => 'Ҷ',
  'ҹ' => 'Ҹ',
  'һ' => 'Һ',
  'ҽ' => 'Ҽ',
  'ҿ' => 'Ҿ',
  'ӂ' => 'Ӂ',
  'ӄ' => 'Ӄ',
  'ӆ' => 'Ӆ',
  'ӈ' => 'Ӈ',
  'ӊ' => 'Ӊ',
  'ӌ' => 'Ӌ',
  'ӎ' => 'Ӎ',
  'ӏ' => 'Ӏ',
  'ӑ' => 'Ӑ',
  'ӓ' => 'Ӓ',
  'ӕ' => 'Ӕ',
  'ӗ' => 'Ӗ',
  'ә' => 'Ә',
  'ӛ' => 'Ӛ',
  'ӝ' => 'Ӝ',
  'ӟ' => 'Ӟ',
  'ӡ' => 'Ӡ',
  'ӣ' => 'Ӣ',
  'ӥ' => 'Ӥ',
  'ӧ' => 'Ӧ',
  'ө' => 'Ө',
  'ӫ' => 'Ӫ',
  'ӭ' => 'Ӭ',
  'ӯ' => 'Ӯ',
  'ӱ' => 'Ӱ',
  'ӳ' => 'Ӳ',
  'ӵ' => 'Ӵ',
  'ӷ' => 'Ӷ',
  'ӹ' => 'Ӹ',
  'ӻ' => 'Ӻ',
  'ӽ' => 'Ӽ',
  'ӿ' => 'Ӿ',
  'ԁ' => 'Ԁ',
  'ԃ' => 'Ԃ',
  'ԅ' => 'Ԅ',
  'ԇ' => 'Ԇ',
  'ԉ' => 'Ԉ',
  'ԋ' => 'Ԋ',
  'ԍ' => 'Ԍ',
  'ԏ' => 'Ԏ',
  'ԑ' => 'Ԑ',
  'ԓ' => 'Ԓ',
  'ԕ' => 'Ԕ',
  'ԗ' => 'Ԗ',
  'ԙ' => 'Ԙ',
  'ԛ' => 'Ԛ',
  'ԝ' => 'Ԝ',
  'ԟ' => 'Ԟ',
  'ԡ' => 'Ԡ',
  'ԣ' => 'Ԣ',
  'ԥ' => 'Ԥ',
  'ԧ' => 'Ԧ',
  'ԩ' => 'Ԩ',
  'ԫ' => 'Ԫ',
  'ԭ' => 'Ԭ',
  'ԯ' => 'Ԯ',
  'ա' => 'Ա',
  'բ' => 'Բ',
  'գ' => 'Գ',
  'դ' => 'Դ',
  'ե' => 'Ե',
  'զ' => 'Զ',
  'է' => 'Է',
  'ը' => 'Ը',
  'թ' => 'Թ',
  'ժ' => 'Ժ',
  'ի' => 'Ի',
  'լ' => 'Լ',
  'խ' => 'Խ',
  'ծ' => 'Ծ',
  'կ' => 'Կ',
  'հ' => 'Հ',
  'ձ' => 'Ձ',
  'ղ' => 'Ղ',
  'ճ' => 'Ճ',
  'մ' => 'Մ',
  'յ' => 'Յ',
  'ն' => 'Ն',
  'շ' => 'Շ',
  'ո' => 'Ո',
  'չ' => 'Չ',
  'պ' => 'Պ',
  'ջ' => 'Ջ',
  'ռ' => 'Ռ',
  'ս' => 'Ս',
  'վ' => 'Վ',
  'տ' => 'Տ',
  'ր' => 'Ր',
  'ց' => 'Ց',
  'ւ' => 'Ւ',
  'փ' => 'Փ',
  'ք' => 'Ք',
  'օ' => 'Օ',
  'ֆ' => 'Ֆ',
  'ა' => 'Ა',
  'ბ' => 'Ბ',
  'გ' => 'Გ',
  'დ' => 'Დ',
  'ე' => 'Ე',
  'ვ' => 'Ვ',
  'ზ' => 'Ზ',
  'თ' => 'Თ',
  'ი' => 'Ი',
  'კ' => 'Კ',
  'ლ' => 'Ლ',
  'მ' => 'Მ',
  'ნ' => 'Ნ',
  'ო' => 'Ო',
  'პ' => 'Პ',
  'ჟ' => 'Ჟ',
  'რ' => 'Რ',
  'ს' => 'Ს',
  'ტ' => 'Ტ',
  'უ' => 'Უ',
  'ფ' => 'Ფ',
  'ქ' => 'Ქ',
  'ღ' => 'Ღ',
  'ყ' => 'Ყ',
  'შ' => 'Შ',
  'ჩ' => 'Ჩ',
  'ც' => 'Ც',
  'ძ' => 'Ძ',
  'წ' => 'Წ',
  'ჭ' => 'Ჭ',
  'ხ' => 'Ხ',
  'ჯ' => 'Ჯ',
  'ჰ' => 'Ჰ',
  'ჱ' => 'Ჱ',
  'ჲ' => 'Ჲ',
  'ჳ' => 'Ჳ',
  'ჴ' => 'Ჴ',
  'ჵ' => 'Ჵ',
  'ჶ' => 'Ჶ',
  'ჷ' => 'Ჷ',
  'ჸ' => 'Ჸ',
  'ჹ' => 'Ჹ',
  'ჺ' => 'Ჺ',
  'ჽ' => 'Ჽ',
  'ჾ' => 'Ჾ',
  'ჿ' => 'Ჿ',
  'ᏸ' => 'Ᏸ',
  'ᏹ' => 'Ᏹ',
  'ᏺ' => 'Ᏺ',
  'ᏻ' => 'Ᏻ',
  'ᏼ' => 'Ᏼ',
  'ᏽ' => 'Ᏽ',
  'ᲀ' => 'В',
  'ᲁ' => 'Д',
  'ᲂ' => 'О',
  'ᲃ' => 'С',
  'ᲄ' => 'Т',
  'ᲅ' => 'Т',
  'ᲆ' => 'Ъ',
  'ᲇ' => 'Ѣ',
  'ᲈ' => 'Ꙋ',
  'ᵹ' => 'Ᵹ',
  'ᵽ' => 'Ᵽ',
  'ᶎ' => 'Ᶎ',
  'ḁ' => 'Ḁ',
  'ḃ' => 'Ḃ',
  'ḅ' => 'Ḅ',
  'ḇ' => 'Ḇ',
  'ḉ' => 'Ḉ',
  'ḋ' => 'Ḋ',
  'ḍ' => 'Ḍ',
  'ḏ' => 'Ḏ',
  'ḑ' => 'Ḑ',
  'ḓ' => 'Ḓ',
  'ḕ' => 'Ḕ',
  'ḗ' => 'Ḗ',
  'ḙ' => 'Ḙ',
  'ḛ' => 'Ḛ',
  'ḝ' => 'Ḝ',
  'ḟ' => 'Ḟ',
  'ḡ' => 'Ḡ',
  'ḣ' => 'Ḣ',
  'ḥ' => 'Ḥ',
  'ḧ' => 'Ḧ',
  'ḩ' => 'Ḩ',
  'ḫ' => 'Ḫ',
  'ḭ' => 'Ḭ',
  'ḯ' => 'Ḯ',
  'ḱ' => 'Ḱ',
  'ḳ' => 'Ḳ',
  'ḵ' => 'Ḵ',
  'ḷ' => 'Ḷ',
  'ḹ' => 'Ḹ',
  'ḻ' => 'Ḻ',
  'ḽ' => 'Ḽ',
  'ḿ' => 'Ḿ',
  'ṁ' => 'Ṁ',
  'ṃ' => 'Ṃ',
  'ṅ' => 'Ṅ',
  'ṇ' => 'Ṇ',
  'ṉ' => 'Ṉ',
  'ṋ' => 'Ṋ',
  'ṍ' => 'Ṍ',
  'ṏ' => 'Ṏ',
  'ṑ' => 'Ṑ',
  'ṓ' => 'Ṓ',
  'ṕ' => 'Ṕ',
  'ṗ' => 'Ṗ',
  'ṙ' => 'Ṙ',
  'ṛ' => 'Ṛ',
  'ṝ' => 'Ṝ',
  'ṟ' => 'Ṟ',
  'ṡ' => 'Ṡ',
  'ṣ' => 'Ṣ',
  'ṥ' => 'Ṥ',
  'ṧ' => 'Ṧ',
  'ṩ' => 'Ṩ',
  'ṫ' => 'Ṫ',
  'ṭ' => 'Ṭ',
  'ṯ' => 'Ṯ',
  'ṱ' => 'Ṱ',
  'ṳ' => 'Ṳ',
  'ṵ' => 'Ṵ',
  'ṷ' => 'Ṷ',
  'ṹ' => 'Ṹ',
  'ṻ' => 'Ṻ',
  'ṽ' => 'Ṽ',
  'ṿ' => 'Ṿ',
  'ẁ' => 'Ẁ',
  'ẃ' => 'Ẃ',
  'ẅ' => 'Ẅ',
  'ẇ' => 'Ẇ',
  'ẉ' => 'Ẉ',
  'ẋ' => 'Ẋ',
  'ẍ' => 'Ẍ',
  'ẏ' => 'Ẏ',
  'ẑ' => 'Ẑ',
  'ẓ' => 'Ẓ',
  'ẕ' => 'Ẕ',
  'ẛ' => 'Ṡ',
  'ạ' => 'Ạ',
  'ả' => 'Ả',
  'ấ' => 'Ấ',
  'ầ' => 'Ầ',
  'ẩ' => 'Ẩ',
  'ẫ' => 'Ẫ',
  'ậ' => 'Ậ',
  'ắ' => 'Ắ',
  'ằ' => 'Ằ',
  'ẳ' => 'Ẳ',
  'ẵ' => 'Ẵ',
  'ặ' => 'Ặ',
  'ẹ' => 'Ẹ',
  'ẻ' => 'Ẻ',
  'ẽ' => 'Ẽ',
  'ế' => 'Ế',
  'ề' => 'Ề',
  'ể' => 'Ể',
  'ễ' => 'Ễ',
  'ệ' => 'Ệ',
  'ỉ' => 'Ỉ',
  'ị' => 'Ị',
  'ọ' => 'Ọ',
  'ỏ' => 'Ỏ',
  'ố' => 'Ố',
  'ồ' => 'Ồ',
  'ổ' => 'Ổ',
  'ỗ' => 'Ỗ',
  'ộ' => 'Ộ',
  'ớ' => 'Ớ',
  'ờ' => 'Ờ',
  'ở' => 'Ở',
  'ỡ' => 'Ỡ',
  'ợ' => 'Ợ',
  'ụ' => 'Ụ',
  'ủ' => 'Ủ',
  'ứ' => 'Ứ',
  'ừ' => 'Ừ',
  'ử' => 'Ử',
  'ữ' => 'Ữ',
  'ự' => 'Ự',
  'ỳ' => 'Ỳ',
  'ỵ' => 'Ỵ',
  'ỷ' => 'Ỷ',
  'ỹ' => 'Ỹ',
  'ỻ' => 'Ỻ',
  'ỽ' => 'Ỽ',
  'ỿ' => 'Ỿ',
  'ἀ' => 'Ἀ',
  'ἁ' => 'Ἁ',
  'ἂ' => 'Ἂ',
  'ἃ' => 'Ἃ',
  'ἄ' => 'Ἄ',
  'ἅ' => 'Ἅ',
  'ἆ' => 'Ἆ',
  'ἇ' => 'Ἇ',
  'ἐ' => 'Ἐ',
  'ἑ' => 'Ἑ',
  'ἒ' => 'Ἒ',
  'ἓ' => 'Ἓ',
  'ἔ' => 'Ἔ',
  'ἕ' => 'Ἕ',
  'ἠ' => 'Ἠ',
  'ἡ' => 'Ἡ',
  'ἢ' => 'Ἢ',
  'ἣ' => 'Ἣ',
  'ἤ' => 'Ἤ',
  'ἥ' => 'Ἥ',
  'ἦ' => 'Ἦ',
  'ἧ' => 'Ἧ',
  'ἰ' => 'Ἰ',
  'ἱ' => 'Ἱ',
  'ἲ' => 'Ἲ',
  'ἳ' => 'Ἳ',
  'ἴ' => 'Ἴ',
  'ἵ' => 'Ἵ',
  'ἶ' => 'Ἶ',
  'ἷ' => 'Ἷ',
  'ὀ' => 'Ὀ',
  'ὁ' => 'Ὁ',
  'ὂ' => 'Ὂ',
  'ὃ' => 'Ὃ',
  'ὄ' => 'Ὄ',
  'ὅ' => 'Ὅ',
  'ὑ' => 'Ὑ',
  'ὓ' => 'Ὓ',
  'ὕ' => 'Ὕ',
  'ὗ' => 'Ὗ',
  'ὠ' => 'Ὠ',
  'ὡ' => 'Ὡ',
  'ὢ' => 'Ὢ',
  'ὣ' => 'Ὣ',
  'ὤ' => 'Ὤ',
  'ὥ' => 'Ὥ',
  'ὦ' => 'Ὦ',
  'ὧ' => 'Ὧ',
  'ὰ' => 'Ὰ',
  'ά' => 'Ά',
  'ὲ' => 'Ὲ',
  'έ' => 'Έ',
  'ὴ' => 'Ὴ',
  'ή' => 'Ή',
  'ὶ' => 'Ὶ',
  'ί' => 'Ί',
  'ὸ' => 'Ὸ',
  'ό' => 'Ό',
  'ὺ' => 'Ὺ',
  'ύ' => 'Ύ',
  'ὼ' => 'Ὼ',
  'ώ' => 'Ώ',
  'ᾀ' => 'ἈΙ',
  'ᾁ' => 'ἉΙ',
  'ᾂ' => 'ἊΙ',
  'ᾃ' => 'ἋΙ',
  'ᾄ' => 'ἌΙ',
  'ᾅ' => 'ἍΙ',
  'ᾆ' => 'ἎΙ',
  'ᾇ' => 'ἏΙ',
  'ᾐ' => 'ἨΙ',
  'ᾑ' => 'ἩΙ',
  'ᾒ' => 'ἪΙ',
  'ᾓ' => 'ἫΙ',
  'ᾔ' => 'ἬΙ',
  'ᾕ' => 'ἭΙ',
  'ᾖ' => 'ἮΙ',
  'ᾗ' => 'ἯΙ',
  'ᾠ' => 'ὨΙ',
  'ᾡ' => 'ὩΙ',
  'ᾢ' => 'ὪΙ',
  'ᾣ' => 'ὫΙ',
  'ᾤ' => 'ὬΙ',
  'ᾥ' => 'ὭΙ',
  'ᾦ' => 'ὮΙ',
  'ᾧ' => 'ὯΙ',
  'ᾰ' => 'Ᾰ',
  'ᾱ' => 'Ᾱ',
  'ᾳ' => 'ΑΙ',
  'ι' => 'Ι',
  'ῃ' => 'ΗΙ',
  'ῐ' => 'Ῐ',
  'ῑ' => 'Ῑ',
  'ῠ' => 'Ῠ',
  'ῡ' => 'Ῡ',
  'ῥ' => 'Ῥ',
  'ῳ' => 'ΩΙ',
  'ⅎ' => 'Ⅎ',
  'ⅰ' => 'Ⅰ',
  'ⅱ' => 'Ⅱ',
  'ⅲ' => 'Ⅲ',
  'ⅳ' => 'Ⅳ',
  'ⅴ' => 'Ⅴ',
  'ⅵ' => 'Ⅵ',
  'ⅶ' => 'Ⅶ',
  'ⅷ' => 'Ⅷ',
  'ⅸ' => 'Ⅸ',
  'ⅹ' => 'Ⅹ',
  'ⅺ' => 'Ⅺ',
  'ⅻ' => 'Ⅻ',
  'ⅼ' => 'Ⅼ',
  'ⅽ' => 'Ⅽ',
  'ⅾ' => 'Ⅾ',
  'ⅿ' => 'Ⅿ',
  'ↄ' => 'Ↄ',
  'ⓐ' => 'Ⓐ',
  'ⓑ' => 'Ⓑ',
  'ⓒ' => 'Ⓒ',
  'ⓓ' => 'Ⓓ',
  'ⓔ' => 'Ⓔ',
  'ⓕ' => 'Ⓕ',
  'ⓖ' => 'Ⓖ',
  'ⓗ' => 'Ⓗ',
  'ⓘ' => 'Ⓘ',
  'ⓙ' => 'Ⓙ',
  'ⓚ' => 'Ⓚ',
  'ⓛ' => 'Ⓛ',
  'ⓜ' => 'Ⓜ',
  'ⓝ' => 'Ⓝ',
  'ⓞ' => 'Ⓞ',
  'ⓟ' => 'Ⓟ',
  'ⓠ' => 'Ⓠ',
  'ⓡ' => 'Ⓡ',
  'ⓢ' => 'Ⓢ',
  'ⓣ' => 'Ⓣ',
  'ⓤ' => 'Ⓤ',
  'ⓥ' => 'Ⓥ',
  'ⓦ' => 'Ⓦ',
  'ⓧ' => 'Ⓧ',
  'ⓨ' => 'Ⓨ',
  'ⓩ' => 'Ⓩ',
  'ⰰ' => 'Ⰰ',
  'ⰱ' => 'Ⰱ',
  'ⰲ' => 'Ⰲ',
  'ⰳ' => 'Ⰳ',
  'ⰴ' => 'Ⰴ',
  'ⰵ' => 'Ⰵ',
  'ⰶ' => 'Ⰶ',
  'ⰷ' => 'Ⰷ',
  'ⰸ' => 'Ⰸ',
  'ⰹ' => 'Ⰹ',
  'ⰺ' => 'Ⰺ',
  'ⰻ' => 'Ⰻ',
  'ⰼ' => 'Ⰼ',
  'ⰽ' => 'Ⰽ',
  'ⰾ' => 'Ⰾ',
  'ⰿ' => 'Ⰿ',
  'ⱀ' => 'Ⱀ',
  'ⱁ' => 'Ⱁ',
  'ⱂ' => 'Ⱂ',
  'ⱃ' => 'Ⱃ',
  'ⱄ' => 'Ⱄ',
  'ⱅ' => 'Ⱅ',
  'ⱆ' => 'Ⱆ',
  'ⱇ' => 'Ⱇ',
  'ⱈ' => 'Ⱈ',
  'ⱉ' => 'Ⱉ',
  'ⱊ' => 'Ⱊ',
  'ⱋ' => 'Ⱋ',
  'ⱌ' => 'Ⱌ',
  'ⱍ' => 'Ⱍ',
  'ⱎ' => 'Ⱎ',
  'ⱏ' => 'Ⱏ',
  'ⱐ' => 'Ⱐ',
  'ⱑ' => 'Ⱑ',
  'ⱒ' => 'Ⱒ',
  'ⱓ' => 'Ⱓ',
  'ⱔ' => 'Ⱔ',
  'ⱕ' => 'Ⱕ',
  'ⱖ' => 'Ⱖ',
  'ⱗ' => 'Ⱗ',
  'ⱘ' => 'Ⱘ',
  'ⱙ' => 'Ⱙ',
  'ⱚ' => 'Ⱚ',
  'ⱛ' => 'Ⱛ',
  'ⱜ' => 'Ⱜ',
  'ⱝ' => 'Ⱝ',
  'ⱞ' => 'Ⱞ',
  'ⱡ' => 'Ⱡ',
  'ⱥ' => 'Ⱥ',
  'ⱦ' => 'Ⱦ',
  'ⱨ' => 'Ⱨ',
  'ⱪ' => 'Ⱪ',
  'ⱬ' => 'Ⱬ',
  'ⱳ' => 'Ⱳ',
  'ⱶ' => 'Ⱶ',
  'ⲁ' => 'Ⲁ',
  'ⲃ' => 'Ⲃ',
  'ⲅ' => 'Ⲅ',
  'ⲇ' => 'Ⲇ',
  'ⲉ' => 'Ⲉ',
  'ⲋ' => 'Ⲋ',
  'ⲍ' => 'Ⲍ',
  'ⲏ' => 'Ⲏ',
  'ⲑ' => 'Ⲑ',
  'ⲓ' => 'Ⲓ',
  'ⲕ' => 'Ⲕ',
  'ⲗ' => 'Ⲗ',
  'ⲙ' => 'Ⲙ',
  'ⲛ' => 'Ⲛ',
  'ⲝ' => 'Ⲝ',
  'ⲟ' => 'Ⲟ',
  'ⲡ' => 'Ⲡ',
  'ⲣ' => 'Ⲣ',
  'ⲥ' => 'Ⲥ',
  'ⲧ' => 'Ⲧ',
  'ⲩ' => 'Ⲩ',
  'ⲫ' => 'Ⲫ',
  'ⲭ' => 'Ⲭ',
  'ⲯ' => 'Ⲯ',
  'ⲱ' => 'Ⲱ',
  'ⲳ' => 'Ⲳ',
  'ⲵ' => 'Ⲵ',
  'ⲷ' => 'Ⲷ',
  'ⲹ' => 'Ⲹ',
  'ⲻ' => 'Ⲻ',
  'ⲽ' => 'Ⲽ',
  'ⲿ' => 'Ⲿ',
  'ⳁ' => 'Ⳁ',
  'ⳃ' => 'Ⳃ',
  'ⳅ' => 'Ⳅ',
  'ⳇ' => 'Ⳇ',
  'ⳉ' => 'Ⳉ',
  'ⳋ' => 'Ⳋ',
  'ⳍ' => 'Ⳍ',
  'ⳏ' => 'Ⳏ',
  'ⳑ' => 'Ⳑ',
  'ⳓ' => 'Ⳓ',
  'ⳕ' => 'Ⳕ',
  'ⳗ' => 'Ⳗ',
  'ⳙ' => 'Ⳙ',
  'ⳛ' => 'Ⳛ',
  'ⳝ' => 'Ⳝ',
  'ⳟ' => 'Ⳟ',
  'ⳡ' => 'Ⳡ',
  'ⳣ' => 'Ⳣ',
  'ⳬ' => 'Ⳬ',
  'ⳮ' => 'Ⳮ',
  'ⳳ' => 'Ⳳ',
  'ⴀ' => 'Ⴀ',
  'ⴁ' => 'Ⴁ',
  'ⴂ' => 'Ⴂ',
  'ⴃ' => 'Ⴃ',
  'ⴄ' => 'Ⴄ',
  'ⴅ' => 'Ⴅ',
  'ⴆ' => 'Ⴆ',
  'ⴇ' => 'Ⴇ',
  'ⴈ' => 'Ⴈ',
  'ⴉ' => 'Ⴉ',
  'ⴊ' => 'Ⴊ',
  'ⴋ' => 'Ⴋ',
  'ⴌ' => 'Ⴌ',
  'ⴍ' => 'Ⴍ',
  'ⴎ' => 'Ⴎ',
  'ⴏ' => 'Ⴏ',
  'ⴐ' => 'Ⴐ',
  'ⴑ' => 'Ⴑ',
  'ⴒ' => 'Ⴒ',
  'ⴓ' => 'Ⴓ',
  'ⴔ' => 'Ⴔ',
  'ⴕ' => 'Ⴕ',
  'ⴖ' => 'Ⴖ',
  'ⴗ' => 'Ⴗ',
  'ⴘ' => 'Ⴘ',
  'ⴙ' => 'Ⴙ',
  'ⴚ' => 'Ⴚ',
  'ⴛ' => 'Ⴛ',
  'ⴜ' => 'Ⴜ',
  'ⴝ' => 'Ⴝ',
  'ⴞ' => 'Ⴞ',
  'ⴟ' => 'Ⴟ',
  'ⴠ' => 'Ⴠ',
  'ⴡ' => 'Ⴡ',
  'ⴢ' => 'Ⴢ',
  'ⴣ' => 'Ⴣ',
  'ⴤ' => 'Ⴤ',
  'ⴥ' => 'Ⴥ',
  'ⴧ' => 'Ⴧ',
  'ⴭ' => 'Ⴭ',
  'ꙁ' => 'Ꙁ',
  'ꙃ' => 'Ꙃ',
  'ꙅ' => 'Ꙅ',
  'ꙇ' => 'Ꙇ',
  'ꙉ' => 'Ꙉ',
  'ꙋ' => 'Ꙋ',
  'ꙍ' => 'Ꙍ',
  'ꙏ' => 'Ꙏ',
  'ꙑ' => 'Ꙑ',
  'ꙓ' => 'Ꙓ',
  'ꙕ' => 'Ꙕ',
  'ꙗ' => 'Ꙗ',
  'ꙙ' => 'Ꙙ',
  'ꙛ' => 'Ꙛ',
  'ꙝ' => 'Ꙝ',
  'ꙟ' => 'Ꙟ',
  'ꙡ' => 'Ꙡ',
  'ꙣ' => 'Ꙣ',
  'ꙥ' => 'Ꙥ',
  'ꙧ' => 'Ꙧ',
  'ꙩ' => 'Ꙩ',
  'ꙫ' => 'Ꙫ',
  'ꙭ' => 'Ꙭ',
  'ꚁ' => 'Ꚁ',
  'ꚃ' => 'Ꚃ',
  'ꚅ' => 'Ꚅ',
  'ꚇ' => 'Ꚇ',
  'ꚉ' => 'Ꚉ',
  'ꚋ' => 'Ꚋ',
  'ꚍ' => 'Ꚍ',
  'ꚏ' => 'Ꚏ',
  'ꚑ' => 'Ꚑ',
  'ꚓ' => 'Ꚓ',
  'ꚕ' => 'Ꚕ',
  'ꚗ' => 'Ꚗ',
  'ꚙ' => 'Ꚙ',
  'ꚛ' => 'Ꚛ',
  'ꜣ' => 'Ꜣ',
  'ꜥ' => 'Ꜥ',
  'ꜧ' => 'Ꜧ',
  'ꜩ' => 'Ꜩ',
  'ꜫ' => 'Ꜫ',
  'ꜭ' => 'Ꜭ',
  'ꜯ' => 'Ꜯ',
  'ꜳ' => 'Ꜳ',
  'ꜵ' => 'Ꜵ',
  'ꜷ' => 'Ꜷ',
  'ꜹ' => 'Ꜹ',
  'ꜻ' => 'Ꜻ',
  'ꜽ' => 'Ꜽ',
  'ꜿ' => 'Ꜿ',
  'ꝁ' => 'Ꝁ',
  'ꝃ' => 'Ꝃ',
  'ꝅ' => 'Ꝅ',
  'ꝇ' => 'Ꝇ',
  'ꝉ' => 'Ꝉ',
  'ꝋ' => 'Ꝋ',
  'ꝍ' => 'Ꝍ',
  'ꝏ' => 'Ꝏ',
  'ꝑ' => 'Ꝑ',
  'ꝓ' => 'Ꝓ',
  'ꝕ' => 'Ꝕ',
  'ꝗ' => 'Ꝗ',
  'ꝙ' => 'Ꝙ',
  'ꝛ' => 'Ꝛ',
  'ꝝ' => 'Ꝝ',
  'ꝟ' => 'Ꝟ',
  'ꝡ' => 'Ꝡ',
  'ꝣ' => 'Ꝣ',
  'ꝥ' => 'Ꝥ',
  'ꝧ' => 'Ꝧ',
  'ꝩ' => 'Ꝩ',
  'ꝫ' => 'Ꝫ',
  'ꝭ' => 'Ꝭ',
  'ꝯ' => 'Ꝯ',
  'ꝺ' => 'Ꝺ',
  'ꝼ' => 'Ꝼ',
  'ꝿ' => 'Ꝿ',
  'ꞁ' => 'Ꞁ',
  'ꞃ' => 'Ꞃ',
  'ꞅ' => 'Ꞅ',
  'ꞇ' => 'Ꞇ',
  'ꞌ' => 'Ꞌ',
  'ꞑ' => 'Ꞑ',
  'ꞓ' => 'Ꞓ',
  'ꞔ' => 'Ꞔ',
  'ꞗ' => 'Ꞗ',
  'ꞙ' => 'Ꞙ',
  'ꞛ' => 'Ꞛ',
  'ꞝ' => 'Ꞝ',
  'ꞟ' => 'Ꞟ',
  'ꞡ' => 'Ꞡ',
  'ꞣ' => 'Ꞣ',
  'ꞥ' => 'Ꞥ',
  'ꞧ' => 'Ꞧ',
  'ꞩ' => 'Ꞩ',
  'ꞵ' => 'Ꞵ',
  'ꞷ' => 'Ꞷ',
  'ꞹ' => 'Ꞹ',
  'ꞻ' => 'Ꞻ',
  'ꞽ' => 'Ꞽ',
  'ꞿ' => 'Ꞿ',
  'ꟃ' => 'Ꟃ',
  'ꟈ' => 'Ꟈ',
  'ꟊ' => 'Ꟊ',
  'ꟶ' => 'Ꟶ',
  'ꭓ' => 'Ꭓ',
  'ꭰ' => 'Ꭰ',
  'ꭱ' => 'Ꭱ',
  'ꭲ' => 'Ꭲ',
  'ꭳ' => 'Ꭳ',
  'ꭴ' => 'Ꭴ',
  'ꭵ' => 'Ꭵ',
  'ꭶ' => 'Ꭶ',
  'ꭷ' => 'Ꭷ',
  'ꭸ' => 'Ꭸ',
  'ꭹ' => 'Ꭹ',
  'ꭺ' => 'Ꭺ',
  'ꭻ' => 'Ꭻ',
  'ꭼ' => 'Ꭼ',
  'ꭽ' => 'Ꭽ',
  'ꭾ' => 'Ꭾ',
  'ꭿ' => 'Ꭿ',
  'ꮀ' => 'Ꮀ',
  'ꮁ' => 'Ꮁ',
  'ꮂ' => 'Ꮂ',
  'ꮃ' => 'Ꮃ',
  'ꮄ' => 'Ꮄ',
  'ꮅ' => 'Ꮅ',
  'ꮆ' => 'Ꮆ',
  'ꮇ' => 'Ꮇ',
  'ꮈ' => 'Ꮈ',
  'ꮉ' => 'Ꮉ',
  'ꮊ' => 'Ꮊ',
  'ꮋ' => 'Ꮋ',
  'ꮌ' => 'Ꮌ',
  'ꮍ' => 'Ꮍ',
  'ꮎ' => 'Ꮎ',
  'ꮏ' => 'Ꮏ',
  'ꮐ' => 'Ꮐ',
  'ꮑ' => 'Ꮑ',
  'ꮒ' => 'Ꮒ',
  'ꮓ' => 'Ꮓ',
  'ꮔ' => 'Ꮔ',
  'ꮕ' => 'Ꮕ',
  'ꮖ' => 'Ꮖ',
  'ꮗ' => 'Ꮗ',
  'ꮘ' => 'Ꮘ',
  'ꮙ' => 'Ꮙ',
  'ꮚ' => 'Ꮚ',
  'ꮛ' => 'Ꮛ',
  'ꮜ' => 'Ꮜ',
  'ꮝ' => 'Ꮝ',
  'ꮞ' => 'Ꮞ',
  'ꮟ' => 'Ꮟ',
  'ꮠ' => 'Ꮠ',
  'ꮡ' => 'Ꮡ',
  'ꮢ' => 'Ꮢ',
  'ꮣ' => 'Ꮣ',
  'ꮤ' => 'Ꮤ',
  'ꮥ' => 'Ꮥ',
  'ꮦ' => 'Ꮦ',
  'ꮧ' => 'Ꮧ',
  'ꮨ' => 'Ꮨ',
  'ꮩ' => 'Ꮩ',
  'ꮪ' => 'Ꮪ',
  'ꮫ' => 'Ꮫ',
  'ꮬ' => 'Ꮬ',
  'ꮭ' => 'Ꮭ',
  'ꮮ' => 'Ꮮ',
  'ꮯ' => 'Ꮯ',
  'ꮰ' => 'Ꮰ',
  'ꮱ' => 'Ꮱ',
  'ꮲ' => 'Ꮲ',
  'ꮳ' => 'Ꮳ',
  'ꮴ' => 'Ꮴ',
  'ꮵ' => 'Ꮵ',
  'ꮶ' => 'Ꮶ',
  'ꮷ' => 'Ꮷ',
  'ꮸ' => 'Ꮸ',
  'ꮹ' => 'Ꮹ',
  'ꮺ' => 'Ꮺ',
  'ꮻ' => 'Ꮻ',
  'ꮼ' => 'Ꮼ',
  'ꮽ' => 'Ꮽ',
  'ꮾ' => 'Ꮾ',
  'ꮿ' => 'Ꮿ',
  'ａ' => 'Ａ',
  'ｂ' => 'Ｂ',
  'ｃ' => 'Ｃ',
  'ｄ' => 'Ｄ',
  'ｅ' => 'Ｅ',
  'ｆ' => 'Ｆ',
  'ｇ' => 'Ｇ',
  'ｈ' => 'Ｈ',
  'ｉ' => 'Ｉ',
  'ｊ' => 'Ｊ',
  'ｋ' => 'Ｋ',
  'ｌ' => 'Ｌ',
  'ｍ' => 'Ｍ',
  'ｎ' => 'Ｎ',
  'ｏ' => 'Ｏ',
  'ｐ' => 'Ｐ',
  'ｑ' => 'Ｑ',
  'ｒ' => 'Ｒ',
  'ｓ' => 'Ｓ',
  'ｔ' => 'Ｔ',
  'ｕ' => 'Ｕ',
  'ｖ' => 'Ｖ',
  'ｗ' => 'Ｗ',
  'ｘ' => 'Ｘ',
  'ｙ' => 'Ｙ',
  'ｚ' => 'Ｚ',
  '𐐨' => '𐐀',
  '𐐩' => '𐐁',
  '𐐪' => '𐐂',
  '𐐫' => '𐐃',
  '𐐬' => '𐐄',
  '𐐭' => '𐐅',
  '𐐮' => '𐐆',
  '𐐯' => '𐐇',
  '𐐰' => '𐐈',
  '𐐱' => '𐐉',
  '𐐲' => '𐐊',
  '𐐳' => '𐐋',
  '𐐴' => '𐐌',
  '𐐵' => '𐐍',
  '𐐶' => '𐐎',
  '𐐷' => '𐐏',
  '𐐸' => '𐐐',
  '𐐹' => '𐐑',
  '𐐺' => '𐐒',
  '𐐻' => '𐐓',
  '𐐼' => '𐐔',
  '𐐽' => '𐐕',
  '𐐾' => '𐐖',
  '𐐿' => '𐐗',
  '𐑀' => '𐐘',
  '𐑁' => '𐐙',
  '𐑂' => '𐐚',
  '𐑃' => '𐐛',
  '𐑄' => '𐐜',
  '𐑅' => '𐐝',
  '𐑆' => '𐐞',
  '𐑇' => '𐐟',
  '𐑈' => '𐐠',
  '𐑉' => '𐐡',
  '𐑊' => '𐐢',
  '𐑋' => '𐐣',
  '𐑌' => '𐐤',
  '𐑍' => '𐐥',
  '𐑎' => '𐐦',
  '𐑏' => '𐐧',
  '𐓘' => '𐒰',
  '𐓙' => '𐒱',
  '𐓚' => '𐒲',
  '𐓛' => '𐒳',
  '𐓜' => '𐒴',
  '𐓝' => '𐒵',
  '𐓞' => '𐒶',
  '𐓟' => '𐒷',
  '𐓠' => '𐒸',
  '𐓡' => '𐒹',
  '𐓢' => '𐒺',
  '𐓣' => '𐒻',
  '𐓤' => '𐒼',
  '𐓥' => '𐒽',
  '𐓦' => '𐒾',
  '𐓧' => '𐒿',
  '𐓨' => '𐓀',
  '𐓩' => '𐓁',
  '𐓪' => '𐓂',
  '𐓫' => '𐓃',
  '𐓬' => '𐓄',
  '𐓭' => '𐓅',
  '𐓮' => '𐓆',
  '𐓯' => '𐓇',
  '𐓰' => '𐓈',
  '𐓱' => '𐓉',
  '𐓲' => '𐓊',
  '𐓳' => '𐓋',
  '𐓴' => '𐓌',
  '𐓵' => '𐓍',
  '𐓶' => '𐓎',
  '𐓷' => '𐓏',
  '𐓸' => '𐓐',
  '𐓹' => '𐓑',
  '𐓺' => '𐓒',
  '𐓻' => '𐓓',
  '𐳀' => '𐲀',
  '𐳁' => '𐲁',
  '𐳂' => '𐲂',
  '𐳃' => '𐲃',
  '𐳄' => '𐲄',
  '𐳅' => '𐲅',
  '𐳆' => '𐲆',
  '𐳇' => '𐲇',
  '𐳈' => '𐲈',
  '𐳉' => '𐲉',
  '𐳊' => '𐲊',
  '𐳋' => '𐲋',
  '𐳌' => '𐲌',
  '𐳍' => '𐲍',
  '𐳎' => '𐲎',
  '𐳏' => '𐲏',
  '𐳐' => '𐲐',
  '𐳑' => '𐲑',
  '𐳒' => '𐲒',
  '𐳓' => '𐲓',
  '𐳔' => '𐲔',
  '𐳕' => '𐲕',
  '𐳖' => '𐲖',
  '𐳗' => '𐲗',
  '𐳘' => '𐲘',
  '𐳙' => '𐲙',
  '𐳚' => '𐲚',
  '𐳛' => '𐲛',
  '𐳜' => '𐲜',
  '𐳝' => '𐲝',
  '𐳞' => '𐲞',
  '𐳟' => '𐲟',
  '𐳠' => '𐲠',
  '𐳡' => '𐲡',
  '𐳢' => '𐲢',
  '𐳣' => '𐲣',
  '𐳤' => '𐲤',
  '𐳥' => '𐲥',
  '𐳦' => '𐲦',
  '𐳧' => '𐲧',
  '𐳨' => '𐲨',
  '𐳩' => '𐲩',
  '𐳪' => '𐲪',
  '𐳫' => '𐲫',
  '𐳬' => '𐲬',
  '𐳭' => '𐲭',
  '𐳮' => '𐲮',
  '𐳯' => '𐲯',
  '𐳰' => '𐲰',
  '𐳱' => '𐲱',
  '𐳲' => '𐲲',
  '𑣀' => '𑢠',
  '𑣁' => '𑢡',
  '𑣂' => '𑢢',
  '𑣃' => '𑢣',
  '𑣄' => '𑢤',
  '𑣅' => '𑢥',
  '𑣆' => '𑢦',
  '𑣇' => '𑢧',
  '𑣈' => '𑢨',
  '𑣉' => '𑢩',
  '𑣊' => '𑢪',
  '𑣋' => '𑢫',
  '𑣌' => '𑢬',
  '𑣍' => '𑢭',
  '𑣎' => '𑢮',
  '𑣏' => '𑢯',
  '𑣐' => '𑢰',
  '𑣑' => '𑢱',
  '𑣒' => '𑢲',
  '𑣓' => '𑢳',
  '𑣔' => '𑢴',
  '𑣕' => '𑢵',
  '𑣖' => '𑢶',
  '𑣗' => '𑢷',
  '𑣘' => '𑢸',
  '𑣙' => '𑢹',
  '𑣚' => '𑢺',
  '𑣛' => '𑢻',
  '𑣜' => '𑢼',
  '𑣝' => '𑢽',
  '𑣞' => '𑢾',
  '𑣟' => '𑢿',
  '𖹠' => '𖹀',
  '𖹡' => '𖹁',
  '𖹢' => '𖹂',
  '𖹣' => '𖹃',
  '𖹤' => '𖹄',
  '𖹥' => '𖹅',
  '𖹦' => '𖹆',
  '𖹧' => '𖹇',
  '𖹨' => '𖹈',
  '𖹩' => '𖹉',
  '𖹪' => '𖹊',
  '𖹫' => '𖹋',
  '𖹬' => '𖹌',
  '𖹭' => '𖹍',
  '𖹮' => '𖹎',
  '𖹯' => '𖹏',
  '𖹰' => '𖹐',
  '𖹱' => '𖹑',
  '𖹲' => '𖹒',
  '𖹳' => '𖹓',
  '𖹴' => '𖹔',
  '𖹵' => '𖹕',
  '𖹶' => '𖹖',
  '𖹷' => '𖹗',
  '𖹸' => '𖹘',
  '𖹹' => '𖹙',
  '𖹺' => '𖹚',
  '𖹻' => '𖹛',
  '𖹼' => '𖹜',
  '𖹽' => '𖹝',
  '𖹾' => '𖹞',
  '𖹿' => '𖹟',
  '𞤢' => '𞤀',
  '𞤣' => '𞤁',
  '𞤤' => '𞤂',
  '𞤥' => '𞤃',
  '𞤦' => '𞤄',
  '𞤧' => '𞤅',
  '𞤨' => '𞤆',
  '𞤩' => '𞤇',
  '𞤪' => '𞤈',
  '𞤫' => '𞤉',
  '𞤬' => '𞤊',
  '𞤭' => '𞤋',
  '𞤮' => '𞤌',
  '𞤯' => '𞤍',
  '𞤰' => '𞤎',
  '𞤱' => '𞤏',
  '𞤲' => '𞤐',
  '𞤳' => '𞤑',
  '𞤴' => '𞤒',
  '𞤵' => '𞤓',
  '𞤶' => '𞤔',
  '𞤷' => '𞤕',
  '𞤸' => '𞤖',
  '𞤹' => '𞤗',
  '𞤺' => '𞤘',
  '𞤻' => '𞤙',
  '𞤼' => '𞤚',
  '𞤽' => '𞤛',
  '𞤾' => '𞤜',
  '𞤿' => '𞤝',
  '𞥀' => '𞤞',
  '𞥁' => '𞤟',
  '𞥂' => '𞤠',
  '𞥃' => '𞤡',
  'ß' => 'SS',
  'ﬀ' => 'FF',
  'ﬁ' => 'FI',
  'ﬂ' => 'FL',
  'ﬃ' => 'FFI',
  'ﬄ' => 'FFL',
  'ﬅ' => 'ST',
  'ﬆ' => 'ST',
  'և' => 'ԵՒ',
  'ﬓ' => 'ՄՆ',
  'ﬔ' => 'ՄԵ',
  'ﬕ' => 'ՄԻ',
  'ﬖ' => 'ՎՆ',
  'ﬗ' => 'ՄԽ',
  'ŉ' => 'ʼN',
  'ΐ' => 'Ϊ́',
  'ΰ' => 'Ϋ́',
  'ǰ' => 'J̌',
  'ẖ' => 'H̱',
  'ẗ' => 'T̈',
  'ẘ' => 'W̊',
  'ẙ' => 'Y̊',
  'ẚ' => 'Aʾ',
  'ὐ' => 'Υ̓',
  'ὒ' => 'Υ̓̀',
  'ὔ' => 'Υ̓́',
  'ὖ' => 'Υ̓͂',
  'ᾶ' => 'Α͂',
  'ῆ' => 'Η͂',
  'ῒ' => 'Ϊ̀',
  'ΐ' => 'Ϊ́',
  'ῖ' => 'Ι͂',
  'ῗ' => 'Ϊ͂',
  'ῢ' => 'Ϋ̀',
  'ΰ' => 'Ϋ́',
  'ῤ' => 'Ρ̓',
  'ῦ' => 'Υ͂',
  'ῧ' => 'Ϋ͂',
  'ῶ' => 'Ω͂',
  'ᾈ' => 'ἈΙ',
  'ᾉ' => 'ἉΙ',
  'ᾊ' => 'ἊΙ',
  'ᾋ' => 'ἋΙ',
  'ᾌ' => 'ἌΙ',
  'ᾍ' => 'ἍΙ',
  'ᾎ' => 'ἎΙ',
  'ᾏ' => 'ἏΙ',
  'ᾘ' => 'ἨΙ',
  'ᾙ' => 'ἩΙ',
  'ᾚ' => 'ἪΙ',
  'ᾛ' => 'ἫΙ',
  'ᾜ' => 'ἬΙ',
  'ᾝ' => 'ἭΙ',
  'ᾞ' => 'ἮΙ',
  'ᾟ' => 'ἯΙ',
  'ᾨ' => 'ὨΙ',
  'ᾩ' => 'ὩΙ',
  'ᾪ' => 'ὪΙ',
  'ᾫ' => 'ὫΙ',
  'ᾬ' => 'ὬΙ',
  'ᾭ' => 'ὭΙ',
  'ᾮ' => 'ὮΙ',
  'ᾯ' => 'ὯΙ',
  'ᾼ' => 'ΑΙ',
  'ῌ' => 'ΗΙ',
  'ῼ' => 'ΩΙ',
  'ᾲ' => 'ᾺΙ',
  'ᾴ' => 'ΆΙ',
  'ῂ' => 'ῊΙ',
  'ῄ' => 'ΉΙ',
  'ῲ' => 'ῺΙ',
  'ῴ' => 'ΏΙ',
  'ᾷ' => 'Α͂Ι',
  'ῇ' => 'Η͂Ι',
  'ῷ' => 'Ω͂Ι',
);
<?php

// from Case_Ignorable in https://unicode.org/Public/UNIDATA/DerivedCoreProperties.txt

return '/(?<![\x{0027}\x{002E}\x{003A}\x{005E}\x{0060}\x{00A8}\x{00AD}\x{00AF}\x{00B4}\x{00B7}\x{00B8}\x{02B0}-\x{02C1}\x{02C2}-\x{02C5}\x{02C6}-\x{02D1}\x{02D2}-\x{02DF}\x{02E0}-\x{02E4}\x{02E5}-\x{02EB}\x{02EC}\x{02ED}\x{02EE}\x{02EF}-\x{02FF}\x{0300}-\x{036F}\x{0374}\x{0375}\x{037A}\x{0384}-\x{0385}\x{0387}\x{0483}-\x{0487}\x{0488}-\x{0489}\x{0559}\x{0591}-\x{05BD}\x{05BF}\x{05C1}-\x{05C2}\x{05C4}-\x{05C5}\x{05C7}\x{05F4}\x{0600}-\x{0605}\x{0610}-\x{061A}\x{061C}\x{0640}\x{064B}-\x{065F}\x{0670}\x{06D6}-\x{06DC}\x{06DD}\x{06DF}-\x{06E4}\x{06E5}-\x{06E6}\x{06E7}-\x{06E8}\x{06EA}-\x{06ED}\x{070F}\x{0711}\x{0730}-\x{074A}\x{07A6}-\x{07B0}\x{07EB}-\x{07F3}\x{07F4}-\x{07F5}\x{07FA}\x{07FD}\x{0816}-\x{0819}\x{081A}\x{081B}-\x{0823}\x{0824}\x{0825}-\x{0827}\x{0828}\x{0829}-\x{082D}\x{0859}-\x{085B}\x{08D3}-\x{08E1}\x{08E2}\x{08E3}-\x{0902}\x{093A}\x{093C}\x{0941}-\x{0948}\x{094D}\x{0951}-\x{0957}\x{0962}-\x{0963}\x{0971}\x{0981}\x{09BC}\x{09C1}-\x{09C4}\x{09CD}\x{09E2}-\x{09E3}\x{09FE}\x{0A01}-\x{0A02}\x{0A3C}\x{0A41}-\x{0A42}\x{0A47}-\x{0A48}\x{0A4B}-\x{0A4D}\x{0A51}\x{0A70}-\x{0A71}\x{0A75}\x{0A81}-\x{0A82}\x{0ABC}\x{0AC1}-\x{0AC5}\x{0AC7}-\x{0AC8}\x{0ACD}\x{0AE2}-\x{0AE3}\x{0AFA}-\x{0AFF}\x{0B01}\x{0B3C}\x{0B3F}\x{0B41}-\x{0B44}\x{0B4D}\x{0B56}\x{0B62}-\x{0B63}\x{0B82}\x{0BC0}\x{0BCD}\x{0C00}\x{0C04}\x{0C3E}-\x{0C40}\x{0C46}-\x{0C48}\x{0C4A}-\x{0C4D}\x{0C55}-\x{0C56}\x{0C62}-\x{0C63}\x{0C81}\x{0CBC}\x{0CBF}\x{0CC6}\x{0CCC}-\x{0CCD}\x{0CE2}-\x{0CE3}\x{0D00}-\x{0D01}\x{0D3B}-\x{0D3C}\x{0D41}-\x{0D44}\x{0D4D}\x{0D62}-\x{0D63}\x{0DCA}\x{0DD2}-\x{0DD4}\x{0DD6}\x{0E31}\x{0E34}-\x{0E3A}\x{0E46}\x{0E47}-\x{0E4E}\x{0EB1}\x{0EB4}-\x{0EB9}\x{0EBB}-\x{0EBC}\x{0EC6}\x{0EC8}-\x{0ECD}\x{0F18}-\x{0F19}\x{0F35}\x{0F37}\x{0F39}\x{0F71}-\x{0F7E}\x{0F80}-\x{0F84}\x{0F86}-\x{0F87}\x{0F8D}-\x{0F97}\x{0F99}-\x{0FBC}\x{0FC6}\x{102D}-\x{1030}\x{1032}-\x{1037}\x{1039}-\x{103A}\x{103D}-\x{103E}\x{1058}-\x{1059}\x{105E}-\x{1060}\x{1071}-\x{1074}\x{1082}\x{1085}-\x{1086}\x{108D}\x{109D}\x{10FC}\x{135D}-\x{135F}\x{1712}-\x{1714}\x{1732}-\x{1734}\x{1752}-\x{1753}\x{1772}-\x{1773}\x{17B4}-\x{17B5}\x{17B7}-\x{17BD}\x{17C6}\x{17C9}-\x{17D3}\x{17D7}\x{17DD}\x{180B}-\x{180D}\x{180E}\x{1843}\x{1885}-\x{1886}\x{18A9}\x{1920}-\x{1922}\x{1927}-\x{1928}\x{1932}\x{1939}-\x{193B}\x{1A17}-\x{1A18}\x{1A1B}\x{1A56}\x{1A58}-\x{1A5E}\x{1A60}\x{1A62}\x{1A65}-\x{1A6C}\x{1A73}-\x{1A7C}\x{1A7F}\x{1AA7}\x{1AB0}-\x{1ABD}\x{1ABE}\x{1B00}-\x{1B03}\x{1B34}\x{1B36}-\x{1B3A}\x{1B3C}\x{1B42}\x{1B6B}-\x{1B73}\x{1B80}-\x{1B81}\x{1BA2}-\x{1BA5}\x{1BA8}-\x{1BA9}\x{1BAB}-\x{1BAD}\x{1BE6}\x{1BE8}-\x{1BE9}\x{1BED}\x{1BEF}-\x{1BF1}\x{1C2C}-\x{1C33}\x{1C36}-\x{1C37}\x{1C78}-\x{1C7D}\x{1CD0}-\x{1CD2}\x{1CD4}-\x{1CE0}\x{1CE2}-\x{1CE8}\x{1CED}\x{1CF4}\x{1CF8}-\x{1CF9}\x{1D2C}-\x{1D6A}\x{1D78}\x{1D9B}-\x{1DBF}\x{1DC0}-\x{1DF9}\x{1DFB}-\x{1DFF}\x{1FBD}\x{1FBF}-\x{1FC1}\x{1FCD}-\x{1FCF}\x{1FDD}-\x{1FDF}\x{1FED}-\x{1FEF}\x{1FFD}-\x{1FFE}\x{200B}-\x{200F}\x{2018}\x{2019}\x{2024}\x{2027}\x{202A}-\x{202E}\x{2060}-\x{2064}\x{2066}-\x{206F}\x{2071}\x{207F}\x{2090}-\x{209C}\x{20D0}-\x{20DC}\x{20DD}-\x{20E0}\x{20E1}\x{20E2}-\x{20E4}\x{20E5}-\x{20F0}\x{2C7C}-\x{2C7D}\x{2CEF}-\x{2CF1}\x{2D6F}\x{2D7F}\x{2DE0}-\x{2DFF}\x{2E2F}\x{3005}\x{302A}-\x{302D}\x{3031}-\x{3035}\x{303B}\x{3099}-\x{309A}\x{309B}-\x{309C}\x{309D}-\x{309E}\x{30FC}-\x{30FE}\x{A015}\x{A4F8}-\x{A4FD}\x{A60C}\x{A66F}\x{A670}-\x{A672}\x{A674}-\x{A67D}\x{A67F}\x{A69C}-\x{A69D}\x{A69E}-\x{A69F}\x{A6F0}-\x{A6F1}\x{A700}-\x{A716}\x{A717}-\x{A71F}\x{A720}-\x{A721}\x{A770}\x{A788}\x{A789}-\x{A78A}\x{A7F8}-\x{A7F9}\x{A802}\x{A806}\x{A80B}\x{A825}-\x{A826}\x{A8C4}-\x{A8C5}\x{A8E0}-\x{A8F1}\x{A8FF}\x{A926}-\x{A92D}\x{A947}-\x{A951}\x{A980}-\x{A982}\x{A9B3}\x{A9B6}-\x{A9B9}\x{A9BC}\x{A9CF}\x{A9E5}\x{A9E6}\x{AA29}-\x{AA2E}\x{AA31}-\x{AA32}\x{AA35}-\x{AA36}\x{AA43}\x{AA4C}\x{AA70}\x{AA7C}\x{AAB0}\x{AAB2}-\x{AAB4}\x{AAB7}-\x{AAB8}\x{AABE}-\x{AABF}\x{AAC1}\x{AADD}\x{AAEC}-\x{AAED}\x{AAF3}-\x{AAF4}\x{AAF6}\x{AB5B}\x{AB5C}-\x{AB5F}\x{ABE5}\x{ABE8}\x{ABED}\x{FB1E}\x{FBB2}-\x{FBC1}\x{FE00}-\x{FE0F}\x{FE13}\x{FE20}-\x{FE2F}\x{FE52}\x{FE55}\x{FEFF}\x{FF07}\x{FF0E}\x{FF1A}\x{FF3E}\x{FF40}\x{FF70}\x{FF9E}-\x{FF9F}\x{FFE3}\x{FFF9}-\x{FFFB}\x{101FD}\x{102E0}\x{10376}-\x{1037A}\x{10A01}-\x{10A03}\x{10A05}-\x{10A06}\x{10A0C}-\x{10A0F}\x{10A38}-\x{10A3A}\x{10A3F}\x{10AE5}-\x{10AE6}\x{10D24}-\x{10D27}\x{10F46}-\x{10F50}\x{11001}\x{11038}-\x{11046}\x{1107F}-\x{11081}\x{110B3}-\x{110B6}\x{110B9}-\x{110BA}\x{110BD}\x{110CD}\x{11100}-\x{11102}\x{11127}-\x{1112B}\x{1112D}-\x{11134}\x{11173}\x{11180}-\x{11181}\x{111B6}-\x{111BE}\x{111C9}-\x{111CC}\x{1122F}-\x{11231}\x{11234}\x{11236}-\x{11237}\x{1123E}\x{112DF}\x{112E3}-\x{112EA}\x{11300}-\x{11301}\x{1133B}-\x{1133C}\x{11340}\x{11366}-\x{1136C}\x{11370}-\x{11374}\x{11438}-\x{1143F}\x{11442}-\x{11444}\x{11446}\x{1145E}\x{114B3}-\x{114B8}\x{114BA}\x{114BF}-\x{114C0}\x{114C2}-\x{114C3}\x{115B2}-\x{115B5}\x{115BC}-\x{115BD}\x{115BF}-\x{115C0}\x{115DC}-\x{115DD}\x{11633}-\x{1163A}\x{1163D}\x{1163F}-\x{11640}\x{116AB}\x{116AD}\x{116B0}-\x{116B5}\x{116B7}\x{1171D}-\x{1171F}\x{11722}-\x{11725}\x{11727}-\x{1172B}\x{1182F}-\x{11837}\x{11839}-\x{1183A}\x{11A01}-\x{11A0A}\x{11A33}-\x{11A38}\x{11A3B}-\x{11A3E}\x{11A47}\x{11A51}-\x{11A56}\x{11A59}-\x{11A5B}\x{11A8A}-\x{11A96}\x{11A98}-\x{11A99}\x{11C30}-\x{11C36}\x{11C38}-\x{11C3D}\x{11C3F}\x{11C92}-\x{11CA7}\x{11CAA}-\x{11CB0}\x{11CB2}-\x{11CB3}\x{11CB5}-\x{11CB6}\x{11D31}-\x{11D36}\x{11D3A}\x{11D3C}-\x{11D3D}\x{11D3F}-\x{11D45}\x{11D47}\x{11D90}-\x{11D91}\x{11D95}\x{11D97}\x{11EF3}-\x{11EF4}\x{16AF0}-\x{16AF4}\x{16B30}-\x{16B36}\x{16B40}-\x{16B43}\x{16F8F}-\x{16F92}\x{16F93}-\x{16F9F}\x{16FE0}-\x{16FE1}\x{1BC9D}-\x{1BC9E}\x{1BCA0}-\x{1BCA3}\x{1D167}-\x{1D169}\x{1D173}-\x{1D17A}\x{1D17B}-\x{1D182}\x{1D185}-\x{1D18B}\x{1D1AA}-\x{1D1AD}\x{1D242}-\x{1D244}\x{1DA00}-\x{1DA36}\x{1DA3B}-\x{1DA6C}\x{1DA75}\x{1DA84}\x{1DA9B}-\x{1DA9F}\x{1DAA1}-\x{1DAAF}\x{1E000}-\x{1E006}\x{1E008}-\x{1E018}\x{1E01B}-\x{1E021}\x{1E023}-\x{1E024}\x{1E026}-\x{1E02A}\x{1E8D0}-\x{1E8D6}\x{1E944}-\x{1E94A}\x{1F3FB}-\x{1F3FF}\x{E0001}\x{E0020}-\x{E007F}\x{E0100}-\x{E01EF}])(\pL)(\pL*+)/u';
<?php

return [
    'İ' => 'i̇',
    'µ' => 'μ',
    'ſ' => 's',
    'ͅ' => 'ι',
    'ς' => 'σ',
    'ϐ' => 'β',
    'ϑ' => 'θ',
    'ϕ' => 'φ',
    'ϖ' => 'π',
    'ϰ' => 'κ',
    'ϱ' => 'ρ',
    'ϵ' => 'ε',
    'ẛ' => 'ṡ',
    'ι' => 'ι',
    'ß' => 'ss',
    'ŉ' => 'ʼn',
    'ǰ' => 'ǰ',
    'ΐ' => 'ΐ',
    'ΰ' => 'ΰ',
    'և' => 'եւ',
    'ẖ' => 'ẖ',
    'ẗ' => 'ẗ',
    'ẘ' => 'ẘ',
    'ẙ' => 'ẙ',
    'ẚ' => 'aʾ',
    'ẞ' => 'ss',
    'ὐ' => 'ὐ',
    'ὒ' => 'ὒ',
    'ὔ' => 'ὔ',
    'ὖ' => 'ὖ',
    'ᾀ' => 'ἀι',
    'ᾁ' => 'ἁι',
    'ᾂ' => 'ἂι',
    'ᾃ' => 'ἃι',
    'ᾄ' => 'ἄι',
    'ᾅ' => 'ἅι',
    'ᾆ' => 'ἆι',
    'ᾇ' => 'ἇι',
    'ᾈ' => 'ἀι',
    'ᾉ' => 'ἁι',
    'ᾊ' => 'ἂι',
    'ᾋ' => 'ἃι',
    'ᾌ' => 'ἄι',
    'ᾍ' => 'ἅι',
    'ᾎ' => 'ἆι',
    'ᾏ' => 'ἇι',
    'ᾐ' => 'ἠι',
    'ᾑ' => 'ἡι',
    'ᾒ' => 'ἢι',
    'ᾓ' => 'ἣι',
    'ᾔ' => 'ἤι',
    'ᾕ' => 'ἥι',
    'ᾖ' => 'ἦι',
    'ᾗ' => 'ἧι',
    'ᾘ' => 'ἠι',
    'ᾙ' => 'ἡι',
    'ᾚ' => 'ἢι',
    'ᾛ' => 'ἣι',
    'ᾜ' => 'ἤι',
    'ᾝ' => 'ἥι',
    'ᾞ' => 'ἦι',
    'ᾟ' => 'ἧι',
    'ᾠ' => 'ὠι',
    'ᾡ' => 'ὡι',
    'ᾢ' => 'ὢι',
    'ᾣ' => 'ὣι',
    'ᾤ' => 'ὤι',
    'ᾥ' => 'ὥι',
    'ᾦ' => 'ὦι',
    'ᾧ' => 'ὧι',
    'ᾨ' => 'ὠι',
    'ᾩ' => 'ὡι',
    'ᾪ' => 'ὢι',
    'ᾫ' => 'ὣι',
    'ᾬ' => 'ὤι',
    'ᾭ' => 'ὥι',
    'ᾮ' => 'ὦι',
    'ᾯ' => 'ὧι',
    'ᾲ' => 'ὰι',
    'ᾳ' => 'αι',
    'ᾴ' => 'άι',
    'ᾶ' => 'ᾶ',
    'ᾷ' => 'ᾶι',
    'ᾼ' => 'αι',
    'ῂ' => 'ὴι',
    'ῃ' => 'ηι',
    'ῄ' => 'ήι',
    'ῆ' => 'ῆ',
    'ῇ' => 'ῆι',
    'ῌ' => 'ηι',
    'ῒ' => 'ῒ',
    'ῖ' => 'ῖ',
    'ῗ' => 'ῗ',
    'ῢ' => 'ῢ',
    'ῤ' => 'ῤ',
    'ῦ' => 'ῦ',
    'ῧ' => 'ῧ',
    'ῲ' => 'ὼι',
    'ῳ' => 'ωι',
    'ῴ' => 'ώι',
    'ῶ' => 'ῶ',
    'ῷ' => 'ῶι',
    'ῼ' => 'ωι',
    'ﬀ' => 'ff',
    'ﬁ' => 'fi',
    'ﬂ' => 'fl',
    'ﬃ' => 'ffi',
    'ﬄ' => 'ffl',
    'ﬅ' => 'st',
    'ﬆ' => 'st',
    'ﬓ' => 'մն',
    'ﬔ' => 'մե',
    'ﬕ' => 'մի',
    'ﬖ' => 'վն',
    'ﬗ' => 'մխ',
];
<?php

return array (
  'A' => 'a',
  'B' => 'b',
  'C' => 'c',
  'D' => 'd',
  'E' => 'e',
  'F' => 'f',
  'G' => 'g',
  'H' => 'h',
  'I' => 'i',
  'J' => 'j',
  'K' => 'k',
  'L' => 'l',
  'M' => 'm',
  'N' => 'n',
  'O' => 'o',
  'P' => 'p',
  'Q' => 'q',
  'R' => 'r',
  'S' => 's',
  'T' => 't',
  'U' => 'u',
  'V' => 'v',
  'W' => 'w',
  'X' => 'x',
  'Y' => 'y',
  'Z' => 'z',
  'À' => 'à',
  'Á' => 'á',
  'Â' => 'â',
  'Ã' => 'ã',
  'Ä' => 'ä',
  'Å' => 'å',
  'Æ' => 'æ',
  'Ç' => 'ç',
  'È' => 'è',
  'É' => 'é',
  'Ê' => 'ê',
  'Ë' => 'ë',
  'Ì' => 'ì',
  'Í' => 'í',
  'Î' => 'î',
  'Ï' => 'ï',
  'Ð' => 'ð',
  'Ñ' => 'ñ',
  'Ò' => 'ò',
  'Ó' => 'ó',
  'Ô' => 'ô',
  'Õ' => 'õ',
  'Ö' => 'ö',
  'Ø' => 'ø',
  'Ù' => 'ù',
  'Ú' => 'ú',
  'Û' => 'û',
  'Ü' => 'ü',
  'Ý' => 'ý',
  'Þ' => 'þ',
  'Ā' => 'ā',
  'Ă' => 'ă',
  'Ą' => 'ą',
  'Ć' => 'ć',
  'Ĉ' => 'ĉ',
  'Ċ' => 'ċ',
  'Č' => 'č',
  'Ď' => 'ď',
  'Đ' => 'đ',
  'Ē' => 'ē',
  'Ĕ' => 'ĕ',
  'Ė' => 'ė',
  'Ę' => 'ę',
  'Ě' => 'ě',
  'Ĝ' => 'ĝ',
  'Ğ' => 'ğ',
  'Ġ' => 'ġ',
  'Ģ' => 'ģ',
  'Ĥ' => 'ĥ',
  'Ħ' => 'ħ',
  'Ĩ' => 'ĩ',
  'Ī' => 'ī',
  'Ĭ' => 'ĭ',
  'Į' => 'į',
  'İ' => 'i̇',
  'Ĳ' => 'ĳ',
  'Ĵ' => 'ĵ',
  'Ķ' => 'ķ',
  'Ĺ' => 'ĺ',
  'Ļ' => 'ļ',
  'Ľ' => 'ľ',
  'Ŀ' => 'ŀ',
  'Ł' => 'ł',
  'Ń' => 'ń',
  'Ņ' => 'ņ',
  'Ň' => 'ň',
  'Ŋ' => 'ŋ',
  'Ō' => 'ō',
  'Ŏ' => 'ŏ',
  'Ő' => 'ő',
  'Œ' => 'œ',
  'Ŕ' => 'ŕ',
  'Ŗ' => 'ŗ',
  'Ř' => 'ř',
  'Ś' => 'ś',
  'Ŝ' => 'ŝ',
  'Ş' => 'ş',
  'Š' => 'š',
  'Ţ' => 'ţ',
  'Ť' => 'ť',
  'Ŧ' => 'ŧ',
  'Ũ' => 'ũ',
  'Ū' => 'ū',
  'Ŭ' => 'ŭ',
  'Ů' => 'ů',
  'Ű' => 'ű',
  'Ų' => 'ų',
  'Ŵ' => 'ŵ',
  'Ŷ' => 'ŷ',
  'Ÿ' => 'ÿ',
  'Ź' => 'ź',
  'Ż' => 'ż',
  'Ž' => 'ž',
  'Ɓ' => 'ɓ',
  'Ƃ' => 'ƃ',
  'Ƅ' => 'ƅ',
  'Ɔ' => 'ɔ',
  'Ƈ' => 'ƈ',
  'Ɖ' => 'ɖ',
  'Ɗ' => 'ɗ',
  'Ƌ' => 'ƌ',
  'Ǝ' => 'ǝ',
  'Ə' => 'ə',
  'Ɛ' => 'ɛ',
  'Ƒ' => 'ƒ',
  'Ɠ' => 'ɠ',
  'Ɣ' => 'ɣ',
  'Ɩ' => 'ɩ',
  'Ɨ' => 'ɨ',
  'Ƙ' => 'ƙ',
  'Ɯ' => 'ɯ',
  'Ɲ' => 'ɲ',
  'Ɵ' => 'ɵ',
  'Ơ' => 'ơ',
  'Ƣ' => 'ƣ',
  'Ƥ' => 'ƥ',
  'Ʀ' => 'ʀ',
  'Ƨ' => 'ƨ',
  'Ʃ' => 'ʃ',
  'Ƭ' => 'ƭ',
  'Ʈ' => 'ʈ',
  'Ư' => 'ư',
  'Ʊ' => 'ʊ',
  'Ʋ' => 'ʋ',
  'Ƴ' => 'ƴ',
  'Ƶ' => 'ƶ',
  'Ʒ' => 'ʒ',
  'Ƹ' => 'ƹ',
  'Ƽ' => 'ƽ',
  'Ǆ' => 'ǆ',
  'ǅ' => 'ǆ',
  'Ǉ' => 'ǉ',
  'ǈ' => 'ǉ',
  'Ǌ' => 'ǌ',
  'ǋ' => 'ǌ',
  'Ǎ' => 'ǎ',
  'Ǐ' => 'ǐ',
  'Ǒ' => 'ǒ',
  'Ǔ' => 'ǔ',
  'Ǖ' => 'ǖ',
  'Ǘ' => 'ǘ',
  'Ǚ' => 'ǚ',
  'Ǜ' => 'ǜ',
  'Ǟ' => 'ǟ',
  'Ǡ' => 'ǡ',
  'Ǣ' => 'ǣ',
  'Ǥ' => 'ǥ',
  'Ǧ' => 'ǧ',
  'Ǩ' => 'ǩ',
  'Ǫ' => 'ǫ',
  'Ǭ' => 'ǭ',
  'Ǯ' => 'ǯ',
  'Ǳ' => 'ǳ',
  'ǲ' => 'ǳ',
  'Ǵ' => 'ǵ',
  'Ƕ' => 'ƕ',
  'Ƿ' => 'ƿ',
  'Ǹ' => 'ǹ',
  'Ǻ' => 'ǻ',
  'Ǽ' => 'ǽ',
  'Ǿ' => 'ǿ',
  'Ȁ' => 'ȁ',
  'Ȃ' => 'ȃ',
  'Ȅ' => 'ȅ',
  'Ȇ' => 'ȇ',
  'Ȉ' => 'ȉ',
  'Ȋ' => 'ȋ',
  'Ȍ' => 'ȍ',
  'Ȏ' => 'ȏ',
  'Ȑ' => 'ȑ',
  'Ȓ' => 'ȓ',
  'Ȕ' => 'ȕ',
  'Ȗ' => 'ȗ',
  'Ș' => 'ș',
  'Ț' => 'ț',
  'Ȝ' => 'ȝ',
  'Ȟ' => 'ȟ',
  'Ƞ' => 'ƞ',
  'Ȣ' => 'ȣ',
  'Ȥ' => 'ȥ',
  'Ȧ' => 'ȧ',
  'Ȩ' => 'ȩ',
  'Ȫ' => 'ȫ',
  'Ȭ' => 'ȭ',
  'Ȯ' => 'ȯ',
  'Ȱ' => 'ȱ',
  'Ȳ' => 'ȳ',
  'Ⱥ' => 'ⱥ',
  'Ȼ' => 'ȼ',
  'Ƚ' => 'ƚ',
  'Ⱦ' => 'ⱦ',
  'Ɂ' => 'ɂ',
  'Ƀ' => 'ƀ',
  'Ʉ' => 'ʉ',
  'Ʌ' => 'ʌ',
  'Ɇ' => 'ɇ',
  'Ɉ' => 'ɉ',
  'Ɋ' => 'ɋ',
  'Ɍ' => 'ɍ',
  'Ɏ' => 'ɏ',
  'Ͱ' => 'ͱ',
  'Ͳ' => 'ͳ',
  'Ͷ' => 'ͷ',
  'Ϳ' => 'ϳ',
  'Ά' => 'ά',
  'Έ' => 'έ',
  'Ή' => 'ή',
  'Ί' => 'ί',
  'Ό' => 'ό',
  'Ύ' => 'ύ',
  'Ώ' => 'ώ',
  'Α' => 'α',
  'Β' => 'β',
  'Γ' => 'γ',
  'Δ' => 'δ',
  'Ε' => 'ε',
  'Ζ' => 'ζ',
  'Η' => 'η',
  'Θ' => 'θ',
  'Ι' => 'ι',
  'Κ' => 'κ',
  'Λ' => 'λ',
  'Μ' => 'μ',
  'Ν' => 'ν',
  'Ξ' => 'ξ',
  'Ο' => 'ο',
  'Π' => 'π',
  'Ρ' => 'ρ',
  'Σ' => 'σ',
  'Τ' => 'τ',
  'Υ' => 'υ',
  'Φ' => 'φ',
  'Χ' => 'χ',
  'Ψ' => 'ψ',
  'Ω' => 'ω',
  'Ϊ' => 'ϊ',
  'Ϋ' => 'ϋ',
  'Ϗ' => 'ϗ',
  'Ϙ' => 'ϙ',
  'Ϛ' => 'ϛ',
  'Ϝ' => 'ϝ',
  'Ϟ' => 'ϟ',
  'Ϡ' => 'ϡ',
  'Ϣ' => 'ϣ',
  'Ϥ' => 'ϥ',
  'Ϧ' => 'ϧ',
  'Ϩ' => 'ϩ',
  'Ϫ' => 'ϫ',
  'Ϭ' => 'ϭ',
  'Ϯ' => 'ϯ',
  'ϴ' => 'θ',
  'Ϸ' => 'ϸ',
  'Ϲ' => 'ϲ',
  'Ϻ' => 'ϻ',
  'Ͻ' => 'ͻ',
  'Ͼ' => 'ͼ',
  'Ͽ' => 'ͽ',
  'Ѐ' => 'ѐ',
  'Ё' => 'ё',
  'Ђ' => 'ђ',
  'Ѓ' => 'ѓ',
  'Є' => 'є',
  'Ѕ' => 'ѕ',
  'І' => 'і',
  'Ї' => 'ї',
  'Ј' => 'ј',
  'Љ' => 'љ',
  'Њ' => 'њ',
  'Ћ' => 'ћ',
  'Ќ' => 'ќ',
  'Ѝ' => 'ѝ',
  'Ў' => 'ў',
  'Џ' => 'џ',
  'А' => 'а',
  'Б' => 'б',
  'В' => 'в',
  'Г' => 'г',
  'Д' => 'д',
  'Е' => 'е',
  'Ж' => 'ж',
  'З' => 'з',
  'И' => 'и',
  'Й' => 'й',
  'К' => 'к',
  'Л' => 'л',
  'М' => 'м',
  'Н' => 'н',
  'О' => 'о',
  'П' => 'п',
  'Р' => 'р',
  'С' => 'с',
  'Т' => 'т',
  'У' => 'у',
  'Ф' => 'ф',
  'Х' => 'х',
  'Ц' => 'ц',
  'Ч' => 'ч',
  'Ш' => 'ш',
  'Щ' => 'щ',
  'Ъ' => 'ъ',
  'Ы' => 'ы',
  'Ь' => 'ь',
  'Э' => 'э',
  'Ю' => 'ю',
  'Я' => 'я',
  'Ѡ' => 'ѡ',
  'Ѣ' => 'ѣ',
  'Ѥ' => 'ѥ',
  'Ѧ' => 'ѧ',
  'Ѩ' => 'ѩ',
  'Ѫ' => 'ѫ',
  'Ѭ' => 'ѭ',
  'Ѯ' => 'ѯ',
  'Ѱ' => 'ѱ',
  'Ѳ' => 'ѳ',
  'Ѵ' => 'ѵ',
  'Ѷ' => 'ѷ',
  'Ѹ' => 'ѹ',
  'Ѻ' => 'ѻ',
  'Ѽ' => 'ѽ',
  'Ѿ' => 'ѿ',
  'Ҁ' => 'ҁ',
  'Ҋ' => 'ҋ',
  'Ҍ' => 'ҍ',
  'Ҏ' => 'ҏ',
  'Ґ' => 'ґ',
  'Ғ' => 'ғ',
  'Ҕ' => 'ҕ',
  'Җ' => 'җ',
  'Ҙ' => 'ҙ',
  'Қ' => 'қ',
  'Ҝ' => 'ҝ',
  'Ҟ' => 'ҟ',
  'Ҡ' => 'ҡ',
  'Ң' => 'ң',
  'Ҥ' => 'ҥ',
  'Ҧ' => 'ҧ',
  'Ҩ' => 'ҩ',
  'Ҫ' => 'ҫ',
  'Ҭ' => 'ҭ',
  'Ү' => 'ү',
  'Ұ' => 'ұ',
  'Ҳ' => 'ҳ',
  'Ҵ' => 'ҵ',
  'Ҷ' => 'ҷ',
  'Ҹ' => 'ҹ',
  'Һ' => 'һ',
  'Ҽ' => 'ҽ',
  'Ҿ' => 'ҿ',
  'Ӏ' => 'ӏ',
  'Ӂ' => 'ӂ',
  'Ӄ' => 'ӄ',
  'Ӆ' => 'ӆ',
  'Ӈ' => 'ӈ',
  'Ӊ' => 'ӊ',
  'Ӌ' => 'ӌ',
  'Ӎ' => 'ӎ',
  'Ӑ' => 'ӑ',
  'Ӓ' => 'ӓ',
  'Ӕ' => 'ӕ',
  'Ӗ' => 'ӗ',
  'Ә' => 'ә',
  'Ӛ' => 'ӛ',
  'Ӝ' => 'ӝ',
  'Ӟ' => 'ӟ',
  'Ӡ' => 'ӡ',
  'Ӣ' => 'ӣ',
  'Ӥ' => 'ӥ',
  'Ӧ' => 'ӧ',
  'Ө' => 'ө',
  'Ӫ' => 'ӫ',
  'Ӭ' => 'ӭ',
  'Ӯ' => 'ӯ',
  'Ӱ' => 'ӱ',
  'Ӳ' => 'ӳ',
  'Ӵ' => 'ӵ',
  'Ӷ' => 'ӷ',
  'Ӹ' => 'ӹ',
  'Ӻ' => 'ӻ',
  'Ӽ' => 'ӽ',
  'Ӿ' => 'ӿ',
  'Ԁ' => 'ԁ',
  'Ԃ' => 'ԃ',
  'Ԅ' => 'ԅ',
  'Ԇ' => 'ԇ',
  'Ԉ' => 'ԉ',
  'Ԋ' => 'ԋ',
  'Ԍ' => 'ԍ',
  'Ԏ' => 'ԏ',
  'Ԑ' => 'ԑ',
  'Ԓ' => 'ԓ',
  'Ԕ' => 'ԕ',
  'Ԗ' => 'ԗ',
  'Ԙ' => 'ԙ',
  'Ԛ' => 'ԛ',
  'Ԝ' => 'ԝ',
  'Ԟ' => 'ԟ',
  'Ԡ' => 'ԡ',
  'Ԣ' => 'ԣ',
  'Ԥ' => 'ԥ',
  'Ԧ' => 'ԧ',
  'Ԩ' => 'ԩ',
  'Ԫ' => 'ԫ',
  'Ԭ' => 'ԭ',
  'Ԯ' => 'ԯ',
  'Ա' => 'ա',
  'Բ' => 'բ',
  'Գ' => 'գ',
  'Դ' => 'դ',
  'Ե' => 'ե',
  'Զ' => 'զ',
  'Է' => 'է',
  'Ը' => 'ը',
  'Թ' => 'թ',
  'Ժ' => 'ժ',
  'Ի' => 'ի',
  'Լ' => 'լ',
  'Խ' => 'խ',
  'Ծ' => 'ծ',
  'Կ' => 'կ',
  'Հ' => 'հ',
  'Ձ' => 'ձ',
  'Ղ' => 'ղ',
  'Ճ' => 'ճ',
  'Մ' => 'մ',
  'Յ' => 'յ',
  'Ն' => 'ն',
  'Շ' => 'շ',
  'Ո' => 'ո',
  'Չ' => 'չ',
  'Պ' => 'պ',
  'Ջ' => 'ջ',
  'Ռ' => 'ռ',
  'Ս' => 'ս',
  'Վ' => 'վ',
  'Տ' => 'տ',
  'Ր' => 'ր',
  'Ց' => 'ց',
  'Ւ' => 'ւ',
  'Փ' => 'փ',
  'Ք' => 'ք',
  'Օ' => 'օ',
  'Ֆ' => 'ֆ',
  'Ⴀ' => 'ⴀ',
  'Ⴁ' => 'ⴁ',
  'Ⴂ' => 'ⴂ',
  'Ⴃ' => 'ⴃ',
  'Ⴄ' => 'ⴄ',
  'Ⴅ' => 'ⴅ',
  'Ⴆ' => 'ⴆ',
  'Ⴇ' => 'ⴇ',
  'Ⴈ' => 'ⴈ',
  'Ⴉ' => 'ⴉ',
  'Ⴊ' => 'ⴊ',
  'Ⴋ' => 'ⴋ',
  'Ⴌ' => 'ⴌ',
  'Ⴍ' => 'ⴍ',
  'Ⴎ' => 'ⴎ',
  'Ⴏ' => 'ⴏ',
  'Ⴐ' => 'ⴐ',
  'Ⴑ' => 'ⴑ',
  'Ⴒ' => 'ⴒ',
  'Ⴓ' => 'ⴓ',
  'Ⴔ' => 'ⴔ',
  'Ⴕ' => 'ⴕ',
  'Ⴖ' => 'ⴖ',
  'Ⴗ' => 'ⴗ',
  'Ⴘ' => 'ⴘ',
  'Ⴙ' => 'ⴙ',
  'Ⴚ' => 'ⴚ',
  'Ⴛ' => 'ⴛ',
  'Ⴜ' => 'ⴜ',
  'Ⴝ' => 'ⴝ',
  'Ⴞ' => 'ⴞ',
  'Ⴟ' => 'ⴟ',
  'Ⴠ' => 'ⴠ',
  'Ⴡ' => 'ⴡ',
  'Ⴢ' => 'ⴢ',
  'Ⴣ' => 'ⴣ',
  'Ⴤ' => 'ⴤ',
  'Ⴥ' => 'ⴥ',
  'Ⴧ' => 'ⴧ',
  'Ⴭ' => 'ⴭ',
  'Ꭰ' => 'ꭰ',
  'Ꭱ' => 'ꭱ',
  'Ꭲ' => 'ꭲ',
  'Ꭳ' => 'ꭳ',
  'Ꭴ' => 'ꭴ',
  'Ꭵ' => 'ꭵ',
  'Ꭶ' => 'ꭶ',
  'Ꭷ' => 'ꭷ',
  'Ꭸ' => 'ꭸ',
  'Ꭹ' => 'ꭹ',
  'Ꭺ' => 'ꭺ',
  'Ꭻ' => 'ꭻ',
  'Ꭼ' => 'ꭼ',
  'Ꭽ' => 'ꭽ',
  'Ꭾ' => 'ꭾ',
  'Ꭿ' => 'ꭿ',
  'Ꮀ' => 'ꮀ',
  'Ꮁ' => 'ꮁ',
  'Ꮂ' => 'ꮂ',
  'Ꮃ' => 'ꮃ',
  'Ꮄ' => 'ꮄ',
  'Ꮅ' => 'ꮅ',
  'Ꮆ' => 'ꮆ',
  'Ꮇ' => 'ꮇ',
  'Ꮈ' => 'ꮈ',
  'Ꮉ' => 'ꮉ',
  'Ꮊ' => 'ꮊ',
  'Ꮋ' => 'ꮋ',
  'Ꮌ' => 'ꮌ',
  'Ꮍ' => 'ꮍ',
  'Ꮎ' => 'ꮎ',
  'Ꮏ' => 'ꮏ',
  'Ꮐ' => 'ꮐ',
  'Ꮑ' => 'ꮑ',
  'Ꮒ' => 'ꮒ',
  'Ꮓ' => 'ꮓ',
  'Ꮔ' => 'ꮔ',
  'Ꮕ' => 'ꮕ',
  'Ꮖ' => 'ꮖ',
  'Ꮗ' => 'ꮗ',
  'Ꮘ' => 'ꮘ',
  'Ꮙ' => 'ꮙ',
  'Ꮚ' => 'ꮚ',
  'Ꮛ' => 'ꮛ',
  'Ꮜ' => 'ꮜ',
  'Ꮝ' => 'ꮝ',
  'Ꮞ' => 'ꮞ',
  'Ꮟ' => 'ꮟ',
  'Ꮠ' => 'ꮠ',
  'Ꮡ' => 'ꮡ',
  'Ꮢ' => 'ꮢ',
  'Ꮣ' => 'ꮣ',
  'Ꮤ' => 'ꮤ',
  'Ꮥ' => 'ꮥ',
  'Ꮦ' => 'ꮦ',
  'Ꮧ' => 'ꮧ',
  'Ꮨ' => 'ꮨ',
  'Ꮩ' => 'ꮩ',
  'Ꮪ' => 'ꮪ',
  'Ꮫ' => 'ꮫ',
  'Ꮬ' => 'ꮬ',
  'Ꮭ' => 'ꮭ',
  'Ꮮ' => 'ꮮ',
  'Ꮯ' => 'ꮯ',
  'Ꮰ' => 'ꮰ',
  'Ꮱ' => 'ꮱ',
  'Ꮲ' => 'ꮲ',
  'Ꮳ' => 'ꮳ',
  'Ꮴ' => 'ꮴ',
  'Ꮵ' => 'ꮵ',
  'Ꮶ' => 'ꮶ',
  'Ꮷ' => 'ꮷ',
  'Ꮸ' => 'ꮸ',
  'Ꮹ' => 'ꮹ',
  'Ꮺ' => 'ꮺ',
  'Ꮻ' => 'ꮻ',
  'Ꮼ' => 'ꮼ',
  'Ꮽ' => 'ꮽ',
  'Ꮾ' => 'ꮾ',
  'Ꮿ' => 'ꮿ',
  'Ᏸ' => 'ᏸ',
  'Ᏹ' => 'ᏹ',
  'Ᏺ' => 'ᏺ',
  'Ᏻ' => 'ᏻ',
  'Ᏼ' => 'ᏼ',
  'Ᏽ' => 'ᏽ',
  'Ა' => 'ა',
  'Ბ' => 'ბ',
  'Გ' => 'გ',
  'Დ' => 'დ',
  'Ე' => 'ე',
  'Ვ' => 'ვ',
  'Ზ' => 'ზ',
  'Თ' => 'თ',
  'Ი' => 'ი',
  'Კ' => 'კ',
  'Ლ' => 'ლ',
  'Მ' => 'მ',
  'Ნ' => 'ნ',
  'Ო' => 'ო',
  'Პ' => 'პ',
  'Ჟ' => 'ჟ',
  'Რ' => 'რ',
  'Ს' => 'ს',
  'Ტ' => 'ტ',
  'Უ' => 'უ',
  'Ფ' => 'ფ',
  'Ქ' => 'ქ',
  'Ღ' => 'ღ',
  'Ყ' => 'ყ',
  'Შ' => 'შ',
  'Ჩ' => 'ჩ',
  'Ც' => 'ც',
  'Ძ' => 'ძ',
  'Წ' => 'წ',
  'Ჭ' => 'ჭ',
  'Ხ' => 'ხ',
  'Ჯ' => 'ჯ',
  'Ჰ' => 'ჰ',
  'Ჱ' => 'ჱ',
  'Ჲ' => 'ჲ',
  'Ჳ' => 'ჳ',
  'Ჴ' => 'ჴ',
  'Ჵ' => 'ჵ',
  'Ჶ' => 'ჶ',
  'Ჷ' => 'ჷ',
  'Ჸ' => 'ჸ',
  'Ჹ' => 'ჹ',
  'Ჺ' => 'ჺ',
  'Ჽ' => 'ჽ',
  'Ჾ' => 'ჾ',
  'Ჿ' => 'ჿ',
  'Ḁ' => 'ḁ',
  'Ḃ' => 'ḃ',
  'Ḅ' => 'ḅ',
  'Ḇ' => 'ḇ',
  'Ḉ' => 'ḉ',
  'Ḋ' => 'ḋ',
  'Ḍ' => 'ḍ',
  'Ḏ' => 'ḏ',
  'Ḑ' => 'ḑ',
  'Ḓ' => 'ḓ',
  'Ḕ' => 'ḕ',
  'Ḗ' => 'ḗ',
  'Ḙ' => 'ḙ',
  'Ḛ' => 'ḛ',
  'Ḝ' => 'ḝ',
  'Ḟ' => 'ḟ',
  'Ḡ' => 'ḡ',
  'Ḣ' => 'ḣ',
  'Ḥ' => 'ḥ',
  'Ḧ' => 'ḧ',
  'Ḩ' => 'ḩ',
  'Ḫ' => 'ḫ',
  'Ḭ' => 'ḭ',
  'Ḯ' => 'ḯ',
  'Ḱ' => 'ḱ',
  'Ḳ' => 'ḳ',
  'Ḵ' => 'ḵ',
  'Ḷ' => 'ḷ',
  'Ḹ' => 'ḹ',
  'Ḻ' => 'ḻ',
  'Ḽ' => 'ḽ',
  'Ḿ' => 'ḿ',
  'Ṁ' => 'ṁ',
  'Ṃ' => 'ṃ',
  'Ṅ' => 'ṅ',
  'Ṇ' => 'ṇ',
  'Ṉ' => 'ṉ',
  'Ṋ' => 'ṋ',
  'Ṍ' => 'ṍ',
  'Ṏ' => 'ṏ',
  'Ṑ' => 'ṑ',
  'Ṓ' => 'ṓ',
  'Ṕ' => 'ṕ',
  'Ṗ' => 'ṗ',
  'Ṙ' => 'ṙ',
  'Ṛ' => 'ṛ',
  'Ṝ' => 'ṝ',
  'Ṟ' => 'ṟ',
  'Ṡ' => 'ṡ',
  'Ṣ' => 'ṣ',
  'Ṥ' => 'ṥ',
  'Ṧ' => 'ṧ',
  'Ṩ' => 'ṩ',
  'Ṫ' => 'ṫ',
  'Ṭ' => 'ṭ',
  'Ṯ' => 'ṯ',
  'Ṱ' => 'ṱ',
  'Ṳ' => 'ṳ',
  'Ṵ' => 'ṵ',
  'Ṷ' => 'ṷ',
  'Ṹ' => 'ṹ',
  'Ṻ' => 'ṻ',
  'Ṽ' => 'ṽ',
  'Ṿ' => 'ṿ',
  'Ẁ' => 'ẁ',
  'Ẃ' => 'ẃ',
  'Ẅ' => 'ẅ',
  'Ẇ' => 'ẇ',
  'Ẉ' => 'ẉ',
  'Ẋ' => 'ẋ',
  'Ẍ' => 'ẍ',
  'Ẏ' => 'ẏ',
  'Ẑ' => 'ẑ',
  'Ẓ' => 'ẓ',
  'Ẕ' => 'ẕ',
  'ẞ' => 'ß',
  'Ạ' => 'ạ',
  'Ả' => 'ả',
  'Ấ' => 'ấ',
  'Ầ' => 'ầ',
  'Ẩ' => 'ẩ',
  'Ẫ' => 'ẫ',
  'Ậ' => 'ậ',
  'Ắ' => 'ắ',
  'Ằ' => 'ằ',
  'Ẳ' => 'ẳ',
  'Ẵ' => 'ẵ',
  'Ặ' => 'ặ',
  'Ẹ' => 'ẹ',
  'Ẻ' => 'ẻ',
  'Ẽ' => 'ẽ',
  'Ế' => 'ế',
  'Ề' => 'ề',
  'Ể' => 'ể',
  'Ễ' => 'ễ',
  'Ệ' => 'ệ',
  'Ỉ' => 'ỉ',
  'Ị' => 'ị',
  'Ọ' => 'ọ',
  'Ỏ' => 'ỏ',
  'Ố' => 'ố',
  'Ồ' => 'ồ',
  'Ổ' => 'ổ',
  'Ỗ' => 'ỗ',
  'Ộ' => 'ộ',
  'Ớ' => 'ớ',
  'Ờ' => 'ờ',
  'Ở' => 'ở',
  'Ỡ' => 'ỡ',
  'Ợ' => 'ợ',
  'Ụ' => 'ụ',
  'Ủ' => 'ủ',
  'Ứ' => 'ứ',
  'Ừ' => 'ừ',
  'Ử' => 'ử',
  'Ữ' => 'ữ',
  'Ự' => 'ự',
  'Ỳ' => 'ỳ',
  'Ỵ' => 'ỵ',
  'Ỷ' => 'ỷ',
  'Ỹ' => 'ỹ',
  'Ỻ' => 'ỻ',
  'Ỽ' => 'ỽ',
  'Ỿ' => 'ỿ',
  'Ἀ' => 'ἀ',
  'Ἁ' => 'ἁ',
  'Ἂ' => 'ἂ',
  'Ἃ' => 'ἃ',
  'Ἄ' => 'ἄ',
  'Ἅ' => 'ἅ',
  'Ἆ' => 'ἆ',
  'Ἇ' => 'ἇ',
  'Ἐ' => 'ἐ',
  'Ἑ' => 'ἑ',
  'Ἒ' => 'ἒ',
  'Ἓ' => 'ἓ',
  'Ἔ' => 'ἔ',
  'Ἕ' => 'ἕ',
  'Ἠ' => 'ἠ',
  'Ἡ' => 'ἡ',
  'Ἢ' => 'ἢ',
  'Ἣ' => 'ἣ',
  'Ἤ' => 'ἤ',
  'Ἥ' => 'ἥ',
  'Ἦ' => 'ἦ',
  'Ἧ' => 'ἧ',
  'Ἰ' => 'ἰ',
  'Ἱ' => 'ἱ',
  'Ἲ' => 'ἲ',
  'Ἳ' => 'ἳ',
  'Ἴ' => 'ἴ',
  'Ἵ' => 'ἵ',
  'Ἶ' => 'ἶ',
  'Ἷ' => 'ἷ',
  'Ὀ' => 'ὀ',
  'Ὁ' => 'ὁ',
  'Ὂ' => 'ὂ',
  'Ὃ' => 'ὃ',
  'Ὄ' => 'ὄ',
  'Ὅ' => 'ὅ',
  'Ὑ' => 'ὑ',
  'Ὓ' => 'ὓ',
  'Ὕ' => 'ὕ',
  'Ὗ' => 'ὗ',
  'Ὠ' => 'ὠ',
  'Ὡ' => 'ὡ',
  'Ὢ' => 'ὢ',
  'Ὣ' => 'ὣ',
  'Ὤ' => 'ὤ',
  'Ὥ' => 'ὥ',
  'Ὦ' => 'ὦ',
  'Ὧ' => 'ὧ',
  'ᾈ' => 'ᾀ',
  'ᾉ' => 'ᾁ',
  'ᾊ' => 'ᾂ',
  'ᾋ' => 'ᾃ',
  'ᾌ' => 'ᾄ',
  'ᾍ' => 'ᾅ',
  'ᾎ' => 'ᾆ',
  'ᾏ' => 'ᾇ',
  'ᾘ' => 'ᾐ',
  'ᾙ' => 'ᾑ',
  'ᾚ' => 'ᾒ',
  'ᾛ' => 'ᾓ',
  'ᾜ' => 'ᾔ',
  'ᾝ' => 'ᾕ',
  'ᾞ' => 'ᾖ',
  'ᾟ' => 'ᾗ',
  'ᾨ' => 'ᾠ',
  'ᾩ' => 'ᾡ',
  'ᾪ' => 'ᾢ',
  'ᾫ' => 'ᾣ',
  'ᾬ' => 'ᾤ',
  'ᾭ' => 'ᾥ',
  'ᾮ' => 'ᾦ',
  'ᾯ' => 'ᾧ',
  'Ᾰ' => 'ᾰ',
  'Ᾱ' => 'ᾱ',
  'Ὰ' => 'ὰ',
  'Ά' => 'ά',
  'ᾼ' => 'ᾳ',
  'Ὲ' => 'ὲ',
  'Έ' => 'έ',
  'Ὴ' => 'ὴ',
  'Ή' => 'ή',
  'ῌ' => 'ῃ',
  'Ῐ' => 'ῐ',
  'Ῑ' => 'ῑ',
  'Ὶ' => 'ὶ',
  'Ί' => 'ί',
  'Ῠ' => 'ῠ',
  'Ῡ' => 'ῡ',
  'Ὺ' => 'ὺ',
  'Ύ' => 'ύ',
  'Ῥ' => 'ῥ',
  'Ὸ' => 'ὸ',
  'Ό' => 'ό',
  'Ὼ' => 'ὼ',
  'Ώ' => 'ώ',
  'ῼ' => 'ῳ',
  'Ω' => 'ω',
  'K' => 'k',
  'Å' => 'å',
  'Ⅎ' => 'ⅎ',
  'Ⅰ' => 'ⅰ',
  'Ⅱ' => 'ⅱ',
  'Ⅲ' => 'ⅲ',
  'Ⅳ' => 'ⅳ',
  'Ⅴ' => 'ⅴ',
  'Ⅵ' => 'ⅵ',
  'Ⅶ' => 'ⅶ',
  'Ⅷ' => 'ⅷ',
  'Ⅸ' => 'ⅸ',
  'Ⅹ' => 'ⅹ',
  'Ⅺ' => 'ⅺ',
  'Ⅻ' => 'ⅻ',
  'Ⅼ' => 'ⅼ',
  'Ⅽ' => 'ⅽ',
  'Ⅾ' => 'ⅾ',
  'Ⅿ' => 'ⅿ',
  'Ↄ' => 'ↄ',
  'Ⓐ' => 'ⓐ',
  'Ⓑ' => 'ⓑ',
  'Ⓒ' => 'ⓒ',
  'Ⓓ' => 'ⓓ',
  'Ⓔ' => 'ⓔ',
  'Ⓕ' => 'ⓕ',
  'Ⓖ' => 'ⓖ',
  'Ⓗ' => 'ⓗ',
  'Ⓘ' => 'ⓘ',
  'Ⓙ' => 'ⓙ',
  'Ⓚ' => 'ⓚ',
  'Ⓛ' => 'ⓛ',
  'Ⓜ' => 'ⓜ',
  'Ⓝ' => 'ⓝ',
  'Ⓞ' => 'ⓞ',
  'Ⓟ' => 'ⓟ',
  'Ⓠ' => 'ⓠ',
  'Ⓡ' => 'ⓡ',
  'Ⓢ' => 'ⓢ',
  'Ⓣ' => 'ⓣ',
  'Ⓤ' => 'ⓤ',
  'Ⓥ' => 'ⓥ',
  'Ⓦ' => 'ⓦ',
  'Ⓧ' => 'ⓧ',
  'Ⓨ' => 'ⓨ',
  'Ⓩ' => 'ⓩ',
  'Ⰰ' => 'ⰰ',
  'Ⰱ' => 'ⰱ',
  'Ⰲ' => 'ⰲ',
  'Ⰳ' => 'ⰳ',
  'Ⰴ' => 'ⰴ',
  'Ⰵ' => 'ⰵ',
  'Ⰶ' => 'ⰶ',
  'Ⰷ' => 'ⰷ',
  'Ⰸ' => 'ⰸ',
  'Ⰹ' => 'ⰹ',
  'Ⰺ' => 'ⰺ',
  'Ⰻ' => 'ⰻ',
  'Ⰼ' => 'ⰼ',
  'Ⰽ' => 'ⰽ',
  'Ⰾ' => 'ⰾ',
  'Ⰿ' => 'ⰿ',
  'Ⱀ' => 'ⱀ',
  'Ⱁ' => 'ⱁ',
  'Ⱂ' => 'ⱂ',
  'Ⱃ' => 'ⱃ',
  'Ⱄ' => 'ⱄ',
  'Ⱅ' => 'ⱅ',
  'Ⱆ' => 'ⱆ',
  'Ⱇ' => 'ⱇ',
  'Ⱈ' => 'ⱈ',
  'Ⱉ' => 'ⱉ',
  'Ⱊ' => 'ⱊ',
  'Ⱋ' => 'ⱋ',
  'Ⱌ' => 'ⱌ',
  'Ⱍ' => 'ⱍ',
  'Ⱎ' => 'ⱎ',
  'Ⱏ' => 'ⱏ',
  'Ⱐ' => 'ⱐ',
  'Ⱑ' => 'ⱑ',
  'Ⱒ' => 'ⱒ',
  'Ⱓ' => 'ⱓ',
  'Ⱔ' => 'ⱔ',
  'Ⱕ' => 'ⱕ',
  'Ⱖ' => 'ⱖ',
  'Ⱗ' => 'ⱗ',
  'Ⱘ' => 'ⱘ',
  'Ⱙ' => 'ⱙ',
  'Ⱚ' => 'ⱚ',
  'Ⱛ' => 'ⱛ',
  'Ⱜ' => 'ⱜ',
  'Ⱝ' => 'ⱝ',
  'Ⱞ' => 'ⱞ',
  'Ⱡ' => 'ⱡ',
  'Ɫ' => 'ɫ',
  'Ᵽ' => 'ᵽ',
  'Ɽ' => 'ɽ',
  'Ⱨ' => 'ⱨ',
  'Ⱪ' => 'ⱪ',
  'Ⱬ' => 'ⱬ',
  'Ɑ' => 'ɑ',
  'Ɱ' => 'ɱ',
  'Ɐ' => 'ɐ',
  'Ɒ' => 'ɒ',
  'Ⱳ' => 'ⱳ',
  'Ⱶ' => 'ⱶ',
  'Ȿ' => 'ȿ',
  'Ɀ' => 'ɀ',
  'Ⲁ' => 'ⲁ',
  'Ⲃ' => 'ⲃ',
  'Ⲅ' => 'ⲅ',
  'Ⲇ' => 'ⲇ',
  'Ⲉ' => 'ⲉ',
  'Ⲋ' => 'ⲋ',
  'Ⲍ' => 'ⲍ',
  'Ⲏ' => 'ⲏ',
  'Ⲑ' => 'ⲑ',
  'Ⲓ' => 'ⲓ',
  'Ⲕ' => 'ⲕ',
  'Ⲗ' => 'ⲗ',
  'Ⲙ' => 'ⲙ',
  'Ⲛ' => 'ⲛ',
  'Ⲝ' => 'ⲝ',
  'Ⲟ' => 'ⲟ',
  'Ⲡ' => 'ⲡ',
  'Ⲣ' => 'ⲣ',
  'Ⲥ' => 'ⲥ',
  'Ⲧ' => 'ⲧ',
  'Ⲩ' => 'ⲩ',
  'Ⲫ' => 'ⲫ',
  'Ⲭ' => 'ⲭ',
  'Ⲯ' => 'ⲯ',
  'Ⲱ' => 'ⲱ',
  'Ⲳ' => 'ⲳ',
  'Ⲵ' => 'ⲵ',
  'Ⲷ' => 'ⲷ',
  'Ⲹ' => 'ⲹ',
  'Ⲻ' => 'ⲻ',
  'Ⲽ' => 'ⲽ',
  'Ⲿ' => 'ⲿ',
  'Ⳁ' => 'ⳁ',
  'Ⳃ' => 'ⳃ',
  'Ⳅ' => 'ⳅ',
  'Ⳇ' => 'ⳇ',
  'Ⳉ' => 'ⳉ',
  'Ⳋ' => 'ⳋ',
  'Ⳍ' => 'ⳍ',
  'Ⳏ' => 'ⳏ',
  'Ⳑ' => 'ⳑ',
  'Ⳓ' => 'ⳓ',
  'Ⳕ' => 'ⳕ',
  'Ⳗ' => 'ⳗ',
  'Ⳙ' => 'ⳙ',
  'Ⳛ' => 'ⳛ',
  'Ⳝ' => 'ⳝ',
  'Ⳟ' => 'ⳟ',
  'Ⳡ' => 'ⳡ',
  'Ⳣ' => 'ⳣ',
  'Ⳬ' => 'ⳬ',
  'Ⳮ' => 'ⳮ',
  'Ⳳ' => 'ⳳ',
  'Ꙁ' => 'ꙁ',
  'Ꙃ' => 'ꙃ',
  'Ꙅ' => 'ꙅ',
  'Ꙇ' => 'ꙇ',
  'Ꙉ' => 'ꙉ',
  'Ꙋ' => 'ꙋ',
  'Ꙍ' => 'ꙍ',
  'Ꙏ' => 'ꙏ',
  'Ꙑ' => 'ꙑ',
  'Ꙓ' => 'ꙓ',
  'Ꙕ' => 'ꙕ',
  'Ꙗ' => 'ꙗ',
  'Ꙙ' => 'ꙙ',
  'Ꙛ' => 'ꙛ',
  'Ꙝ' => 'ꙝ',
  'Ꙟ' => 'ꙟ',
  'Ꙡ' => 'ꙡ',
  'Ꙣ' => 'ꙣ',
  'Ꙥ' => 'ꙥ',
  'Ꙧ' => 'ꙧ',
  'Ꙩ' => 'ꙩ',
  'Ꙫ' => 'ꙫ',
  'Ꙭ' => 'ꙭ',
  'Ꚁ' => 'ꚁ',
  'Ꚃ' => 'ꚃ',
  'Ꚅ' => 'ꚅ',
  'Ꚇ' => 'ꚇ',
  'Ꚉ' => 'ꚉ',
  'Ꚋ' => 'ꚋ',
  'Ꚍ' => 'ꚍ',
  'Ꚏ' => 'ꚏ',
  'Ꚑ' => 'ꚑ',
  'Ꚓ' => 'ꚓ',
  'Ꚕ' => 'ꚕ',
  'Ꚗ' => 'ꚗ',
  'Ꚙ' => 'ꚙ',
  'Ꚛ' => 'ꚛ',
  'Ꜣ' => 'ꜣ',
  'Ꜥ' => 'ꜥ',
  'Ꜧ' => 'ꜧ',
  'Ꜩ' => 'ꜩ',
  'Ꜫ' => 'ꜫ',
  'Ꜭ' => 'ꜭ',
  'Ꜯ' => 'ꜯ',
  'Ꜳ' => 'ꜳ',
  'Ꜵ' => 'ꜵ',
  'Ꜷ' => 'ꜷ',
  'Ꜹ' => 'ꜹ',
  'Ꜻ' => 'ꜻ',
  'Ꜽ' => 'ꜽ',
  'Ꜿ' => 'ꜿ',
  'Ꝁ' => 'ꝁ',
  'Ꝃ' => 'ꝃ',
  'Ꝅ' => 'ꝅ',
  'Ꝇ' => 'ꝇ',
  'Ꝉ' => 'ꝉ',
  'Ꝋ' => 'ꝋ',
  'Ꝍ' => 'ꝍ',
  'Ꝏ' => 'ꝏ',
  'Ꝑ' => 'ꝑ',
  'Ꝓ' => 'ꝓ',
  'Ꝕ' => 'ꝕ',
  'Ꝗ' => 'ꝗ',
  'Ꝙ' => 'ꝙ',
  'Ꝛ' => 'ꝛ',
  'Ꝝ' => 'ꝝ',
  'Ꝟ' => 'ꝟ',
  'Ꝡ' => 'ꝡ',
  'Ꝣ' => 'ꝣ',
  'Ꝥ' => 'ꝥ',
  'Ꝧ' => 'ꝧ',
  'Ꝩ' => 'ꝩ',
  'Ꝫ' => 'ꝫ',
  'Ꝭ' => 'ꝭ',
  'Ꝯ' => 'ꝯ',
  'Ꝺ' => 'ꝺ',
  'Ꝼ' => 'ꝼ',
  'Ᵹ' => 'ᵹ',
  'Ꝿ' => 'ꝿ',
  'Ꞁ' => 'ꞁ',
  'Ꞃ' => 'ꞃ',
  'Ꞅ' => 'ꞅ',
  'Ꞇ' => 'ꞇ',
  'Ꞌ' => 'ꞌ',
  'Ɥ' => 'ɥ',
  'Ꞑ' => 'ꞑ',
  'Ꞓ' => 'ꞓ',
  'Ꞗ' => 'ꞗ',
  'Ꞙ' => 'ꞙ',
  'Ꞛ' => 'ꞛ',
  'Ꞝ' => 'ꞝ',
  'Ꞟ' => 'ꞟ',
  'Ꞡ' => 'ꞡ',
  'Ꞣ' => 'ꞣ',
  'Ꞥ' => 'ꞥ',
  'Ꞧ' => 'ꞧ',
  'Ꞩ' => 'ꞩ',
  'Ɦ' => 'ɦ',
  'Ɜ' => 'ɜ',
  'Ɡ' => 'ɡ',
  'Ɬ' => 'ɬ',
  'Ɪ' => 'ɪ',
  'Ʞ' => 'ʞ',
  'Ʇ' => 'ʇ',
  'Ʝ' => 'ʝ',
  'Ꭓ' => 'ꭓ',
  'Ꞵ' => 'ꞵ',
  'Ꞷ' => 'ꞷ',
  'Ꞹ' => 'ꞹ',
  'Ꞻ' => 'ꞻ',
  'Ꞽ' => 'ꞽ',
  'Ꞿ' => 'ꞿ',
  'Ꟃ' => 'ꟃ',
  'Ꞔ' => 'ꞔ',
  'Ʂ' => 'ʂ',
  'Ᶎ' => 'ᶎ',
  'Ꟈ' => 'ꟈ',
  'Ꟊ' => 'ꟊ',
  'Ꟶ' => 'ꟶ',
  'Ａ' => 'ａ',
  'Ｂ' => 'ｂ',
  'Ｃ' => 'ｃ',
  'Ｄ' => 'ｄ',
  'Ｅ' => 'ｅ',
  'Ｆ' => 'ｆ',
  'Ｇ' => 'ｇ',
  'Ｈ' => 'ｈ',
  'Ｉ' => 'ｉ',
  'Ｊ' => 'ｊ',
  'Ｋ' => 'ｋ',
  'Ｌ' => 'ｌ',
  'Ｍ' => 'ｍ',
  'Ｎ' => 'ｎ',
  'Ｏ' => 'ｏ',
  'Ｐ' => 'ｐ',
  'Ｑ' => 'ｑ',
  'Ｒ' => 'ｒ',
  'Ｓ' => 'ｓ',
  'Ｔ' => 'ｔ',
  'Ｕ' => 'ｕ',
  'Ｖ' => 'ｖ',
  'Ｗ' => 'ｗ',
  'Ｘ' => 'ｘ',
  'Ｙ' => 'ｙ',
  'Ｚ' => 'ｚ',
  '𐐀' => '𐐨',
  '𐐁' => '𐐩',
  '𐐂' => '𐐪',
  '𐐃' => '𐐫',
  '𐐄' => '𐐬',
  '𐐅' => '𐐭',
  '𐐆' => '𐐮',
  '𐐇' => '𐐯',
  '𐐈' => '𐐰',
  '𐐉' => '𐐱',
  '𐐊' => '𐐲',
  '𐐋' => '𐐳',
  '𐐌' => '𐐴',
  '𐐍' => '𐐵',
  '𐐎' => '𐐶',
  '𐐏' => '𐐷',
  '𐐐' => '𐐸',
  '𐐑' => '𐐹',
  '𐐒' => '𐐺',
  '𐐓' => '𐐻',
  '𐐔' => '𐐼',
  '𐐕' => '𐐽',
  '𐐖' => '𐐾',
  '𐐗' => '𐐿',
  '𐐘' => '𐑀',
  '𐐙' => '𐑁',
  '𐐚' => '𐑂',
  '𐐛' => '𐑃',
  '𐐜' => '𐑄',
  '𐐝' => '𐑅',
  '𐐞' => '𐑆',
  '𐐟' => '𐑇',
  '𐐠' => '𐑈',
  '𐐡' => '𐑉',
  '𐐢' => '𐑊',
  '𐐣' => '𐑋',
  '𐐤' => '𐑌',
  '𐐥' => '𐑍',
  '𐐦' => '𐑎',
  '𐐧' => '𐑏',
  '𐒰' => '𐓘',
  '𐒱' => '𐓙',
  '𐒲' => '𐓚',
  '𐒳' => '𐓛',
  '𐒴' => '𐓜',
  '𐒵' => '𐓝',
  '𐒶' => '𐓞',
  '𐒷' => '𐓟',
  '𐒸' => '𐓠',
  '𐒹' => '𐓡',
  '𐒺' => '𐓢',
  '𐒻' => '𐓣',
  '𐒼' => '𐓤',
  '𐒽' => '𐓥',
  '𐒾' => '𐓦',
  '𐒿' => '𐓧',
  '𐓀' => '𐓨',
  '𐓁' => '𐓩',
  '𐓂' => '𐓪',
  '𐓃' => '𐓫',
  '𐓄' => '𐓬',
  '𐓅' => '𐓭',
  '𐓆' => '𐓮',
  '𐓇' => '𐓯',
  '𐓈' => '𐓰',
  '𐓉' => '𐓱',
  '𐓊' => '𐓲',
  '𐓋' => '𐓳',
  '𐓌' => '𐓴',
  '𐓍' => '𐓵',
  '𐓎' => '𐓶',
  '𐓏' => '𐓷',
  '𐓐' => '𐓸',
  '𐓑' => '𐓹',
  '𐓒' => '𐓺',
  '𐓓' => '𐓻',
  '𐲀' => '𐳀',
  '𐲁' => '𐳁',
  '𐲂' => '𐳂',
  '𐲃' => '𐳃',
  '𐲄' => '𐳄',
  '𐲅' => '𐳅',
  '𐲆' => '𐳆',
  '𐲇' => '𐳇',
  '𐲈' => '𐳈',
  '𐲉' => '𐳉',
  '𐲊' => '𐳊',
  '𐲋' => '𐳋',
  '𐲌' => '𐳌',
  '𐲍' => '𐳍',
  '𐲎' => '𐳎',
  '𐲏' => '𐳏',
  '𐲐' => '𐳐',
  '𐲑' => '𐳑',
  '𐲒' => '𐳒',
  '𐲓' => '𐳓',
  '𐲔' => '𐳔',
  '𐲕' => '𐳕',
  '𐲖' => '𐳖',
  '𐲗' => '𐳗',
  '𐲘' => '𐳘',
  '𐲙' => '𐳙',
  '𐲚' => '𐳚',
  '𐲛' => '𐳛',
  '𐲜' => '𐳜',
  '𐲝' => '𐳝',
  '𐲞' => '𐳞',
  '𐲟' => '𐳟',
  '𐲠' => '𐳠',
  '𐲡' => '𐳡',
  '𐲢' => '𐳢',
  '𐲣' => '𐳣',
  '𐲤' => '𐳤',
  '𐲥' => '𐳥',
  '𐲦' => '𐳦',
  '𐲧' => '𐳧',
  '𐲨' => '𐳨',
  '𐲩' => '𐳩',
  '𐲪' => '𐳪',
  '𐲫' => '𐳫',
  '𐲬' => '𐳬',
  '𐲭' => '𐳭',
  '𐲮' => '𐳮',
  '𐲯' => '𐳯',
  '𐲰' => '𐳰',
  '𐲱' => '𐳱',
  '𐲲' => '𐳲',
  '𑢠' => '𑣀',
  '𑢡' => '𑣁',
  '𑢢' => '𑣂',
  '𑢣' => '𑣃',
  '𑢤' => '𑣄',
  '𑢥' => '𑣅',
  '𑢦' => '𑣆',
  '𑢧' => '𑣇',
  '𑢨' => '𑣈',
  '𑢩' => '𑣉',
  '𑢪' => '𑣊',
  '𑢫' => '𑣋',
  '𑢬' => '𑣌',
  '𑢭' => '𑣍',
  '𑢮' => '𑣎',
  '𑢯' => '𑣏',
  '𑢰' => '𑣐',
  '𑢱' => '𑣑',
  '𑢲' => '𑣒',
  '𑢳' => '𑣓',
  '𑢴' => '𑣔',
  '𑢵' => '𑣕',
  '𑢶' => '𑣖',
  '𑢷' => '𑣗',
  '𑢸' => '𑣘',
  '𑢹' => '𑣙',
  '𑢺' => '𑣚',
  '𑢻' => '𑣛',
  '𑢼' => '𑣜',
  '𑢽' => '𑣝',
  '𑢾' => '𑣞',
  '𑢿' => '𑣟',
  '𖹀' => '𖹠',
  '𖹁' => '𖹡',
  '𖹂' => '𖹢',
  '𖹃' => '𖹣',
  '𖹄' => '𖹤',
  '𖹅' => '𖹥',
  '𖹆' => '𖹦',
  '𖹇' => '𖹧',
  '𖹈' => '𖹨',
  '𖹉' => '𖹩',
  '𖹊' => '𖹪',
  '𖹋' => '𖹫',
  '𖹌' => '𖹬',
  '𖹍' => '𖹭',
  '𖹎' => '𖹮',
  '𖹏' => '𖹯',
  '𖹐' => '𖹰',
  '𖹑' => '𖹱',
  '𖹒' => '𖹲',
  '𖹓' => '𖹳',
  '𖹔' => '𖹴',
  '𖹕' => '𖹵',
  '𖹖' => '𖹶',
  '𖹗' => '𖹷',
  '𖹘' => '𖹸',
  '𖹙' => '𖹹',
  '𖹚' => '𖹺',
  '𖹛' => '𖹻',
  '𖹜' => '𖹼',
  '𖹝' => '𖹽',
  '𖹞' => '𖹾',
  '𖹟' => '𖹿',
  '𞤀' => '𞤢',
  '𞤁' => '𞤣',
  '𞤂' => '𞤤',
  '𞤃' => '𞤥',
  '𞤄' => '𞤦',
  '𞤅' => '𞤧',
  '𞤆' => '𞤨',
  '𞤇' => '𞤩',
  '𞤈' => '𞤪',
  '𞤉' => '𞤫',
  '𞤊' => '𞤬',
  '𞤋' => '𞤭',
  '𞤌' => '𞤮',
  '𞤍' => '𞤯',
  '𞤎' => '𞤰',
  '𞤏' => '𞤱',
  '𞤐' => '𞤲',
  '𞤑' => '𞤳',
  '𞤒' => '𞤴',
  '𞤓' => '𞤵',
  '𞤔' => '𞤶',
  '𞤕' => '𞤷',
  '𞤖' => '𞤸',
  '𞤗' => '𞤹',
  '𞤘' => '𞤺',
  '𞤙' => '𞤻',
  '𞤚' => '𞤼',
  '𞤛' => '𞤽',
  '𞤜' => '𞤾',
  '𞤝' => '𞤿',
  '𞤞' => '𞥀',
  '𞤟' => '𞥁',
  '𞤠' => '𞥂',
  '𞤡' => '𞥃',
);
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Finder\Exception;

/**
 * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
 */
class AccessDeniedException extends \UnexpectedValueException
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Finder\Exception;

/**
 * @author Andreas Erhard <andreas.erhard@i-med.ac.at>
 */
class DirectoryNotFoundException extends \InvalidArgumentException
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Finder;

/**
 * Extends \SplFileInfo to support relative paths.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class SplFileInfo extends \SplFileInfo
{
    private string $relativePath;
    private string $relativePathname;

    /**
     * @param string $file             The file name
     * @param string $relativePath     The relative path
     * @param string $relativePathname The relative path name
     */
    public function __construct(string $file, string $relativePath, string $relativePathname)
    {
        parent::__construct($file);
        $this->relativePath = $relativePath;
        $this->relativePathname = $relativePathname;
    }

    /**
     * Returns the relative path.
     *
     * This path does not contain the file name.
     */
    public function getRelativePath(): string
    {
        return $this->relativePath;
    }

    /**
     * Returns the relative path name.
     *
     * This path contains the file name.
     */
    public function getRelativePathname(): string
    {
        return $this->relativePathname;
    }

    public function getFilenameWithoutExtension(): string
    {
        $filename = $this->getFilename();

        return pathinfo($filename, \PATHINFO_FILENAME);
    }

    /**
     * Returns the contents of the file.
     *
     * @throws \RuntimeException
     */
    public function getContents(): string
    {
        set_error_handler(static function ($type, $msg) use (&$error) { $error = $msg; });
        try {
            $content = file_get_contents($this->getPathname());
        } finally {
            restore_error_handler();
        }
        if (false === $content) {
            throw new \RuntimeException($error);
        }

        return $content;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Finder;

/**
 * Gitignore matches against text.
 *
 * @author Michael Voříšek <vorismi3@fel.cvut.cz>
 * @author Ahmed Abdou <mail@ahmd.io>
 */
class Gitignore
{
    /**
     * Returns a regexp which is the equivalent of the gitignore pattern.
     *
     * Format specification: https://git-scm.com/docs/gitignore#_pattern_format
     */
    public static function toRegex(string $gitignoreFileContent): string
    {
        return self::buildRegex($gitignoreFileContent, false);
    }

    public static function toRegexMatchingNegatedPatterns(string $gitignoreFileContent): string
    {
        return self::buildRegex($gitignoreFileContent, true);
    }

    private static function buildRegex(string $gitignoreFileContent, bool $inverted): string
    {
        $gitignoreFileContent = preg_replace('~(?<!\\\\)#[^\n\r]*~', '', $gitignoreFileContent);
        $gitignoreLines = preg_split('~\r\n?|\n~', $gitignoreFileContent);

        $res = self::lineToRegex('');
        foreach ($gitignoreLines as $line) {
            $line = preg_replace('~(?<!\\\\)[ \t]+$~', '', $line);

            if (str_starts_with($line, '!')) {
                $line = substr($line, 1);
                $isNegative = true;
            } else {
                $isNegative = false;
            }

            if ('' !== $line) {
                if ($isNegative xor $inverted) {
                    $res = '(?!'.self::lineToRegex($line).'$)'.$res;
                } else {
                    $res = '(?:'.$res.'|'.self::lineToRegex($line).')';
                }
            }
        }

        return '~^(?:'.$res.')~s';
    }

    private static function lineToRegex(string $gitignoreLine): string
    {
        if ('' === $gitignoreLine) {
            return '$f'; // always false
        }

        $slashPos = strpos($gitignoreLine, '/');
        if (false !== $slashPos && \strlen($gitignoreLine) - 1 !== $slashPos) {
            if (0 === $slashPos) {
                $gitignoreLine = substr($gitignoreLine, 1);
            }
            $isAbsolute = true;
        } else {
            $isAbsolute = false;
        }

        $regex = preg_quote(str_replace('\\', '', $gitignoreLine), '~');
        $regex = preg_replace_callback('~\\\\\[((?:\\\\!)?)([^\[\]]*)\\\\\]~', static fn (array $matches): string => '['.('' !== $matches[1] ? '^' : '').str_replace('\\-', '-', $matches[2]).']', $regex);
        $regex = preg_replace('~(?:(?:\\\\\*){2,}(/?))+~', '(?:(?:(?!//).(?<!//))+$1)?', $regex);
        $regex = preg_replace('~\\\\\*~', '[^/]*', $regex);
        $regex = preg_replace('~\\\\\?~', '[^/]', $regex);

        return ($isAbsolute ? '' : '(?:[^/]+/)*')
            .$regex
            .(!str_ends_with($gitignoreLine, '/') ? '(?:$|/)' : '');
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Finder\Iterator;

/**
 * FileTypeFilterIterator only keeps files, directories, or both.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 *
 * @extends \FilterIterator<string, \SplFileInfo>
 */
class FileTypeFilterIterator extends \FilterIterator
{
    public const ONLY_FILES = 1;
    public const ONLY_DIRECTORIES = 2;

    private int $mode;

    /**
     * @param \Iterator<string, \SplFileInfo> $iterator The Iterator to filter
     * @param int                             $mode     The mode (self::ONLY_FILES or self::ONLY_DIRECTORIES)
     */
    public function __construct(\Iterator $iterator, int $mode)
    {
        $this->mode = $mode;

        parent::__construct($iterator);
    }

    /**
     * Filters the iterator values.
     */
    public function accept(): bool
    {
        $fileinfo = $this->current();
        if (self::ONLY_DIRECTORIES === (self::ONLY_DIRECTORIES & $this->mode) && $fileinfo->isFile()) {
            return false;
        } elseif (self::ONLY_FILES === (self::ONLY_FILES & $this->mode) && $fileinfo->isDir()) {
            return false;
        }

        return true;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Finder\Iterator;

use Symfony\Component\Finder\SplFileInfo;

/**
 * PathFilterIterator filters files by path patterns (e.g. some/special/dir).
 *
 * @author Fabien Potencier  <fabien@symfony.com>
 * @author Włodzimierz Gajda <gajdaw@gajdaw.pl>
 *
 * @extends MultiplePcreFilterIterator<string, SplFileInfo>
 */
class PathFilterIterator extends MultiplePcreFilterIterator
{
    /**
     * Filters the iterator values.
     */
    public function accept(): bool
    {
        $filename = $this->current()->getRelativePathname();

        if ('\\' === \DIRECTORY_SEPARATOR) {
            $filename = str_replace('\\', '/', $filename);
        }

        return $this->isAccepted($filename);
    }

    /**
     * Converts strings to regexp.
     *
     * PCRE patterns are left unchanged.
     *
     * Default conversion:
     *     'lorem/ipsum/dolor' ==>  'lorem\/ipsum\/dolor/'
     *
     * Use only / as directory separator (on Windows also).
     *
     * @param string $str Pattern: regexp or dirname
     */
    protected function toRegex(string $str): string
    {
        return $this->isRegex($str) ? $str : '/'.preg_quote($str, '/').'/';
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Finder\Iterator;

/**
 * MultiplePcreFilterIterator filters files using patterns (regexps, globs or strings).
 *
 * @author Fabien Potencier <fabien@symfony.com>
 *
 * @template-covariant TKey
 * @template-covariant TValue
 *
 * @extends \FilterIterator<TKey, TValue>
 */
abstract class MultiplePcreFilterIterator extends \FilterIterator
{
    protected $matchRegexps = [];
    protected $noMatchRegexps = [];

    /**
     * @param \Iterator<TKey, TValue> $iterator        The Iterator to filter
     * @param string[]                $matchPatterns   An array of patterns that need to match
     * @param string[]                $noMatchPatterns An array of patterns that need to not match
     */
    public function __construct(\Iterator $iterator, array $matchPatterns, array $noMatchPatterns)
    {
        foreach ($matchPatterns as $pattern) {
            $this->matchRegexps[] = $this->toRegex($pattern);
        }

        foreach ($noMatchPatterns as $pattern) {
            $this->noMatchRegexps[] = $this->toRegex($pattern);
        }

        parent::__construct($iterator);
    }

    /**
     * Checks whether the string is accepted by the regex filters.
     *
     * If there is no regexps defined in the class, this method will accept the string.
     * Such case can be handled by child classes before calling the method if they want to
     * apply a different behavior.
     */
    protected function isAccepted(string $string): bool
    {
        // should at least not match one rule to exclude
        foreach ($this->noMatchRegexps as $regex) {
            if (preg_match($regex, $string)) {
                return false;
            }
        }

        // should at least match one rule
        if ($this->matchRegexps) {
            foreach ($this->matchRegexps as $regex) {
                if (preg_match($regex, $string)) {
                    return true;
                }
            }

            return false;
        }

        // If there is no match rules, the file is accepted
        return true;
    }

    /**
     * Checks whether the string is a regex.
     */
    protected function isRegex(string $str): bool
    {
        $availableModifiers = 'imsxuADU';

        if (\PHP_VERSION_ID >= 80200) {
            $availableModifiers .= 'n';
        }

        if (preg_match('/^(.{3,}?)['.$availableModifiers.']*$/', $str, $m)) {
            $start = substr($m[1], 0, 1);
            $end = substr($m[1], -1);

            if ($start === $end) {
                return !preg_match('/[*?[:alnum:] \\\\]/', $start);
            }

            foreach ([['{', '}'], ['(', ')'], ['[', ']'], ['<', '>']] as $delimiters) {
                if ($start === $delimiters[0] && $end === $delimiters[1]) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * Converts string into regexp.
     */
    abstract protected function toRegex(string $str): string;
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Finder\Iterator;

use Symfony\Component\Finder\Gitignore;

/**
 * @extends \FilterIterator<string, \SplFileInfo>
 */
final class VcsIgnoredFilterIterator extends \FilterIterator
{
    private string $baseDir;

    /**
     * @var array<string, array{0: string, 1: string}|null>
     */
    private array $gitignoreFilesCache = [];

    /**
     * @var array<string, bool>
     */
    private array $ignoredPathsCache = [];

    /**
     * @param \Iterator<string, \SplFileInfo> $iterator
     */
    public function __construct(\Iterator $iterator, string $baseDir)
    {
        $this->baseDir = $this->normalizePath($baseDir);

        foreach ([$this->baseDir, ...$this->parentDirectoriesUpwards($this->baseDir)] as $directory) {
            if (@is_dir("{$directory}/.git")) {
                $this->baseDir = $directory;
                break;
            }
        }

        parent::__construct($iterator);
    }

    public function accept(): bool
    {
        $file = $this->current();

        $fileRealPath = $this->normalizePath($file->getRealPath());

        return !$this->isIgnored($fileRealPath);
    }

    private function isIgnored(string $fileRealPath): bool
    {
        if (is_dir($fileRealPath) && !str_ends_with($fileRealPath, '/')) {
            $fileRealPath .= '/';
        }

        if (isset($this->ignoredPathsCache[$fileRealPath])) {
            return $this->ignoredPathsCache[$fileRealPath];
        }

        $ignored = false;

        foreach ($this->parentDirectoriesDownwards($fileRealPath) as $parentDirectory) {
            if ($this->isIgnored($parentDirectory)) {
                // rules in ignored directories are ignored, no need to check further.
                break;
            }

            $fileRelativePath = substr($fileRealPath, \strlen($parentDirectory) + 1);

            if (null === $regexps = $this->readGitignoreFile("{$parentDirectory}/.gitignore")) {
                continue;
            }

            [$exclusionRegex, $inclusionRegex] = $regexps;

            if (preg_match($exclusionRegex, $fileRelativePath)) {
                $ignored = true;

                continue;
            }

            if (preg_match($inclusionRegex, $fileRelativePath)) {
                $ignored = false;
            }
        }

        return $this->ignoredPathsCache[$fileRealPath] = $ignored;
    }

    /**
     * @return list<string>
     */
    private function parentDirectoriesUpwards(string $from): array
    {
        $parentDirectories = [];

        $parentDirectory = $from;

        while (true) {
            $newParentDirectory = \dirname($parentDirectory);

            // dirname('/') = '/'
            if ($newParentDirectory === $parentDirectory) {
                break;
            }

            $parentDirectories[] = $parentDirectory = $newParentDirectory;
        }

        return $parentDirectories;
    }

    private function parentDirectoriesUpTo(string $from, string $upTo): array
    {
        return array_filter(
            $this->parentDirectoriesUpwards($from),
            static fn (string $directory): bool => str_starts_with($directory, $upTo)
        );
    }

    /**
     * @return list<string>
     */
    private function parentDirectoriesDownwards(string $fileRealPath): array
    {
        return array_reverse(
            $this->parentDirectoriesUpTo($fileRealPath, $this->baseDir)
        );
    }

    /**
     * @return array{0: string, 1: string}|null
     */
    private function readGitignoreFile(string $path): ?array
    {
        if (\array_key_exists($path, $this->gitignoreFilesCache)) {
            return $this->gitignoreFilesCache[$path];
        }

        if (!file_exists($path)) {
            return $this->gitignoreFilesCache[$path] = null;
        }

        if (!is_file($path) || !is_readable($path)) {
            throw new \RuntimeException("The \"ignoreVCSIgnored\" option cannot be used by the Finder as the \"{$path}\" file is not readable.");
        }

        $gitignoreFileContent = file_get_contents($path);

        return $this->gitignoreFilesCache[$path] = [
            Gitignore::toRegex($gitignoreFileContent),
            Gitignore::toRegexMatchingNegatedPatterns($gitignoreFileContent),
        ];
    }

    private function normalizePath(string $path): string
    {
        if ('\\' === \DIRECTORY_SEPARATOR) {
            return str_replace('\\', '/', $path);
        }

        return $path;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Finder\Iterator;

use Symfony\Component\Finder\SplFileInfo;

/**
 * FilecontentFilterIterator filters files by their contents using patterns (regexps or strings).
 *
 * @author Fabien Potencier  <fabien@symfony.com>
 * @author Włodzimierz Gajda <gajdaw@gajdaw.pl>
 *
 * @extends MultiplePcreFilterIterator<string, SplFileInfo>
 */
class FilecontentFilterIterator extends MultiplePcreFilterIterator
{
    /**
     * Filters the iterator values.
     */
    public function accept(): bool
    {
        if (!$this->matchRegexps && !$this->noMatchRegexps) {
            return true;
        }

        $fileinfo = $this->current();

        if ($fileinfo->isDir() || !$fileinfo->isReadable()) {
            return false;
        }

        $content = $fileinfo->getContents();
        if (!$content) {
            return false;
        }

        return $this->isAccepted($content);
    }

    /**
     * Converts string to regexp if necessary.
     *
     * @param string $str Pattern: string or regexp
     */
    protected function toRegex(string $str): string
    {
        return $this->isRegex($str) ? $str : '/'.preg_quote($str, '/').'/';
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Finder\Iterator;

use Symfony\Component\Finder\SplFileInfo;

/**
 * ExcludeDirectoryFilterIterator filters out directories.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 *
 * @extends \FilterIterator<string, SplFileInfo>
 *
 * @implements \RecursiveIterator<string, SplFileInfo>
 */
class ExcludeDirectoryFilterIterator extends \FilterIterator implements \RecursiveIterator
{
    /** @var \Iterator<string, SplFileInfo> */
    private \Iterator $iterator;
    private bool $isRecursive;
    /** @var array<string, true> */
    private array $excludedDirs = [];
    private ?string $excludedPattern = null;
    /** @var list<callable(SplFileInfo):bool> */
    private array $pruneFilters = [];

    /**
     * @param \Iterator<string, SplFileInfo>          $iterator    The Iterator to filter
     * @param list<string|callable(SplFileInfo):bool> $directories An array of directories to exclude
     */
    public function __construct(\Iterator $iterator, array $directories)
    {
        $this->iterator = $iterator;
        $this->isRecursive = $iterator instanceof \RecursiveIterator;
        $patterns = [];
        foreach ($directories as $directory) {
            if (!\is_string($directory)) {
                if (!\is_callable($directory)) {
                    throw new \InvalidArgumentException('Invalid PHP callback.');
                }

                $this->pruneFilters[] = $directory;

                continue;
            }

            $directory = rtrim($directory, '/');
            if (!$this->isRecursive || str_contains($directory, '/')) {
                $patterns[] = preg_quote($directory, '#');
            } else {
                $this->excludedDirs[$directory] = true;
            }
        }
        if ($patterns) {
            $this->excludedPattern = '#(?:^|/)(?:'.implode('|', $patterns).')(?:/|$)#';
        }

        parent::__construct($iterator);
    }

    /**
     * Filters the iterator values.
     */
    public function accept(): bool
    {
        if ($this->isRecursive && isset($this->excludedDirs[$this->getFilename()]) && $this->isDir()) {
            return false;
        }

        if ($this->excludedPattern) {
            $path = $this->isDir() ? $this->current()->getRelativePathname() : $this->current()->getRelativePath();
            $path = str_replace('\\', '/', $path);

            return !preg_match($this->excludedPattern, $path);
        }

        if ($this->pruneFilters && $this->hasChildren()) {
            foreach ($this->pruneFilters as $pruneFilter) {
                if (!$pruneFilter($this->current())) {
                    return false;
                }
            }
        }

        return true;
    }

    public function hasChildren(): bool
    {
        return $this->isRecursive && $this->iterator->hasChildren();
    }

    public function getChildren(): self
    {
        $children = new self($this->iterator->getChildren(), []);
        $children->excludedDirs = $this->excludedDirs;
        $children->excludedPattern = $this->excludedPattern;

        return $children;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Finder\Iterator;

/**
 * DepthRangeFilterIterator limits the directory depth.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 *
 * @template-covariant TKey
 * @template-covariant TValue
 *
 * @extends \FilterIterator<TKey, TValue>
 */
class DepthRangeFilterIterator extends \FilterIterator
{
    private int $minDepth = 0;

    /**
     * @param \RecursiveIteratorIterator<\RecursiveIterator<TKey, TValue>> $iterator The Iterator to filter
     * @param int                                                          $minDepth The min depth
     * @param int                                                          $maxDepth The max depth
     */
    public function __construct(\RecursiveIteratorIterator $iterator, int $minDepth = 0, int $maxDepth = \PHP_INT_MAX)
    {
        $this->minDepth = $minDepth;
        $iterator->setMaxDepth(\PHP_INT_MAX === $maxDepth ? -1 : $maxDepth);

        parent::__construct($iterator);
    }

    /**
     * Filters the iterator values.
     */
    public function accept(): bool
    {
        return $this->getInnerIterator()->getDepth() >= $this->minDepth;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Finder\Iterator;

use Symfony\Component\Finder\Exception\AccessDeniedException;
use Symfony\Component\Finder\SplFileInfo;

/**
 * Extends the \RecursiveDirectoryIterator to support relative paths.
 *
 * @author Victor Berchet <victor@suumit.com>
 *
 * @extends \RecursiveDirectoryIterator<string, SplFileInfo>
 */
class RecursiveDirectoryIterator extends \RecursiveDirectoryIterator
{
    private bool $ignoreUnreadableDirs;
    private bool $ignoreFirstRewind = true;

    // these 3 properties take part of the performance optimization to avoid redoing the same work in all iterations
    private string $rootPath;
    private string $subPath;
    private string $directorySeparator = '/';

    /**
     * @throws \RuntimeException
     */
    public function __construct(string $path, int $flags, bool $ignoreUnreadableDirs = false)
    {
        if ($flags & (self::CURRENT_AS_PATHNAME | self::CURRENT_AS_SELF)) {
            throw new \RuntimeException('This iterator only support returning current as fileinfo.');
        }

        parent::__construct($path, $flags);
        $this->ignoreUnreadableDirs = $ignoreUnreadableDirs;
        $this->rootPath = $path;
        if ('/' !== \DIRECTORY_SEPARATOR && !($flags & self::UNIX_PATHS)) {
            $this->directorySeparator = \DIRECTORY_SEPARATOR;
        }
    }

    /**
     * Return an instance of SplFileInfo with support for relative paths.
     */
    public function current(): SplFileInfo
    {
        // the logic here avoids redoing the same work in all iterations

        if (!isset($this->subPath)) {
            $this->subPath = $this->getSubPath();
        }
        $subPathname = $this->subPath;
        if ('' !== $subPathname) {
            $subPathname .= $this->directorySeparator;
        }
        $subPathname .= $this->getFilename();
        $basePath = $this->rootPath;

        if ('/' !== $basePath && !str_ends_with($basePath, $this->directorySeparator) && !str_ends_with($basePath, '/')) {
            $basePath .= $this->directorySeparator;
        }

        return new SplFileInfo($basePath.$subPathname, $this->subPath, $subPathname);
    }

    public function hasChildren(bool $allowLinks = false): bool
    {
        $hasChildren = parent::hasChildren($allowLinks);

        if (!$hasChildren || !$this->ignoreUnreadableDirs) {
            return $hasChildren;
        }

        try {
            parent::getChildren();

            return true;
        } catch (\UnexpectedValueException) {
            // If directory is unreadable and finder is set to ignore it, skip children
            return false;
        }
    }

    /**
     * @throws AccessDeniedException
     */
    public function getChildren(): \RecursiveDirectoryIterator
    {
        try {
            $children = parent::getChildren();

            if ($children instanceof self) {
                // parent method will call the constructor with default arguments, so unreadable dirs won't be ignored anymore
                $children->ignoreUnreadableDirs = $this->ignoreUnreadableDirs;

                // performance optimization to avoid redoing the same work in all children
                $children->rootPath = $this->rootPath;
            }

            return $children;
        } catch (\UnexpectedValueException $e) {
            throw new AccessDeniedException($e->getMessage(), $e->getCode(), $e);
        }
    }

    public function next(): void
    {
        $this->ignoreFirstRewind = false;

        parent::next();
    }

    public function rewind(): void
    {
        // some streams like FTP are not rewindable, ignore the first rewind after creation,
        // as newly created DirectoryIterator does not need to be rewound
        if ($this->ignoreFirstRewind) {
            $this->ignoreFirstRewind = false;

            return;
        }

        parent::rewind();
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Finder\Iterator;

/**
 * SortableIterator applies a sort on a given Iterator.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 *
 * @implements \IteratorAggregate<string, \SplFileInfo>
 */
class SortableIterator implements \IteratorAggregate
{
    public const SORT_BY_NONE = 0;
    public const SORT_BY_NAME = 1;
    public const SORT_BY_TYPE = 2;
    public const SORT_BY_ACCESSED_TIME = 3;
    public const SORT_BY_CHANGED_TIME = 4;
    public const SORT_BY_MODIFIED_TIME = 5;
    public const SORT_BY_NAME_NATURAL = 6;
    public const SORT_BY_NAME_CASE_INSENSITIVE = 7;
    public const SORT_BY_NAME_NATURAL_CASE_INSENSITIVE = 8;
    public const SORT_BY_EXTENSION = 9;
    public const SORT_BY_SIZE = 10;

    /** @var \Traversable<string, \SplFileInfo> */
    private \Traversable $iterator;
    private \Closure|int $sort;

    /**
     * @param \Traversable<string, \SplFileInfo> $iterator
     * @param int|callable                       $sort     The sort type (SORT_BY_NAME, SORT_BY_TYPE, or a PHP callback)
     *
     * @throws \InvalidArgumentException
     */
    public function __construct(\Traversable $iterator, int|callable $sort, bool $reverseOrder = false)
    {
        $this->iterator = $iterator;
        $order = $reverseOrder ? -1 : 1;

        if (self::SORT_BY_NAME === $sort) {
            $this->sort = static fn (\SplFileInfo $a, \SplFileInfo $b) => $order * strcmp($a->getRealPath() ?: $a->getPathname(), $b->getRealPath() ?: $b->getPathname());
        } elseif (self::SORT_BY_NAME_NATURAL === $sort) {
            $this->sort = static fn (\SplFileInfo $a, \SplFileInfo $b) => $order * strnatcmp($a->getRealPath() ?: $a->getPathname(), $b->getRealPath() ?: $b->getPathname());
        } elseif (self::SORT_BY_NAME_CASE_INSENSITIVE === $sort) {
            $this->sort = static fn (\SplFileInfo $a, \SplFileInfo $b) => $order * strcasecmp($a->getRealPath() ?: $a->getPathname(), $b->getRealPath() ?: $b->getPathname());
        } elseif (self::SORT_BY_NAME_NATURAL_CASE_INSENSITIVE === $sort) {
            $this->sort = static fn (\SplFileInfo $a, \SplFileInfo $b) => $order * strnatcasecmp($a->getRealPath() ?: $a->getPathname(), $b->getRealPath() ?: $b->getPathname());
        } elseif (self::SORT_BY_TYPE === $sort) {
            $this->sort = static function (\SplFileInfo $a, \SplFileInfo $b) use ($order) {
                if ($a->isDir() && $b->isFile()) {
                    return -$order;
                } elseif ($a->isFile() && $b->isDir()) {
                    return $order;
                }

                return $order * strcmp($a->getRealPath() ?: $a->getPathname(), $b->getRealPath() ?: $b->getPathname());
            };
        } elseif (self::SORT_BY_ACCESSED_TIME === $sort) {
            $this->sort = static fn (\SplFileInfo $a, \SplFileInfo $b) => $order * ($a->getATime() - $b->getATime());
        } elseif (self::SORT_BY_CHANGED_TIME === $sort) {
            $this->sort = static fn (\SplFileInfo $a, \SplFileInfo $b) => $order * ($a->getCTime() - $b->getCTime());
        } elseif (self::SORT_BY_MODIFIED_TIME === $sort) {
            $this->sort = static fn (\SplFileInfo $a, \SplFileInfo $b) => $order * ($a->getMTime() - $b->getMTime());
        } elseif (self::SORT_BY_EXTENSION === $sort) {
            $this->sort = static fn (\SplFileInfo $a, \SplFileInfo $b) => $order * strnatcmp($a->getExtension(), $b->getExtension());
        } elseif (self::SORT_BY_SIZE === $sort) {
            $this->sort = static fn (\SplFileInfo $a, \SplFileInfo $b) => $order * ($a->getSize() - $b->getSize());
        } elseif (self::SORT_BY_NONE === $sort) {
            $this->sort = $order;
        } elseif (\is_callable($sort)) {
            $this->sort = $reverseOrder ? static fn (\SplFileInfo $a, \SplFileInfo $b) => -$sort($a, $b) : $sort(...);
        } else {
            throw new \InvalidArgumentException('The SortableIterator takes a PHP callable or a valid built-in sort algorithm as an argument.');
        }
    }

    public function getIterator(): \Traversable
    {
        if (1 === $this->sort) {
            yield from $this->iterator;

            return;
        }

        $keys = $values = [];
        foreach ($this->iterator as $key => $value) {
            $keys[] = $key;
            $values[] = $value;
        }

        if (-1 === $this->sort) {
            for ($i = \count($values) - 1; $i >= 0; --$i) {
                yield $keys[$i] => $values[$i];
            }

            return;
        }

        uasort($values, $this->sort);

        foreach ($values as $i => $v) {
            yield $keys[$i] => $v;
        }
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Finder\Iterator;

use Symfony\Component\Finder\Glob;

/**
 * FilenameFilterIterator filters files by patterns (a regexp, a glob, or a string).
 *
 * @author Fabien Potencier <fabien@symfony.com>
 *
 * @extends MultiplePcreFilterIterator<string, \SplFileInfo>
 */
class FilenameFilterIterator extends MultiplePcreFilterIterator
{
    /**
     * Filters the iterator values.
     */
    public function accept(): bool
    {
        return $this->isAccepted($this->current()->getFilename());
    }

    /**
     * Converts glob to regexp.
     *
     * PCRE patterns are left unchanged.
     * Glob strings are transformed with Glob::toRegex().
     *
     * @param string $str Pattern: glob or regexp
     */
    protected function toRegex(string $str): string
    {
        return $this->isRegex($str) ? $str : Glob::toRegex($str);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Finder\Iterator;

use Symfony\Component\Finder\Comparator\NumberComparator;

/**
 * SizeRangeFilterIterator filters out files that are not in the given size range.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 *
 * @extends \FilterIterator<string, \SplFileInfo>
 */
class SizeRangeFilterIterator extends \FilterIterator
{
    private array $comparators = [];

    /**
     * @param \Iterator<string, \SplFileInfo> $iterator
     * @param NumberComparator[]              $comparators
     */
    public function __construct(\Iterator $iterator, array $comparators)
    {
        $this->comparators = $comparators;

        parent::__construct($iterator);
    }

    /**
     * Filters the iterator values.
     */
    public function accept(): bool
    {
        $fileinfo = $this->current();
        if (!$fileinfo->isFile()) {
            return true;
        }

        $filesize = $fileinfo->getSize();
        foreach ($this->comparators as $compare) {
            if (!$compare->test($filesize)) {
                return false;
            }
        }

        return true;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Finder\Iterator;

/**
 * @author Jérémy Derussé <jeremy@derusse.com>
 *
 * @internal
 */
class LazyIterator implements \IteratorAggregate
{
    private \Closure $iteratorFactory;

    public function __construct(callable $iteratorFactory)
    {
        $this->iteratorFactory = $iteratorFactory(...);
    }

    public function getIterator(): \Traversable
    {
        yield from ($this->iteratorFactory)();
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Finder\Iterator;

use Symfony\Component\Finder\Comparator\DateComparator;

/**
 * DateRangeFilterIterator filters out files that are not in the given date range (last modified dates).
 *
 * @author Fabien Potencier <fabien@symfony.com>
 *
 * @extends \FilterIterator<string, \SplFileInfo>
 */
class DateRangeFilterIterator extends \FilterIterator
{
    private array $comparators = [];

    /**
     * @param \Iterator<string, \SplFileInfo> $iterator
     * @param DateComparator[]                $comparators
     */
    public function __construct(\Iterator $iterator, array $comparators)
    {
        $this->comparators = $comparators;

        parent::__construct($iterator);
    }

    /**
     * Filters the iterator values.
     */
    public function accept(): bool
    {
        $fileinfo = $this->current();

        if (!file_exists($fileinfo->getPathname())) {
            return false;
        }

        $filedate = $fileinfo->getMTime();
        foreach ($this->comparators as $compare) {
            if (!$compare->test($filedate)) {
                return false;
            }
        }

        return true;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Finder\Iterator;

/**
 * CustomFilterIterator filters files by applying anonymous functions.
 *
 * The anonymous function receives a \SplFileInfo and must return false
 * to remove files.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 *
 * @extends \FilterIterator<string, \SplFileInfo>
 */
class CustomFilterIterator extends \FilterIterator
{
    private array $filters = [];

    /**
     * @param \Iterator<string, \SplFileInfo> $iterator The Iterator to filter
     * @param callable[]                      $filters  An array of PHP callbacks
     *
     * @throws \InvalidArgumentException
     */
    public function __construct(\Iterator $iterator, array $filters)
    {
        foreach ($filters as $filter) {
            if (!\is_callable($filter)) {
                throw new \InvalidArgumentException('Invalid PHP callback.');
            }
        }
        $this->filters = $filters;

        parent::__construct($iterator);
    }

    /**
     * Filters the iterator values.
     */
    public function accept(): bool
    {
        $fileinfo = $this->current();

        foreach ($this->filters as $filter) {
            if (false === $filter($fileinfo)) {
                return false;
            }
        }

        return true;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Finder\Comparator;

/**
 * @author Fabien Potencier <fabien@symfony.com>
 */
class Comparator
{
    private string $target;
    private string $operator;

    public function __construct(string $target, string $operator = '==')
    {
        if (!\in_array($operator, ['>', '<', '>=', '<=', '==', '!='])) {
            throw new \InvalidArgumentException(\sprintf('Invalid operator "%s".', $operator));
        }

        $this->target = $target;
        $this->operator = $operator;
    }

    /**
     * Gets the target value.
     */
    public function getTarget(): string
    {
        return $this->target;
    }

    /**
     * Gets the comparison operator.
     */
    public function getOperator(): string
    {
        return $this->operator;
    }

    /**
     * Tests against the target.
     */
    public function test(mixed $test): bool
    {
        return match ($this->operator) {
            '>' => $test > $this->target,
            '>=' => $test >= $this->target,
            '<' => $test < $this->target,
            '<=' => $test <= $this->target,
            '!=' => $test != $this->target,
            default => $test == $this->target,
        };
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Finder\Comparator;

/**
 * DateCompare compiles date comparisons.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class DateComparator extends Comparator
{
    /**
     * @param string $test A comparison string
     *
     * @throws \InvalidArgumentException If the test is not understood
     */
    public function __construct(string $test)
    {
        if (!preg_match('#^\s*(==|!=|[<>]=?|after|since|before|until)?\s*(.+?)\s*$#i', $test, $matches)) {
            throw new \InvalidArgumentException(\sprintf('Don\'t understand "%s" as a date test.', $test));
        }

        try {
            $date = new \DateTimeImmutable($matches[2]);
            $target = $date->format('U');
        } catch (\Exception) {
            throw new \InvalidArgumentException(\sprintf('"%s" is not a valid date.', $matches[2]));
        }

        $operator = $matches[1] ?: '==';
        if ('since' === $operator || 'after' === $operator) {
            $operator = '>';
        }

        if ('until' === $operator || 'before' === $operator) {
            $operator = '<';
        }

        parent::__construct($target, $operator);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Finder\Comparator;

/**
 * NumberComparator compiles a simple comparison to an anonymous
 * subroutine, which you can call with a value to be tested again.
 *
 * Now this would be very pointless, if NumberCompare didn't understand
 * magnitudes.
 *
 * The target value may use magnitudes of kilobytes (k, ki),
 * megabytes (m, mi), or gigabytes (g, gi).  Those suffixed
 * with an i use the appropriate 2**n version in accordance with the
 * IEC standard: http://physics.nist.gov/cuu/Units/binary.html
 *
 * Based on the Perl Number::Compare module.
 *
 * @author    Fabien Potencier <fabien@symfony.com> PHP port
 * @author    Richard Clamp <richardc@unixbeard.net> Perl version
 * @copyright 2004-2005 Fabien Potencier <fabien@symfony.com>
 * @copyright 2002 Richard Clamp <richardc@unixbeard.net>
 *
 * @see http://physics.nist.gov/cuu/Units/binary.html
 */
class NumberComparator extends Comparator
{
    /**
     * @param string|null $test A comparison string or null
     *
     * @throws \InvalidArgumentException If the test is not understood
     */
    public function __construct(?string $test)
    {
        if (null === $test || !preg_match('#^\s*(==|!=|[<>]=?)?\s*([0-9\.]+)\s*([kmg]i?)?\s*$#i', $test, $matches)) {
            throw new \InvalidArgumentException(\sprintf('Don\'t understand "%s" as a number test.', $test ?? 'null'));
        }

        $target = $matches[2];
        if (!is_numeric($target)) {
            throw new \InvalidArgumentException(\sprintf('Invalid number "%s".', $target));
        }
        if (isset($matches[3])) {
            // magnitude
            switch (strtolower($matches[3])) {
                case 'k':
                    $target *= 1000;
                    break;
                case 'ki':
                    $target *= 1024;
                    break;
                case 'm':
                    $target *= 1000000;
                    break;
                case 'mi':
                    $target *= 1024 * 1024;
                    break;
                case 'g':
                    $target *= 1000000000;
                    break;
                case 'gi':
                    $target *= 1024 * 1024 * 1024;
                    break;
            }
        }

        parent::__construct($target, $matches[1] ?: '==');
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Finder;

use Symfony\Component\Finder\Comparator\DateComparator;
use Symfony\Component\Finder\Comparator\NumberComparator;
use Symfony\Component\Finder\Exception\DirectoryNotFoundException;
use Symfony\Component\Finder\Iterator\CustomFilterIterator;
use Symfony\Component\Finder\Iterator\DateRangeFilterIterator;
use Symfony\Component\Finder\Iterator\DepthRangeFilterIterator;
use Symfony\Component\Finder\Iterator\ExcludeDirectoryFilterIterator;
use Symfony\Component\Finder\Iterator\FilecontentFilterIterator;
use Symfony\Component\Finder\Iterator\FilenameFilterIterator;
use Symfony\Component\Finder\Iterator\LazyIterator;
use Symfony\Component\Finder\Iterator\SizeRangeFilterIterator;
use Symfony\Component\Finder\Iterator\SortableIterator;

/**
 * Finder allows to build rules to find files and directories.
 *
 * It is a thin wrapper around several specialized iterator classes.
 *
 * All rules may be invoked several times.
 *
 * All methods return the current Finder object to allow chaining:
 *
 *     $finder = Finder::create()->files()->name('*.php')->in(__DIR__);
 *
 * @author Fabien Potencier <fabien@symfony.com>
 *
 * @implements \IteratorAggregate<string, SplFileInfo>
 */
class Finder implements \IteratorAggregate, \Countable
{
    public const IGNORE_VCS_FILES = 1;
    public const IGNORE_DOT_FILES = 2;
    public const IGNORE_VCS_IGNORED_FILES = 4;

    private int $mode = 0;
    private array $names = [];
    private array $notNames = [];
    private array $exclude = [];
    private array $filters = [];
    private array $pruneFilters = [];
    private array $depths = [];
    private array $sizes = [];
    private bool $followLinks = false;
    private bool $reverseSorting = false;
    private \Closure|int|false $sort = false;
    private int $ignore = 0;
    /** @var list<string> */
    private array $dirs = [];
    private array $dates = [];
    /** @var list<iterable<SplFileInfo|\SplFileInfo|string>> */
    private array $iterators = [];
    private array $contains = [];
    private array $notContains = [];
    private array $paths = [];
    private array $notPaths = [];
    private bool $ignoreUnreadableDirs = false;

    private static array $vcsPatterns = ['.svn', '_svn', 'CVS', '_darcs', '.arch-params', '.monotone', '.bzr', '.git', '.hg'];

    public function __construct()
    {
        $this->ignore = static::IGNORE_VCS_FILES | static::IGNORE_DOT_FILES;
    }

    /**
     * Creates a new Finder.
     */
    public static function create(): static
    {
        return new static();
    }

    /**
     * Restricts the matching to directories only.
     *
     * @return $this
     */
    public function directories(): static
    {
        $this->mode = Iterator\FileTypeFilterIterator::ONLY_DIRECTORIES;

        return $this;
    }

    /**
     * Restricts the matching to files only.
     *
     * @return $this
     */
    public function files(): static
    {
        $this->mode = Iterator\FileTypeFilterIterator::ONLY_FILES;

        return $this;
    }

    /**
     * Adds tests for the directory depth.
     *
     * Usage:
     *
     *     $finder->depth('> 1') // the Finder will start matching at level 1.
     *     $finder->depth('< 3') // the Finder will descend at most 3 levels of directories below the starting point.
     *     $finder->depth(['>= 1', '< 3'])
     *
     * @param string|int|string[]|int[] $levels The depth level expression or an array of depth levels
     *
     * @return $this
     *
     * @see DepthRangeFilterIterator
     * @see NumberComparator
     */
    public function depth(string|int|array $levels): static
    {
        foreach ((array) $levels as $level) {
            $this->depths[] = new NumberComparator($level);
        }

        return $this;
    }

    /**
     * Adds tests for file dates (last modified).
     *
     * The date must be something that strtotime() is able to parse:
     *
     *     $finder->date('since yesterday');
     *     $finder->date('until 2 days ago');
     *     $finder->date('> now - 2 hours');
     *     $finder->date('>= 2005-10-15');
     *     $finder->date(['>= 2005-10-15', '<= 2006-05-27']);
     *
     * @param string|string[] $dates A date range string or an array of date ranges
     *
     * @return $this
     *
     * @see strtotime
     * @see DateRangeFilterIterator
     * @see DateComparator
     */
    public function date(string|array $dates): static
    {
        foreach ((array) $dates as $date) {
            $this->dates[] = new DateComparator($date);
        }

        return $this;
    }

    /**
     * Adds rules that files must match.
     *
     * You can use patterns (delimited with / sign), globs or simple strings.
     *
     *     $finder->name('/\.php$/')
     *     $finder->name('*.php') // same as above, without dot files
     *     $finder->name('test.php')
     *     $finder->name(['test.py', 'test.php'])
     *
     * @param string|string[] $patterns A pattern (a regexp, a glob, or a string) or an array of patterns
     *
     * @return $this
     *
     * @see FilenameFilterIterator
     */
    public function name(string|array $patterns): static
    {
        $this->names = array_merge($this->names, (array) $patterns);

        return $this;
    }

    /**
     * Adds rules that files must not match.
     *
     * @param string|string[] $patterns A pattern (a regexp, a glob, or a string) or an array of patterns
     *
     * @return $this
     *
     * @see FilenameFilterIterator
     */
    public function notName(string|array $patterns): static
    {
        $this->notNames = array_merge($this->notNames, (array) $patterns);

        return $this;
    }

    /**
     * Adds tests that file contents must match.
     *
     * Strings or PCRE patterns can be used:
     *
     *     $finder->contains('Lorem ipsum')
     *     $finder->contains('/Lorem ipsum/i')
     *     $finder->contains(['dolor', '/ipsum/i'])
     *
     * @param string|string[] $patterns A pattern (string or regexp) or an array of patterns
     *
     * @return $this
     *
     * @see FilecontentFilterIterator
     */
    public function contains(string|array $patterns): static
    {
        $this->contains = array_merge($this->contains, (array) $patterns);

        return $this;
    }

    /**
     * Adds tests that file contents must not match.
     *
     * Strings or PCRE patterns can be used:
     *
     *     $finder->notContains('Lorem ipsum')
     *     $finder->notContains('/Lorem ipsum/i')
     *     $finder->notContains(['lorem', '/dolor/i'])
     *
     * @param string|string[] $patterns A pattern (string or regexp) or an array of patterns
     *
     * @return $this
     *
     * @see FilecontentFilterIterator
     */
    public function notContains(string|array $patterns): static
    {
        $this->notContains = array_merge($this->notContains, (array) $patterns);

        return $this;
    }

    /**
     * Adds rules that filenames must match.
     *
     * You can use patterns (delimited with / sign) or simple strings.
     *
     *     $finder->path('some/special/dir')
     *     $finder->path('/some\/special\/dir/') // same as above
     *     $finder->path(['some dir', 'another/dir'])
     *
     * Use only / as dirname separator.
     *
     * @param string|string[] $patterns A pattern (a regexp or a string) or an array of patterns
     *
     * @return $this
     *
     * @see FilenameFilterIterator
     */
    public function path(string|array $patterns): static
    {
        $this->paths = array_merge($this->paths, (array) $patterns);

        return $this;
    }

    /**
     * Adds rules that filenames must not match.
     *
     * You can use patterns (delimited with / sign) or simple strings.
     *
     *     $finder->notPath('some/special/dir')
     *     $finder->notPath('/some\/special\/dir/') // same as above
     *     $finder->notPath(['some/file.txt', 'another/file.log'])
     *
     * Use only / as dirname separator.
     *
     * @param string|string[] $patterns A pattern (a regexp or a string) or an array of patterns
     *
     * @return $this
     *
     * @see FilenameFilterIterator
     */
    public function notPath(string|array $patterns): static
    {
        $this->notPaths = array_merge($this->notPaths, (array) $patterns);

        return $this;
    }

    /**
     * Adds tests for file sizes.
     *
     *     $finder->size('> 10K');
     *     $finder->size('<= 1Ki');
     *     $finder->size(4);
     *     $finder->size(['> 10K', '< 20K'])
     *
     * @param string|int|string[]|int[] $sizes A size range string or an integer or an array of size ranges
     *
     * @return $this
     *
     * @see SizeRangeFilterIterator
     * @see NumberComparator
     */
    public function size(string|int|array $sizes): static
    {
        foreach ((array) $sizes as $size) {
            $this->sizes[] = new NumberComparator($size);
        }

        return $this;
    }

    /**
     * Excludes directories.
     *
     * Directories passed as argument must be relative to the ones defined with the `in()` method. For example:
     *
     *     $finder->in(__DIR__)->exclude('ruby');
     *
     * @param string|array $dirs A directory path or an array of directories
     *
     * @return $this
     *
     * @see ExcludeDirectoryFilterIterator
     */
    public function exclude(string|array $dirs): static
    {
        $this->exclude = array_merge($this->exclude, (array) $dirs);

        return $this;
    }

    /**
     * Excludes "hidden" directories and files (starting with a dot).
     *
     * This option is enabled by default.
     *
     * @return $this
     *
     * @see ExcludeDirectoryFilterIterator
     */
    public function ignoreDotFiles(bool $ignoreDotFiles): static
    {
        if ($ignoreDotFiles) {
            $this->ignore |= static::IGNORE_DOT_FILES;
        } else {
            $this->ignore &= ~static::IGNORE_DOT_FILES;
        }

        return $this;
    }

    /**
     * Forces the finder to ignore version control directories.
     *
     * This option is enabled by default.
     *
     * @return $this
     *
     * @see ExcludeDirectoryFilterIterator
     */
    public function ignoreVCS(bool $ignoreVCS): static
    {
        if ($ignoreVCS) {
            $this->ignore |= static::IGNORE_VCS_FILES;
        } else {
            $this->ignore &= ~static::IGNORE_VCS_FILES;
        }

        return $this;
    }

    /**
     * Forces Finder to obey .gitignore and ignore files based on rules listed there.
     *
     * This option is disabled by default.
     *
     * @return $this
     */
    public function ignoreVCSIgnored(bool $ignoreVCSIgnored): static
    {
        if ($ignoreVCSIgnored) {
            $this->ignore |= static::IGNORE_VCS_IGNORED_FILES;
        } else {
            $this->ignore &= ~static::IGNORE_VCS_IGNORED_FILES;
        }

        return $this;
    }

    /**
     * Adds VCS patterns.
     *
     * @see ignoreVCS()
     *
     * @param string|string[] $pattern VCS patterns to ignore
     *
     * @return void
     */
    public static function addVCSPattern(string|array $pattern)
    {
        foreach ((array) $pattern as $p) {
            self::$vcsPatterns[] = $p;
        }

        self::$vcsPatterns = array_unique(self::$vcsPatterns);
    }

    /**
     * Sorts files and directories by an anonymous function.
     *
     * The anonymous function receives two \SplFileInfo instances to compare.
     *
     * This can be slow as all the matching files and directories must be retrieved for comparison.
     *
     * @return $this
     *
     * @see SortableIterator
     */
    public function sort(\Closure $closure): static
    {
        $this->sort = $closure;

        return $this;
    }

    /**
     * Sorts files and directories by extension.
     *
     * This can be slow as all the matching files and directories must be retrieved for comparison.
     *
     * @return $this
     *
     * @see SortableIterator
     */
    public function sortByExtension(): static
    {
        $this->sort = SortableIterator::SORT_BY_EXTENSION;

        return $this;
    }

    /**
     * Sorts files and directories by name.
     *
     * This can be slow as all the matching files and directories must be retrieved for comparison.
     *
     * @return $this
     *
     * @see SortableIterator
     */
    public function sortByName(bool $useNaturalSort = false): static
    {
        $this->sort = $useNaturalSort ? SortableIterator::SORT_BY_NAME_NATURAL : SortableIterator::SORT_BY_NAME;

        return $this;
    }

    /**
     * Sorts files and directories by name case insensitive.
     *
     * This can be slow as all the matching files and directories must be retrieved for comparison.
     *
     * @return $this
     *
     * @see SortableIterator
     */
    public function sortByCaseInsensitiveName(bool $useNaturalSort = false): static
    {
        $this->sort = $useNaturalSort ? SortableIterator::SORT_BY_NAME_NATURAL_CASE_INSENSITIVE : SortableIterator::SORT_BY_NAME_CASE_INSENSITIVE;

        return $this;
    }

    /**
     * Sorts files and directories by size.
     *
     * This can be slow as all the matching files and directories must be retrieved for comparison.
     *
     * @return $this
     *
     * @see SortableIterator
     */
    public function sortBySize(): static
    {
        $this->sort = SortableIterator::SORT_BY_SIZE;

        return $this;
    }

    /**
     * Sorts files and directories by type (directories before files), then by name.
     *
     * This can be slow as all the matching files and directories must be retrieved for comparison.
     *
     * @return $this
     *
     * @see SortableIterator
     */
    public function sortByType(): static
    {
        $this->sort = SortableIterator::SORT_BY_TYPE;

        return $this;
    }

    /**
     * Sorts files and directories by the last accessed time.
     *
     * This is the time that the file was last accessed, read or written to.
     *
     * This can be slow as all the matching files and directories must be retrieved for comparison.
     *
     * @return $this
     *
     * @see SortableIterator
     */
    public function sortByAccessedTime(): static
    {
        $this->sort = SortableIterator::SORT_BY_ACCESSED_TIME;

        return $this;
    }

    /**
     * Reverses the sorting.
     *
     * @return $this
     */
    public function reverseSorting(): static
    {
        $this->reverseSorting = true;

        return $this;
    }

    /**
     * Sorts files and directories by the last inode changed time.
     *
     * This is the time that the inode information was last modified (permissions, owner, group or other metadata).
     *
     * On Windows, since inode is not available, changed time is actually the file creation time.
     *
     * This can be slow as all the matching files and directories must be retrieved for comparison.
     *
     * @return $this
     *
     * @see SortableIterator
     */
    public function sortByChangedTime(): static
    {
        $this->sort = SortableIterator::SORT_BY_CHANGED_TIME;

        return $this;
    }

    /**
     * Sorts files and directories by the last modified time.
     *
     * This is the last time the actual contents of the file were last modified.
     *
     * This can be slow as all the matching files and directories must be retrieved for comparison.
     *
     * @return $this
     *
     * @see SortableIterator
     */
    public function sortByModifiedTime(): static
    {
        $this->sort = SortableIterator::SORT_BY_MODIFIED_TIME;

        return $this;
    }

    /**
     * Filters the iterator with an anonymous function.
     *
     * The anonymous function receives a \SplFileInfo and must return false
     * to remove files.
     *
     * @param \Closure(SplFileInfo): bool $closure
     * @param bool                        $prune   Whether to skip traversing directories further
     *
     * @return $this
     *
     * @see CustomFilterIterator
     */
    public function filter(\Closure $closure /* , bool $prune = false */): static
    {
        $prune = 1 < \func_num_args() ? func_get_arg(1) : false;
        $this->filters[] = $closure;

        if ($prune) {
            $this->pruneFilters[] = $closure;
        }

        return $this;
    }

    /**
     * Forces the following of symlinks.
     *
     * @return $this
     */
    public function followLinks(): static
    {
        $this->followLinks = true;

        return $this;
    }

    /**
     * Tells finder to ignore unreadable directories.
     *
     * By default, scanning unreadable directories content throws an AccessDeniedException.
     *
     * @return $this
     */
    public function ignoreUnreadableDirs(bool $ignore = true): static
    {
        $this->ignoreUnreadableDirs = $ignore;

        return $this;
    }

    /**
     * Searches files and directories which match defined rules.
     *
     * @param string|string[] $dirs A directory path or an array of directories
     *
     * @return $this
     *
     * @throws DirectoryNotFoundException if one of the directories does not exist
     */
    public function in(string|array $dirs): static
    {
        $resolvedDirs = [];

        foreach ((array) $dirs as $dir) {
            if (is_dir($dir)) {
                $resolvedDirs[] = [$this->normalizeDir($dir)];
            } elseif ($glob = glob($dir, (\defined('GLOB_BRACE') ? \GLOB_BRACE : 0) | \GLOB_ONLYDIR | \GLOB_NOSORT)) {
                sort($glob);
                $resolvedDirs[] = array_map($this->normalizeDir(...), $glob);
            } else {
                throw new DirectoryNotFoundException(\sprintf('The "%s" directory does not exist.', $dir));
            }
        }

        $this->dirs = array_merge($this->dirs, ...$resolvedDirs);

        return $this;
    }

    /**
     * Returns an Iterator for the current Finder configuration.
     *
     * This method implements the IteratorAggregate interface.
     *
     * @return \Iterator<string, SplFileInfo>
     *
     * @throws \LogicException if the in() method has not been called
     */
    public function getIterator(): \Iterator
    {
        if (!$this->dirs && !$this->iterators) {
            throw new \LogicException('You must call one of in() or append() methods before iterating over a Finder.');
        }

        if (1 === \count($this->dirs) && !$this->iterators) {
            $iterator = $this->searchInDirectory($this->dirs[0]);
        } else {
            $iterator = new \AppendIterator();
            foreach ($this->dirs as $dir) {
                $iterator->append(new \IteratorIterator(new LazyIterator(fn () => $this->searchInDirectory($dir))));
            }

            foreach ($this->iterators as $it) {
                $iterator->append(new \IteratorIterator(new LazyIterator(static function () use ($it) {
                    foreach ($it as $file) {
                        if (!$file instanceof \SplFileInfo) {
                            $file = new \SplFileInfo($file);
                        }
                        $key = $file->getPathname();
                        if (!$file instanceof SplFileInfo) {
                            $file = new SplFileInfo($key, $file->getPath(), $key);
                        }

                        yield $key => $file;
                    }
                })));
            }
        }

        if ($this->sort || $this->reverseSorting) {
            $iterator = (new SortableIterator($iterator, $this->sort, $this->reverseSorting))->getIterator();
        }

        return $iterator;
    }

    /**
     * Appends an existing set of files/directories to the finder.
     *
     * The set can be another Finder, an Iterator, an IteratorAggregate, or even a plain array.
     *
     * @param iterable<SplFileInfo|\SplFileInfo|string> $iterator
     *
     * @return $this
     */
    public function append(iterable $iterator): static
    {
        $this->iterators[] = $iterator;

        return $this;
    }

    /**
     * Check if any results were found.
     */
    public function hasResults(): bool
    {
        foreach ($this->getIterator() as $_) {
            return true;
        }

        return false;
    }

    /**
     * Counts all the results collected by the iterators.
     */
    public function count(): int
    {
        return iterator_count($this->getIterator());
    }

    private function searchInDirectory(string $dir): \Iterator
    {
        $exclude = $this->exclude;
        $notPaths = $this->notPaths;

        if ($this->pruneFilters) {
            $exclude = array_merge($exclude, $this->pruneFilters);
        }

        if (static::IGNORE_VCS_FILES === (static::IGNORE_VCS_FILES & $this->ignore)) {
            $exclude = array_merge($exclude, self::$vcsPatterns);
        }

        if (static::IGNORE_DOT_FILES === (static::IGNORE_DOT_FILES & $this->ignore)) {
            $notPaths[] = '#(^|/)\..+(/|$)#';
        }

        $minDepth = 0;
        $maxDepth = \PHP_INT_MAX;

        foreach ($this->depths as $comparator) {
            switch ($comparator->getOperator()) {
                case '>':
                    $minDepth = $comparator->getTarget() + 1;
                    break;
                case '>=':
                    $minDepth = $comparator->getTarget();
                    break;
                case '<':
                    $maxDepth = $comparator->getTarget() - 1;
                    break;
                case '<=':
                    $maxDepth = $comparator->getTarget();
                    break;
                default:
                    $minDepth = $maxDepth = $comparator->getTarget();
            }
        }

        $flags = \RecursiveDirectoryIterator::SKIP_DOTS;

        if ($this->followLinks) {
            $flags |= \RecursiveDirectoryIterator::FOLLOW_SYMLINKS;
        }

        $iterator = new Iterator\RecursiveDirectoryIterator($dir, $flags, $this->ignoreUnreadableDirs);

        if ($exclude) {
            $iterator = new ExcludeDirectoryFilterIterator($iterator, $exclude);
        }

        $iterator = new \RecursiveIteratorIterator($iterator, \RecursiveIteratorIterator::SELF_FIRST);

        if ($minDepth > 0 || $maxDepth < \PHP_INT_MAX) {
            $iterator = new DepthRangeFilterIterator($iterator, $minDepth, $maxDepth);
        }

        if ($this->mode) {
            $iterator = new Iterator\FileTypeFilterIterator($iterator, $this->mode);
        }

        if ($this->names || $this->notNames) {
            $iterator = new FilenameFilterIterator($iterator, $this->names, $this->notNames);
        }

        if ($this->contains || $this->notContains) {
            $iterator = new FilecontentFilterIterator($iterator, $this->contains, $this->notContains);
        }

        if ($this->sizes) {
            $iterator = new SizeRangeFilterIterator($iterator, $this->sizes);
        }

        if ($this->dates) {
            $iterator = new DateRangeFilterIterator($iterator, $this->dates);
        }

        if ($this->filters) {
            $iterator = new CustomFilterIterator($iterator, $this->filters);
        }

        if ($this->paths || $notPaths) {
            $iterator = new Iterator\PathFilterIterator($iterator, $this->paths, $notPaths);
        }

        if (static::IGNORE_VCS_IGNORED_FILES === (static::IGNORE_VCS_IGNORED_FILES & $this->ignore)) {
            $iterator = new Iterator\VcsIgnoredFilterIterator($iterator, $dir);
        }

        return $iterator;
    }

    /**
     * Normalizes given directory names by removing trailing slashes.
     *
     * Excluding: stream wrapper schemes such as ftp:// or s3://
     */
    private function normalizeDir(string $dir): string
    {
        if ('/' === $dir) {
            return $dir;
        }

        $dir = rtrim($dir, '/'.\DIRECTORY_SEPARATOR);

        if (preg_match('#^[a-zA-Z][a-zA-Z0-9.+-]*://#', $dir)) {
            $dir .= '/';
        }

        return $dir;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Finder;

/**
 * Glob matches globbing patterns against text.
 *
 *     if match_glob("foo.*", "foo.bar") echo "matched\n";
 *
 *     // prints foo.bar and foo.baz
 *     $regex = glob_to_regex("foo.*");
 *     for (['foo.bar', 'foo.baz', 'foo', 'bar'] as $t)
 *     {
 *         if (/$regex/) echo "matched: $car\n";
 *     }
 *
 * Glob implements glob(3) style matching that can be used to match
 * against text, rather than fetching names from a filesystem.
 *
 * Based on the Perl Text::Glob module.
 *
 * @author Fabien Potencier <fabien@symfony.com> PHP port
 * @author     Richard Clamp <richardc@unixbeard.net> Perl version
 * @copyright  2004-2005 Fabien Potencier <fabien@symfony.com>
 * @copyright  2002 Richard Clamp <richardc@unixbeard.net>
 */
class Glob
{
    /**
     * Returns a regexp which is the equivalent of the glob pattern.
     */
    public static function toRegex(string $glob, bool $strictLeadingDot = true, bool $strictWildcardSlash = true, string $delimiter = '#'): string
    {
        $firstByte = true;
        $escaping = false;
        $inCurlies = 0;
        $regex = '';
        if ($unanchored = str_starts_with($glob, '**/')) {
            $glob = '/'.$glob;
        }
        $sizeGlob = \strlen($glob);
        for ($i = 0; $i < $sizeGlob; ++$i) {
            $car = $glob[$i];
            if ($firstByte && $strictLeadingDot && '.' !== $car) {
                $regex .= '(?=[^\.])';
            }

            $firstByte = '/' === $car;

            if ($firstByte && $strictWildcardSlash && isset($glob[$i + 2]) && '**' === $glob[$i + 1].$glob[$i + 2] && (!isset($glob[$i + 3]) || '/' === $glob[$i + 3])) {
                $car = '[^/]++/';
                if (!isset($glob[$i + 3])) {
                    $car .= '?';
                }

                if ($strictLeadingDot) {
                    $car = '(?=[^\.])'.$car;
                }

                $car = '/(?:'.$car.')*';
                $i += 2 + isset($glob[$i + 3]);

                if ('/' === $delimiter) {
                    $car = str_replace('/', '\\/', $car);
                }
            }

            if ($delimiter === $car || '.' === $car || '(' === $car || ')' === $car || '|' === $car || '+' === $car || '^' === $car || '$' === $car) {
                $regex .= "\\$car";
            } elseif ('*' === $car) {
                $regex .= $escaping ? '\\*' : ($strictWildcardSlash ? '[^/]*' : '.*');
            } elseif ('?' === $car) {
                $regex .= $escaping ? '\\?' : ($strictWildcardSlash ? '[^/]' : '.');
            } elseif ('{' === $car) {
                $regex .= $escaping ? '\\{' : '(';
                if (!$escaping) {
                    ++$inCurlies;
                }
            } elseif ('}' === $car && $inCurlies) {
                $regex .= $escaping ? '}' : ')';
                if (!$escaping) {
                    --$inCurlies;
                }
            } elseif (',' === $car && $inCurlies) {
                $regex .= $escaping ? ',' : '|';
            } elseif ('\\' === $car) {
                if ($escaping) {
                    $regex .= '\\\\';
                    $escaping = false;
                } else {
                    $escaping = true;
                }

                continue;
            } else {
                $regex .= $car;
            }
            $escaping = false;
        }

        if ($unanchored) {
            $regex = substr_replace($regex, '?', 1 + ('/' === $delimiter) + ($strictLeadingDot ? \strlen('(?=[^\.])') : 0), 0);
        }

        return $delimiter.'^'.$regex.'$'.$delimiter;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

use Symfony\Polyfill\Intl\Grapheme as p;

if (!function_exists('grapheme_str_split')) {
    function grapheme_str_split(string $string, int $length = 1): array|false { return p\Grapheme::grapheme_str_split($string, $length); }
}
if (!function_exists('grapheme_levenshtein')) {
    function grapheme_levenshtein(string $string1, string $string2, int $insertion_cost = 1, int $replacement_cost = 1, int $deletion_cost = 1, string $locale = ''): int|false { return p\Grapheme::grapheme_levenshtein($string1, $string2, $insertion_cost, $replacement_cost, $deletion_cost); }
}
if (!function_exists('grapheme_strrev')) {
    function grapheme_strrev(string $string): string|false { return p\Grapheme::grapheme_strrev($string); }
}

if (extension_loaded('intl')) {
    return;
}

if (!defined('GRAPHEME_EXTR_COUNT')) {
    define('GRAPHEME_EXTR_COUNT', 0);
}
if (!defined('GRAPHEME_EXTR_MAXBYTES')) {
    define('GRAPHEME_EXTR_MAXBYTES', 1);
}
if (!defined('GRAPHEME_EXTR_MAXCHARS')) {
    define('GRAPHEME_EXTR_MAXCHARS', 2);
}

if (!function_exists('grapheme_extract')) {
    function grapheme_extract(?string $haystack, ?int $size, ?int $type = GRAPHEME_EXTR_COUNT, ?int $offset = 0, &$next = null): string|false { return p\Grapheme::grapheme_extract((string) $haystack, (int) $size, (int) $type, (int) $offset, $next); }
}
if (\PHP_VERSION_ID >= 80500) {
    return require __DIR__.'/bootstrap85.php';
}

if (!function_exists('grapheme_stripos')) {
    function grapheme_stripos(?string $haystack, ?string $needle, ?int $offset = 0): int|false { return p\Grapheme::grapheme_stripos((string) $haystack, (string) $needle, (int) $offset); }
}
if (!function_exists('grapheme_stristr')) {
    function grapheme_stristr(?string $haystack, ?string $needle, ?bool $beforeNeedle = false): string|false { return p\Grapheme::grapheme_stristr((string) $haystack, (string) $needle, (bool) $beforeNeedle); }
}
if (!function_exists('grapheme_strlen')) {
    function grapheme_strlen(?string $string): int|false|null { return p\Grapheme::grapheme_strlen((string) $string); }
}
if (!function_exists('grapheme_strpos')) {
    function grapheme_strpos(?string $haystack, ?string $needle, ?int $offset = 0): int|false { return p\Grapheme::grapheme_strpos((string) $haystack, (string) $needle, (int) $offset); }
}
if (!function_exists('grapheme_strripos')) {
    function grapheme_strripos(?string $haystack, ?string $needle, ?int $offset = 0): int|false { return p\Grapheme::grapheme_strripos((string) $haystack, (string) $needle, (int) $offset); }
}
if (!function_exists('grapheme_strrpos')) {
    function grapheme_strrpos(?string $haystack, ?string $needle, ?int $offset = 0): int|false { return p\Grapheme::grapheme_strrpos((string) $haystack, (string) $needle, (int) $offset); }
}
if (!function_exists('grapheme_strstr')) {
    function grapheme_strstr(?string $haystack, ?string $needle, ?bool $beforeNeedle = false): string|false { return p\Grapheme::grapheme_strstr((string) $haystack, (string) $needle, (bool) $beforeNeedle); }
}
if (!function_exists('grapheme_substr')) {
    function grapheme_substr(?string $string, ?int $offset, ?int $length = null): string|false { return p\Grapheme::grapheme_substr((string) $string, (int) $offset, $length); }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

use Symfony\Polyfill\Intl\Grapheme as p;

if (\PHP_VERSION_ID >= 80000) {
    return require __DIR__.'/bootstrap80.php';
}

if (!class_exists('ValueError', false)) {
    class ValueError extends Error
    {
    }
}

if (!defined('GRAPHEME_EXTR_COUNT')) {
    define('GRAPHEME_EXTR_COUNT', 0);
}
if (!defined('GRAPHEME_EXTR_MAXBYTES')) {
    define('GRAPHEME_EXTR_MAXBYTES', 1);
}
if (!defined('GRAPHEME_EXTR_MAXCHARS')) {
    define('GRAPHEME_EXTR_MAXCHARS', 2);
}

if (!function_exists('grapheme_extract')) {
    function grapheme_extract($haystack, $size, $type = 0, $start = 0, &$next = 0) { return p\Grapheme::grapheme_extract($haystack, $size, $type, $start, $next); }
}
if (!function_exists('grapheme_stripos')) {
    function grapheme_stripos($haystack, $needle, $offset = 0) { return p\Grapheme::grapheme_stripos($haystack, $needle, $offset); }
}
if (!function_exists('grapheme_stristr')) {
    function grapheme_stristr($haystack, $needle, $beforeNeedle = false) { return p\Grapheme::grapheme_stristr($haystack, $needle, $beforeNeedle); }
}
if (!function_exists('grapheme_strlen')) {
    function grapheme_strlen($input) { return p\Grapheme::grapheme_strlen($input); }
}
if (!function_exists('grapheme_strpos')) {
    function grapheme_strpos($haystack, $needle, $offset = 0) { return p\Grapheme::grapheme_strpos($haystack, $needle, $offset); }
}
if (!function_exists('grapheme_strripos')) {
    function grapheme_strripos($haystack, $needle, $offset = 0) { return p\Grapheme::grapheme_strripos($haystack, $needle, $offset); }
}
if (!function_exists('grapheme_strrpos')) {
    function grapheme_strrpos($haystack, $needle, $offset = 0) { return p\Grapheme::grapheme_strrpos($haystack, $needle, $offset); }
}
if (!function_exists('grapheme_strstr')) {
    function grapheme_strstr($haystack, $needle, $beforeNeedle = false) { return p\Grapheme::grapheme_strstr($haystack, $needle, $beforeNeedle); }
}
if (!function_exists('grapheme_substr')) {
    function grapheme_substr($string, $offset, $length = null) { return p\Grapheme::grapheme_substr($string, $offset, $length); }
}
if (!function_exists('grapheme_str_split')) {
    function grapheme_str_split(string $string, int $length = 1) { return p\Grapheme::grapheme_str_split($string, $length); }
}
if (!function_exists('grapheme_levenshtein')) {
    function grapheme_levenshtein(string $string1, string $string2, int $insertion_cost = 1, int $replacement_cost = 1, int $deletion_cost = 1, string $locale = '') { return p\Grapheme::grapheme_levenshtein($string1, $string2, $insertion_cost, $replacement_cost, $deletion_cost); }
}
if (!function_exists('grapheme_strrev')) {
    function grapheme_strrev(string $string) { return p\Grapheme::grapheme_strrev($string); }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Polyfill\Intl\Grapheme;

\define('SYMFONY_GRAPHEME_CLUSTER_RX', ((float) \PCRE_VERSION >= 10.44) ? '\X' : Grapheme::GRAPHEME_CLUSTER_RX);

/**
 * Partial intl implementation in pure PHP.
 *
 * Implemented:
 * - grapheme_extract  - Extract a sequence of grapheme clusters from a text buffer, which must be encoded in UTF-8
 * - grapheme_stripos  - Find position (in grapheme units) of first occurrence of a case-insensitive string
 * - grapheme_stristr  - Returns part of haystack string from the first occurrence of case-insensitive needle to the end of haystack
 * - grapheme_strlen   - Get string length in grapheme units
 * - grapheme_strpos   - Find position (in grapheme units) of first occurrence of a string
 * - grapheme_strripos - Find position (in grapheme units) of last occurrence of a case-insensitive string
 * - grapheme_strrpos  - Find position (in grapheme units) of last occurrence of a string
 * - grapheme_strstr   - Returns part of haystack string from the first occurrence of needle to the end of haystack
 * - grapheme_substr   - Return part of a string
 * - grapheme_str_split - Splits a string into an array of individual or chunks of graphemes
 * - grapheme_levenshtein - Calculate the grapheme-unit Levenshtein distance between two strings
 * - grapheme_strrev - Reverse a string by grapheme clusters
 *
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @internal
 */
final class Grapheme
{
    // (CRLF|RI RI?|(([ZWNJ-ZWJ]|T+|L*(LV?V+|LV|LVT)T*|L+|[^Control])[Extend]*)(ZWJ([ZWNJ-ZWJ]|T+|L*(LV?V+|LV|LVT)T*|L+|[^Control])[Extend]*)*|[Control])
    // This regular expression is a work around for http://bugs.exim.org/1279
    public const GRAPHEME_CLUSTER_RX = '(?:\r\n|[\x{1F1E6}-\x{1F1FF}][\x{1F1E6}-\x{1F1FF}]?|(?:[ -~\x{200C}\x{200D}]|[ᆨ-ᇹ]+|[ᄀ-ᅟ]*(?:[가개갸걔거게겨계고과괘괴교구궈궤귀규그긔기까깨꺄꺠꺼께껴꼐꼬꽈꽤꾀꾜꾸꿔꿰뀌뀨끄끠끼나내냐냬너네녀녜노놔놰뇌뇨누눠눼뉘뉴느늬니다대댜댸더데뎌뎨도돠돼되됴두둬뒈뒤듀드듸디따때땨떄떠떼뗘뗴또똬뙈뙤뚀뚜뚸뛔뛰뜌뜨띄띠라래랴럐러레려례로롸뢔뢰료루뤄뤠뤼류르릐리마매먀먜머메며몌모뫄뫠뫼묘무뭐뭬뮈뮤므믜미바배뱌뱨버베벼볘보봐봬뵈뵤부붜붸뷔뷰브븨비빠빼뺘뺴뻐뻬뼈뼤뽀뽜뽸뾔뾰뿌뿨쀄쀠쀼쁘쁴삐사새샤섀서세셔셰소솨쇄쇠쇼수숴쉐쉬슈스싀시싸쌔쌰썌써쎄쎠쎼쏘쏴쐐쐬쑈쑤쒀쒜쒸쓔쓰씌씨아애야얘어에여예오와왜외요우워웨위유으의이자재쟈쟤저제져졔조좌좨죄죠주줘줴쥐쥬즈즤지짜째쨔쨰쩌쩨쪄쪠쪼쫘쫴쬐쬬쭈쭤쮀쮜쮸쯔쯰찌차채챠챼처체쳐쳬초촤쵀최쵸추춰췌취츄츠츼치카캐캬컈커케켜켸코콰쾌쾨쿄쿠쿼퀘퀴큐크킈키타태탸턔터테텨톄토톼퇘퇴툐투퉈퉤튀튜트틔티파패퍄퍠퍼페펴폐포퐈퐤푀표푸풔풰퓌퓨프픠피하해햐햬허헤혀혜호화홰회효후훠훼휘휴흐희히]?[ᅠ-ᆢ]+|[가-힣])[ᆨ-ᇹ]*|[ᄀ-ᅟ]+|[^\p{Cc}\p{Cf}\p{Zl}\p{Zp}])[\p{Mn}\p{Mc}\p{Me}\x{09BE}\x{09D7}\x{0B3E}\x{0B57}\x{0BBE}\x{0BD7}\x{0CC2}\x{0CD5}\x{0CD6}\x{0D3E}\x{0D57}\x{0DCF}\x{0DDF}\x{200C}\x{1D165}\x{1D16E}-\x{1D172}\x{1F3FB}-\x{1F3FF}\x{FE0E}-\x{FE0F}\x{E0020}-\x{E007F}]*(?:\x{200D}(?:[\x{1F1E6}-\x{1F1FF}]|[ -~\x{200C}\x{200D}]|[ᆨ-ᇹ]+|[ᄀ-ᅟ]*(?:[가-힣]?[ᅠ-ᆢ]+|[가-힣])[ᆨ-ᇹ]*|[ᄀ-ᅟ]+|[^\p{Cc}\p{Cf}\p{Zl}\p{Zp}])[\p{Mn}\p{Mc}\p{Me}\x{09BE}\x{09D7}\x{0B3E}\x{0B57}\x{0BBE}\x{0BD7}\x{0CC2}\x{0CD5}\x{0CD6}\x{0D3E}\x{0D57}\x{0DCF}\x{0DDF}\x{200C}\x{1D165}\x{1D16E}-\x{1D172}\x{1F3FB}-\x{1F3FF}\x{FE0E}-\x{FE0F}\x{E0020}-\x{E007F}]*)*|[\p{Cc}\p{Cf}\p{Zl}\p{Zp}])';

    private const CASE_FOLD = [
        ['µ', 'ſ', "\xCD\x85", 'ς', "\xCF\x90", "\xCF\x91", "\xCF\x95", "\xCF\x96", "\xCF\xB0", "\xCF\xB1", "\xCF\xB5", "\xE1\xBA\x9B", "\xE1\xBE\xBE"],
        ['μ', 's', 'ι',        'σ', 'β',        'θ',        'φ',        'π',        'κ',        'ρ',        'ε',        "\xE1\xB9\xA1", 'ι'],
    ];

    // indexed by the $mode argument of grapheme_position()
    private const POSITION_FUNCTIONS = ['grapheme_strpos', 'grapheme_stripos', 'grapheme_strrpos', 'grapheme_strripos'];

    public static function grapheme_extract($s, $size, $type = \GRAPHEME_EXTR_COUNT, $start = 0, &$next = 0)
    {
        if (0 > $start) {
            $start = \strlen($s) + $start;
        }

        if (!\is_scalar($s)) {
            $hasError = false;
            set_error_handler(static function () use (&$hasError) { $hasError = true; });
            $next = substr($s, $start);
            restore_error_handler();
            if ($hasError) {
                substr($s, $start);
                $s = '';
            } else {
                $s = $next;
            }
        } else {
            $s = substr($s, $start);
        }
        $size = (int) $size;
        $type = (int) $type;
        $start = (int) $start;

        if (\GRAPHEME_EXTR_COUNT !== $type && \GRAPHEME_EXTR_MAXBYTES !== $type && \GRAPHEME_EXTR_MAXCHARS !== $type) {
            if (80000 > \PHP_VERSION_ID) {
                return false;
            }

            throw new \ValueError('grapheme_extract(): Argument #3 ($type) must be one of GRAPHEME_EXTR_COUNT, GRAPHEME_EXTR_MAXBYTES, or GRAPHEME_EXTR_MAXCHARS');
        }

        if (!isset($s[0]) || 0 > $size || 0 > $start) {
            return false;
        }
        if (0 === $size) {
            return '';
        }

        $next = $start;

        $s = preg_split('/('.SYMFONY_GRAPHEME_CLUSTER_RX.')/u', "\r\n".$s, $size + 1, \PREG_SPLIT_NO_EMPTY | \PREG_SPLIT_DELIM_CAPTURE);

        if (!isset($s[1])) {
            return false;
        }

        $i = 1;
        $ret = '';

        do {
            if (\GRAPHEME_EXTR_COUNT === $type) {
                --$size;
            } elseif (\GRAPHEME_EXTR_MAXBYTES === $type) {
                $size -= \strlen($s[$i]);
            } else {
                $size -= iconv_strlen($s[$i], 'UTF-8//IGNORE');
            }

            if ($size >= 0) {
                $ret .= $s[$i];
            }
        } while (isset($s[++$i]) && $size > 0);

        $next += \strlen($ret);

        return $ret;
    }

    public static function grapheme_strlen($s)
    {
        preg_replace('/'.SYMFONY_GRAPHEME_CLUSTER_RX.'/u', '', $s, -1, $len);

        return 0 === $len && '' !== $s ? null : $len;
    }

    public static function grapheme_substr($s, $start, $len = null)
    {
        if (null === $len) {
            $len = 2147483647;
        }

        preg_match_all('/'.SYMFONY_GRAPHEME_CLUSTER_RX.'/u', $s, $s);

        $slen = \count($s[0]);
        $start = (int) $start;

        if (0 > $start) {
            $start += $slen;
        }
        if (0 > $start) {
            if (\PHP_VERSION_ID < 80000) {
                return false;
            }

            $start = 0;
        }
        if ($start >= $slen) {
            return \PHP_VERSION_ID >= 80000 ? '' : false;
        }

        $rem = $slen - $start;

        if (0 > $len) {
            $len += $rem;
        }
        if (0 === $len) {
            return '';
        }
        if (0 > $len) {
            return \PHP_VERSION_ID >= 80000 ? '' : false;
        }
        if ($len > $rem) {
            $len = $rem;
        }

        return implode('', \array_slice($s[0], $start, $len));
    }

    public static function grapheme_strpos($s, $needle, $offset = 0)
    {
        return self::grapheme_position($s, $needle, $offset, 0);
    }

    public static function grapheme_stripos($s, $needle, $offset = 0)
    {
        return self::grapheme_position($s, $needle, $offset, 1);
    }

    public static function grapheme_strrpos($s, $needle, $offset = 0)
    {
        return self::grapheme_position($s, $needle, $offset, 2);
    }

    public static function grapheme_strripos($s, $needle, $offset = 0)
    {
        return self::grapheme_position($s, $needle, $offset, 3);
    }

    public static function grapheme_stristr($s, $needle, $beforeNeedle = false)
    {
        return mb_stristr($s, $needle, $beforeNeedle, 'UTF-8');
    }

    public static function grapheme_strstr($s, $needle, $beforeNeedle = false)
    {
        return mb_strstr($s, $needle, $beforeNeedle, 'UTF-8');
    }

    public static function grapheme_str_split($s, $len = 1)
    {
        if (0 > $len || 1073741823 < $len) {
            throw new \ValueError('grapheme_str_split(): Argument #2 ($length) must be greater than 0 and less than or equal to 1073741823.');
        }

        if ('' === $s) {
            return [];
        }

        if (!preg_match_all('/('.SYMFONY_GRAPHEME_CLUSTER_RX.')/u', $s, $matches)) {
            return false;
        }

        if (1 === $len) {
            return $matches[0];
        }

        $chunks = array_chunk($matches[0], $len);

        foreach ($chunks as &$chunk) {
            $chunk = implode('', $chunk);
        }

        return $chunks;
    }

    public static function grapheme_levenshtein($s1, $s2, $insertion_cost = 1, $replacement_cost = 1, $deletion_cost = 1)
    {
        if (!preg_match('//u', $s1) || !preg_match('//u', $s2)) {
            return false;
        }

        if (0 > $insertion_cost || 0 > $replacement_cost || 0 > $deletion_cost) {
            throw new \ValueError('grapheme_levenshtein(): Argument #3 ($insertion_cost), #4 ($replacement_cost), and #5 ($deletion_cost) must be greater than or equal to 0');
        }

        preg_match_all('/'.SYMFONY_GRAPHEME_CLUSTER_RX.'/u', $s1, $s1);
        preg_match_all('/'.SYMFONY_GRAPHEME_CLUSTER_RX.'/u', $s2, $s2);

        $s1 = $s1[0];
        $s2 = $s2[0];
        $l1 = \count($s1);
        $l2 = \count($s2);

        if (0 === $l1) {
            return $l2 * $insertion_cost;
        }
        if (0 === $l2) {
            return $l1 * $deletion_cost;
        }

        $dp = array_fill(0, $l1 + 1, array_fill(0, $l2 + 1, 0));

        for ($i = 1; $i <= $l1; ++$i) {
            $dp[$i][0] = $dp[$i - 1][0] + $deletion_cost;
        }
        for ($j = 1; $j <= $l2; ++$j) {
            $dp[0][$j] = $dp[0][$j - 1] + $insertion_cost;
        }

        for ($i = 1; $i <= $l1; ++$i) {
            for ($j = 1; $j <= $l2; ++$j) {
                $cost = ($s1[$i - 1] === $s2[$j - 1]) ? 0 : $replacement_cost;
                $dp[$i][$j] = min($dp[$i - 1][$j] + $deletion_cost, $dp[$i][$j - 1] + $insertion_cost, $dp[$i - 1][$j - 1] + $cost);
            }
        }

        return $dp[$l1][$l2];
    }

    private static function grapheme_position($s, $needle, $offset, $mode)
    {
        $needle = (string) $needle;
        if (80000 > \PHP_VERSION_ID && !preg_match('/./us', $needle)) {
            return false;
        }
        $s = (string) $s;
        // let the empty string through: it accepts no offset but 0, which is checked below
        if ('' !== $s && !preg_match('/./us', $s)) {
            return false;
        }
        if ($offset && ($offset > ($len = self::grapheme_strlen($s)) || $offset < -$len)) {
            if (80000 > \PHP_VERSION_ID) {
                return false;
            }

            throw new \ValueError(self::POSITION_FUNCTIONS[$mode].'(): Argument #3 ($offset) must be contained in argument #1 ($haystack)');
        }
        if ('' === $s) {
            return false;
        }
        if ($offset > 0) {
            $s = self::grapheme_substr($s, $offset);
        } elseif ($offset < 0) {
            if (2 > $mode) {
                $offset += $len;
                $s = self::grapheme_substr($s, $offset);
                if (0 > $offset) {
                    $offset = 0;
                }
            } elseif (0 > $offset += self::grapheme_strlen($needle)) {
                $s = self::grapheme_substr($s, 0, $offset);
                $offset = 0;
            } else {
                $offset = 0;
            }
        }

        // As UTF-8 is self-synchronizing, and we have ensured the strings are valid UTF-8,
        // we can use normal binary string functions here. For case-insensitive searches,
        // case fold the strings first.
        $caseInsensitive = $mode & 1;
        $reverse = $mode & 2;
        if ($caseInsensitive) {
            // Use the same case folding mode as mbstring does for mb_stripos().
            // Stick to SIMPLE case folding to avoid changing the length of the string, which
            // might result in offsets being shifted.
            $mode = \defined('MB_CASE_FOLD_SIMPLE') ? \MB_CASE_FOLD_SIMPLE : \MB_CASE_LOWER;
            $s = mb_convert_case($s, $mode, 'UTF-8');
            $needle = mb_convert_case($needle, $mode, 'UTF-8');

            if (!\defined('MB_CASE_FOLD_SIMPLE')) {
                $s = str_replace(self::CASE_FOLD[0], self::CASE_FOLD[1], $s);
                $needle = str_replace(self::CASE_FOLD[0], self::CASE_FOLD[1], $needle);
            }
        }
        if ($reverse) {
            $needlePos = strrpos($s, $needle);
        } else {
            $needlePos = strpos($s, $needle);
        }

        return false !== $needlePos ? self::grapheme_strlen(substr($s, 0, $needlePos)) + $offset : false;
    }

    public static function grapheme_strrev(string $string)
    {
        if (!preg_match('//u', $string)) {
            return false;
        }

        $units = grapheme_str_split($string);

        if (false === $units) {
            return false;
        }

        return implode('', array_reverse($units));
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

// The $locale argument added to grapheme_*() in PHP 8.5 / ICU 74 is accepted
// for native-signature compatibility but ignored by the polyfill, which only
// performs UTF-8 case folding.

use Symfony\Polyfill\Intl\Grapheme as p;

if (!function_exists('grapheme_stripos')) {
    function grapheme_stripos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $locale = ''): int|false { return p\Grapheme::grapheme_stripos((string) $haystack, (string) $needle, (int) $offset); }
}
if (!function_exists('grapheme_stristr')) {
    function grapheme_stristr(?string $haystack, ?string $needle, ?bool $beforeNeedle = false, ?string $locale = ''): string|false { return p\Grapheme::grapheme_stristr((string) $haystack, (string) $needle, (bool) $beforeNeedle); }
}
if (!function_exists('grapheme_strlen')) {
    function grapheme_strlen(?string $string): int|false|null { return p\Grapheme::grapheme_strlen((string) $string); }
}
if (!function_exists('grapheme_strpos')) {
    function grapheme_strpos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $locale = ''): int|false { return p\Grapheme::grapheme_strpos((string) $haystack, (string) $needle, (int) $offset); }
}
if (!function_exists('grapheme_strripos')) {
    function grapheme_strripos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $locale = ''): int|false { return p\Grapheme::grapheme_strripos((string) $haystack, (string) $needle, (int) $offset); }
}
if (!function_exists('grapheme_strrpos')) {
    function grapheme_strrpos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $locale = ''): int|false { return p\Grapheme::grapheme_strrpos((string) $haystack, (string) $needle, (int) $offset); }
}
if (!function_exists('grapheme_strstr')) {
    function grapheme_strstr(?string $haystack, ?string $needle, ?bool $beforeNeedle = false, ?string $locale = ''): string|false { return p\Grapheme::grapheme_strstr((string) $haystack, (string) $needle, (bool) $beforeNeedle); }
}
if (!function_exists('grapheme_substr')) {
    function grapheme_substr(?string $string, ?int $offset, ?int $length = null, ?string $locale = ''): string|false { return p\Grapheme::grapheme_substr((string) $string, (int) $offset, $length); }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Event;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Contracts\EventDispatcher\Event;

/**
 * Allows to inspect input and output of a command.
 *
 * @author Francesco Levorato <git@flevour.net>
 */
class ConsoleEvent extends Event
{
    protected $command;

    private InputInterface $input;
    private OutputInterface $output;

    public function __construct(?Command $command, InputInterface $input, OutputInterface $output)
    {
        $this->command = $command;
        $this->input = $input;
        $this->output = $output;
    }

    /**
     * Gets the command that is executed.
     */
    public function getCommand(): ?Command
    {
        return $this->command;
    }

    /**
     * Gets the input instance.
     */
    public function getInput(): InputInterface
    {
        return $this->input;
    }

    /**
     * Gets the output instance.
     */
    public function getOutput(): OutputInterface
    {
        return $this->output;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Event;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

/**
 * Allows to handle throwables thrown while running a command.
 *
 * @author Wouter de Jong <wouter@wouterj.nl>
 */
final class ConsoleErrorEvent extends ConsoleEvent
{
    private \Throwable $error;
    private int $exitCode;

    public function __construct(InputInterface $input, OutputInterface $output, \Throwable $error, ?Command $command = null)
    {
        parent::__construct($command, $input, $output);

        $this->error = $error;
    }

    public function getError(): \Throwable
    {
        return $this->error;
    }

    public function setError(\Throwable $error): void
    {
        $this->error = $error;
    }

    public function setExitCode(int $exitCode): void
    {
        $this->exitCode = $exitCode;

        $r = new \ReflectionProperty($this->error, 'code');
        $r->setValue($this->error, $this->exitCode);
    }

    public function getExitCode(): int
    {
        return $this->exitCode ?? (\is_int($this->error->getCode()) && 0 !== $this->error->getCode() ? $this->error->getCode() : 1);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Event;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

/**
 * Allows to manipulate the exit code of a command after its execution.
 *
 * @author Francesco Levorato <git@flevour.net>
 * @author Jules Pietri <jules@heahprod.com>
 */
final class ConsoleTerminateEvent extends ConsoleEvent
{
    public function __construct(
        Command $command,
        InputInterface $input,
        OutputInterface $output,
        private int $exitCode,
        private readonly ?int $interruptingSignal = null,
    ) {
        parent::__construct($command, $input, $output);
    }

    public function setExitCode(int $exitCode): void
    {
        $this->exitCode = $exitCode;
    }

    public function getExitCode(): int
    {
        return $this->exitCode;
    }

    public function getInterruptingSignal(): ?int
    {
        return $this->interruptingSignal;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Event;

/**
 * Allows to do things before the command is executed, like skipping the command or executing code before the command is
 * going to be executed.
 *
 * Changing the input arguments will have no effect.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
final class ConsoleCommandEvent extends ConsoleEvent
{
    /**
     * The return code for skipped commands, this will also be passed into the terminate event.
     */
    public const RETURN_CODE_DISABLED = 113;

    /**
     * Indicates if the command should be run or skipped.
     */
    private bool $commandShouldRun = true;

    /**
     * Disables the command, so it won't be run.
     */
    public function disableCommand(): bool
    {
        return $this->commandShouldRun = false;
    }

    public function enableCommand(): bool
    {
        return $this->commandShouldRun = true;
    }

    /**
     * Returns true if the command is runnable, false otherwise.
     */
    public function commandShouldRun(): bool
    {
        return $this->commandShouldRun;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Event;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

/**
 * @author marie <marie@users.noreply.github.com>
 */
final class ConsoleSignalEvent extends ConsoleEvent
{
    private int $handlingSignal;
    private int|false $exitCode;

    public function __construct(Command $command, InputInterface $input, OutputInterface $output, int $handlingSignal, int|false $exitCode = 0)
    {
        parent::__construct($command, $input, $output);
        $this->handlingSignal = $handlingSignal;
        $this->exitCode = $exitCode;
    }

    public function getHandlingSignal(): int
    {
        return $this->handlingSignal;
    }

    public function setExitCode(int $exitCode): void
    {
        if ($exitCode < 0 || $exitCode > 255) {
            throw new \InvalidArgumentException('Exit code must be between 0 and 255.');
        }

        $this->exitCode = $exitCode;
    }

    public function abortExit(): void
    {
        $this->exitCode = false;
    }

    public function getExitCode(): int|false
    {
        return $this->exitCode;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\DependencyInjection;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Command\LazyCommand;
use Symfony\Component\Console\CommandLoader\ContainerCommandLoader;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\TypedReference;

/**
 * Registers console commands.
 *
 * @author Grégoire Pineau <lyrixx@lyrixx.info>
 */
class AddConsoleCommandPass implements CompilerPassInterface
{
    /**
     * @return void
     */
    public function process(ContainerBuilder $container)
    {
        $commandServices = $container->findTaggedServiceIds('console.command', true);
        $lazyCommandMap = [];
        $lazyCommandRefs = [];
        $serviceIds = [];

        foreach ($commandServices as $id => $tags) {
            $definition = $container->getDefinition($id);
            $definition->addTag('container.no_preload');
            $class = $container->getParameterBag()->resolveValue($definition->getClass());

            if (isset($tags[0]['command'])) {
                $aliases = $tags[0]['command'];
            } else {
                if (!$r = $container->getReflectionClass($class)) {
                    throw new InvalidArgumentException(\sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id));
                }
                if (!$r->isSubclassOf(Command::class)) {
                    throw new InvalidArgumentException(\sprintf('The service "%s" tagged "%s" must be a subclass of "%s".', $id, 'console.command', Command::class));
                }
                $aliases = str_replace('%', '%%', $class::getDefaultName() ?? '');
            }

            $aliases = explode('|', $aliases ?? '');
            $commandName = array_shift($aliases);

            if ($isHidden = '' === $commandName) {
                $commandName = array_shift($aliases);
            }

            if (null === $commandName) {
                if (!$definition->isPublic() || $definition->isPrivate() || $definition->hasTag('container.private')) {
                    $commandId = 'console.command.public_alias.'.$id;
                    $container->setAlias($commandId, $id)->setPublic(true);
                    $id = $commandId;
                }
                $serviceIds[] = $id;

                continue;
            }

            $description = $tags[0]['description'] ?? null;

            unset($tags[0]);
            $lazyCommandMap[$commandName] = $id;
            $lazyCommandRefs[$id] = new TypedReference($id, $class);

            foreach ($aliases as $alias) {
                $lazyCommandMap[$alias] = $id;
            }

            foreach ($tags as $tag) {
                if (isset($tag['command'])) {
                    $aliases[] = $tag['command'];
                    $lazyCommandMap[$tag['command']] = $id;
                }

                $description ??= $tag['description'] ?? null;
            }

            $definition->addMethodCall('setName', [$commandName]);

            if ($aliases) {
                $definition->addMethodCall('setAliases', [$aliases]);
            }

            if ($isHidden) {
                $definition->addMethodCall('setHidden', [true]);
            }

            if (!$description) {
                if (!$r = $container->getReflectionClass($class)) {
                    throw new InvalidArgumentException(\sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id));
                }
                if (!$r->isSubclassOf(Command::class)) {
                    throw new InvalidArgumentException(\sprintf('The service "%s" tagged "%s" must be a subclass of "%s".', $id, 'console.command', Command::class));
                }
                $description = str_replace('%', '%%', $class::getDefaultDescription() ?? '');
            }

            if ($description) {
                $definition->addMethodCall('setDescription', [$description]);

                $container->register('.'.$id.'.lazy', LazyCommand::class)
                    ->setArguments([$commandName, $aliases, $description, $isHidden, new ServiceClosureArgument($lazyCommandRefs[$id])]);

                $lazyCommandRefs[$id] = new Reference('.'.$id.'.lazy');
            }
        }

        $container
            ->register('console.command_loader', ContainerCommandLoader::class)
            ->setPublic(true)
            ->addTag('container.no_preload')
            ->setArguments([ServiceLocatorTagPass::register($container, $lazyCommandRefs), $lazyCommandMap]);

        $container->setParameter('console.command.ids', $serviceIds);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Exception;

/**
 * Represents failure to read input from stdin.
 *
 * @author Gabriel Ostrolucký <gabriel.ostrolucky@gmail.com>
 */
class MissingInputException extends RuntimeException implements ExceptionInterface
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Exception;

/**
 * @author Jérôme Tamarelle <jerome@tamarelle.net>
 */
class LogicException extends \LogicException implements ExceptionInterface
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Exception;

/**
 * Represents an incorrect command name typed in the console.
 *
 * @author Jérôme Tamarelle <jerome@tamarelle.net>
 */
class CommandNotFoundException extends \InvalidArgumentException implements ExceptionInterface
{
    private array $alternatives;

    /**
     * @param string          $message      Exception message to throw
     * @param string[]        $alternatives List of similar defined names
     * @param int             $code         Exception code
     * @param \Throwable|null $previous     Previous exception used for the exception chaining
     */
    public function __construct(string $message, array $alternatives = [], int $code = 0, ?\Throwable $previous = null)
    {
        parent::__construct($message, $code, $previous);

        $this->alternatives = $alternatives;
    }

    /**
     * @return string[]
     */
    public function getAlternatives(): array
    {
        return $this->alternatives;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Exception;

/**
 * @author Jérôme Tamarelle <jerome@tamarelle.net>
 */
class RuntimeException extends \RuntimeException implements ExceptionInterface
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Exception;

/**
 * ExceptionInterface.
 *
 * @author Jérôme Tamarelle <jerome@tamarelle.net>
 */
interface ExceptionInterface extends \Throwable
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Exception;

use Symfony\Component\Console\Messenger\RunCommandContext;

/**
 * @author Kevin Bond <kevinbond@gmail.com>
 */
final class RunCommandFailedException extends RuntimeException
{
    public function __construct(\Throwable|string $exception, public readonly RunCommandContext $context)
    {
        parent::__construct(
            $exception instanceof \Throwable ? $exception->getMessage() : $exception,
            $exception instanceof \Throwable && \is_int($exception->getCode()) ? $exception->getCode() : 0,
            $exception instanceof \Throwable ? $exception : null,
        );
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Exception;

/**
 * Represents an incorrect option name or value typed in the console.
 *
 * @author Jérôme Tamarelle <jerome@tamarelle.net>
 */
class InvalidOptionException extends \InvalidArgumentException implements ExceptionInterface
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Exception;

/**
 * Represents an incorrect namespace typed in the console.
 *
 * @author Pierre du Plessis <pdples@gmail.com>
 */
class NamespaceNotFoundException extends CommandNotFoundException
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Exception;

/**
 * @author Jérôme Tamarelle <jerome@tamarelle.net>
 */
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Debug;

use Symfony\Component\Console\Command\TraceableCommand;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

/**
 * @internal
 */
final class CliRequest extends Request
{
    public function __construct(
        public readonly TraceableCommand $command,
    ) {
        parent::__construct(
            attributes: ['_controller' => \get_class($command->command), '_virtual_type' => 'command'],
            server: $_SERVER,
        );
    }

    // Methods below allow to populate a profile, thus enable search and filtering
    public function getUri(): string
    {
        if ($this->server->has('SYMFONY_CLI_BINARY_NAME')) {
            $binary = $this->server->get('SYMFONY_CLI_BINARY_NAME').' console';
        } else {
            $binary = $this->server->get('argv')[0];
        }

        return $binary.' '.$this->command->input;
    }

    public function getMethod(): string
    {
        return $this->command->isInteractive ? 'INTERACTIVE' : 'BATCH';
    }

    public function getResponse(): Response
    {
        return new class($this->command->exitCode) extends Response {
            public function __construct(private readonly int $exitCode)
            {
                parent::__construct();
            }

            public function getStatusCode(): int
            {
                return $this->exitCode;
            }
        };
    }

    public function getClientIp(): string
    {
        $application = $this->command->getApplication();

        return $application->getName().' '.$application->getVersion();
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console;

use Symfony\Component\Console\Exception\InvalidArgumentException;

/**
 * @author Fabien Potencier <fabien@symfony.com>
 */
final class Color
{
    private const COLORS = [
        'black' => 0,
        'red' => 1,
        'green' => 2,
        'yellow' => 3,
        'blue' => 4,
        'magenta' => 5,
        'cyan' => 6,
        'white' => 7,
        'default' => 9,
    ];

    private const BRIGHT_COLORS = [
        'gray' => 0,
        'bright-red' => 1,
        'bright-green' => 2,
        'bright-yellow' => 3,
        'bright-blue' => 4,
        'bright-magenta' => 5,
        'bright-cyan' => 6,
        'bright-white' => 7,
    ];

    private const AVAILABLE_OPTIONS = [
        'bold' => ['set' => 1, 'unset' => 22],
        'underscore' => ['set' => 4, 'unset' => 24],
        'blink' => ['set' => 5, 'unset' => 25],
        'reverse' => ['set' => 7, 'unset' => 27],
        'conceal' => ['set' => 8, 'unset' => 28],
    ];

    private string $foreground;
    private string $background;
    private array $options = [];

    public function __construct(string $foreground = '', string $background = '', array $options = [])
    {
        $this->foreground = $this->parseColor($foreground);
        $this->background = $this->parseColor($background, true);

        foreach ($options as $option) {
            if (!isset(self::AVAILABLE_OPTIONS[$option])) {
                throw new InvalidArgumentException(\sprintf('Invalid option specified: "%s". Expected one of (%s).', $option, implode(', ', array_keys(self::AVAILABLE_OPTIONS))));
            }

            $this->options[$option] = self::AVAILABLE_OPTIONS[$option];
        }
    }

    public function apply(string $text): string
    {
        return $this->set().$text.$this->unset();
    }

    public function set(): string
    {
        $setCodes = [];
        if ('' !== $this->foreground) {
            $setCodes[] = $this->foreground;
        }
        if ('' !== $this->background) {
            $setCodes[] = $this->background;
        }
        foreach ($this->options as $option) {
            $setCodes[] = $option['set'];
        }
        if (0 === \count($setCodes)) {
            return '';
        }

        return \sprintf("\033[%sm", implode(';', $setCodes));
    }

    public function unset(): string
    {
        $unsetCodes = [];
        if ('' !== $this->foreground) {
            $unsetCodes[] = 39;
        }
        if ('' !== $this->background) {
            $unsetCodes[] = 49;
        }
        foreach ($this->options as $option) {
            $unsetCodes[] = $option['unset'];
        }
        if (0 === \count($unsetCodes)) {
            return '';
        }

        return \sprintf("\033[%sm", implode(';', $unsetCodes));
    }

    private function parseColor(string $color, bool $background = false): string
    {
        if ('' === $color) {
            return '';
        }

        if ('#' === $color[0]) {
            return ($background ? '4' : '3').Terminal::getColorMode()->convertFromHexToAnsiColorCode($color);
        }

        if (isset(self::COLORS[$color])) {
            return ($background ? '4' : '3').self::COLORS[$color];
        }

        if (isset(self::BRIGHT_COLORS[$color])) {
            return ($background ? '10' : '9').self::BRIGHT_COLORS[$color];
        }

        throw new InvalidArgumentException(\sprintf('Invalid "%s" color; expected one of (%s).', $color, implode(', ', array_merge(array_keys(self::COLORS), array_keys(self::BRIGHT_COLORS)))));
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Completion;

use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputOption;

/**
 * An input specialized for shell completion.
 *
 * This input allows unfinished option names or values and exposes what kind of
 * completion is expected.
 *
 * @author Wouter de Jong <wouter@wouterj.nl>
 */
final class CompletionInput extends ArgvInput
{
    public const TYPE_ARGUMENT_VALUE = 'argument_value';
    public const TYPE_OPTION_VALUE = 'option_value';
    public const TYPE_OPTION_NAME = 'option_name';
    public const TYPE_NONE = 'none';

    private array $tokens;
    private int $currentIndex;
    private string $completionType;
    private ?string $completionName = null;
    private string $completionValue = '';

    /**
     * Converts a terminal string into tokens.
     *
     * This is required for shell completions without COMP_WORDS support.
     */
    public static function fromString(string $inputStr, int $currentIndex): self
    {
        preg_match_all('/(?<=^|\s)([\'"]?)(.+?)(?<!\\\\)\1(?=$|\s)/', $inputStr, $tokens);

        return self::fromTokens($tokens[0], $currentIndex);
    }

    /**
     * Create an input based on an COMP_WORDS token list.
     *
     * @param string[] $tokens       the set of split tokens (e.g. COMP_WORDS or argv)
     * @param int      $currentIndex the index of the cursor (e.g. COMP_CWORD)
     */
    public static function fromTokens(array $tokens, int $currentIndex): self
    {
        $input = new self($tokens);
        $input->tokens = $tokens;
        $input->currentIndex = $currentIndex;

        return $input;
    }

    public function bind(InputDefinition $definition): void
    {
        parent::bind($definition);

        $relevantToken = $this->getRelevantToken();
        if ('-' === $relevantToken[0]) {
            // the current token is an input option: complete either option name or option value
            [$optionToken, $optionValue] = explode('=', $relevantToken, 2) + ['', ''];

            $option = $this->getOptionFromToken($optionToken);
            if (null === $option && !$this->isCursorFree()) {
                $this->completionType = self::TYPE_OPTION_NAME;
                $this->completionValue = $relevantToken;

                return;
            }

            if ($option?->acceptValue()) {
                $this->completionType = self::TYPE_OPTION_VALUE;
                $this->completionName = $option->getName();
                $this->completionValue = $optionValue ?: (!str_starts_with($optionToken, '--') ? substr($optionToken, 2) : '');

                return;
            }
        }

        $previousToken = $this->tokens[$this->currentIndex - 1];
        if ('-' === $previousToken[0] && '' !== trim($previousToken, '-')) {
            // check if previous option accepted a value
            $previousOption = $this->getOptionFromToken($previousToken);
            if ($previousOption?->acceptValue()) {
                $this->completionType = self::TYPE_OPTION_VALUE;
                $this->completionName = $previousOption->getName();
                $this->completionValue = $relevantToken;

                return;
            }
        }

        // complete argument value
        $this->completionType = self::TYPE_ARGUMENT_VALUE;

        foreach ($this->definition->getArguments() as $argumentName => $argument) {
            if (!isset($this->arguments[$argumentName])) {
                break;
            }

            $argumentValue = $this->arguments[$argumentName];
            $this->completionName = $argumentName;
            if (\is_array($argumentValue)) {
                $this->completionValue = $argumentValue ? $argumentValue[array_key_last($argumentValue)] : null;
            } else {
                $this->completionValue = $argumentValue;
            }
        }

        if ($this->currentIndex >= \count($this->tokens)) {
            if (!isset($this->arguments[$argumentName]) || $this->definition->getArgument($argumentName)->isArray()) {
                $this->completionName = $argumentName;
                $this->completionValue = '';
            } else {
                // we've reached the end
                $this->completionType = self::TYPE_NONE;
                $this->completionName = null;
                $this->completionValue = '';
            }
        }
    }

    /**
     * Returns the type of completion required.
     *
     * TYPE_ARGUMENT_VALUE when completing the value of an input argument
     * TYPE_OPTION_VALUE   when completing the value of an input option
     * TYPE_OPTION_NAME    when completing the name of an input option
     * TYPE_NONE           when nothing should be completed
     *
     * TYPE_OPTION_NAME and TYPE_NONE are already implemented by the Console component.
     *
     * @return self::TYPE_*
     */
    public function getCompletionType(): string
    {
        return $this->completionType;
    }

    /**
     * The name of the input option or argument when completing a value.
     *
     * @return string|null returns null when completing an option name
     */
    public function getCompletionName(): ?string
    {
        return $this->completionName;
    }

    /**
     * The value already typed by the user (or empty string).
     */
    public function getCompletionValue(): string
    {
        return $this->completionValue;
    }

    public function mustSuggestOptionValuesFor(string $optionName): bool
    {
        return self::TYPE_OPTION_VALUE === $this->getCompletionType() && $optionName === $this->getCompletionName();
    }

    public function mustSuggestArgumentValuesFor(string $argumentName): bool
    {
        return self::TYPE_ARGUMENT_VALUE === $this->getCompletionType() && $argumentName === $this->getCompletionName();
    }

    protected function parseToken(string $token, bool $parseOptions): bool
    {
        try {
            return parent::parseToken($token, $parseOptions);
        } catch (RuntimeException) {
            // suppress errors, completed input is almost never valid
        }

        return $parseOptions;
    }

    private function getOptionFromToken(string $optionToken): ?InputOption
    {
        $optionName = ltrim($optionToken, '-');
        if (!$optionName) {
            return null;
        }

        if ('-' === ($optionToken[1] ?? ' ')) {
            // long option name
            return $this->definition->hasOption($optionName) ? $this->definition->getOption($optionName) : null;
        }

        // short option name
        return $this->definition->hasShortcut($optionName[0]) ? $this->definition->getOptionForShortcut($optionName[0]) : null;
    }

    /**
     * The token of the cursor, or the last token if the cursor is at the end of the input.
     */
    private function getRelevantToken(): string
    {
        return $this->tokens[$this->isCursorFree() ? $this->currentIndex - 1 : $this->currentIndex];
    }

    /**
     * Whether the cursor is "free" (i.e. at the end of the input preceded by a space).
     */
    private function isCursorFree(): bool
    {
        $nrOfTokens = \count($this->tokens);
        if ($this->currentIndex > $nrOfTokens) {
            throw new \LogicException('Current index is invalid, it must be the number of input tokens or one more.');
        }

        return $this->currentIndex >= $nrOfTokens;
    }

    public function __toString()
    {
        $str = '';
        foreach ($this->tokens as $i => $token) {
            $str .= $token;

            if ($this->currentIndex === $i) {
                $str .= '|';
            }

            $str .= ' ';
        }

        if ($this->currentIndex > $i) {
            $str .= '|';
        }

        return rtrim($str);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Completion;

/**
 * Represents a single suggested value.
 *
 * @author Wouter de Jong <wouter@wouterj.nl>
 */
class Suggestion implements \Stringable
{
    public function __construct(
        private readonly string $value,
        private readonly string $description = '',
    ) {
    }

    public function getValue(): string
    {
        return $this->value;
    }

    public function getDescription(): string
    {
        return $this->description;
    }

    public function __toString(): string
    {
        return $this->getValue();
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Completion\Output;

use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Output\OutputInterface;

/**
 * @author Wouter de Jong <wouter@wouterj.nl>
 */
class BashCompletionOutput implements CompletionOutputInterface
{
    public function write(CompletionSuggestions $suggestions, OutputInterface $output): void
    {
        $values = $suggestions->getValueSuggestions();
        foreach ($suggestions->getOptionSuggestions() as $option) {
            $values[] = '--'.$option->getName();
            if ($option->isNegatable()) {
                $values[] = '--no-'.$option->getName();
            }
        }
        $output->writeln(implode("\n", $values));
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Completion\Output;

use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Output\OutputInterface;

/**
 * @author Guillaume Aveline <guillaume.aveline@pm.me>
 */
class FishCompletionOutput implements CompletionOutputInterface
{
    public function write(CompletionSuggestions $suggestions, OutputInterface $output): void
    {
        $values = $suggestions->getValueSuggestions();
        foreach ($suggestions->getOptionSuggestions() as $option) {
            $values[] = '--'.$option->getName();
            if ($option->isNegatable()) {
                $values[] = '--no-'.$option->getName();
            }
        }
        $output->write(implode("\n", $values));
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Completion\Output;

use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Output\OutputInterface;

/**
 * @author Jitendra A <adhocore@gmail.com>
 */
class ZshCompletionOutput implements CompletionOutputInterface
{
    public function write(CompletionSuggestions $suggestions, OutputInterface $output): void
    {
        $values = [];
        foreach ($suggestions->getValueSuggestions() as $value) {
            $values[] = $value->getValue().($value->getDescription() ? "\t".$value->getDescription() : '');
        }
        foreach ($suggestions->getOptionSuggestions() as $option) {
            $values[] = '--'.$option->getName().($option->getDescription() ? "\t".$option->getDescription() : '');
            if ($option->isNegatable()) {
                $values[] = '--no-'.$option->getName().($option->getDescription() ? "\t".$option->getDescription() : '');
            }
        }
        $output->write(implode("\n", $values)."\n");
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Completion\Output;

use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Output\OutputInterface;

/**
 * Transforms the {@see CompletionSuggestions} object into output readable by the shell completion.
 *
 * @author Wouter de Jong <wouter@wouterj.nl>
 */
interface CompletionOutputInterface
{
    public function write(CompletionSuggestions $suggestions, OutputInterface $output): void;
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Completion;

use Symfony\Component\Console\Input\InputOption;

/**
 * Stores all completion suggestions for the current input.
 *
 * @author Wouter de Jong <wouter@wouterj.nl>
 */
final class CompletionSuggestions
{
    private array $valueSuggestions = [];
    private array $optionSuggestions = [];

    /**
     * Add a suggested value for an input option or argument.
     *
     * @return $this
     */
    public function suggestValue(string|Suggestion $value): static
    {
        $this->valueSuggestions[] = !$value instanceof Suggestion ? new Suggestion($value) : $value;

        return $this;
    }

    /**
     * Add multiple suggested values at once for an input option or argument.
     *
     * @param list<string|Suggestion> $values
     *
     * @return $this
     */
    public function suggestValues(array $values): static
    {
        foreach ($values as $value) {
            $this->suggestValue($value);
        }

        return $this;
    }

    /**
     * Add a suggestion for an input option name.
     *
     * @return $this
     */
    public function suggestOption(InputOption $option): static
    {
        $this->optionSuggestions[] = $option;

        return $this;
    }

    /**
     * Add multiple suggestions for input option names at once.
     *
     * @param InputOption[] $options
     *
     * @return $this
     */
    public function suggestOptions(array $options): static
    {
        foreach ($options as $option) {
            $this->suggestOption($option);
        }

        return $this;
    }

    /**
     * @return InputOption[]
     */
    public function getOptionSuggestions(): array
    {
        return $this->optionSuggestions;
    }

    /**
     * @return Suggestion[]
     */
    public function getValueSuggestions(): array
    {
        return $this->valueSuggestions;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Command\CompleteCommand;
use Symfony\Component\Console\Command\DumpCompletionCommand;
use Symfony\Component\Console\Command\HelpCommand;
use Symfony\Component\Console\Command\LazyCommand;
use Symfony\Component\Console\Command\ListCommand;
use Symfony\Component\Console\Command\SignalableCommandInterface;
use Symfony\Component\Console\CommandLoader\CommandLoaderInterface;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Completion\Suggestion;
use Symfony\Component\Console\Event\ConsoleCommandEvent;
use Symfony\Component\Console\Event\ConsoleErrorEvent;
use Symfony\Component\Console\Event\ConsoleSignalEvent;
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
use Symfony\Component\Console\Exception\CommandNotFoundException;
use Symfony\Component\Console\Exception\ExceptionInterface;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Console\Exception\NamespaceNotFoundException;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Helper\DebugFormatterHelper;
use Symfony\Component\Console\Helper\DescriptorHelper;
use Symfony\Component\Console\Helper\FormatterHelper;
use Symfony\Component\Console\Helper\Helper;
use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Helper\ProcessHelper;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputAwareInterface;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\SignalRegistry\SignalRegistry;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\ErrorHandler\ErrorHandler;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Symfony\Contracts\Service\ResetInterface;

/**
 * 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 <fabien@symfony.com>
 */
class Application implements ResetInterface
{
    private array $commands = [];
    private bool $wantHelps = false;
    private ?Command $runningCommand = null;
    private string $name;
    private string $version;
    private ?CommandLoaderInterface $commandLoader = null;
    private bool $catchExceptions = true;
    private bool $catchErrors = false;
    private bool $autoExit = true;
    private InputDefinition $definition;
    private HelperSet $helperSet;
    private ?EventDispatcherInterface $dispatcher = null;
    private Terminal $terminal;
    private string $defaultCommand;
    private bool $singleCommand = false;
    private bool $initialized = false;
    private ?SignalRegistry $signalRegistry = null;
    private array $signalsToDispatchEvent = [];

    public function __construct(string $name = 'UNKNOWN', string $version = 'UNKNOWN')
    {
        $this->name = $name;
        $this->version = $version;
        $this->terminal = new Terminal();
        $this->defaultCommand = 'list';
        if (\defined('SIGINT') && SignalRegistry::isSupported()) {
            $this->signalRegistry = new SignalRegistry();
            $this->signalsToDispatchEvent = [\SIGINT, \SIGTERM, \SIGUSR1, \SIGUSR2];
        }
    }

    /**
     * @final
     */
    public function setDispatcher(EventDispatcherInterface $dispatcher): void
    {
        $this->dispatcher = $dispatcher;
    }

    /**
     * @return void
     */
    public function setCommandLoader(CommandLoaderInterface $commandLoader)
    {
        $this->commandLoader = $commandLoader;
    }

    public function getSignalRegistry(): SignalRegistry
    {
        if (!$this->signalRegistry) {
            throw new RuntimeException('Signals are not supported. Make sure that the "pcntl" extension is installed and that "pcntl_*" functions are not disabled by your php.ini\'s "disable_functions" directive.');
        }

        return $this->signalRegistry;
    }

    /**
     * @return void
     */
    public function setSignalsToDispatchEvent(int ...$signalsToDispatchEvent)
    {
        $this->signalsToDispatchEvent = $signalsToDispatchEvent;
    }

    /**
     * Runs the current application.
     *
     * @return int 0 if everything went fine, or an error code
     *
     * @throws \Exception When running fails. Bypass this when {@link setCatchExceptions()}.
     */
    public function run(?InputInterface $input = null, ?OutputInterface $output = null): int
    {
        if (\function_exists('putenv')) {
            @putenv('LINES='.$this->terminal->getHeight());
            @putenv('COLUMNS='.$this->terminal->getWidth());
        }

        $input ??= new ArgvInput();
        $output ??= new ConsoleOutput();

        $renderException = function (\Throwable $e) use ($output) {
            if ($output instanceof ConsoleOutputInterface) {
                $this->renderThrowable($e, $output->getErrorOutput());
            } else {
                $this->renderThrowable($e, $output);
            }
        };
        if ($phpHandler = set_exception_handler($renderException)) {
            restore_exception_handler();
            if (!\is_array($phpHandler) || !$phpHandler[0] instanceof ErrorHandler) {
                $errorHandler = true;
            } elseif ($errorHandler = $phpHandler[0]->setExceptionHandler($renderException)) {
                $phpHandler[0]->setExceptionHandler($errorHandler);
            }
        }

        try {
            $this->configureIO($input, $output);

            $exitCode = $this->doRun($input, $output);
        } catch (\Throwable $e) {
            if ($e instanceof \Exception && !$this->catchExceptions) {
                throw $e;
            }
            if (!$e instanceof \Exception && !$this->catchErrors) {
                throw $e;
            }

            $renderException($e);

            $exitCode = $e->getCode();
            if (is_numeric($exitCode)) {
                $exitCode = (int) $exitCode;
                if ($exitCode <= 0) {
                    $exitCode = 1;
                }
            } else {
                $exitCode = 1;
            }
        } finally {
            // if the exception handler changed, keep it
            // otherwise, unregister $renderException
            if (!$phpHandler) {
                if (set_exception_handler($renderException) === $renderException) {
                    restore_exception_handler();
                }
                restore_exception_handler();
            } elseif (!$errorHandler) {
                $finalHandler = $phpHandler[0]->setExceptionHandler(null);
                if ($finalHandler !== $renderException) {
                    $phpHandler[0]->setExceptionHandler($finalHandler);
                }
            }
        }

        if ($this->autoExit) {
            if ($exitCode > 255) {
                $exitCode = 255;
            }

            exit($exitCode);
        }

        return $exitCode;
    }

    /**
     * Runs the current application.
     *
     * @return int 0 if everything went fine, or an error code
     */
    public function doRun(InputInterface $input, OutputInterface $output)
    {
        if (true === $input->hasParameterOption(['--version', '-V'], true)) {
            $output->writeln($this->getLongVersion());

            return 0;
        }

        try {
            // Makes ArgvInput::getFirstArgument() able to distinguish an option from an argument.
            $input->bind($this->getDefinition());
        } catch (ExceptionInterface) {
            // Errors must be ignored, full binding/validation happens later when the command is known.
        }

        $name = $this->getCommandName($input);
        if (true === $input->hasParameterOption(['--help', '-h'], true)) {
            if (!$name) {
                $name = 'help';
                $input = new ArrayInput(['command_name' => $this->defaultCommand]);
            } else {
                $this->wantHelps = true;
            }
        }

        if (!$name) {
            $name = $this->defaultCommand;
            $definition = $this->getDefinition();
            $definition->setArguments(array_merge(
                $definition->getArguments(),
                [
                    'command' => new InputArgument('command', InputArgument::OPTIONAL, $definition->getArgument('command')->getDescription(), $name),
                ]
            ));
        }

        try {
            $this->runningCommand = null;
            // the command name MUST be the first element of the input
            $command = $this->find($name);
        } catch (\Throwable $e) {
            if (($e instanceof CommandNotFoundException && !$e instanceof NamespaceNotFoundException) && 1 === \count($alternatives = $e->getAlternatives()) && $input->isInteractive()) {
                $alternative = $alternatives[0];

                $style = new SymfonyStyle($input, $output);
                $output->writeln('');
                $formattedBlock = (new FormatterHelper())->formatBlock(\sprintf('Command "%s" is not defined.', $name), 'error', true);
                $output->writeln($formattedBlock);
                if (!$style->confirm(\sprintf('Do you want to run "%s" instead? ', $alternative), false)) {
                    if (null !== $this->dispatcher) {
                        $event = new ConsoleErrorEvent($input, $output, $e);
                        $this->dispatcher->dispatch($event, ConsoleEvents::ERROR);

                        return $event->getExitCode();
                    }

                    return 1;
                }

                $command = $this->find($alternative);
            } else {
                if (null !== $this->dispatcher) {
                    $event = new ConsoleErrorEvent($input, $output, $e);
                    $this->dispatcher->dispatch($event, ConsoleEvents::ERROR);

                    if (0 === $event->getExitCode()) {
                        return 0;
                    }

                    $e = $event->getError();
                }

                try {
                    if ($e instanceof CommandNotFoundException && $namespace = $this->findNamespace($name)) {
                        $helper = new DescriptorHelper();
                        $helper->describe($output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output, $this, [
                            'format' => 'txt',
                            'raw_text' => false,
                            'namespace' => $namespace,
                            'short' => false,
                        ]);

                        return isset($event) ? $event->getExitCode() : 1;
                    }

                    throw $e;
                } catch (NamespaceNotFoundException) {
                    throw $e;
                }
            }
        }

        if ($command instanceof LazyCommand) {
            $command = $command->getCommand();
        }

        $this->runningCommand = $command;
        $exitCode = $this->doRunCommand($command, $input, $output);
        $this->runningCommand = null;

        return $exitCode;
    }

    /**
     * @return void
     */
    public function reset()
    {
    }

    /**
     * @return void
     */
    public function setHelperSet(HelperSet $helperSet)
    {
        $this->helperSet = $helperSet;
    }

    /**
     * Get the helper set associated with the command.
     */
    public function getHelperSet(): HelperSet
    {
        return $this->helperSet ??= $this->getDefaultHelperSet();
    }

    /**
     * @return void
     */
    public function setDefinition(InputDefinition $definition)
    {
        $this->definition = $definition;
    }

    /**
     * Gets the InputDefinition related to this Application.
     */
    public function getDefinition(): InputDefinition
    {
        $this->definition ??= $this->getDefaultInputDefinition();

        if ($this->singleCommand) {
            $inputDefinition = $this->definition;
            $inputDefinition->setArguments();

            return $inputDefinition;
        }

        return $this->definition;
    }

    /**
     * Adds suggestions to $suggestions for the current completion input (e.g. option or argument).
     */
    public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
    {
        if (
            CompletionInput::TYPE_ARGUMENT_VALUE === $input->getCompletionType()
            && 'command' === $input->getCompletionName()
        ) {
            foreach ($this->all() as $name => $command) {
                // skip hidden commands and aliased commands as they already get added below
                if ($command->isHidden() || $command->getName() !== $name) {
                    continue;
                }
                $suggestions->suggestValue(new Suggestion($command->getName(), $command->getDescription()));
                foreach ($command->getAliases() as $name) {
                    $suggestions->suggestValue(new Suggestion($name, $command->getDescription()));
                }
            }

            return;
        }

        if (CompletionInput::TYPE_OPTION_NAME === $input->getCompletionType()) {
            $suggestions->suggestOptions($this->getDefinition()->getOptions());

            return;
        }

        if (
            CompletionInput::TYPE_OPTION_VALUE === $input->getCompletionType()
            && ($definition = $this->getDefinition())->hasOption($input->getCompletionName())
        ) {
            $definition->getOption($input->getCompletionName())->complete($input, $suggestions);

            return;
        }
    }

    /**
     * Gets the help message.
     */
    public function getHelp(): string
    {
        return $this->getLongVersion();
    }

    /**
     * Gets whether to catch exceptions or not during commands execution.
     */
    public function areExceptionsCaught(): bool
    {
        return $this->catchExceptions;
    }

    /**
     * Sets whether to catch exceptions or not during commands execution.
     *
     * @return void
     */
    public function setCatchExceptions(bool $boolean)
    {
        $this->catchExceptions = $boolean;
    }

    /**
     * Sets whether to catch errors or not during commands execution.
     */
    public function setCatchErrors(bool $catchErrors = true): void
    {
        $this->catchErrors = $catchErrors;
    }

    /**
     * Gets whether to automatically exit after a command execution or not.
     */
    public function isAutoExitEnabled(): bool
    {
        return $this->autoExit;
    }

    /**
     * Sets whether to automatically exit after a command execution or not.
     *
     * @return void
     */
    public function setAutoExit(bool $boolean)
    {
        $this->autoExit = $boolean;
    }

    /**
     * Gets the name of the application.
     */
    public function getName(): string
    {
        return $this->name;
    }

    /**
     * Sets the application name.
     *
     * @return void
     */
    public function setName(string $name)
    {
        $this->name = $name;
    }

    /**
     * Gets the application version.
     */
    public function getVersion(): string
    {
        return $this->version;
    }

    /**
     * Sets the application version.
     *
     * @return void
     */
    public function setVersion(string $version)
    {
        $this->version = $version;
    }

    /**
     * Returns the long version of the application.
     *
     * @return string
     */
    public function getLongVersion()
    {
        if ('UNKNOWN' !== $this->getName()) {
            if ('UNKNOWN' !== $this->getVersion()) {
                return \sprintf('%s <info>%s</info>', $this->getName(), $this->getVersion());
            }

            return $this->getName();
        }

        return 'Console Tool';
    }

    /**
     * Registers a new command.
     */
    public function register(string $name): Command
    {
        return $this->add(new Command($name));
    }

    /**
     * Adds an array of command objects.
     *
     * If a Command is not enabled it will not be added.
     *
     * @param Command[] $commands An array of commands
     *
     * @return void
     */
    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.
     * If the command is not enabled it will not be added.
     *
     * @return Command|null
     */
    public function add(Command $command)
    {
        $this->init();

        $command->setApplication($this);

        if (!$command->isEnabled()) {
            $command->setApplication(null);

            return null;
        }

        if (!$command instanceof LazyCommand) {
            // Will throw if the command is not correctly initialized.
            $command->getDefinition();
        }

        if (!$command->getName()) {
            throw new LogicException(\sprintf('The command defined in "%s" cannot have an empty name.', get_debug_type($command)));
        }

        $this->commands[$command->getName()] = $command;

        foreach ($command->getAliases() as $alias) {
            $this->commands[$alias] = $command;
        }

        return $command;
    }

    /**
     * Returns a registered command by name or alias.
     *
     * @return Command
     *
     * @throws CommandNotFoundException When given command name does not exist
     */
    public function get(string $name)
    {
        $this->init();

        if (!$this->has($name)) {
            throw new CommandNotFoundException(\sprintf('The command "%s" does not exist.', $name));
        }

        // When the command has a different name than the one used at the command loader level
        if (!isset($this->commands[$name])) {
            throw new CommandNotFoundException(\sprintf('The "%s" command cannot be found because it is registered under multiple names. Make sure you don\'t set a different name via constructor or "setName()".', $name));
        }

        $command = $this->commands[$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.
     */
    public function has(string $name): bool
    {
        $this->init();

        return isset($this->commands[$name]) || ($this->commandLoader?->has($name) && $this->add($this->commandLoader->get($name)));
    }

    /**
     * Returns an array of all unique namespaces used by currently registered commands.
     *
     * It does not return the global namespace which always exists.
     *
     * @return string[]
     */
    public function getNamespaces(): array
    {
        $namespaces = [];
        foreach ($this->all() as $command) {
            if ($command->isHidden()) {
                continue;
            }

            $namespaces[] = $this->extractAllNamespaces($command->getName());

            foreach ($command->getAliases() as $alias) {
                $namespaces[] = $this->extractAllNamespaces($alias);
            }
        }

        return array_values(array_unique(array_filter(array_merge([], ...$namespaces))));
    }

    /**
     * Finds a registered namespace by a name or an abbreviation.
     *
     * @throws NamespaceNotFoundException When namespace is incorrect or ambiguous
     */
    public function findNamespace(string $namespace): string
    {
        $allNamespaces = $this->getNamespaces();
        $expr = implode('[^:]*:', array_map('preg_quote', explode(':', $namespace))).'[^:]*';
        $namespaces = preg_grep('{^'.$expr.'}', $allNamespaces);

        if (empty($namespaces)) {
            $message = \sprintf('There are no commands defined in the "%s" namespace.', $namespace);

            if ($alternatives = $this->findAlternatives($namespace, $allNamespaces)) {
                if (1 == \count($alternatives)) {
                    $message .= "\n\nDid you mean this?\n    ";
                } else {
                    $message .= "\n\nDid you mean one of these?\n    ";
                }

                $message .= implode("\n    ", $alternatives);
            }

            throw new NamespaceNotFoundException($message, $alternatives);
        }

        $exact = \in_array($namespace, $namespaces, true);
        if (\count($namespaces) > 1 && !$exact) {
            throw new NamespaceNotFoundException(\sprintf("The namespace \"%s\" is ambiguous.\nDid you mean one of these?\n%s.", $namespace, $this->getAbbreviationSuggestions(array_values($namespaces))), array_values($namespaces));
        }

        return $exact ? $namespace : reset($namespaces);
    }

    /**
     * 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.
     *
     * @return Command
     *
     * @throws CommandNotFoundException When command name is incorrect or ambiguous
     */
    public function find(string $name)
    {
        $this->init();

        $aliases = [];

        foreach ($this->commands as $command) {
            foreach ($command->getAliases() as $alias) {
                if (!$this->has($alias)) {
                    $this->commands[$alias] = $command;
                }
            }
        }

        if ($this->has($name)) {
            return $this->get($name);
        }

        $allCommands = $this->commandLoader ? array_merge($this->commandLoader->getNames(), array_keys($this->commands)) : array_keys($this->commands);
        $expr = implode('[^:]*:', array_map('preg_quote', explode(':', $name))).'[^:]*';
        $commands = preg_grep('{^'.$expr.'}', $allCommands);

        if (empty($commands)) {
            $commands = preg_grep('{^'.$expr.'}i', $allCommands);
        }

        // if no commands matched or we just matched namespaces
        if (empty($commands) || \count(preg_grep('{^'.$expr.'$}i', $commands)) < 1) {
            if (false !== $pos = strrpos($name, ':')) {
                // check if a namespace exists and contains commands
                $this->findNamespace(substr($name, 0, $pos));
            }

            $message = \sprintf('Command "%s" is not defined.', $name);

            if ($alternatives = $this->findAlternatives($name, $allCommands)) {
                $wantHelps = $this->wantHelps;
                $this->wantHelps = false;

                // remove hidden commands
                if ($alternatives = array_filter($alternatives, fn ($name) => !$this->get($name)->isHidden())) {
                    $message .= \sprintf("\n\nDid you mean %s?\n    %s", 1 === \count($alternatives) ? 'this' : 'one of these', implode("\n    ", $alternatives));
                }
                $this->wantHelps = $wantHelps;
            }

            throw new CommandNotFoundException($message, array_values($alternatives));
        }

        // filter out aliases for commands which are already on the list
        if (\count($commands) > 1) {
            $commandList = $this->commandLoader ? array_merge(array_flip($this->commandLoader->getNames()), $this->commands) : $this->commands;
            $commands = array_unique(array_filter($commands, function ($nameOrAlias) use (&$commandList, $commands, &$aliases) {
                if (!$commandList[$nameOrAlias] instanceof Command) {
                    $commandList[$nameOrAlias] = $this->commandLoader->get($nameOrAlias);
                }

                $commandName = $commandList[$nameOrAlias]->getName();

                $aliases[$nameOrAlias] = $commandName;

                return $commandName === $nameOrAlias || !\in_array($commandName, $commands);
            }));
        }

        // check whether all commands left are aliases to the same one
        if (\count($commands) > 1) {
            $uniqueCommands = array_unique(array_map(function ($nameOrAlias) use (&$commandList) {
                if (!$commandList[$nameOrAlias] instanceof Command) {
                    $commandList[$nameOrAlias] = $this->commandLoader->get($nameOrAlias);
                }

                return $commandList[$nameOrAlias]->getName();
            }, $commands));

            if (1 === \count($uniqueCommands)) {
                $commands = [reset($uniqueCommands)];
            }
        }

        if (\count($commands) > 1) {
            $usableWidth = $this->terminal->getWidth() - 10;
            $abbrevs = array_values($commands);
            $maxLen = 0;
            foreach ($abbrevs as $abbrev) {
                $maxLen = max(Helper::width($abbrev), $maxLen);
            }
            $abbrevs = array_map(static function ($cmd) use ($commandList, $usableWidth, $maxLen, &$commands) {
                if ($commandList[$cmd]->isHidden()) {
                    unset($commands[array_search($cmd, $commands)]);

                    return false;
                }

                $abbrev = str_pad($cmd, $maxLen, ' ').' '.$commandList[$cmd]->getDescription();

                return Helper::width($abbrev) > $usableWidth ? Helper::substr($abbrev, 0, $usableWidth - 3).'...' : $abbrev;
            }, array_values($commands));

            if (\count($commands) > 1) {
                $suggestions = $this->getAbbreviationSuggestions(array_filter($abbrevs));

                throw new CommandNotFoundException(\sprintf("Command \"%s\" is ambiguous.\nDid you mean one of these?\n%s.", $name, $suggestions), array_values($commands));
            }
        }

        $command = $commands ? $this->get(reset($commands)) : null;

        if (!$command || $command->isHidden()) {
            throw new CommandNotFoundException(\sprintf('The command "%s" does not exist.', $name));
        }

        return $command;
    }

    /**
     * Gets the commands (registered in the given namespace if provided).
     *
     * The array keys are the full names and the values the command instances.
     *
     * @return Command[]
     */
    public function all(?string $namespace = null)
    {
        $this->init();

        if (null === $namespace) {
            if (!$this->commandLoader) {
                return $this->commands;
            }

            $commands = $this->commands;
            foreach ($this->commandLoader->getNames() as $name) {
                if (!isset($commands[$name]) && $this->has($name)) {
                    $commands[$name] = $this->get($name);
                }
            }

            return $commands;
        }

        $commands = [];
        foreach ($this->commands as $name => $command) {
            if ($namespace === $this->extractNamespace($name, substr_count($namespace, ':') + 1)) {
                $commands[$name] = $command;
            }
        }

        if ($this->commandLoader) {
            foreach ($this->commandLoader->getNames() as $name) {
                if (!isset($commands[$name]) && $namespace === $this->extractNamespace($name, substr_count($namespace, ':') + 1) && $this->has($name)) {
                    $commands[$name] = $this->get($name);
                }
            }
        }

        return $commands;
    }

    /**
     * Returns an array of possible abbreviations given a set of names.
     *
     * @return string[][]
     */
    public static function getAbbreviations(array $names): array
    {
        $abbrevs = [];
        foreach ($names as $name) {
            for ($len = \strlen($name); $len > 0; --$len) {
                $abbrev = substr($name, 0, $len);
                $abbrevs[$abbrev][] = $name;
            }
        }

        return $abbrevs;
    }

    public function renderThrowable(\Throwable $e, OutputInterface $output): void
    {
        $output->writeln('', OutputInterface::VERBOSITY_QUIET);

        $this->doRenderThrowable($e, $output);

        if (null !== $this->runningCommand) {
            $output->writeln(\sprintf('<info>%s</info>', OutputFormatter::escape(\sprintf($this->runningCommand->getSynopsis(), $this->getName()))), OutputInterface::VERBOSITY_QUIET);
            $output->writeln('', OutputInterface::VERBOSITY_QUIET);
        }
    }

    protected function doRenderThrowable(\Throwable $e, OutputInterface $output): void
    {
        do {
            $message = trim($e->getMessage());
            if ('' === $message || OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) {
                $class = get_debug_type($e);
                $title = \sprintf('  [%s%s]  ', $class, 0 !== ($code = $e->getCode()) ? ' ('.$code.')' : '');
                $len = Helper::width($title);
            } else {
                $len = 0;
            }

            if (str_contains($message, "@anonymous\0")) {
                $message = preg_replace_callback('/[a-zA-Z_\x7f-\xff][\\\\a-zA-Z0-9_\x7f-\xff]*+@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)?[0-9a-fA-F]++/', static fn ($m) => class_exists($m[0], false) ? (get_parent_class($m[0]) ?: key(class_implements($m[0])) ?: 'class').'@anonymous' : $m[0], $message);
            }

            $width = $this->terminal->getWidth() ? $this->terminal->getWidth() - 1 : \PHP_INT_MAX;
            $lines = [];
            foreach ('' !== $message ? preg_split('/\r?\n/', $message) : [] as $line) {
                foreach ($this->splitStringByWidth($line, $width - 4) as $line) {
                    // pre-format lines to get the right string length
                    $lineLength = Helper::width($line) + 4;
                    $lines[] = [$line, $lineLength];

                    $len = max($lineLength, $len);
                }
            }

            $messages = [];
            if (!$e instanceof ExceptionInterface || OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) {
                $messages[] = \sprintf('<comment>%s</comment>', OutputFormatter::escape(\sprintf('In %s line %s:', basename($e->getFile()) ?: 'n/a', $e->getLine() ?: 'n/a')));
            }
            $messages[] = $emptyLine = \sprintf('<error>%s</error>', str_repeat(' ', $len));
            if ('' === $message || OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) {
                $messages[] = \sprintf('<error>%s%s</error>', $title, str_repeat(' ', max(0, $len - Helper::width($title))));
            }
            foreach ($lines as $line) {
                $messages[] = \sprintf('<error>  %s  %s</error>', OutputFormatter::escape($line[0]), str_repeat(' ', $len - $line[1]));
            }
            $messages[] = $emptyLine;
            $messages[] = '';

            $output->writeln($messages, OutputInterface::VERBOSITY_QUIET);

            if (OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) {
                $output->writeln('<comment>Exception trace:</comment>', OutputInterface::VERBOSITY_QUIET);

                // exception related properties
                $trace = $e->getTrace();

                array_unshift($trace, [
                    'function' => '',
                    'file' => $e->getFile() ?: 'n/a',
                    'line' => $e->getLine() ?: 'n/a',
                    'args' => [],
                ]);

                for ($i = 0, $count = \count($trace); $i < $count; ++$i) {
                    $class = $trace[$i]['class'] ?? '';
                    $type = $trace[$i]['type'] ?? '';
                    $function = $trace[$i]['function'] ?? '';
                    $file = $trace[$i]['file'] ?? 'n/a';
                    $line = $trace[$i]['line'] ?? 'n/a';

                    $output->writeln(\sprintf(' %s%s at <info>%s:%s</info>', $class, $function ? $type.$function.'()' : '', $file, $line), OutputInterface::VERBOSITY_QUIET);
                }

                $output->writeln('', OutputInterface::VERBOSITY_QUIET);
            }
        } while ($e = $e->getPrevious());
    }

    /**
     * Configures the input and output instances based on the user arguments and options.
     *
     * @return void
     */
    protected function configureIO(InputInterface $input, OutputInterface $output)
    {
        if (true === $input->hasParameterOption(['--ansi'], true)) {
            $output->setDecorated(true);
        } elseif (true === $input->hasParameterOption(['--no-ansi'], true)) {
            $output->setDecorated(false);
        }

        if (true === $input->hasParameterOption(['--no-interaction', '-n'], true)) {
            $input->setInteractive(false);
        }

        switch ($shellVerbosity = (int) getenv('SHELL_VERBOSITY')) {
            case -1:
                $output->setVerbosity(OutputInterface::VERBOSITY_QUIET);
                break;
            case 1:
                $output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE);
                break;
            case 2:
                $output->setVerbosity(OutputInterface::VERBOSITY_VERY_VERBOSE);
                break;
            case 3:
                $output->setVerbosity(OutputInterface::VERBOSITY_DEBUG);
                break;
            default:
                $shellVerbosity = 0;
                break;
        }

        if (true === $input->hasParameterOption(['--quiet', '-q'], true)) {
            $output->setVerbosity(OutputInterface::VERBOSITY_QUIET);
            $shellVerbosity = -1;
        } else {
            if ($input->hasParameterOption('-vvv', true) || $input->hasParameterOption('--verbose=3', true) || 3 === $input->getParameterOption('--verbose', false, true)) {
                $output->setVerbosity(OutputInterface::VERBOSITY_DEBUG);
                $shellVerbosity = 3;
            } elseif ($input->hasParameterOption('-vv', true) || $input->hasParameterOption('--verbose=2', true) || 2 === $input->getParameterOption('--verbose', false, true)) {
                $output->setVerbosity(OutputInterface::VERBOSITY_VERY_VERBOSE);
                $shellVerbosity = 2;
            } elseif ($input->hasParameterOption('-v', true) || $input->hasParameterOption('--verbose=1', true) || $input->hasParameterOption('--verbose', true) || $input->getParameterOption('--verbose', false, true)) {
                $output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE);
                $shellVerbosity = 1;
            }
        }

        if (-1 === $shellVerbosity) {
            $input->setInteractive(false);
        }

        if (\function_exists('putenv')) {
            @putenv('SHELL_VERBOSITY='.$shellVerbosity);
        }
        $_ENV['SHELL_VERBOSITY'] = $shellVerbosity;
        $_SERVER['SHELL_VERBOSITY'] = $shellVerbosity;
    }

    /**
     * Runs the current command.
     *
     * If an event dispatcher has been attached to the application,
     * events are also dispatched during the life-cycle of the command.
     *
     * @return int 0 if everything went fine, or an error code
     */
    protected function doRunCommand(Command $command, InputInterface $input, OutputInterface $output)
    {
        foreach ($command->getHelperSet() as $helper) {
            if ($helper instanceof InputAwareInterface) {
                $helper->setInput($input);
            }
        }

        $registeredSignals = false;
        $commandSignals = $command instanceof SignalableCommandInterface ? $command->getSubscribedSignals() : [];
        if ($commandSignals || $this->dispatcher && $this->signalsToDispatchEvent) {
            if (!$this->signalRegistry) {
                throw new RuntimeException('Unable to subscribe to signal events. Make sure that the "pcntl" extension is installed and that "pcntl_*" functions are not disabled by your php.ini\'s "disable_functions" directive.');
            }

            $registeredSignals = true;
            $this->getSignalRegistry()->pushCurrentHandlers();

            if ($this->dispatcher) {
                // We register application signals, so that we can dispatch the event
                foreach ($this->signalsToDispatchEvent as $signal) {
                    $event = new ConsoleSignalEvent($command, $input, $output, $signal);

                    $this->signalRegistry->register($signal, function ($signal) use ($event, $command, $commandSignals) {
                        $this->dispatcher->dispatch($event, ConsoleEvents::SIGNAL);
                        $exitCode = $event->getExitCode();

                        // If the command is signalable, we call the handleSignal() method
                        if (\in_array($signal, $commandSignals, true)) {
                            $exitCode = $command->handleSignal($signal, $exitCode);
                            // BC layer for Symfony <= 5
                            if (null === $exitCode) {
                                trigger_deprecation('symfony/console', '6.3', 'Not returning an exit code from "%s::handleSignal()" is deprecated, return "false" to keep the command running or "0" to exit successfully.', get_debug_type($command));
                                $exitCode = 0;
                            }
                        }

                        if (false !== $exitCode) {
                            $event = new ConsoleTerminateEvent($command, $event->getInput(), $event->getOutput(), $exitCode, $signal);
                            $this->dispatcher->dispatch($event, ConsoleEvents::TERMINATE);

                            exit($event->getExitCode());
                        }
                    });
                }

                // then we register command signals, but not if already handled after the dispatcher
                $commandSignals = array_diff($commandSignals, $this->signalsToDispatchEvent);
            }

            foreach ($commandSignals as $signal) {
                $this->signalRegistry->register($signal, static function (int $signal) use ($command): void {
                    $exitCode = $command->handleSignal($signal);
                    // BC layer for Symfony <= 5
                    if (null === $exitCode) {
                        trigger_deprecation('symfony/console', '6.3', 'Not returning an exit code from "%s::handleSignal()" is deprecated, return "false" to keep the command running or "0" to exit successfully.', get_debug_type($command));
                        $exitCode = 0;
                    }

                    if (false !== $exitCode) {
                        exit($exitCode);
                    }
                });
            }
        }

        if (null === $this->dispatcher) {
            try {
                return $command->run($input, $output);
            } finally {
                if ($registeredSignals) {
                    $this->getSignalRegistry()->popPreviousHandlers();
                }
            }
        }

        // bind before the console.command event, so the listeners have access to input options/arguments
        try {
            $command->mergeApplicationDefinition();
            $input->bind($command->getDefinition());
        } catch (ExceptionInterface) {
            // ignore invalid options/arguments for now, to allow the event listeners to customize the InputDefinition
        }

        $event = new ConsoleCommandEvent($command, $input, $output);
        $e = null;

        try {
            $this->dispatcher->dispatch($event, ConsoleEvents::COMMAND);

            if ($event->commandShouldRun()) {
                $exitCode = $command->run($input, $output);
            } else {
                $exitCode = ConsoleCommandEvent::RETURN_CODE_DISABLED;
            }
        } catch (\Throwable $e) {
            $event = new ConsoleErrorEvent($input, $output, $e, $command);
            $this->dispatcher->dispatch($event, ConsoleEvents::ERROR);
            $e = $event->getError();

            if (0 === $exitCode = $event->getExitCode()) {
                $e = null;
            }
        } finally {
            if ($registeredSignals) {
                $this->getSignalRegistry()->popPreviousHandlers();
            }
        }

        $event = new ConsoleTerminateEvent($command, $input, $output, $exitCode);
        $this->dispatcher->dispatch($event, ConsoleEvents::TERMINATE);

        if (null !== $e) {
            throw $e;
        }

        return $event->getExitCode();
    }

    /**
     * Gets the name of the command based on input.
     */
    protected function getCommandName(InputInterface $input): ?string
    {
        return $this->singleCommand ? $this->defaultCommand : $input->getFirstArgument();
    }

    /**
     * Gets the default input definition.
     */
    protected function getDefaultInputDefinition(): InputDefinition
    {
        return new InputDefinition([
            new InputArgument('command', InputArgument::REQUIRED, 'The command to execute'),
            new InputOption('--help', '-h', InputOption::VALUE_NONE, 'Display help for the given command. When no command is given display help for the <info>'.$this->defaultCommand.'</info> command'),
            new InputOption('--quiet', '-q', InputOption::VALUE_NONE, 'Do not output any message'),
            new InputOption('--verbose', '-v|vv|vvv', InputOption::VALUE_NONE, 'Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug'),
            new InputOption('--version', '-V', InputOption::VALUE_NONE, 'Display this application version'),
            new InputOption('--ansi', '', InputOption::VALUE_NEGATABLE, 'Force (or disable --no-ansi) ANSI output', null),
            new InputOption('--no-interaction', '-n', InputOption::VALUE_NONE, 'Do not ask any interactive question'),
        ]);
    }

    /**
     * Gets the default commands that should always be available.
     *
     * @return Command[]
     */
    protected function getDefaultCommands(): array
    {
        return [new HelpCommand(), new ListCommand(), new CompleteCommand(), new DumpCompletionCommand()];
    }

    /**
     * Gets the default helper set with the helpers that should always be available.
     */
    protected function getDefaultHelperSet(): HelperSet
    {
        return new HelperSet([
            new FormatterHelper(),
            new DebugFormatterHelper(),
            new ProcessHelper(),
            new QuestionHelper(),
        ]);
    }

    /**
     * Returns abbreviated suggestions in string format.
     */
    private function getAbbreviationSuggestions(array $abbrevs): string
    {
        return '    '.implode("\n    ", $abbrevs);
    }

    /**
     * Returns the namespace part of the command name.
     *
     * This method is not part of public API and should not be used directly.
     */
    public function extractNamespace(string $name, ?int $limit = null): string
    {
        $parts = explode(':', $name, -1);

        return implode(':', null === $limit ? $parts : \array_slice($parts, 0, $limit));
    }

    /**
     * Finds alternative of $name among $collection,
     * if nothing is found in $collection, try in $abbrevs.
     *
     * @return string[]
     */
    private function findAlternatives(string $name, iterable $collection): array
    {
        $threshold = 1e3;
        $alternatives = [];

        $collectionParts = [];
        foreach ($collection as $item) {
            $collectionParts[$item] = explode(':', $item);
        }

        foreach (explode(':', $name) as $i => $subname) {
            foreach ($collectionParts as $collectionName => $parts) {
                $exists = isset($alternatives[$collectionName]);
                if (!isset($parts[$i]) && $exists) {
                    $alternatives[$collectionName] += $threshold;
                    continue;
                } elseif (!isset($parts[$i])) {
                    continue;
                }

                $lev = levenshtein($subname, $parts[$i]);
                if ($lev <= \strlen($subname) / 3 || '' !== $subname && str_contains($parts[$i], $subname)) {
                    $alternatives[$collectionName] = $exists ? $alternatives[$collectionName] + $lev : $lev;
                } elseif ($exists) {
                    $alternatives[$collectionName] += $threshold;
                }
            }
        }

        foreach ($collection as $item) {
            $lev = levenshtein($name, $item);
            if ($lev <= \strlen($name) / 3 || str_contains($item, $name)) {
                $alternatives[$item] = isset($alternatives[$item]) ? $alternatives[$item] - $lev : $lev;
            }
        }

        $alternatives = array_filter($alternatives, static fn ($lev) => $lev < 2 * $threshold);
        ksort($alternatives, \SORT_NATURAL | \SORT_FLAG_CASE);

        return array_keys($alternatives);
    }

    /**
     * Sets the default Command name.
     *
     * @return $this
     */
    public function setDefaultCommand(string $commandName, bool $isSingleCommand = false): static
    {
        $this->defaultCommand = explode('|', ltrim($commandName, '|'))[0];

        if ($isSingleCommand) {
            // Ensure the command exist
            $this->find($commandName);

            $this->singleCommand = true;
        }

        return $this;
    }

    /**
     * @internal
     */
    public function isSingleCommand(): bool
    {
        return $this->singleCommand;
    }

    private function splitStringByWidth(string $string, int $width): array
    {
        // str_split is not suitable for multi-byte characters, we should use preg_split to get char array properly.
        // additionally, array_slice() is not enough as some character has doubled width.
        // we need a function to split string not by character count but by string width
        if (false === $encoding = mb_detect_encoding($string, null, true)) {
            return str_split($string, $width);
        }

        $utf8String = mb_convert_encoding($string, 'utf8', $encoding);
        $lines = [];
        $line = '';

        $offset = 0;
        while (preg_match('/.{1,10000}/u', $utf8String, $m, 0, $offset)) {
            $offset += \strlen($m[0]);

            foreach (preg_split('//u', $m[0]) as $char) {
                // test if $char could be appended to current line
                if (Helper::width($line.$char) <= $width) {
                    $line .= $char;
                    continue;
                }
                // if not, push current line to array and make new line
                $lines[] = str_pad($line, $width);
                $line = $char;
            }
        }

        $lines[] = \count($lines) ? str_pad($line, $width) : $line;

        return mb_convert_encoding($lines, $encoding, 'utf8');
    }

    /**
     * Returns all namespaces of the command name.
     *
     * @return string[]
     */
    private function extractAllNamespaces(string $name): array
    {
        // -1 as third argument is needed to skip the command short name when exploding
        $parts = explode(':', $name, -1);
        $namespaces = [];

        foreach ($parts as $part) {
            if (\count($namespaces)) {
                $namespaces[] = end($namespaces).':'.$part;
            } else {
                $namespaces[] = $part;
            }
        }

        return $namespaces;
    }

    private function init(): void
    {
        if ($this->initialized) {
            return;
        }
        $this->initialized = true;

        foreach ($this->getDefaultCommands() as $command) {
            $this->add($command);
        }
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Attribute;

/**
 * Service tag to autoconfigure commands.
 */
#[\Attribute(\Attribute::TARGET_CLASS)]
class AsCommand
{
    public function __construct(
        public string $name,
        public ?string $description = null,
        array $aliases = [],
        bool $hidden = false,
    ) {
        if (!$hidden && !$aliases) {
            return;
        }

        $name = explode('|', $name);
        $name = array_merge($name, $aliases);

        if ($hidden && '' !== $name[0]) {
            array_unshift($name, '');
        }

        $this->name = implode('|', $name);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Formatter;

/**
 * @author Tien Xuan Vo <tien.xuan.vo@gmail.com>
 */
final class NullOutputFormatter implements OutputFormatterInterface
{
    private NullOutputFormatterStyle $style;

    public function format(?string $message): ?string
    {
        return null;
    }

    public function getStyle(string $name): OutputFormatterStyleInterface
    {
        // to comply with the interface we must return a OutputFormatterStyleInterface
        return $this->style ??= new NullOutputFormatterStyle();
    }

    public function hasStyle(string $name): bool
    {
        return false;
    }

    public function isDecorated(): bool
    {
        return false;
    }

    public function setDecorated(bool $decorated): void
    {
        // do nothing
    }

    public function setStyle(string $name, OutputFormatterStyleInterface $style): void
    {
        // do nothing
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Formatter;

/**
 * @author Tien Xuan Vo <tien.xuan.vo@gmail.com>
 */
final class NullOutputFormatterStyle implements OutputFormatterStyleInterface
{
    public function apply(string $text): string
    {
        return $text;
    }

    public function setBackground(?string $color = null): void
    {
        if (1 > \func_num_args()) {
            trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__);
        }
        // do nothing
    }

    public function setForeground(?string $color = null): void
    {
        if (1 > \func_num_args()) {
            trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__);
        }
        // do nothing
    }

    public function setOption(string $option): void
    {
        // do nothing
    }

    public function setOptions(array $options): void
    {
        // do nothing
    }

    public function unsetOption(string $option): void
    {
        // do nothing
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Formatter;

/**
 * Formatter style interface for defining styles.
 *
 * @author Konstantin Kudryashov <ever.zet@gmail.com>
 */
interface OutputFormatterStyleInterface
{
    /**
     * Sets style foreground color.
     *
     * @return void
     */
    public function setForeground(?string $color);

    /**
     * Sets style background color.
     *
     * @return void
     */
    public function setBackground(?string $color);

    /**
     * Sets some specific style option.
     *
     * @return void
     */
    public function setOption(string $option);

    /**
     * Unsets some specific style option.
     *
     * @return void
     */
    public function unsetOption(string $option);

    /**
     * Sets multiple style options at once.
     *
     * @return void
     */
    public function setOptions(array $options);

    /**
     * Applies the style to a given text.
     */
    public function apply(string $text): string;
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Formatter;

/**
 * Formatter interface for console output that supports word wrapping.
 *
 * @author Roland Franssen <franssen.roland@gmail.com>
 */
interface WrappableOutputFormatterInterface extends OutputFormatterInterface
{
    /**
     * Formats a message according to the given styles, wrapping at `$width` (0 means no wrapping).
     *
     * @return string
     */
    public function formatAndWrap(?string $message, int $width);
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Formatter;

/**
 * Formatter interface for console output.
 *
 * @author Konstantin Kudryashov <ever.zet@gmail.com>
 */
interface OutputFormatterInterface
{
    /**
     * Sets the decorated flag.
     *
     * @return void
     */
    public function setDecorated(bool $decorated);

    /**
     * Whether the output will decorate messages.
     */
    public function isDecorated(): bool;

    /**
     * Sets a new style.
     *
     * @return void
     */
    public function setStyle(string $name, OutputFormatterStyleInterface $style);

    /**
     * Checks if output formatter has style with specified name.
     */
    public function hasStyle(string $name): bool;

    /**
     * Gets style options from style with specified name.
     *
     * @throws \InvalidArgumentException When style isn't defined
     */
    public function getStyle(string $name): OutputFormatterStyleInterface;

    /**
     * Formats a message according to the given styles.
     */
    public function format(?string $message): ?string;
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Formatter;

use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Helper\Helper;

use function Symfony\Component\String\b;

/**
 * Formatter class for console output.
 *
 * @author Konstantin Kudryashov <ever.zet@gmail.com>
 * @author Roland Franssen <franssen.roland@gmail.com>
 */
class OutputFormatter implements WrappableOutputFormatterInterface
{
    private bool $decorated;
    private array $styles = [];
    private OutputFormatterStyleStack $styleStack;

    public function __clone()
    {
        $this->styleStack = clone $this->styleStack;
        foreach ($this->styles as $key => $value) {
            $this->styles[$key] = clone $value;
        }
    }

    /**
     * Escapes "<" and ">" special chars in given text.
     */
    public static function escape(string $text): string
    {
        $text = preg_replace('/([^\\\\]|^)([<>])/', '$1\\\\$2', $text);

        return self::escapeTrailingBackslash($text);
    }

    /**
     * Escapes trailing "\" in given text.
     *
     * @internal
     */
    public static function escapeTrailingBackslash(string $text): string
    {
        if (str_ends_with($text, '\\')) {
            $len = \strlen($text);
            $text = rtrim($text, '\\');
            $text = str_replace("\0", '', $text);
            $text .= str_repeat("\0", $len - \strlen($text));
        }

        return $text;
    }

    /**
     * Initializes console output formatter.
     *
     * @param OutputFormatterStyleInterface[] $styles Array of "name => FormatterStyle" instances
     */
    public function __construct(bool $decorated = false, array $styles = [])
    {
        $this->decorated = $decorated;

        $this->setStyle('error', new OutputFormatterStyle('white', 'red'));
        $this->setStyle('info', new OutputFormatterStyle('green'));
        $this->setStyle('comment', new OutputFormatterStyle('yellow'));
        $this->setStyle('question', new OutputFormatterStyle('black', 'cyan'));

        foreach ($styles as $name => $style) {
            $this->setStyle($name, $style);
        }

        $this->styleStack = new OutputFormatterStyleStack();
    }

    /**
     * @return void
     */
    public function setDecorated(bool $decorated)
    {
        $this->decorated = $decorated;
    }

    public function isDecorated(): bool
    {
        return $this->decorated;
    }

    /**
     * @return void
     */
    public function setStyle(string $name, OutputFormatterStyleInterface $style)
    {
        $this->styles[strtolower($name)] = $style;
    }

    public function hasStyle(string $name): bool
    {
        return isset($this->styles[strtolower($name)]);
    }

    public function getStyle(string $name): OutputFormatterStyleInterface
    {
        if (!$this->hasStyle($name)) {
            throw new InvalidArgumentException(\sprintf('Undefined style: "%s".', $name));
        }

        return $this->styles[strtolower($name)];
    }

    public function format(?string $message): ?string
    {
        return $this->formatAndWrap($message, 0);
    }

    /**
     * @return string
     */
    public function formatAndWrap(?string $message, int $width)
    {
        if (null === $message) {
            return '';
        }

        // For ASCII-only strings, byte positions equal character positions,
        // so we can use native strlen/substr which is much faster than Helper::length/substr.
        $isAscii = !preg_match('/[\x80-\xFF]/', $message);

        $offset = 0;
        $output = '';
        $openTagRegex = '[a-z](?:[^\\\\<>]*+ | \\\\.)*';
        $closeTagRegex = '[a-z][^<>]*+';
        $currentLineLength = 0;
        preg_match_all("#<(($openTagRegex) | /($closeTagRegex)?)>#ix", $message, $matches, \PREG_OFFSET_CAPTURE);
        foreach ($matches[0] as $i => $match) {
            $pos = $match[1];
            $text = $match[0];

            if (0 != $pos && '\\' == $message[$pos - 1]) {
                continue;
            }

            if ($isAscii) {
                // For ASCII, byte position = character position, no conversion needed
                $output .= $this->applyCurrentStyle(substr($message, $offset, $pos - $offset), $output, $width, $currentLineLength);
                $offset = $pos + \strlen($text);
            } else {
                // convert byte position to character position.
                $pos = Helper::length(substr($message, 0, $pos));
                // add the text up to the next tag
                $output .= $this->applyCurrentStyle(Helper::substr($message, $offset, $pos - $offset), $output, $width, $currentLineLength);
                $offset = $pos + Helper::length($text);
            }

            // opening tag?
            if ($open = '/' !== $text[1]) {
                $tag = $matches[1][$i][0];
            } else {
                $tag = $matches[3][$i][0] ?? '';
            }

            if (!$open && !$tag) {
                // </>
                $this->styleStack->pop();
            } elseif (null === $style = $this->createStyleFromString($tag)) {
                $output .= $this->applyCurrentStyle($text, $output, $width, $currentLineLength);
            } elseif ($open) {
                $this->styleStack->push($style);
            } else {
                $this->styleStack->pop($style);
            }
        }

        $output .= $this->applyCurrentStyle($isAscii ? substr($message, $offset) : Helper::substr($message, $offset), $output, $width, $currentLineLength);

        return strtr($output, ["\0" => '\\', '\\<' => '<', '\\>' => '>']);
    }

    public function getStyleStack(): OutputFormatterStyleStack
    {
        return $this->styleStack;
    }

    /**
     * Tries to create new style instance from string.
     */
    private function createStyleFromString(string $string): ?OutputFormatterStyleInterface
    {
        if (isset($this->styles[$string])) {
            return $this->styles[$string];
        }

        if (!preg_match_all('/([^=]+)=([^;]+)(;|$)/', $string, $matches, \PREG_SET_ORDER)) {
            return null;
        }

        $style = new OutputFormatterStyle();
        foreach ($matches as $match) {
            array_shift($match);
            $match[0] = strtolower($match[0]);

            if ('fg' == $match[0]) {
                $style->setForeground(strtolower($match[1]));
            } elseif ('bg' == $match[0]) {
                $style->setBackground(strtolower($match[1]));
            } elseif ('href' === $match[0]) {
                $url = preg_replace('{\\\\([<>])}', '$1', $match[1]);
                $style->setHref($url);
            } elseif ('options' === $match[0]) {
                preg_match_all('([^,;]+)', strtolower($match[1]), $options);
                $options = array_shift($options);
                foreach ($options as $option) {
                    $style->setOption($option);
                }
            } else {
                return null;
            }
        }

        return $style;
    }

    /**
     * Applies current style from stack to text, if must be applied.
     */
    private function applyCurrentStyle(string $text, string $current, int $width, int &$currentLineLength): string
    {
        if ('' === $text) {
            return '';
        }

        if (!$width) {
            return $this->isDecorated() ? $this->styleStack->getCurrent()->apply($text) : $text;
        }

        if (!$currentLineLength && '' !== $current) {
            $text = ltrim($text);
        }

        if ($currentLineLength) {
            $lines = explode("\n", $text, 2);
            $prefix = Helper::substr($lines[0], 0, $i = $width - $currentLineLength)."\n";
            $text = Helper::substr($lines[0], $i);

            if (isset($lines[1])) {
                // $prefix may contain the full first line in which the \n is already a part of $prefix.
                if ('' !== $text) {
                    $text .= "\n";
                }

                $text .= $lines[1];
            }
        } else {
            $prefix = '';
        }

        preg_match('~(\\n)$~', $text, $matches);
        $text = $prefix.$this->addLineBreaks($text, $width);
        $text = rtrim($text, "\n").($matches[1] ?? '');

        if (!$currentLineLength && '' !== $current && !str_ends_with($current, "\n")) {
            $text = "\n".$text;
        }

        $lines = explode("\n", $text);

        foreach ($lines as $i => $line) {
            $currentLineLength = 0 === $i ? $currentLineLength + Helper::length($line) : Helper::length($line);
            if ($width <= $currentLineLength) {
                $currentLineLength = 0;
            }
        }

        if ($this->isDecorated()) {
            foreach ($lines as $i => $line) {
                $lines[$i] = $this->styleStack->getCurrent()->apply($line);
            }
        }

        return implode("\n", $lines);
    }

    private function addLineBreaks(string $text, int $width): string
    {
        $encoding = mb_detect_encoding($text, null, true) ?: 'UTF-8';

        return b($text)->toUnicodeString($encoding)->wordwrap($width, "\n", true)->toByteString($encoding);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Formatter;

use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Contracts\Service\ResetInterface;

/**
 * @author Jean-François Simon <contact@jfsimon.fr>
 */
class OutputFormatterStyleStack implements ResetInterface
{
    /**
     * @var OutputFormatterStyleInterface[]
     */
    private array $styles = [];

    private OutputFormatterStyleInterface $emptyStyle;

    public function __construct(?OutputFormatterStyleInterface $emptyStyle = null)
    {
        $this->emptyStyle = $emptyStyle ?? new OutputFormatterStyle();
        $this->reset();
    }

    /**
     * Resets stack (ie. empty internal arrays).
     *
     * @return void
     */
    public function reset()
    {
        $this->styles = [];
    }

    /**
     * Pushes a style in the stack.
     *
     * @return void
     */
    public function push(OutputFormatterStyleInterface $style)
    {
        $this->styles[] = $style;
    }

    /**
     * Pops a style from the stack.
     *
     * @throws InvalidArgumentException When style tags incorrectly nested
     */
    public function pop(?OutputFormatterStyleInterface $style = null): OutputFormatterStyleInterface
    {
        if (!$this->styles) {
            return $this->emptyStyle;
        }

        if (null === $style) {
            return array_pop($this->styles);
        }

        foreach (array_reverse($this->styles, true) as $index => $stackedStyle) {
            if ($style->apply('') === $stackedStyle->apply('')) {
                $this->styles = \array_slice($this->styles, 0, $index);

                return $stackedStyle;
            }
        }

        throw new InvalidArgumentException('Incorrectly nested style tag found.');
    }

    /**
     * Computes current style with stacks top codes.
     */
    public function getCurrent(): OutputFormatterStyleInterface
    {
        if (!$this->styles) {
            return $this->emptyStyle;
        }

        return $this->styles[\count($this->styles) - 1];
    }

    /**
     * @return $this
     */
    public function setEmptyStyle(OutputFormatterStyleInterface $emptyStyle): static
    {
        $this->emptyStyle = $emptyStyle;

        return $this;
    }

    public function getEmptyStyle(): OutputFormatterStyleInterface
    {
        return $this->emptyStyle;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Formatter;

use Symfony\Component\Console\Color;

/**
 * Formatter style class for defining styles.
 *
 * @author Konstantin Kudryashov <ever.zet@gmail.com>
 */
class OutputFormatterStyle implements OutputFormatterStyleInterface
{
    private Color $color;
    private string $foreground;
    private string $background;
    private array $options;
    private ?string $href = null;
    private bool $handlesHrefGracefully;

    /**
     * Initializes output formatter style.
     *
     * @param string|null $foreground The style foreground color name
     * @param string|null $background The style background color name
     */
    public function __construct(?string $foreground = null, ?string $background = null, array $options = [])
    {
        $this->color = new Color($this->foreground = $foreground ?: '', $this->background = $background ?: '', $this->options = $options);
    }

    /**
     * @return void
     */
    public function setForeground(?string $color = null)
    {
        if (1 > \func_num_args()) {
            trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__);
        }
        $this->color = new Color($this->foreground = $color ?: '', $this->background, $this->options);
    }

    /**
     * @return void
     */
    public function setBackground(?string $color = null)
    {
        if (1 > \func_num_args()) {
            trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__);
        }
        $this->color = new Color($this->foreground, $this->background = $color ?: '', $this->options);
    }

    public function setHref(string $url): void
    {
        $this->href = $url;
    }

    /**
     * @return void
     */
    public function setOption(string $option)
    {
        $this->options[] = $option;
        $this->color = new Color($this->foreground, $this->background, $this->options);
    }

    /**
     * @return void
     */
    public function unsetOption(string $option)
    {
        $pos = array_search($option, $this->options);
        if (false !== $pos) {
            unset($this->options[$pos]);
        }

        $this->color = new Color($this->foreground, $this->background, $this->options);
    }

    /**
     * @return void
     */
    public function setOptions(array $options)
    {
        $this->color = new Color($this->foreground, $this->background, $this->options = $options);
    }

    public function apply(string $text): string
    {
        $this->handlesHrefGracefully ??= 'JetBrains-JediTerm' !== getenv('TERMINAL_EMULATOR')
            && (!getenv('KONSOLE_VERSION') || (int) getenv('KONSOLE_VERSION') > 201100)
            && !isset($_SERVER['IDEA_INITIAL_DIRECTORY']);

        if (null !== $this->href && $this->handlesHrefGracefully) {
            $text = "\033]8;;$this->href\033\\$text\033]8;;\033\\";
        }

        return $this->color->apply($text);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Logger;

use Psr\Log\AbstractLogger;
use Psr\Log\InvalidArgumentException;
use Psr\Log\LogLevel;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;

/**
 * PSR-3 compliant console logger.
 *
 * @author Kévin Dunglas <dunglas@gmail.com>
 *
 * @see https://www.php-fig.org/psr/psr-3/
 */
class ConsoleLogger extends AbstractLogger
{
    public const INFO = 'info';
    public const ERROR = 'error';

    private OutputInterface $output;
    private array $verbosityLevelMap = [
        LogLevel::EMERGENCY => OutputInterface::VERBOSITY_NORMAL,
        LogLevel::ALERT => OutputInterface::VERBOSITY_NORMAL,
        LogLevel::CRITICAL => OutputInterface::VERBOSITY_NORMAL,
        LogLevel::ERROR => OutputInterface::VERBOSITY_NORMAL,
        LogLevel::WARNING => OutputInterface::VERBOSITY_NORMAL,
        LogLevel::NOTICE => OutputInterface::VERBOSITY_VERBOSE,
        LogLevel::INFO => OutputInterface::VERBOSITY_VERY_VERBOSE,
        LogLevel::DEBUG => OutputInterface::VERBOSITY_DEBUG,
    ];
    private array $formatLevelMap = [
        LogLevel::EMERGENCY => self::ERROR,
        LogLevel::ALERT => self::ERROR,
        LogLevel::CRITICAL => self::ERROR,
        LogLevel::ERROR => self::ERROR,
        LogLevel::WARNING => self::INFO,
        LogLevel::NOTICE => self::INFO,
        LogLevel::INFO => self::INFO,
        LogLevel::DEBUG => self::INFO,
    ];
    private bool $errored = false;

    public function __construct(OutputInterface $output, array $verbosityLevelMap = [], array $formatLevelMap = [])
    {
        $this->output = $output;
        $this->verbosityLevelMap = $verbosityLevelMap + $this->verbosityLevelMap;
        $this->formatLevelMap = $formatLevelMap + $this->formatLevelMap;
    }

    public function log($level, $message, array $context = []): void
    {
        if (!isset($this->verbosityLevelMap[$level])) {
            throw new InvalidArgumentException(\sprintf('The log level "%s" does not exist.', $level));
        }

        $output = $this->output;

        // Write to the error output if necessary and available
        if (self::ERROR === $this->formatLevelMap[$level]) {
            if ($this->output instanceof ConsoleOutputInterface) {
                $output = $output->getErrorOutput();
            }
            $this->errored = true;
        }

        // the if condition check isn't necessary -- it's the same one that $output will do internally anyway.
        // We only do it for efficiency here as the message formatting is relatively expensive.
        if ($output->getVerbosity() >= $this->verbosityLevelMap[$level]) {
            $output->writeln(\sprintf('<%1$s>[%2$s] %3$s</%1$s>', $this->formatLevelMap[$level], $level, $this->interpolate($message, $context)), $this->verbosityLevelMap[$level]);
        }
    }

    /**
     * Returns true when any messages have been logged at error levels.
     */
    public function hasErrored(): bool
    {
        return $this->errored;
    }

    /**
     * Interpolates context values into the message placeholders.
     *
     * @author PHP Framework Interoperability Group
     */
    private function interpolate(string $message, array $context): string
    {
        if (!str_contains($message, '{')) {
            return $message;
        }

        $replacements = [];
        foreach ($context as $key => $val) {
            if (null === $val || \is_scalar($val) || $val instanceof \Stringable) {
                $replacements["{{$key}}"] = $val;
            } elseif ($val instanceof \DateTimeInterface) {
                $replacements["{{$key}}"] = $val->format(\DateTimeInterface::RFC3339);
            } elseif (\is_object($val)) {
                $replacements["{{$key}}"] = '[object '.$val::class.']';
            } else {
                $replacements["{{$key}}"] = '['.\gettype($val).']';
            }
        }

        return strtr($message, $replacements);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Tester;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\ArrayInput;

/**
 * Eases the testing of console commands.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 * @author Robin Chalas <robin.chalas@gmail.com>
 */
class CommandTester
{
    use TesterTrait;

    private Command $command;

    public function __construct(Command $command)
    {
        $this->command = $command;
    }

    /**
     * Executes the command.
     *
     * Available execution options:
     *
     *  * interactive:               Sets the input interactive flag
     *  * decorated:                 Sets the output decorated flag
     *  * verbosity:                 Sets the output verbosity flag
     *  * capture_stderr_separately: Make output of stdOut and stdErr separately available
     *
     * @param array $input   An array of command arguments and options
     * @param array $options An array of execution options
     *
     * @return int The command exit code
     */
    public function execute(array $input, array $options = []): int
    {
        // set the command name automatically if the application requires
        // this argument and no command name was passed
        if (!isset($input['command'])
            && (null !== $application = $this->command->getApplication())
            && $application->getDefinition()->hasArgument('command')
        ) {
            $input = array_merge(['command' => $this->command->getName()], $input);
        }

        $this->input = new ArrayInput($input);
        // Use an in-memory input stream even if no inputs are set so that QuestionHelper::ask() does not rely on the blocking STDIN.
        $this->input->setStream(self::createStream($this->inputs));

        if (isset($options['interactive'])) {
            $this->input->setInteractive($options['interactive']);
        }

        if (!isset($options['decorated'])) {
            $options['decorated'] = false;
        }

        $this->initOutput($options);

        return $this->statusCode = $this->command->run($this->input, $this->output);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Tester;

use Symfony\Component\Console\Application;
use Symfony\Component\Console\Input\ArrayInput;

/**
 * Eases the testing of console applications.
 *
 * When testing an application, don't forget to disable the auto exit flag:
 *
 *     $application = new Application();
 *     $application->setAutoExit(false);
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class ApplicationTester
{
    use TesterTrait;

    private Application $application;

    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
     *  * capture_stderr_separately: Make output of stdOut and stdErr separately available
     *
     * @return int The command exit code
     */
    public function run(array $input, array $options = []): int
    {
        $prevShellVerbosity = [getenv('SHELL_VERBOSITY'), $_ENV['SHELL_VERBOSITY'] ?? false, $_SERVER['SHELL_VERBOSITY'] ?? false];

        try {
            $this->input = new ArrayInput($input);
            if (isset($options['interactive'])) {
                $this->input->setInteractive($options['interactive']);
            }

            if ($this->inputs) {
                $this->input->setStream(self::createStream($this->inputs));
            }

            $this->initOutput($options);

            // Temporarily clear SHELL_VERBOSITY to prevent Application::configureIO
            // from overriding the interactive and verbosity settings set above
            if (\function_exists('putenv')) {
                @putenv('SHELL_VERBOSITY');
            }
            unset($_ENV['SHELL_VERBOSITY'], $_SERVER['SHELL_VERBOSITY']);

            return $this->statusCode = $this->application->run($this->input, $this->output);
        } finally {
            // SHELL_VERBOSITY is set by Application::configureIO so we need to unset/reset it
            // to its previous value to avoid one test's verbosity to spread to the following tests
            if (false === $prevShellVerbosity[0]) {
                if (\function_exists('putenv')) {
                    @putenv('SHELL_VERBOSITY');
                }
            } else {
                if (\function_exists('putenv')) {
                    @putenv('SHELL_VERBOSITY='.$prevShellVerbosity[0]);
                }
            }
            if (false === $prevShellVerbosity[1]) {
                unset($_ENV['SHELL_VERBOSITY']);
            } else {
                $_ENV['SHELL_VERBOSITY'] = $prevShellVerbosity[1];
            }
            if (false === $prevShellVerbosity[2]) {
                unset($_SERVER['SHELL_VERBOSITY']);
            } else {
                $_SERVER['SHELL_VERBOSITY'] = $prevShellVerbosity[2];
            }
        }
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Tester\Constraint;

use PHPUnit\Framework\Constraint\Constraint;
use Symfony\Component\Console\Command\Command;

final class CommandIsSuccessful extends Constraint
{
    public function toString(): string
    {
        return 'is successful';
    }

    protected function matches($other): bool
    {
        return Command::SUCCESS === $other;
    }

    protected function failureDescription($other): string
    {
        return 'the command '.$this->toString();
    }

    protected function additionalFailureDescription($other): string
    {
        $mapping = [
            Command::FAILURE => 'Command failed.',
            Command::INVALID => 'Command was invalid.',
        ];

        return $mapping[$other] ?? \sprintf('Command returned exit status %d.', $other);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Tester;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;

/**
 * Eases the testing of command completion.
 *
 * @author Jérôme Tamarelle <jerome@tamarelle.net>
 */
class CommandCompletionTester
{
    private Command $command;

    public function __construct(Command $command)
    {
        $this->command = $command;
    }

    /**
     * Create completion suggestions from input tokens.
     */
    public function complete(array $input): array
    {
        $currentIndex = \count($input);
        if ('' === end($input)) {
            array_pop($input);
        }
        array_unshift($input, $this->command->getName());

        $completionInput = CompletionInput::fromTokens($input, $currentIndex);
        $completionInput->bind($this->command->getDefinition());
        $suggestions = new CompletionSuggestions();

        $this->command->complete($completionInput, $suggestions);

        $options = [];
        foreach ($suggestions->getOptionSuggestions() as $option) {
            $options[] = '--'.$option->getName();
        }

        return array_map('strval', array_merge($options, $suggestions->getValueSuggestions()));
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Tester;

use PHPUnit\Framework\Assert;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Output\StreamOutput;
use Symfony\Component\Console\Tester\Constraint\CommandIsSuccessful;

/**
 * @author Amrouche Hamza <hamza.simperfit@gmail.com>
 */
trait TesterTrait
{
    private StreamOutput $output;
    private array $inputs = [];
    private bool $captureStreamsIndependently = false;
    private InputInterface $input;
    private int $statusCode;

    /**
     * Gets the display returned by the last execution of the command or application.
     *
     * @throws \RuntimeException If it's called before the execute method
     */
    public function getDisplay(bool $normalize = false): string
    {
        if (!isset($this->output)) {
            throw new \RuntimeException('Output not initialized, did you execute the command before requesting the display?');
        }

        rewind($this->output->getStream());

        $display = stream_get_contents($this->output->getStream());

        if ($normalize) {
            $display = str_replace(\PHP_EOL, "\n", $display);
        }

        return $display;
    }

    /**
     * Gets the output written to STDERR by the application.
     *
     * @param bool $normalize Whether to normalize end of lines to \n or not
     */
    public function getErrorOutput(bool $normalize = false): string
    {
        if (!$this->captureStreamsIndependently) {
            throw new \LogicException('The error output is not available when the tester is run without "capture_stderr_separately" option set.');
        }

        rewind($this->output->getErrorOutput()->getStream());

        $display = stream_get_contents($this->output->getErrorOutput()->getStream());

        if ($normalize) {
            $display = str_replace(\PHP_EOL, "\n", $display);
        }

        return $display;
    }

    /**
     * Gets the input instance used by the last execution of the command or application.
     */
    public function getInput(): InputInterface
    {
        return $this->input;
    }

    /**
     * Gets the output instance used by the last execution of the command or application.
     */
    public function getOutput(): OutputInterface
    {
        return $this->output;
    }

    /**
     * Gets the status code returned by the last execution of the command or application.
     *
     * @throws \RuntimeException If it's called before the execute method
     */
    public function getStatusCode(): int
    {
        return $this->statusCode ?? throw new \RuntimeException('Status code not initialized, did you execute the command before requesting the status code?');
    }

    public function assertCommandIsSuccessful(string $message = ''): void
    {
        Assert::assertThat($this->statusCode, new CommandIsSuccessful(), $message);
    }

    /**
     * Sets the user inputs.
     *
     * @param array $inputs An array of strings representing each input
     *                      passed to the command input stream
     *
     * @return $this
     */
    public function setInputs(array $inputs): static
    {
        $this->inputs = $inputs;

        return $this;
    }

    /**
     * Initializes the output property.
     *
     * Available options:
     *
     *  * decorated:                 Sets the output decorated flag
     *  * verbosity:                 Sets the output verbosity flag
     *  * capture_stderr_separately: Make output of stdOut and stdErr separately available
     */
    private function initOutput(array $options): void
    {
        $this->captureStreamsIndependently = $options['capture_stderr_separately'] ?? false;
        if (!$this->captureStreamsIndependently) {
            $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']);
            }
        } else {
            $this->output = new ConsoleOutput(
                $options['verbosity'] ?? ConsoleOutput::VERBOSITY_NORMAL,
                $options['decorated'] ?? null
            );

            $errorOutput = new StreamOutput(fopen('php://memory', 'w', false));
            $errorOutput->setFormatter($this->output->getFormatter());
            $errorOutput->setVerbosity($this->output->getVerbosity());
            $errorOutput->setDecorated($this->output->isDecorated());

            $reflectedOutput = new \ReflectionObject($this->output);
            $strErrProperty = $reflectedOutput->getProperty('stderr');
            $strErrProperty->setValue($this->output, $errorOutput);

            $reflectedParent = $reflectedOutput->getParentClass();
            $streamProperty = $reflectedParent->getProperty('stream');
            $streamProperty->setValue($this->output, fopen('php://memory', 'w', false));
        }
    }

    /**
     * @return resource
     */
    private static function createStream(array $inputs)
    {
        $stream = fopen('php://memory', 'r+', false);

        foreach ($inputs as $input) {
            fwrite($stream, $input);

            if (!str_ends_with($input, "\x4")) {
                fwrite($stream, \PHP_EOL);
            }
        }

        rewind($stream);

        return $stream;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Output;

/**
 * ConsoleOutputInterface is the interface implemented by ConsoleOutput class.
 * This adds information about stderr and section output stream.
 *
 * @author Dariusz Górecki <darek.krk@gmail.com>
 */
interface ConsoleOutputInterface extends OutputInterface
{
    /**
     * Gets the OutputInterface for errors.
     */
    public function getErrorOutput(): OutputInterface;

    /**
     * @return void
     */
    public function setErrorOutput(OutputInterface $error);

    public function section(): ConsoleSectionOutput;
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Output;

use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Formatter\OutputFormatterInterface;

/**
 * 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 <fabien@symfony.com>
 */
class StreamOutput extends Output
{
    /** @var resource */
    private $stream;

    /**
     * @param resource                      $stream    A stream resource
     * @param int                           $verbosity The verbosity level (one of the VERBOSITY constants in OutputInterface)
     * @param bool|null                     $decorated Whether to decorate messages (null for auto-guessing)
     * @param OutputFormatterInterface|null $formatter Output formatter instance (null to use default OutputFormatter)
     *
     * @throws InvalidArgumentException When first argument is not a real stream
     */
    public function __construct($stream, int $verbosity = self::VERBOSITY_NORMAL, ?bool $decorated = null, ?OutputFormatterInterface $formatter = 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;

        $decorated ??= $this->hasColorSupport();

        parent::__construct($verbosity, $decorated, $formatter);
    }

    /**
     * Gets the stream attached to this StreamOutput instance.
     *
     * @return resource
     */
    public function getStream()
    {
        return $this->stream;
    }

    /**
     * @return void
     */
    protected function doWrite(string $message, bool $newline)
    {
        if ($newline) {
            $message .= \PHP_EOL;
        }

        @fwrite($this->stream, $message);

        fflush($this->stream);
    }

    /**
     * Returns true if the stream supports colorization.
     *
     * Colorization is disabled if not supported by the stream:
     *
     * This is tricky on Windows, because Cygwin, Msys2 etc emulate pseudo
     * terminals via named pipes, so we can only check the environment.
     *
     * Reference: Composer\XdebugHandler\Process::supportsColor
     * https://github.com/composer/xdebug-handler
     *
     * @return bool true if the stream supports colorization, false otherwise
     */
    protected function hasColorSupport(): bool
    {
        // Follow https://no-color.org/
        if ('' !== (($_SERVER['NO_COLOR'] ?? getenv('NO_COLOR'))[0] ?? '')) {
            return false;
        }

        // Detect msysgit/mingw and assume this is a tty because detection
        // does not work correctly, see https://github.com/composer/composer/issues/9690
        if (!@stream_isatty($this->stream) && !\in_array(strtoupper((string) getenv('MSYSTEM')), ['MINGW32', 'MINGW64'], true)) {
            return false;
        }

        if ('\\' === \DIRECTORY_SEPARATOR && @sapi_windows_vt100_support($this->stream)) {
            return true;
        }

        if ('Hyper' === getenv('TERM_PROGRAM')
            || false !== getenv('COLORTERM')
            || false !== getenv('ANSICON')
            || 'ON' === getenv('ConEmuANSI')
        ) {
            return true;
        }

        if ('dumb' === $term = (string) getenv('TERM')) {
            return false;
        }

        // See https://github.com/chalk/supports-color/blob/d4f413efaf8da045c5ab440ed418ef02dbb28bf1/index.js#L157
        return preg_match('/^((screen|xterm|vt100|vt220|putty|rxvt|ansi|cygwin|linux).*)|(.*-256(color)?(-bce)?)$/', $term);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Output;

use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Formatter\OutputFormatterInterface;

/**
 * A BufferedOutput that keeps only the last N chars.
 *
 * @author Jérémy Derussé <jeremy@derusse.com>
 */
class TrimmedBufferOutput extends Output
{
    private int $maxLength;
    private string $buffer = '';

    public function __construct(int $maxLength, ?int $verbosity = self::VERBOSITY_NORMAL, bool $decorated = false, ?OutputFormatterInterface $formatter = null)
    {
        if ($maxLength <= 0) {
            throw new InvalidArgumentException(\sprintf('"%s()" expects a strictly positive maxLength. Got %d.', __METHOD__, $maxLength));
        }

        parent::__construct($verbosity, $decorated, $formatter);
        $this->maxLength = $maxLength;
    }

    /**
     * Empties buffer and returns its content.
     */
    public function fetch(): string
    {
        $content = $this->buffer;
        $this->buffer = '';

        return $content;
    }

    /**
     * @return void
     */
    protected function doWrite(string $message, bool $newline)
    {
        $this->buffer .= $message;

        if ($newline) {
            $this->buffer .= \PHP_EOL;
        }

        $this->buffer = substr($this->buffer, 0 - $this->maxLength);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Output;

use Symfony\Component\Console\Formatter\OutputFormatterInterface;

/**
 * OutputInterface is the interface implemented by all Output classes.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
interface OutputInterface
{
    public const VERBOSITY_QUIET = 16;
    public const VERBOSITY_NORMAL = 32;
    public const VERBOSITY_VERBOSE = 64;
    public const VERBOSITY_VERY_VERBOSE = 128;
    public const VERBOSITY_DEBUG = 256;

    public const OUTPUT_NORMAL = 1;
    public const OUTPUT_RAW = 2;
    public const OUTPUT_PLAIN = 4;

    /**
     * Writes a message to the output.
     *
     * @param bool $newline Whether to add a newline
     * @param int  $options A bitmask of options (one of the OUTPUT or VERBOSITY constants),
     *                      0 is considered the same as self::OUTPUT_NORMAL | self::VERBOSITY_NORMAL
     *
     * @return void
     */
    public function write(string|iterable $messages, bool $newline = false, int $options = 0);

    /**
     * Writes a message to the output and adds a newline at the end.
     *
     * @param int $options A bitmask of options (one of the OUTPUT or VERBOSITY constants),
     *                     0 is considered the same as self::OUTPUT_NORMAL | self::VERBOSITY_NORMAL
     *
     * @return void
     */
    public function writeln(string|iterable $messages, int $options = 0);

    /**
     * Sets the verbosity of the output.
     *
     * @param self::VERBOSITY_* $level
     *
     * @return void
     */
    public function setVerbosity(int $level);

    /**
     * Gets the current verbosity of the output.
     *
     * @return self::VERBOSITY_*
     */
    public function getVerbosity(): int;

    /**
     * Returns whether verbosity is quiet (-q).
     */
    public function isQuiet(): bool;

    /**
     * Returns whether verbosity is verbose (-v).
     */
    public function isVerbose(): bool;

    /**
     * Returns whether verbosity is very verbose (-vv).
     */
    public function isVeryVerbose(): bool;

    /**
     * Returns whether verbosity is debug (-vvv).
     */
    public function isDebug(): bool;

    /**
     * Sets the decorated flag.
     *
     * @return void
     */
    public function setDecorated(bool $decorated);

    /**
     * Gets the decorated flag.
     */
    public function isDecorated(): bool;

    /**
     * @return void
     */
    public function setFormatter(OutputFormatterInterface $formatter);

    /**
     * Returns current output formatter instance.
     */
    public function getFormatter(): OutputFormatterInterface;
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Output;

use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Formatter\OutputFormatterInterface;

/**
 * Base class for output classes.
 *
 * There are five levels of verbosity:
 *
 *  * normal: no option passed (normal output)
 *  * verbose: -v (more output)
 *  * very verbose: -vv (highly extended output)
 *  * debug: -vvv (all debug output)
 *  * quiet: -q (no output)
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
abstract class Output implements OutputInterface
{
    private int $verbosity;
    private OutputFormatterInterface $formatter;

    /**
     * @param int|null                      $verbosity The verbosity level (one of the VERBOSITY constants in OutputInterface)
     * @param bool                          $decorated Whether to decorate messages
     * @param OutputFormatterInterface|null $formatter Output formatter instance (null to use default OutputFormatter)
     */
    public function __construct(?int $verbosity = self::VERBOSITY_NORMAL, bool $decorated = false, ?OutputFormatterInterface $formatter = null)
    {
        $this->verbosity = $verbosity ?? self::VERBOSITY_NORMAL;
        $this->formatter = $formatter ?? new OutputFormatter();
        $this->formatter->setDecorated($decorated);
    }

    /**
     * @return void
     */
    public function setFormatter(OutputFormatterInterface $formatter)
    {
        $this->formatter = $formatter;
    }

    public function getFormatter(): OutputFormatterInterface
    {
        return $this->formatter;
    }

    /**
     * @return void
     */
    public function setDecorated(bool $decorated)
    {
        $this->formatter->setDecorated($decorated);
    }

    public function isDecorated(): bool
    {
        return $this->formatter->isDecorated();
    }

    /**
     * @return void
     */
    public function setVerbosity(int $level)
    {
        $this->verbosity = $level;
    }

    public function getVerbosity(): int
    {
        return $this->verbosity;
    }

    public function isQuiet(): bool
    {
        return self::VERBOSITY_QUIET === $this->verbosity;
    }

    public function isVerbose(): bool
    {
        return self::VERBOSITY_VERBOSE <= $this->verbosity;
    }

    public function isVeryVerbose(): bool
    {
        return self::VERBOSITY_VERY_VERBOSE <= $this->verbosity;
    }

    public function isDebug(): bool
    {
        return self::VERBOSITY_DEBUG <= $this->verbosity;
    }

    /**
     * @return void
     */
    public function writeln(string|iterable $messages, int $options = self::OUTPUT_NORMAL)
    {
        $this->write($messages, true, $options);
    }

    /**
     * @return void
     */
    public function write(string|iterable $messages, bool $newline = false, int $options = self::OUTPUT_NORMAL)
    {
        if (!is_iterable($messages)) {
            $messages = [$messages];
        }

        $types = self::OUTPUT_NORMAL | self::OUTPUT_RAW | self::OUTPUT_PLAIN;
        $type = $types & $options ?: self::OUTPUT_NORMAL;

        $verbosities = self::VERBOSITY_QUIET | self::VERBOSITY_NORMAL | self::VERBOSITY_VERBOSE | self::VERBOSITY_VERY_VERBOSE | self::VERBOSITY_DEBUG;
        $verbosity = $verbosities & $options ?: self::VERBOSITY_NORMAL;

        if ($verbosity > $this->getVerbosity()) {
            return;
        }

        foreach ($messages as $message) {
            switch ($type) {
                case OutputInterface::OUTPUT_NORMAL:
                    $message = $this->formatter->format($message);
                    break;
                case OutputInterface::OUTPUT_RAW:
                    break;
                case OutputInterface::OUTPUT_PLAIN:
                    $message = strip_tags($this->formatter->format($message));
                    break;
            }

            $this->doWrite($message ?? '', $newline);
        }
    }

    /**
     * Writes a message to the output.
     *
     * @return void
     */
    abstract protected function doWrite(string $message, bool $newline);
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Output;

use Symfony\Component\Console\Exception\InvalidArgumentException;

/**
 * @author Fabien Potencier <fabien@symfony.com>
 * @author Julien Boudry <julien@condorcet.vote>
 */
enum AnsiColorMode
{
    /*
     * Classical 4-bit Ansi colors, including 8 classical colors and 8 bright color. Output syntax is "ESC[${foreGroundColorcode};${backGroundColorcode}m"
     * Must be compatible with all terminals and it's the minimal version supported.
     */
    case Ansi4;

    /*
     * 8-bit Ansi colors (240 different colors + 16 duplicate color codes, ensuring backward compatibility).
     * Output syntax is: "ESC[38;5;${foreGroundColorcode};48;5;${backGroundColorcode}m"
     * Should be compatible with most terminals.
     */
    case Ansi8;

    /*
     * 24-bit Ansi colors (RGB).
     * Output syntax is: "ESC[38;2;${foreGroundColorcodeRed};${foreGroundColorcodeGreen};${foreGroundColorcodeBlue};48;2;${backGroundColorcodeRed};${backGroundColorcodeGreen};${backGroundColorcodeBlue}m"
     * May be compatible with many modern terminals.
     */
    case Ansi24;

    /**
     * Converts an RGB hexadecimal color to the corresponding Ansi code.
     */
    public function convertFromHexToAnsiColorCode(string $hexColor): string
    {
        $hexColor = str_replace('#', '', $hexColor);

        if (3 === \strlen($hexColor)) {
            $hexColor = $hexColor[0].$hexColor[0].$hexColor[1].$hexColor[1].$hexColor[2].$hexColor[2];
        }

        if (6 !== \strlen($hexColor)) {
            throw new InvalidArgumentException(\sprintf('Invalid "#%s" color.', $hexColor));
        }

        $color = hexdec($hexColor);

        $r = ($color >> 16) & 255;
        $g = ($color >> 8) & 255;
        $b = $color & 255;

        return match ($this) {
            self::Ansi4 => (string) $this->convertFromRGB($r, $g, $b),
            self::Ansi8 => '8;5;'.((string) $this->convertFromRGB($r, $g, $b)),
            self::Ansi24 => \sprintf('8;2;%d;%d;%d', $r, $g, $b),
        };
    }

    private function convertFromRGB(int $r, int $g, int $b): int
    {
        return match ($this) {
            self::Ansi4 => $this->degradeHexColorToAnsi4($r, $g, $b),
            self::Ansi8 => $this->degradeHexColorToAnsi8($r, $g, $b),
            default => throw new InvalidArgumentException("RGB cannot be converted to {$this->name}."),
        };
    }

    private function degradeHexColorToAnsi4(int $r, int $g, int $b): int
    {
        return round($b / 255) << 2 | (round($g / 255) << 1) | round($r / 255);
    }

    /**
     * Inspired from https://github.com/ajalt/colormath/blob/e464e0da1b014976736cf97250063248fc77b8e7/colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/Ansi256.kt code (MIT license).
     */
    private function degradeHexColorToAnsi8(int $r, int $g, int $b): int
    {
        if ($r === $g && $g === $b) {
            if ($r < 8) {
                return 16;
            }

            if ($r > 248) {
                return 231;
            }

            return (int) round(($r - 8) / 247 * 24) + 232;
        }

        return 16 +
                (36 * (int) round($r / 255 * 5)) +
                (6 * (int) round($g / 255 * 5)) +
                (int) round($b / 255 * 5);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Output;

use Symfony\Component\Console\Formatter\OutputFormatterInterface;

/**
 * ConsoleOutput is the default class for all CLI output. It uses STDOUT and STDERR.
 *
 * This class is a convenient wrapper around `StreamOutput` for both STDOUT and STDERR.
 *
 *     $output = new ConsoleOutput();
 *
 * This is equivalent to:
 *
 *     $output = new StreamOutput(fopen('php://stdout', 'w'));
 *     $stdErr = new StreamOutput(fopen('php://stderr', 'w'));
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class ConsoleOutput extends StreamOutput implements ConsoleOutputInterface
{
    private OutputInterface $stderr;
    private array $consoleSectionOutputs = [];

    /**
     * @param int                           $verbosity The verbosity level (one of the VERBOSITY constants in OutputInterface)
     * @param bool|null                     $decorated Whether to decorate messages (null for auto-guessing)
     * @param OutputFormatterInterface|null $formatter Output formatter instance (null to use default OutputFormatter)
     */
    public function __construct(int $verbosity = self::VERBOSITY_NORMAL, ?bool $decorated = null, ?OutputFormatterInterface $formatter = null)
    {
        parent::__construct($this->openOutputStream(), $verbosity, $decorated, $formatter);

        // stderr gets its own formatter: decoration is held by the formatter, so a shared one
        // would force both streams to the same state instead of detecting each independently
        $this->stderr = new StreamOutput($this->openErrorStream(), $verbosity, $decorated, $formatter ? clone $formatter : null);
    }

    /**
     * Creates a new output section.
     */
    public function section(): ConsoleSectionOutput
    {
        return new ConsoleSectionOutput($this->getStream(), $this->consoleSectionOutputs, $this->getVerbosity(), $this->isDecorated(), $this->getFormatter());
    }

    /**
     * @return void
     */
    public function setDecorated(bool $decorated)
    {
        parent::setDecorated($decorated);
        $this->stderr->setDecorated($decorated);
    }

    /**
     * @return void
     */
    public function setFormatter(OutputFormatterInterface $formatter)
    {
        parent::setFormatter($formatter);
        $this->stderr->setFormatter($formatter);
    }

    /**
     * @return void
     */
    public function setVerbosity(int $level)
    {
        parent::setVerbosity($level);
        $this->stderr->setVerbosity($level);
    }

    public function getErrorOutput(): OutputInterface
    {
        return $this->stderr;
    }

    /**
     * @return void
     */
    public function setErrorOutput(OutputInterface $error)
    {
        $this->stderr = $error;
    }

    /**
     * Returns true if current environment supports writing console output to
     * STDOUT.
     */
    protected function hasStdoutSupport(): bool
    {
        return false === $this->isRunningOS400();
    }

    /**
     * Returns true if current environment supports writing console output to
     * STDERR.
     */
    protected function hasStderrSupport(): bool
    {
        return false === $this->isRunningOS400();
    }

    /**
     * Checks if current executing environment is IBM iSeries (OS400), which
     * doesn't properly convert character-encodings between ASCII to EBCDIC.
     */
    private function isRunningOS400(): bool
    {
        $checks = [
            \function_exists('php_uname') ? php_uname('s') : '',
            getenv('OSTYPE'),
            \PHP_OS,
        ];

        return false !== stripos(implode(';', $checks), 'OS400');
    }

    /**
     * @return resource
     */
    private function openOutputStream()
    {
        static $stdout;

        if ($stdout) {
            return $stdout;
        }

        if (!$this->hasStdoutSupport()) {
            return $stdout = fopen('php://output', 'w');
        }

        // Use STDOUT when possible to prevent from opening too many file descriptors
        if (!\defined('STDOUT')) {
            return $stdout = @fopen('php://stdout', 'w') ?: fopen('php://output', 'w');
        }

        // On Windows, STDOUT is opened in text mode; reopen in binary mode to prevent \n to \r\n conversion
        if ('\\' === \DIRECTORY_SEPARATOR) {
            return $stdout = @fopen('php://stdout', 'w') ?: \STDOUT;
        }

        return $stdout = \STDOUT;
    }

    /**
     * @return resource
     */
    private function openErrorStream()
    {
        static $stderr;

        if ($stderr) {
            return $stderr;
        }

        if (!$this->hasStderrSupport()) {
            return $stderr = fopen('php://output', 'w');
        }

        // Use STDERR when possible to prevent from opening too many file descriptors
        if (!\defined('STDERR')) {
            return $stderr = @fopen('php://stderr', 'w') ?: fopen('php://output', 'w');
        }

        // On Windows, STDERR is opened in text mode; reopen in binary mode to prevent \n → \r\n conversion
        if ('\\' === \DIRECTORY_SEPARATOR) {
            return $stderr = @fopen('php://stderr', 'w') ?: \STDERR;
        }

        return $stderr ??= \STDERR;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Output;

/**
 * @author Jean-François Simon <contact@jfsimon.fr>
 */
class BufferedOutput extends Output
{
    private string $buffer = '';

    /**
     * Empties buffer and returns its content.
     */
    public function fetch(): string
    {
        $content = $this->buffer;
        $this->buffer = '';

        return $content;
    }

    /**
     * @return void
     */
    protected function doWrite(string $message, bool $newline)
    {
        $this->buffer .= $message;

        if ($newline) {
            $this->buffer .= \PHP_EOL;
        }
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Output;

use Symfony\Component\Console\Formatter\NullOutputFormatter;
use Symfony\Component\Console\Formatter\OutputFormatterInterface;

/**
 * NullOutput suppresses all output.
 *
 *     $output = new NullOutput();
 *
 * @author Fabien Potencier <fabien@symfony.com>
 * @author Tobias Schultze <http://tobion.de>
 */
class NullOutput implements OutputInterface
{
    private NullOutputFormatter $formatter;

    /**
     * @return void
     */
    public function setFormatter(OutputFormatterInterface $formatter)
    {
        // do nothing
    }

    public function getFormatter(): OutputFormatterInterface
    {
        // to comply with the interface we must return a OutputFormatterInterface
        return $this->formatter ??= new NullOutputFormatter();
    }

    /**
     * @return void
     */
    public function setDecorated(bool $decorated)
    {
        // do nothing
    }

    public function isDecorated(): bool
    {
        return false;
    }

    /**
     * @return void
     */
    public function setVerbosity(int $level)
    {
        // do nothing
    }

    public function getVerbosity(): int
    {
        return self::VERBOSITY_QUIET;
    }

    public function isQuiet(): bool
    {
        return true;
    }

    public function isVerbose(): bool
    {
        return false;
    }

    public function isVeryVerbose(): bool
    {
        return false;
    }

    public function isDebug(): bool
    {
        return false;
    }

    /**
     * @return void
     */
    public function writeln(string|iterable $messages, int $options = self::OUTPUT_NORMAL)
    {
        // do nothing
    }

    /**
     * @return void
     */
    public function write(string|iterable $messages, bool $newline = false, int $options = self::OUTPUT_NORMAL)
    {
        // do nothing
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Output;

use Symfony\Component\Console\Formatter\OutputFormatterInterface;
use Symfony\Component\Console\Helper\Helper;
use Symfony\Component\Console\Terminal;

/**
 * @author Pierre du Plessis <pdples@gmail.com>
 * @author Gabriel Ostrolucký <gabriel.ostrolucky@gmail.com>
 */
class ConsoleSectionOutput extends StreamOutput
{
    private array $content = [];
    private int $lines = 0;
    private array $sections;
    private Terminal $terminal;
    private int $maxHeight = 0;

    /**
     * @param resource               $stream
     * @param ConsoleSectionOutput[] $sections
     */
    public function __construct($stream, array &$sections, int $verbosity, bool $decorated, OutputFormatterInterface $formatter)
    {
        parent::__construct($stream, $verbosity, $decorated, $formatter);
        array_unshift($sections, $this);
        $this->sections = &$sections;
        $this->terminal = new Terminal();
    }

    /**
     * Defines a maximum number of lines for this section.
     *
     * When more lines are added, the section will automatically scroll to the
     * end (i.e. remove the first lines to comply with the max height).
     */
    public function setMaxHeight(int $maxHeight): void
    {
        // when changing max height, clear output of current section and redraw again with the new height
        $previousMaxHeight = $this->maxHeight;
        $this->maxHeight = $maxHeight;
        $existingContent = $this->popStreamContentUntilCurrentSection($previousMaxHeight ? min($previousMaxHeight, $this->lines) : $this->lines);

        parent::doWrite($this->getVisibleContent(), false);
        parent::doWrite($existingContent, false);
    }

    /**
     * Clears previous output for this section.
     *
     * @param int $lines Number of lines to clear. If null, then the entire output of this section is cleared
     *
     * @return void
     */
    public function clear(?int $lines = null)
    {
        if (empty($this->content) || !$this->isDecorated()) {
            return;
        }

        if ($lines) {
            array_splice($this->content, -$lines);
        } else {
            $lines = $this->lines;
            $this->content = [];
        }

        $this->lines -= $lines;

        parent::doWrite($this->popStreamContentUntilCurrentSection($this->maxHeight ? min($this->maxHeight, $lines) : $lines), false);
    }

    /**
     * Overwrites the previous output with a new message.
     *
     * @return void
     */
    public function overwrite(string|iterable $message)
    {
        if (!$this->content || !$this->isDecorated()) {
            $this->writeln($message);

            return;
        }

        // Replace own content and write everything in a single cursor-up + erase
        // pass, to avoid the flicker (and the line-eating artifacts on some
        // terminals) caused by calling clear() then writeln() back-to-back.
        $linesCleared = $this->lines;
        $this->content = [];
        $this->lines = 0;

        if (!is_iterable($message)) {
            $message = [$message];
        }

        foreach ($message as $line) {
            $this->addContent($this->getFormatter()->format($line) ?? '', true);
        }

        $erasedContent = $this->popStreamContentUntilCurrentSection($this->maxHeight ? min($this->maxHeight, $linesCleared) : $linesCleared);

        parent::doWrite($this->getVisibleContent(), false);
        parent::doWrite($erasedContent, false);
    }

    public function getContent(): string
    {
        return implode('', $this->content);
    }

    public function getVisibleContent(): string
    {
        if (0 === $this->maxHeight) {
            return $this->getContent();
        }

        return implode('', \array_slice($this->content, -$this->maxHeight));
    }

    /**
     * @internal
     */
    public function addContent(string $input, bool $newline = true): int
    {
        $width = $this->terminal->getWidth();
        $lines = explode(\PHP_EOL, $input);
        $linesAdded = 0;
        $count = \count($lines) - 1;
        foreach ($lines as $i => $lineContent) {
            // re-add the line break (that has been removed in the above `explode()` for
            // - every line that is not the last line
            // - if $newline is required, also add it to the last line
            if ($i < $count || $newline) {
                $lineContent .= \PHP_EOL;
            }

            // skip line if there is no text (or newline for that matter)
            if ('' === $lineContent) {
                continue;
            }

            // For the first line, check if the previous line (last entry of `$this->content`)
            // needs to be continued (i.e. does not end with a line break).
            if (0 === $i
                && (false !== $lastLine = end($this->content))
                && !str_ends_with($lastLine, \PHP_EOL)
            ) {
                // deduct the line count of the previous line
                $this->lines -= (int) ceil($this->getDisplayLength($lastLine) / $width) ?: 1;
                // concatenate previous and new line
                $lineContent = $lastLine.$lineContent;
                // replace last entry of `$this->content` with the new expanded line
                array_splice($this->content, -1, 1, $lineContent);
            } else {
                // otherwise just add the new content
                $this->content[] = $lineContent;
            }

            $linesAdded += (int) ceil($this->getDisplayLength($lineContent) / $width) ?: 1;
        }

        $this->lines += $linesAdded;

        return $linesAdded;
    }

    /**
     * @internal
     */
    public function addNewLineOfInputSubmit(): void
    {
        $this->content[] = \PHP_EOL;
        ++$this->lines;
    }

    /**
     * @return void
     */
    protected function doWrite(string $message, bool $newline)
    {
        // Simulate newline behavior for consistent output formatting, avoiding extra logic
        if (!$newline && str_ends_with($message, \PHP_EOL)) {
            $message = substr($message, 0, -\strlen(\PHP_EOL));
            $newline = true;
        }

        if (!$this->isDecorated()) {
            parent::doWrite($message, $newline);

            return;
        }

        // Check if the previous line (last entry of `$this->content`) needs to be continued
        // (i.e. does not end with a line break). In which case, it needs to be erased first.
        $linesToClear = $deleteLastLine = ($lastLine = end($this->content) ?: '') && !str_ends_with($lastLine, \PHP_EOL) ? 1 : 0;

        $linesAdded = $this->addContent($message, $newline);

        if ($lineOverflow = $this->maxHeight > 0 && $this->lines > $this->maxHeight) {
            // on overflow, clear the whole section and redraw again (to remove the first lines)
            $linesToClear = $this->maxHeight;
        }

        $erasedContent = $this->popStreamContentUntilCurrentSection($linesToClear);

        if ($lineOverflow) {
            // redraw existing lines of the section
            $previousLinesOfSection = \array_slice($this->content, $this->lines - $this->maxHeight, $this->maxHeight - $linesAdded);
            parent::doWrite(implode('', $previousLinesOfSection), false);
        }

        // if the last line was removed, re-print its content together with the new content.
        // otherwise, just print the new content.
        parent::doWrite($deleteLastLine ? $lastLine.$message : $message, true);
        parent::doWrite($erasedContent, false);
    }

    /**
     * At initial stage, cursor is at the end of stream output. This method makes cursor crawl upwards until it hits
     * current section. Then it erases content it crawled through. Optionally, it erases part of current section too.
     */
    private function popStreamContentUntilCurrentSection(int $numberOfLinesToClearFromCurrentSection = 0): string
    {
        $numberOfLinesToClear = $numberOfLinesToClearFromCurrentSection;
        $erasedContent = [];

        foreach ($this->sections as $section) {
            if ($section === $this) {
                break;
            }

            $numberOfLinesToClear += $section->maxHeight ? min($section->lines, $section->maxHeight) : $section->lines;
            if ('' !== $sectionContent = $section->getVisibleContent()) {
                if (!str_ends_with($sectionContent, \PHP_EOL)) {
                    $sectionContent .= \PHP_EOL;
                }
                $erasedContent[] = $sectionContent;
            }
        }

        if ($numberOfLinesToClear > 0) {
            // move cursor up n lines
            parent::doWrite(\sprintf("\x1b[%dA", $numberOfLinesToClear), false);
            // erase to end of screen
            parent::doWrite("\x1b[0J", false);
        }

        return implode('', array_reverse($erasedContent));
    }

    private function getDisplayLength(string $text): int
    {
        return Helper::width(Helper::removeDecoration($this->getFormatter(), str_replace("\t", '        ', $text)));
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Command;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Completion\Output\BashCompletionOutput;
use Symfony\Component\Console\Completion\Output\CompletionOutputInterface;
use Symfony\Component\Console\Completion\Output\FishCompletionOutput;
use Symfony\Component\Console\Completion\Output\ZshCompletionOutput;
use Symfony\Component\Console\Exception\CommandNotFoundException;
use Symfony\Component\Console\Exception\ExceptionInterface;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

/**
 * Responsible for providing the values to the shell completion.
 *
 * @author Wouter de Jong <wouter@wouterj.nl>
 */
#[AsCommand(name: '|_complete', description: 'Internal command to provide shell completion suggestions')]
final class CompleteCommand extends Command
{
    public const COMPLETION_API_VERSION = '1';

    /**
     * @deprecated since Symfony 6.1
     */
    protected static $defaultName = '|_complete';

    /**
     * @deprecated since Symfony 6.1
     */
    protected static $defaultDescription = 'Internal command to provide shell completion suggestions';

    private array $completionOutputs;

    private bool $isDebug = false;

    /**
     * @param array<string, class-string<CompletionOutputInterface>> $completionOutputs A list of additional completion outputs, with shell name as key and FQCN as value
     */
    public function __construct(array $completionOutputs = [])
    {
        // must be set before the parent constructor, as the property value is used in configure()
        $this->completionOutputs = $completionOutputs + [
            'bash' => BashCompletionOutput::class,
            'fish' => FishCompletionOutput::class,
            'zsh' => ZshCompletionOutput::class,
        ];

        parent::__construct();
    }

    protected function configure(): void
    {
        $this
            ->addOption('shell', 's', InputOption::VALUE_REQUIRED, 'The shell type ("'.implode('", "', array_keys($this->completionOutputs)).'")')
            ->addOption('input', 'i', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'An array of input tokens (e.g. COMP_WORDS or argv)')
            ->addOption('current', 'c', InputOption::VALUE_REQUIRED, 'The index of the "input" array that the cursor is in (e.g. COMP_CWORD)')
            ->addOption('api-version', 'a', InputOption::VALUE_REQUIRED, 'The API version of the completion script')
            ->addOption('symfony', 'S', InputOption::VALUE_REQUIRED, 'deprecated')
        ;
    }

    protected function initialize(InputInterface $input, OutputInterface $output): void
    {
        $this->isDebug = filter_var(getenv('SYMFONY_COMPLETION_DEBUG'), \FILTER_VALIDATE_BOOL);
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        try {
            // "symfony" must be kept for compat with the shell scripts generated by Symfony Console 5.4 - 6.1
            $version = $input->getOption('symfony') ? '1' : $input->getOption('api-version');
            if ($version && version_compare($version, self::COMPLETION_API_VERSION, '<')) {
                $message = \sprintf('Completion script version is not supported ("%s" given, ">=%s" required).', $version, self::COMPLETION_API_VERSION);
                $this->log($message);

                $output->writeln($message.' Install the Symfony completion script again by using the "completion" command.');

                return 126;
            }

            $shell = $input->getOption('shell');
            if (!$shell) {
                throw new \RuntimeException('The "--shell" option must be set.');
            }

            if (!$completionOutput = $this->completionOutputs[$shell] ?? false) {
                throw new \RuntimeException(\sprintf('Shell completion is not supported for your shell: "%s" (supported: "%s").', $shell, implode('", "', array_keys($this->completionOutputs))));
            }

            $completionInput = $this->createCompletionInput($input);
            $suggestions = new CompletionSuggestions();

            $this->log([
                '',
                '<comment>'.date('Y-m-d H:i:s').'</>',
                '<info>Input:</> <comment>("|" indicates the cursor position)</>',
                '  '.(string) $completionInput,
                '<info>Command:</>',
                '  '.(string) implode(' ', $_SERVER['argv']),
                '<info>Messages:</>',
            ]);

            if ($command = $this->findCommand($completionInput, $output)) {
                $command->mergeApplicationDefinition();
                $completionInput->bind($command->getDefinition());
            }

            if (null === $command) {
                $this->log('  No command found, completing using the Application class.');

                $this->getApplication()->complete($completionInput, $suggestions);
            } elseif (
                $completionInput->mustSuggestArgumentValuesFor('command')
            ) {
                $this->log('  Command found, completing command name.');

                // expand shortcut names ("cache:cl<TAB>") into their full name ("cache:clear")
                $commandNames = array_filter(array_merge([$command->getName()], $command->getAliases()));
                foreach ($commandNames as $name) {
                    if (str_starts_with($name, $completionInput->getCompletionValue())) {
                        $commandNames = [$name];
                        break;
                    }
                }
                $suggestions->suggestValues($commandNames);
            } else {
                if (CompletionInput::TYPE_OPTION_NAME === $completionInput->getCompletionType()) {
                    $this->log('  Completing option names for the <comment>'.($command instanceof LazyCommand ? $command->getCommand() : $command)::class.'</> command.');

                    $suggestions->suggestOptions($command->getDefinition()->getOptions());
                } else {
                    $this->log([
                        '  Completing using the <comment>'.($command instanceof LazyCommand ? $command->getCommand() : $command)::class.'</> class.',
                        '  Completing <comment>'.$completionInput->getCompletionType().'</> for <comment>'.$completionInput->getCompletionName().'</>',
                    ]);
                    if (null !== $compval = $completionInput->getCompletionValue()) {
                        $this->log('  Current value: <comment>'.$compval.'</>');
                    }

                    $command->complete($completionInput, $suggestions);
                }
            }

            /** @var CompletionOutputInterface $completionOutput */
            $completionOutput = new $completionOutput();

            $this->log('<info>Suggestions:</>');
            if ($options = $suggestions->getOptionSuggestions()) {
                $this->log('  --'.implode(' --', array_map(static fn ($o) => $o->getName(), $options)));
            } elseif ($values = $suggestions->getValueSuggestions()) {
                $this->log('  '.implode(' ', $values));
            } else {
                $this->log('  <comment>No suggestions were provided</>');
            }

            $completionOutput->write($suggestions, $output);
        } catch (\Throwable $e) {
            $this->log([
                '<error>Error!</error>',
                (string) $e,
            ]);

            if ($output->isDebug()) {
                throw $e;
            }

            return 2;
        }

        return 0;
    }

    private function createCompletionInput(InputInterface $input): CompletionInput
    {
        $currentIndex = $input->getOption('current');
        if (!$currentIndex || !ctype_digit($currentIndex)) {
            throw new \RuntimeException('The "--current" option must be set and it must be an integer.');
        }

        $completionInput = CompletionInput::fromTokens($input->getOption('input'), (int) $currentIndex);

        try {
            $completionInput->bind($this->getApplication()->getDefinition());
        } catch (ExceptionInterface) {
        }

        return $completionInput;
    }

    private function findCommand(CompletionInput $completionInput, OutputInterface $output): ?Command
    {
        try {
            $inputName = $completionInput->getFirstArgument();
            if (null === $inputName) {
                return null;
            }

            return $this->getApplication()->find($inputName);
        } catch (CommandNotFoundException) {
        }

        return null;
    }

    private function log($messages): void
    {
        if (!$this->isDebug) {
            return;
        }

        $commandName = basename($_SERVER['argv'][0]);
        file_put_contents(sys_get_temp_dir().'/sf_'.$commandName.'.log', implode(\PHP_EOL, (array) $messages).\PHP_EOL, \FILE_APPEND);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Command;

use Symfony\Component\Console\Application;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Completion\Suggestion;
use Symfony\Component\Console\Exception\ExceptionInterface;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Console\Helper\HelperInterface;
use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

/**
 * Base class for all commands.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class Command
{
    // see https://tldp.org/LDP/abs/html/exitcodes.html
    public const SUCCESS = 0;
    public const FAILURE = 1;
    public const INVALID = 2;

    /**
     * @var string|null The default command name
     *
     * @deprecated since Symfony 6.1, use the AsCommand attribute instead
     */
    protected static $defaultName;

    /**
     * @var string|null The default command description
     *
     * @deprecated since Symfony 6.1, use the AsCommand attribute instead
     */
    protected static $defaultDescription;

    private ?Application $application = null;
    private ?string $name = null;
    private ?string $processTitle = null;
    private array $aliases = [];
    private InputDefinition $definition;
    private bool $hidden = false;
    private string $help = '';
    private string $description = '';
    private ?InputDefinition $fullDefinition = null;
    private bool $ignoreValidationErrors = false;
    private ?\Closure $code = null;
    private array $synopsis = [];
    private array $usages = [];
    private ?HelperSet $helperSet = null;

    public static function getDefaultName(): ?string
    {
        $class = static::class;

        if ($attribute = (new \ReflectionClass($class))->getAttributes(AsCommand::class)) {
            return $attribute[0]->newInstance()->name;
        }

        $r = new \ReflectionProperty($class, 'defaultName');

        if ($class !== $r->class || null === static::$defaultName) {
            return null;
        }

        trigger_deprecation('symfony/console', '6.1', 'Relying on the static property "$defaultName" for setting a command name is deprecated. Add the "%s" attribute to the "%s" class instead.', AsCommand::class, static::class);

        return static::$defaultName;
    }

    public static function getDefaultDescription(): ?string
    {
        $class = static::class;

        if ($attribute = (new \ReflectionClass($class))->getAttributes(AsCommand::class)) {
            return $attribute[0]->newInstance()->description;
        }

        $r = new \ReflectionProperty($class, 'defaultDescription');

        if ($class !== $r->class || null === static::$defaultDescription) {
            return null;
        }

        trigger_deprecation('symfony/console', '6.1', 'Relying on the static property "$defaultDescription" for setting a command description is deprecated. Add the "%s" attribute to the "%s" class instead.', AsCommand::class, static::class);

        return static::$defaultDescription;
    }

    /**
     * @param string|null $name The name of the command; passing null means it must be set in configure()
     *
     * @throws LogicException When the command name is empty
     */
    public function __construct(?string $name = null)
    {
        $this->definition = new InputDefinition();

        if (null === $name && null !== $name = static::getDefaultName()) {
            $aliases = explode('|', $name);

            if ('' === $name = array_shift($aliases)) {
                $this->setHidden(true);
                $name = array_shift($aliases);
            }

            $this->setAliases($aliases);
        }

        if (null !== $name) {
            $this->setName($name);
        }

        if ('' === $this->description) {
            $this->setDescription(static::getDefaultDescription() ?? '');
        }

        $this->configure();
    }

    /**
     * Ignores validation errors.
     *
     * This is mainly useful for the help command.
     *
     * @return void
     */
    public function ignoreValidationErrors()
    {
        $this->ignoreValidationErrors = true;
    }

    /**
     * @return void
     */
    public function setApplication(?Application $application = null)
    {
        if (1 > \func_num_args()) {
            trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__);
        }
        $this->application = $application;
        if ($application) {
            $this->setHelperSet($application->getHelperSet());
        } else {
            $this->helperSet = null;
        }

        $this->fullDefinition = null;
    }

    /**
     * @return void
     */
    public function setHelperSet(HelperSet $helperSet)
    {
        $this->helperSet = $helperSet;
    }

    /**
     * Gets the helper set.
     */
    public function getHelperSet(): ?HelperSet
    {
        return $this->helperSet;
    }

    /**
     * Gets the application instance for this command.
     */
    public function getApplication(): ?Application
    {
        return $this->application;
    }

    /**
     * Checks whether the command is enabled or not in the current environment.
     *
     * Override this to check for x or y and return false if the command cannot
     * run properly under the current conditions.
     *
     * @return bool
     */
    public function isEnabled()
    {
        return true;
    }

    /**
     * Configures the current command.
     *
     * @return void
     */
    protected function configure()
    {
    }

    /**
     * Executes the current command.
     *
     * This method is not abstract because you can use this class
     * as a concrete class. In this case, instead of defining the
     * execute() method, you set the code to execute by passing
     * a Closure to the setCode() method.
     *
     * @return int 0 if everything went fine, or an exit code
     *
     * @throws LogicException When this abstract method is not implemented
     *
     * @see setCode()
     */
    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.
     *
     * This method is executed before the InputDefinition is validated.
     * This means that this is the only place where the command can
     * interactively ask for values of missing required arguments.
     *
     * @return void
     */
    protected function interact(InputInterface $input, OutputInterface $output)
    {
    }

    /**
     * Initializes the command after the input has been bound and before the input
     * is 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.
     *
     * @see InputInterface::bind()
     * @see InputInterface::validate()
     *
     * @return void
     */
    protected function initialize(InputInterface $input, OutputInterface $output)
    {
    }

    /**
     * Runs the command.
     *
     * The code to execute is either defined directly with the
     * setCode() method or by overriding the execute() method
     * in a sub-class.
     *
     * @return int The command exit code
     *
     * @throws ExceptionInterface When input binding fails. Bypass this by calling {@link ignoreValidationErrors()}.
     *
     * @see setCode()
     * @see execute()
     */
    public function run(InputInterface $input, OutputInterface $output): int
    {
        // add the application arguments and options
        $this->mergeApplicationDefinition();

        // bind the input against the command specific arguments/options
        try {
            $input->bind($this->getDefinition());
        } catch (ExceptionInterface $e) {
            if (!$this->ignoreValidationErrors) {
                throw $e;
            }
        }

        $this->initialize($input, $output);

        if (null !== $this->processTitle) {
            if (\function_exists('cli_set_process_title')) {
                if (!@cli_set_process_title($this->processTitle)) {
                    if ('Darwin' === \PHP_OS) {
                        $output->writeln('<comment>Running "cli_set_process_title" as an unprivileged user is not supported on MacOS.</comment>', OutputInterface::VERBOSITY_VERY_VERBOSE);
                    } else {
                        cli_set_process_title($this->processTitle);
                    }
                }
            } elseif (\function_exists('setproctitle')) {
                setproctitle($this->processTitle);
            } elseif (OutputInterface::VERBOSITY_VERY_VERBOSE === $output->getVerbosity()) {
                $output->writeln('<comment>Install the proctitle PECL to be able to change the process title.</comment>');
            }
        }

        if ($input->isInteractive()) {
            $this->interact($input, $output);
        }

        // The command name argument is often omitted when a command is executed directly with its run() method.
        // It would fail the validation if we didn't make sure the command argument is present,
        // since it's required by the application.
        if ($input->hasArgument('command') && null === $input->getArgument('command')) {
            $input->setArgument('command', $this->getName());
        }

        $input->validate();

        if ($this->code) {
            $statusCode = ($this->code)($input, $output);
        } else {
            $statusCode = $this->execute($input, $output);

            if (!\is_int($statusCode)) {
                throw new \TypeError(\sprintf('Return value of "%s::execute()" must be of the type int, "%s" returned.', static::class, get_debug_type($statusCode)));
            }
        }

        return is_numeric($statusCode) ? (int) $statusCode : 0;
    }

    /**
     * Adds suggestions to $suggestions for the current completion input (e.g. option or argument).
     */
    public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
    {
        $definition = $this->getDefinition();
        if (CompletionInput::TYPE_OPTION_VALUE === $input->getCompletionType() && $definition->hasOption($input->getCompletionName())) {
            $definition->getOption($input->getCompletionName())->complete($input, $suggestions);
        } elseif (CompletionInput::TYPE_ARGUMENT_VALUE === $input->getCompletionType() && $definition->hasArgument($input->getCompletionName())) {
            $definition->getArgument($input->getCompletionName())->complete($input, $suggestions);
        }
    }

    /**
     * Sets the code to execute when running this command.
     *
     * If this method is used, it overrides the code defined
     * in the execute() method.
     *
     * @param callable $code A callable(InputInterface $input, OutputInterface $output)
     *
     * @return $this
     *
     * @throws InvalidArgumentException
     *
     * @see execute()
     */
    public function setCode(callable $code): static
    {
        if ($code instanceof \Closure) {
            $r = new \ReflectionFunction($code);
            if (null === $r->getClosureThis()) {
                set_error_handler(static function () {});
                try {
                    if ($c = \Closure::bind($code, $this)) {
                        $code = $c;
                    }
                } finally {
                    restore_error_handler();
                }
            }
        } else {
            $code = $code(...);
        }

        $this->code = $code;

        return $this;
    }

    /**
     * Merges the application definition with the command definition.
     *
     * This method is not part of public API and should not be used directly.
     *
     * @param bool $mergeArgs Whether to merge or not the Application definition arguments to Command definition arguments
     *
     * @internal
     */
    public function mergeApplicationDefinition(bool $mergeArgs = true): void
    {
        if (null === $this->application) {
            return;
        }

        $this->fullDefinition = new InputDefinition();
        $this->fullDefinition->setOptions($this->definition->getOptions());
        $this->fullDefinition->addOptions($this->application->getDefinition()->getOptions());

        if ($mergeArgs) {
            $this->fullDefinition->setArguments($this->application->getDefinition()->getArguments());
            $this->fullDefinition->addArguments($this->definition->getArguments());
        } else {
            $this->fullDefinition->setArguments($this->definition->getArguments());
        }
    }

    /**
     * Sets an array of argument and option instances.
     *
     * @return $this
     */
    public function setDefinition(array|InputDefinition $definition): static
    {
        if ($definition instanceof InputDefinition) {
            $this->definition = $definition;
        } else {
            $this->definition->setDefinition($definition);
        }

        $this->fullDefinition = null;

        return $this;
    }

    /**
     * Gets the InputDefinition attached to this Command.
     */
    public function getDefinition(): InputDefinition
    {
        return $this->fullDefinition ?? $this->getNativeDefinition();
    }

    /**
     * Gets the InputDefinition to be used to create representations of this Command.
     *
     * Can be overridden to provide the original command representation when it would otherwise
     * be changed by merging with the application InputDefinition.
     *
     * This method is not part of public API and should not be used directly.
     */
    public function getNativeDefinition(): InputDefinition
    {
        return $this->definition ?? throw new LogicException(\sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', static::class));
    }

    /**
     * Adds an argument.
     *
     * @param $mode    The argument mode: InputArgument::REQUIRED or InputArgument::OPTIONAL
     * @param $default The default value (for InputArgument::OPTIONAL mode only)
     * @param array|\Closure(CompletionInput,CompletionSuggestions):list<string|Suggestion> $suggestedValues The values used for input completion
     *
     * @return $this
     *
     * @throws InvalidArgumentException When argument mode is not valid
     */
    public function addArgument(string $name, ?int $mode = null, string $description = '', mixed $default = null /* array|\Closure $suggestedValues = null */): static
    {
        $suggestedValues = 5 <= \func_num_args() ? func_get_arg(4) : [];
        if (!\is_array($suggestedValues) && !$suggestedValues instanceof \Closure) {
            throw new \TypeError(\sprintf('Argument 5 passed to "%s()" must be array or \Closure, "%s" given.', __METHOD__, get_debug_type($suggestedValues)));
        }
        $this->definition->addArgument(new InputArgument($name, $mode, $description, $default, $suggestedValues));
        $this->fullDefinition?->addArgument(new InputArgument($name, $mode, $description, $default, $suggestedValues));

        return $this;
    }

    /**
     * Adds an option.
     *
     * @param $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts
     * @param $mode     The option mode: One of the InputOption::VALUE_* constants
     * @param $default  The default value (must be null for InputOption::VALUE_NONE)
     * @param array|\Closure(CompletionInput,CompletionSuggestions):list<string|Suggestion> $suggestedValues The values used for input completion
     *
     * @return $this
     *
     * @throws InvalidArgumentException If option mode is invalid or incompatible
     */
    public function addOption(string $name, string|array|null $shortcut = null, ?int $mode = null, string $description = '', mixed $default = null /* array|\Closure $suggestedValues = [] */): static
    {
        $suggestedValues = 6 <= \func_num_args() ? func_get_arg(5) : [];
        if (!\is_array($suggestedValues) && !$suggestedValues instanceof \Closure) {
            throw new \TypeError(\sprintf('Argument 5 passed to "%s()" must be array or \Closure, "%s" given.', __METHOD__, get_debug_type($suggestedValues)));
        }
        $this->definition->addOption(new InputOption($name, $shortcut, $mode, $description, $default, $suggestedValues));
        $this->fullDefinition?->addOption(new InputOption($name, $shortcut, $mode, $description, $default, $suggestedValues));

        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');
     *
     * @return $this
     *
     * @throws InvalidArgumentException When the name is invalid
     */
    public function setName(string $name): static
    {
        $this->validateName($name);

        $this->name = $name;

        return $this;
    }

    /**
     * Sets the process title of the command.
     *
     * This feature should be used only when creating a long process command,
     * like a daemon.
     *
     * @return $this
     */
    public function setProcessTitle(string $title): static
    {
        $this->processTitle = $title;

        return $this;
    }

    /**
     * Returns the command name.
     */
    public function getName(): ?string
    {
        return $this->name;
    }

    /**
     * @param bool $hidden Whether or not the command should be hidden from the list of commands
     *
     * @return $this
     */
    public function setHidden(bool $hidden = true): static
    {
        $this->hidden = $hidden;

        return $this;
    }

    /**
     * @return bool whether the command should be publicly shown or not
     */
    public function isHidden(): bool
    {
        return $this->hidden;
    }

    /**
     * Sets the description for the command.
     *
     * @return $this
     */
    public function setDescription(string $description): static
    {
        $this->description = $description;

        return $this;
    }

    /**
     * Returns the description for the command.
     */
    public function getDescription(): string
    {
        return $this->description;
    }

    /**
     * Sets the help for the command.
     *
     * @return $this
     */
    public function setHelp(string $help): static
    {
        $this->help = $help;

        return $this;
    }

    /**
     * Returns the help for the command.
     */
    public function getHelp(): string
    {
        return $this->help;
    }

    /**
     * Returns the processed help for the command replacing the %command.name% and
     * %command.full_name% patterns with the real values dynamically.
     */
    public function getProcessedHelp(): string
    {
        $name = $this->name;
        $isSingleCommand = $this->application?->isSingleCommand();

        $placeholders = [
            '%command.name%',
            '%command.full_name%',
        ];
        $replacements = [
            $name,
            $isSingleCommand ? $_SERVER['PHP_SELF'] : $_SERVER['PHP_SELF'].' '.$name,
        ];

        return str_replace($placeholders, $replacements, $this->getHelp() ?: $this->getDescription());
    }

    /**
     * Sets the aliases for the command.
     *
     * @param string[] $aliases An array of aliases for the command
     *
     * @return $this
     *
     * @throws InvalidArgumentException When an alias is invalid
     */
    public function setAliases(iterable $aliases): static
    {
        $list = [];

        foreach ($aliases as $alias) {
            $this->validateName($alias);
            $list[] = $alias;
        }

        $this->aliases = \is_array($aliases) ? $aliases : $list;

        return $this;
    }

    /**
     * Returns the aliases for the command.
     */
    public function getAliases(): array
    {
        return $this->aliases;
    }

    /**
     * Returns the synopsis for the command.
     *
     * @param bool $short Whether to show the short version of the synopsis (with options folded) or not
     */
    public function getSynopsis(bool $short = false): string
    {
        $key = $short ? 'short' : 'long';

        if (!isset($this->synopsis[$key])) {
            $this->synopsis[$key] = trim(\sprintf('%s %s', $this->name, $this->definition->getSynopsis($short)));
        }

        return $this->synopsis[$key];
    }

    /**
     * Add a command usage example, it'll be prefixed with the command name.
     *
     * @return $this
     */
    public function addUsage(string $usage): static
    {
        if (!str_starts_with($usage, $this->name)) {
            $usage = \sprintf('%s %s', $this->name, $usage);
        }

        $this->usages[] = $usage;

        return $this;
    }

    /**
     * Returns alternative usages of the command.
     */
    public function getUsages(): array
    {
        return $this->usages;
    }

    /**
     * Gets a helper instance by name.
     *
     * @return HelperInterface
     *
     * @throws LogicException           if no HelperSet is defined
     * @throws InvalidArgumentException if the helper is not defined
     */
    public function getHelper(string $name): mixed
    {
        if (null === $this->helperSet) {
            throw new LogicException(\sprintf('Cannot retrieve helper "%s" because there is no HelperSet defined. Did you forget to add your command to the application or to set the application on the command using the setApplication() method? You can also set the HelperSet directly using the setHelperSet() method.', $name));
        }

        return $this->helperSet->get($name);
    }

    /**
     * Validates a command name.
     *
     * It must be non-empty and parts can optionally be separated by ":".
     *
     * @throws InvalidArgumentException When the name is invalid
     */
    private function validateName(string $name): void
    {
        if (!preg_match('/^[^\:]++(\:[^\:]++)*$/', $name)) {
            throw new InvalidArgumentException(\sprintf('Command name "%s" is invalid.', $name));
        }
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Command;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Process\Process;

/**
 * Dumps the completion script for the current shell.
 *
 * @author Wouter de Jong <wouter@wouterj.nl>
 */
#[AsCommand(name: 'completion', description: 'Dump the shell completion script')]
final class DumpCompletionCommand extends Command
{
    /**
     * @deprecated since Symfony 6.1
     */
    protected static $defaultName = 'completion';

    /**
     * @deprecated since Symfony 6.1
     */
    protected static $defaultDescription = 'Dump the shell completion script';

    private array $supportedShells;

    protected function configure(): void
    {
        $fullCommand = $_SERVER['PHP_SELF'];
        $commandName = basename($fullCommand);
        $fullCommand = @realpath($fullCommand) ?: $fullCommand;

        $shell = $this->guessShell();
        [$rcFile, $completionFile] = match ($shell) {
            'fish' => ['~/.config/fish/config.fish', "/etc/fish/completions/$commandName.fish"],
            'zsh' => ['~/.zshrc', '$fpath[1]/_'.$commandName],
            default => ['~/.bashrc', "/etc/bash_completion.d/$commandName"],
        };

        $supportedShells = implode(', ', $this->getSupportedShells());

        $this
            ->setHelp(<<<EOH
                The <info>%command.name%</> command dumps the shell completion script required
                to use shell autocompletion (currently, {$supportedShells} completion are supported).

                <comment>Static installation
                -------------------</>

                Dump the script to a global completion file and restart your shell:

                    <info>%command.full_name% {$shell} | sudo tee {$completionFile}</>

                Or dump the script to a local file and source it:

                    <info>%command.full_name% {$shell} > completion.sh</>

                    <comment># source the file whenever you use the project</>
                    <info>source completion.sh</>

                    <comment># or add this line at the end of your "{$rcFile}" file:</>
                    <info>source /path/to/completion.sh</>

                <comment>Dynamic installation
                --------------------</>

                Add this to the end of your shell configuration file (e.g. <info>"{$rcFile}"</>):

                    <info>eval "$({$fullCommand} completion {$shell})"</>
                EOH
            )
            ->addArgument('shell', InputArgument::OPTIONAL, 'The shell type (e.g. "bash"), the value of the "$SHELL" env var will be used if this is not given', null, $this->getSupportedShells(...))
            ->addOption('debug', null, InputOption::VALUE_NONE, 'Tail the completion debug log')
        ;
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $commandName = basename($_SERVER['argv'][0]);

        if ($input->getOption('debug')) {
            $this->tailDebugLog($commandName, $output);

            return 0;
        }

        $shell = $input->getArgument('shell') ?? self::guessShell();
        $completionFile = __DIR__.'/../Resources/completion.'.$shell;
        if (!file_exists($completionFile)) {
            $supportedShells = $this->getSupportedShells();

            if ($output instanceof ConsoleOutputInterface) {
                $output = $output->getErrorOutput();
            }
            if ($shell) {
                $output->writeln(\sprintf('<error>Detected shell "%s", which is not supported by Symfony shell completion (supported shells: "%s").</>', $shell, implode('", "', $supportedShells)));
            } else {
                $output->writeln(\sprintf('<error>Shell not detected, Symfony shell completion only supports "%s").</>', implode('", "', $supportedShells)));
            }

            return 2;
        }

        $output->write(str_replace(['{{ COMMAND_NAME }}', '{{ VERSION }}'], [$commandName, CompleteCommand::COMPLETION_API_VERSION], file_get_contents($completionFile)));

        return 0;
    }

    private static function guessShell(): string
    {
        return basename($_SERVER['SHELL'] ?? '');
    }

    private function tailDebugLog(string $commandName, OutputInterface $output): void
    {
        $debugFile = sys_get_temp_dir().'/sf_'.$commandName.'.log';
        if (!file_exists($debugFile)) {
            touch($debugFile);
        }
        $process = new Process(['tail', '-f', $debugFile], null, null, null, 0);
        $process->run(static function (string $type, string $line) use ($output): void {
            $output->write($line);
        });
    }

    /**
     * @return string[]
     */
    private function getSupportedShells(): array
    {
        if (isset($this->supportedShells)) {
            return $this->supportedShells;
        }

        $shells = [];

        foreach (new \DirectoryIterator(__DIR__.'/../Resources/') as $file) {
            if (str_starts_with($file->getBasename(), 'completion.') && $file->isFile()) {
                $shells[] = $file->getExtension();
            }
        }
        sort($shells);

        return $this->supportedShells = $shells;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Command;

use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Lock\LockFactory;
use Symfony\Component\Lock\LockInterface;
use Symfony\Component\Lock\Store\FlockStore;
use Symfony\Component\Lock\Store\SemaphoreStore;

/**
 * Basic lock feature for commands.
 *
 * @author Geoffrey Brier <geoffrey.brier@gmail.com>
 */
trait LockableTrait
{
    private ?LockInterface $lock = null;

    /**
     * Locks a command.
     */
    private function lock(?string $name = null, bool $blocking = false): bool
    {
        if (!class_exists(SemaphoreStore::class)) {
            throw new LogicException('To enable the locking feature you must install the symfony/lock component. Try running "composer require symfony/lock".');
        }

        if (null !== $this->lock) {
            throw new LogicException('A lock is already in place.');
        }

        if (SemaphoreStore::isSupported()) {
            $store = new SemaphoreStore();
        } else {
            $store = new FlockStore();
        }

        $this->lock = (new LockFactory($store))->createLock($name ?: $this->getName());
        if (!$this->lock->acquire($blocking)) {
            $this->lock = null;

            return false;
        }

        return true;
    }

    /**
     * Releases the command lock if there is one.
     */
    private function release(): void
    {
        if ($this->lock) {
            $this->lock->release();
            $this->lock = null;
        }
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Command;

use Symfony\Component\Console\Descriptor\ApplicationDescription;
use Symfony\Component\Console\Helper\DescriptorHelper;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

/**
 * HelpCommand displays the help for a given command.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class HelpCommand extends Command
{
    private Command $command;

    /**
     * @return void
     */
    protected function configure()
    {
        $this->ignoreValidationErrors();

        $this
            ->setName('help')
            ->setDefinition([
                new InputArgument('command_name', InputArgument::OPTIONAL, 'The command name', 'help', fn () => array_keys((new ApplicationDescription($this->getApplication()))->getCommands())),
                new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt', static fn () => (new DescriptorHelper())->getFormats()),
                new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command help'),
            ])
            ->setDescription('Display help for a command')
            ->setHelp(<<<'EOF'
                The <info>%command.name%</info> command displays help for a given command:

                  <info>%command.full_name% list</info>

                You can also output the help in other formats by using the <comment>--format</comment> option:

                  <info>%command.full_name% --format=xml list</info>

                To display the list of available commands, please use the <info>list</info> command.
                EOF
            )
        ;
    }

    /**
     * @return void
     */
    public function setCommand(Command $command)
    {
        $this->command = $command;
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $this->command ??= $this->getApplication()->find($input->getArgument('command_name'));

        $helper = new DescriptorHelper();
        $helper->describe($output, $this->command, [
            'format' => $input->getOption('format'),
            'raw_text' => $input->getOption('raw'),
        ]);

        unset($this->command);

        return 0;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Command;

use Symfony\Component\Console\Application;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Helper\HelperInterface;
use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Stopwatch\Stopwatch;

/**
 * @internal
 *
 * @author Jules Pietri <jules@heahprod.com>
 */
final class TraceableCommand extends Command implements SignalableCommandInterface
{
    public readonly Command $command;
    public int $exitCode;
    public ?int $interruptedBySignal = null;
    public bool $ignoreValidation;
    public bool $isInteractive = false;
    public string $duration = 'n/a';
    public string $maxMemoryUsage = 'n/a';
    public InputInterface $input;
    public OutputInterface $output;
    /** @var array<string, mixed> */
    public array $arguments;
    /** @var array<string, mixed> */
    public array $options;
    /** @var array<string, mixed> */
    public array $interactiveInputs = [];
    public array $handledSignals = [];

    public function __construct(
        Command $command,
        private readonly Stopwatch $stopwatch,
    ) {
        if ($command instanceof LazyCommand) {
            $command = $command->getCommand();
        }

        $this->command = $command;

        // prevent call to self::getDefaultDescription()
        $this->setDescription($command->getDescription());

        parent::__construct($command->getName());

        // init below enables calling {@see parent::run()}
        [$code, $processTitle, $ignoreValidationErrors] = \Closure::bind(fn () => [$this->code, $this->processTitle, $this->ignoreValidationErrors], $command, Command::class)();

        if (\is_callable($code)) {
            $this->setCode($code);
        }

        if ($processTitle) {
            parent::setProcessTitle($processTitle);
        }

        if ($ignoreValidationErrors) {
            parent::ignoreValidationErrors();
        }

        $this->ignoreValidation = $ignoreValidationErrors;
    }

    public function __call(string $name, array $arguments): mixed
    {
        return $this->command->{$name}(...$arguments);
    }

    public function getSubscribedSignals(): array
    {
        return $this->command instanceof SignalableCommandInterface ? $this->command->getSubscribedSignals() : [];
    }

    public function handleSignal(int $signal, int|false $previousExitCode = 0): int|false
    {
        if (!$this->command instanceof SignalableCommandInterface) {
            return false;
        }

        $event = $this->stopwatch->start($this->getName().'.handle_signal');

        $exit = $this->command->handleSignal($signal, $previousExitCode);

        $event->stop();

        if (!isset($this->handledSignals[$signal])) {
            $this->handledSignals[$signal] = [
                'handled' => 0,
                'duration' => 0,
                'memory' => 0,
            ];
        }

        ++$this->handledSignals[$signal]['handled'];
        $this->handledSignals[$signal]['duration'] += $event->getDuration();
        $this->handledSignals[$signal]['memory'] = max(
            $this->handledSignals[$signal]['memory'],
            $event->getMemory() >> 20
        );

        return $exit;
    }

    /**
     * {@inheritdoc}
     *
     * Calling parent method is required to be used in {@see parent::run()}.
     */
    public function ignoreValidationErrors(): void
    {
        $this->ignoreValidation = true;
        $this->command->ignoreValidationErrors();

        parent::ignoreValidationErrors();
    }

    public function setApplication(?Application $application = null): void
    {
        $this->command->setApplication($application);
    }

    public function getApplication(): ?Application
    {
        return $this->command->getApplication();
    }

    public function setHelperSet(HelperSet $helperSet): void
    {
        $this->command->setHelperSet($helperSet);
    }

    public function getHelperSet(): ?HelperSet
    {
        return $this->command->getHelperSet();
    }

    public function isEnabled(): bool
    {
        return $this->command->isEnabled();
    }

    public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
    {
        $this->command->complete($input, $suggestions);
    }

    /**
     * {@inheritdoc}
     *
     * Calling parent method is required to be used in {@see parent::run()}.
     */
    public function setCode(callable $code): static
    {
        $this->command->setCode($code);

        return parent::setCode(function (InputInterface $input, OutputInterface $output) use ($code): int {
            $event = $this->stopwatch->start($this->getName().'.code');

            $this->exitCode = $code($input, $output);

            $event->stop();

            return $this->exitCode;
        });
    }

    /**
     * @internal
     */
    public function mergeApplicationDefinition(bool $mergeArgs = true): void
    {
        $this->command->mergeApplicationDefinition($mergeArgs);
    }

    public function setDefinition(array|InputDefinition $definition): static
    {
        $this->command->setDefinition($definition);

        return $this;
    }

    public function getDefinition(): InputDefinition
    {
        return $this->command->getDefinition();
    }

    public function getNativeDefinition(): InputDefinition
    {
        return $this->command->getNativeDefinition();
    }

    public function addArgument(string $name, ?int $mode = null, string $description = '', mixed $default = null, array|\Closure $suggestedValues = []): static
    {
        $this->command->addArgument($name, $mode, $description, $default, $suggestedValues);

        return $this;
    }

    public function addOption(string $name, string|array|null $shortcut = null, ?int $mode = null, string $description = '', mixed $default = null, array|\Closure $suggestedValues = []): static
    {
        $this->command->addOption($name, $shortcut, $mode, $description, $default, $suggestedValues);

        return $this;
    }

    /**
     * {@inheritdoc}
     *
     * Calling parent method is required to be used in {@see parent::run()}.
     */
    public function setProcessTitle(string $title): static
    {
        $this->command->setProcessTitle($title);

        return parent::setProcessTitle($title);
    }

    public function setHelp(string $help): static
    {
        $this->command->setHelp($help);

        return $this;
    }

    public function getHelp(): string
    {
        return $this->command->getHelp();
    }

    public function getProcessedHelp(): string
    {
        return $this->command->getProcessedHelp();
    }

    public function getSynopsis(bool $short = false): string
    {
        return $this->command->getSynopsis($short);
    }

    public function addUsage(string $usage): static
    {
        $this->command->addUsage($usage);

        return $this;
    }

    public function getUsages(): array
    {
        return $this->command->getUsages();
    }

    public function getHelper(string $name): HelperInterface
    {
        return $this->command->getHelper($name);
    }

    public function run(InputInterface $input, OutputInterface $output): int
    {
        $this->input = $input;
        $this->output = $output;
        $this->arguments = $input->getArguments();
        $this->options = $input->getOptions();
        $event = $this->stopwatch->start($this->getName(), 'command');

        try {
            $this->exitCode = $this->command->run($input, $output);
        } finally {
            $event->stop();

            if ($output instanceof ConsoleOutputInterface && $output->isDebug()) {
                $output->getErrorOutput()->writeln((string) $event);
            }

            $this->duration = $event->getDuration().' ms';
            $this->maxMemoryUsage = ($event->getMemory() >> 20).' MiB';

            if ($this->isInteractive) {
                $this->extractInteractiveInputs($input->getArguments(), $input->getOptions());
            }
        }

        return $this->exitCode;
    }

    protected function initialize(InputInterface $input, OutputInterface $output): void
    {
        $event = $this->stopwatch->start($this->getName().'.init', 'command');

        $this->command->initialize($input, $output);

        $event->stop();
    }

    protected function interact(InputInterface $input, OutputInterface $output): void
    {
        if (!$this->isInteractive = Command::class !== (new \ReflectionMethod($this->command, 'interact'))->getDeclaringClass()->getName()) {
            return;
        }

        $event = $this->stopwatch->start($this->getName().'.interact', 'command');

        $this->command->interact($input, $output);

        $event->stop();
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $event = $this->stopwatch->start($this->getName().'.execute', 'command');

        $exitCode = $this->command->execute($input, $output);

        $event->stop();

        return $exitCode;
    }

    private function extractInteractiveInputs(array $arguments, array $options): void
    {
        foreach ($arguments as $argName => $argValue) {
            if (\array_key_exists($argName, $this->arguments) && $this->arguments[$argName] === $argValue) {
                continue;
            }

            $this->interactiveInputs[$argName] = $argValue;
        }

        foreach ($options as $optName => $optValue) {
            if (\array_key_exists($optName, $this->options) && $this->options[$optName] === $optValue) {
                continue;
            }

            $this->interactiveInputs['--'.$optName] = $optValue;
        }
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Command;

use Symfony\Component\Console\Descriptor\ApplicationDescription;
use Symfony\Component\Console\Helper\DescriptorHelper;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

/**
 * ListCommand displays the list of all available commands for the application.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class ListCommand extends Command
{
    /**
     * @return void
     */
    protected function configure()
    {
        $this
            ->setName('list')
            ->setDefinition([
                new InputArgument('namespace', InputArgument::OPTIONAL, 'The namespace name', null, fn () => array_keys((new ApplicationDescription($this->getApplication()))->getNamespaces())),
                new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command list'),
                new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt', static fn () => (new DescriptorHelper())->getFormats()),
                new InputOption('short', null, InputOption::VALUE_NONE, 'To skip describing commands\' arguments'),
            ])
            ->setDescription('List commands')
            ->setHelp(<<<'EOF'
                The <info>%command.name%</info> command lists all commands:

                  <info>%command.full_name%</info>

                You can also display the commands for a specific namespace:

                  <info>%command.full_name% test</info>

                You can also output the information in other formats by using the <comment>--format</comment> option:

                  <info>%command.full_name% --format=xml</info>

                It's also possible to get raw list of commands (useful for embedding command runner):

                  <info>%command.full_name% --raw</info>
                EOF
            )
        ;
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $helper = new DescriptorHelper();
        $helper->describe($output, $this->getApplication(), [
            'format' => $input->getOption('format'),
            'raw_text' => $input->getOption('raw'),
            'namespace' => $input->getArgument('namespace'),
            'short' => $input->getOption('short'),
        ]);

        return 0;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Command;

/**
 * Interface for command reacting to signal.
 *
 * @author Grégoire Pineau <lyrixx@lyrix.info>
 */
interface SignalableCommandInterface
{
    /**
     * Returns the list of signals to subscribe.
     */
    public function getSubscribedSignals(): array;

    /**
     * The method will be called when the application is signaled.
     *
     * @param int|false $previousExitCode
     *
     * @return int|false The exit code to return or false to continue the normal execution
     */
    public function handleSignal(int $signal/* , int|false $previousExitCode = 0 */);
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Command;

use Symfony\Component\Console\Application;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Completion\Suggestion;
use Symfony\Component\Console\Helper\HelperInterface;
use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 */
final class LazyCommand extends Command
{
    private \Closure|Command $command;
    private ?bool $isEnabled;

    public function __construct(string $name, array $aliases, string $description, bool $isHidden, \Closure $commandFactory, ?bool $isEnabled = true)
    {
        $this->setName($name)
            ->setAliases($aliases)
            ->setHidden($isHidden)
            ->setDescription($description);

        $this->command = $commandFactory;
        $this->isEnabled = $isEnabled;
    }

    public function ignoreValidationErrors(): void
    {
        $this->getCommand()->ignoreValidationErrors();
    }

    public function setApplication(?Application $application = null): void
    {
        if (1 > \func_num_args()) {
            trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__);
        }
        if ($this->command instanceof parent) {
            $this->command->setApplication($application);
        }

        parent::setApplication($application);
    }

    public function setHelperSet(HelperSet $helperSet): void
    {
        if ($this->command instanceof parent) {
            $this->command->setHelperSet($helperSet);
        }

        parent::setHelperSet($helperSet);
    }

    public function isEnabled(): bool
    {
        return $this->isEnabled ?? $this->getCommand()->isEnabled();
    }

    public function run(InputInterface $input, OutputInterface $output): int
    {
        return $this->getCommand()->run($input, $output);
    }

    public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
    {
        $this->getCommand()->complete($input, $suggestions);
    }

    public function setCode(callable $code): static
    {
        $this->getCommand()->setCode($code);

        return $this;
    }

    /**
     * @internal
     */
    public function mergeApplicationDefinition(bool $mergeArgs = true): void
    {
        $this->getCommand()->mergeApplicationDefinition($mergeArgs);
    }

    public function setDefinition(array|InputDefinition $definition): static
    {
        $this->getCommand()->setDefinition($definition);

        return $this;
    }

    public function getDefinition(): InputDefinition
    {
        return $this->getCommand()->getDefinition();
    }

    public function getNativeDefinition(): InputDefinition
    {
        return $this->getCommand()->getNativeDefinition();
    }

    /**
     * @param array|\Closure(CompletionInput,CompletionSuggestions):list<string|Suggestion> $suggestedValues The values used for input completion
     */
    public function addArgument(string $name, ?int $mode = null, string $description = '', mixed $default = null /* array|\Closure $suggestedValues = [] */): static
    {
        $suggestedValues = 5 <= \func_num_args() ? func_get_arg(4) : [];
        $this->getCommand()->addArgument($name, $mode, $description, $default, $suggestedValues);

        return $this;
    }

    /**
     * @param array|\Closure(CompletionInput,CompletionSuggestions):list<string|Suggestion> $suggestedValues The values used for input completion
     */
    public function addOption(string $name, string|array|null $shortcut = null, ?int $mode = null, string $description = '', mixed $default = null /* array|\Closure $suggestedValues = [] */): static
    {
        $suggestedValues = 6 <= \func_num_args() ? func_get_arg(5) : [];
        $this->getCommand()->addOption($name, $shortcut, $mode, $description, $default, $suggestedValues);

        return $this;
    }

    public function setProcessTitle(string $title): static
    {
        $this->getCommand()->setProcessTitle($title);

        return $this;
    }

    public function setHelp(string $help): static
    {
        $this->getCommand()->setHelp($help);

        return $this;
    }

    public function getHelp(): string
    {
        return $this->getCommand()->getHelp();
    }

    public function getProcessedHelp(): string
    {
        return $this->getCommand()->getProcessedHelp();
    }

    public function getSynopsis(bool $short = false): string
    {
        return $this->getCommand()->getSynopsis($short);
    }

    public function addUsage(string $usage): static
    {
        $this->getCommand()->addUsage($usage);

        return $this;
    }

    public function getUsages(): array
    {
        return $this->getCommand()->getUsages();
    }

    public function getHelper(string $name): HelperInterface
    {
        return $this->getCommand()->getHelper($name);
    }

    public function getCommand(): parent
    {
        if (!$this->command instanceof \Closure) {
            return $this->command;
        }

        $command = $this->command = ($this->command)();
        $command->setApplication($this->getApplication());

        if (null !== $this->getHelperSet()) {
            $command->setHelperSet($this->getHelperSet());
        }

        $command->setName($this->getName())
            ->setAliases($this->getAliases())
            ->setHidden($this->isHidden())
            ->setDescription($this->getDescription());

        // Will throw if the command is not correctly initialized.
        $command->getDefinition();

        return $command;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console;

use Symfony\Component\Console\Event\ConsoleCommandEvent;
use Symfony\Component\Console\Event\ConsoleErrorEvent;
use Symfony\Component\Console\Event\ConsoleSignalEvent;
use Symfony\Component\Console\Event\ConsoleTerminateEvent;

/**
 * Contains all events dispatched by an Application.
 *
 * @author Francesco Levorato <git@flevour.net>
 */
final class ConsoleEvents
{
    /**
     * The COMMAND event allows you to attach listeners before any command is
     * executed by the console. It also allows you to modify the command, input and output
     * before they are handed to the command.
     *
     * @Event("Symfony\Component\Console\Event\ConsoleCommandEvent")
     */
    public const COMMAND = 'console.command';

    /**
     * The SIGNAL event allows you to perform some actions
     * after the command execution was interrupted.
     *
     * @Event("Symfony\Component\Console\Event\ConsoleSignalEvent")
     */
    public const SIGNAL = 'console.signal';

    /**
     * The TERMINATE event allows you to attach listeners after a command is
     * executed by the console.
     *
     * @Event("Symfony\Component\Console\Event\ConsoleTerminateEvent")
     */
    public const TERMINATE = 'console.terminate';

    /**
     * The ERROR event occurs when an uncaught exception or error appears.
     *
     * This event allows you to deal with the exception/error or
     * to modify the thrown exception.
     *
     * @Event("Symfony\Component\Console\Event\ConsoleErrorEvent")
     */
    public const ERROR = 'console.error';

    /**
     * Event aliases.
     *
     * These aliases can be consumed by RegisterListenersPass.
     */
    public const ALIASES = [
        ConsoleCommandEvent::class => self::COMMAND,
        ConsoleErrorEvent::class => self::ERROR,
        ConsoleSignalEvent::class => self::SIGNAL,
        ConsoleTerminateEvent::class => self::TERMINATE,
    ];
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

/**
 * @author Grégoire Pineau <lyrixx@lyrixx.info>
 */
class SingleCommandApplication extends Command
{
    private string $version = 'UNKNOWN';
    private bool $autoExit = true;
    private bool $running = false;

    /**
     * @return $this
     */
    public function setVersion(string $version): static
    {
        $this->version = $version;

        return $this;
    }

    /**
     * @final
     *
     * @return $this
     */
    public function setAutoExit(bool $autoExit): static
    {
        $this->autoExit = $autoExit;

        return $this;
    }

    public function run(?InputInterface $input = null, ?OutputInterface $output = null): int
    {
        if ($this->running) {
            return parent::run($input, $output);
        }

        // We use the command name as the application name
        $application = new Application($this->getName() ?: 'UNKNOWN', $this->version);
        $application->setAutoExit($this->autoExit);
        // Fix the usage of the command displayed with "--help"
        $this->setName($_SERVER['argv'][0]);
        $application->add($this);
        $application->setDefaultCommand($this->getName(), true);

        $this->running = true;
        try {
            $ret = $application->run($input, $output);
        } finally {
            $this->running = false;
        }

        return $ret ?? 1;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Descriptor;

use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\Helper;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\String\UnicodeString;

class ReStructuredTextDescriptor extends Descriptor
{
    // <h1>
    private string $partChar = '=';
    // <h2>
    private string $chapterChar = '-';
    // <h3>
    private string $sectionChar = '~';
    // <h4>
    private string $subsectionChar = '.';
    // <h5>
    private string $subsubsectionChar = '^';
    // <h6>
    private string $paragraphsChar = '"';

    private array $visibleNamespaces = [];

    public function describe(OutputInterface $output, object $object, array $options = []): void
    {
        $decorated = $output->isDecorated();
        $output->setDecorated(false);

        parent::describe($output, $object, $options);

        $output->setDecorated($decorated);
    }

    /**
     * Override parent method to set $decorated = true.
     */
    protected function write(string $content, bool $decorated = true): void
    {
        parent::write($content, $decorated);
    }

    protected function describeInputArgument(InputArgument $argument, array $options = []): void
    {
        $this->write(
            $argument->getName() ?: '<none>'."\n".str_repeat($this->paragraphsChar, Helper::width($argument->getName()))."\n\n"
                .($argument->getDescription() ? preg_replace('/\s*[\r\n]\s*/', "\n", $argument->getDescription())."\n\n" : '')
                .'- **Is required**: '.($argument->isRequired() ? 'yes' : 'no')."\n"
                .'- **Is array**: '.($argument->isArray() ? 'yes' : 'no')."\n"
                .'- **Default**: ``'.str_replace("\n", '', var_export($argument->getDefault(), true)).'``'
        );
    }

    protected function describeInputOption(InputOption $option, array $options = []): void
    {
        $name = '\-\-'.$option->getName();
        if ($option->isNegatable()) {
            $name .= '|\-\-no-'.$option->getName();
        }
        if ($option->getShortcut()) {
            $name .= '|-'.str_replace('|', '|-', $option->getShortcut());
        }

        $optionDescription = $option->getDescription() ? preg_replace('/\s*[\r\n]\s*/', "\n\n", $option->getDescription())."\n\n" : '';
        $optionDescription = (new UnicodeString($optionDescription))->ascii();
        $this->write(
            $name."\n".str_repeat($this->paragraphsChar, Helper::width($name))."\n\n"
            .$optionDescription
            .'- **Accept value**: '.($option->acceptValue() ? 'yes' : 'no')."\n"
            .'- **Is value required**: '.($option->isValueRequired() ? 'yes' : 'no')."\n"
            .'- **Is multiple**: '.($option->isArray() ? 'yes' : 'no')."\n"
            .'- **Is negatable**: '.($option->isNegatable() ? 'yes' : 'no')."\n"
            .'- **Default**: ``'.str_replace("\n", '', var_export($option->getDefault(), true)).'``'."\n"
        );
    }

    protected function describeInputDefinition(InputDefinition $definition, array $options = []): void
    {
        if ($showArguments = ((bool) $definition->getArguments())) {
            $this->write("Arguments\n".str_repeat($this->subsubsectionChar, 9))."\n\n";
            foreach ($definition->getArguments() as $argument) {
                $this->write("\n\n");
                $this->describeInputArgument($argument);
            }
        }

        if ($nonDefaultOptions = $this->getNonDefaultOptions($definition)) {
            if ($showArguments) {
                $this->write("\n\n");
            }

            $this->write("Options\n".str_repeat($this->subsubsectionChar, 7)."\n\n");
            foreach ($nonDefaultOptions as $option) {
                $this->describeInputOption($option);
                $this->write("\n");
            }
        }
    }

    protected function describeCommand(Command $command, array $options = []): void
    {
        if ($options['short'] ?? false) {
            $this->write(
                '``'.$command->getName()."``\n"
                .str_repeat($this->subsectionChar, Helper::width($command->getName()))."\n\n"
                .($command->getDescription() ? $command->getDescription()."\n\n" : '')
                ."Usage\n".str_repeat($this->paragraphsChar, 5)."\n\n"
                .array_reduce($command->getAliases(), static fn ($carry, $usage) => $carry.'- ``'.$usage.'``'."\n")
            );

            return;
        }

        $command->mergeApplicationDefinition(false);

        foreach ($command->getAliases() as $alias) {
            $this->write('.. _'.$alias.":\n\n");
        }
        $this->write(
            $command->getName()."\n"
            .str_repeat($this->subsectionChar, Helper::width($command->getName()))."\n\n"
            .($command->getDescription() ? $command->getDescription()."\n\n" : '')
            ."Usage\n".str_repeat($this->subsubsectionChar, 5)."\n\n"
            .array_reduce(array_merge([$command->getSynopsis()], $command->getAliases(), $command->getUsages()), static fn ($carry, $usage) => $carry.'- ``'.$usage.'``'."\n")
        );

        if ($help = $command->getProcessedHelp()) {
            $this->write("\n");
            $this->write($help);
        }

        $definition = $command->getDefinition();
        if ($definition->getOptions() || $definition->getArguments()) {
            $this->write("\n\n");
            $this->describeInputDefinition($definition);
        }
    }

    protected function describeApplication(Application $application, array $options = []): void
    {
        $description = new ApplicationDescription($application, $options['namespace'] ?? null);
        $title = $this->getApplicationTitle($application);

        $this->write($title."\n".str_repeat($this->partChar, Helper::width($title)));
        $this->createTableOfContents($description, $application);
        $this->describeCommands($application, $options);
    }

    private function getApplicationTitle(Application $application): string
    {
        if ('UNKNOWN' === $application->getName()) {
            return 'Console Tool';
        }
        if ('UNKNOWN' !== $application->getVersion()) {
            return \sprintf('%s %s', $application->getName(), $application->getVersion());
        }

        return $application->getName();
    }

    private function describeCommands($application, array $options): void
    {
        $title = 'Commands';
        $this->write("\n\n$title\n".str_repeat($this->chapterChar, Helper::width($title))."\n\n");
        foreach ($this->visibleNamespaces as $namespace) {
            if ('_global' === $namespace) {
                $commands = $application->all('');
                $this->write('Global'."\n".str_repeat($this->sectionChar, Helper::width('Global'))."\n\n");
            } else {
                $commands = $application->all($namespace);
                $this->write($namespace."\n".str_repeat($this->sectionChar, Helper::width($namespace))."\n\n");
            }

            foreach ($this->removeAliasesAndHiddenCommands($commands) as $command) {
                $this->describeCommand($command, $options);
                $this->write("\n\n");
            }
        }
    }

    private function createTableOfContents(ApplicationDescription $description, Application $application): void
    {
        $this->setVisibleNamespaces($description);
        $chapterTitle = 'Table of Contents';
        $this->write("\n\n$chapterTitle\n".str_repeat($this->chapterChar, Helper::width($chapterTitle))."\n\n");
        foreach ($this->visibleNamespaces as $namespace) {
            if ('_global' === $namespace) {
                $commands = $application->all('');
            } else {
                $commands = $application->all($namespace);
                $this->write("\n\n");
                $this->write($namespace."\n".str_repeat($this->sectionChar, Helper::width($namespace))."\n\n");
            }
            $commands = $this->removeAliasesAndHiddenCommands($commands);

            $this->write("\n\n");
            $this->write(implode("\n", array_map(static fn ($commandName) => \sprintf('- `%s`_', $commandName), array_keys($commands))));
        }
    }

    private function getNonDefaultOptions(InputDefinition $definition): array
    {
        $globalOptions = [
            'help',
            'quiet',
            'verbose',
            'version',
            'ansi',
            'no-interaction',
        ];
        $nonDefaultOptions = [];
        foreach ($definition->getOptions() as $option) {
            // Skip global options.
            if (!\in_array($option->getName(), $globalOptions)) {
                $nonDefaultOptions[] = $option;
            }
        }

        return $nonDefaultOptions;
    }

    private function setVisibleNamespaces(ApplicationDescription $description): void
    {
        $commands = $description->getCommands();
        foreach ($description->getNamespaces() as $namespace) {
            try {
                $namespaceCommands = $namespace['commands'];
                foreach ($namespaceCommands as $key => $commandName) {
                    if (!\array_key_exists($commandName, $commands)) {
                        // If the array key does not exist, then this is an alias.
                        unset($namespaceCommands[$key]);
                    } elseif ($commands[$commandName]->isHidden()) {
                        unset($namespaceCommands[$key]);
                    }
                }
                if (!$namespaceCommands) {
                    // If the namespace contained only aliases or hidden commands, skip the namespace.
                    continue;
                }
            } catch (\Exception) {
            }
            $this->visibleNamespaces[] = $namespace['id'];
        }
    }

    private function removeAliasesAndHiddenCommands(array $commands): array
    {
        foreach ($commands as $key => $command) {
            if ($command->isHidden() || \in_array($key, $command->getAliases(), true)) {
                unset($commands[$key]);
            }
        }
        unset($commands['completion']);

        return $commands;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Descriptor;

use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Helper\Helper;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputOption;

/**
 * Text descriptor.
 *
 * @author Jean-François Simon <contact@jfsimon.fr>
 *
 * @internal
 */
class TextDescriptor extends Descriptor
{
    protected function describeInputArgument(InputArgument $argument, array $options = []): void
    {
        if (null !== $argument->getDefault() && (!\is_array($argument->getDefault()) || \count($argument->getDefault()))) {
            $default = \sprintf('<comment> [default: %s]</comment>', $this->formatDefaultValue($argument->getDefault()));
        } else {
            $default = '';
        }

        $totalWidth = $options['total_width'] ?? Helper::width($argument->getName());
        $spacingWidth = $totalWidth - Helper::width($argument->getName());

        $this->writeText(\sprintf('  <info>%s</info>  %s%s%s',
            $argument->getName(),
            str_repeat(' ', $spacingWidth),
            // + 4 = 2 spaces before <info>, 2 spaces after </info>
            preg_replace('/\s*[\r\n]\s*/', "\n".str_repeat(' ', $totalWidth + 4), $argument->getDescription()),
            $default
        ), $options);
    }

    protected function describeInputOption(InputOption $option, array $options = []): void
    {
        if ($option->acceptValue() && null !== $option->getDefault() && (!\is_array($option->getDefault()) || \count($option->getDefault()))) {
            $default = \sprintf('<comment> [default: %s]</comment>', $this->formatDefaultValue($option->getDefault()));
        } else {
            $default = '';
        }

        $value = '';
        if ($option->acceptValue()) {
            $value = '='.strtoupper($option->getName());

            if ($option->isValueOptional()) {
                $value = '['.$value.']';
            }
        }

        $totalWidth = $options['total_width'] ?? $this->calculateTotalWidthForOptions([$option]);
        $synopsis = \sprintf('%s%s',
            $option->getShortcut() ? \sprintf('-%s, ', $option->getShortcut()) : '    ',
            \sprintf($option->isNegatable() ? '--%1$s|--no-%1$s' : '--%1$s%2$s', $option->getName(), $value)
        );

        $spacingWidth = $totalWidth - Helper::width($synopsis);

        $this->writeText(\sprintf('  <info>%s</info>  %s%s%s%s',
            $synopsis,
            str_repeat(' ', $spacingWidth),
            // + 4 = 2 spaces before <info>, 2 spaces after </info>
            preg_replace('/\s*[\r\n]\s*/', "\n".str_repeat(' ', $totalWidth + 4), $option->getDescription()),
            $default,
            $option->isArray() ? '<comment> (multiple values allowed)</comment>' : ''
        ), $options);
    }

    protected function describeInputDefinition(InputDefinition $definition, array $options = []): void
    {
        $totalWidth = $this->calculateTotalWidthForOptions($definition->getOptions());
        foreach ($definition->getArguments() as $argument) {
            $totalWidth = max($totalWidth, Helper::width($argument->getName()));
        }

        if ($definition->getArguments()) {
            $this->writeText('<comment>Arguments:</comment>', $options);
            $this->writeText("\n");
            foreach ($definition->getArguments() as $argument) {
                $this->describeInputArgument($argument, array_merge($options, ['total_width' => $totalWidth]));
                $this->writeText("\n");
            }
        }

        if ($definition->getArguments() && $definition->getOptions()) {
            $this->writeText("\n");
        }

        if ($definition->getOptions()) {
            $laterOptions = [];

            $this->writeText('<comment>Options:</comment>', $options);
            foreach ($definition->getOptions() as $option) {
                if (\strlen($option->getShortcut() ?? '') > 1) {
                    $laterOptions[] = $option;
                    continue;
                }
                $this->writeText("\n");
                $this->describeInputOption($option, array_merge($options, ['total_width' => $totalWidth]));
            }
            foreach ($laterOptions as $option) {
                $this->writeText("\n");
                $this->describeInputOption($option, array_merge($options, ['total_width' => $totalWidth]));
            }
        }
    }

    protected function describeCommand(Command $command, array $options = []): void
    {
        $command->mergeApplicationDefinition(false);

        if ($description = $command->getDescription()) {
            $this->writeText('<comment>Description:</comment>', $options);
            $this->writeText("\n");
            $this->writeText('  '.$description);
            $this->writeText("\n\n");
        }

        $this->writeText('<comment>Usage:</comment>', $options);
        foreach (array_merge([$command->getSynopsis(true)], $command->getAliases(), $command->getUsages()) as $usage) {
            $this->writeText("\n");
            $this->writeText('  '.OutputFormatter::escape($usage), $options);
        }
        $this->writeText("\n");

        $definition = $command->getDefinition();
        if ($definition->getOptions() || $definition->getArguments()) {
            $this->writeText("\n");
            $this->describeInputDefinition($definition, $options);
            $this->writeText("\n");
        }

        $help = $command->getProcessedHelp();
        if ($help && $help !== $description) {
            $this->writeText("\n");
            $this->writeText('<comment>Help:</comment>', $options);
            $this->writeText("\n");
            $this->writeText('  '.str_replace("\n", "\n  ", $help), $options);
            $this->writeText("\n");
        }
    }

    protected function describeApplication(Application $application, array $options = []): void
    {
        $describedNamespace = $options['namespace'] ?? null;
        $description = new ApplicationDescription($application, $describedNamespace);

        if (isset($options['raw_text']) && $options['raw_text']) {
            $width = $this->getColumnWidth($description->getCommands());

            foreach ($description->getCommands() as $command) {
                $this->writeText(\sprintf("%-{$width}s %s", $command->getName(), $command->getDescription()), $options);
                $this->writeText("\n");
            }
        } else {
            if ('' != $help = $application->getHelp()) {
                $this->writeText("$help\n\n", $options);
            }

            $this->writeText("<comment>Usage:</comment>\n", $options);
            $this->writeText("  command [options] [arguments]\n\n", $options);

            $this->describeInputDefinition(new InputDefinition($application->getDefinition()->getOptions()), $options);

            $this->writeText("\n");
            $this->writeText("\n");

            $commands = $description->getCommands();
            $namespaces = $description->getNamespaces();
            if ($describedNamespace && $namespaces) {
                // make sure all alias commands are included when describing a specific namespace
                $describedNamespaceInfo = reset($namespaces);
                foreach ($describedNamespaceInfo['commands'] as $name) {
                    $commands[$name] = $description->getCommand($name);
                }
            }

            // calculate max. width based on available commands per namespace
            $width = $this->getColumnWidth(array_merge(...array_values(array_map(static fn ($namespace) => array_intersect($namespace['commands'], array_keys($commands)), array_values($namespaces)))));

            if ($describedNamespace) {
                $this->writeText(\sprintf('<comment>Available commands for the "%s" namespace:</comment>', $describedNamespace), $options);
            } else {
                $this->writeText('<comment>Available commands:</comment>', $options);
            }

            foreach ($namespaces as $namespace) {
                $namespace['commands'] = array_filter($namespace['commands'], static fn ($name) => isset($commands[$name]));

                if (!$namespace['commands']) {
                    continue;
                }

                if (!$describedNamespace && ApplicationDescription::GLOBAL_NAMESPACE !== $namespace['id']) {
                    $this->writeText("\n");
                    $this->writeText(' <comment>'.$namespace['id'].'</comment>', $options);
                }

                foreach ($namespace['commands'] as $name) {
                    $this->writeText("\n");
                    $spacingWidth = $width - Helper::width($name);
                    $command = $commands[$name];
                    $commandAliases = $name === $command->getName() ? $this->getCommandAliasesText($command) : '';
                    $this->writeText(\sprintf('  <info>%s</info>%s%s', $name, str_repeat(' ', $spacingWidth), $commandAliases.$command->getDescription()), $options);
                }
            }

            $this->writeText("\n");
        }
    }

    private function writeText(string $content, array $options = []): void
    {
        $this->write(
            isset($options['raw_text']) && $options['raw_text'] ? strip_tags($content) : $content,
            isset($options['raw_output']) ? !$options['raw_output'] : true
        );
    }

    /**
     * Formats command aliases to show them in the command description.
     */
    private function getCommandAliasesText(Command $command): string
    {
        $text = '';
        $aliases = $command->getAliases();

        if ($aliases) {
            $text = '['.implode('|', $aliases).'] ';
        }

        return $text;
    }

    /**
     * Formats input option/argument default value.
     */
    private function formatDefaultValue(mixed $default): string
    {
        if (\INF === $default) {
            return 'INF';
        }

        if (\is_string($default)) {
            $default = OutputFormatter::escape($default);
        } elseif (\is_array($default)) {
            foreach ($default as $key => $value) {
                if (\is_string($value)) {
                    $default[$key] = OutputFormatter::escape($value);
                }
            }
        }

        return str_replace('\\\\', '\\', json_encode($default, \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE));
    }

    /**
     * @param array<Command|string> $commands
     */
    private function getColumnWidth(array $commands): int
    {
        $widths = [];

        foreach ($commands as $command) {
            if ($command instanceof Command) {
                $widths[] = Helper::width($command->getName());
                foreach ($command->getAliases() as $alias) {
                    $widths[] = Helper::width($alias);
                }
            } else {
                $widths[] = Helper::width($command);
            }
        }

        return $widths ? max($widths) + 2 : 0;
    }

    /**
     * @param InputOption[] $options
     */
    private function calculateTotalWidthForOptions(array $options): int
    {
        $totalWidth = 0;
        foreach ($options as $option) {
            // "-" + shortcut + ", --" + name
            $nameLength = 1 + max(Helper::width($option->getShortcut()), 1) + 4 + Helper::width($option->getName());
            if ($option->isNegatable()) {
                $nameLength += 6 + Helper::width($option->getName()); // |--no- + name
            } elseif ($option->acceptValue()) {
                $valueLength = 1 + Helper::width($option->getName()); // = + value
                $valueLength += $option->isValueOptional() ? 2 : 0; // [ + ]

                $nameLength += $valueLength;
            }
            $totalWidth = max($totalWidth, $nameLength);
        }

        return $totalWidth;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Descriptor;

use Symfony\Component\Console\Output\OutputInterface;

/**
 * Descriptor interface.
 *
 * @author Jean-François Simon <contact@jfsimon.fr>
 */
interface DescriptorInterface
{
    /**
     * @return void
     */
    public function describe(OutputInterface $output, object $object, array $options = []);
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Descriptor;

use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputOption;

/**
 * JSON descriptor.
 *
 * @author Jean-François Simon <contact@jfsimon.fr>
 *
 * @internal
 */
class JsonDescriptor extends Descriptor
{
    protected function describeInputArgument(InputArgument $argument, array $options = []): void
    {
        $this->writeData($this->getInputArgumentData($argument), $options);
    }

    protected function describeInputOption(InputOption $option, array $options = []): void
    {
        $this->writeData($this->getInputOptionData($option), $options);
        if ($option->isNegatable()) {
            $this->writeData($this->getInputOptionData($option, true), $options);
        }
    }

    protected function describeInputDefinition(InputDefinition $definition, array $options = []): void
    {
        $this->writeData($this->getInputDefinitionData($definition), $options);
    }

    protected function describeCommand(Command $command, array $options = []): void
    {
        $this->writeData($this->getCommandData($command, $options['short'] ?? false), $options);
    }

    protected function describeApplication(Application $application, array $options = []): void
    {
        $describedNamespace = $options['namespace'] ?? null;
        $description = new ApplicationDescription($application, $describedNamespace, true);
        $commands = [];

        foreach ($description->getCommands() as $command) {
            $commands[] = $this->getCommandData($command, $options['short'] ?? false);
        }

        $data = [];
        if ('UNKNOWN' !== $application->getName()) {
            $data['application']['name'] = $application->getName();
            if ('UNKNOWN' !== $application->getVersion()) {
                $data['application']['version'] = $application->getVersion();
            }
        }

        $data['commands'] = $commands;

        if ($describedNamespace) {
            $data['namespace'] = $describedNamespace;
        } else {
            $data['namespaces'] = array_values($description->getNamespaces());
        }

        $this->writeData($data, $options);
    }

    /**
     * Writes data as json.
     */
    private function writeData(array $data, array $options): void
    {
        $flags = $options['json_encoding'] ?? 0;

        $this->write(json_encode($data, $flags));
    }

    private function getInputArgumentData(InputArgument $argument): array
    {
        return [
            'name' => $argument->getName(),
            'is_required' => $argument->isRequired(),
            'is_array' => $argument->isArray(),
            'description' => preg_replace('/\s*[\r\n]\s*/', ' ', $argument->getDescription()),
            'default' => \INF === $argument->getDefault() ? 'INF' : $argument->getDefault(),
        ];
    }

    private function getInputOptionData(InputOption $option, bool $negated = false): array
    {
        return $negated ? [
            'name' => '--no-'.$option->getName(),
            'shortcut' => '',
            'accept_value' => false,
            'is_value_required' => false,
            'is_multiple' => false,
            'description' => 'Negate the "--'.$option->getName().'" option',
            'default' => null === $option->getDefault() ? null : !$option->getDefault(),
        ] : [
            'name' => '--'.$option->getName(),
            'shortcut' => $option->getShortcut() ? '-'.str_replace('|', '|-', $option->getShortcut()) : '',
            'accept_value' => $option->acceptValue(),
            'is_value_required' => $option->isValueRequired(),
            'is_multiple' => $option->isArray(),
            'description' => preg_replace('/\s*[\r\n]\s*/', ' ', $option->getDescription()),
            'default' => \INF === $option->getDefault() ? 'INF' : $option->getDefault(),
        ];
    }

    private function getInputDefinitionData(InputDefinition $definition): array
    {
        $inputArguments = [];
        foreach ($definition->getArguments() as $name => $argument) {
            $inputArguments[$name] = $this->getInputArgumentData($argument);
        }

        $inputOptions = [];
        foreach ($definition->getOptions() as $name => $option) {
            $inputOptions[$name] = $this->getInputOptionData($option);
            if ($option->isNegatable()) {
                $inputOptions['no-'.$name] = $this->getInputOptionData($option, true);
            }
        }

        return ['arguments' => $inputArguments, 'options' => $inputOptions];
    }

    private function getCommandData(Command $command, bool $short = false): array
    {
        $data = [
            'name' => $command->getName(),
            'description' => $command->getDescription(),
        ];

        if ($short) {
            $data += [
                'usage' => $command->getAliases(),
            ];
        } else {
            $command->mergeApplicationDefinition(false);

            $data += [
                'usage' => array_merge([$command->getSynopsis()], $command->getUsages(), $command->getAliases()),
                'help' => $command->getProcessedHelp(),
                'definition' => $this->getInputDefinitionData($command->getDefinition()),
            ];
        }

        $data['hidden'] = $command->isHidden();

        return $data;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Descriptor;

use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputOption;

/**
 * XML descriptor.
 *
 * @author Jean-François Simon <contact@jfsimon.fr>
 *
 * @internal
 */
class XmlDescriptor extends Descriptor
{
    public function getInputDefinitionDocument(InputDefinition $definition): \DOMDocument
    {
        $dom = new \DOMDocument('1.0', 'UTF-8');
        $dom->appendChild($definitionXML = $dom->createElement('definition'));

        $definitionXML->appendChild($argumentsXML = $dom->createElement('arguments'));
        foreach ($definition->getArguments() as $argument) {
            $this->appendDocument($argumentsXML, $this->getInputArgumentDocument($argument));
        }

        $definitionXML->appendChild($optionsXML = $dom->createElement('options'));
        foreach ($definition->getOptions() as $option) {
            $this->appendDocument($optionsXML, $this->getInputOptionDocument($option));
        }

        return $dom;
    }

    public function getCommandDocument(Command $command, bool $short = false): \DOMDocument
    {
        $dom = new \DOMDocument('1.0', 'UTF-8');
        $dom->appendChild($commandXML = $dom->createElement('command'));

        $commandXML->setAttribute('id', $command->getName());
        $commandXML->setAttribute('name', $command->getName());
        $commandXML->setAttribute('hidden', $command->isHidden() ? 1 : 0);

        $commandXML->appendChild($usagesXML = $dom->createElement('usages'));

        $commandXML->appendChild($descriptionXML = $dom->createElement('description'));
        $descriptionXML->appendChild($dom->createTextNode(str_replace("\n", "\n ", $command->getDescription())));

        if ($short) {
            foreach ($command->getAliases() as $usage) {
                $usagesXML->appendChild($dom->createElement('usage', $usage));
            }
        } else {
            $command->mergeApplicationDefinition(false);

            foreach (array_merge([$command->getSynopsis()], $command->getAliases(), $command->getUsages()) as $usage) {
                $usagesXML->appendChild($dom->createElement('usage', $usage));
            }

            $commandXML->appendChild($helpXML = $dom->createElement('help'));
            $helpXML->appendChild($dom->createTextNode(str_replace("\n", "\n ", $command->getProcessedHelp())));

            $definitionXML = $this->getInputDefinitionDocument($command->getDefinition());
            $this->appendDocument($commandXML, $definitionXML->getElementsByTagName('definition')->item(0));
        }

        return $dom;
    }

    public function getApplicationDocument(Application $application, ?string $namespace = null, bool $short = false): \DOMDocument
    {
        $dom = new \DOMDocument('1.0', 'UTF-8');
        $dom->appendChild($rootXml = $dom->createElement('symfony'));

        if ('UNKNOWN' !== $application->getName()) {
            $rootXml->setAttribute('name', $application->getName());
            if ('UNKNOWN' !== $application->getVersion()) {
                $rootXml->setAttribute('version', $application->getVersion());
            }
        }

        $rootXml->appendChild($commandsXML = $dom->createElement('commands'));

        $description = new ApplicationDescription($application, $namespace, true);

        if ($namespace) {
            $commandsXML->setAttribute('namespace', $namespace);
        }

        foreach ($description->getCommands() as $command) {
            $this->appendDocument($commandsXML, $this->getCommandDocument($command, $short));
        }

        if (!$namespace) {
            $rootXml->appendChild($namespacesXML = $dom->createElement('namespaces'));

            foreach ($description->getNamespaces() as $namespaceDescription) {
                $namespacesXML->appendChild($namespaceArrayXML = $dom->createElement('namespace'));
                $namespaceArrayXML->setAttribute('id', $namespaceDescription['id']);

                foreach ($namespaceDescription['commands'] as $name) {
                    $namespaceArrayXML->appendChild($commandXML = $dom->createElement('command'));
                    $commandXML->appendChild($dom->createTextNode($name));
                }
            }
        }

        return $dom;
    }

    protected function describeInputArgument(InputArgument $argument, array $options = []): void
    {
        $this->writeDocument($this->getInputArgumentDocument($argument));
    }

    protected function describeInputOption(InputOption $option, array $options = []): void
    {
        $this->writeDocument($this->getInputOptionDocument($option));
    }

    protected function describeInputDefinition(InputDefinition $definition, array $options = []): void
    {
        $this->writeDocument($this->getInputDefinitionDocument($definition));
    }

    protected function describeCommand(Command $command, array $options = []): void
    {
        $this->writeDocument($this->getCommandDocument($command, $options['short'] ?? false));
    }

    protected function describeApplication(Application $application, array $options = []): void
    {
        $this->writeDocument($this->getApplicationDocument($application, $options['namespace'] ?? null, $options['short'] ?? false));
    }

    /**
     * Appends document children to parent node.
     */
    private function appendDocument(\DOMNode $parentNode, \DOMNode $importedParent): void
    {
        foreach ($importedParent->childNodes as $childNode) {
            $parentNode->appendChild($parentNode->ownerDocument->importNode($childNode, true));
        }
    }

    /**
     * Writes DOM document.
     */
    private function writeDocument(\DOMDocument $dom): void
    {
        $dom->formatOutput = true;
        $this->write($dom->saveXML());
    }

    private function getInputArgumentDocument(InputArgument $argument): \DOMDocument
    {
        $dom = new \DOMDocument('1.0', 'UTF-8');

        $dom->appendChild($objectXML = $dom->createElement('argument'));
        $objectXML->setAttribute('name', $argument->getName());
        $objectXML->setAttribute('is_required', $argument->isRequired() ? 1 : 0);
        $objectXML->setAttribute('is_array', $argument->isArray() ? 1 : 0);
        $objectXML->appendChild($descriptionXML = $dom->createElement('description'));
        $descriptionXML->appendChild($dom->createTextNode($argument->getDescription()));

        $objectXML->appendChild($defaultsXML = $dom->createElement('defaults'));
        $defaults = \is_array($argument->getDefault()) ? $argument->getDefault() : (\is_bool($argument->getDefault()) ? [var_export($argument->getDefault(), true)] : ($argument->getDefault() ? [$argument->getDefault()] : []));
        foreach ($defaults as $default) {
            $defaultsXML->appendChild($defaultXML = $dom->createElement('default'));
            $defaultXML->appendChild($dom->createTextNode($default));
        }

        return $dom;
    }

    private function getInputOptionDocument(InputOption $option): \DOMDocument
    {
        $dom = new \DOMDocument('1.0', 'UTF-8');

        $dom->appendChild($objectXML = $dom->createElement('option'));
        $objectXML->setAttribute('name', '--'.$option->getName());
        $pos = strpos($option->getShortcut() ?? '', '|');
        if (false !== $pos) {
            $objectXML->setAttribute('shortcut', '-'.substr($option->getShortcut(), 0, $pos));
            $objectXML->setAttribute('shortcuts', '-'.str_replace('|', '|-', $option->getShortcut()));
        } else {
            $objectXML->setAttribute('shortcut', $option->getShortcut() ? '-'.$option->getShortcut() : '');
        }
        $objectXML->setAttribute('accept_value', $option->acceptValue() ? 1 : 0);
        $objectXML->setAttribute('is_value_required', $option->isValueRequired() ? 1 : 0);
        $objectXML->setAttribute('is_multiple', $option->isArray() ? 1 : 0);
        $objectXML->appendChild($descriptionXML = $dom->createElement('description'));
        $descriptionXML->appendChild($dom->createTextNode($option->getDescription()));

        if ($option->acceptValue()) {
            $defaults = \is_array($option->getDefault()) ? $option->getDefault() : (\is_bool($option->getDefault()) ? [var_export($option->getDefault(), true)] : ($option->getDefault() ? [$option->getDefault()] : []));
            $objectXML->appendChild($defaultsXML = $dom->createElement('defaults'));

            if (!empty($defaults)) {
                foreach ($defaults as $default) {
                    $defaultsXML->appendChild($defaultXML = $dom->createElement('default'));
                    $defaultXML->appendChild($dom->createTextNode($default));
                }
            }
        }

        if ($option->isNegatable()) {
            $dom->appendChild($objectXML = $dom->createElement('option'));
            $objectXML->setAttribute('name', '--no-'.$option->getName());
            $objectXML->setAttribute('shortcut', '');
            $objectXML->setAttribute('accept_value', 0);
            $objectXML->setAttribute('is_value_required', 0);
            $objectXML->setAttribute('is_multiple', 0);
            $objectXML->appendChild($descriptionXML = $dom->createElement('description'));
            $descriptionXML->appendChild($dom->createTextNode('Negate the "--'.$option->getName().'" option'));
        }

        return $dom;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Descriptor;

use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

/**
 * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
 *
 * @internal
 */
abstract class Descriptor implements DescriptorInterface
{
    protected OutputInterface $output;

    public function describe(OutputInterface $output, object $object, array $options = []): void
    {
        $this->output = $output;

        match (true) {
            $object instanceof InputArgument => $this->describeInputArgument($object, $options),
            $object instanceof InputOption => $this->describeInputOption($object, $options),
            $object instanceof InputDefinition => $this->describeInputDefinition($object, $options),
            $object instanceof Command => $this->describeCommand($object, $options),
            $object instanceof Application => $this->describeApplication($object, $options),
            default => throw new InvalidArgumentException(\sprintf('Object of type "%s" is not describable.', get_debug_type($object))),
        };
    }

    protected function write(string $content, bool $decorated = false): void
    {
        $this->output->write($content, false, $decorated ? OutputInterface::OUTPUT_NORMAL : OutputInterface::OUTPUT_RAW);
    }

    /**
     * Describes an InputArgument instance.
     */
    abstract protected function describeInputArgument(InputArgument $argument, array $options = []): void;

    /**
     * Describes an InputOption instance.
     */
    abstract protected function describeInputOption(InputOption $option, array $options = []): void;

    /**
     * Describes an InputDefinition instance.
     */
    abstract protected function describeInputDefinition(InputDefinition $definition, array $options = []): void;

    /**
     * Describes a Command instance.
     */
    abstract protected function describeCommand(Command $command, array $options = []): void;

    /**
     * Describes an Application instance.
     */
    abstract protected function describeApplication(Application $application, array $options = []): void;
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Descriptor;

use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\CommandNotFoundException;

/**
 * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
 *
 * @internal
 */
class ApplicationDescription
{
    public const GLOBAL_NAMESPACE = '_global';

    private Application $application;
    private ?string $namespace;
    private bool $showHidden;
    private array $namespaces;

    /**
     * @var array<string, Command>
     */
    private array $commands;

    /**
     * @var array<string, Command>
     */
    private array $aliases = [];

    public function __construct(Application $application, ?string $namespace = null, bool $showHidden = false)
    {
        $this->application = $application;
        $this->namespace = $namespace;
        $this->showHidden = $showHidden;
    }

    public function getNamespaces(): array
    {
        if (!isset($this->namespaces)) {
            $this->inspectApplication();
        }

        return $this->namespaces;
    }

    /**
     * @return Command[]
     */
    public function getCommands(): array
    {
        if (!isset($this->commands)) {
            $this->inspectApplication();
        }

        return $this->commands;
    }

    /**
     * @throws CommandNotFoundException
     */
    public function getCommand(string $name): Command
    {
        if (!isset($this->commands[$name]) && !isset($this->aliases[$name])) {
            throw new CommandNotFoundException(\sprintf('Command "%s" does not exist.', $name));
        }

        return $this->commands[$name] ?? $this->aliases[$name];
    }

    private function inspectApplication(): void
    {
        $this->commands = [];
        $this->namespaces = [];

        $all = $this->application->all($this->namespace ? $this->application->findNamespace($this->namespace) : null);
        foreach ($this->sortCommands($all) as $namespace => $commands) {
            $names = [];

            /** @var Command $command */
            foreach ($commands as $name => $command) {
                if (!$command->getName() || (!$this->showHidden && $command->isHidden())) {
                    continue;
                }

                if ($command->getName() === $name) {
                    $this->commands[$name] = $command;
                } else {
                    $this->aliases[$name] = $command;
                }

                $names[] = $name;
            }

            $this->namespaces[$namespace] = ['id' => $namespace, 'commands' => $names];
        }
    }

    private function sortCommands(array $commands): array
    {
        $namespacedCommands = [];
        $globalCommands = [];
        $sortedCommands = [];
        foreach ($commands as $name => $command) {
            $key = $this->application->extractNamespace($name, 1);
            if (\in_array($key, ['', self::GLOBAL_NAMESPACE], true)) {
                $globalCommands[$name] = $command;
            } else {
                $namespacedCommands[$key][$name] = $command;
            }
        }

        if ($globalCommands) {
            ksort($globalCommands);
            $sortedCommands[self::GLOBAL_NAMESPACE] = $globalCommands;
        }

        if ($namespacedCommands) {
            ksort($namespacedCommands, \SORT_STRING);
            foreach ($namespacedCommands as $key => $commandsSet) {
                ksort($commandsSet);
                $sortedCommands[$key] = $commandsSet;
            }
        }

        return $sortedCommands;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Descriptor;

use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\Helper;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

/**
 * Markdown descriptor.
 *
 * @author Jean-François Simon <contact@jfsimon.fr>
 *
 * @internal
 */
class MarkdownDescriptor extends Descriptor
{
    public function describe(OutputInterface $output, object $object, array $options = []): void
    {
        $decorated = $output->isDecorated();
        $output->setDecorated(false);

        parent::describe($output, $object, $options);

        $output->setDecorated($decorated);
    }

    protected function write(string $content, bool $decorated = true): void
    {
        parent::write($content, $decorated);
    }

    protected function describeInputArgument(InputArgument $argument, array $options = []): void
    {
        $this->write(
            '#### `'.($argument->getName() ?: '<none>')."`\n\n"
            .($argument->getDescription() ? preg_replace('/\s*[\r\n]\s*/', "\n", $argument->getDescription())."\n\n" : '')
            .'* Is required: '.($argument->isRequired() ? 'yes' : 'no')."\n"
            .'* Is array: '.($argument->isArray() ? 'yes' : 'no')."\n"
            .'* Default: `'.str_replace("\n", '', var_export($argument->getDefault(), true)).'`'
        );
    }

    protected function describeInputOption(InputOption $option, array $options = []): void
    {
        $name = '--'.$option->getName();
        if ($option->isNegatable()) {
            $name .= '|--no-'.$option->getName();
        }
        if ($option->getShortcut()) {
            $name .= '|-'.str_replace('|', '|-', $option->getShortcut()).'';
        }

        $this->write(
            '#### `'.$name.'`'."\n\n"
            .($option->getDescription() ? preg_replace('/\s*[\r\n]\s*/', "\n", $option->getDescription())."\n\n" : '')
            .'* Accept value: '.($option->acceptValue() ? 'yes' : 'no')."\n"
            .'* Is value required: '.($option->isValueRequired() ? 'yes' : 'no')."\n"
            .'* Is multiple: '.($option->isArray() ? 'yes' : 'no')."\n"
            .'* Is negatable: '.($option->isNegatable() ? 'yes' : 'no')."\n"
            .'* Default: `'.str_replace("\n", '', var_export($option->getDefault(), true)).'`'
        );
    }

    protected function describeInputDefinition(InputDefinition $definition, array $options = []): void
    {
        if ($showArguments = \count($definition->getArguments()) > 0) {
            $this->write('### Arguments');
            foreach ($definition->getArguments() as $argument) {
                $this->write("\n\n");
                $this->describeInputArgument($argument);
            }
        }

        if (\count($definition->getOptions()) > 0) {
            if ($showArguments) {
                $this->write("\n\n");
            }

            $this->write('### Options');
            foreach ($definition->getOptions() as $option) {
                $this->write("\n\n");
                $this->describeInputOption($option);
            }
        }
    }

    protected function describeCommand(Command $command, array $options = []): void
    {
        if ($options['short'] ?? false) {
            $this->write(
                '`'.$command->getName()."`\n"
                .str_repeat('-', Helper::width($command->getName()) + 2)."\n\n"
                .($command->getDescription() ? $command->getDescription()."\n\n" : '')
                .'### Usage'."\n\n"
                .array_reduce($command->getAliases(), static fn ($carry, $usage) => $carry.'* `'.$usage.'`'."\n")
            );

            return;
        }

        $command->mergeApplicationDefinition(false);

        $this->write(
            '`'.$command->getName()."`\n"
            .str_repeat('-', Helper::width($command->getName()) + 2)."\n\n"
            .($command->getDescription() ? $command->getDescription()."\n\n" : '')
            .'### Usage'."\n\n"
            .array_reduce(array_merge([$command->getSynopsis()], $command->getAliases(), $command->getUsages()), static fn ($carry, $usage) => $carry.'* `'.$usage.'`'."\n")
        );

        if ($help = $command->getProcessedHelp()) {
            $this->write("\n");
            $this->write($help);
        }

        $definition = $command->getDefinition();
        if ($definition->getOptions() || $definition->getArguments()) {
            $this->write("\n\n");
            $this->describeInputDefinition($definition);
        }
    }

    protected function describeApplication(Application $application, array $options = []): void
    {
        $describedNamespace = $options['namespace'] ?? null;
        $description = new ApplicationDescription($application, $describedNamespace);
        $title = $this->getApplicationTitle($application);

        $this->write($title."\n".str_repeat('=', Helper::width($title)));

        foreach ($description->getNamespaces() as $namespace) {
            if (ApplicationDescription::GLOBAL_NAMESPACE !== $namespace['id']) {
                $this->write("\n\n");
                $this->write('**'.$namespace['id'].':**');
            }

            $this->write("\n\n");
            $this->write(implode("\n", array_map(static fn ($commandName) => \sprintf('* [`%s`](#%s)', $commandName, str_replace(':', '', $description->getCommand($commandName)->getName())), $namespace['commands'])));
        }

        foreach ($description->getCommands() as $command) {
            $this->write("\n\n");
            $this->describeCommand($command, $options);
        }
    }

    private function getApplicationTitle(Application $application): string
    {
        if ('UNKNOWN' !== $application->getName()) {
            if ('UNKNOWN' !== $application->getVersion()) {
                return \sprintf('%s %s', $application->getName(), $application->getVersion());
            }

            return $application->getName();
        }

        return 'Console Tool';
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Helper;

use Symfony\Component\Console\Input\InputAwareInterface;
use Symfony\Component\Console\Input\InputInterface;

/**
 * An implementation of InputAwareInterface for Helpers.
 *
 * @author Wouter J <waldio.webdesign@gmail.com>
 */
abstract class InputAwareHelper extends Helper implements InputAwareInterface
{
    protected $input;

    /**
     * @return void
     */
    public function setInput(InputInterface $input)
    {
        $this->input = $input;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Helper;

use Symfony\Component\Console\Cursor;
use Symfony\Component\Console\Exception\MissingInputException;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\StreamableInputInterface;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\ConsoleSectionOutput;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ChoiceQuestion;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Terminal;

use function Symfony\Component\String\s;

/**
 * The QuestionHelper class provides helpers to interact with the user.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class QuestionHelper extends Helper
{
    /**
     * @var resource|null
     */
    private $inputStream;

    private static bool $stty = true;
    private static bool $stdinIsInteractive;

    /**
     * Asks a question to the user.
     *
     * @return mixed The user answer
     *
     * @throws RuntimeException If there is no data to read in the input stream
     */
    public function ask(InputInterface $input, OutputInterface $output, Question $question): mixed
    {
        if ($output instanceof ConsoleOutputInterface) {
            $output = $output->getErrorOutput();
        }

        if (!$input->isInteractive()) {
            return $this->getDefaultAnswer($question);
        }

        if ($input instanceof StreamableInputInterface && $stream = $input->getStream()) {
            $this->inputStream = $stream;
        }

        try {
            if (!$question->getValidator()) {
                return $this->doAsk($output, $question);
            }

            $interviewer = fn () => $this->doAsk($output, $question);

            return $this->validateAttempts($interviewer, $output, $question);
        } catch (MissingInputException $exception) {
            $input->setInteractive(false);

            if (null === $fallbackOutput = $this->getDefaultAnswer($question)) {
                throw $exception;
            }

            return $fallbackOutput;
        }
    }

    public function getName(): string
    {
        return 'question';
    }

    /**
     * Prevents usage of stty.
     *
     * @return void
     */
    public static function disableStty()
    {
        self::$stty = false;
    }

    /**
     * Asks the question to the user.
     *
     * @throws RuntimeException In case the fallback is deactivated and the response cannot be hidden
     */
    private function doAsk(OutputInterface $output, Question $question): mixed
    {
        $this->writePrompt($output, $question);

        $inputStream = $this->inputStream ?: \STDIN;
        $autocomplete = $question->getAutocompleterCallback();

        if (null === $autocomplete || !self::$stty || !Terminal::hasSttyAvailable()) {
            $ret = false;
            if ($question->isHidden()) {
                try {
                    $hiddenResponse = $this->getHiddenResponse($output, $inputStream, $question->isTrimmable());
                    $ret = $question->isTrimmable() ? trim($hiddenResponse) : $hiddenResponse;
                } catch (RuntimeException $e) {
                    if (!$question->isHiddenFallback()) {
                        throw $e;
                    }
                }
            }

            if (false === $ret) {
                $isBlocked = stream_get_meta_data($inputStream)['blocked'] ?? true;

                if (!$isBlocked) {
                    stream_set_blocking($inputStream, true);
                }

                $ret = $this->readInput($inputStream, $question);

                if (!$isBlocked) {
                    stream_set_blocking($inputStream, false);
                }

                if (false === $ret) {
                    throw new MissingInputException('Aborted.');
                }
                if ($question->isTrimmable()) {
                    $ret = trim($ret);
                }
            }
        } else {
            $autocomplete = $this->autocomplete($output, $question, $inputStream, $autocomplete);
            $ret = $question->isTrimmable() ? trim($autocomplete) : $autocomplete;
        }

        if ($output instanceof ConsoleSectionOutput) {
            $output->addContent(''); // add EOL to the question
            $output->addContent($ret);
        }

        $ret = \strlen($ret) > 0 ? $ret : $question->getDefault();

        if ($normalizer = $question->getNormalizer()) {
            return $normalizer($ret);
        }

        return $ret;
    }

    private function getDefaultAnswer(Question $question): mixed
    {
        $default = $question->getDefault();

        if (null === $default) {
            return $default;
        }

        if ($validator = $question->getValidator()) {
            return \call_user_func($validator, $default);
        } elseif ($question instanceof ChoiceQuestion) {
            $choices = $question->getChoices();

            if (!$question->isMultiselect()) {
                return $choices[$default] ?? $default;
            }

            $default = explode(',', $default);
            foreach ($default as $k => $v) {
                $v = $question->isTrimmable() ? trim($v) : $v;
                $default[$k] = $choices[$v] ?? $v;
            }
        }

        return $default;
    }

    /**
     * Outputs the question prompt.
     *
     * @return void
     */
    protected function writePrompt(OutputInterface $output, Question $question)
    {
        $message = $question->getQuestion();

        if ($question instanceof ChoiceQuestion) {
            $output->writeln(array_merge([
                $question->getQuestion(),
            ], $this->formatChoiceQuestionChoices($question, 'info')));

            $message = $question->getPrompt();
        }

        $output->write($message);
    }

    /**
     * @return string[]
     */
    protected function formatChoiceQuestionChoices(ChoiceQuestion $question, string $tag): array
    {
        $messages = [];

        $maxWidth = max(array_map([__CLASS__, 'width'], array_keys($choices = $question->getChoices())));

        foreach ($choices as $key => $value) {
            $padding = str_repeat(' ', $maxWidth - self::width($key));

            $messages[] = \sprintf("  [<$tag>%s$padding</$tag>] %s", $key, $value);
        }

        return $messages;
    }

    /**
     * Outputs an error message.
     *
     * @return void
     */
    protected function writeError(OutputInterface $output, \Exception $error)
    {
        if (null !== $this->getHelperSet() && $this->getHelperSet()->has('formatter')) {
            $message = $this->getHelperSet()->get('formatter')->formatBlock($error->getMessage(), 'error');
        } else {
            $message = '<error>'.$error->getMessage().'</error>';
        }

        $output->writeln($message);
    }

    /**
     * Autocompletes a question.
     *
     * @param resource $inputStream
     */
    private function autocomplete(OutputInterface $output, Question $question, $inputStream, callable $autocomplete): string
    {
        $cursor = new Cursor($output, $inputStream);

        $fullChoice = '';
        $ret = '';

        $i = 0;
        $ofs = -1;
        $matches = $autocomplete($ret);
        $numMatches = \count($matches);
        $inputHelper = new TerminalInputHelper($inputStream);

        // Disable icanon (so we can fread each keypress) and echo (we'll do echoing here instead)
        shell_exec('stty -icanon -echo');

        // Add highlighted text style
        $output->getFormatter()->setStyle('hl', new OutputFormatterStyle('black', 'white'));

        // Read a keypress
        while (!feof($inputStream)) {
            $inputHelper->waitForInput();
            $c = fread($inputStream, 1);

            // as opposed to fgets(), fread() returns an empty string when the stream content is empty, not false.
            if (false === $c || ('' === $ret && '' === $c && null === $question->getDefault())) {
                // Restore the terminal so it behaves normally again
                $inputHelper->finish();
                throw new MissingInputException('Aborted.');
            } elseif ("\177" === $c) { // Backspace Character
                if (0 === $numMatches && 0 !== $i) {
                    --$i;
                    $cursor->moveLeft(s($fullChoice)->slice(-1)->width(false));

                    $fullChoice = self::substr($fullChoice, 0, $i);
                }

                if (0 === $i) {
                    $ofs = -1;
                    $matches = $autocomplete($ret);
                    $numMatches = \count($matches);
                } else {
                    $numMatches = 0;
                }

                // Pop the last character off the end of our string
                $ret = self::substr($ret, 0, $i);
            } elseif ("\033" === $c) {
                // Did we read an escape sequence?
                $c .= fread($inputStream, 2);

                // A = Up Arrow. B = Down Arrow
                if (isset($c[2]) && ('A' === $c[2] || 'B' === $c[2])) {
                    if ('A' === $c[2] && -1 === $ofs) {
                        $ofs = 0;
                    }

                    if (0 === $numMatches) {
                        continue;
                    }

                    $ofs += ('A' === $c[2]) ? -1 : 1;
                    $ofs = ($numMatches + $ofs) % $numMatches;
                }
            } elseif ('' === $c || \ord($c) < 32) {
                if ("\t" === $c || "\n" === $c) {
                    if ($numMatches > 0 && -1 !== $ofs) {
                        $ret = (string) $matches[$ofs];
                        // Echo out remaining chars for current match
                        $remainingCharacters = substr($ret, \strlen($this->mostRecentlyEnteredValue($fullChoice)));
                        $output->write($remainingCharacters);
                        $fullChoice .= $remainingCharacters;
                        $i = (false === $encoding = mb_detect_encoding($fullChoice, null, true)) ? \strlen($fullChoice) : mb_strlen($fullChoice, $encoding);

                        $matches = array_filter(
                            $autocomplete($ret),
                            static fn ($match) => '' === $ret || str_starts_with($match, $ret)
                        );
                        $numMatches = \count($matches);
                        $ofs = -1;
                    }

                    if ("\n" === $c) {
                        $output->write($c);
                        break;
                    }

                    $numMatches = 0;
                }

                continue;
            } else {
                if ("\x80" <= $c) {
                    $c .= fread($inputStream, ["\xC0" => 1, "\xD0" => 1, "\xE0" => 2, "\xF0" => 3][$c & "\xF0"]);
                }

                $output->write($c);
                $ret .= $c;
                $fullChoice .= $c;
                ++$i;

                $tempRet = $ret;

                if ($question instanceof ChoiceQuestion && $question->isMultiselect()) {
                    $tempRet = $this->mostRecentlyEnteredValue($fullChoice);
                }

                $numMatches = 0;
                $ofs = 0;

                foreach ($autocomplete($ret) as $value) {
                    // If typed characters match the beginning chunk of value (e.g. [AcmeDe]moBundle)
                    if (str_starts_with($value, $tempRet)) {
                        $matches[$numMatches++] = $value;
                    }
                }
            }

            $cursor->clearLineAfter();

            if ($numMatches > 0 && -1 !== $ofs) {
                $cursor->savePosition();
                // Write highlighted text, complete the partially entered response
                $charactersEntered = \strlen($this->mostRecentlyEnteredValue($fullChoice));
                $output->write('<hl>'.OutputFormatter::escapeTrailingBackslash(substr($matches[$ofs], $charactersEntered)).'</hl>');
                $cursor->restorePosition();
            }
        }

        // Restore the terminal so it behaves normally again
        $inputHelper->finish();

        return $fullChoice;
    }

    private function mostRecentlyEnteredValue(string $entered): string
    {
        // Determine the most recent value that the user entered
        if (!str_contains($entered, ',')) {
            return $entered;
        }

        $choices = explode(',', $entered);
        if ('' !== $lastChoice = trim($choices[\count($choices) - 1])) {
            return $lastChoice;
        }

        return $entered;
    }

    /**
     * Gets a hidden response from user.
     *
     * @param resource $inputStream The handler resource
     * @param bool     $trimmable   Is the answer trimmable
     *
     * @throws RuntimeException In case the fallback is deactivated and the response cannot be hidden
     */
    private function getHiddenResponse(OutputInterface $output, $inputStream, bool $trimmable = true): string
    {
        if ('\\' === \DIRECTORY_SEPARATOR) {
            $exe = __DIR__.'/../Resources/bin/hiddeninput.exe';

            // handle code running from a phar
            if (str_starts_with(__FILE__, 'phar:')) {
                $tmpExe = sys_get_temp_dir().'/hiddeninput.exe';
                copy($exe, $tmpExe);
                $exe = $tmpExe;
            }

            $sExec = shell_exec('"'.$exe.'"');
            $value = $trimmable ? rtrim($sExec) : $sExec;
            $output->writeln('');

            if (isset($tmpExe)) {
                unlink($tmpExe);
            }

            return $value;
        }

        $inputHelper = null;

        if (self::$stty && Terminal::hasSttyAvailable()) {
            $inputHelper = new TerminalInputHelper($inputStream);
            shell_exec('stty -echo');
        } elseif ($this->isInteractiveInput($inputStream)) {
            throw new RuntimeException('Unable to hide the response.');
        }

        $value = $this->doReadInput($inputStream, helper: $inputHelper);

        if (4095 === \strlen($value)) {
            $errOutput = $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output;
            $errOutput->warning('The value was possibly truncated by your shell or terminal emulator');
        }

        // Restore the terminal so it behaves normally again
        $inputHelper?->finish();

        if ($trimmable) {
            $value = trim($value);
        }
        $output->writeln('');

        return $value;
    }

    /**
     * Validates an attempt.
     *
     * @param callable $interviewer A callable that will ask for a question and return the result
     *
     * @throws \Exception In case the max number of attempts has been reached and no valid response has been given
     */
    private function validateAttempts(callable $interviewer, OutputInterface $output, Question $question): mixed
    {
        $error = null;
        $attempts = $question->getMaxAttempts();

        while (null === $attempts || $attempts--) {
            if (null !== $error) {
                $this->writeError($output, $error);
            }

            try {
                return $question->getValidator()($interviewer());
            } catch (MissingInputException $e) {
                throw $error ?? $e;
            } catch (RuntimeException $e) {
                throw $e;
            } catch (\Exception $error) {
            }
        }

        throw $error;
    }

    private function isInteractiveInput($inputStream): bool
    {
        if ('php://stdin' !== (stream_get_meta_data($inputStream)['uri'] ?? null)) {
            return false;
        }

        if (isset(self::$stdinIsInteractive)) {
            return self::$stdinIsInteractive;
        }

        return self::$stdinIsInteractive = @stream_isatty(fopen('php://stdin', 'r'));
    }

    /**
     * Reads one or more lines of input and returns what is read.
     *
     * @param resource $inputStream The handler resource
     * @param Question $question    The question being asked
     */
    private function readInput($inputStream, Question $question): string|false
    {
        if (!$question->isMultiline()) {
            $cp = $this->setIOCodepage();
            $ret = $this->doReadInput($inputStream);

            return $this->resetIOCodepage($cp, $ret);
        }

        $multiLineStreamReader = $this->cloneInputStream($inputStream);
        if (null === $multiLineStreamReader) {
            return false;
        }

        $cp = $this->setIOCodepage();
        $ret = $this->doReadInput($multiLineStreamReader, "\x4");

        if (stream_get_meta_data($inputStream)['seekable']) {
            fseek($inputStream, ftell($multiLineStreamReader));
        }

        return $this->resetIOCodepage($cp, $ret);
    }

    private function setIOCodepage(): int
    {
        if (\function_exists('sapi_windows_cp_set')) {
            $cp = sapi_windows_cp_get();
            sapi_windows_cp_set(sapi_windows_cp_get('oem'));

            return $cp;
        }

        return 0;
    }

    /**
     * Sets console I/O to the specified code page and converts the user input.
     */
    private function resetIOCodepage(int $cp, string|false $input): string|false
    {
        if (0 !== $cp) {
            sapi_windows_cp_set($cp);

            if (false !== $input && '' !== $input) {
                $input = sapi_windows_cp_conv(sapi_windows_cp_get('oem'), $cp, $input);
            }
        }

        return $input;
    }

    /**
     * Clones an input stream in order to act on one instance of the same
     * stream without affecting the other instance.
     *
     * @param resource $inputStream The handler resource
     *
     * @return resource|null The cloned resource, null in case it could not be cloned
     */
    private function cloneInputStream($inputStream)
    {
        $streamMetaData = stream_get_meta_data($inputStream);
        $seekable = $streamMetaData['seekable'] ?? false;
        $mode = $streamMetaData['mode'] ?? 'rb';
        $uri = $streamMetaData['uri'] ?? null;

        if (null === $uri) {
            return null;
        }

        $cloneStream = fopen($uri, $mode);

        // For seekable and writable streams, add all the same data to the
        // cloned stream and then seek to the same offset.
        if (true === $seekable && !\in_array($mode, ['r', 'rb', 'rt'])) {
            $offset = ftell($inputStream);
            rewind($inputStream);
            stream_copy_to_stream($inputStream, $cloneStream);
            fseek($inputStream, $offset);
            fseek($cloneStream, $offset);
        }

        return $cloneStream;
    }

    /**
     * @param resource $inputStream
     */
    private function doReadInput($inputStream, ?string $exitChar = null, ?TerminalInputHelper $helper = null): string
    {
        $ret = '';
        $helper ??= new TerminalInputHelper($inputStream, false);

        while (!feof($inputStream)) {
            $helper->waitForInput();
            $char = fread($inputStream, 1);

            // as opposed to fgets(), fread() returns an empty string when the stream content is empty, not false.
            if (false === $char || ('' === $ret && '' === $char)) {
                throw new MissingInputException('Aborted.');
            }

            if (\PHP_EOL === "{$ret}{$char}" || $exitChar === $char) {
                break;
            }

            $ret .= $char;

            if (null === $exitChar && "\n" === $char) {
                break;
            }
        }

        return $ret;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Helper;

use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\LogicException;

/**
 * Defines the styles for a Table.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 * @author Саша Стаменковић <umpirsky@gmail.com>
 * @author Dany Maillard <danymaillard93b@gmail.com>
 */
class TableStyle
{
    private string $paddingChar = ' ';
    private string $horizontalOutsideBorderChar = '-';
    private string $horizontalInsideBorderChar = '-';
    private string $verticalOutsideBorderChar = '|';
    private string $verticalInsideBorderChar = '|';
    private string $crossingChar = '+';
    private string $crossingTopRightChar = '+';
    private string $crossingTopMidChar = '+';
    private string $crossingTopLeftChar = '+';
    private string $crossingMidRightChar = '+';
    private string $crossingBottomRightChar = '+';
    private string $crossingBottomMidChar = '+';
    private string $crossingBottomLeftChar = '+';
    private string $crossingMidLeftChar = '+';
    private string $crossingTopLeftBottomChar = '+';
    private string $crossingTopMidBottomChar = '+';
    private string $crossingTopRightBottomChar = '+';
    private string $headerTitleFormat = '<fg=black;bg=white;options=bold> %s </>';
    private string $footerTitleFormat = '<fg=black;bg=white;options=bold> %s </>';
    private string $cellHeaderFormat = '<info>%s</info>';
    private string $cellRowFormat = '%s';
    private string $cellRowContentFormat = ' %s ';
    private string $borderFormat = '%s';
    private int $padType = \STR_PAD_RIGHT;

    /**
     * Sets padding character, used for cell padding.
     *
     * @return $this
     */
    public function setPaddingChar(string $paddingChar): static
    {
        if (!$paddingChar) {
            throw new LogicException('The padding char must not be empty.');
        }

        $this->paddingChar = $paddingChar;

        return $this;
    }

    /**
     * Gets padding character, used for cell padding.
     */
    public function getPaddingChar(): string
    {
        return $this->paddingChar;
    }

    /**
     * Sets horizontal border characters.
     *
     * <code>
     * ╔═══════════════╤══════════════════════════╤══════════════════╗
     * ║ ISBN          │ Title                    │ Author           ║
     * ╠═══════1═══════╪══════════════════════════╪══════════════════╣
     * ║ 99921-58-10-7 │ Divine Comedy            │ Dante Alighieri  ║
     * ║ 9971-5-0210-0 │ A Tale of Two Cities     │ Charles Dickens  ║
     * ╟───────2───────┼──────────────────────────┼──────────────────╢
     * ║ 960-425-059-0 │ The Lord of the Rings    │ J. R. R. Tolkien ║
     * ║ 80-902734-1-6 │ And Then There Were None │ Agatha Christie  ║
     * ╚═══════════════╧══════════════════════════╧══════════════════╝
     * </code>
     *
     * @return $this
     */
    public function setHorizontalBorderChars(string $outside, ?string $inside = null): static
    {
        $this->horizontalOutsideBorderChar = $outside;
        $this->horizontalInsideBorderChar = $inside ?? $outside;

        return $this;
    }

    /**
     * Sets vertical border characters.
     *
     * <code>
     * ╔═══════════════╤══════════════════════════╤══════════════════╗
     * 1 ISBN          2 Title                    │ Author           ║
     * ╠═══════════════╪══════════════════════════╪══════════════════╣
     * ║ 99921-58-10-7 │ Divine Comedy            │ Dante Alighieri  ║
     * ║ 9971-5-0210-0 │ A Tale of Two Cities     │ Charles Dickens  ║
     * ║ 960-425-059-0 │ The Lord of the Rings    │ J. R. R. Tolkien ║
     * ║ 80-902734-1-6 │ And Then There Were None │ Agatha Christie  ║
     * ╚═══════════════╧══════════════════════════╧══════════════════╝
     * </code>
     *
     * @return $this
     */
    public function setVerticalBorderChars(string $outside, ?string $inside = null): static
    {
        $this->verticalOutsideBorderChar = $outside;
        $this->verticalInsideBorderChar = $inside ?? $outside;

        return $this;
    }

    /**
     * Gets border characters.
     *
     * @internal
     */
    public function getBorderChars(): array
    {
        return [
            $this->horizontalOutsideBorderChar,
            $this->verticalOutsideBorderChar,
            $this->horizontalInsideBorderChar,
            $this->verticalInsideBorderChar,
        ];
    }

    /**
     * Sets crossing characters.
     *
     * Example:
     * <code>
     * 1═══════════════2══════════════════════════2══════════════════3
     * ║ ISBN          │ Title                    │ Author           ║
     * 8'══════════════0'═════════════════════════0'═════════════════4'
     * ║ 99921-58-10-7 │ Divine Comedy            │ Dante Alighieri  ║
     * ║ 9971-5-0210-0 │ A Tale of Two Cities     │ Charles Dickens  ║
     * 8───────────────0──────────────────────────0──────────────────4
     * ║ 960-425-059-0 │ The Lord of the Rings    │ J. R. R. Tolkien ║
     * ║ 80-902734-1-6 │ And Then There Were None │ Agatha Christie  ║
     * 7═══════════════6══════════════════════════6══════════════════5
     * </code>
     *
     * @param string      $cross          Crossing char (see #0 of example)
     * @param string      $topLeft        Top left char (see #1 of example)
     * @param string      $topMid         Top mid char (see #2 of example)
     * @param string      $topRight       Top right char (see #3 of example)
     * @param string      $midRight       Mid right char (see #4 of example)
     * @param string      $bottomRight    Bottom right char (see #5 of example)
     * @param string      $bottomMid      Bottom mid char (see #6 of example)
     * @param string      $bottomLeft     Bottom left char (see #7 of example)
     * @param string      $midLeft        Mid left char (see #8 of example)
     * @param string|null $topLeftBottom  Top left bottom char (see #8' of example), equals to $midLeft if null
     * @param string|null $topMidBottom   Top mid bottom char (see #0' of example), equals to $cross if null
     * @param string|null $topRightBottom Top right bottom char (see #4' of example), equals to $midRight if null
     *
     * @return $this
     */
    public function setCrossingChars(string $cross, string $topLeft, string $topMid, string $topRight, string $midRight, string $bottomRight, string $bottomMid, string $bottomLeft, string $midLeft, ?string $topLeftBottom = null, ?string $topMidBottom = null, ?string $topRightBottom = null): static
    {
        $this->crossingChar = $cross;
        $this->crossingTopLeftChar = $topLeft;
        $this->crossingTopMidChar = $topMid;
        $this->crossingTopRightChar = $topRight;
        $this->crossingMidRightChar = $midRight;
        $this->crossingBottomRightChar = $bottomRight;
        $this->crossingBottomMidChar = $bottomMid;
        $this->crossingBottomLeftChar = $bottomLeft;
        $this->crossingMidLeftChar = $midLeft;
        $this->crossingTopLeftBottomChar = $topLeftBottom ?? $midLeft;
        $this->crossingTopMidBottomChar = $topMidBottom ?? $cross;
        $this->crossingTopRightBottomChar = $topRightBottom ?? $midRight;

        return $this;
    }

    /**
     * Sets default crossing character used for each cross.
     *
     * @see {@link setCrossingChars()} for setting each crossing individually.
     */
    public function setDefaultCrossingChar(string $char): self
    {
        return $this->setCrossingChars($char, $char, $char, $char, $char, $char, $char, $char, $char);
    }

    /**
     * Gets crossing character.
     */
    public function getCrossingChar(): string
    {
        return $this->crossingChar;
    }

    /**
     * Gets crossing characters.
     *
     * @internal
     */
    public function getCrossingChars(): array
    {
        return [
            $this->crossingChar,
            $this->crossingTopLeftChar,
            $this->crossingTopMidChar,
            $this->crossingTopRightChar,
            $this->crossingMidRightChar,
            $this->crossingBottomRightChar,
            $this->crossingBottomMidChar,
            $this->crossingBottomLeftChar,
            $this->crossingMidLeftChar,
            $this->crossingTopLeftBottomChar,
            $this->crossingTopMidBottomChar,
            $this->crossingTopRightBottomChar,
        ];
    }

    /**
     * Sets header cell format.
     *
     * @return $this
     */
    public function setCellHeaderFormat(string $cellHeaderFormat): static
    {
        $this->cellHeaderFormat = $cellHeaderFormat;

        return $this;
    }

    /**
     * Gets header cell format.
     */
    public function getCellHeaderFormat(): string
    {
        return $this->cellHeaderFormat;
    }

    /**
     * Sets row cell format.
     *
     * @return $this
     */
    public function setCellRowFormat(string $cellRowFormat): static
    {
        $this->cellRowFormat = $cellRowFormat;

        return $this;
    }

    /**
     * Gets row cell format.
     */
    public function getCellRowFormat(): string
    {
        return $this->cellRowFormat;
    }

    /**
     * Sets row cell content format.
     *
     * @return $this
     */
    public function setCellRowContentFormat(string $cellRowContentFormat): static
    {
        $this->cellRowContentFormat = $cellRowContentFormat;

        return $this;
    }

    /**
     * Gets row cell content format.
     */
    public function getCellRowContentFormat(): string
    {
        return $this->cellRowContentFormat;
    }

    /**
     * Sets table border format.
     *
     * @return $this
     */
    public function setBorderFormat(string $borderFormat): static
    {
        $this->borderFormat = $borderFormat;

        return $this;
    }

    /**
     * Gets table border format.
     */
    public function getBorderFormat(): string
    {
        return $this->borderFormat;
    }

    /**
     * Sets cell padding type.
     *
     * @return $this
     */
    public function setPadType(int $padType): static
    {
        if (!\in_array($padType, [\STR_PAD_LEFT, \STR_PAD_RIGHT, \STR_PAD_BOTH], true)) {
            throw new InvalidArgumentException('Invalid padding type. Expected one of (STR_PAD_LEFT, STR_PAD_RIGHT, STR_PAD_BOTH).');
        }

        $this->padType = $padType;

        return $this;
    }

    /**
     * Gets cell padding type.
     */
    public function getPadType(): int
    {
        return $this->padType;
    }

    public function getHeaderTitleFormat(): string
    {
        return $this->headerTitleFormat;
    }

    /**
     * @return $this
     */
    public function setHeaderTitleFormat(string $format): static
    {
        $this->headerTitleFormat = $format;

        return $this;
    }

    public function getFooterTitleFormat(): string
    {
        return $this->footerTitleFormat;
    }

    /**
     * @return $this
     */
    public function setFooterTitleFormat(string $format): static
    {
        $this->footerTitleFormat = $format;

        return $this;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Helper;

/**
 * HelperInterface is the interface all helpers must implement.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
interface HelperInterface
{
    /**
     * Sets the helper set associated with this helper.
     *
     * @return void
     */
    public function setHelperSet(?HelperSet $helperSet);

    /**
     * Gets the helper set associated with this helper.
     */
    public function getHelperSet(): ?HelperSet;

    /**
     * Returns the canonical name of this helper.
     *
     * @return string
     */
    public function getName();
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Helper;

/**
 * Marks a row as being a separator.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class TableSeparator extends TableCell
{
    public function __construct(array $options = [])
    {
        parent::__construct('', $options);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Helper;

use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;

/**
 * The ProcessHelper class provides helpers to run external processes.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 *
 * @final
 */
class ProcessHelper extends Helper
{
    /**
     * Runs an external process.
     *
     * @param array|Process $cmd      An instance of Process or an array of the command and arguments
     * @param callable|null $callback A PHP callback to run whenever there is some
     *                                output available on STDOUT or STDERR
     */
    public function run(OutputInterface $output, array|Process $cmd, ?string $error = null, ?callable $callback = null, int $verbosity = OutputInterface::VERBOSITY_VERY_VERBOSE): Process
    {
        if (!class_exists(Process::class)) {
            throw new \LogicException('The ProcessHelper cannot be run as the Process component is not installed. Try running "compose require symfony/process".');
        }

        if ($output instanceof ConsoleOutputInterface) {
            $output = $output->getErrorOutput();
        }

        $formatter = $this->getHelperSet()->get('debug_formatter');

        if ($cmd instanceof Process) {
            $cmd = [$cmd];
        }

        if (\is_string($cmd[0] ?? null)) {
            $process = new Process($cmd);
            $cmd = [];
        } elseif (($cmd[0] ?? null) instanceof Process) {
            $process = $cmd[0];
            unset($cmd[0]);
        } else {
            throw new \InvalidArgumentException(\sprintf('Invalid command provided to "%s()": the command should be an array whose first element is either the path to the binary to run or a "Process" object.', __METHOD__));
        }

        if ($verbosity <= $output->getVerbosity()) {
            $output->write($formatter->start(spl_object_hash($process), $this->escapeString($process->getCommandLine())));
        }

        if ($output->isDebug()) {
            $callback = $this->wrapCallback($output, $process, $callback);
        }

        $process->run($callback, $cmd);

        if ($verbosity <= $output->getVerbosity()) {
            $message = $process->isSuccessful() ? 'Command ran successfully' : \sprintf('%s Command did not run successfully', $process->getExitCode());
            $output->write($formatter->stop(spl_object_hash($process), $message, $process->isSuccessful()));
        }

        if (!$process->isSuccessful() && null !== $error) {
            $output->writeln(\sprintf('<error>%s</error>', $this->escapeString($error)));
        }

        return $process;
    }

    /**
     * Runs the process.
     *
     * This is identical to run() except that an exception is thrown if the process
     * exits with a non-zero exit code.
     *
     * @param array|Process $cmd      An instance of Process or a command to run
     * @param callable|null $callback A PHP callback to run whenever there is some
     *                                output available on STDOUT or STDERR
     *
     * @throws ProcessFailedException
     *
     * @see run()
     */
    public function mustRun(OutputInterface $output, array|Process $cmd, ?string $error = null, ?callable $callback = null): Process
    {
        $process = $this->run($output, $cmd, $error, $callback);

        if (!$process->isSuccessful()) {
            throw new ProcessFailedException($process);
        }

        return $process;
    }

    /**
     * Wraps a Process callback to add debugging output.
     */
    public function wrapCallback(OutputInterface $output, Process $process, ?callable $callback = null): callable
    {
        if ($output instanceof ConsoleOutputInterface) {
            $output = $output->getErrorOutput();
        }

        $formatter = $this->getHelperSet()->get('debug_formatter');

        return function ($type, $buffer) use ($output, $process, $callback, $formatter) {
            $output->write($formatter->progress(spl_object_hash($process), $this->escapeString($buffer), Process::ERR === $type));

            if (null !== $callback) {
                $callback($type, $buffer);
            }
        };
    }

    private function escapeString(string $str): string
    {
        return str_replace('<', '\\<', $str);
    }

    public function getName(): string
    {
        return 'process';
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Helper;

use Symfony\Component\Console\Exception\InvalidArgumentException;

/**
 * HelperSet represents a set of helpers to be used with a command.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 *
 * @implements \IteratorAggregate<string, HelperInterface>
 */
class HelperSet implements \IteratorAggregate
{
    /** @var array<string, HelperInterface> */
    private array $helpers = [];

    /**
     * @param HelperInterface[] $helpers
     */
    public function __construct(array $helpers = [])
    {
        foreach ($helpers as $alias => $helper) {
            $this->set($helper, \is_int($alias) ? null : $alias);
        }
    }

    /**
     * @return void
     */
    public function set(HelperInterface $helper, ?string $alias = null)
    {
        $this->helpers[$helper->getName()] = $helper;
        if (null !== $alias) {
            $this->helpers[$alias] = $helper;
        }

        $helper->setHelperSet($this);
    }

    /**
     * Returns true if the helper if defined.
     */
    public function has(string $name): bool
    {
        return isset($this->helpers[$name]);
    }

    /**
     * Gets a helper value.
     *
     * @throws InvalidArgumentException if the helper is not defined
     */
    public function get(string $name): HelperInterface
    {
        if (!$this->has($name)) {
            throw new InvalidArgumentException(\sprintf('The helper "%s" is not defined.', $name));
        }

        return $this->helpers[$name];
    }

    public function getIterator(): \Traversable
    {
        return new \ArrayIterator($this->helpers);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Helper;

use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\VarDumper\Cloner\ClonerInterface;
use Symfony\Component\VarDumper\Cloner\VarCloner;
use Symfony\Component\VarDumper\Dumper\CliDumper;

/**
 * @author Roland Franssen <franssen.roland@gmail.com>
 */
final class Dumper
{
    private OutputInterface $output;
    private ?CliDumper $dumper;
    private ?ClonerInterface $cloner;
    private \Closure $handler;

    public function __construct(OutputInterface $output, ?CliDumper $dumper = null, ?ClonerInterface $cloner = null)
    {
        $this->output = $output;
        $this->dumper = $dumper;
        $this->cloner = $cloner;

        if (class_exists(CliDumper::class)) {
            $this->handler = function ($var): string {
                $dumper = $this->dumper ??= new CliDumper(null, null, CliDumper::DUMP_LIGHT_ARRAY | CliDumper::DUMP_COMMA_SEPARATOR);
                $dumper->setColors($this->output->isDecorated());

                return rtrim($dumper->dump(($this->cloner ??= new VarCloner())->cloneVar($var)->withRefHandles(false), true));
            };
        } else {
            $this->handler = static fn ($var): string => match (true) {
                null === $var => 'null',
                true === $var => 'true',
                false === $var => 'false',
                \is_string($var) => '"'.$var.'"',
                default => rtrim(print_r($var, true)),
            };
        }
    }

    public function __invoke(mixed $var): string
    {
        return ($this->handler)($var);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Helper;

use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ChoiceQuestion;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Style\SymfonyStyle;

/**
 * Symfony Style Guide compliant question helper.
 *
 * @author Kevin Bond <kevinbond@gmail.com>
 */
class SymfonyQuestionHelper extends QuestionHelper
{
    /**
     * @return void
     */
    protected function writePrompt(OutputInterface $output, Question $question)
    {
        $text = OutputFormatter::escapeTrailingBackslash($question->getQuestion());
        $default = $question->getDefault();

        if ($question->isMultiline()) {
            $text .= \sprintf(' (press %s to continue)', $this->getEofShortcut($output));
        }

        switch (true) {
            case null === $default:
                $text = \sprintf(' <info>%s</info>:', $text);

                break;

            case $question instanceof ConfirmationQuestion:
                $text = \sprintf(' <info>%s (yes/no)</info> [<comment>%s</comment>]:', $text, $default ? 'yes' : 'no');

                break;

            case $question instanceof ChoiceQuestion && $question->isMultiselect():
                $choices = $question->getChoices();
                $default = explode(',', $default);

                foreach ($default as $key => $value) {
                    $default[$key] = $choices[trim($value)];
                }

                $text = \sprintf(' <info>%s</info> [<comment>%s</comment>]:', $text, implode(', ', $default));

                break;

            case $question instanceof ChoiceQuestion:
                $choices = $question->getChoices();
                $text = \sprintf(' <info>%s</info> [<comment>%s</comment>]:', $text, $choices[$default] ?? $default);

                break;

            default:
                $text = \sprintf(' <info>%s</info> [<comment>%s</comment>]:', $text, OutputFormatter::escape($default));
        }

        $output->writeln($text);

        $prompt = ' > ';

        if ($question instanceof ChoiceQuestion) {
            $output->writeln($this->formatChoiceQuestionChoices($question, 'comment'));

            $prompt = $question->getPrompt();
        }

        $output->write($prompt);
    }

    /**
     * @return void
     */
    protected function writeError(OutputInterface $output, \Exception $error)
    {
        if ($output instanceof SymfonyStyle) {
            $output->newLine();
            $output->error($error->getMessage());

            return;
        }

        parent::writeError($output, $error);
    }

    private function getEofShortcut(OutputInterface $output): string
    {
        if ('\\' === \DIRECTORY_SEPARATOR && !$output->isDecorated()) {
            return '<comment>Ctrl+Z</comment> then <comment>Enter</comment>';
        }

        return '<comment>Ctrl+D</comment>';
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Helper;

use Symfony\Component\Console\Formatter\OutputFormatter;

/**
 * The Formatter class provides helpers to format messages.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class FormatterHelper extends Helper
{
    /**
     * Formats a message within a section.
     */
    public function formatSection(string $section, string $message, string $style = 'info'): string
    {
        return \sprintf('<%s>[%s]</%s> %s', $style, $section, $style, $message);
    }

    /**
     * Formats a message as a block of text.
     */
    public function formatBlock(string|array $messages, string $style, bool $large = false): string
    {
        if (!\is_array($messages)) {
            $messages = [$messages];
        }

        $len = 0;
        $lines = [];
        foreach ($messages as $message) {
            $message = OutputFormatter::escape($message);
            $lines[] = \sprintf($large ? '  %s  ' : ' %s ', $message);
            $len = max(self::width($message) + ($large ? 4 : 2), $len);
        }

        $messages = $large ? [str_repeat(' ', $len)] : [];
        for ($i = 0; isset($lines[$i]); ++$i) {
            $messages[] = $lines[$i].str_repeat(' ', $len - self::width($lines[$i]));
        }
        if ($large) {
            $messages[] = str_repeat(' ', $len);
        }

        for ($i = 0; isset($messages[$i]); ++$i) {
            $messages[$i] = \sprintf('<%s>%s</%s>', $style, $messages[$i], $style);
        }

        return implode("\n", $messages);
    }

    /**
     * Truncates a message to the given length.
     */
    public function truncate(string $message, int $length, string $suffix = '...'): string
    {
        $computedLength = $length - self::width($suffix);

        if ($computedLength > self::width($message)) {
            return $message;
        }

        return self::substr($message, 0, $length).$suffix;
    }

    public function getName(): string
    {
        return 'formatter';
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Helper;

use Symfony\Component\Console\Exception\InvalidArgumentException;

/**
 * @author Abdellatif Ait boudad <a.aitboudad@gmail.com>
 */
class TableCell
{
    private string $value;
    private array $options = [
        'rowspan' => 1,
        'colspan' => 1,
        'style' => null,
    ];

    public function __construct(string $value = '', array $options = [])
    {
        $this->value = $value;

        // check option names
        if ($diff = array_diff(array_keys($options), array_keys($this->options))) {
            throw new InvalidArgumentException(\sprintf('The TableCell does not support the following options: \'%s\'.', implode('\', \'', $diff)));
        }

        if (isset($options['style']) && !$options['style'] instanceof TableCellStyle) {
            throw new InvalidArgumentException('The style option must be an instance of "TableCellStyle".');
        }

        $this->options = array_merge($this->options, $options);
    }

    /**
     * Returns the cell value.
     */
    public function __toString(): string
    {
        return $this->value;
    }

    /**
     * Gets number of colspan.
     */
    public function getColspan(): int
    {
        return (int) $this->options['colspan'];
    }

    /**
     * Gets number of rowspan.
     */
    public function getRowspan(): int
    {
        return (int) $this->options['rowspan'];
    }

    public function getStyle(): ?TableCellStyle
    {
        return $this->options['style'];
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Helper;

use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Console\Output\ConsoleSectionOutput;
use Symfony\Component\Console\Output\OutputInterface;

/**
 * @author Kevin Bond <kevinbond@gmail.com>
 */
class ProgressIndicator
{
    private const FORMATS = [
        'normal' => ' %indicator% %message%',
        'normal_no_ansi' => ' %message%',

        'verbose' => ' %indicator% %message% (%elapsed:6s%)',
        'verbose_no_ansi' => ' %message% (%elapsed:6s%)',

        'very_verbose' => ' %indicator% %message% (%elapsed:6s%, %memory:6s%)',
        'very_verbose_no_ansi' => ' %message% (%elapsed:6s%, %memory:6s%)',
    ];

    private OutputInterface $output;
    private int $startTime;
    private ?string $format = null;
    private ?string $message = null;
    private array $indicatorValues;
    private int $indicatorCurrent;
    private int $indicatorChangeInterval;
    private float $indicatorUpdateTime;
    private bool $started = false;

    /**
     * @var array<string, callable>
     */
    private static array $formatters;

    /**
     * @param int        $indicatorChangeInterval Change interval in milliseconds
     * @param array|null $indicatorValues         Animated indicator characters
     */
    public function __construct(OutputInterface $output, ?string $format = null, int $indicatorChangeInterval = 100, ?array $indicatorValues = null)
    {
        $this->output = $output;

        $format ??= $this->determineBestFormat();
        $indicatorValues ??= ['-', '\\', '|', '/'];
        $indicatorValues = array_values($indicatorValues);

        if (2 > \count($indicatorValues)) {
            throw new InvalidArgumentException('Must have at least 2 indicator value characters.');
        }

        $this->format = self::getFormatDefinition($format);
        $this->indicatorChangeInterval = $indicatorChangeInterval;
        $this->indicatorValues = $indicatorValues;
        $this->startTime = time();
    }

    /**
     * Sets the current indicator message.
     *
     * @return void
     */
    public function setMessage(?string $message)
    {
        $this->message = $message;

        $this->display();
    }

    /**
     * Starts the indicator output.
     *
     * @return void
     */
    public function start(string $message)
    {
        if ($this->started) {
            throw new LogicException('Progress indicator already started.');
        }

        $this->message = $message;
        $this->started = true;
        $this->startTime = time();
        $this->indicatorUpdateTime = $this->getCurrentTimeInMilliseconds() + $this->indicatorChangeInterval;
        $this->indicatorCurrent = 0;

        $this->display();
    }

    /**
     * Advances the indicator.
     *
     * @return void
     */
    public function advance()
    {
        if (!$this->started) {
            throw new LogicException('Progress indicator has not yet been started.');
        }

        if (!$this->output->isDecorated()) {
            return;
        }

        $currentTime = $this->getCurrentTimeInMilliseconds();

        if ($currentTime < $this->indicatorUpdateTime) {
            return;
        }

        $this->indicatorUpdateTime = $currentTime + $this->indicatorChangeInterval;
        ++$this->indicatorCurrent;

        $this->display();
    }

    /**
     * Finish the indicator with message.
     *
     * @return void
     */
    public function finish(string $message)
    {
        if (!$this->started) {
            throw new LogicException('Progress indicator has not yet been started.');
        }

        $this->message = $message;
        $this->display();
        if (!$this->output instanceof ConsoleSectionOutput) {
            $this->output->writeln('');
        }
        $this->started = false;
    }

    /**
     * Gets the format for a given name.
     */
    public static function getFormatDefinition(string $name): ?string
    {
        return self::FORMATS[$name] ?? null;
    }

    /**
     * Sets a placeholder formatter for a given name.
     *
     * This method also allow you to override an existing placeholder.
     *
     * @return void
     */
    public static function setPlaceholderFormatterDefinition(string $name, callable $callable)
    {
        self::$formatters ??= self::initPlaceholderFormatters();

        self::$formatters[$name] = $callable;
    }

    /**
     * Gets the placeholder formatter for a given name (including the delimiter char like %).
     */
    public static function getPlaceholderFormatterDefinition(string $name): ?callable
    {
        self::$formatters ??= self::initPlaceholderFormatters();

        return self::$formatters[$name] ?? null;
    }

    private function display(): void
    {
        if (OutputInterface::VERBOSITY_QUIET === $this->output->getVerbosity()) {
            return;
        }

        $this->overwrite(preg_replace_callback("{%([a-z\-_]+)(?:\:([^%]+))?%}i", function ($matches) {
            if ($formatter = self::getPlaceholderFormatterDefinition($matches[1])) {
                return $formatter($this);
            }

            return $matches[0];
        }, $this->format ?? ''));
    }

    private function determineBestFormat(): string
    {
        return match ($this->output->getVerbosity()) {
            // OutputInterface::VERBOSITY_QUIET: display is disabled anyway
            OutputInterface::VERBOSITY_VERBOSE => $this->output->isDecorated() ? 'verbose' : 'verbose_no_ansi',
            OutputInterface::VERBOSITY_VERY_VERBOSE,
            OutputInterface::VERBOSITY_DEBUG => $this->output->isDecorated() ? 'very_verbose' : 'very_verbose_no_ansi',
            default => $this->output->isDecorated() ? 'normal' : 'normal_no_ansi',
        };
    }

    /**
     * Overwrites a previous message to the output.
     */
    private function overwrite(string $message): void
    {
        if ($this->output instanceof ConsoleSectionOutput) {
            $this->output->overwrite($message);
        } elseif ($this->output->isDecorated()) {
            $this->output->write("\x0D\x1B[2K");
            $this->output->write($message);
        } else {
            $this->output->writeln($message);
        }
    }

    private function getCurrentTimeInMilliseconds(): float
    {
        return round(microtime(true) * 1000);
    }

    /**
     * @return array<string, \Closure>
     */
    private static function initPlaceholderFormatters(): array
    {
        return [
            'indicator' => static fn (self $indicator) => $indicator->indicatorValues[$indicator->indicatorCurrent % \count($indicator->indicatorValues)],
            'message' => static fn (self $indicator) => $indicator->message,
            'elapsed' => static fn (self $indicator) => Helper::formatTime(time() - $indicator->startTime, 2),
            'memory' => static fn () => Helper::formatMemory(memory_get_usage(true)),
        ];
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Helper;

use Symfony\Component\Console\Descriptor\DescriptorInterface;
use Symfony\Component\Console\Descriptor\JsonDescriptor;
use Symfony\Component\Console\Descriptor\MarkdownDescriptor;
use Symfony\Component\Console\Descriptor\ReStructuredTextDescriptor;
use Symfony\Component\Console\Descriptor\TextDescriptor;
use Symfony\Component\Console\Descriptor\XmlDescriptor;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Output\OutputInterface;

/**
 * This class adds helper method to describe objects in various formats.
 *
 * @author Jean-François Simon <contact@jfsimon.fr>
 */
class DescriptorHelper extends Helper
{
    /**
     * @var DescriptorInterface[]
     */
    private array $descriptors = [];

    public function __construct()
    {
        $this
            ->register('txt', new TextDescriptor())
            ->register('xml', new XmlDescriptor())
            ->register('json', new JsonDescriptor())
            ->register('md', new MarkdownDescriptor())
            ->register('rst', new ReStructuredTextDescriptor())
        ;
    }

    /**
     * Describes an object if supported.
     *
     * Available options are:
     * * format: string, the output format name
     * * raw_text: boolean, sets output type as raw
     *
     * @return void
     *
     * @throws InvalidArgumentException when the given format is not supported
     */
    public function describe(OutputInterface $output, ?object $object, array $options = [])
    {
        $options = array_merge([
            'raw_text' => false,
            'format' => 'txt',
        ], $options);

        if (!isset($this->descriptors[$options['format']])) {
            throw new InvalidArgumentException(\sprintf('Unsupported format "%s".', $options['format']));
        }

        $descriptor = $this->descriptors[$options['format']];
        $descriptor->describe($output, $object, $options);
    }

    /**
     * Registers a descriptor.
     *
     * @return $this
     */
    public function register(string $format, DescriptorInterface $descriptor): static
    {
        $this->descriptors[$format] = $descriptor;

        return $this;
    }

    public function getName(): string
    {
        return 'descriptor';
    }

    public function getFormats(): array
    {
        return array_keys($this->descriptors);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Helper;

/**
 * Helps outputting debug information when running an external program from a command.
 *
 * An external program can be a Process, an HTTP request, or anything else.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class DebugFormatterHelper extends Helper
{
    private const COLORS = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white', 'default'];
    private array $started = [];
    private int $count = -1;

    /**
     * Starts a debug formatting session.
     */
    public function start(string $id, string $message, string $prefix = 'RUN'): string
    {
        $this->started[$id] = ['border' => ++$this->count % \count(self::COLORS)];

        return \sprintf("%s<bg=blue;fg=white> %s </> <fg=blue>%s</>\n", $this->getBorder($id), $prefix, $message);
    }

    /**
     * Adds progress to a formatting session.
     */
    public function progress(string $id, string $buffer, bool $error = false, string $prefix = 'OUT', string $errorPrefix = 'ERR'): string
    {
        $message = '';

        if ($error) {
            if (isset($this->started[$id]['out'])) {
                $message .= "\n";
                unset($this->started[$id]['out']);
            }
            if (!isset($this->started[$id]['err'])) {
                $message .= \sprintf('%s<bg=red;fg=white> %s </> ', $this->getBorder($id), $errorPrefix);
                $this->started[$id]['err'] = true;
            }

            $message .= str_replace("\n", \sprintf("\n%s<bg=red;fg=white> %s </> ", $this->getBorder($id), $errorPrefix), $buffer);
        } else {
            if (isset($this->started[$id]['err'])) {
                $message .= "\n";
                unset($this->started[$id]['err']);
            }
            if (!isset($this->started[$id]['out'])) {
                $message .= \sprintf('%s<bg=green;fg=white> %s </> ', $this->getBorder($id), $prefix);
                $this->started[$id]['out'] = true;
            }

            $message .= str_replace("\n", \sprintf("\n%s<bg=green;fg=white> %s </> ", $this->getBorder($id), $prefix), $buffer);
        }

        return $message;
    }

    /**
     * Stops a formatting session.
     */
    public function stop(string $id, string $message, bool $successful, string $prefix = 'RES'): string
    {
        $trailingEOL = isset($this->started[$id]['out']) || isset($this->started[$id]['err']) ? "\n" : '';

        if ($successful) {
            return \sprintf("%s%s<bg=green;fg=white> %s </> <fg=green>%s</>\n", $trailingEOL, $this->getBorder($id), $prefix, $message);
        }

        $message = \sprintf("%s%s<bg=red;fg=white> %s </> <fg=red>%s</>\n", $trailingEOL, $this->getBorder($id), $prefix, $message);

        unset($this->started[$id]['out'], $this->started[$id]['err']);

        return $message;
    }

    private function getBorder(string $id): string
    {
        return \sprintf('<bg=%s> </>', self::COLORS[$this->started[$id]['border']]);
    }

    public function getName(): string
    {
        return 'debug_formatter';
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Helper;

use Symfony\Component\Console\Cursor;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\ConsoleSectionOutput;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Terminal;

/**
 * The ProgressBar provides helpers to display progress output.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 * @author Chris Jones <leeked@gmail.com>
 */
final class ProgressBar
{
    public const FORMAT_VERBOSE = 'verbose';
    public const FORMAT_VERY_VERBOSE = 'very_verbose';
    public const FORMAT_DEBUG = 'debug';
    public const FORMAT_NORMAL = 'normal';

    private const FORMAT_VERBOSE_NOMAX = 'verbose_nomax';
    private const FORMAT_VERY_VERBOSE_NOMAX = 'very_verbose_nomax';
    private const FORMAT_DEBUG_NOMAX = 'debug_nomax';
    private const FORMAT_NORMAL_NOMAX = 'normal_nomax';

    private int $barWidth = 28;
    private string $barChar;
    private string $emptyBarChar = '-';
    private string $progressChar = '>';
    private ?string $format = null;
    private ?string $internalFormat = null;
    private ?int $redrawFreq = 1;
    private int $writeCount = 0;
    private float $lastWriteTime = 0;
    private float $minSecondsBetweenRedraws = 0;
    private float $maxSecondsBetweenRedraws = 1;
    private OutputInterface $output;
    private int $step = 0;
    private int $startingStep = 0;
    private ?int $max = null;
    private int $startTime;
    private int $stepWidth;
    private float $percent = 0.0;
    private array $messages = [];
    private bool $overwrite = true;
    private Terminal $terminal;
    private ?string $previousMessage = null;
    private Cursor $cursor;
    private array $placeholders = [];

    private static array $formatters;
    private static array $formats;

    /**
     * @param int $max Maximum steps (0 if unknown)
     */
    public function __construct(OutputInterface $output, int $max = 0, float $minSecondsBetweenRedraws = 1 / 25)
    {
        if ($output instanceof ConsoleOutputInterface) {
            $output = $output->getErrorOutput();
        }

        $this->output = $output;
        $this->setMaxSteps($max);
        $this->terminal = new Terminal();

        if (0 < $minSecondsBetweenRedraws) {
            $this->redrawFreq = null;
            $this->minSecondsBetweenRedraws = $minSecondsBetweenRedraws;
        }

        if (!$this->output->isDecorated()) {
            // disable overwrite when output does not support ANSI codes.
            $this->overwrite = false;

            // set a reasonable redraw frequency so output isn't flooded
            $this->redrawFreq = null;
        }

        $this->startTime = time();
        $this->cursor = new Cursor($output);
    }

    /**
     * Sets a placeholder formatter for a given name, globally for all instances of ProgressBar.
     *
     * This method also allow you to override an existing placeholder.
     *
     * @param string                       $name     The placeholder name (including the delimiter char like %)
     * @param callable(ProgressBar):string $callable A PHP callable
     */
    public static function setPlaceholderFormatterDefinition(string $name, callable $callable): void
    {
        self::$formatters ??= self::initPlaceholderFormatters();

        self::$formatters[$name] = $callable;
    }

    /**
     * Gets the placeholder formatter for a given name.
     *
     * @param string $name The placeholder name (including the delimiter char like %)
     */
    public static function getPlaceholderFormatterDefinition(string $name): ?callable
    {
        self::$formatters ??= self::initPlaceholderFormatters();

        return self::$formatters[$name] ?? null;
    }

    /**
     * Sets a placeholder formatter for a given name, for this instance only.
     *
     * @param callable(ProgressBar):string $callable A PHP callable
     */
    public function setPlaceholderFormatter(string $name, callable $callable): void
    {
        $this->placeholders[$name] = $callable;
    }

    /**
     * Gets the placeholder formatter for a given name.
     *
     * @param string $name The placeholder name (including the delimiter char like %)
     */
    public function getPlaceholderFormatter(string $name): ?callable
    {
        return $this->placeholders[$name] ?? $this::getPlaceholderFormatterDefinition($name);
    }

    /**
     * Sets a format for a given name.
     *
     * This method also allow you to override an existing format.
     *
     * @param string $name   The format name
     * @param string $format A format string
     */
    public static function setFormatDefinition(string $name, string $format): void
    {
        self::$formats ??= self::initFormats();

        self::$formats[$name] = $format;
    }

    /**
     * Gets the format for a given name.
     *
     * @param string $name The format name
     */
    public static function getFormatDefinition(string $name): ?string
    {
        self::$formats ??= self::initFormats();

        return self::$formats[$name] ?? null;
    }

    /**
     * Associates a text with a named placeholder.
     *
     * The text is displayed when the progress bar is rendered but only
     * when the corresponding placeholder is part of the custom format line
     * (by wrapping the name with %).
     *
     * @param string $message The text to associate with the placeholder
     * @param string $name    The name of the placeholder
     */
    public function setMessage(string $message, string $name = 'message'): void
    {
        $this->messages[$name] = $message;
    }

    public function getMessage(string $name = 'message'): ?string
    {
        return $this->messages[$name] ?? null;
    }

    public function getStartTime(): int
    {
        return $this->startTime;
    }

    public function getMaxSteps(): int
    {
        return $this->max;
    }

    public function getProgress(): int
    {
        return $this->step;
    }

    private function getStepWidth(): int
    {
        return $this->stepWidth;
    }

    public function getProgressPercent(): float
    {
        return $this->percent;
    }

    public function getBarOffset(): float
    {
        return floor($this->max ? $this->percent * $this->barWidth : (null === $this->redrawFreq ? (int) (min(5, $this->barWidth / 15) * $this->writeCount) : $this->step) % $this->barWidth);
    }

    public function getEstimated(): float
    {
        if (0 === $this->step || $this->step === $this->startingStep) {
            return 0;
        }

        return round((time() - $this->startTime) / ($this->step - $this->startingStep) * $this->max);
    }

    public function getRemaining(): float
    {
        if (0 === $this->step || $this->step === $this->startingStep) {
            return 0;
        }

        return round((time() - $this->startTime) / ($this->step - $this->startingStep) * ($this->max - $this->step));
    }

    public function setBarWidth(int $size): void
    {
        $this->barWidth = max(1, $size);
    }

    public function getBarWidth(): int
    {
        return $this->barWidth;
    }

    public function setBarCharacter(string $char): void
    {
        $this->barChar = $char;
    }

    public function getBarCharacter(): string
    {
        return $this->barChar ?? ($this->max ? '=' : $this->emptyBarChar);
    }

    public function setEmptyBarCharacter(string $char): void
    {
        $this->emptyBarChar = $char;
    }

    public function getEmptyBarCharacter(): string
    {
        return $this->emptyBarChar;
    }

    public function setProgressCharacter(string $char): void
    {
        $this->progressChar = $char;
    }

    public function getProgressCharacter(): string
    {
        return $this->progressChar;
    }

    public function setFormat(string $format): void
    {
        $this->format = null;
        $this->internalFormat = $format;
    }

    /**
     * Sets the redraw frequency.
     *
     * @param int|null $freq The frequency in steps
     */
    public function setRedrawFrequency(?int $freq): void
    {
        $this->redrawFreq = null !== $freq ? max(1, $freq) : null;
    }

    public function minSecondsBetweenRedraws(float $seconds): void
    {
        $this->minSecondsBetweenRedraws = $seconds;
    }

    public function maxSecondsBetweenRedraws(float $seconds): void
    {
        $this->maxSecondsBetweenRedraws = $seconds;
    }

    /**
     * Returns an iterator that will automatically update the progress bar when iterated.
     *
     * @template TKey
     * @template TValue
     *
     * @param iterable<TKey, TValue> $iterable
     * @param int|null               $max      Number of steps to complete the bar (0 if indeterminate), if null it will be inferred from $iterable
     *
     * @return iterable<TKey, TValue>
     */
    public function iterate(iterable $iterable, ?int $max = null): iterable
    {
        $this->start($max ?? (is_countable($iterable) ? \count($iterable) : 0));

        foreach ($iterable as $key => $value) {
            yield $key => $value;

            $this->advance();
        }

        $this->finish();
    }

    /**
     * Starts the progress output.
     *
     * @param int|null $max     Number of steps to complete the bar (0 if indeterminate), null to leave unchanged
     * @param int      $startAt The starting point of the bar (useful e.g. when resuming a previously started bar)
     */
    public function start(?int $max = null, int $startAt = 0): void
    {
        $this->startTime = time();
        $this->step = $startAt;
        $this->startingStep = $startAt;

        $startAt > 0 ? $this->setProgress($startAt) : $this->percent = 0.0;

        if (null !== $max) {
            $this->setMaxSteps($max);
        }

        $this->display();
    }

    /**
     * Advances the progress output X steps.
     *
     * @param int $step Number of steps to advance
     */
    public function advance(int $step = 1): void
    {
        $this->setProgress($this->step + $step);
    }

    /**
     * Sets whether to overwrite the progressbar, false for new line.
     */
    public function setOverwrite(bool $overwrite): void
    {
        $this->overwrite = $overwrite;
    }

    public function setProgress(int $step): void
    {
        if ($this->max && $step > $this->max) {
            $this->max = $step;
        } elseif ($step < 0) {
            $step = 0;
        }

        $redrawFreq = $this->redrawFreq ?? (($this->max ?: 10) / 10);
        $prevPeriod = (int) ($this->step / $redrawFreq);
        $currPeriod = (int) ($step / $redrawFreq);
        $this->step = $step;
        $this->percent = $this->max ? (float) $this->step / $this->max : 0;
        $timeInterval = microtime(true) - $this->lastWriteTime;

        // Draw regardless of other limits
        if ($this->max === $step) {
            $this->display();

            return;
        }

        // Throttling
        if ($timeInterval < $this->minSecondsBetweenRedraws) {
            return;
        }

        // Draw each step period, but not too late
        if ($prevPeriod !== $currPeriod || $timeInterval >= $this->maxSecondsBetweenRedraws) {
            $this->display();
        }
    }

    public function setMaxSteps(int $max): void
    {
        $this->format = null;
        $this->max = max(0, $max);
        $this->stepWidth = $this->max ? Helper::width((string) $this->max) : 4;
    }

    /**
     * Finishes the progress output.
     */
    public function finish(): void
    {
        if (!$this->max) {
            $this->max = $this->step;
        }

        if ($this->step === $this->max && !$this->overwrite) {
            // prevent double 100% output
            return;
        }

        $this->setProgress($this->max);
    }

    /**
     * Outputs the current progress string.
     */
    public function display(): void
    {
        if (OutputInterface::VERBOSITY_QUIET === $this->output->getVerbosity()) {
            return;
        }

        if (null === $this->format) {
            $this->setRealFormat($this->internalFormat ?: $this->determineBestFormat());
        }

        $this->overwrite($this->buildLine());
    }

    /**
     * Removes the progress bar from the current line.
     *
     * This is useful if you wish to write some output
     * while a progress bar is running.
     * Call display() to show the progress bar again.
     */
    public function clear(): void
    {
        if (!$this->overwrite) {
            return;
        }

        if (null === $this->format) {
            $this->setRealFormat($this->internalFormat ?: $this->determineBestFormat());
        }

        $this->overwrite('');
    }

    private function setRealFormat(string $format): void
    {
        // try to use the _nomax variant if available
        if (!$this->max && null !== self::getFormatDefinition($format.'_nomax')) {
            $this->format = self::getFormatDefinition($format.'_nomax');
        } elseif (null !== self::getFormatDefinition($format)) {
            $this->format = self::getFormatDefinition($format);
        } else {
            $this->format = $format;
        }
    }

    /**
     * Overwrites a previous message to the output.
     */
    private function overwrite(string $message): void
    {
        if ($this->previousMessage === $message) {
            return;
        }

        $originalMessage = $message;

        if ($this->overwrite) {
            if (null !== $this->previousMessage) {
                if ($this->output instanceof ConsoleSectionOutput) {
                    $messageLines = explode("\n", $this->previousMessage);
                    $lineCount = \count($messageLines);

                    $lastLineWithoutDecoration = Helper::removeDecoration($this->output->getFormatter(), end($messageLines) ?? '');

                    // When the last previous line is empty (without formatting) it is already cleared by the section output, so we don't need to clear it again
                    if ('' === $lastLineWithoutDecoration) {
                        --$lineCount;
                    }

                    foreach ($messageLines as $messageLine) {
                        $messageLineLength = Helper::width(Helper::removeDecoration($this->output->getFormatter(), $messageLine));
                        if ($messageLineLength > $this->terminal->getWidth()) {
                            $lineCount += floor($messageLineLength / $this->terminal->getWidth());
                        }
                    }

                    $this->output->clear($lineCount);
                } else {
                    $lineCount = substr_count($this->previousMessage, "\n");
                    for ($i = 0; $i < $lineCount; ++$i) {
                        $this->cursor->moveToColumn(1);
                        $this->cursor->clearLine();
                        $this->cursor->moveUp();
                    }

                    $this->cursor->moveToColumn(1);
                    $this->cursor->clearLine();
                }
            }
        } elseif ($this->step > 0) {
            $message = \PHP_EOL.$message;
        }

        $this->previousMessage = $originalMessage;
        $this->lastWriteTime = microtime(true);

        $this->output->write($message);
        ++$this->writeCount;
    }

    private function determineBestFormat(): string
    {
        return match ($this->output->getVerbosity()) {
            // OutputInterface::VERBOSITY_QUIET: display is disabled anyway
            OutputInterface::VERBOSITY_VERBOSE => $this->max ? self::FORMAT_VERBOSE : self::FORMAT_VERBOSE_NOMAX,
            OutputInterface::VERBOSITY_VERY_VERBOSE => $this->max ? self::FORMAT_VERY_VERBOSE : self::FORMAT_VERY_VERBOSE_NOMAX,
            OutputInterface::VERBOSITY_DEBUG => $this->max ? self::FORMAT_DEBUG : self::FORMAT_DEBUG_NOMAX,
            default => $this->max ? self::FORMAT_NORMAL : self::FORMAT_NORMAL_NOMAX,
        };
    }

    private static function initPlaceholderFormatters(): array
    {
        return [
            'bar' => static function (self $bar, OutputInterface $output) {
                $completeBars = $bar->getBarOffset();
                $display = str_repeat($bar->getBarCharacter(), $completeBars);
                if ($completeBars < $bar->getBarWidth()) {
                    $emptyBars = $bar->getBarWidth() - $completeBars - Helper::length(Helper::removeDecoration($output->getFormatter(), $bar->getProgressCharacter()));
                    $display .= $bar->getProgressCharacter().str_repeat($bar->getEmptyBarCharacter(), max(0, $emptyBars));
                }

                return $display;
            },
            'elapsed' => static fn (self $bar) => Helper::formatTime(time() - $bar->getStartTime(), 2),
            'remaining' => static function (self $bar) {
                if (!$bar->getMaxSteps()) {
                    throw new LogicException('Unable to display the remaining time if the maximum number of steps is not set.');
                }

                return Helper::formatTime($bar->getRemaining(), 2);
            },
            'estimated' => static function (self $bar) {
                if (!$bar->getMaxSteps()) {
                    throw new LogicException('Unable to display the estimated time if the maximum number of steps is not set.');
                }

                return Helper::formatTime($bar->getEstimated(), 2);
            },
            'memory' => static fn (self $bar) => Helper::formatMemory(memory_get_usage(true)),
            'current' => static fn (self $bar) => str_pad($bar->getProgress(), $bar->getStepWidth(), ' ', \STR_PAD_LEFT),
            'max' => static fn (self $bar) => $bar->getMaxSteps(),
            'percent' => static fn (self $bar) => floor($bar->getProgressPercent() * 100),
        ];
    }

    private static function initFormats(): array
    {
        return [
            self::FORMAT_NORMAL => ' %current%/%max% [%bar%] %percent:3s%%',
            self::FORMAT_NORMAL_NOMAX => ' %current% [%bar%]',

            self::FORMAT_VERBOSE => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%',
            self::FORMAT_VERBOSE_NOMAX => ' %current% [%bar%] %elapsed:6s%',

            self::FORMAT_VERY_VERBOSE => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s%',
            self::FORMAT_VERY_VERBOSE_NOMAX => ' %current% [%bar%] %elapsed:6s%',

            self::FORMAT_DEBUG => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%',
            self::FORMAT_DEBUG_NOMAX => ' %current% [%bar%] %elapsed:6s% %memory:6s%',
        ];
    }

    private function buildLine(): string
    {
        \assert(null !== $this->format);

        $regex = "{%([a-z\-_]+)(?:\:([^%]+))?%}i";
        $callback = function ($matches) {
            if ($formatter = $this->getPlaceholderFormatter($matches[1])) {
                $text = $formatter($this, $this->output);
            } elseif (isset($this->messages[$matches[1]])) {
                $text = $this->messages[$matches[1]];
            } else {
                return $matches[0];
            }

            if (isset($matches[2])) {
                $text = \sprintf('%'.$matches[2], $text);
            }

            return $text;
        };
        $line = preg_replace_callback($regex, $callback, $this->format);

        // gets string length for each sub line with multiline format
        $linesLength = array_map(fn ($subLine) => Helper::width(Helper::removeDecoration($this->output->getFormatter(), rtrim($subLine, "\r"))), explode("\n", $line));

        $linesWidth = max($linesLength);

        $terminalWidth = $this->terminal->getWidth();
        if ($linesWidth <= $terminalWidth) {
            return $line;
        }

        $this->setBarWidth($this->barWidth - $linesWidth + $terminalWidth);

        return preg_replace_callback($regex, $callback, $this->format);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Helper;

use Symfony\Component\Console\Formatter\OutputFormatterInterface;
use Symfony\Component\String\UnicodeString;

/**
 * Helper is the base class for all helper classes.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
abstract class Helper implements HelperInterface
{
    protected $helperSet;

    /**
     * @return void
     */
    public function setHelperSet(?HelperSet $helperSet = null)
    {
        if (1 > \func_num_args()) {
            trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__);
        }
        $this->helperSet = $helperSet;
    }

    public function getHelperSet(): ?HelperSet
    {
        return $this->helperSet;
    }

    /**
     * Returns the width of a string, using mb_strwidth if it is available.
     * The width is how many characters positions the string will use.
     */
    public static function width(?string $string): int
    {
        $string ??= '';

        if (preg_match('//u', $string)) {
            $string = preg_replace('/[\p{Cc}\x7F]++/u', '', $string, -1, $count);

            return (new UnicodeString($string))->width(false) + $count;
        }

        if (false === $encoding = mb_detect_encoding($string, null, true)) {
            return \strlen($string);
        }

        return mb_strwidth($string, $encoding);
    }

    /**
     * Returns the length of a string, using mb_strlen if it is available.
     * The length is related to how many bytes the string will use.
     */
    public static function length(?string $string): int
    {
        $string ??= '';

        if (preg_match('//u', $string)) {
            return (new UnicodeString($string))->length();
        }

        if (false === $encoding = mb_detect_encoding($string, null, true)) {
            return \strlen($string);
        }

        return mb_strlen($string, $encoding);
    }

    /**
     * Returns the subset of a string, using mb_substr if it is available.
     */
    public static function substr(?string $string, int $from, ?int $length = null): string
    {
        $string ??= '';

        if (preg_match('//u', $string)) {
            return (new UnicodeString($string))->slice($from, $length);
        }

        if (false === $encoding = mb_detect_encoding($string, null, true)) {
            return substr($string, $from, $length);
        }

        return mb_substr($string, $from, $length, $encoding);
    }

    /**
     * @return string
     */
    public static function formatTime(int|float $secs, int $precision = 1)
    {
        $secs = (int) floor($secs);

        if (0 === $secs) {
            return '< 1 sec';
        }

        static $timeFormats = [
            [1, '1 sec', 'secs'],
            [60, '1 min', 'mins'],
            [3600, '1 hr', 'hrs'],
            [86400, '1 day', 'days'],
        ];

        $times = [];
        foreach ($timeFormats as $index => $format) {
            $seconds = isset($timeFormats[$index + 1]) ? $secs % $timeFormats[$index + 1][0] : $secs;

            if (isset($times[$index - $precision])) {
                unset($times[$index - $precision]);
            }

            if (0 === $seconds) {
                continue;
            }

            $unitCount = ($seconds / $format[0]);
            $times[$index] = 1 === $unitCount ? $format[1] : $unitCount.' '.$format[2];

            if ($secs === $seconds) {
                break;
            }

            $secs -= $seconds;
        }

        return implode(', ', array_reverse($times));
    }

    /**
     * @return string
     */
    public static function formatMemory(int $memory)
    {
        if ($memory >= 1024 * 1024 * 1024) {
            return \sprintf('%.1f GiB', $memory / 1024 / 1024 / 1024);
        }

        if ($memory >= 1024 * 1024) {
            return \sprintf('%.1f MiB', $memory / 1024 / 1024);
        }

        if ($memory >= 1024) {
            return \sprintf('%d KiB', $memory / 1024);
        }

        return \sprintf('%d B', $memory);
    }

    /**
     * @return string
     */
    public static function removeDecoration(OutputFormatterInterface $formatter, ?string $string)
    {
        $isDecorated = $formatter->isDecorated();
        $formatter->setDecorated(false);
        // remove <...> formatting
        $string = $formatter->format($string ?? '');
        // remove already formatted characters
        $string = preg_replace("/\033\[[^m]*m/", '', $string ?? '');
        // remove terminal hyperlinks
        $string = preg_replace('/\\033]8;[^;]*;[^\\033]*\\033\\\\/', '', $string ?? '');
        $formatter->setDecorated($isDecorated);

        return $string;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Helper;

/**
 * Simple output wrapper for "tagged outputs" instead of wordwrap(). This solution is based on a StackOverflow
 * answer: https://stackoverflow.com/a/20434776/1476819 from user557597 (alias SLN).
 *
 *  (?:
 *       # -- Words/Characters
 *       (                       # (1 start)
 *            (?>                     # Atomic Group - Match words with valid breaks
 *                 .{1,16}                 #  1-N characters
 *                                         #  Followed by one of 4 prioritized, non-linebreak whitespace
 *                 (?:                     #  break types:
 *                      (?<= [^\S\r\n] )        # 1. - Behind a non-linebreak whitespace
 *                      [^\S\r\n]?              #      ( optionally accept an extra non-linebreak whitespace )
 *                   |  (?= \r? \n )            # 2. - Ahead a linebreak
 *                   |  $                       # 3. - EOS
 *                   |  [^\S\r\n]               # 4. - Accept an extra non-linebreak whitespace
 *                 )
 *            )                       # End atomic group
 *         |
 *            .{1,16}                 # No valid word breaks, just break on the N'th character
 *       )                       # (1 end)
 *       (?: \r? \n )?           # Optional linebreak after Words/Characters
 *    |
 *       # -- Or, Linebreak
 *       (?: \r? \n | $ )        # Stand alone linebreak or at EOS
 *  )
 *
 * @author Krisztián Ferenczi <ferenczi.krisztian@gmail.com>
 *
 * @see https://stackoverflow.com/a/20434776/1476819
 */
final class OutputWrapper
{
    private const TAG_OPEN_REGEX_SEGMENT = '[a-z](?:[^\\\\<>]*+ | \\\\.)*';
    private const TAG_CLOSE_REGEX_SEGMENT = '[a-z][^<>]*+';
    private const URL_PATTERN = 'https?://\S+';

    public function __construct(
        private bool $allowCutUrls = false,
    ) {
    }

    public function wrap(string $text, int $width, string $break = "\n"): string
    {
        if (!$width) {
            return $text;
        }

        $tagPattern = \sprintf('<(?:(?:%s)|/(?:%s)?)>', self::TAG_OPEN_REGEX_SEGMENT, self::TAG_CLOSE_REGEX_SEGMENT);
        $limitPattern = "{1,$width}";
        $patternBlocks = [$tagPattern];
        if (!$this->allowCutUrls) {
            $patternBlocks[] = self::URL_PATTERN;
        }
        $patternBlocks[] = '.';
        $blocks = implode('|', $patternBlocks);
        $rowPattern = "(?:$blocks)$limitPattern";
        $pattern = \sprintf('#(?:((?>(%1$s)((?<=[^\S\r\n])[^\S\r\n]?|(?=\r?\n)|$|[^\S\r\n]))|(%1$s))(?:\r?\n)?|(?:\r?\n|$))#imux', $rowPattern);
        $output = rtrim(preg_replace($pattern, '\\1'.$break, $text), $break);

        return str_replace(' '.$break, $break, $output);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Helper;

use Symfony\Component\Console\Exception\InvalidArgumentException;

/**
 * @author Yewhen Khoptynskyi <khoptynskyi@gmail.com>
 */
class TableCellStyle
{
    public const DEFAULT_ALIGN = 'left';

    private const TAG_OPTIONS = [
        'fg',
        'bg',
        'options',
    ];

    private const ALIGN_MAP = [
        'left' => \STR_PAD_RIGHT,
        'center' => \STR_PAD_BOTH,
        'right' => \STR_PAD_LEFT,
    ];

    private array $options = [
        'fg' => 'default',
        'bg' => 'default',
        'options' => null,
        'align' => self::DEFAULT_ALIGN,
        'cellFormat' => null,
    ];

    public function __construct(array $options = [])
    {
        if ($diff = array_diff(array_keys($options), array_keys($this->options))) {
            throw new InvalidArgumentException(\sprintf('The TableCellStyle does not support the following options: \'%s\'.', implode('\', \'', $diff)));
        }

        if (isset($options['align']) && !\array_key_exists($options['align'], self::ALIGN_MAP)) {
            throw new InvalidArgumentException(\sprintf('Wrong align value. Value must be following: \'%s\'.', implode('\', \'', array_keys(self::ALIGN_MAP))));
        }

        $this->options = array_merge($this->options, $options);
    }

    public function getOptions(): array
    {
        return $this->options;
    }

    /**
     * Gets options we need for tag for example fg, bg.
     *
     * @return string[]
     */
    public function getTagOptions(): array
    {
        return array_filter(
            $this->getOptions(),
            fn ($key) => \in_array($key, self::TAG_OPTIONS) && isset($this->options[$key]),
            \ARRAY_FILTER_USE_KEY
        );
    }

    public function getPadByAlign(): int
    {
        return self::ALIGN_MAP[$this->getOptions()['align']];
    }

    public function getCellFormat(): ?string
    {
        return $this->getOptions()['cellFormat'];
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Helper;

use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Formatter\WrappableOutputFormatterInterface;
use Symfony\Component\Console\Output\ConsoleSectionOutput;
use Symfony\Component\Console\Output\OutputInterface;

/**
 * Provides helpers to display a table.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 * @author Саша Стаменковић <umpirsky@gmail.com>
 * @author Abdellatif Ait boudad <a.aitboudad@gmail.com>
 * @author Max Grigorian <maxakawizard@gmail.com>
 * @author Dany Maillard <danymaillard93b@gmail.com>
 */
class Table
{
    private const SEPARATOR_TOP = 0;
    private const SEPARATOR_TOP_BOTTOM = 1;
    private const SEPARATOR_MID = 2;
    private const SEPARATOR_BOTTOM = 3;
    private const BORDER_OUTSIDE = 0;
    private const BORDER_INSIDE = 1;
    private const DISPLAY_ORIENTATION_DEFAULT = 'default';
    private const DISPLAY_ORIENTATION_HORIZONTAL = 'horizontal';
    private const DISPLAY_ORIENTATION_VERTICAL = 'vertical';

    private ?string $headerTitle = null;
    private ?string $footerTitle = null;
    private array $headers = [];
    private array $rows = [];
    private array $effectiveColumnWidths = [];
    private int $numberOfColumns;
    private OutputInterface $output;
    private TableStyle $style;
    private array $columnStyles = [];
    private array $columnWidths = [];
    private array $columnMaxWidths = [];
    private bool $rendered = false;
    private string $displayOrientation = self::DISPLAY_ORIENTATION_DEFAULT;

    private static array $styles;

    public function __construct(OutputInterface $output)
    {
        $this->output = $output;

        self::$styles ??= self::initStyles();

        $this->setStyle('default');
    }

    /**
     * Sets a style definition.
     *
     * @return void
     */
    public static function setStyleDefinition(string $name, TableStyle $style)
    {
        self::$styles ??= self::initStyles();

        self::$styles[$name] = $style;
    }

    /**
     * Gets a style definition by name.
     */
    public static function getStyleDefinition(string $name): TableStyle
    {
        self::$styles ??= self::initStyles();

        return self::$styles[$name] ?? throw new InvalidArgumentException(\sprintf('Style "%s" is not defined.', $name));
    }

    /**
     * Sets table style.
     *
     * @return $this
     */
    public function setStyle(TableStyle|string $name): static
    {
        $this->style = $this->resolveStyle($name);

        return $this;
    }

    /**
     * Gets the current table style.
     */
    public function getStyle(): TableStyle
    {
        return $this->style;
    }

    /**
     * Sets table column style.
     *
     * @param TableStyle|string $name The style name or a TableStyle instance
     *
     * @return $this
     */
    public function setColumnStyle(int $columnIndex, TableStyle|string $name): static
    {
        $this->columnStyles[$columnIndex] = $this->resolveStyle($name);

        return $this;
    }

    /**
     * Gets the current style for a column.
     *
     * If style was not set, it returns the global table style.
     */
    public function getColumnStyle(int $columnIndex): TableStyle
    {
        return $this->columnStyles[$columnIndex] ?? $this->getStyle();
    }

    /**
     * Sets the minimum width of a column.
     *
     * @return $this
     */
    public function setColumnWidth(int $columnIndex, int $width): static
    {
        $this->columnWidths[$columnIndex] = $width;

        return $this;
    }

    /**
     * Sets the minimum width of all columns.
     *
     * @return $this
     */
    public function setColumnWidths(array $widths): static
    {
        $this->columnWidths = [];
        foreach ($widths as $index => $width) {
            $this->setColumnWidth($index, $width);
        }

        return $this;
    }

    /**
     * Sets the maximum width of a column.
     *
     * Any cell within this column which contents exceeds the specified width will be wrapped into multiple lines, while
     * formatted strings are preserved.
     *
     * @return $this
     */
    public function setColumnMaxWidth(int $columnIndex, int $width): static
    {
        if (!$this->output->getFormatter() instanceof WrappableOutputFormatterInterface) {
            throw new \LogicException(\sprintf('Setting a maximum column width is only supported when using a "%s" formatter, got "%s".', WrappableOutputFormatterInterface::class, get_debug_type($this->output->getFormatter())));
        }

        $this->columnMaxWidths[$columnIndex] = $width;

        return $this;
    }

    /**
     * @return $this
     */
    public function setHeaders(array $headers): static
    {
        $headers = array_values($headers);
        if ($headers && !\is_array($headers[0])) {
            $headers = [$headers];
        }

        $this->headers = $headers;

        return $this;
    }

    /**
     * @return $this
     */
    public function setRows(array $rows)
    {
        $this->rows = [];

        return $this->addRows($rows);
    }

    /**
     * @return $this
     */
    public function addRows(array $rows): static
    {
        foreach ($rows as $row) {
            $this->addRow($row);
        }

        return $this;
    }

    /**
     * @return $this
     */
    public function addRow(TableSeparator|array $row): static
    {
        if ($row instanceof TableSeparator) {
            $this->rows[] = $row;

            return $this;
        }

        $this->rows[] = array_values($row);

        return $this;
    }

    /**
     * Adds a row to the table, and re-renders the table.
     *
     * @return $this
     */
    public function appendRow(TableSeparator|array $row): static
    {
        if (!$this->output instanceof ConsoleSectionOutput) {
            throw new RuntimeException(\sprintf('Output should be an instance of "%s" when calling "%s".', ConsoleSectionOutput::class, __METHOD__));
        }

        if ($this->rendered) {
            $this->output->clear($this->calculateRowCount());
        }

        $this->addRow($row);
        $this->render();

        return $this;
    }

    /**
     * @return $this
     */
    public function setRow(int|string $column, array $row): static
    {
        $this->rows[$column] = $row;

        return $this;
    }

    /**
     * @return $this
     */
    public function setHeaderTitle(?string $title): static
    {
        $this->headerTitle = $title;

        return $this;
    }

    /**
     * @return $this
     */
    public function setFooterTitle(?string $title): static
    {
        $this->footerTitle = $title;

        return $this;
    }

    /**
     * @return $this
     */
    public function setHorizontal(bool $horizontal = true): static
    {
        $this->displayOrientation = $horizontal ? self::DISPLAY_ORIENTATION_HORIZONTAL : self::DISPLAY_ORIENTATION_DEFAULT;

        return $this;
    }

    /**
     * @return $this
     */
    public function setVertical(bool $vertical = true): static
    {
        $this->displayOrientation = $vertical ? self::DISPLAY_ORIENTATION_VERTICAL : self::DISPLAY_ORIENTATION_DEFAULT;

        return $this;
    }

    /**
     * Renders table to output.
     *
     * Example:
     *
     *     +---------------+-----------------------+------------------+
     *     | ISBN          | Title                 | Author           |
     *     +---------------+-----------------------+------------------+
     *     | 99921-58-10-7 | Divine Comedy         | Dante Alighieri  |
     *     | 9971-5-0210-0 | A Tale of Two Cities  | Charles Dickens  |
     *     | 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien |
     *     +---------------+-----------------------+------------------+
     *
     * @return void
     */
    public function render()
    {
        $divider = new TableSeparator();
        $isCellWithColspan = static fn ($cell) => $cell instanceof TableCell && $cell->getColspan() >= 2;

        $horizontal = self::DISPLAY_ORIENTATION_HORIZONTAL === $this->displayOrientation;
        $vertical = self::DISPLAY_ORIENTATION_VERTICAL === $this->displayOrientation;

        $rows = [];
        if ($horizontal) {
            foreach ($this->headers[0] ?? [] as $i => $header) {
                $rows[$i] = [$header];
                foreach ($this->rows as $row) {
                    if ($row instanceof TableSeparator) {
                        continue;
                    }
                    if (isset($row[$i])) {
                        $rows[$i][] = $row[$i];
                    } elseif ($isCellWithColspan($rows[$i][0])) {
                        // Noop, there is a "title"
                    } else {
                        $rows[$i][] = null;
                    }
                }
            }
        } elseif ($vertical) {
            $formatter = $this->output->getFormatter();
            $maxHeaderLength = array_reduce($this->headers[0] ?? [], static fn ($max, $header) => max($max, Helper::width(Helper::removeDecoration($formatter, $header))), 0);

            foreach ($this->rows as $row) {
                if ($row instanceof TableSeparator) {
                    continue;
                }

                if ($rows) {
                    $rows[] = [$divider];
                }

                $containsColspan = false;
                foreach ($row as $cell) {
                    if ($containsColspan = $isCellWithColspan($cell)) {
                        break;
                    }
                }

                $headers = $this->headers[0] ?? [];
                $maxRows = max(\count($headers), \count($row));
                for ($i = 0; $i < $maxRows; ++$i) {
                    $cell = (string) ($row[$i] ?? '');

                    $eol = str_contains($cell, "\r\n") ? "\r\n" : "\n";
                    $parts = explode($eol, $cell);
                    foreach ($parts as $idx => $part) {
                        if ($headers && !$containsColspan) {
                            if (0 === $idx) {
                                $rows[] = [\sprintf(
                                    '<comment>%s%s</>: %s',
                                    str_repeat(' ', $maxHeaderLength - Helper::width(Helper::removeDecoration($formatter, $headers[$i] ?? ''))),
                                    $headers[$i] ?? '',
                                    $part
                                )];
                            } else {
                                $rows[] = [\sprintf(
                                    '%s  %s',
                                    str_pad('', $maxHeaderLength, ' ', \STR_PAD_LEFT),
                                    $part
                                )];
                            }
                        } elseif ('' !== $cell) {
                            $rows[] = [$part];
                        }
                    }
                }
            }
        } else {
            $rows = array_merge($this->headers, [$divider], $this->rows);
        }

        $this->calculateNumberOfColumns($rows);

        $rowGroups = $this->buildTableRows($rows);
        $this->calculateColumnsWidth($rowGroups);

        $isHeader = !$horizontal;
        $isFirstRow = $horizontal;
        $hasTitle = (bool) $this->headerTitle;
        $previousRow = null;
        $pendingSeparators = 0;

        foreach ($rowGroups as $rowGroup) {
            $isHeaderSeparatorRendered = false;

            foreach ($rowGroup as $row) {
                if ($divider === $row) {
                    $isHeader = false;
                    $isFirstRow = true;

                    continue;
                }

                if ($row instanceof TableSeparator) {
                    // defer rendering until the row below is known, to align crossings with it
                    ++$pendingSeparators;

                    continue;
                }

                if (!$row) {
                    continue;
                }

                for (; $pendingSeparators > 0; --$pendingSeparators) {
                    $this->renderRowSeparator(self::SEPARATOR_MID, null, null, $previousRow, $row);
                }

                if ($isHeader && !$isHeaderSeparatorRendered) {
                    $this->renderRowSeparator(
                        self::SEPARATOR_TOP,
                        $hasTitle ? $this->headerTitle : null,
                        $hasTitle ? $this->style->getHeaderTitleFormat() : null,
                        null,
                        $row
                    );
                    $hasTitle = false;
                    $isHeaderSeparatorRendered = true;
                }

                if ($isFirstRow) {
                    $this->renderRowSeparator(
                        $horizontal || !array_filter($this->headers) ? self::SEPARATOR_TOP : self::SEPARATOR_TOP_BOTTOM,
                        $hasTitle ? $this->headerTitle : null,
                        $hasTitle ? $this->style->getHeaderTitleFormat() : null,
                        $previousRow,
                        $row
                    );
                    $isFirstRow = false;
                    $hasTitle = false;
                }

                if ($vertical) {
                    $isHeader = false;
                    $isFirstRow = false;
                }

                if ($horizontal) {
                    $this->renderRow($row, $this->style->getCellRowFormat(), $this->style->getCellHeaderFormat());
                } else {
                    $this->renderRow($row, $isHeader ? $this->style->getCellHeaderFormat() : $this->style->getCellRowFormat());
                }

                $previousRow = $row;
            }
        }

        for (; $pendingSeparators > 0; --$pendingSeparators) {
            $this->renderRowSeparator(self::SEPARATOR_MID, null, null, $previousRow);
        }

        $this->renderRowSeparator(self::SEPARATOR_BOTTOM, $this->footerTitle, $this->style->getFooterTitleFormat(), $previousRow);

        $this->cleanup();
        $this->rendered = true;
    }

    /**
     * Renders horizontal header separator.
     *
     * Example:
     *
     *     +-----+-----------+-------+
     */
    private function renderRowSeparator(int $type = self::SEPARATOR_MID, ?string $title = null, ?string $titleFormat = null, ?array $rowAbove = null, ?array $rowBelow = null): void
    {
        if (!$count = $this->numberOfColumns) {
            return;
        }

        $borders = $this->style->getBorderChars();
        if (!$borders[0] && !$borders[2] && !$this->style->getCrossingChar()) {
            return;
        }

        $crossings = $this->style->getCrossingChars();
        if (self::SEPARATOR_MID === $type) {
            [$horizontal, $leftChar, $midChar, $rightChar] = [$borders[2], $crossings[8], $crossings[0], $crossings[4]];
        } elseif (self::SEPARATOR_TOP === $type) {
            [$horizontal, $leftChar, $midChar, $rightChar] = [$borders[0], $crossings[1], $crossings[2], $crossings[3]];
        } elseif (self::SEPARATOR_TOP_BOTTOM === $type) {
            [$horizontal, $leftChar, $midChar, $rightChar] = [$borders[0], $crossings[9], $crossings[10], $crossings[11]];
        } else {
            [$horizontal, $leftChar, $midChar, $rightChar] = [$borders[0], $crossings[7], $crossings[6], $crossings[5]];
        }

        // only draw a crossing where a vertical border meets the separator; a cell
        // spanning several columns has no border on the columns it covers
        $hasLineAbove = self::SEPARATOR_TOP !== $type;
        $hasLineBelow = self::SEPARATOR_BOTTOM !== $type;
        $columnsAbove = null !== $rowAbove ? $this->getRowColumns($rowAbove) : null;
        $columnsBelow = null !== $rowBelow ? $this->getRowColumns($rowBelow) : null;

        $markup = $leftChar;
        for ($column = 0; $column < $count; ++$column) {
            $markup .= str_repeat($horizontal, $this->effectiveColumnWidths[$column]);
            if ($column === $count - 1) {
                $markup .= $rightChar;

                continue;
            }

            $borderAbove = $hasLineAbove && (null === $columnsAbove || \in_array($column + 1, $columnsAbove, true));
            $borderBelow = $hasLineBelow && (null === $columnsBelow || \in_array($column + 1, $columnsBelow, true));
            // the one-sided junctions reuse the top/bottom crossing chars; for the "box-double"
            // style these are double-horizontal (╤/╧) while an inner separator is single (─), as
            // that style has no single-line ┬/┴ to fall back to
            $markup .= match (true) {
                $borderAbove && $borderBelow => $midChar,
                $borderBelow => $crossings[2],
                $borderAbove => $crossings[6],
                default => $horizontal,
            };
        }

        if (null !== $title) {
            $titleLength = Helper::width(Helper::removeDecoration($formatter = $this->output->getFormatter(), $formattedTitle = \sprintf($titleFormat, $title)));
            $markupLength = Helper::width($markup);
            if ($titleLength > $limit = $markupLength - 4) {
                $titleLength = $limit;
                $formatLength = Helper::width(Helper::removeDecoration($formatter, \sprintf($titleFormat, '')));
                $formattedTitle = \sprintf($titleFormat, Helper::substr($title, 0, $limit - $formatLength - 3).'...');
            }

            $titleStart = intdiv($markupLength - $titleLength, 2);
            if (false === mb_detect_encoding($markup, null, true)) {
                $markup = substr_replace($markup, $formattedTitle, $titleStart, $titleLength);
            } else {
                $markup = mb_substr($markup, 0, $titleStart).$formattedTitle.mb_substr($markup, $titleStart + $titleLength);
            }
        }

        $this->output->writeln(\sprintf($this->style->getBorderFormat(), $markup));
    }

    /**
     * Renders vertical column separator.
     */
    private function renderColumnSeparator(int $type = self::BORDER_OUTSIDE): string
    {
        $borders = $this->style->getBorderChars();

        return \sprintf($this->style->getBorderFormat(), self::BORDER_OUTSIDE === $type ? $borders[1] : $borders[3]);
    }

    /**
     * Renders table row.
     *
     * Example:
     *
     *     | 9971-5-0210-0 | A Tale of Two Cities  | Charles Dickens  |
     */
    private function renderRow(array $row, string $cellFormat, ?string $firstCellFormat = null): void
    {
        $rowContent = $this->renderColumnSeparator(self::BORDER_OUTSIDE);
        $columns = $this->getRowColumns($row);
        $last = array_key_last($columns);
        foreach ($columns as $i => $column) {
            if ($firstCellFormat && 0 === $i) {
                $rowContent .= $this->renderCell($row, $column, $firstCellFormat);
            } else {
                $rowContent .= $this->renderCell($row, $column, $cellFormat);
            }
            $rowContent .= $this->renderColumnSeparator($last === $i ? self::BORDER_OUTSIDE : self::BORDER_INSIDE);
        }
        $this->output->writeln($rowContent);
    }

    /**
     * Renders table cell with padding.
     */
    private function renderCell(array $row, int $column, string $cellFormat): string
    {
        $cell = $row[$column] ?? '';
        $width = $this->effectiveColumnWidths[$column];
        if ($cell instanceof TableCell && $cell->getColspan() > 1) {
            // add the width of the following columns(numbers of colspan).
            foreach (range($column + 1, $column + $cell->getColspan() - 1) as $nextColumn) {
                $width += $this->getColumnSeparatorWidth() + $this->effectiveColumnWidths[$nextColumn];
            }
        }

        // str_pad won't work properly with multi-byte strings, we need to fix the padding
        $width += \strlen($cell) - Helper::width($cell) - substr_count($cell, "\0");
        $style = $this->getColumnStyle($column);

        if ($cell instanceof TableSeparator) {
            return \sprintf($style->getBorderFormat(), str_repeat($style->getBorderChars()[2], $width));
        }

        $width += Helper::length($cell) - Helper::length(Helper::removeDecoration($this->output->getFormatter(), $cell));
        $content = \sprintf($style->getCellRowContentFormat(), $cell);

        $padType = $style->getPadType();
        if ($cell instanceof TableCell && $cell->getStyle() instanceof TableCellStyle) {
            $isNotStyledByTag = !preg_match('/^<(\w+|(\w+=[\w,]+;?)*)>.+<\/(\w+|(\w+=\w+;?)*)?>$/', $cell);
            if ($isNotStyledByTag) {
                $cellFormat = $cell->getStyle()->getCellFormat();
                if (!\is_string($cellFormat)) {
                    $tag = http_build_query($cell->getStyle()->getTagOptions(), '', ';');
                    $cellFormat = '<'.$tag.'>%s</>';
                }

                if (str_contains($content, '</>')) {
                    $content = str_replace('</>', '', $content);
                    $width -= 3;
                }
                if (str_contains($content, '<fg=default;bg=default>')) {
                    $content = str_replace('<fg=default;bg=default>', '', $content);
                    $width -= \strlen('<fg=default;bg=default>');
                }
            }

            $padType = $cell->getStyle()->getPadByAlign();
        }

        return \sprintf($cellFormat, str_pad($content, $width, $style->getPaddingChar(), $padType));
    }

    /**
     * Calculate number of columns for this table.
     */
    private function calculateNumberOfColumns(array $rows): void
    {
        $columns = [0];
        foreach ($rows as $row) {
            if ($row instanceof TableSeparator) {
                continue;
            }

            $columns[] = $this->getNumberOfColumns($row);
        }

        $this->numberOfColumns = max($columns);
    }

    private function buildTableRows(array $rows): TableRows
    {
        /** @var WrappableOutputFormatterInterface $formatter */
        $formatter = $this->output->getFormatter();
        $unmergedRows = [];
        for ($rowKey = 0; $rowKey < \count($rows); ++$rowKey) {
            $rows = $this->fillNextRows($rows, $rowKey);

            // Remove any new line breaks and replace it with a new line
            foreach ($rows[$rowKey] as $column => $cell) {
                $colspan = $cell instanceof TableCell ? $cell->getColspan() : 1;

                $minWrappedWidth = 0;
                $widthApplied = [];
                $lengthColumnBorder = $this->getColumnSeparatorWidth() + Helper::width($this->style->getCellRowContentFormat()) - 2;
                for ($i = $column; $i < ($column + $colspan); ++$i) {
                    if (isset($this->columnMaxWidths[$i])) {
                        $minWrappedWidth += $this->columnMaxWidths[$i];
                        $widthApplied[] = ['type' => 'max', 'column' => $i];
                    } elseif (($this->columnWidths[$i] ?? 0) > 0 && $colspan > 1) {
                        $minWrappedWidth += $this->columnWidths[$i];
                        $widthApplied[] = ['type' => 'min', 'column' => $i];
                    }
                }
                if (1 === \count($widthApplied)) {
                    if ($colspan > 1) {
                        $minWrappedWidth *= $colspan;  // previous logic
                    }
                } elseif (\count($widthApplied) > 1) {
                    $minWrappedWidth += (\count($widthApplied) - 1) * $lengthColumnBorder;
                }

                $cellWidth = Helper::width(Helper::removeDecoration($formatter, $cell));
                if ($minWrappedWidth && $cellWidth > $minWrappedWidth) {
                    $cell = $formatter->formatAndWrap($cell, $minWrappedWidth);
                }
                // update minimal columnWidths for spanned columns
                if ($colspan > 1 && $minWrappedWidth > 0) {
                    $columnsMinWidthProcessed = [];
                    $cellWidth = min($cellWidth, $minWrappedWidth);
                    foreach ($widthApplied as $item) {
                        if ('max' === $item['type'] && $cellWidth >= $this->columnMaxWidths[$item['column']]) {
                            $minWidthColumn = $this->columnMaxWidths[$item['column']];
                            $this->columnWidths[$item['column']] = $minWidthColumn;
                            $columnsMinWidthProcessed[$item['column']] = true;
                            $cellWidth -= $minWidthColumn + $lengthColumnBorder;
                        }
                    }
                    for ($i = $column; $i < ($column + $colspan); ++$i) {
                        if (isset($columnsMinWidthProcessed[$i])) {
                            continue;
                        }
                        $this->columnWidths[$i] = $cellWidth + $lengthColumnBorder;
                    }
                }
                if (!str_contains($cell ?? '', "\n")) {
                    continue;
                }
                $eol = str_contains($cell ?? '', "\r\n") ? "\r\n" : "\n";
                $escaped = implode($eol, array_map(OutputFormatter::escapeTrailingBackslash(...), explode($eol, $cell)));
                $cell = $cell instanceof TableCell ? new TableCell($escaped, ['colspan' => $cell->getColspan()]) : $escaped;
                $lines = explode($eol, str_replace($eol, '<fg=default;bg=default></>'.$eol, $cell));
                foreach ($lines as $lineKey => $line) {
                    if ($colspan > 1) {
                        $line = new TableCell($line, ['colspan' => $colspan]);
                    }
                    if (0 === $lineKey) {
                        $rows[$rowKey][$column] = $line;
                    } else {
                        if (!\array_key_exists($rowKey, $unmergedRows) || !\array_key_exists($lineKey, $unmergedRows[$rowKey])) {
                            $unmergedRows[$rowKey][$lineKey] = $this->copyRow($rows, $rowKey);
                        }
                        $unmergedRows[$rowKey][$lineKey][$column] = $line;
                    }
                }
            }
        }

        return new TableRows(function () use ($rows, $unmergedRows): \Traversable {
            foreach ($rows as $rowKey => $row) {
                $rowGroup = [$row instanceof TableSeparator ? $row : $this->fillCells($row)];

                if (isset($unmergedRows[$rowKey])) {
                    foreach ($unmergedRows[$rowKey] as $row) {
                        $rowGroup[] = $row instanceof TableSeparator ? $row : $this->fillCells($row);
                    }
                }
                yield $rowGroup;
            }
        });
    }

    private function calculateRowCount(): int
    {
        $numberOfRows = \count(iterator_to_array($this->buildTableRows(array_merge($this->headers, [new TableSeparator()], $this->rows))));

        if ($this->headers) {
            ++$numberOfRows; // Add row for header separator
        }

        if ($this->rows) {
            ++$numberOfRows; // Add row for footer separator
        }

        return $numberOfRows;
    }

    /**
     * fill rows that contains rowspan > 1.
     *
     * @throws InvalidArgumentException
     */
    private function fillNextRows(array $rows, int $line): array
    {
        $unmergedRows = [];
        foreach ($rows[$line] as $column => $cell) {
            if (null !== $cell && !$cell instanceof TableCell && !\is_scalar($cell) && !$cell instanceof \Stringable) {
                throw new InvalidArgumentException(\sprintf('A cell must be a TableCell, a scalar or an object implementing "__toString()", "%s" given.', get_debug_type($cell)));
            }
            if ($cell instanceof TableCell && $cell->getRowspan() > 1) {
                $nbLines = $cell->getRowspan() - 1;
                $lines = [$cell];
                if (str_contains($cell, "\n")) {
                    $eol = str_contains($cell, "\r\n") ? "\r\n" : "\n";
                    $lines = explode($eol, str_replace($eol, '<fg=default;bg=default>'.$eol.'</>', $cell));
                    $nbLines = \count($lines) > $nbLines ? substr_count($cell, $eol) : $nbLines;

                    $rows[$line][$column] = new TableCell($lines[0], ['colspan' => $cell->getColspan(), 'style' => $cell->getStyle()]);
                    unset($lines[0]);
                }

                // create a two dimensional array (rowspan x colspan)
                $unmergedRows = array_replace_recursive(array_fill($line + 1, $nbLines, []), $unmergedRows);
                foreach ($unmergedRows as $unmergedRowKey => $unmergedRow) {
                    $value = $lines[$unmergedRowKey - $line] ?? '';
                    $unmergedRows[$unmergedRowKey][$column] = new TableCell($value, ['colspan' => $cell->getColspan(), 'style' => $cell->getStyle()]);
                    if ($nbLines === $unmergedRowKey - $line) {
                        break;
                    }
                }
            }
        }

        foreach ($unmergedRows as $unmergedRowKey => $unmergedRow) {
            // we need to know if $unmergedRow will be merged or inserted into $rows
            if (isset($rows[$unmergedRowKey]) && \is_array($rows[$unmergedRowKey]) && ($this->getNumberOfColumns($rows[$unmergedRowKey]) + $this->getNumberOfColumns($unmergedRows[$unmergedRowKey]) <= $this->numberOfColumns)) {
                foreach ($unmergedRow as $cellKey => $cell) {
                    // insert cell into row at cellKey position
                    array_splice($rows[$unmergedRowKey], $cellKey, 0, [$cell]);
                }
            } else {
                $row = $this->copyRow($rows, $unmergedRowKey - 1);
                foreach ($unmergedRow as $column => $cell) {
                    if (!empty($cell)) {
                        $row[$column] = $unmergedRow[$column];
                    }
                }
                array_splice($rows, $unmergedRowKey, 0, [$row]);
            }
        }

        return $rows;
    }

    /**
     * fill cells for a row that contains colspan > 1.
     */
    private function fillCells(iterable $row): iterable
    {
        $newRow = [];

        foreach ($row as $column => $cell) {
            $newRow[] = $cell;
            if ($cell instanceof TableCell && $cell->getColspan() > 1) {
                foreach (range($column + 1, $column + $cell->getColspan() - 1) as $position) {
                    // insert empty value at column position
                    $newRow[] = '';
                }
            }
        }

        return $newRow ?: $row;
    }

    private function copyRow(array $rows, int $line): array
    {
        $row = $rows[$line];
        foreach ($row as $cellKey => $cellValue) {
            $row[$cellKey] = '';
            if ($cellValue instanceof TableCell) {
                $row[$cellKey] = new TableCell('', ['colspan' => $cellValue->getColspan()]);
            }
        }

        return $row;
    }

    /**
     * Gets number of columns by row.
     */
    private function getNumberOfColumns(array $row): int
    {
        $columns = \count($row);
        foreach ($row as $column) {
            $columns += $column instanceof TableCell ? ($column->getColspan() - 1) : 0;
        }

        return $columns;
    }

    /**
     * Gets list of columns for the given row.
     */
    private function getRowColumns(array $row): array
    {
        $columns = range(0, $this->numberOfColumns - 1);
        foreach ($row as $cellKey => $cell) {
            if ($cell instanceof TableCell && $cell->getColspan() > 1) {
                // exclude grouped columns.
                $columns = array_diff($columns, range($cellKey + 1, $cellKey + $cell->getColspan() - 1));
            }
        }

        return $columns;
    }

    /**
     * Calculates columns widths.
     */
    private function calculateColumnsWidth(iterable $groups): void
    {
        for ($column = 0; $column < $this->numberOfColumns; ++$column) {
            $lengths = [];
            foreach ($groups as $group) {
                foreach ($group as $row) {
                    if ($row instanceof TableSeparator) {
                        continue;
                    }

                    foreach ($row as $i => $cell) {
                        if ($cell instanceof TableCell) {
                            $textContent = Helper::removeDecoration($this->output->getFormatter(), $cell);
                            $textLength = Helper::width($textContent);
                            if ($textLength > 0) {
                                $contentColumns = mb_str_split($textContent, ceil($textLength / $cell->getColspan()));
                                foreach ($contentColumns as $position => $content) {
                                    $row[$i + $position] = $content;
                                }
                            }
                        }
                    }

                    $lengths[] = $this->getCellWidth($row, $column);
                }
            }

            $this->effectiveColumnWidths[$column] = max($lengths) + Helper::width($this->style->getCellRowContentFormat()) - 2;
        }
    }

    private function getColumnSeparatorWidth(): int
    {
        return Helper::width(\sprintf($this->style->getBorderFormat(), $this->style->getBorderChars()[3]));
    }

    private function getCellWidth(array $row, int $column): int
    {
        $cellWidth = 0;

        if (isset($row[$column])) {
            $cell = $row[$column];
            $cellWidth = Helper::width(Helper::removeDecoration($this->output->getFormatter(), $cell));
        }

        $columnWidth = $this->columnWidths[$column] ?? 0;
        $cellWidth = max($cellWidth, $columnWidth);

        return isset($this->columnMaxWidths[$column]) ? min($this->columnMaxWidths[$column], $cellWidth) : $cellWidth;
    }

    /**
     * Called after rendering to cleanup cache data.
     */
    private function cleanup(): void
    {
        $this->effectiveColumnWidths = [];
        unset($this->numberOfColumns);
    }

    /**
     * @return array<string, TableStyle>
     */
    private static function initStyles(): array
    {
        $borderless = new TableStyle();
        $borderless
            ->setHorizontalBorderChars('=')
            ->setVerticalBorderChars(' ')
            ->setDefaultCrossingChar(' ')
        ;

        $compact = new TableStyle();
        $compact
            ->setHorizontalBorderChars('')
            ->setVerticalBorderChars('')
            ->setDefaultCrossingChar('')
            ->setCellRowContentFormat('%s ')
        ;

        $styleGuide = new TableStyle();
        $styleGuide
            ->setHorizontalBorderChars('-')
            ->setVerticalBorderChars(' ')
            ->setDefaultCrossingChar(' ')
            ->setCellHeaderFormat('%s')
        ;

        $box = (new TableStyle())
            ->setHorizontalBorderChars('─')
            ->setVerticalBorderChars('│')
            ->setCrossingChars('┼', '┌', '┬', '┐', '┤', '┘', '┴', '└', '├')
        ;

        $boxDouble = (new TableStyle())
            ->setHorizontalBorderChars('═', '─')
            ->setVerticalBorderChars('║', '│')
            ->setCrossingChars('┼', '╔', '╤', '╗', '╢', '╝', '╧', '╚', '╟', '╠', '╪', '╣')
        ;

        return [
            'default' => new TableStyle(),
            'borderless' => $borderless,
            'compact' => $compact,
            'symfony-style-guide' => $styleGuide,
            'box' => $box,
            'box-double' => $boxDouble,
        ];
    }

    private function resolveStyle(TableStyle|string $name): TableStyle
    {
        if ($name instanceof TableStyle) {
            return $name;
        }

        return self::$styles[$name] ?? throw new InvalidArgumentException(\sprintf('Style "%s" is not defined.', $name));
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Helper;

/**
 * TerminalInputHelper stops Ctrl-C and similar signals from leaving the terminal in
 * an unusable state if its settings have been modified when reading user input.
 * This can be an issue on non-Windows platforms.
 *
 * Usage:
 *
 *     $inputHelper = new TerminalInputHelper($inputStream);
 *
 *     ...change terminal settings
 *
 *     // Wait for input before all input reads
 *     $inputHelper->waitForInput();
 *
 *     ...read input
 *
 *     // Call finish to restore terminal settings and signal handlers
 *     $inputHelper->finish()
 *
 * @internal
 */
final class TerminalInputHelper
{
    /** @var resource */
    private $inputStream;
    private bool $isStdin;
    private string $initialState = '';
    private int $signalToKill = 0;
    private array $signalHandlers = [];
    private array $targetSignals = [];
    private bool $withStty;

    /**
     * @param resource $inputStream
     *
     * @throws \RuntimeException If unable to read terminal settings
     */
    public function __construct($inputStream, bool $withStty = true)
    {
        $this->inputStream = $inputStream;
        $this->isStdin = 'php://stdin' === stream_get_meta_data($inputStream)['uri'];
        $this->withStty = $withStty;

        if ($withStty) {
            if (!\is_string($state = shell_exec('stty -g'))) {
                throw new \RuntimeException('Unable to read the terminal settings.');
            }

            $this->initialState = trim($state);

            $this->createSignalHandlers();
        }
    }

    /**
     * Waits for input.
     */
    public function waitForInput(): void
    {
        if ($this->isStdin) {
            $r = [$this->inputStream];
            $w = [];

            // Allow signal handlers to run
            while (0 === @stream_select($r, $w, $w, 0, 100)) {
                $r = [$this->inputStream];
            }
        }

        if ($this->withStty) {
            $this->checkForKillSignal();
        }
    }

    /**
     * Restores terminal state and signal handlers.
     */
    public function finish(): void
    {
        if (!$this->withStty) {
            return;
        }

        // Safeguard in case an unhandled kill signal exists
        $this->checkForKillSignal();

        // The captured "stty -g" state is not guaranteed to be accepted back by "stty" on every
        // platform/terminal (e.g. some nested pty implementations reject it with "invalid argument"),
        // and shell_exec() gives no way to detect that failure. Try the exact restore first so
        // any custom terminal settings survive, but always fall back to "stty sane" so the
        // terminal is left in a usable state even when the exact restore silently fails.
        shell_exec('stty '.$this->initialState.' 2>/dev/null || stty sane');
        $this->signalToKill = 0;

        foreach ($this->signalHandlers as $signal => $originalHandler) {
            pcntl_signal($signal, $originalHandler);
        }
        $this->signalHandlers = [];
        $this->targetSignals = [];
    }

    private function createSignalHandlers(): void
    {
        if (!\function_exists('pcntl_async_signals') || !\function_exists('pcntl_signal')) {
            return;
        }

        pcntl_async_signals(true);
        $this->targetSignals = [\SIGINT, \SIGQUIT, \SIGTERM];

        foreach ($this->targetSignals as $signal) {
            $this->signalHandlers[$signal] = pcntl_signal_get_handler($signal);

            pcntl_signal($signal, function ($signal) {
                // Save current state, then restore to initial state. The original signal
                // handler is about to run, so fall back to "stty sane" if the exact restore
                // is rejected by "stty" (see the same fallback in finish()), to make sure it
                // runs with a usable terminal.
                $currentState = shell_exec('stty -g');
                shell_exec('stty '.$this->initialState.' 2>/dev/null || stty sane');
                $originalHandler = $this->signalHandlers[$signal];

                if (\is_callable($originalHandler)) {
                    $originalHandler($signal);
                    // Handler did not exit, so restore to current state
                    shell_exec('stty '.$currentState);

                    return;
                }

                // Not a callable, so SIG_DFL or SIG_IGN
                if (\SIG_DFL === $originalHandler) {
                    $this->signalToKill = $signal;
                }
            });
        }
    }

    private function checkForKillSignal(): void
    {
        if (\in_array($this->signalToKill, $this->targetSignals, true)) {
            // Try posix_kill
            if (\function_exists('posix_kill')) {
                pcntl_signal($this->signalToKill, \SIG_DFL);
                posix_kill(getmypid(), $this->signalToKill);
            }

            // Best attempt fallback
            exit(128 + $this->signalToKill);
        }
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Helper;

/**
 * @internal
 */
class TableRows implements \IteratorAggregate
{
    private \Closure $generator;

    public function __construct(\Closure $generator)
    {
        $this->generator = $generator;
    }

    public function getIterator(): \Traversable
    {
        return ($this->generator)();
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Messenger;

/**
 * @author Kevin Bond <kevinbond@gmail.com>
 */
final class RunCommandContext
{
    public function __construct(
        public readonly RunCommandMessage $message,
        public readonly int $exitCode,
        public readonly string $output,
    ) {
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Messenger;

use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\RunCommandFailedException;
use Symfony\Component\Console\Input\StringInput;
use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\Messenger\Exception\RecoverableExceptionInterface;
use Symfony\Component\Messenger\Exception\UnrecoverableExceptionInterface;

/**
 * @author Kevin Bond <kevinbond@gmail.com>
 */
final class RunCommandMessageHandler
{
    public function __construct(private readonly Application $application)
    {
    }

    public function __invoke(RunCommandMessage $message): RunCommandContext
    {
        $input = new StringInput($message->input);
        $output = new BufferedOutput();

        $this->application->setCatchExceptions($message->catchExceptions);

        try {
            $exitCode = $this->application->run($input, $output);
        } catch (UnrecoverableExceptionInterface|RecoverableExceptionInterface $e) {
            throw $e;
        } catch (\Throwable $e) {
            throw new RunCommandFailedException($e, new RunCommandContext($message, Command::FAILURE, $output->fetch()));
        }

        if ($message->throwOnFailure && Command::SUCCESS !== $exitCode) {
            throw new RunCommandFailedException(\sprintf('Command "%s" exited with code "%s".', $message->input, $exitCode), new RunCommandContext($message, $exitCode, $output->fetch()));
        }

        return new RunCommandContext($message, $exitCode, $output->fetch());
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Messenger;

use Symfony\Component\Console\Exception\RunCommandFailedException;

/**
 * @author Kevin Bond <kevinbond@gmail.com>
 */
class RunCommandMessage implements \Stringable
{
    /**
     * @param bool $throwOnFailure  If the command has a non-zero exit code, throw {@see RunCommandFailedException}
     * @param bool $catchExceptions @see Application::setCatchExceptions()
     */
    public function __construct(
        public readonly string $input,
        public readonly bool $throwOnFailure = true,
        public readonly bool $catchExceptions = false,
    ) {
    }

    public function __toString(): string
    {
        return $this->input;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Input;

use Symfony\Component\Console\Exception\InvalidArgumentException;

/**
 * StringInput represents an input provided as a string.
 *
 * Usage:
 *
 *     $input = new StringInput('foo --bar="foobar"');
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class StringInput extends ArgvInput
{
    /**
     * @deprecated since Symfony 6.1
     */
    public const REGEX_STRING = '([^\s]+?)(?:\s|(?<!\\\\)"|(?<!\\\\)\'|$)';
    public const REGEX_UNQUOTED_STRING = '([^\s\\\\]+?)';
    public const REGEX_QUOTED_STRING = '(?:"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\')';

    /**
     * @param string $input A string representing the parameters from the CLI
     */
    public function __construct(string $input)
    {
        parent::__construct([]);

        $this->setTokens($this->tokenize($input));
    }

    /**
     * Tokenizes a string.
     *
     * @throws InvalidArgumentException When unable to parse input (should never happen)
     */
    private function tokenize(string $input): array
    {
        $tokens = [];
        $length = \strlen($input);
        $cursor = 0;
        $token = null;
        while ($cursor < $length) {
            if ('\\' === $input[$cursor]) {
                $token .= $input[++$cursor] ?? '';
                ++$cursor;
                continue;
            }

            if (preg_match('/\s+/A', $input, $match, 0, $cursor)) {
                if (null !== $token) {
                    $tokens[] = $token;
                    $token = null;
                }
            } elseif (preg_match('/([^="\'\s]+?)(=?)('.self::REGEX_QUOTED_STRING.'+)/A', $input, $match, 0, $cursor)) {
                $token .= $match[1].$match[2].stripcslashes(str_replace(['"\'', '\'"', '\'\'', '""'], '', substr($match[3], 1, -1)));
            } elseif (preg_match('/'.self::REGEX_QUOTED_STRING.'/A', $input, $match, 0, $cursor)) {
                $token .= stripcslashes(substr($match[0], 1, -1));
            } elseif (preg_match('/'.self::REGEX_UNQUOTED_STRING.'/A', $input, $match, 0, $cursor)) {
                $token .= $match[1];
            } else {
                // should never happen
                throw new InvalidArgumentException(\sprintf('Unable to parse input near "... %s ...".', substr($input, $cursor, 10)));
            }

            $cursor += \strlen($match[0]);
        }

        if (null !== $token) {
            $tokens[] = $token;
        }

        return $tokens;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Input;

use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\LogicException;

/**
 * A InputDefinition represents a set of valid command line arguments and options.
 *
 * Usage:
 *
 *     $definition = new InputDefinition([
 *         new InputArgument('name', InputArgument::REQUIRED),
 *         new InputOption('foo', 'f', InputOption::VALUE_REQUIRED),
 *     ]);
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class InputDefinition
{
    private array $arguments = [];
    private int $requiredCount = 0;
    private ?InputArgument $lastArrayArgument = null;
    private ?InputArgument $lastOptionalArgument = null;
    private array $options = [];
    private array $negations = [];
    private array $shortcuts = [];

    /**
     * @param array $definition An array of InputArgument and InputOption instance
     */
    public function __construct(array $definition = [])
    {
        $this->setDefinition($definition);
    }

    /**
     * Sets the definition of the input.
     *
     * @return void
     */
    public function setDefinition(array $definition)
    {
        $arguments = [];
        $options = [];
        foreach ($definition as $item) {
            if ($item instanceof InputOption) {
                $options[] = $item;
            } else {
                $arguments[] = $item;
            }
        }

        $this->setArguments($arguments);
        $this->setOptions($options);
    }

    /**
     * Sets the InputArgument objects.
     *
     * @param InputArgument[] $arguments An array of InputArgument objects
     *
     * @return void
     */
    public function setArguments(array $arguments = [])
    {
        $this->arguments = [];
        $this->requiredCount = 0;
        $this->lastOptionalArgument = null;
        $this->lastArrayArgument = null;
        $this->addArguments($arguments);
    }

    /**
     * Adds an array of InputArgument objects.
     *
     * @param InputArgument[] $arguments An array of InputArgument objects
     *
     * @return void
     */
    public function addArguments(?array $arguments = [])
    {
        if (null !== $arguments) {
            foreach ($arguments as $argument) {
                $this->addArgument($argument);
            }
        }
    }

    /**
     * @return void
     *
     * @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 exists.', $argument->getName()));
        }

        if (null !== $this->lastArrayArgument) {
            throw new LogicException(\sprintf('Cannot add a required argument "%s" after an array argument "%s".', $argument->getName(), $this->lastArrayArgument->getName()));
        }

        if ($argument->isRequired() && null !== $this->lastOptionalArgument) {
            throw new LogicException(\sprintf('Cannot add a required argument "%s" after an optional one "%s".', $argument->getName(), $this->lastOptionalArgument->getName()));
        }

        if ($argument->isArray()) {
            $this->lastArrayArgument = $argument;
        }

        if ($argument->isRequired()) {
            ++$this->requiredCount;
        } else {
            $this->lastOptionalArgument = $argument;
        }

        $this->arguments[$argument->getName()] = $argument;
    }

    /**
     * Returns an InputArgument by name or by position.
     *
     * @throws InvalidArgumentException When argument given doesn't exist
     */
    public function getArgument(string|int $name): InputArgument
    {
        if (!$this->hasArgument($name)) {
            throw new InvalidArgumentException(\sprintf('The "%s" argument does not exist.', $name));
        }

        $arguments = \is_int($name) ? array_values($this->arguments) : $this->arguments;

        return $arguments[$name];
    }

    /**
     * Returns true if an InputArgument object exists by name or position.
     */
    public function hasArgument(string|int $name): bool
    {
        $arguments = \is_int($name) ? array_values($this->arguments) : $this->arguments;

        return isset($arguments[$name]);
    }

    /**
     * Gets the array of InputArgument objects.
     *
     * @return InputArgument[]
     */
    public function getArguments(): array
    {
        return $this->arguments;
    }

    /**
     * Returns the number of InputArguments.
     */
    public function getArgumentCount(): int
    {
        return null !== $this->lastArrayArgument ? \PHP_INT_MAX : \count($this->arguments);
    }

    /**
     * Returns the number of required InputArguments.
     */
    public function getArgumentRequiredCount(): int
    {
        return $this->requiredCount;
    }

    /**
     * @return array<string|bool|int|float|array|null>
     */
    public function getArgumentDefaults(): array
    {
        $values = [];
        foreach ($this->arguments as $argument) {
            $values[$argument->getName()] = $argument->getDefault();
        }

        return $values;
    }

    /**
     * Sets the InputOption objects.
     *
     * @param InputOption[] $options An array of InputOption objects
     *
     * @return void
     */
    public function setOptions(array $options = [])
    {
        $this->options = [];
        $this->shortcuts = [];
        $this->negations = [];
        $this->addOptions($options);
    }

    /**
     * Adds an array of InputOption objects.
     *
     * @param InputOption[] $options An array of InputOption objects
     *
     * @return void
     */
    public function addOptions(array $options = [])
    {
        foreach ($options as $option) {
            $this->addOption($option);
        }
    }

    /**
     * @return void
     *
     * @throws LogicException When option given already exist
     */
    public function addOption(InputOption $option)
    {
        if (isset($this->options[$option->getName()]) && !$option->equals($this->options[$option->getName()])) {
            throw new LogicException(\sprintf('An option named "%s" already exists.', $option->getName()));
        }
        if (isset($this->negations[$option->getName()])) {
            throw new LogicException(\sprintf('An option named "%s" already exists.', $option->getName()));
        }

        if ($option->getShortcut()) {
            foreach (explode('|', $option->getShortcut()) as $shortcut) {
                if (isset($this->shortcuts[$shortcut]) && !$option->equals($this->options[$this->shortcuts[$shortcut]])) {
                    throw new LogicException(\sprintf('An option with shortcut "%s" already exists.', $shortcut));
                }
            }
        }

        $this->options[$option->getName()] = $option;
        if ($option->getShortcut()) {
            foreach (explode('|', $option->getShortcut()) as $shortcut) {
                $this->shortcuts[$shortcut] = $option->getName();
            }
        }

        if ($option->isNegatable()) {
            $negatedName = 'no-'.$option->getName();
            if (isset($this->options[$negatedName])) {
                throw new LogicException(\sprintf('An option named "%s" already exists.', $negatedName));
            }
            $this->negations[$negatedName] = $option->getName();
        }
    }

    /**
     * Returns an InputOption by name.
     *
     * @throws InvalidArgumentException When option given doesn't exist
     */
    public function getOption(string $name): InputOption
    {
        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.
     *
     * This method can't be used to check if the user included the option when
     * executing the command (use getOption() instead).
     */
    public function hasOption(string $name): bool
    {
        return isset($this->options[$name]);
    }

    /**
     * Gets the array of InputOption objects.
     *
     * @return InputOption[]
     */
    public function getOptions(): array
    {
        return $this->options;
    }

    /**
     * Returns true if an InputOption object exists by shortcut.
     */
    public function hasShortcut(string $name): bool
    {
        return isset($this->shortcuts[$name]);
    }

    /**
     * Returns true if an InputOption object exists by negated name.
     */
    public function hasNegation(string $name): bool
    {
        return isset($this->negations[$name]);
    }

    /**
     * Gets an InputOption by shortcut.
     */
    public function getOptionForShortcut(string $shortcut): InputOption
    {
        return $this->getOption($this->shortcutToName($shortcut));
    }

    /**
     * @return array<string|bool|int|float|array|null>
     */
    public function getOptionDefaults(): array
    {
        $values = [];
        foreach ($this->options as $option) {
            $values[$option->getName()] = $option->getDefault();
        }

        return $values;
    }

    /**
     * Returns the InputOption name given a shortcut.
     *
     * @throws InvalidArgumentException When option given does not exist
     *
     * @internal
     */
    public function shortcutToName(string $shortcut): string
    {
        if (!isset($this->shortcuts[$shortcut])) {
            throw new InvalidArgumentException(\sprintf('The "-%s" option does not exist.', $shortcut));
        }

        return $this->shortcuts[$shortcut];
    }

    /**
     * Returns the InputOption name given a negation.
     *
     * @throws InvalidArgumentException When option given does not exist
     *
     * @internal
     */
    public function negationToName(string $negation): string
    {
        if (!isset($this->negations[$negation])) {
            throw new InvalidArgumentException(\sprintf('The "--%s" option does not exist.', $negation));
        }

        return $this->negations[$negation];
    }

    /**
     * Gets the synopsis.
     */
    public function getSynopsis(bool $short = false): string
    {
        $elements = [];

        if ($short && $this->getOptions()) {
            $elements[] = '[options]';
        } elseif (!$short) {
            foreach ($this->getOptions() as $option) {
                $value = '';
                if ($option->acceptValue()) {
                    $value = \sprintf(
                        ' %s%s%s',
                        $option->isValueOptional() ? '[' : '',
                        strtoupper($option->getName()),
                        $option->isValueOptional() ? ']' : ''
                    );
                }

                $shortcut = $option->getShortcut() ? \sprintf('-%s|', $option->getShortcut()) : '';
                $negation = $option->isNegatable() ? \sprintf('|--no-%s', $option->getName()) : '';
                $elements[] = \sprintf('[%s--%s%s%s]', $shortcut, $option->getName(), $value, $negation);
            }
        }

        if (\count($elements) && $this->getArguments()) {
            $elements[] = '[--]';
        }

        $tail = '';
        foreach ($this->getArguments() as $argument) {
            $element = '<'.$argument->getName().'>';
            if ($argument->isArray()) {
                $element .= '...';
            }

            if (!$argument->isRequired()) {
                $element = '['.$element;
                $tail .= ']';
            }

            $elements[] = $element;
        }

        return implode(' ', $elements).$tail;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Input;

/**
 * InputAwareInterface should be implemented by classes that depends on the
 * Console Input.
 *
 * @author Wouter J <waldio.webdesign@gmail.com>
 */
interface InputAwareInterface
{
    /**
     * Sets the Console Input.
     *
     * @return void
     */
    public function setInput(InputInterface $input);
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Input;

use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\RuntimeException;

/**
 * InputInterface is the interface implemented by all input classes.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 *
 * @method string __toString() Returns a stringified representation of the args passed to the command.
 *                             InputArguments MUST be escaped as well as the InputOption values passed to the command.
 */
interface InputInterface
{
    /**
     * Returns the first argument from the raw parameters (not parsed).
     */
    public function getFirstArgument(): ?string;

    /**
     * Returns true if the raw parameters (not parsed) contain a value.
     *
     * This method is to be used to introspect the input parameters
     * before they have been validated. It must be used carefully.
     * Does not necessarily return the correct result for short options
     * when multiple flags are combined in the same option.
     *
     * @param string|array $values     The values to look for in the raw parameters (can be an array)
     * @param bool         $onlyParams Only check real parameters, skip those following an end of options (--) signal
     */
    public function hasParameterOption(string|array $values, bool $onlyParams = false): bool;

    /**
     * Returns the value of a raw option (not parsed).
     *
     * This method is to be used to introspect the input parameters
     * before they have been validated. It must be used carefully.
     * Does not necessarily return the correct result for short options
     * when multiple flags are combined in the same option.
     *
     * @param string|array                     $values     The value(s) to look for in the raw parameters (can be an array)
     * @param string|bool|int|float|array|null $default    The default value to return if no result is found
     * @param bool                             $onlyParams Only check real parameters, skip those following an end of options (--) signal
     *
     * @return mixed
     */
    public function getParameterOption(string|array $values, string|bool|int|float|array|null $default = false, bool $onlyParams = false);

    /**
     * Binds the current Input instance with the given arguments and options.
     *
     * @return void
     *
     * @throws RuntimeException
     */
    public function bind(InputDefinition $definition);

    /**
     * Validates the input.
     *
     * @return void
     *
     * @throws RuntimeException When not enough arguments are given
     */
    public function validate();

    /**
     * Returns all the given arguments merged with the default values.
     *
     * @return array<string|bool|int|float|array|null>
     */
    public function getArguments(): array;

    /**
     * Returns the argument value for a given argument name.
     *
     * @return mixed
     *
     * @throws InvalidArgumentException When argument given doesn't exist
     */
    public function getArgument(string $name);

    /**
     * Sets an argument value by name.
     *
     * @return void
     *
     * @throws InvalidArgumentException When argument given doesn't exist
     */
    public function setArgument(string $name, mixed $value);

    /**
     * Returns true if an InputArgument object exists by name or position.
     */
    public function hasArgument(string $name): bool;

    /**
     * Returns all the given options merged with the default values.
     *
     * @return array<string|bool|int|float|array|null>
     */
    public function getOptions(): array;

    /**
     * Returns the option value for a given option name.
     *
     * @return mixed
     *
     * @throws InvalidArgumentException When option given doesn't exist
     */
    public function getOption(string $name);

    /**
     * Sets an option value by name.
     *
     * @return void
     *
     * @throws InvalidArgumentException When option given doesn't exist
     */
    public function setOption(string $name, mixed $value);

    /**
     * Returns true if an InputOption object exists by name.
     */
    public function hasOption(string $name): bool;

    /**
     * Is this input means interactive?
     */
    public function isInteractive(): bool;

    /**
     * Sets the input interactivity.
     *
     * @return void
     */
    public function setInteractive(bool $interactive);
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Input;

/**
 * StreamableInputInterface is the interface implemented by all input classes
 * that have an input stream.
 *
 * @author Robin Chalas <robin.chalas@gmail.com>
 */
interface StreamableInputInterface extends InputInterface
{
    /**
     * Sets the input stream to read from when interacting with the user.
     *
     * This is mainly useful for testing purpose.
     *
     * @param resource $stream The input stream
     *
     * @return void
     */
    public function setStream($stream);

    /**
     * Returns the input stream.
     *
     * @return resource|null
     */
    public function getStream();
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Input;

use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\InvalidOptionException;

/**
 * ArrayInput represents an input provided as an array.
 *
 * Usage:
 *
 *     $input = new ArrayInput(['command' => 'foo:bar', 'foo' => 'bar', '--bar' => 'foobar']);
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class ArrayInput extends Input
{
    private array $parameters;

    public function __construct(array $parameters, ?InputDefinition $definition = null)
    {
        $this->parameters = $parameters;

        parent::__construct($definition);
    }

    public function getFirstArgument(): ?string
    {
        foreach ($this->parameters as $param => $value) {
            if ($param && \is_string($param) && '-' === $param[0]) {
                continue;
            }

            return $value;
        }

        return null;
    }

    public function hasParameterOption(string|array $values, bool $onlyParams = false): bool
    {
        $values = (array) $values;

        foreach ($this->parameters as $k => $v) {
            if (!\is_int($k)) {
                $v = $k;
            }

            if ($onlyParams && '--' === $v) {
                return false;
            }

            if (\in_array($v, $values)) {
                return true;
            }
        }

        return false;
    }

    public function getParameterOption(string|array $values, string|bool|int|float|array|null $default = false, bool $onlyParams = false): mixed
    {
        $values = (array) $values;

        foreach ($this->parameters as $k => $v) {
            if ($onlyParams && ('--' === $k || (\is_int($k) && '--' === $v))) {
                return $default;
            }

            if (\is_int($k)) {
                if (\in_array($v, $values)) {
                    return true;
                }
            } elseif (\in_array($k, $values)) {
                return $v;
            }
        }

        return $default;
    }

    /**
     * Returns a stringified representation of the args passed to the command.
     */
    public function __toString(): string
    {
        $params = [];
        foreach ($this->parameters as $param => $val) {
            if ($param && \is_string($param) && '-' === $param[0]) {
                $glue = ('-' === $param[1]) ? '=' : ' ';
                if (\is_array($val)) {
                    foreach ($val as $v) {
                        $params[] = $param.('' != $v ? $glue.$this->escapeToken($v) : '');
                    }
                } else {
                    $params[] = $param.('' != $val ? $glue.$this->escapeToken($val) : '');
                }
            } else {
                $params[] = \is_array($val) ? implode(' ', array_map($this->escapeToken(...), $val)) : $this->escapeToken($val);
            }
        }

        return implode(' ', $params);
    }

    /**
     * @return void
     */
    protected function parse()
    {
        foreach ($this->parameters as $key => $value) {
            if ('--' === $key) {
                return;
            }
            if (str_starts_with($key, '--')) {
                $this->addLongOption(substr($key, 2), $value);
            } elseif (str_starts_with($key, '-')) {
                $this->addShortOption(substr($key, 1), $value);
            } else {
                $this->addArgument($key, $value);
            }
        }
    }

    /**
     * Adds a short option value.
     *
     * @throws InvalidOptionException When option given doesn't exist
     */
    private function addShortOption(string $shortcut, mixed $value): void
    {
        if (!$this->definition->hasShortcut($shortcut)) {
            throw new InvalidOptionException(\sprintf('The "-%s" option does not exist.', $shortcut));
        }

        $this->addLongOption($this->definition->getOptionForShortcut($shortcut)->getName(), $value);
    }

    /**
     * Adds a long option value.
     *
     * @throws InvalidOptionException When option given doesn't exist
     * @throws InvalidOptionException When a required value is missing
     */
    private function addLongOption(string $name, mixed $value): void
    {
        if (!$this->definition->hasOption($name)) {
            if (!$this->definition->hasNegation($name)) {
                throw new InvalidOptionException(\sprintf('The "--%s" option does not exist.', $name));
            }

            $optionName = $this->definition->negationToName($name);
            $this->options[$optionName] = false;

            return;
        }

        $option = $this->definition->getOption($name);

        if (null === $value) {
            if ($option->isValueRequired()) {
                throw new InvalidOptionException(\sprintf('The "--%s" option requires a value.', $name));
            }

            if (!$option->isValueOptional()) {
                $value = true;
            }
        }

        $this->options[$name] = $value;
    }

    /**
     * Adds an argument value.
     *
     * @throws InvalidArgumentException When argument given doesn't exist
     */
    private function addArgument(string|int $name, mixed $value): void
    {
        if (!$this->definition->hasArgument($name)) {
            throw new InvalidArgumentException(\sprintf('The "%s" argument does not exist.', $name));
        }

        $this->arguments[$name] = $value;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Input;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Completion\Suggestion;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\LogicException;

/**
 * Represents a command line argument.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class InputArgument
{
    public const REQUIRED = 1;
    public const OPTIONAL = 2;
    public const IS_ARRAY = 4;

    private string $name;
    private int $mode;
    private string|int|bool|array|float|null $default;
    private array|\Closure $suggestedValues;
    private string $description;

    /**
     * @param string                                                                        $name            The argument name
     * @param int|null                                                                      $mode            The argument mode: a bit mask of self::REQUIRED, self::OPTIONAL and self::IS_ARRAY
     * @param string                                                                        $description     A description text
     * @param string|bool|int|float|array|null                                              $default         The default value (for self::OPTIONAL mode only)
     * @param array|\Closure(CompletionInput,CompletionSuggestions):list<string|Suggestion> $suggestedValues The values used for input completion
     *
     * @throws InvalidArgumentException When argument mode is not valid
     */
    public function __construct(string $name, ?int $mode = null, string $description = '', string|bool|int|float|array|null $default = null, \Closure|array $suggestedValues = [])
    {
        if (null === $mode) {
            $mode = self::OPTIONAL;
        } elseif ($mode > 7 || $mode < 1) {
            throw new InvalidArgumentException(\sprintf('Argument mode "%s" is not valid.', $mode));
        }

        $this->name = $name;
        $this->mode = $mode;
        $this->description = $description;
        $this->suggestedValues = $suggestedValues;

        $this->setDefault($default);
    }

    /**
     * Returns the argument name.
     */
    public function getName(): string
    {
        return $this->name;
    }

    /**
     * Returns true if the argument is required.
     *
     * @return bool true if parameter mode is self::REQUIRED, false otherwise
     */
    public function isRequired(): bool
    {
        return self::REQUIRED === (self::REQUIRED & $this->mode);
    }

    /**
     * Returns true if the argument can take multiple values.
     *
     * @return bool true if mode is self::IS_ARRAY, false otherwise
     */
    public function isArray(): bool
    {
        return self::IS_ARRAY === (self::IS_ARRAY & $this->mode);
    }

    /**
     * Sets the default value.
     *
     * @return void
     *
     * @throws LogicException When incorrect default value is given
     */
    public function setDefault(string|bool|int|float|array|null $default = null)
    {
        if (1 > \func_num_args()) {
            trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__);
        }
        if ($this->isRequired() && null !== $default) {
            throw new LogicException('Cannot set a default value except for InputArgument::OPTIONAL mode.');
        }

        if ($this->isArray()) {
            if (null === $default) {
                $default = [];
            } elseif (!\is_array($default)) {
                throw new LogicException('A default value for an array argument must be an array.');
            }
        }

        $this->default = $default;
    }

    /**
     * Returns the default value.
     */
    public function getDefault(): string|bool|int|float|array|null
    {
        return $this->default;
    }

    public function hasCompletion(): bool
    {
        return [] !== $this->suggestedValues;
    }

    /**
     * Adds suggestions to $suggestions for the current completion input.
     *
     * @see Command::complete()
     */
    public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
    {
        $values = $this->suggestedValues;
        if ($values instanceof \Closure && !\is_array($values = $values($input))) {
            throw new LogicException(\sprintf('Closure for argument "%s" must return an array. Got "%s".', $this->name, get_debug_type($values)));
        }
        if ($values) {
            $suggestions->suggestValues($values);
        }
    }

    /**
     * Returns the description text.
     */
    public function getDescription(): string
    {
        return $this->description;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Input;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Completion\Suggestion;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\LogicException;

/**
 * Represents a command line option.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class InputOption
{
    /**
     * Do not accept input for the option (e.g. --yell). This is the default behavior of options.
     */
    public const VALUE_NONE = 1;

    /**
     * A value must be passed when the option is used (e.g. --iterations=5 or -i5).
     */
    public const VALUE_REQUIRED = 2;

    /**
     * The option may or may not have a value (e.g. --yell or --yell=loud).
     */
    public const VALUE_OPTIONAL = 4;

    /**
     * The option accepts multiple values (e.g. --dir=/foo --dir=/bar).
     */
    public const VALUE_IS_ARRAY = 8;

    /**
     * The option may have either positive or negative value (e.g. --ansi or --no-ansi).
     */
    public const VALUE_NEGATABLE = 16;

    private string $name;
    private string|array|null $shortcut;
    private int $mode;
    private string|int|bool|array|float|null $default;
    private array|\Closure $suggestedValues;
    private string $description;

    /**
     * @param string|array|null                                                             $shortcut        The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts
     * @param int|null                                                                      $mode            The option mode: One of the VALUE_* constants
     * @param string|bool|int|float|array|null                                              $default         The default value (must be null for self::VALUE_NONE)
     * @param array|\Closure(CompletionInput,CompletionSuggestions):list<string|Suggestion> $suggestedValues The values used for input completion
     *
     * @throws InvalidArgumentException If option mode is invalid or incompatible
     */
    public function __construct(string $name, string|array|null $shortcut = null, ?int $mode = null, string $description = '', string|bool|int|float|array|null $default = null, array|\Closure $suggestedValues = [])
    {
        if (str_starts_with($name, '--')) {
            $name = substr($name, 2);
        }

        if (empty($name)) {
            throw new InvalidArgumentException('An option name cannot be empty.');
        }

        if ('' === $shortcut || [] === $shortcut || false === $shortcut) {
            $shortcut = null;
        }

        if (null !== $shortcut) {
            if (\is_array($shortcut)) {
                $shortcut = implode('|', $shortcut);
            }
            $shortcuts = preg_split('{(\|)-?}', ltrim($shortcut, '-'));
            $shortcuts = array_filter($shortcuts, 'strlen');
            $shortcut = implode('|', $shortcuts);

            if ('' === $shortcut) {
                throw new InvalidArgumentException('An option shortcut cannot be empty.');
            }
        }

        if (null === $mode) {
            $mode = self::VALUE_NONE;
        } elseif ($mode >= (self::VALUE_NEGATABLE << 1) || $mode < 1) {
            throw new InvalidArgumentException(\sprintf('Option mode "%s" is not valid.', $mode));
        }

        $this->name = $name;
        $this->shortcut = $shortcut;
        $this->mode = $mode;
        $this->description = $description;
        $this->suggestedValues = $suggestedValues;

        if ($suggestedValues && !$this->acceptValue()) {
            throw new LogicException('Cannot set suggested values if the option does not accept a value.');
        }
        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.');
        }
        if ($this->isNegatable() && $this->acceptValue()) {
            throw new InvalidArgumentException('Impossible to have an option mode VALUE_NEGATABLE if the option also accepts a value.');
        }

        $this->setDefault($default);
    }

    /**
     * Returns the option shortcut.
     */
    public function getShortcut(): ?string
    {
        return $this->shortcut;
    }

    /**
     * Returns the option name.
     */
    public function getName(): string
    {
        return $this->name;
    }

    /**
     * Returns true if the option accepts a value.
     *
     * @return bool true if value mode is not self::VALUE_NONE, false otherwise
     */
    public function acceptValue(): bool
    {
        return $this->isValueRequired() || $this->isValueOptional();
    }

    /**
     * Returns true if the option requires a value.
     *
     * @return bool true if value mode is self::VALUE_REQUIRED, false otherwise
     */
    public function isValueRequired(): bool
    {
        return self::VALUE_REQUIRED === (self::VALUE_REQUIRED & $this->mode);
    }

    /**
     * Returns true if the option takes an optional value.
     *
     * @return bool true if value mode is self::VALUE_OPTIONAL, false otherwise
     */
    public function isValueOptional(): bool
    {
        return self::VALUE_OPTIONAL === (self::VALUE_OPTIONAL & $this->mode);
    }

    /**
     * Returns true if the option can take multiple values.
     *
     * @return bool true if mode is self::VALUE_IS_ARRAY, false otherwise
     */
    public function isArray(): bool
    {
        return self::VALUE_IS_ARRAY === (self::VALUE_IS_ARRAY & $this->mode);
    }

    public function isNegatable(): bool
    {
        return self::VALUE_NEGATABLE === (self::VALUE_NEGATABLE & $this->mode);
    }

    /**
     * @return void
     */
    public function setDefault(string|bool|int|float|array|null $default = null)
    {
        if (1 > \func_num_args()) {
            trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__);
        }
        if (self::VALUE_NONE === (self::VALUE_NONE & $this->mode) && null !== $default) {
            throw new LogicException('Cannot set a default value when using InputOption::VALUE_NONE mode.');
        }

        if ($this->isArray()) {
            if (null === $default) {
                $default = [];
            } elseif (!\is_array($default)) {
                throw new LogicException('A default value for an array option must be an array.');
            }
        }

        $this->default = $this->acceptValue() || $this->isNegatable() ? $default : false;
    }

    /**
     * Returns the default value.
     */
    public function getDefault(): string|bool|int|float|array|null
    {
        return $this->default;
    }

    /**
     * Returns the description text.
     */
    public function getDescription(): string
    {
        return $this->description;
    }

    public function hasCompletion(): bool
    {
        return [] !== $this->suggestedValues;
    }

    /**
     * Adds suggestions to $suggestions for the current completion input.
     *
     * @see Command::complete()
     */
    public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
    {
        $values = $this->suggestedValues;
        if ($values instanceof \Closure && !\is_array($values = $values($input))) {
            throw new LogicException(\sprintf('Closure for option "%s" must return an array. Got "%s".', $this->name, get_debug_type($values)));
        }
        if ($values) {
            $suggestions->suggestValues($values);
        }
    }

    /**
     * Checks whether the given option equals this one.
     */
    public function equals(self $option): bool
    {
        return $option->getName() === $this->getName()
            && $option->getShortcut() === $this->getShortcut()
            && $option->getDefault() === $this->getDefault()
            && $option->isNegatable() === $this->isNegatable()
            && $option->isArray() === $this->isArray()
            && $option->isValueRequired() === $this->isValueRequired()
            && $option->isValueOptional() === $this->isValueOptional()
        ;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Input;

use Symfony\Component\Console\Exception\RuntimeException;

/**
 * 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 application.
 *
 * 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 <fabien@symfony.com>
 *
 * @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
{
    private array $tokens;
    private array $parsed;

    public function __construct(?array $argv = null, ?InputDefinition $definition = null)
    {
        $argv ??= $_SERVER['argv'] ?? [];

        // strip the application name
        array_shift($argv);

        $this->tokens = $argv;

        parent::__construct($definition);
    }

    /**
     * @return void
     */
    protected function setTokens(array $tokens)
    {
        $this->tokens = $tokens;
    }

    /**
     * @return void
     */
    protected function parse()
    {
        $parseOptions = true;
        $this->parsed = $this->tokens;
        while (null !== $token = array_shift($this->parsed)) {
            $parseOptions = $this->parseToken($token, $parseOptions);
        }
    }

    protected function parseToken(string $token, bool $parseOptions): bool
    {
        if ($parseOptions && '' == $token) {
            $this->parseArgument($token);
        } elseif ($parseOptions && '--' == $token) {
            return false;
        } elseif ($parseOptions && str_starts_with($token, '--')) {
            $this->parseLongOption($token);
        } elseif ($parseOptions && '-' === $token[0] && '-' !== $token) {
            $this->parseShortOption($token);
        } else {
            $this->parseArgument($token);
        }

        return $parseOptions;
    }

    /**
     * Parses a short option.
     */
    private function parseShortOption(string $token): void
    {
        $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.
     *
     * @throws RuntimeException When option given doesn't exist
     */
    private function parseShortOptionSet(string $name): void
    {
        $len = \strlen($name);
        for ($i = 0; $i < $len; ++$i) {
            if (!$this->definition->hasShortcut($name[$i])) {
                $encoding = mb_detect_encoding($name, null, true);
                throw new RuntimeException(\sprintf('The "-%s" option does not exist.', false === $encoding ? $name[$i] : mb_substr($name, $i, 1, $encoding)));
            }

            $option = $this->definition->getOptionForShortcut($name[$i]);
            if ($option->acceptValue()) {
                $this->addLongOption($option->getName(), $i === $len - 1 ? null : substr($name, $i + 1));

                break;
            }
            $this->addLongOption($option->getName(), null);
        }
    }

    /**
     * Parses a long option.
     */
    private function parseLongOption(string $token): void
    {
        $name = substr($token, 2);

        if (false !== $pos = strpos($name, '=')) {
            if ('' === $value = substr($name, $pos + 1)) {
                array_unshift($this->parsed, $value);
            }
            $this->addLongOption(substr($name, 0, $pos), $value);
        } else {
            $this->addLongOption($name, null);
        }
    }

    /**
     * Parses an argument.
     *
     * @throws RuntimeException When too many arguments are given
     */
    private function parseArgument(string $token): void
    {
        $c = \count($this->arguments);

        // if input is expecting another argument, add it
        if ($this->definition->hasArgument($c)) {
            $arg = $this->definition->getArgument($c);
            $this->arguments[$arg->getName()] = $arg->isArray() ? [$token] : $token;

        // if last argument isArray(), append token to last argument
        } elseif ($this->definition->hasArgument($c - 1) && $this->definition->getArgument($c - 1)->isArray()) {
            $arg = $this->definition->getArgument($c - 1);
            $this->arguments[$arg->getName()][] = $token;

        // unexpected argument
        } else {
            $all = $this->definition->getArguments();
            $symfonyCommandName = null;
            if (($inputArgument = $all[$key = array_key_first($all) ?? ''] ?? null) && 'command' === $inputArgument->getName()) {
                $symfonyCommandName = $this->arguments['command'] ?? null;
                unset($all[$key]);
            }

            if (\count($all)) {
                if ($symfonyCommandName) {
                    $message = \sprintf('Too many arguments to "%s" command, expected arguments "%s".', $symfonyCommandName, implode('" "', array_keys($all)));
                } else {
                    $message = \sprintf('Too many arguments, expected arguments "%s".', implode('" "', array_keys($all)));
                }
            } elseif ($symfonyCommandName) {
                $message = \sprintf('No arguments expected for "%s" command, got "%s".', $symfonyCommandName, $token);
            } else {
                $message = \sprintf('No arguments expected, got "%s".', $token);
            }

            throw new RuntimeException($message);
        }
    }

    /**
     * Adds a short option value.
     *
     * @throws RuntimeException When option given doesn't exist
     */
    private function addShortOption(string $shortcut, mixed $value): void
    {
        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.
     *
     * @throws RuntimeException When option given doesn't exist
     */
    private function addLongOption(string $name, mixed $value): void
    {
        if (!$this->definition->hasOption($name)) {
            if (!$this->definition->hasNegation($name)) {
                throw new RuntimeException(\sprintf('The "--%s" option does not exist.', $name));
            }

            $optionName = $this->definition->negationToName($name);
            if (null !== $value) {
                throw new RuntimeException(\sprintf('The "--%s" option does not accept a value.', $name));
            }
            $this->options[$optionName] = false;

            return;
        }

        $option = $this->definition->getOption($name);

        if (null !== $value && !$option->acceptValue()) {
            throw new RuntimeException(\sprintf('The "--%s" option does not accept a value.', $name));
        }

        if (\in_array($value, ['', null], true) && $option->acceptValue() && \count($this->parsed)) {
            // if option accepts an optional or mandatory argument
            // let's see if there is one provided
            $next = array_shift($this->parsed);
            if ((isset($next[0]) && '-' !== $next[0]) || \in_array($next, ['', null], true)) {
                $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));
            }

            if (!$option->isArray() && !$option->isValueOptional()) {
                $value = true;
            }
        }

        if ($option->isArray()) {
            $this->options[$name][] = $value;
        } else {
            $this->options[$name] = $value;
        }
    }

    public function getFirstArgument(): ?string
    {
        $isOption = false;
        foreach ($this->tokens as $i => $token) {
            if ($token && '-' === $token[0]) {
                if (str_contains($token, '=') || !isset($this->tokens[$i + 1])) {
                    continue;
                }

                // If it's a long option, consider that everything after "--" is the option name.
                // Otherwise, use the last char (if it's a short option set, only the last one can take a value with space separator)
                $name = '-' === $token[1] ? substr($token, 2) : substr($token, -1);
                if (!isset($this->options[$name]) && !$this->definition->hasShortcut($name)) {
                    // noop
                } elseif ((isset($this->options[$name]) || isset($this->options[$name = $this->definition->shortcutToName($name)])) && $this->tokens[$i + 1] === $this->options[$name]) {
                    $isOption = true;
                }

                continue;
            }

            if ($isOption) {
                $isOption = false;
                continue;
            }

            return $token;
        }

        return null;
    }

    public function hasParameterOption(string|array $values, bool $onlyParams = false): bool
    {
        $values = (array) $values;

        foreach ($this->tokens as $token) {
            if ($onlyParams && '--' === $token) {
                return false;
            }
            foreach ($values as $value) {
                // Options with values:
                //   For long options, test for '--option=' at beginning
                //   For short options, test for '-o' at beginning
                $leading = str_starts_with($value, '--') ? $value.'=' : $value;
                if ($token === $value || '' !== $leading && str_starts_with($token, $leading)) {
                    return true;
                }
            }
        }

        return false;
    }

    public function getParameterOption(string|array $values, string|bool|int|float|array|null $default = false, bool $onlyParams = false): mixed
    {
        $values = (array) $values;
        $tokens = $this->tokens;

        while (0 < \count($tokens)) {
            $token = array_shift($tokens);
            if ($onlyParams && '--' === $token) {
                return $default;
            }

            foreach ($values as $value) {
                if ($token === $value) {
                    return array_shift($tokens);
                }
                // Options with values:
                //   For long options, test for '--option=' at beginning
                //   For short options, test for '-o' at beginning
                $leading = str_starts_with($value, '--') ? $value.'=' : $value;
                if ('' !== $leading && str_starts_with($token, $leading)) {
                    return substr($token, \strlen($leading));
                }
            }
        }

        return $default;
    }

    /**
     * Returns a stringified representation of the args passed to the command.
     */
    public function __toString(): string
    {
        $tokens = array_map(function ($token) {
            if (preg_match('{^(-[^=]+=)(.+)}', $token, $match)) {
                return $match[1].$this->escapeToken($match[2]);
            }

            if ($token && '-' !== $token[0]) {
                return $this->escapeToken($token);
            }

            return $token;
        }, $this->tokens);

        return implode(' ', $tokens);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Input;

use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\RuntimeException;

/**
 * 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 <fabien@symfony.com>
 */
abstract class Input implements InputInterface, StreamableInputInterface
{
    protected $definition;
    /** @var resource */
    protected $stream;
    protected $options = [];
    protected $arguments = [];
    protected $interactive = true;

    public function __construct(?InputDefinition $definition = null)
    {
        if (null === $definition) {
            $this->definition = new InputDefinition();
        } else {
            $this->bind($definition);
            $this->validate();
        }
    }

    /**
     * @return void
     */
    public function bind(InputDefinition $definition)
    {
        $this->arguments = [];
        $this->options = [];
        $this->definition = $definition;

        $this->parse();
    }

    /**
     * Processes command line arguments.
     *
     * @return void
     */
    abstract protected function parse();

    /**
     * @return void
     */
    public function validate()
    {
        $definition = $this->definition;
        $givenArguments = $this->arguments;

        $missingArguments = array_filter(array_keys($definition->getArguments()), static fn ($argument) => !\array_key_exists($argument, $givenArguments) && $definition->getArgument($argument)->isRequired());

        if (\count($missingArguments) > 0) {
            throw new RuntimeException(\sprintf('Not enough arguments (missing: "%s").', implode(', ', $missingArguments)));
        }
    }

    public function isInteractive(): bool
    {
        return $this->interactive;
    }

    /**
     * @return void
     */
    public function setInteractive(bool $interactive)
    {
        $this->interactive = $interactive;
    }

    public function getArguments(): array
    {
        return array_merge($this->definition->getArgumentDefaults(), $this->arguments);
    }

    public function getArgument(string $name): mixed
    {
        if (!$this->definition->hasArgument($name)) {
            throw new InvalidArgumentException(\sprintf('The "%s" argument does not exist.', $name));
        }

        return $this->arguments[$name] ?? $this->definition->getArgument($name)->getDefault();
    }

    /**
     * @return void
     */
    public function setArgument(string $name, mixed $value)
    {
        if (!$this->definition->hasArgument($name)) {
            throw new InvalidArgumentException(\sprintf('The "%s" argument does not exist.', $name));
        }

        $this->arguments[$name] = $value;
    }

    public function hasArgument(string $name): bool
    {
        return $this->definition->hasArgument($name);
    }

    public function getOptions(): array
    {
        return array_merge($this->definition->getOptionDefaults(), $this->options);
    }

    public function getOption(string $name): mixed
    {
        if ($this->definition->hasNegation($name)) {
            if (null === $value = $this->getOption($this->definition->negationToName($name))) {
                return $value;
            }

            return !$value;
        }

        if (!$this->definition->hasOption($name)) {
            throw new InvalidArgumentException(\sprintf('The "%s" option does not exist.', $name));
        }

        return \array_key_exists($name, $this->options) ? $this->options[$name] : $this->definition->getOption($name)->getDefault();
    }

    /**
     * @return void
     */
    public function setOption(string $name, mixed $value)
    {
        if ($this->definition->hasNegation($name)) {
            $this->options[$this->definition->negationToName($name)] = !$value;

            return;
        } elseif (!$this->definition->hasOption($name)) {
            throw new InvalidArgumentException(\sprintf('The "%s" option does not exist.', $name));
        }

        $this->options[$name] = $value;
    }

    public function hasOption(string $name): bool
    {
        return $this->definition->hasOption($name) || $this->definition->hasNegation($name);
    }

    /**
     * Escapes a token through escapeshellarg if it contains unsafe chars.
     */
    public function escapeToken(string $token): string
    {
        return preg_match('{^[\w-]+$}', $token) ? $token : escapeshellarg($token);
    }

    /**
     * @param resource $stream
     *
     * @return void
     */
    public function setStream($stream)
    {
        $this->stream = $stream;
    }

    /**
     * @return resource
     */
    public function getStream()
    {
        return $this->stream;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console;

use Symfony\Component\Console\Output\AnsiColorMode;

class Terminal
{
    public const DEFAULT_COLOR_MODE = AnsiColorMode::Ansi4;

    private static ?AnsiColorMode $colorMode = null;
    private static ?int $width = null;
    private static ?int $height = null;
    private static ?bool $stty = null;

    /**
     * About Ansi color types: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
     * For more information about true color support with terminals https://github.com/termstandard/colors/.
     */
    public static function getColorMode(): AnsiColorMode
    {
        // Use Cache from previous run (or user forced mode)
        if (null !== self::$colorMode) {
            return self::$colorMode;
        }

        // Try with $COLORTERM first
        if (\is_string($colorterm = getenv('COLORTERM'))) {
            $colorterm = strtolower($colorterm);

            if (str_contains($colorterm, 'truecolor')) {
                self::setColorMode(AnsiColorMode::Ansi24);

                return self::$colorMode;
            }

            if (str_contains($colorterm, '256color')) {
                self::setColorMode(AnsiColorMode::Ansi8);

                return self::$colorMode;
            }
        }

        // Try with $TERM
        if (\is_string($term = getenv('TERM'))) {
            $term = strtolower($term);

            if (str_contains($term, 'truecolor')) {
                self::setColorMode(AnsiColorMode::Ansi24);

                return self::$colorMode;
            }

            if (str_contains($term, '256color')) {
                self::setColorMode(AnsiColorMode::Ansi8);

                return self::$colorMode;
            }
        }

        self::setColorMode(self::DEFAULT_COLOR_MODE);

        return self::$colorMode;
    }

    /**
     * Force a terminal color mode rendering.
     */
    public static function setColorMode(?AnsiColorMode $colorMode): void
    {
        self::$colorMode = $colorMode;
    }

    /**
     * Gets the terminal width.
     */
    public function getWidth(): int
    {
        $width = getenv('COLUMNS');
        if (false !== $width) {
            return (int) trim($width);
        }

        if (null === self::$width) {
            self::initDimensions();
        }

        return self::$width ?: 80;
    }

    /**
     * Gets the terminal height.
     */
    public function getHeight(): int
    {
        $height = getenv('LINES');
        if (false !== $height) {
            return (int) trim($height);
        }

        if (null === self::$height) {
            self::initDimensions();
        }

        return self::$height ?: 50;
    }

    /**
     * @internal
     */
    public static function hasSttyAvailable(): bool
    {
        if (null !== self::$stty) {
            return self::$stty;
        }

        // skip check if shell_exec function is disabled
        if (!\function_exists('shell_exec')) {
            return false;
        }

        return self::$stty = (bool) @shell_exec('stty 2> '.('\\' === \DIRECTORY_SEPARATOR ? 'NUL' : '/dev/null'));
    }

    private static function initDimensions(): void
    {
        if ('\\' === \DIRECTORY_SEPARATOR) {
            $ansicon = getenv('ANSICON');
            if (false !== $ansicon && preg_match('/^(\d+)x(\d+)(?: \((\d+)x(\d+)\))?$/', trim($ansicon), $matches)) {
                // extract [w, H] from "wxh (WxH)"
                // or [w, h] from "wxh"
                self::$width = (int) $matches[1];
                self::$height = isset($matches[4]) ? (int) $matches[4] : (int) $matches[2];
            } elseif (!self::hasVt100Support() && self::hasSttyAvailable()) {
                // only use stty on Windows if the terminal does not support vt100 (e.g. Windows 7 + git-bash)
                // testing for stty in a Windows 10 vt100-enabled console will implicitly disable vt100 support on STDOUT
                self::initDimensionsUsingStty();
            } elseif (null !== $dimensions = self::getConsoleMode()) {
                // extract [w, h] from "wxh"
                self::$width = (int) $dimensions[0];
                self::$height = (int) $dimensions[1];
            }
        } else {
            self::initDimensionsUsingStty();
        }
    }

    /**
     * Returns whether STDOUT has vt100 support (some Windows 10+ configurations).
     */
    private static function hasVt100Support(): bool
    {
        return \function_exists('sapi_windows_vt100_support') && sapi_windows_vt100_support(fopen('php://stdout', 'w'));
    }

    /**
     * Initializes dimensions using the output of an stty columns line.
     */
    private static function initDimensionsUsingStty(): void
    {
        if ($sttyString = self::getSttyColumns()) {
            if (preg_match('/rows.(\d+);.columns.(\d+);/is', $sttyString, $matches)) {
                // extract [w, h] from "rows h; columns w;"
                self::$width = (int) $matches[2];
                self::$height = (int) $matches[1];
            } elseif (preg_match('/;.(\d+).rows;.(\d+).columns/is', $sttyString, $matches)) {
                // extract [w, h] from "; h rows; w columns"
                self::$width = (int) $matches[2];
                self::$height = (int) $matches[1];
            }
        }
    }

    /**
     * Runs and parses mode CON if it's available, suppressing any error output.
     *
     * @return int[]|null An array composed of the width and the height or null if it could not be parsed
     */
    private static function getConsoleMode(): ?array
    {
        $info = self::readFromProcess('mode CON');

        if (null === $info || !preg_match('/--------+\r?\n.+?(\d+)\r?\n.+?(\d+)\r?\n/', $info, $matches)) {
            return null;
        }

        return [(int) $matches[2], (int) $matches[1]];
    }

    /**
     * Runs and parses stty -a if it's available, suppressing any error output.
     */
    private static function getSttyColumns(): ?string
    {
        return self::readFromProcess(['stty', '-a']);
    }

    private static function readFromProcess(string|array $command): ?string
    {
        if (!\function_exists('proc_open')) {
            return null;
        }

        $descriptorspec = [
            1 => ['pipe', 'w'],
            2 => ['pipe', 'w'],
        ];

        $cp = \function_exists('sapi_windows_cp_set') ? sapi_windows_cp_get() : 0;

        if (!$process = @proc_open($command, $descriptorspec, $pipes, null, null, ['suppress_errors' => true])) {
            return null;
        }

        $info = stream_get_contents($pipes[1]);
        fclose($pipes[1]);
        fclose($pipes[2]);
        proc_close($process);

        if ($cp) {
            sapi_windows_cp_set($cp);
        }

        return $info;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\DataCollector;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Debug\CliRequest;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\SignalRegistry\SignalMap;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
use Symfony\Component\VarDumper\Cloner\Data;

/**
 * @internal
 *
 * @author Jules Pietri <jules@heahprod.com>
 */
final class CommandDataCollector extends DataCollector
{
    public function collect(Request $request, Response $response, ?\Throwable $exception = null): void
    {
        if (!$request instanceof CliRequest) {
            return;
        }

        $command = $request->command;
        $application = $command->getApplication();

        $this->data = [
            'command' => $this->cloneVar($command->command),
            'exit_code' => $command->exitCode,
            'interrupted_by_signal' => $command->interruptedBySignal,
            'duration' => $command->duration,
            'max_memory_usage' => $command->maxMemoryUsage,
            'verbosity_level' => match ($command->output->getVerbosity()) {
                OutputInterface::VERBOSITY_QUIET => 'quiet',
                OutputInterface::VERBOSITY_NORMAL => 'normal',
                OutputInterface::VERBOSITY_VERBOSE => 'verbose',
                OutputInterface::VERBOSITY_VERY_VERBOSE => 'very verbose',
                OutputInterface::VERBOSITY_DEBUG => 'debug',
            },
            'interactive' => $command->isInteractive,
            'validate_input' => !$command->ignoreValidation,
            'enabled' => $command->isEnabled(),
            'visible' => !$command->isHidden(),
            'input' => $this->cloneVar($command->input),
            'output' => $this->cloneVar($command->output),
            'interactive_inputs' => array_map($this->cloneVar(...), $command->interactiveInputs),
            'signalable' => $command->getSubscribedSignals(),
            'handled_signals' => $command->handledSignals,
            'helper_set' => array_map($this->cloneVar(...), iterator_to_array($command->getHelperSet())),
        ];

        $baseDefinition = $application->getDefinition();

        foreach ($command->arguments as $argName => $argValue) {
            if ($baseDefinition->hasArgument($argName)) {
                $this->data['application_inputs'][$argName] = $this->cloneVar($argValue);
            } else {
                $this->data['arguments'][$argName] = $this->cloneVar($argValue);
            }
        }

        foreach ($command->options as $optName => $optValue) {
            if ($baseDefinition->hasOption($optName)) {
                $this->data['application_inputs']['--'.$optName] = $this->cloneVar($optValue);
            } else {
                $this->data['options'][$optName] = $this->cloneVar($optValue);
            }
        }
    }

    public function getName(): string
    {
        return 'command';
    }

    /**
     * @return array{
     *     class?: class-string,
     *     executor?: string,
     *     file: string,
     *     line: int,
     * }
     */
    public function getCommand(): array
    {
        $class = $this->data['command']->getType();
        $r = new \ReflectionMethod($class, 'execute');

        if (Command::class !== $r->getDeclaringClass()) {
            return [
                'executor' => $class.'::'.$r->name,
                'file' => $r->getFileName(),
                'line' => $r->getStartLine(),
            ];
        }

        $r = new \ReflectionClass($class);

        return [
            'class' => $class,
            'file' => $r->getFileName(),
            'line' => $r->getStartLine(),
        ];
    }

    public function getInterruptedBySignal(): ?string
    {
        if (isset($this->data['interrupted_by_signal'])) {
            return \sprintf('%s (%d)', SignalMap::getSignalName($this->data['interrupted_by_signal']), $this->data['interrupted_by_signal']);
        }

        return null;
    }

    public function getDuration(): string
    {
        return $this->data['duration'];
    }

    public function getMaxMemoryUsage(): string
    {
        return $this->data['max_memory_usage'];
    }

    public function getVerbosityLevel(): string
    {
        return $this->data['verbosity_level'];
    }

    public function getInteractive(): bool
    {
        return $this->data['interactive'];
    }

    public function getValidateInput(): bool
    {
        return $this->data['validate_input'];
    }

    public function getEnabled(): bool
    {
        return $this->data['enabled'];
    }

    public function getVisible(): bool
    {
        return $this->data['visible'];
    }

    public function getInput(): Data
    {
        return $this->data['input'];
    }

    public function getOutput(): Data
    {
        return $this->data['output'];
    }

    /**
     * @return Data[]
     */
    public function getArguments(): array
    {
        return $this->data['arguments'] ?? [];
    }

    /**
     * @return Data[]
     */
    public function getOptions(): array
    {
        return $this->data['options'] ?? [];
    }

    /**
     * @return Data[]
     */
    public function getApplicationInputs(): array
    {
        return $this->data['application_inputs'] ?? [];
    }

    /**
     * @return Data[]
     */
    public function getInteractiveInputs(): array
    {
        return $this->data['interactive_inputs'] ?? [];
    }

    public function getSignalable(): array
    {
        return array_map(
            static fn (int $signal): string => \sprintf('%s (%d)', SignalMap::getSignalName($signal), $signal),
            $this->data['signalable']
        );
    }

    public function getHandledSignals(): array
    {
        $keys = array_map(
            static fn (int $signal): string => \sprintf('%s (%d)', SignalMap::getSignalName($signal), $signal),
            array_keys($this->data['handled_signals'])
        );

        return array_combine($keys, array_values($this->data['handled_signals']));
    }

    /**
     * @return Data[]
     */
    public function getHelperSet(): array
    {
        return $this->data['helper_set'] ?? [];
    }

    public function reset(): void
    {
        $this->data = [];
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\CommandLoader;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\CommandNotFoundException;

/**
 * A simple command loader using factories to instantiate commands lazily.
 *
 * @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
 */
class FactoryCommandLoader implements CommandLoaderInterface
{
    private array $factories;

    /**
     * @param callable[] $factories Indexed by command names
     */
    public function __construct(array $factories)
    {
        $this->factories = $factories;
    }

    public function has(string $name): bool
    {
        return isset($this->factories[$name]);
    }

    public function get(string $name): Command
    {
        if (!isset($this->factories[$name])) {
            throw new CommandNotFoundException(\sprintf('Command "%s" does not exist.', $name));
        }

        $factory = $this->factories[$name];

        return $factory();
    }

    public function getNames(): array
    {
        return array_keys($this->factories);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\CommandLoader;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\CommandNotFoundException;

/**
 * @author Robin Chalas <robin.chalas@gmail.com>
 */
interface CommandLoaderInterface
{
    /**
     * Loads a command.
     *
     * @throws CommandNotFoundException
     */
    public function get(string $name): Command;

    /**
     * Checks if a command exists.
     */
    public function has(string $name): bool;

    /**
     * @return string[]
     */
    public function getNames(): array;
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\CommandLoader;

use Psr\Container\ContainerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\CommandNotFoundException;

/**
 * Loads commands from a PSR-11 container.
 *
 * @author Robin Chalas <robin.chalas@gmail.com>
 */
class ContainerCommandLoader implements CommandLoaderInterface
{
    private ContainerInterface $container;
    private array $commandMap;

    /**
     * @param array $commandMap An array with command names as keys and service ids as values
     */
    public function __construct(ContainerInterface $container, array $commandMap)
    {
        $this->container = $container;
        $this->commandMap = $commandMap;
    }

    public function get(string $name): Command
    {
        if (!$this->has($name)) {
            throw new CommandNotFoundException(\sprintf('Command "%s" does not exist.', $name));
        }

        return $this->container->get($this->commandMap[$name]);
    }

    public function has(string $name): bool
    {
        return isset($this->commandMap[$name]) && $this->container->has($this->commandMap[$name]);
    }

    public function getNames(): array
    {
        return array_keys($this->commandMap);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\CI;

use Symfony\Component\Console\Output\OutputInterface;

/**
 * Utility class for Github actions.
 *
 * @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
 */
class GithubActionReporter
{
    private OutputInterface $output;

    /**
     * @see https://github.com/actions/toolkit/blob/5e5e1b7aacba68a53836a34db4a288c3c1c1585b/packages/core/src/command.ts#L80-L85
     */
    private const ESCAPED_DATA = [
        '%' => '%25',
        "\r" => '%0D',
        "\n" => '%0A',
    ];

    /**
     * @see https://github.com/actions/toolkit/blob/5e5e1b7aacba68a53836a34db4a288c3c1c1585b/packages/core/src/command.ts#L87-L94
     */
    private const ESCAPED_PROPERTIES = [
        '%' => '%25',
        "\r" => '%0D',
        "\n" => '%0A',
        ':' => '%3A',
        ',' => '%2C',
    ];

    public function __construct(OutputInterface $output)
    {
        $this->output = $output;
    }

    public static function isGithubActionEnvironment(): bool
    {
        return false !== getenv('GITHUB_ACTIONS');
    }

    /**
     * Output an error using the Github annotations format.
     *
     * @see https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-an-error-message
     */
    public function error(string $message, ?string $file = null, ?int $line = null, ?int $col = null): void
    {
        $this->log('error', $message, $file, $line, $col);
    }

    /**
     * Output a warning using the Github annotations format.
     *
     * @see https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-a-warning-message
     */
    public function warning(string $message, ?string $file = null, ?int $line = null, ?int $col = null): void
    {
        $this->log('warning', $message, $file, $line, $col);
    }

    /**
     * Output a debug log using the Github annotations format.
     *
     * @see https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-a-debug-message
     */
    public function debug(string $message, ?string $file = null, ?int $line = null, ?int $col = null): void
    {
        $this->log('debug', $message, $file, $line, $col);
    }

    private function log(string $type, string $message, ?string $file = null, ?int $line = null, ?int $col = null): void
    {
        // Some values must be encoded.
        $message = strtr($message, self::ESCAPED_DATA);

        if (!$file) {
            // No file provided, output the message solely:
            $this->output->writeln(\sprintf('::%s::%s', $type, $message));

            return;
        }

        $this->output->writeln(\sprintf('::%s file=%s,line=%s,col=%s::%s', $type, strtr($file, self::ESCAPED_PROPERTIES), strtr($line ?? 1, self::ESCAPED_PROPERTIES), strtr($col ?? 0, self::ESCAPED_PROPERTIES), $message));
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\EventListener;

use Psr\Log\LoggerInterface;
use Symfony\Component\Console\ConsoleEvents;
use Symfony\Component\Console\Event\ConsoleErrorEvent;
use Symfony\Component\Console\Event\ConsoleEvent;
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
 * @author James Halsall <james.t.halsall@googlemail.com>
 * @author Robin Chalas <robin.chalas@gmail.com>
 */
class ErrorListener implements EventSubscriberInterface
{
    private ?LoggerInterface $logger;

    public function __construct(?LoggerInterface $logger = null)
    {
        $this->logger = $logger;
    }

    /**
     * @return void
     */
    public function onConsoleError(ConsoleErrorEvent $event)
    {
        if (null === $this->logger) {
            return;
        }

        $error = $event->getError();

        if (!$inputString = $this->getInputString($event)) {
            $this->logger->critical('An error occurred while using the console. Message: "{message}"', ['exception' => $error, 'message' => $error->getMessage()]);

            return;
        }

        $this->logger->critical('Error thrown while running command "{command}". Message: "{message}"', ['exception' => $error, 'command' => $inputString, 'message' => $error->getMessage()]);
    }

    /**
     * @return void
     */
    public function onConsoleTerminate(ConsoleTerminateEvent $event)
    {
        if (null === $this->logger) {
            return;
        }

        $exitCode = $event->getExitCode();

        if (0 === $exitCode) {
            return;
        }

        if (!$inputString = $this->getInputString($event)) {
            $this->logger->debug('The console exited with code "{code}"', ['code' => $exitCode]);

            return;
        }

        $this->logger->debug('Command "{command}" exited with code "{code}"', ['command' => $inputString, 'code' => $exitCode]);
    }

    public static function getSubscribedEvents(): array
    {
        return [
            ConsoleEvents::ERROR => ['onConsoleError', -128],
            ConsoleEvents::TERMINATE => ['onConsoleTerminate', -128],
        ];
    }

    private static function getInputString(ConsoleEvent $event): ?string
    {
        $commandName = $event->getCommand()?->getName();
        $input = $event->getInput();

        if ($input instanceof \Stringable) {
            if ($commandName) {
                return str_replace(["'$commandName'", "\"$commandName\""], $commandName, (string) $input);
            }

            return (string) $input;
        }

        return $commandName;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\SignalRegistry;

final class SignalRegistry
{
    /**
     * @var array<int, array<callable>>
     */
    private array $signalHandlers = [];

    /**
     * @var array<array<int, array<callable>>>
     */
    private array $stack = [];

    /**
     * @var array<int, callable|int|string>
     */
    private array $originalHandlers = [];

    public function __construct()
    {
        if (\function_exists('pcntl_async_signals')) {
            pcntl_async_signals(true);
        }
    }

    public function register(int $signal, callable $signalHandler): void
    {
        $previous = pcntl_signal_get_handler($signal);

        if (!isset($this->originalHandlers[$signal])) {
            $this->originalHandlers[$signal] = $previous;
        }

        if (!isset($this->signalHandlers[$signal])) {
            if (\is_callable($previous) && [$this, 'handle'] !== $previous) {
                $this->signalHandlers[$signal][] = $previous;
            }
        }

        $this->signalHandlers[$signal][] = $signalHandler;

        pcntl_signal($signal, [$this, 'handle']);
    }

    public static function isSupported(): bool
    {
        return \function_exists('pcntl_signal');
    }

    /**
     * @internal
     */
    public function handle(int $signal): void
    {
        $count = \count($this->signalHandlers[$signal]);

        foreach ($this->signalHandlers[$signal] as $i => $signalHandler) {
            $hasNext = $i !== $count - 1;
            $signalHandler($signal, $hasNext);
        }
    }

    /**
     * Pushes the current active handlers onto the stack and clears the active list.
     *
     * This prepares the registry for a new set of handlers within a specific scope.
     *
     * @internal
     */
    public function pushCurrentHandlers(): void
    {
        // Restore the original OS-level disposition while the active map is empty,
        // so signals are not routed through handle() without a registered handler.
        foreach ($this->signalHandlers as $signal => $handlers) {
            if (isset($this->originalHandlers[$signal])) {
                pcntl_signal($signal, $this->originalHandlers[$signal]);
            }
        }

        $this->stack[] = $this->signalHandlers;
        $this->signalHandlers = [];
    }

    /**
     * Restores the previous handlers from the stack, making them active.
     *
     * This also restores the original OS-level signal handler if no
     * more handlers are registered for a signal that was just popped.
     *
     * @internal
     */
    public function popPreviousHandlers(): void
    {
        $popped = $this->signalHandlers;
        $previous = array_pop($this->stack) ?? [];

        // Expose a transitional superset so handle() never reads a missing key
        // if a signal lands while the OS-level handlers below are being swapped.
        $this->signalHandlers = $previous + $popped;

        // Reinstall the registry handler for signals owned by the restored scope.
        foreach ($previous as $signal => $handlers) {
            pcntl_signal($signal, [$this, 'handle']);
        }

        // Restore the original OS-level handler for signals no scope owns anymore.
        foreach ($popped as $signal => $handlers) {
            if (!isset($previous[$signal]) && isset($this->originalHandlers[$signal])) {
                pcntl_signal($signal, $this->originalHandlers[$signal]);
            }
        }

        $this->signalHandlers = $previous;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\SignalRegistry;

/**
 * @author Grégoire Pineau <lyrixx@lyrixx.info>
 */
class SignalMap
{
    private static array $map;

    public static function getSignalName(int $signal): ?string
    {
        if (!\extension_loaded('pcntl')) {
            return null;
        }

        if (!isset(self::$map)) {
            $r = new \ReflectionExtension('pcntl');
            $c = $r->getConstants();
            $map = array_filter($c, static fn ($k) => str_starts_with($k, 'SIG') && !str_starts_with($k, 'SIG_') && 'SIGBABY' !== $k, \ARRAY_FILTER_USE_KEY);
            self::$map = array_flip($map);
        }

        return self::$map[$signal] ?? null;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Style;

use Symfony\Component\Console\Formatter\OutputFormatterInterface;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;

/**
 * Decorates output to add console style guide helpers.
 *
 * @author Kevin Bond <kevinbond@gmail.com>
 */
abstract class OutputStyle implements OutputInterface, StyleInterface
{
    private OutputInterface $output;

    public function __construct(OutputInterface $output)
    {
        $this->output = $output;
    }

    /**
     * @return void
     */
    public function newLine(int $count = 1)
    {
        $this->output->write(str_repeat(\PHP_EOL, $count));
    }

    public function createProgressBar(int $max = 0): ProgressBar
    {
        return new ProgressBar($this->output, $max);
    }

    /**
     * @return void
     */
    public function write(string|iterable $messages, bool $newline = false, int $type = self::OUTPUT_NORMAL)
    {
        $this->output->write($messages, $newline, $type);
    }

    /**
     * @return void
     */
    public function writeln(string|iterable $messages, int $type = self::OUTPUT_NORMAL)
    {
        $this->output->writeln($messages, $type);
    }

    /**
     * @return void
     */
    public function setVerbosity(int $level)
    {
        $this->output->setVerbosity($level);
    }

    public function getVerbosity(): int
    {
        return $this->output->getVerbosity();
    }

    /**
     * @return void
     */
    public function setDecorated(bool $decorated)
    {
        $this->output->setDecorated($decorated);
    }

    public function isDecorated(): bool
    {
        return $this->output->isDecorated();
    }

    /**
     * @return void
     */
    public function setFormatter(OutputFormatterInterface $formatter)
    {
        $this->output->setFormatter($formatter);
    }

    public function getFormatter(): OutputFormatterInterface
    {
        return $this->output->getFormatter();
    }

    public function isQuiet(): bool
    {
        return $this->output->isQuiet();
    }

    public function isVerbose(): bool
    {
        return $this->output->isVerbose();
    }

    public function isVeryVerbose(): bool
    {
        return $this->output->isVeryVerbose();
    }

    public function isDebug(): bool
    {
        return $this->output->isDebug();
    }

    /**
     * @return OutputInterface
     */
    protected function getErrorOutput()
    {
        if (!$this->output instanceof ConsoleOutputInterface) {
            return $this->output;
        }

        return $this->output->getErrorOutput();
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Style;

use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Helper\Helper;
use Symfony\Component\Console\Helper\OutputWrapper;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Helper\SymfonyQuestionHelper;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Helper\TableCell;
use Symfony\Component\Console\Helper\TableSeparator;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\ConsoleSectionOutput;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Output\TrimmedBufferOutput;
use Symfony\Component\Console\Question\ChoiceQuestion;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Terminal;

/**
 * Output decorator helpers for the Symfony Style Guide.
 *
 * @author Kevin Bond <kevinbond@gmail.com>
 */
class SymfonyStyle extends OutputStyle
{
    public const MAX_LINE_LENGTH = 120;

    private InputInterface $input;
    private OutputInterface $output;
    private SymfonyQuestionHelper $questionHelper;
    private ProgressBar $progressBar;
    private int $lineLength;
    private TrimmedBufferOutput $bufferedOutput;

    public function __construct(InputInterface $input, OutputInterface $output)
    {
        $this->input = $input;
        $this->bufferedOutput = new TrimmedBufferOutput(\DIRECTORY_SEPARATOR === '\\' ? 4 : 2, $output->getVerbosity(), false, clone $output->getFormatter());
        // Windows cmd wraps lines as soon as the terminal width is reached, whether there are following chars or not.
        $width = (new Terminal())->getWidth() ?: self::MAX_LINE_LENGTH;
        $this->lineLength = min($width - (int) (\DIRECTORY_SEPARATOR === '\\'), self::MAX_LINE_LENGTH);

        parent::__construct($this->output = $output);
    }

    /**
     * Formats a message as a block of text.
     *
     * @return void
     */
    public function block(string|array $messages, ?string $type = null, ?string $style = null, string $prefix = ' ', bool $padding = false, bool $escape = true)
    {
        $messages = \is_array($messages) ? array_values($messages) : [$messages];

        $this->autoPrependBlock();
        $this->writeln($this->createBlock($messages, $type, $style, $prefix, $padding, $escape));
        $this->newLine();
    }

    /**
     * @return void
     */
    public function title(string $message)
    {
        $this->autoPrependBlock();
        $this->writeln([
            \sprintf('<comment>%s</>', OutputFormatter::escapeTrailingBackslash($message)),
            \sprintf('<comment>%s</>', str_repeat('=', Helper::width(Helper::removeDecoration($this->getFormatter(), $message)))),
        ]);
        $this->newLine();
    }

    /**
     * @return void
     */
    public function section(string $message)
    {
        $this->autoPrependBlock();
        $this->writeln([
            \sprintf('<comment>%s</>', OutputFormatter::escapeTrailingBackslash($message)),
            \sprintf('<comment>%s</>', str_repeat('-', Helper::width(Helper::removeDecoration($this->getFormatter(), $message)))),
        ]);
        $this->newLine();
    }

    /**
     * @return void
     */
    public function listing(array $elements)
    {
        $this->autoPrependText();
        $elements = array_map(static fn ($element) => \sprintf(' * %s', $element), $elements);

        $this->writeln($elements);
        $this->newLine();
    }

    /**
     * @return void
     */
    public function text(string|array $message)
    {
        $this->autoPrependText();

        $messages = \is_array($message) ? array_values($message) : [$message];
        foreach ($messages as $message) {
            $this->writeln(\sprintf(' %s', $message));
        }
    }

    /**
     * Formats a command comment.
     *
     * @return void
     */
    public function comment(string|array $message)
    {
        $this->block($message, null, null, '<fg=default;bg=default> // </>', false, false);
    }

    /**
     * @return void
     */
    public function success(string|array $message)
    {
        $this->block($message, 'OK', 'fg=black;bg=green', ' ', true);
    }

    /**
     * @return void
     */
    public function error(string|array $message)
    {
        $this->block($message, 'ERROR', 'fg=white;bg=red', ' ', true);
    }

    /**
     * @return void
     */
    public function warning(string|array $message)
    {
        $this->block($message, 'WARNING', 'fg=black;bg=yellow', ' ', true);
    }

    /**
     * @return void
     */
    public function note(string|array $message)
    {
        $this->block($message, 'NOTE', 'fg=yellow', ' ! ');
    }

    /**
     * Formats an info message.
     *
     * @return void
     */
    public function info(string|array $message)
    {
        $this->block($message, 'INFO', 'fg=green', ' ', true);
    }

    /**
     * @return void
     */
    public function caution(string|array $message)
    {
        $this->block($message, 'CAUTION', 'fg=white;bg=red', ' ! ', true);
    }

    /**
     * @return void
     */
    public function table(array $headers, array $rows)
    {
        $this->createTable()
            ->setHeaders($headers)
            ->setRows($rows)
            ->render()
        ;

        $this->newLine();
    }

    /**
     * Formats a horizontal table.
     *
     * @return void
     */
    public function horizontalTable(array $headers, array $rows)
    {
        $this->createTable()
            ->setHorizontal(true)
            ->setHeaders($headers)
            ->setRows($rows)
            ->render()
        ;

        $this->newLine();
    }

    /**
     * Formats a list of key/value horizontally.
     *
     * Each row can be one of:
     * * 'A title'
     * * ['key' => 'value']
     * * new TableSeparator()
     *
     * @return void
     */
    public function definitionList(string|array|TableSeparator ...$list)
    {
        $headers = [];
        $row = [];
        foreach ($list as $value) {
            if ($value instanceof TableSeparator) {
                $headers[] = $value;
                $row[] = $value;
                continue;
            }
            if (\is_string($value)) {
                $headers[] = new TableCell($value, ['colspan' => 2]);
                $row[] = null;
                continue;
            }
            if (!\is_array($value)) {
                throw new InvalidArgumentException('Value should be an array, string, or an instance of TableSeparator.');
            }
            $headers[] = key($value);
            $row[] = current($value);
        }

        $this->horizontalTable($headers, [$row]);
    }

    public function ask(string $question, ?string $default = null, ?callable $validator = null): mixed
    {
        $question = new Question($question, $default);
        $question->setValidator($validator);

        return $this->askQuestion($question);
    }

    public function askHidden(string $question, ?callable $validator = null): mixed
    {
        $question = new Question($question);

        $question->setHidden(true);
        $question->setValidator($validator);

        return $this->askQuestion($question);
    }

    public function confirm(string $question, bool $default = true): bool
    {
        return $this->askQuestion(new ConfirmationQuestion($question, $default));
    }

    public function choice(string $question, array $choices, mixed $default = null, bool $multiSelect = false): mixed
    {
        if (null !== $default) {
            $values = array_flip($choices);
            $default = $values[$default] ?? $default;
        }

        $questionChoice = new ChoiceQuestion($question, $choices, $default);
        $questionChoice->setMultiselect($multiSelect);

        return $this->askQuestion($questionChoice);
    }

    /**
     * @return void
     */
    public function progressStart(int $max = 0)
    {
        $this->progressBar = $this->createProgressBar($max);
        $this->progressBar->start();
    }

    /**
     * @return void
     */
    public function progressAdvance(int $step = 1)
    {
        $this->getProgressBar()->advance($step);
    }

    /**
     * @return void
     */
    public function progressFinish()
    {
        $this->getProgressBar()->finish();
        $this->newLine(2);
        unset($this->progressBar);
    }

    public function createProgressBar(int $max = 0): ProgressBar
    {
        $progressBar = parent::createProgressBar($max);

        if ('\\' !== \DIRECTORY_SEPARATOR || 'Hyper' === getenv('TERM_PROGRAM')) {
            $progressBar->setEmptyBarCharacter('░'); // light shade character \u2591
            $progressBar->setProgressCharacter('');
            $progressBar->setBarCharacter('▓'); // dark shade character \u2593
        }

        return $progressBar;
    }

    /**
     * @see ProgressBar::iterate()
     *
     * @template TKey
     * @template TValue
     *
     * @param iterable<TKey, TValue> $iterable
     * @param int|null               $max      Number of steps to complete the bar (0 if indeterminate), if null it will be inferred from $iterable
     *
     * @return iterable<TKey, TValue>
     */
    public function progressIterate(iterable $iterable, ?int $max = null): iterable
    {
        yield from $this->createProgressBar()->iterate($iterable, $max);

        $this->newLine(2);
    }

    public function askQuestion(Question $question): mixed
    {
        if ($this->input->isInteractive()) {
            $this->autoPrependBlock();
        }

        $this->questionHelper ??= new SymfonyQuestionHelper();

        $answer = $this->questionHelper->ask($this->input, $this, $question);

        if ($this->input->isInteractive()) {
            if ($this->output instanceof ConsoleSectionOutput) {
                // add the new line of the `return` to submit the input to ConsoleSectionOutput, because ConsoleSectionOutput is holding all it's lines.
                // this is relevant when a `ConsoleSectionOutput::clear` is called.
                // the section already renders the prompt as a whole line, so an extra
                // newLine() here would leave a doubled blank line below the answer; only
                // keep the buffer in sync so autoPrependBlock() spaces the next block.
                $this->output->addNewLineOfInputSubmit();
                $this->bufferedOutput->write("\n\n");
            } else {
                $this->newLine();
                $this->bufferedOutput->write("\n");
            }
        }

        return $answer;
    }

    /**
     * @return void
     */
    public function writeln(string|iterable $messages, int $type = self::OUTPUT_NORMAL)
    {
        if (!is_iterable($messages)) {
            $messages = [$messages];
        }

        foreach ($messages as $message) {
            parent::writeln($message, $type);
            $this->writeBuffer($message, true, $type);
        }
    }

    /**
     * @return void
     */
    public function write(string|iterable $messages, bool $newline = false, int $type = self::OUTPUT_NORMAL)
    {
        if (!is_iterable($messages)) {
            $messages = [$messages];
        }

        foreach ($messages as $message) {
            parent::write($message, $newline, $type);
            $this->writeBuffer($message, $newline, $type);
        }
    }

    /**
     * @return void
     */
    public function newLine(int $count = 1)
    {
        parent::newLine($count);
        $this->bufferedOutput->write(str_repeat("\n", $count));
    }

    /**
     * Returns a new instance which makes use of stderr if available.
     */
    public function getErrorStyle(): self
    {
        return new self($this->input, $this->getErrorOutput());
    }

    public function createTable(): Table
    {
        $output = $this->output instanceof ConsoleOutputInterface ? $this->output->section() : $this->output;
        $style = clone Table::getStyleDefinition('symfony-style-guide');
        $style->setCellHeaderFormat('<info>%s</info>');

        return (new Table($output))->setStyle($style);
    }

    private function getProgressBar(): ProgressBar
    {
        return $this->progressBar
            ?? throw new RuntimeException('The ProgressBar is not started.');
    }

    private function autoPrependBlock(): void
    {
        $chars = substr(str_replace(\PHP_EOL, "\n", $this->bufferedOutput->fetch()), -2);

        if (!isset($chars[0])) {
            $this->newLine(); // empty history, so we should start with a new line.

            return;
        }
        // Prepend new line for each non LF chars (This means no blank line was output before)
        $this->newLine(2 - substr_count($chars, "\n"));
    }

    private function autoPrependText(): void
    {
        $fetched = $this->bufferedOutput->fetch();
        // Prepend new line if last char isn't EOL:
        if ($fetched && !str_ends_with($fetched, "\n")) {
            $this->newLine();
        }
    }

    private function writeBuffer(string $message, bool $newLine, int $type): void
    {
        // We need to know if the last chars are PHP_EOL
        $this->bufferedOutput->write($message, $newLine, $type);
    }

    private function createBlock(iterable $messages, ?string $type = null, ?string $style = null, string $prefix = ' ', bool $padding = false, bool $escape = false): array
    {
        $indentLength = 0;
        $prefixLength = Helper::width(Helper::removeDecoration($this->getFormatter(), $prefix));
        $lines = [];

        if (null !== $type) {
            $type = \sprintf('[%s] ', $type);
            $indentLength = Helper::width($type);
            $lineIndentation = str_repeat(' ', $indentLength);
        }

        // wrap and add newlines for each element
        $outputWrapper = new OutputWrapper();
        foreach ($messages as $key => $message) {
            if ($escape) {
                $message = OutputFormatter::escape($message);
            }

            $message = str_replace("\r\n", "\n", $message);

            $lines = array_merge(
                $lines,
                explode("\n", $outputWrapper->wrap(
                    $message,
                    $this->lineLength - $prefixLength - $indentLength,
                    "\n"
                ))
            );

            if (\count($messages) > 1 && $key < \count($messages) - 1) {
                $lines[] = '';
            }
        }

        $firstLineIndex = 0;
        if ($padding && $this->isDecorated()) {
            $firstLineIndex = 1;
            array_unshift($lines, '');
            $lines[] = '';
        }

        foreach ($lines as $i => &$line) {
            if (null !== $type) {
                $line = $firstLineIndex === $i ? $type.$line : $lineIndentation.$line;
            }

            $line = $prefix.$line;
            $line .= str_repeat(' ', max($this->lineLength - Helper::width(Helper::removeDecoration($this->getFormatter(), $line)), 0));

            if ($style) {
                $line = \sprintf('<%s>%s</>', $style, $line);
            }
        }

        return $lines;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Style;

/**
 * Output style helpers.
 *
 * @author Kevin Bond <kevinbond@gmail.com>
 */
interface StyleInterface
{
    /**
     * Formats a command title.
     *
     * @return void
     */
    public function title(string $message);

    /**
     * Formats a section title.
     *
     * @return void
     */
    public function section(string $message);

    /**
     * Formats a list.
     *
     * @return void
     */
    public function listing(array $elements);

    /**
     * Formats informational text.
     *
     * @return void
     */
    public function text(string|array $message);

    /**
     * Formats a success result bar.
     *
     * @return void
     */
    public function success(string|array $message);

    /**
     * Formats an error result bar.
     *
     * @return void
     */
    public function error(string|array $message);

    /**
     * Formats an warning result bar.
     *
     * @return void
     */
    public function warning(string|array $message);

    /**
     * Formats a note admonition.
     *
     * @return void
     */
    public function note(string|array $message);

    /**
     * Formats a caution admonition.
     *
     * @return void
     */
    public function caution(string|array $message);

    /**
     * Formats a table.
     *
     * @return void
     */
    public function table(array $headers, array $rows);

    /**
     * Asks a question.
     */
    public function ask(string $question, ?string $default = null, ?callable $validator = null): mixed;

    /**
     * Asks a question with the user input hidden.
     */
    public function askHidden(string $question, ?callable $validator = null): mixed;

    /**
     * Asks for confirmation.
     */
    public function confirm(string $question, bool $default = true): bool;

    /**
     * Asks a choice question.
     */
    public function choice(string $question, array $choices, mixed $default = null): mixed;

    /**
     * Add newline(s).
     *
     * @return void
     */
    public function newLine(int $count = 1);

    /**
     * Starts the progress output.
     *
     * @return void
     */
    public function progressStart(int $max = 0);

    /**
     * Advances the progress output X steps.
     *
     * @return void
     */
    public function progressAdvance(int $step = 1);

    /**
     * Finishes the progress output.
     *
     * @return void
     */
    public function progressFinish();
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Question;

/**
 * Represents a yes/no question.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class ConfirmationQuestion extends Question
{
    private string $trueAnswerRegex;

    /**
     * @param string $question        The question to ask to the user
     * @param bool   $default         The default answer to return, true or false
     * @param string $trueAnswerRegex A regex to match the "yes" answer
     */
    public function __construct(string $question, bool $default = true, string $trueAnswerRegex = '/^y/i')
    {
        parent::__construct($question, $default);

        $this->trueAnswerRegex = $trueAnswerRegex;
        $this->setNormalizer($this->getDefaultNormalizer());
    }

    /**
     * Returns the default answer normalizer.
     */
    private function getDefaultNormalizer(): callable
    {
        $default = $this->getDefault();
        $regex = $this->trueAnswerRegex;

        return static function ($answer) use ($default, $regex) {
            if (\is_bool($answer)) {
                return $answer;
            }

            $answerIsTrue = (bool) preg_match($regex, $answer);
            if (false === $default) {
                return $answer && $answerIsTrue;
            }

            return '' === $answer || $answerIsTrue;
        };
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Question;

use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\LogicException;

/**
 * Represents a Question.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class Question
{
    private string $question;
    private ?int $attempts = null;
    private bool $hidden = false;
    private bool $hiddenFallback = true;
    private ?\Closure $autocompleterCallback = null;
    private ?\Closure $validator = null;
    private string|int|bool|float|null $default;
    private ?\Closure $normalizer = null;
    private bool $trimmable = true;
    private bool $multiline = false;

    /**
     * @param string                     $question The question to ask to the user
     * @param string|bool|int|float|null $default  The default answer to return if the user enters nothing
     */
    public function __construct(string $question, string|bool|int|float|null $default = null)
    {
        $this->question = $question;
        $this->default = $default;
    }

    /**
     * Returns the question.
     */
    public function getQuestion(): string
    {
        return $this->question;
    }

    /**
     * Returns the default answer.
     */
    public function getDefault(): string|bool|int|float|null
    {
        return $this->default;
    }

    /**
     * Returns whether the user response accepts newline characters.
     */
    public function isMultiline(): bool
    {
        return $this->multiline;
    }

    /**
     * Sets whether the user response should accept newline characters.
     *
     * @return $this
     */
    public function setMultiline(bool $multiline): static
    {
        $this->multiline = $multiline;

        return $this;
    }

    /**
     * Returns whether the user response must be hidden.
     */
    public function isHidden(): bool
    {
        return $this->hidden;
    }

    /**
     * Sets whether the user response must be hidden or not.
     *
     * @return $this
     *
     * @throws LogicException In case the autocompleter is also used
     */
    public function setHidden(bool $hidden): static
    {
        if ($this->autocompleterCallback) {
            throw new LogicException('A hidden question cannot use the autocompleter.');
        }

        $this->hidden = $hidden;

        return $this;
    }

    /**
     * In case the response cannot be hidden, whether to fallback on non-hidden question or not.
     */
    public function isHiddenFallback(): bool
    {
        return $this->hiddenFallback;
    }

    /**
     * Sets whether to fallback on non-hidden question if the response cannot be hidden.
     *
     * @return $this
     */
    public function setHiddenFallback(bool $fallback): static
    {
        $this->hiddenFallback = $fallback;

        return $this;
    }

    /**
     * Gets values for the autocompleter.
     */
    public function getAutocompleterValues(): ?iterable
    {
        $callback = $this->getAutocompleterCallback();

        return $callback ? $callback('') : null;
    }

    /**
     * Sets values for the autocompleter.
     *
     * @return $this
     *
     * @throws LogicException
     */
    public function setAutocompleterValues(?iterable $values): static
    {
        if (\is_array($values)) {
            $values = $this->isAssoc($values) ? array_merge(array_keys($values), array_values($values)) : array_values($values);

            $callback = static fn () => $values;
        } elseif ($values instanceof \Traversable) {
            $callback = static function () use ($values) {
                static $valueCache;

                return $valueCache ??= iterator_to_array($values, false);
            };
        } else {
            $callback = null;
        }

        return $this->setAutocompleterCallback($callback);
    }

    /**
     * Gets the callback function used for the autocompleter.
     */
    public function getAutocompleterCallback(): ?callable
    {
        return $this->autocompleterCallback;
    }

    /**
     * Sets the callback function used for the autocompleter.
     *
     * The callback is passed the user input as argument and should return an iterable of corresponding suggestions.
     *
     * @return $this
     */
    public function setAutocompleterCallback(?callable $callback = null): static
    {
        if (1 > \func_num_args()) {
            trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__);
        }
        if ($this->hidden && null !== $callback) {
            throw new LogicException('A hidden question cannot use the autocompleter.');
        }

        $this->autocompleterCallback = null === $callback ? null : $callback(...);

        return $this;
    }

    /**
     * Sets a validator for the question.
     *
     * @return $this
     */
    public function setValidator(?callable $validator = null): static
    {
        if (1 > \func_num_args()) {
            trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__);
        }
        $this->validator = null === $validator ? null : $validator(...);

        return $this;
    }

    /**
     * Gets the validator for the question.
     */
    public function getValidator(): ?callable
    {
        return $this->validator;
    }

    /**
     * Sets the maximum number of attempts.
     *
     * Null means an unlimited number of attempts.
     *
     * @return $this
     *
     * @throws InvalidArgumentException in case the number of attempts is invalid
     */
    public function setMaxAttempts(?int $attempts): static
    {
        if (null !== $attempts && $attempts < 1) {
            throw new InvalidArgumentException('Maximum number of attempts must be a positive value.');
        }

        $this->attempts = $attempts;

        return $this;
    }

    /**
     * Gets the maximum number of attempts.
     *
     * Null means an unlimited number of attempts.
     */
    public function getMaxAttempts(): ?int
    {
        return $this->attempts;
    }

    /**
     * Sets a normalizer for the response.
     *
     * The normalizer can be a callable (a string), a closure or a class implementing __invoke.
     *
     * @return $this
     */
    public function setNormalizer(callable $normalizer): static
    {
        $this->normalizer = $normalizer(...);

        return $this;
    }

    /**
     * Gets the normalizer for the response.
     *
     * The normalizer can ba a callable (a string), a closure or a class implementing __invoke.
     */
    public function getNormalizer(): ?callable
    {
        return $this->normalizer;
    }

    /**
     * @return bool
     */
    protected function isAssoc(array $array)
    {
        return (bool) \count(array_filter(array_keys($array), 'is_string'));
    }

    public function isTrimmable(): bool
    {
        return $this->trimmable;
    }

    /**
     * @return $this
     */
    public function setTrimmable(bool $trimmable): static
    {
        $this->trimmable = $trimmable;

        return $this;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Question;

use Symfony\Component\Console\Exception\InvalidArgumentException;

/**
 * Represents a choice question.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class ChoiceQuestion extends Question
{
    private array $choices;
    private bool $multiselect = false;
    private string $prompt = ' > ';
    private string $errorMessage = 'Value "%s" is invalid';

    /**
     * @param string                                   $question The question to ask to the user
     * @param array<string|bool|int|float|\Stringable> $choices  The list of available choices
     * @param string|bool|int|float|null               $default  The default answer to return
     */
    public function __construct(string $question, array $choices, string|bool|int|float|null $default = null)
    {
        if (!$choices) {
            throw new \LogicException('Choice question must have at least 1 choice available.');
        }

        parent::__construct($question, $default);

        $this->choices = $choices;
        $this->setValidator($this->getDefaultValidator());
        $this->setAutocompleterValues($choices);
    }

    /**
     * @return array<string|bool|int|float|\Stringable>
     */
    public function getChoices(): array
    {
        return $this->choices;
    }

    /**
     * Sets multiselect option.
     *
     * When multiselect is set to true, multiple choices can be answered.
     *
     * @return $this
     */
    public function setMultiselect(bool $multiselect): static
    {
        $this->multiselect = $multiselect;
        $this->setValidator($this->getDefaultValidator());

        return $this;
    }

    /**
     * Returns whether the choices are multiselect.
     */
    public function isMultiselect(): bool
    {
        return $this->multiselect;
    }

    /**
     * Gets the prompt for choices.
     */
    public function getPrompt(): string
    {
        return $this->prompt;
    }

    /**
     * Sets the prompt for choices.
     *
     * @return $this
     */
    public function setPrompt(string $prompt): static
    {
        $this->prompt = $prompt;

        return $this;
    }

    /**
     * Sets the error message for invalid values.
     *
     * The error message has a string placeholder (%s) for the invalid value.
     *
     * @return $this
     */
    public function setErrorMessage(string $errorMessage): static
    {
        $this->errorMessage = $errorMessage;
        $this->setValidator($this->getDefaultValidator());

        return $this;
    }

    private function getDefaultValidator(): callable
    {
        $choices = $this->choices;
        $errorMessage = $this->errorMessage;
        $multiselect = $this->multiselect;
        $isAssoc = $this->isAssoc($choices);

        return function ($selected) use ($choices, $errorMessage, $multiselect, $isAssoc) {
            if ($multiselect) {
                // Check for a separated comma values
                if (!preg_match('/^[^,]+(?:,[^,]+)*$/', (string) $selected, $matches)) {
                    throw new InvalidArgumentException(\sprintf($errorMessage, $selected));
                }

                $selectedChoices = explode(',', (string) $selected);
            } else {
                $selectedChoices = [$selected];
            }

            if ($this->isTrimmable()) {
                foreach ($selectedChoices as $k => $v) {
                    $selectedChoices[$k] = trim((string) $v);
                }
            }

            $multiselectChoices = [];
            foreach ($selectedChoices as $value) {
                $results = [];
                foreach ($choices as $key => $choice) {
                    if ($choice === $value) {
                        $results[] = $key;
                    }
                }

                if (\count($results) > 1) {
                    throw new InvalidArgumentException(\sprintf('The provided answer is ambiguous. Value should be one of "%s".', implode('" or "', $results)));
                }

                $result = array_search($value, $choices);

                if (!$isAssoc) {
                    if (false !== $result) {
                        $result = $choices[$result];
                    } elseif (isset($choices[$value])) {
                        $result = $choices[$value];
                    }
                } elseif (false === $result && isset($choices[$value])) {
                    $result = $value;
                }

                if (false === $result) {
                    throw new InvalidArgumentException(\sprintf($errorMessage, $value));
                }

                // For associative choices, consistently return the key as string:
                $multiselectChoices[] = $isAssoc ? (string) $result : $result;
            }

            if ($multiselect) {
                return $multiselectChoices;
            }

            return current($multiselectChoices);
        };
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console;

use Symfony\Component\Console\Output\OutputInterface;

/**
 * @author Pierre du Plessis <pdples@gmail.com>
 */
final class Cursor
{
    private OutputInterface $output;
    /** @var resource */
    private $input;

    /**
     * @param resource|null $input
     */
    public function __construct(OutputInterface $output, $input = null)
    {
        $this->output = $output;
        $this->input = $input ?? (\defined('STDIN') ? \STDIN : fopen('php://input', 'r+'));
    }

    /**
     * @return $this
     */
    public function moveUp(int $lines = 1): static
    {
        $this->output->write(\sprintf("\x1b[%dA", $lines));

        return $this;
    }

    /**
     * @return $this
     */
    public function moveDown(int $lines = 1): static
    {
        $this->output->write(\sprintf("\x1b[%dB", $lines));

        return $this;
    }

    /**
     * @return $this
     */
    public function moveRight(int $columns = 1): static
    {
        $this->output->write(\sprintf("\x1b[%dC", $columns));

        return $this;
    }

    /**
     * @return $this
     */
    public function moveLeft(int $columns = 1): static
    {
        $this->output->write(\sprintf("\x1b[%dD", $columns));

        return $this;
    }

    /**
     * @return $this
     */
    public function moveToColumn(int $column): static
    {
        $this->output->write(\sprintf("\x1b[%dG", $column));

        return $this;
    }

    /**
     * @return $this
     */
    public function moveToPosition(int $column, int $row): static
    {
        $this->output->write(\sprintf("\x1b[%d;%dH", $row + 1, $column));

        return $this;
    }

    /**
     * @return $this
     */
    public function savePosition(): static
    {
        $this->output->write("\x1b7");

        return $this;
    }

    /**
     * @return $this
     */
    public function restorePosition(): static
    {
        $this->output->write("\x1b8");

        return $this;
    }

    /**
     * @return $this
     */
    public function hide(): static
    {
        $this->output->write("\x1b[?25l");

        return $this;
    }

    /**
     * @return $this
     */
    public function show(): static
    {
        $this->output->write("\x1b[?25h\x1b[?0c");

        return $this;
    }

    /**
     * Clears all the output from the current line.
     *
     * @return $this
     */
    public function clearLine(): static
    {
        $this->output->write("\x1b[2K");

        return $this;
    }

    /**
     * Clears all the output from the current line after the current position.
     */
    public function clearLineAfter(): self
    {
        $this->output->write("\x1b[K");

        return $this;
    }

    /**
     * Clears all the output from the cursors' current position to the end of the screen.
     *
     * @return $this
     */
    public function clearOutput(): static
    {
        $this->output->write("\x1b[0J");

        return $this;
    }

    /**
     * Clears the entire screen.
     *
     * @return $this
     */
    public function clearScreen(): static
    {
        $this->output->write("\x1b[2J");

        return $this;
    }

    /**
     * Returns the current cursor position as x,y coordinates.
     */
    public function getCurrentPosition(): array
    {
        static $isTtySupported;

        if (!$isTtySupported ??= '/' === \DIRECTORY_SEPARATOR && stream_isatty(\STDOUT)) {
            return [1, 1];
        }

        $sttyMode = shell_exec('stty -g');
        shell_exec('stty -icanon -echo');

        @fwrite($this->input, "\033[6n");

        $code = trim(fread($this->input, 1024));

        shell_exec(\sprintf('stty %s', $sttyMode));

        sscanf($code, "\033[%d;%dR", $row, $col);

        return [$col, $row];
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Polyfill\Ctype;

/**
 * Ctype implementation through regex.
 *
 * @internal
 *
 * @author Gert de Pagter <BackEndTea@gmail.com>
 */
final class Ctype
{
    /**
     * Returns TRUE if every character in text is either a letter or a digit, FALSE otherwise.
     *
     * @see https://php.net/ctype-alnum
     *
     * @param mixed $text
     *
     * @return bool
     */
    public static function ctype_alnum($text)
    {
        $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__);

        return \is_string($text) && '' !== $text && !preg_match('/[^A-Za-z0-9]/', $text);
    }

    /**
     * Returns TRUE if every character in text is a letter, FALSE otherwise.
     *
     * @see https://php.net/ctype-alpha
     *
     * @param mixed $text
     *
     * @return bool
     */
    public static function ctype_alpha($text)
    {
        $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__);

        return \is_string($text) && '' !== $text && !preg_match('/[^A-Za-z]/', $text);
    }

    /**
     * Returns TRUE if every character in text is a control character from the current locale, FALSE otherwise.
     *
     * @see https://php.net/ctype-cntrl
     *
     * @param mixed $text
     *
     * @return bool
     */
    public static function ctype_cntrl($text)
    {
        $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__);

        return \is_string($text) && '' !== $text && !preg_match('/[^\x00-\x1f\x7f]/', $text);
    }

    /**
     * Returns TRUE if every character in the string text is a decimal digit, FALSE otherwise.
     *
     * @see https://php.net/ctype-digit
     *
     * @param mixed $text
     *
     * @return bool
     */
    public static function ctype_digit($text)
    {
        $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__);

        return \is_string($text) && '' !== $text && !preg_match('/[^0-9]/', $text);
    }

    /**
     * Returns TRUE if every character in text is printable and actually creates visible output (no white space), FALSE otherwise.
     *
     * @see https://php.net/ctype-graph
     *
     * @param mixed $text
     *
     * @return bool
     */
    public static function ctype_graph($text)
    {
        $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__);

        return \is_string($text) && '' !== $text && !preg_match('/[^!-~]/', $text);
    }

    /**
     * Returns TRUE if every character in text is a lowercase letter.
     *
     * @see https://php.net/ctype-lower
     *
     * @param mixed $text
     *
     * @return bool
     */
    public static function ctype_lower($text)
    {
        $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__);

        return \is_string($text) && '' !== $text && !preg_match('/[^a-z]/', $text);
    }

    /**
     * Returns TRUE if every character in text will actually create output (including blanks). Returns FALSE if text contains control characters or characters that do not have any output or control function at all.
     *
     * @see https://php.net/ctype-print
     *
     * @param mixed $text
     *
     * @return bool
     */
    public static function ctype_print($text)
    {
        $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__);

        return \is_string($text) && '' !== $text && !preg_match('/[^ -~]/', $text);
    }

    /**
     * Returns TRUE if every character in text is printable, but neither letter, digit or blank, FALSE otherwise.
     *
     * @see https://php.net/ctype-punct
     *
     * @param mixed $text
     *
     * @return bool
     */
    public static function ctype_punct($text)
    {
        $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__);

        return \is_string($text) && '' !== $text && !preg_match('/[^!-\/\:-@\[-`\{-~]/', $text);
    }

    /**
     * Returns TRUE if every character in text creates some sort of white space, FALSE otherwise. Besides the blank character this also includes tab, vertical tab, line feed, carriage return and form feed characters.
     *
     * @see https://php.net/ctype-space
     *
     * @param mixed $text
     *
     * @return bool
     */
    public static function ctype_space($text)
    {
        $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__);

        return \is_string($text) && '' !== $text && !preg_match('/[^\s]/', $text);
    }

    /**
     * Returns TRUE if every character in text is an uppercase letter.
     *
     * @see https://php.net/ctype-upper
     *
     * @param mixed $text
     *
     * @return bool
     */
    public static function ctype_upper($text)
    {
        $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__);

        return \is_string($text) && '' !== $text && !preg_match('/[^A-Z]/', $text);
    }

    /**
     * Returns TRUE if every character in text is a hexadecimal 'digit', that is a decimal digit or a character from [A-Fa-f] , FALSE otherwise.
     *
     * @see https://php.net/ctype-xdigit
     *
     * @param mixed $text
     *
     * @return bool
     */
    public static function ctype_xdigit($text)
    {
        $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__);

        return \is_string($text) && '' !== $text && !preg_match('/[^A-Fa-f0-9]/', $text);
    }

    /**
     * Converts integers to their char versions according to normal ctype behaviour, if needed.
     *
     * If an integer between -128 and 255 inclusive is provided,
     * it is interpreted as the ASCII value of a single character
     * (negative values have 256 added in order to allow characters in the Extended ASCII range).
     * Any other integer is interpreted as a string containing the decimal digits of the integer.
     *
     * @param mixed  $int
     * @param string $function
     *
     * @return mixed
     */
    private static function convert_int_to_char_for_ctype($int, $function)
    {
        if (\PHP_VERSION_ID >= 80100 && !\is_string($int)) {
            @trigger_error($function.'(): Argument of type '.get_debug_type($int).' will be interpreted as string in the future', \E_USER_DEPRECATED);
        }

        if (!\is_int($int)) {
            return $int;
        }

        if ($int < -128 || $int > 255) {
            return (string) $int;
        }

        if ($int < 0) {
            $int += 256;
        }

        return \chr($int);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

use Symfony\Polyfill\Ctype as p;

if (!function_exists('ctype_alnum')) {
    function ctype_alnum(mixed $text): bool { return p\Ctype::ctype_alnum($text); }
}
if (!function_exists('ctype_alpha')) {
    function ctype_alpha(mixed $text): bool { return p\Ctype::ctype_alpha($text); }
}
if (!function_exists('ctype_cntrl')) {
    function ctype_cntrl(mixed $text): bool { return p\Ctype::ctype_cntrl($text); }
}
if (!function_exists('ctype_digit')) {
    function ctype_digit(mixed $text): bool { return p\Ctype::ctype_digit($text); }
}
if (!function_exists('ctype_graph')) {
    function ctype_graph(mixed $text): bool { return p\Ctype::ctype_graph($text); }
}
if (!function_exists('ctype_lower')) {
    function ctype_lower(mixed $text): bool { return p\Ctype::ctype_lower($text); }
}
if (!function_exists('ctype_print')) {
    function ctype_print(mixed $text): bool { return p\Ctype::ctype_print($text); }
}
if (!function_exists('ctype_punct')) {
    function ctype_punct(mixed $text): bool { return p\Ctype::ctype_punct($text); }
}
if (!function_exists('ctype_space')) {
    function ctype_space(mixed $text): bool { return p\Ctype::ctype_space($text); }
}
if (!function_exists('ctype_upper')) {
    function ctype_upper(mixed $text): bool { return p\Ctype::ctype_upper($text); }
}
if (!function_exists('ctype_xdigit')) {
    function ctype_xdigit(mixed $text): bool { return p\Ctype::ctype_xdigit($text); }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

use Symfony\Polyfill\Ctype as p;

if (\PHP_VERSION_ID >= 80000) {
    return require __DIR__.'/bootstrap80.php';
}

if (!function_exists('ctype_alnum')) {
    function ctype_alnum($text) { return p\Ctype::ctype_alnum($text); }
}
if (!function_exists('ctype_alpha')) {
    function ctype_alpha($text) { return p\Ctype::ctype_alpha($text); }
}
if (!function_exists('ctype_cntrl')) {
    function ctype_cntrl($text) { return p\Ctype::ctype_cntrl($text); }
}
if (!function_exists('ctype_digit')) {
    function ctype_digit($text) { return p\Ctype::ctype_digit($text); }
}
if (!function_exists('ctype_graph')) {
    function ctype_graph($text) { return p\Ctype::ctype_graph($text); }
}
if (!function_exists('ctype_lower')) {
    function ctype_lower($text) { return p\Ctype::ctype_lower($text); }
}
if (!function_exists('ctype_print')) {
    function ctype_print($text) { return p\Ctype::ctype_print($text); }
}
if (!function_exists('ctype_punct')) {
    function ctype_punct($text) { return p\Ctype::ctype_punct($text); }
}
if (!function_exists('ctype_space')) {
    function ctype_space($text) { return p\Ctype::ctype_space($text); }
}
if (!function_exists('ctype_upper')) {
    function ctype_upper($text) { return p\Ctype::ctype_upper($text); }
}
if (!function_exists('ctype_xdigit')) {
    function ctype_xdigit($text) { return p\Ctype::ctype_xdigit($text); }
}
<?php
namespace Tygh\Sdk\Entities;

class Addon
{
    protected $id;

    protected $root_directory_path;

    public function __construct($id, $root_directory_path)
    {
        $this->id = $id;
        $this->root_directory_path = $root_directory_path;
    }

    /**
     * @return mixed
     */
    public function getXmlSchemePath()
    {
        return "{$this->getRootDirectoryPath()}/app/addons/{$this->id}/addon.xml";
    }

    /**
     * @return mixed
     */
    public function getRootDirectoryPath()
    {
        return $this->root_directory_path;
    }

    public function getFilesGlobMasks()
    {
        $addon_files_glob_masks = [
            // General files
            "app/addons/{$this->id}",
            "var/langs/**/addons/{$this->id}.po",
            "js/addons/{$this->id}",

            // Backend templates and assets
            "design/backend/css/addons/{$this->id}",
            "design/backend/mail/templates/addons/{$this->id}",
            "design/backend/media/images/addons/{$this->id}",
            "design/backend/media/fonts/addons/{$this->id}",
            "design/backend/templates/addons/{$this->id}",

            // Frontend templates and assets
            "design/themes/**/css/addons/{$this->id}",
            "design/themes/**/templates/addons/{$this->id}",
            "design/themes/**/layouts/addons/{$this->id}",
            "design/themes/**/mail/templates/addons/{$this->id}",
            "design/themes/**/media/images/addons/{$this->id}",
            "design/themes/**/media/images/logos/addons/{$this->id}", 
            
            "var/themes_repository/**/css/addons/{$this->id}",
            "var/themes_repository/**/templates/addons/{$this->id}",
            "var/themes_repository/**/layouts/addons/{$this->id}",
            "var/themes_repository/**/mail/templates/addons/{$this->id}",
            "var/themes_repository/**/media/images/addons/{$this->id}",
            "var/themes_repository/**/media/images/logos/addons/{$this->id}",
        ];

        if (file_exists($this->getXmlSchemePath())) {
            $addon_xml_manifest = simplexml_load_file($this->getXmlSchemePath());

            if (!empty($addon_xml_manifest->files->file)) {
                foreach ($addon_xml_manifest->files->file as $additional_file) {
                    $addon_files_glob_masks[] = $additional_file;
                }
            }
        }

        return $addon_files_glob_masks;
    }

    public function matchFilesAgainstGlobMasks($files_glob_masks, $at_directory)
    {
        $glob_matches = [];
        foreach ($files_glob_masks as $glob_mask) {
            $glob_mask = $at_directory . $glob_mask;

            foreach (glob($glob_mask) as $glob_mask_match) {
                $glob_matches[] = substr_replace($glob_mask_match, '', 0, mb_strlen($at_directory));
            }
        }

        return $glob_matches;
    }
}
<?php
namespace Tygh\Sdk\Commands\Traits;

use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;

trait ValidateCartPathTrait
{
    protected function validateCartPath($abs_cart_path, InputInterface $input, OutputInterface $output)
    {
        if (!file_exists($abs_cart_path . 'config.php') || !file_exists($abs_cart_path . 'config.local.php')) {
            $helper = $this->getHelper('question');
            $confirm_cart_path_question = new ConfirmationQuestion(sprintf(
                '<fg=white;bg=red;options=bold>'
                . 'Looks like the specified CS-Cart installation path "%s" is invalid.' . PHP_EOL
                . 'Please make sure that the "cart-directory"'
                . ' and "addon-directory" arguments are not messed up.' . PHP_EOL
                . 'Are you sure you want to proceed? [y/N]:</> ',
                $abs_cart_path
            ), false);

            if (!$helper->ask($input, $output, $confirm_cart_path_question)) {
                exit;
            }
        }
    }
}<?php

namespace Tygh\Sdk\Commands;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Filesystem\Exception\FileNotFoundException;
use Symfony\Component\Filesystem\Filesystem;
use upgrade\builder\AddonBuilder;
use upgrade\builder\AddonReleaseArchive;

class AddonBuildUpgradeCommand extends Command
{
    /**
     * @inheritdoc
     */
    protected function configure()
    {
        $this
            ->setName('addon:build_upgrade')
            ->setDescription(
                'Creates upgrade package between add-on versions'
            )
            ->addArgument('old_addon_version_archive_path',
                InputArgument::REQUIRED,
                'Old add-on version archive path'
            )
            ->addArgument('new_addon_version_archive_path',
                InputArgument::REQUIRED,
                'New add-on version archive path'
            )
            ->addArgument('result_dir_path',
                InputArgument::REQUIRED,
                'Path to a directory where the built upgrade package will be placed'
            )
        ;
    }

    /**
     * @inheritdoc
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $fs = new Filesystem();

        $old_archive_path = $input->getArgument('old_addon_version_archive_path');
        $new_archive_path = $input->getArgument('new_addon_version_archive_path');
        $result_dir_path = $input->getArgument('result_dir_path');

        if (!$fs->exists($old_archive_path)) {
            throw new FileNotFoundException($old_archive_path);
        }
        if (!$fs->exists($new_archive_path)) {
            throw new FileNotFoundException($new_archive_path);
        }

        $fs->mkdir($result_dir_path, 0755);

        $old_version_archive = new AddonReleaseArchive($old_archive_path);
        $new_version_archive = new AddonReleaseArchive($new_archive_path);

        $addon_upgrade_builder = new AddonBuilder($old_version_archive, $new_version_archive, $result_dir_path);
        $addon_upgrade_builder->initPaths();

        $addon_upgrade_builder->run();

        return 0;
    }
}
<?php
namespace Tygh\Sdk\Commands;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Filesystem\Filesystem;
use Tygh\Sdk\Commands\Traits\ValidateCartPathTrait;
use Tygh\Sdk\Entities\Addon;

class AddonSymlinkCommand extends Command
{
    use ValidateCartPathTrait;

    /**
     * @inheritdoc
     */
    protected function configure()
    {
        $this
            ->setName('addon:symlink')
            ->setDescription(
                'Creates symlinks for add-on files at the CS-Cart installation directory,'
                . ' allowing you to develop and store add-on files in a separate Git repository.'
            )
            ->addArgument('name',
                InputArgument::REQUIRED,
                'Add-on ID (name)'
            )
            ->addArgument('addon-directory',
                InputArgument::REQUIRED,
                'Path to directory with add-on files'
            )
            ->addArgument('cart-directory',
                InputArgument::REQUIRED,
                'Path to CS-Cart installation directory'
            )
            ->addOption('relative',
                'r',
                InputOption::VALUE_NONE,
                'Created symlinks will have a relative path to the target file. By default the created symlinks have an absolute path to target.'
            )
            ->addOption('templates-to-design',
                null,
                InputOption::VALUE_NONE,
                'Whether to take the add-on templates from "var/themes_repository" path at the add-on directory and put them at "design/themes" path in the CS-Cart installation directory . When this option is not specified, the templates are being taken from "var/themes_repository" and also put into "var/themes_repository" directory.'
            );
    }

    /**
     * @inheritdoc
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $fs = new Filesystem();

        $addon_id = $input->getArgument('name');

        $abs_cart_path = rtrim(realpath($input->getArgument('cart-directory')), '\\/') . '/';
        $this->validateCartPath($abs_cart_path, $input, $output);

        $abs_addon_path = rtrim(realpath($input->getArgument('addon-directory')), '\\/') . '/';

        chdir($abs_addon_path);

        $addon = new Addon($addon_id, $abs_addon_path);
        $addon_files_glob_masks = $addon->getFilesGlobMasks();

        $addon_files_glob_masks = array_filter($addon_files_glob_masks, function ($glob_mask) {

            // Always ignore "design/themes/" masks because there shouldn't
            // be such directory in add-on files directory
            if (mb_strpos($glob_mask, 'design/themes/') === 0) {
                return false;
            }

            return true;
        });

        $glob_matches = $addon->matchFilesAgainstGlobMasks($addon_files_glob_masks, $abs_addon_path);

        $output->writeln(sprintf('<fg=magenta;options=bold>Creating symlinks at the "%s" directory:</>',
            $abs_cart_path
        ));

        foreach ($glob_matches as $rel_filepath) {
            $abs_addon_filepath = $abs_addon_path . $rel_filepath;

            // Add-on templates at the "var/themes_repository/" directory will be
            // symlinked to the "design/themes/" directory.
            if ($input->getOption('templates-to-design') && mb_strpos($rel_filepath, 'var/themes_repository/') === 0) {
                $abs_cart_filepath = $abs_cart_path
                    . 'design/themes/'
                    . mb_substr($rel_filepath, mb_strlen('var/themes_repository/'));
            } else {
                $abs_cart_filepath = $abs_cart_path . $rel_filepath;
            }

            // Delete existing files and links located at cart directory
            clearstatcache(true, $abs_cart_filepath);

            if (file_exists($abs_cart_filepath)) {
                $is_link = is_link($abs_cart_filepath);
                $is_file = $is_link ? is_file(readlink($abs_cart_filepath)) : is_file($abs_cart_filepath);
                $is_dir = $is_link ? is_dir(readlink($abs_cart_filepath)) : is_dir($abs_cart_filepath);

                // Confirm overwriting of the found conflicting file or directory on the same path.
                // We only ask confirmation for files which are not symbolic links, because we assume
                // that symbolic links were created by this command earlier and can be overwritten without
                // the loss of any data.
                if (!$is_link && ($is_file || $is_dir)) {
                    $helper = $this->getHelper('question');
                    $question = new ConfirmationQuestion(sprintf(
                        '<question>%s "%s" already exists. Overwrite? [y/N]:</question> ',
                        $is_dir ? 'Directory' : 'File',
                        $abs_cart_filepath
                    ), false);

                    if (!$helper->ask($input, $output, $question)) {
                        continue;
                    }
                }

                $fs->remove($abs_cart_filepath);
            }

            $symlink_target_filepath = $input->getOption('relative')
                ? $fs->makePathRelative(
                    dirname($abs_addon_filepath), dirname($abs_cart_filepath)
                ) . basename($abs_cart_filepath)
                : $abs_addon_filepath;

            $fs->symlink(
                $symlink_target_filepath,
                $abs_cart_filepath
            );

            $output->writeln(sprintf('Creating symlink for %s... <info>OK</info>',
                $rel_filepath
            ));
        }

        return 0;
    }
}<?php
namespace Tygh\Sdk\Commands;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Filesystem\Filesystem;

class AddonSyncCommand extends Command
{
    /**
     * @inheritdoc
     */
    protected function configure()
    {
        $this
            ->setName('addon:sync')
            ->setDescription(
                'Synchronizes add-on files between CS-Cart installation directory and the separate directory storing'
                . ' all add-on files. Calling this command has the same effect as calling the "addon:export"'
                . ' and "addon:symlink" commands simultaneously.'
            )
            ->addArgument('name',
                InputArgument::REQUIRED,
                'Add-on ID (name)'
            )
            ->addArgument('addon-directory',
                InputArgument::REQUIRED,
                'Path to directory where files should be moved to'
            )
            ->addArgument('cart-directory',
                InputArgument::REQUIRED,
                'Path to CS-Cart installation directory'
            )
            ->addOption('relative',
                'r',
                InputOption::VALUE_NONE,
                'Created symlinks will have a relative path to the target file. By default the created symlinks have an absolute path to target.'
            );
    }

    /**
     * @inheritdoc
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $addon_export_command = $this->getApplication()->find('addon:export');
        $addon_symlink_command = $this->getApplication()->find('addon:symlink');

        $arguments = array(
            'name' => $input->getArgument('name'),
            'addon-directory' => $input->getArgument('addon-directory'),
            'cart-directory' => $input->getArgument('cart-directory'),
        );

        $export_input = new ArrayInput($arguments + [
                '--delete' => true,
                '--templates-from-design' => true,
            ]
        );
        $symlink_input = new ArrayInput($arguments + [
                '--relative' => $input->getOption('relative'),
                '--templates-to-design' => true,
            ]
        );

        $export_status = $addon_export_command->run($export_input, $output);
        $output->writeln('');
        $symlink_status = $addon_symlink_command->run($symlink_input, $output);

        return $export_status !== 0 ? $export_status : $symlink_status;
    }
}<?php
namespace Tygh\Sdk\Commands;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Filesystem\Filesystem;
use Tygh\Sdk\Commands\Traits\ValidateCartPathTrait;
use Tygh\Sdk\Entities\Addon;

class AddonExportCommand extends Command
{
    use ValidateCartPathTrait;

    /**
     * @inheritdoc
     */
    protected function configure()
    {
        $this
            ->setName('addon:export')
            ->setDescription(
                'Copies or moves all add-on files to the separate directory, preserving the structure of directories.'
            )
            ->addArgument('name',
                InputArgument::REQUIRED,
                'Add-on ID (name)'
            )
            ->addArgument('addon-directory',
                InputArgument::REQUIRED,
                'Path to directory where files should be moved to'
            )
            ->addArgument('cart-directory',
                InputArgument::REQUIRED,
                'Path to CS-Cart installation directory'
            )
            ->addOption('delete',
                'd',
                InputOption::VALUE_NONE,
                'Files and directories will be moved instead of being copied.'
            )
            ->addOption('templates-from-design',
                null,
                InputOption::VALUE_NONE,
                'Whether to take the add-on templates from "design/themes" path at CS-Cart installation directory and put them at "var/themes_repository" path in the add-on files directory. When this option is not specified, the templates are being taken from "var/themes_repository" and also put into "var/themes_repository" directory.'
            );
    }

    /**
     * @inheritdoc
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $fs = new Filesystem();

        $addon_id = $input->getArgument('name');
        $abs_cart_path = rtrim(realpath($input->getArgument('cart-directory')), '\\/') . '/';

        $this->validateCartPath($abs_cart_path, $input, $output);

        $addon_path = $input->getArgument('addon-directory').'/src';

        if($fs->exists($addon_path)) {
            $output->writeln('<fg=magenta;options=bold>Target directory '. $addon_path.' exists!</>');
            $output->writeln('<fg=red;options=bold>Will be cleared before export</>');
            $helper = $this->getHelper('question');
            $question = new ConfirmationQuestion(
                '<question>Will be cleared before export. Continue? [y/N]:</question> ',
                false
            );

            if (!$helper->ask($input, $output, $question)) {
                die(1);
            }
            else {
                //printf("%s\n", $abs_cart_path);die(1);
                $fs->remove($addon_path);
            }

        }
        $fs->mkdir($addon_path, 0755);

        $abs_addon_path = rtrim(realpath($addon_path), '\\/') . '/';
        $output->writeln(sprintf('<fg=magenta;options=bold>%s add-on files to the "%s" directory:</>',
            $input->getOption('delete') ? 'Moving' : 'Copying',
            $abs_addon_path
        ));

        $addon = new Addon($addon_id, $abs_cart_path);
        $addon_files_glob_masks = $addon->getFilesGlobMasks();

        $templates_from_design = !$input->getOption('templates-from-design');

        $addon_files_glob_masks = array_filter($addon_files_glob_masks, function ($glob_mask) use ($input,$templates_from_design) {
            if ($templates_from_design) {

                // Ignore "var/themes_repository/" masks
                if (mb_strpos($glob_mask, 'var/themes_repository/') === 0) {
                    return false;
                }

                return true;
            } else {
                // Ignore "design/themes/" masks
                if (mb_strpos($glob_mask, 'design/themes/') === 0) {
                    return false;
                }

                return true;
            }
        });

        $glob_matches = $addon->matchFilesAgainstGlobMasks($addon_files_glob_masks, $abs_cart_path);

        $counter = 0;
        foreach ($glob_matches as $rel_filepath) {
            $abs_cart_filepath = $abs_cart_path . $rel_filepath;

            // Skip links pointing to target add-on directory
            if (is_link($abs_cart_filepath)) {
                $output->writeln(sprintf('Found symlink "%s", <info>skipping</info>',
                    $rel_filepath
                ));

                continue;
            }

            // Add-on templates at the "design/" directory will be
            // exported to the "var/themes_repository/" directory.
            if ($templates_from_design && mb_strpos($rel_filepath, 'design/themes/') === 0) {
                $abs_addon_filepath = $abs_addon_path
                    . 'var/themes_repository/'
                    . mb_substr($rel_filepath, mb_strlen('design/themes/'));
            } else {
                $abs_addon_filepath = $abs_addon_path . $rel_filepath;
            }

            if (file_exists($abs_addon_filepath)) {
                $helper = $this->getHelper('question');
                $question = new ConfirmationQuestion(sprintf(
                    '<question>%s "%s" already exists. Overwrite? [y/N]:</question> ',
                    is_dir($abs_addon_filepath) ? 'Directory' : 'File',
                    $abs_addon_filepath
                ), false);

                if (!$helper->ask($input, $output, $question)) {
                    continue;
                }
            }

            $fs->mkdir(dirname($abs_addon_filepath), 0755);

            $output->write(sprintf('%s "%s" to "%s" ... ',
                $input->getOption('delete') ? 'Moving' : 'Copying',
                $rel_filepath,
                $abs_addon_filepath
            ));

            if ($input->getOption('delete')) {
                $fs->rename($abs_cart_filepath, $abs_addon_filepath, true);
            } else {
                if (is_dir($abs_cart_filepath)) {
                    $fs->mirror($abs_cart_filepath, $abs_addon_filepath, null, [
                        'override' => true, // Override existing files at target directory
                    ]);
                } elseif (is_file($abs_cart_filepath)) {
                    $fs->copy($abs_cart_filepath, $abs_addon_filepath, true);
                }
            }

            $counter++;
            $output->writeln('<info>OK</info>');
        }

        $output->writeln(sprintf('<options=bold>%u</> <info>files and directories have been %s.</info>',
            $counter,
            $input->getOption('delete') ? 'moved' : 'copied'
        ));

        return 0;
    }
}<?php
/**
 * @copyright 2019 Kartpay.com.
 * @author Panos <panos@kartpay.com>
 * Date: 6/4/2019
 * Time: 1:50 μμ
 */

namespace Tygh\Slx\Util;


class NameConversions {

    public static function camelize($input, $separator = '_') {
        return str_replace($separator, '', ucwords($input, $separator));
    }

    /**
     * @param $addonId
     */
    public static function addonIdToNamespace($addonId) {
        $parts = explode('_',$addonId);
        $outParts = [];
        $namespace = '';
        if(reset($parts)=='kp') {
            $namespace = 'Kartpay';
            array_shift($parts);
        }
        elseif(reset($parts)=='slx') {
            $namespace = 'Slx';
            array_shift($parts);
        }

        foreach($parts as $part) {
            $outParts[] = ucfirst($part);
        }
        return [$namespace,implode('',$outParts)];
    }
}<?php
/**
 * @copyright Salix.me
 * @author Panos <panos@salix.gr>
 *
 * Given a template path (e.g. "views/products/update.tpl", relative to a
 * templates root such as design/backend/templates), walks the whole
 * rendering graph CS-Cart would actually touch:
 *
 *   - the core/base file at that path
 *   - any active addon's full override at
 *       addons/<addon>/overrides/<path>
 *   - every {hook name="controller:hook_name"} found in whichever file(s)
 *     are in play, resolved against every active addon's
 *       addons/<addon>/hooks/<controller>/<hook_name>.pre.tpl
 *       addons/<addon>/hooks/<controller>/<hook_name>.post.tpl
 *       addons/<addon>/hooks/<controller>/<hook_name>.override.tpl
 *   - every {include file="..."} / {include_ext file="..."} found along
 *     the way, resolved (recursively, through the same base/override/hook
 *     logic) against the templates root
 *
 * Addon precedence: active addons ordered by
 *   SELECT addon FROM ?:addons WHERE status = 'A' ORDER BY priority
 * — the first one in that list is overridden by the second, which is
 * overridden by the third, and so on. So the LAST active addon in
 * ascending-priority order that has an override file for a given path is
 * the one that actually renders. This class marks that one
 * 'assumed_winner' => true. (This ordering rule is confirmed by direct
 * project experience, not by independently reading core's compiled
 * resource-resolution code - treat "which override wins" as
 * understood-but-not-source-verified.)
 *
 * Exclude roots: some includes (common/subheader.tpl, buttons/*.tpl, ...)
 * are generic UI chrome that's rarely relevant to whatever you're
 * debugging. Pass $excludeRoots (path prefixes, e.g. ['common', 'buttons'])
 * to skip resolving/recursing into anything under them - the request that
 * pulled them in is still recorded as a single 'excluded' entry, so you
 * can see they were pulled in without the noise of expanding them.
 * Exclusion never applies to the top-level path you explicitly asked
 * collect() for, only to things discovered along the way.
 *
 * Originally built inside jcd_changes_log (a jcclubbelize-specific addon)
 * as Jcd\ChangesLog\Debug\TemplateFileCollector, using CS-Cart's own
 * db_get_fields() and the DIR_ROOT bootstrap constant. This is a portable
 * version for standalone use in cscart-sdk: the core algorithm is 100%
 * plain PHP (is_file/file_get_contents/preg_match - no CS-Cart runtime
 * dependency at all), so only the convenience "get real data for this
 * environment" factory needed replacing - fromCsCartBootstrap() (DIR_ROOT
 * + db_get_fields()) became fromDoctrineConnection() (explicit root path
 * + a Doctrine query). While porting, also fixed a real bug in the
 * original factory: it built the templates root as
 * "design/<area>/templates" unconditionally, which is only correct for
 * area='backend' - a theme's actual root is "design/themes/<theme>/templates",
 * confirmed against a real install (design/themes/responsive/ exists,
 * design/responsive/ does not). This never surfaced in jcd_changes_log
 * because it only ever collected backend (admin edit-form) templates.
 *
 * A second bug was found and fixed after deployment: resolveHook() had no
 * cycle guard at all, unlike resolveAndScan()'s $scannedContent guard for
 * regular includes. If a hook file's own content re-emits the same (or a
 * mutually-cyclical) {hook name="..."} tag, this recursed infinitely -
 * confirmed via a real infinite-recursion crash and reproduced with a
 * minimal fixture before the fix. See $resolvedHooks below.
 *
 * Usage:
 *   $collector = TemplateFileCollector::fromDoctrineConnection($conn, $tablePrefix, $cartRoot, 'backend');
 *   $collector = TemplateFileCollector::fromDoctrineConnection($conn, $tablePrefix, $cartRoot, 'responsive');
 *   $files = $collector->collect('views/products/update.tpl');
 *   foreach ($files as $f) { ... }
 *
 * Each entry in the returned array:
 *   [
 *     'kind'   => 'base' | 'override' | 'hook_pre' | 'hook_post' | 'hook_override' | 'include' | 'include_unresolved' | 'excluded',
 *     'path'   => absolute filesystem path (or null if unresolved/excluded),
 *     'relative' => path relative to templates root,
 *     'addon'  => addon id, or null for core/base,
 *     'exists' => bool,
 *     'assumed_winner' => bool (only meaningful for 'override'/'base' entries
 *                               that share the same 'relative' path),
 *     'via'    => the relative path of the file this was discovered from
 *                 (null for the root template you asked for),
 *     'note'   => free-text detail (e.g. hook name, raw include expression)
 *   ]
 */

namespace Tygh\Slx\Tools;

class TemplateFileCollector
{
    /** @var string absolute path to the templates root, no trailing slash */
    private $templatesRoot;

    /**
     * @var array ordered list of active addon ids, ascending priority
     *            (index 0 = lowest priority = overridden first; last =
     *            highest priority = wins)
     */
    private $activeAddonsInOrder;

    /** @var array normalized exclude-root prefixes, e.g. ['common', 'buttons'] */
    private $excludeRoots;

    /** @var array flat result list, built up during collect() */
    private $files = [];

    /** @var array relativePath => true, guards re-scanning the same template's content twice */
    private $scannedContent = [];

    /** @var array hookFullName => true, guards re-resolving the same hook name twice.
     *  Without this, a hook file whose own content re-emits the same {hook name="..."}
     *  it's a handler for (directly, or via a chain through other hooks/includes) causes
     *  unbounded recursion: resolveHook() has no memory of hook names already resolved,
     *  so it keeps re-finding the same hook files via is_file() and re-scanning their
     *  content forever. resolveAndScan() already guards the equivalent case for regular
     *  includes via $scannedContent; this is the same idea, keyed by hook name instead
     *  of file path, since a single hook name can resolve to several hook files at once
     *  (one per matching addon/suffix) that none individually "own" the dedup key. */
    private $resolvedHooks = [];

    /**
     * @param string $templatesRoot absolute path, e.g. <cartRoot> . '/design/backend/templates'
     * @param array  $activeAddonsInOrder addon ids, ascending priority (see class docblock)
     * @param array  $excludeRoots path prefixes to not resolve/recurse into,
     *               e.g. ['common', 'buttons', 'common/*', 'buttons/*'] -
     *               a trailing '/*' or '*' is stripped automatically.
     */
    public function __construct($templatesRoot, array $activeAddonsInOrder, array $excludeRoots = [])
    {
        $this->templatesRoot = rtrim($templatesRoot, '/\\');
        $this->activeAddonsInOrder = array_values($activeAddonsInOrder);
        $this->excludeRoots = array_map([$this, 'normalizeExcludeRoot'], $excludeRoots);
    }

    private function normalizeExcludeRoot($pattern)
    {
        $pattern = str_replace('\\', '/', trim($pattern));
        $pattern = rtrim($pattern, '/');
        if (substr($pattern, -2) === '/*') {
            $pattern = substr($pattern, 0, -2);
        } elseif (substr($pattern, -1) === '*') {
            $pattern = substr($pattern, 0, -1);
        }
        return trim($pattern, '/');
    }

    /**
     * Convenience constructor for standalone (non-bootstrapped) use, e.g.
     * from an SDK console command. Pulls active addons in true
     * override-precedence order via a plain Doctrine query - equivalent
     * to the original fromCsCartBootstrap()'s
     *   SELECT addon FROM ?:addons WHERE status = 'A' ORDER BY priority
     * just without CS-Cart's db_get_fields() / ?: table-prefix helper.
     *
     * @param object $conn Any object exposing query($sql): Statement-with-fetch(),
     *   e.g. a real \Doctrine\DBAL\Connection or a plain \PDO. Not type-hinted
     *   against Doctrine specifically so this class has no hard dependency on it.
     * @param string $tablePrefix e.g. 'cscart_' - this install's actual table prefix
     * @param string $cartRoot absolute path to the CS-Cart installation root
     * @param string $area 'backend', or a theme name (e.g. 'responsive') -
     *   NOT the literal 'themes/responsive' path, just the theme's own name
     * @param array $excludeRoots see class docblock
     */
    public static function fromDoctrineConnection($conn, $tablePrefix, $cartRoot, $area, array $excludeRoots = [])
    {
        $templatesRoot = self::resolveTemplatesRoot($cartRoot, $area);

        $sql = sprintf(
            "SELECT addon FROM %saddons WHERE status = 'A' ORDER BY priority ASC",
            $tablePrefix
        );
        $activeAddonsInOrder = [];
        $stmt = $conn->query($sql);
        while ($row = $stmt->fetch()) {
            $addonId = isset($row['addon']) ? $row['addon'] : reset($row);
            if ($addonId !== false && $addonId !== null) {
                $activeAddonsInOrder[] = $addonId;
            }
        }

        return new self($templatesRoot, $activeAddonsInOrder, $excludeRoots);
    }

    /** 'backend' -> design/backend/templates; anything else is treated as a
     *  theme name -> design/themes/<theme>/templates (NOT design/<theme>/templates -
     *  see class docblock for the real-install confirmation behind this). */
    private static function resolveTemplatesRoot($cartRoot, $area)
    {
        $cartRoot = rtrim($cartRoot, '/\\');
        if ($area === 'backend') {
            return $cartRoot . '/design/backend/templates';
        }
        return $cartRoot . '/design/themes/' . $area . '/templates';
    }

    /**
     * @return array see class docblock for entry shape
     */
    public function collect($relativeTplPath)
    {
        $this->files = [];
        $this->scannedContent = [];
        $this->resolvedHooks = [];
        $this->resolveAndScan($this->normalize($relativeTplPath), null, true);
        return $this->files;
    }

    private function normalize($relativePath)
    {
        return ltrim(str_replace('\\', '/', $relativePath), '/');
    }

    private function isExcluded($relativePath)
    {
        foreach ($this->excludeRoots as $root) {
            if ($root === '') {
                continue;
            }
            if ($relativePath === $root || strpos($relativePath, $root . '/') === 0) {
                return true;
            }
        }
        return false;
    }

    private function resolveAndScan($relativePath, $via, $isTopLevel = false)
    {
        if (!$isTopLevel && $this->isExcluded($relativePath)) {
            $this->files[] = [
                'kind' => 'excluded',
                'path' => null,
                'relative' => $relativePath,
                'addon' => null,
                'exists' => null,
                'assumed_winner' => false,
                'via' => $via,
                'note' => 'skipped (excluded root)',
            ];
            return;
        }

        // 1) Base/core file
        $baseAbs = $this->templatesRoot . '/' . $relativePath;
        $baseExists = is_file($baseAbs);

        // 2) Every active addon's full override candidate for this exact path,
        //    in ascending-priority order (later entries override earlier ones).
        $overrideCandidates = [];
        foreach ($this->activeAddonsInOrder as $addonId) {
            $abs = $this->templatesRoot . '/addons/' . $addonId . '/overrides/' . $relativePath;
            if (is_file($abs)) {
                $overrideCandidates[] = ['addon' => $addonId, 'path' => $abs];
            }
        }

        // Record base
        $this->files[] = [
            'kind' => 'base',
            'path' => $baseExists ? $baseAbs : null,
            'relative' => $relativePath,
            'addon' => null,
            'exists' => $baseExists,
            'assumed_winner' => $baseExists && empty($overrideCandidates),
            'via' => $via,
            'note' => null,
        ];

        // Record every override candidate; last in priority order wins.
        $winningContent = $baseExists ? @file_get_contents($baseAbs) : false;
        foreach ($overrideCandidates as $i => $cand) {
            $isLast = ($i === count($overrideCandidates) - 1);
            $this->files[] = [
                'kind' => 'override',
                'path' => $cand['path'],
                'relative' => $relativePath,
                'addon' => $cand['addon'],
                'exists' => true,
                'assumed_winner' => $isLast,
                'via' => $via,
                'note' => null,
            ];
            if ($isLast) {
                $winningContent = @file_get_contents($cand['path']);
            }
        }

        if ($winningContent === false) {
            // Neither base nor any override exists - dead include, nothing to scan.
            return;
        }

        // Avoid re-parsing the same relative path's content twice if it's
        // include-chained back to itself or reached via multiple routes.
        if (isset($this->scannedContent[$relativePath])) {
            return;
        }
        $this->scannedContent[$relativePath] = true;

        $this->scanContentForHooksAndIncludes($winningContent, $relativePath);
    }

    /**
     * Parses a template's content for {hook ...} and {include ...} /
     * {include_ext ...} tags and recurses/expands accordingly. Used both
     * for the "winning" base/override content and for the content of any
     * .pre/.post/.override hook file we find (those can themselves contain
     * further hooks/includes).
     */
    private function scanContentForHooksAndIncludes($content, $foundIn)
    {
        // --- hooks: {hook name="controller:hook_name"} ---
        if (preg_match_all('/\{hook\s+name\s*=\s*["\']([^"\']+)["\']/', $content, $m)) {
            foreach (array_unique($m[1]) as $hookFullName) {
                $this->resolveHook($hookFullName, $foundIn);
            }
        }

        // --- includes: {include file="..."} / {include_ext file="..."} ---
        if (preg_match_all('/\{include(?:_ext)?\s+[^}]*?file\s*=\s*["\']([^"\']+)["\']/', $content, $m)) {
            foreach (array_unique($m[1]) as $includePath) {
                $this->resolveAndScan($this->normalize($includePath), $foundIn);
            }
        }

        // --- includes with a dynamic/computed file expression we can't resolve statically ---
        if (preg_match_all('/\{include(?:_ext)?\s+[^}]*?file\s*=\s*(\$[A-Za-z0-9_.]+)/', $content, $m)) {
            foreach (array_unique($m[1]) as $dynamicExpr) {
                $this->files[] = [
                    'kind' => 'include_unresolved',
                    'path' => null,
                    'relative' => null,
                    'addon' => null,
                    'exists' => false,
                    'assumed_winner' => false,
                    'via' => $foundIn,
                    'note' => "dynamic file expression: {$dynamicExpr} - resolve manually",
                ];
            }
        }
    }

    /**
     * hook name is "controller:hook_name" - the part before ":" is the
     * hooks/<controller>/ folder (e.g. "products:update_product_code"
     * -> hooks/products/update_product_code.{pre,post,override}.tpl).
     */
    private function resolveHook($hookFullName, $foundIn)
    {
        if (isset($this->resolvedHooks[$hookFullName])) {
            // already fully resolved this hook name once this collect() run - stops
            // any cycle where a hook file's own content re-emits the same hook name
            return;
        }
        $this->resolvedHooks[$hookFullName] = true;

        if (strpos($hookFullName, ':') === false) {
            // Malformed / unexpected hook name shape; record and move on.
            $this->files[] = [
                'kind' => 'hook_override',
                'path' => null,
                'relative' => null,
                'addon' => null,
                'exists' => false,
                'assumed_winner' => false,
                'via' => $foundIn,
                'note' => "unrecognized hook name shape: {$hookFullName}",
            ];
            return;
        }

        list($controller, $hookShort) = explode(':', $hookFullName, 2);

        foreach (['pre', 'post', 'override'] as $suffix) {
            foreach ($this->activeAddonsInOrder as $addonId) {
                $relative = "addons/{$addonId}/hooks/{$controller}/{$hookShort}.{$suffix}.tpl";
                $abs = $this->templatesRoot . '/' . $relative;
                if (!is_file($abs)) {
                    continue;
                }
                $this->files[] = [
                    'kind' => 'hook_' . $suffix,
                    'path' => $abs,
                    'relative' => $relative,
                    'addon' => $addonId,
                    'exists' => true,
                    'assumed_winner' => null, // pre/post always run if present; override always wins over base hook content
                    'via' => $foundIn,
                    'note' => "hook: {$hookFullName}",
                ];

                // Hook files can themselves contain further hooks/includes.
                $hookContent = @file_get_contents($abs);
                if ($hookContent !== false) {
                    $this->scanContentForHooksAndIncludes($hookContent, $relative);
                }
            }
        }
    }
}
<?php
/**
 * Step 3: resolves handler functions registered via fn_register_hooks(),
 * including the addon-override array form ['hook', priority, 'other_addon'].
 *
 * Instantiated per (root, activeAddons) pair so the global registration
 * map - built by scanning every addon's init.php - is computed at most
 * once per instance and reused across every hook lookup, instead of being
 * rebuilt from scratch on every call (as the old static findAllAddonHandlers()
 * did whenever a function name resolved to multiple definitions).
 */

namespace Tygh\Slx\Tools\FnHookExplorer;

class AddonRegistrationResolver {

    use FindsPhpFilesTrait;

    /** fn_<one of these>_<hook>() is never an addon-registration handler -
     *  it's the fn_core_ / fn_<edition_acronym>_ naming-convention mechanism
     *  (see CoreEditionHandlerResolver). Kept in sync with the table in
     *  fn_get_edition_acronym() (app/functions/fn.common.php). */
    const RESERVED_CALLBACK_PREFIXES = ['core', 'pro', 'mve', 'ult'];

    /** @var string */
    private $root;

    /** @var string[]|null */
    private $activeAddons;

    /** @var array|null Memoized once built - see getRegistrationMap(). */
    private $registrationMap;

    /**
     * @param string $root
     * @param string[]|null $activeAddons List of addon IDs currently active on this
     *   installation. null disables all active-addon filtering, so results reflect
     *   what's present in the codebase regardless of what's actually enabled - this
     *   keeps the class usable standalone, without a DB connection. When given, it's
     *   used two ways, matching fn_register_hooks()'s own source:
     *     - an addon's init.php/func.php only ever runs if THAT addon is active, so
     *       inactive addons are skipped entirely as both registration sources and
     *       handler-function sources;
     *     - for the addon-override array form specifically, fn_register_hooks() has
     *       its own extra check (`Registry::get('addons.'.$addon.'.status') != 'A'`)
     *       that skips the registration if the TARGET addon isn't active, even when
     *       the declaring addon is.
     */
    public function __construct($root, array $activeAddons = null) {
        $this->root = $root;
        $this->activeAddons = $activeAddons;
    }

    /**
     * @param string[] $hookNames
     * @return array hookName => list of handler match records
     */
    public function findHandlers(array $hookNames) {
        $results = array_fill_keys($hookNames, []);
        $addonsDir = $this->root . '/app/addons';
        if (!is_dir($addonsDir) || empty($hookNames)) {
            return $results;
        }

        // memoized - cheap the first time (~1 file per addon), free after
        $registrationMap = $this->getRegistrationMap();

        // single combined pattern: function fn_<addon>_<hook1|hook2|...>(
        $escaped = array_map(function ($h) {
            return preg_quote($h, '/');
        }, $hookNames);
        $alternation = implode('|', $escaped);
        $pattern = '/\bfunction\s+(fn_(\w+?)_(' . $alternation . '))\s*\(/';

        foreach (scandir($addonsDir) as $addonName) {
            if ($addonName === '.' || $addonName === '..') {
                continue;
            }
            $addonPath = $addonsDir . '/' . $addonName;
            if (!is_dir($addonPath)) {
                continue;
            }
            if ($this->activeAddons !== null && !in_array($addonName, $this->activeAddons, true)) {
                continue; // addon inactive -> its files are never loaded, handler can't exist at runtime
            }

            foreach ($this->findPhpFiles($addonPath) as $file) {
                $lines = file($file);
                if ($lines === false) {
                    continue;
                }
                foreach ($lines as $i => $line) {
                    if (preg_match($pattern, $line, $m)) {
                        $matchedHook = $m[3];
                        $calleeAddon = $m[2]; // the addon embedded in the callback name itself -
                                               // this is what CS-Cart actually dispatches by,
                                               // regardless of which addon's file defines it
                        // 'core' and the edition acronyms (pro/mve/ult) are a completely
                        // different mechanism (see CoreEditionHandlerResolver) -
                        // fn_<one of those>_<hook> never means "addon named 'core'/'mve'/
                        // etc", so treating it as a registration-based handler here would
                        // produce a misleading "NOT REGISTERED" false alarm for something
                        // that actually fires just fine by naming convention alone.
                        if (in_array($calleeAddon, self::RESERVED_CALLBACK_PREFIXES, true)) {
                            continue;
                        }
                        $registered = isset($registrationMap[$matchedHook][$calleeAddon]);
                        $results[$matchedHook][] = [
                            'calleeAddon' => $calleeAddon,
                            'declaredIn'  => $addonName,
                            'function'    => $m[1],
                            'file'        => $file,
                            'line'        => $i + 1,
                            'registered'  => $registered,
                        ];
                    }
                }
            }
        }
        return $results;
    }

    /** Builds (once, memoized) a global map: hookName => set of addon names
     *  whose callback (fn_<addon>_<hook>) is expected to be registered for
     *  that hook - resolving the addon-override array form across ALL
     *  addons' init.php files, not just the one that owns the handler
     *  function. */
    public function getRegistrationMap() {
        if ($this->registrationMap === null) {
            $this->registrationMap = $this->buildGlobalRegistrationMap();
        }
        return $this->registrationMap;
    }

    /** When $activeAddons is given, mirrors fn_register_hooks()'s own two
     *  activity checks: (1) an inactive addon's init.php never runs at all,
     *  so it's skipped as a registration source entirely; (2) for the
     *  addon-override form specifically, the registration is additionally
     *  skipped if the TARGET addon isn't active, even when the declaring
     *  addon is - both checks have to pass. */
    private function buildGlobalRegistrationMap() {
        $map = [];
        $addonsDir = $this->root . '/app/addons';
        if (!is_dir($addonsDir)) {
            return $map;
        }
        foreach (scandir($addonsDir) as $addonName) {
            if ($addonName === '.' || $addonName === '..') {
                continue;
            }
            $addonPath = $addonsDir . '/' . $addonName;
            if (!is_dir($addonPath)) {
                continue;
            }
            if ($this->activeAddons !== null && !in_array($addonName, $this->activeAddons, true)) {
                continue; // declaring addon inactive -> its init.php never runs
            }
            foreach ($this->getEffectiveRegistrations($addonPath) as $reg) {
                $effectiveAddon = $reg['targetAddon'] !== null ? $reg['targetAddon'] : $addonName;
                if ($reg['targetAddon'] !== null && $this->activeAddons !== null
                    && !in_array($reg['targetAddon'], $this->activeAddons, true)) {
                    continue; // override target addon inactive -> registration skipped too
                }
                $map[$reg['hook']][$effectiveAddon] = true;
            }
        }
        return $map;
    }

    /** Parses every fn_register_hooks(...) call in an addon's init.php into
     *  a list of ['hook' => name, 'targetAddon' => override-or-null]. */
    private function getEffectiveRegistrations($addonPath) {
        $initFile = $addonPath . '/init.php';
        if (!file_exists($initFile)) {
            return [];
        }
        $source = file_get_contents($initFile);
        if ($source === false) {
            return [];
        }
        $tokens = token_get_all($source);
        $n = count($tokens);

        $registrations = [];

        for ($i = 0; $i < $n; $i++) {
            $tok = $tokens[$i];
            if (!is_array($tok) || $tok[0] !== T_STRING || $tok[1] !== 'fn_register_hooks') {
                continue;
            }

            $j = $i + 1;
            while ($j < $n && is_array($tokens[$j])
                && in_array($tokens[$j][0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true)) {
                $j++;
            }
            if ($j >= $n || $tokens[$j] !== '(') {
                continue;
            }

            $depth = 0;
            $closeParen = null;
            for ($k = $j; $k < $n; $k++) {
                if ($tokens[$k] === '(') {
                    $depth++;
                } elseif ($tokens[$k] === ')') {
                    $depth--;
                    if ($depth === 0) {
                        $closeParen = $k;
                        break;
                    }
                }
            }
            if ($closeParen === null) {
                continue;
            }

            $argTokens = array_slice($tokens, $j + 1, $closeParen - $j - 1);
            foreach (self::splitTopLevelTokens($argTokens) as $argGroup) {
                $reg = self::parseRegisterHooksArg($argGroup);
                if ($reg !== null) {
                    $registrations[] = $reg;
                }
            }

            $i = $closeParen; // skip past this call; keep scanning for more
        }

        return $registrations;
    }

    /**
     * Parses one fn_register_hooks() argument (already split on top-level
     * commas) into ['hook' => name, 'targetAddon' => override-or-null].
     * Handles: 'bare_hook_name', ['hook', priority], array('hook', priority),
     * and ['hook', priority, 'other_addon'] (the callback-prefix override).
     *
     * Pure token parsing, no instance state needed - stays a private static.
     */
    private static function parseRegisterHooksArg($tokens) {
        // strip leading whitespace/comments
        $tokens = array_values(array_filter($tokens, function ($t) {
            return !(is_array($t) && in_array($t[0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true));
        }));
        if (empty($tokens)) {
            return null;
        }

        $idx = 0;
        if (is_array($tokens[0]) && $tokens[0][0] === T_ARRAY) {
            $idx = 1; // skip the "array" keyword of old-style array(...)
        }

        if (isset($tokens[$idx]) && ($tokens[$idx] === '(' || $tokens[$idx] === '[')) {
            // array literal - collect its interior tokens (depth-tracked)
            $depth = 0;
            $interior = [];
            for ($p = $idx; $p < count($tokens); $p++) {
                $t = $tokens[$p];
                if ($t === '(' || $t === '[') {
                    $depth++;
                    if ($depth === 1) {
                        continue;
                    }
                }
                if ($t === ')' || $t === ']') {
                    $depth--;
                    if ($depth === 0) {
                        break;
                    }
                }
                $interior[] = $t;
            }
            $elements = self::splitTopLevelTokens($interior);

            $hookName = self::firstStringLiteral(isset($elements[0]) ? $elements[0] : []);
            if ($hookName === null) {
                return null;
            }

            $targetAddon = null;
            if (isset($elements[2])) {
                $override = self::firstStringLiteral($elements[2]);
                if ($override !== null && $override !== '') {
                    $targetAddon = $override; // the actual callback-prefix override
                }
            }

            return ['hook' => $hookName, 'targetAddon' => $targetAddon];
        }

        // bare string literal
        $hookName = self::firstStringLiteral($tokens);
        if ($hookName === null) {
            return null;
        }
        return ['hook' => $hookName, 'targetAddon' => null];
    }

    /** Splits a flat (whitespace/comment-free) token list on top-level commas
     *  (i.e. not inside a nested (), [], {}). */
    private static function splitTopLevelTokens($tokens) {
        $groups = [];
        $current = [];
        $depth = 0;
        foreach ($tokens as $tok) {
            if ($tok === '(' || $tok === '[' || $tok === '{') {
                $depth++;
                $current[] = $tok;
            } elseif ($tok === ')' || $tok === ']' || $tok === '}') {
                $depth--;
                $current[] = $tok;
            } elseif ($tok === ',' && $depth === 0) {
                $groups[] = $current;
                $current = [];
            } else {
                $current[] = $tok;
            }
        }
        if (!empty($current)) {
            $groups[] = $current;
        }
        return $groups;
    }

    /** Pulls the first plain string literal out of a token list, or null. */
    private static function firstStringLiteral($tokens) {
        foreach ($tokens as $t) {
            if (is_array($t) && $t[0] === T_CONSTANT_ENCAPSED_STRING) {
                return trim($t[1], "'\"");
            }
        }
        return null;
    }
}
<?php
namespace Tygh\Slx\Tools\FnHookExplorer;

trait FindsPhpFilesTrait {

    /** @return string[] */
    protected function findPhpFiles($dir) {
        if (!is_dir($dir)) {
            return [];
        }
        $files = [];
        $rii = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS)
        );
        foreach ($rii as $file) {
            if ($file->isFile() && substr($file->getFilename(), -4) === '.php') {
                $files[] = $file->getPathname();
            }
        }
        return $files;
    }

    /** Given an absolute file path, returns the addon ID it lives under
     *  (the folder directly inside app/addons/), or null if the path
     *  isn't under app/addons at all (core function, app/Tygh, etc.). */
    protected function extractAddonNameFromPath($root, $file) {
        $addonsDir = str_replace('\\', '/', rtrim($root, '/\\') . '/app/addons/');
        $normalizedFile = str_replace('\\', '/', $file);
        if (strpos($normalizedFile, $addonsDir) !== 0) {
            return null;
        }
        $rest = substr($normalizedFile, strlen($addonsDir));
        $parts = explode('/', $rest, 2);
        return $parts[0];
    }
}
<?php
/**
 * Given a CS-Cart core function (or class method) name, this:
 *   1. Locates its definition under app/functions/, app/Tygh/, or any
 *      addon's own PHP files under app/addons - a function that calls
 *      fn_set_hook() isn't necessarily a core function; addons chain
 *      hooks off other addons' hooks constantly. (DefinitionLocator)
 *   2. Lists every fn_set_hook() call inside its body, with line numbers.
 *      (HookBodyExtractor)
 *   3. For each hook name, scans app/addons/* for handler functions
 *      (fn_<addon>_<hook_name>) and checks whether each addon actually
 *      registers that hook via fn_register_hooks() in its init.php - an
 *      unregistered handler silently never runs. (AddonRegistrationResolver)
 *   4. ALSO checks the two naming-convention handler mechanisms that
 *      fn_set_hook() itself resolves before it ever looks at the
 *      registered-hooks list, and that need no fn_register_hooks() call
 *      at all: fn_core_<hook_name>() (always fires if it exists) and
 *      fn_<edition_acronym>_<hook_name>() (fires only if this install's
 *      PRODUCT_EDITION matches that acronym - pro/mve/ult).
 *      (CoreEditionHandlerResolver)
 *   5. ALSO checks the modern OOP mechanism used by v4-scheme addons -
 *      addon.xml's <bootstrap> class returns a hook -> [service, method]
 *      map instead of calling fn_register_hooks(). This feeds into the
 *      SAME underlying hook registry, just via container-service lookup
 *      rather than a fn_<addon>_<hook> function name.
 *      (OopHookHandlerResolver)
 *
 * This class is the orchestrator: one instance per (cart root, active
 * addons) pair. Constructing it builds the four resolvers above, which
 * hold that state so it doesn't need to be re-passed on every call, and
 * so expensive per-installation data (the addon registration map, the
 * detected PRODUCT_EDITION) is computed at most once per instance and
 * reused across every function/hook looked up through it.
 *
 * Each resolver is also exposed directly (definitions(), registrations(),
 * coreEditionHandlers(), oopHandlers()) for callers who want just one piece rather than
 * the full analyze() pipeline.
 *
 * Pure static text/token analysis throughout - no Reflection, no CS-Cart
 * bootstrap.
 *
 * Accepts two input forms for the function/method being analyzed:
 *   fn_some_core_function       plain function (app/functions, app/Tygh,
 *                                or any addon's own PHP files)
 *   ClassName::methodName       class method under app/Tygh, resolved by
 *                                first finding the class declaration, then
 *                                locating the method strictly within that
 *                                class's own body. A fully-qualified
 *                                "Ns\Class::method" is also accepted.
 */

namespace Tygh\Slx\Tools\FnHookExplorer;

class FnHookExplorer {

    use FindsPhpFilesTrait;

    /** @var string */
    private $root;

    /** @var string[]|null */
    private $activeAddons;

    /** @var DefinitionLocator */
    private $definitionLocator;

    /** @var AddonRegistrationResolver */
    private $registrationResolver;

    /** @var CoreEditionHandlerResolver */
    private $coreEditionResolver;

    /** @var OopHookHandlerResolver */
    private $oopHandlerResolver;

    /**
     * @param string $root Absolute path to the CS-Cart installation (no trailing slash)
     * @param string[]|null $activeAddons List of addon IDs currently active on this
     *   installation (typically from `SELECT addon FROM {prefix}addons WHERE status='A'`).
     *   null disables all active-addon filtering, so results reflect what's present in
     *   the codebase regardless of what's actually enabled - this keeps the class usable
     *   standalone, without a DB connection.
     */
    public function __construct($root, array $activeAddons = null) {
        $this->root = $root;
        $this->activeAddons = $activeAddons;
        $this->definitionLocator = new DefinitionLocator($root);
        $this->registrationResolver = new AddonRegistrationResolver($root, $activeAddons);
        $this->coreEditionResolver = new CoreEditionHandlerResolver($root, $activeAddons);
        $this->oopHandlerResolver = new OopHookHandlerResolver($root, $activeAddons);
    }

    public function definitions() {
        return $this->definitionLocator;
    }

    public function registrations() {
        return $this->registrationResolver;
    }

    public function oopHandlers() {
        return $this->oopHandlerResolver;
    }

    public function coreEditionHandlers() {
        return $this->coreEditionResolver;
    }

    /** Convenience passthrough - equivalent to coreEditionHandlers()->getEditionAcronym(). */
    public function getEditionAcronym() {
        return $this->coreEditionResolver->getEditionAcronym();
    }

    /**
     * Resolves $functionOrMethod (either a bare function name or
     * "Class::method") and returns a fully structured result - no I/O, no
     * output formatting, so the caller (a Command, a test, whatever) can
     * render it however it likes.
     *
     * @param string $functionOrMethod e.g. "fn_get_cart_product_data" or "Shippings::groupProductsList"
     * @return array{
     *     displayName: string,
     *     error: string|null,
     *     defs: array
     * }
     */
    public function analyze($functionOrMethod) {
        $displayName = $functionOrMethod;

        if (strpos($functionOrMethod, '::') !== false) {
            list($rawClassName, $searchName) = explode('::', $functionOrMethod, 2);
            $classParts = explode('\\', $rawClassName);
            $className = end($classParts);

            $rawDefs = [];
            foreach ($this->definitionLocator->locateClassDeclarations($className) as $classDecl) {
                $found = $this->definitionLocator->findMethodInClass($classDecl['file'], $className, $searchName);
                if ($found !== null) {
                    $rawDefs[] = ['file' => $found['file'], 'line' => $found['line'], 'anchorLine' => $found['line']];
                }
            }

            if (empty($rawDefs)) {
                return [
                    'displayName' => $displayName,
                    'error' => sprintf(
                        "Could not find %s::%s() - class not found under app/Tygh, or the method isn't a direct member of it.",
                        $className, $searchName
                    ),
                    'defs' => [],
                ];
            }
        } else {
            $searchName = $functionOrMethod;
            $found = $this->definitionLocator->locateFunction($functionOrMethod);
            if (empty($found)) {
                return [
                    'displayName' => $displayName,
                    'error' => sprintf(
                        "Could not find a definition of %s() under app/functions, app/Tygh, or app/addons.",
                        $functionOrMethod
                    ),
                    'defs' => [],
                ];
            }
            $rawDefs = array_map(function ($d) {
                $d['anchorLine'] = null;
                return $d;
            }, $found);
        }

        $defs = [];
        foreach ($rawDefs as $def) {
            $sourceAddon = $this->extractAddonNameFromPath($this->root, $def['file']);
            $entry = [
                'file' => $def['file'],
                'line' => $def['line'],
                'abstract' => false,
                'error' => null,
                'hooks' => [],
                // null when not an addon file (core function) or when $activeAddons
                // wasn't supplied (no DB info available) - true/false only when known
                'sourceAddon' => $sourceAddon,
                'addonActive' => ($sourceAddon === null || $this->activeAddons === null)
                    ? null
                    : in_array($sourceAddon, $this->activeAddons, true),
            ];

            $result = HookBodyExtractor::extract($def['file'], $searchName, $def['anchorLine']);
            if (!$result['found']) {
                $entry['error'] = 'could not re-locate token position for body extraction';
                $defs[] = $entry;
                continue;
            }
            if (!empty($result['abstract'])) {
                $entry['abstract'] = true;
                $defs[] = $entry;
                continue;
            }

            $hooks = $result['hooks'];
            if (empty($hooks)) {
                $defs[] = $entry;
                continue;
            }

            // one pass over app/addons for every hook this function calls,
            // instead of re-scanning the whole addon tree per hook
            $hookNames = array_values(array_unique(array_map(function ($h) {
                return $h['name'];
            }, $hooks)));
            $allHandlers = $this->registrationResolver->findHandlers($hookNames);

            // fn_core_<hook>() and fn_<edition_acronym>_<hook>() fire purely by naming
            // convention - fn_set_hook() checks is_callable()/function_exists() on them
            // directly, with NO fn_register_hooks() call involved at all.
            $coreEditionHandlers = $this->coreEditionResolver->findHandlers($hookNames);

            // the modern OOP mechanism (v4-scheme addons, addon.xml -> Bootstrap.php ->
            // getHookHandlerMap()) feeds into the SAME hook registry as fn_register_hooks(),
            // just via a completely different, non-fn_-named callback resolution path.
            $oopHandlers = $this->oopHandlerResolver->findHandlers($hookNames);

            foreach ($hooks as $h) {
                $entry['hooks'][] = [
                    'name' => $h['name'],
                    'line' => $h['line'],
                    'handlers' => isset($allHandlers[$h['name']]) ? $allHandlers[$h['name']] : [],
                    'coreEditionHandlers' => isset($coreEditionHandlers[$h['name']]) ? $coreEditionHandlers[$h['name']] : [],
                    'oopHandlers' => isset($oopHandlers[$h['name']]) ? $oopHandlers[$h['name']] : [],
                ];
            }

            $defs[] = $entry;
        }

        return [
            'displayName' => $displayName,
            'error' => null,
            'defs' => $defs,
        ];
    }
}
<?php
/**
 * Step 2: given a file and a function/method name already known to be
 * defined there, walks its token stream to extract every fn_set_hook()
 * call inside its body, with line numbers. Pure function of its inputs -
 * no shared state, so this stays static rather than becoming an object.
 */

namespace Tygh\Slx\Tools\FnHookExplorer;

class HookBodyExtractor {

    /** If $anchorLine is given, only the definition AT that exact line is
     *  accepted (disambiguates same-named methods in different classes,
     *  paired with DefinitionLocator::findMethodInClass()'s result). */
    public static function extract($file, $functionName, $anchorLine = null) {
        $source = file_get_contents($file);
        if ($source === false) {
            return ['found' => false];
        }
        $tokens = token_get_all($source);
        $n = count($tokens);

        $currentLine = 1;
        $defTokenIndex = null;

        for ($i = 0; $i < $n; $i++) {
            $tok = $tokens[$i];
            if (is_array($tok)) {
                $currentLine = $tok[2];
                if ($tok[0] === T_STRING && $tok[1] === $functionName) {
                    // confirm preceding non-trivial token is T_FUNCTION
                    $j = $i - 1;
                    while ($j >= 0 && is_array($tokens[$j])
                        && in_array($tokens[$j][0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true)) {
                        $j--;
                    }
                    if ($j >= 0 && is_array($tokens[$j]) && $tokens[$j][0] === T_FUNCTION) {
                        if ($anchorLine === null || $currentLine === $anchorLine) {
                            $defTokenIndex = $i;
                            break;
                        }
                    }
                }
            }
        }

        if ($defTokenIndex === null) {
            return ['found' => false];
        }
        $defLine = $currentLine;

        // scan forward for the opening '{' of the body
        $braceStart = null;
        for ($k = $defTokenIndex; $k < $n; $k++) {
            $tok = $tokens[$k];
            if (is_array($tok)) {
                $currentLine = $tok[2];
            } elseif ($tok === '{') {
                $braceStart = $k;
                break;
            } elseif ($tok === ';') {
                // abstract/interface method - no body
                return ['found' => true, 'defLine' => $defLine, 'abstract' => true, 'hooks' => []];
            }
        }
        if ($braceStart === null) {
            return ['found' => true, 'defLine' => $defLine, 'hooks' => [], 'error' => 'body not found'];
        }

        // walk to the matching closing brace, collecting fn_set_hook() calls
        $depth = 0;
        $hooks = [];
        for ($k = $braceStart; $k < $n; $k++) {
            $tok = $tokens[$k];
            if (is_array($tok)) {
                $currentLine = $tok[2];
                if ($tok[0] === T_STRING && $tok[1] === 'fn_set_hook') {
                    $m = $k + 1;
                    while ($m < $n && is_array($tokens[$m])
                        && in_array($tokens[$m][0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true)) {
                        $m++;
                    }
                    if ($m < $n && $tokens[$m] === '(') {
                        $m++;
                        while ($m < $n && is_array($tokens[$m])
                            && in_array($tokens[$m][0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true)) {
                            $m++;
                        }
                        if ($m < $n && is_array($tokens[$m]) && $tokens[$m][0] === T_CONSTANT_ENCAPSED_STRING) {
                            $hookName = trim($tokens[$m][1], "'\"");
                            $hooks[] = ['name' => $hookName, 'line' => $currentLine];
                        }
                    }
                }
            } elseif ($tok === '{') {
                $depth++;
            } elseif ($tok === '}') {
                $depth--;
                if ($depth === 0) {
                    break;
                }
            }
        }

        return ['found' => true, 'defLine' => $defLine, 'hooks' => $hooks];
    }
}
<?php
/**
 * Step 4: fn_core_<hook>() and fn_<edition_acronym>_<hook>() - two
 * handler conventions that fn_set_hook() checks BEFORE it even looks
 * at the registered-hooks list, and that need NO fn_register_hooks()
 * call at all:
 *
 *   if (is_callable('fn_core_' . $hook_name)) { ... }
 *   if ($edition_acronym !== false && function_exists("fn_{$edition_acronym}_{$hook_name}")) { ... }
 *
 * (straight from fn_set_hook()'s own source in app/functions/fn.control.php)
 *
 * fn_core_<hook>() always fires if it exists. fn_<acronym>_<hook>()
 * only fires if this install's own PRODUCT_EDITION resolves to that
 * acronym - PROFESSIONAL=pro, MULTIVENDOR=mve, ULTIMATE=ult (the same
 * table fn_get_edition_acronym() uses). Either can technically live
 * inside an addon's file, in which case it's still gated by that
 * addon being active - an inactive addon's file simply never loads,
 * so the function was never defined in the first place.
 *
 * Instantiated per (root, activeAddons) pair so PRODUCT_EDITION is read
 * from config.php at most once and reused, instead of the file being
 * re-read and re-regexed on every lookup.
 */

namespace Tygh\Slx\Tools\FnHookExplorer;

class CoreEditionHandlerResolver {

    use FindsPhpFilesTrait;

    /** @var string */
    private $root;

    /** @var string[]|null */
    private $activeAddons;

    /** @var string|null Memoized once computed - see getEditionAcronym(). */
    private $editionAcronym;

    /** @var bool Whether editionAcronym has been computed yet - null is a
     *  valid resolved value (undetermined), so this can't rely on ===null. */
    private $editionAcronymComputed = false;

    /**
     * @param string $root
     * @param string[]|null $activeAddons Same semantics as AddonRegistrationResolver.
     */
    public function __construct($root, array $activeAddons = null) {
        $this->root = $root;
        $this->activeAddons = $activeAddons;
    }

    /** Reads PRODUCT_EDITION straight out of config.php (plain define(),
     *  no bootstrap needed) and maps it to its acronym, or null if it
     *  can't be determined. Memoized after the first call. */
    public function getEditionAcronym() {
        if (!$this->editionAcronymComputed) {
            $this->editionAcronym = $this->computeEditionAcronym();
            $this->editionAcronymComputed = true;
        }
        return $this->editionAcronym;
    }

    private function computeEditionAcronym() {
        $configFile = rtrim($this->root, '/\\') . '/config.php';
        if (!file_exists($configFile)) {
            return null;
        }
        $content = file_get_contents($configFile);
        if ($content === false) {
            return null;
        }
        if (!preg_match('/define\s*\(\s*[\'"]PRODUCT_EDITION[\'"]\s*,\s*[\'"](\w+)[\'"]\s*\)/', $content, $m)) {
            return null;
        }
        $editionMap = [
            'PROFESSIONAL' => 'pro',
            'MULTIVENDOR'  => 'mve',
            'ULTIMATE'     => 'ult',
        ];
        return isset($editionMap[$m[1]]) ? $editionMap[$m[1]] : null;
    }

    /**
     * @param string[] $hookNames
     * @return array hookName => list of handler match records
     */
    public function findHandlers(array $hookNames) {
        $results = array_fill_keys($hookNames, []);
        if (empty($hookNames)) {
            return $results;
        }

        $editionAcronym = $this->getEditionAcronym();

        $kinds = ['core'];
        if ($editionAcronym !== null) {
            $kinds[] = $editionAcronym;
        } else {
            // undetermined - check all three possible acronyms as candidates,
            // each flagged "can't confirm" rather than silently skipped
            $kinds = array_merge($kinds, ['pro', 'mve', 'ult']);
        }

        $escapedHooks = array_map(function ($h) {
            return preg_quote($h, '/');
        }, $hookNames);
        $escapedKinds = array_map(function ($k) {
            return preg_quote($k, '/');
        }, $kinds);
        $pattern = '/\bfunction\s+(fn_(' . implode('|', $escapedKinds) . ')_(' . implode('|', $escapedHooks) . '))\s*\(/';

        $searchDirs = [
            $this->root . '/app/functions',
            $this->root . '/app/Tygh',
            $this->root . '/app/addons',
        ];

        foreach ($searchDirs as $dir) {
            foreach ($this->findPhpFiles($dir) as $file) {
                $lines = file($file);
                if ($lines === false) {
                    continue;
                }
                foreach ($lines as $i => $line) {
                    if (preg_match($pattern, $line, $m)) {
                        $matchedKind = $m[2];
                        $matchedHook = $m[3];
                        $addonName = $this->extractAddonNameFromPath($this->root, $file);
                        $classification = $this->classifyHandler($matchedKind, $editionAcronym, $addonName);
                        $results[$matchedHook][] = [
                            'kind'     => $matchedKind, // 'core' or an edition acronym
                            'function' => $m[1],
                            'file'     => $file,
                            'line'     => $i + 1,
                            'addon'    => $addonName, // null when defined in core code, not an addon
                            'fires'    => $classification['fires'], // true / false / null (unknown)
                            'reason'   => $classification['reason'],
                        ];
                    }
                }
            }
        }
        return $results;
    }

    /** Decides whether a found fn_core_/fn_<acronym>_ handler would
     *  actually fire on this specific installation, and why (or why not). */
    private function classifyHandler($kind, $editionAcronym, $addonName) {
        // first: is this edition-tier handler even for the right edition?
        if ($kind !== 'core') {
            if ($editionAcronym === null) {
                $editionKnown = null; // we don't know this install's edition at all
            } else {
                $editionKnown = ($kind === $editionAcronym);
            }
            if ($editionKnown === false) {
                return ['fires' => false, 'reason' => "for a different edition ('{$kind}') than this install runs"];
            }
        } else {
            $editionKnown = true; // 'core' has no edition condition
        }

        // second: if it's physically defined inside an addon, that addon
        // has to actually be active for its file (and this function) to load
        if ($addonName !== null) {
            if ($this->activeAddons === null) {
                $addonKnown = null;
            } else {
                $addonKnown = in_array($addonName, $this->activeAddons, true);
            }
            if ($addonKnown === false) {
                return ['fires' => false, 'reason' => "defined in addon '{$addonName}', which is not active"];
            }
            if ($addonKnown === null || $editionKnown === null) {
                return ['fires' => null, 'reason' => "can't confirm - addon '{$addonName}' active status unknown"];
            }
            return ['fires' => true, 'reason' => "defined in active addon '{$addonName}'"];
        }

        if ($editionKnown === null) {
            return ['fires' => null, 'reason' => "can't confirm - this install's PRODUCT_EDITION is unknown"];
        }

        $reason = ($kind === 'core')
            ? 'core handler - always fires regardless of addon status or edition'
            : "matches this install's edition ('{$kind}')";
        return ['fires' => true, 'reason' => $reason];
    }
}
<?php
/**
 * Step 1: locates where a plain function or class method is defined.
 * Pure regex/token text search - no Reflection, no CS-Cart bootstrap.
 */

namespace Tygh\Slx\Tools\FnHookExplorer;

class DefinitionLocator {

    use FindsPhpFilesTrait;

    /** @var string */
    private $root;

    public function __construct($root) {
        $this->root = $root;
    }

    /** Locates a plain function's definition across app/functions, app/Tygh,
     *  and any addon's own PHP files - a function calling fn_set_hook()
     *  isn't necessarily a core function. */
    public function locateFunction($functionName) {
        $searchDirs = [
            $this->root . '/app/functions',
            $this->root . '/app/Tygh',
            $this->root . '/app/addons',
        ];

        $pattern = '/\bfunction\s+' . preg_quote($functionName, '/') . '\s*\(/';

        $matches = [];
        foreach ($searchDirs as $dir) {
            foreach ($this->findPhpFiles($dir) as $file) {
                $lines = file($file);
                if ($lines === false) {
                    continue;
                }
                foreach ($lines as $i => $line) {
                    if (preg_match($pattern, $line)) {
                        $matches[] = ['file' => $file, 'line' => $i + 1];
                    }
                }
            }
        }
        return $matches;
    }

    /** Class::method lookup, step 1 of 2 - finds candidate class declarations
     *  under app/Tygh. Step 2 is findMethodInClass(), scoped to one file. */
    public function locateClassDeclarations($className) {
        $dir = $this->root . '/app/Tygh';
        $pattern = '/\bclass\s+' . preg_quote($className, '/') . '\b/';

        $matches = [];
        foreach ($this->findPhpFiles($dir) as $file) {
            $lines = file($file);
            if ($lines === false) {
                continue;
            }
            foreach ($lines as $i => $line) {
                if (preg_match($pattern, $line)) {
                    $matches[] = ['file' => $file, 'line' => $i + 1];
                }
            }
        }
        return $matches;
    }

    /** Finds a method's definition line strictly within the named class's
     *  own body (depth 1 relative to the class's opening brace), so a
     *  same-named method in a different class, or a same-named nested
     *  closure, can't be mismatched. Doesn't need $this->root - $file is
     *  already an absolute path from locateClassDeclarations(). */
    public function findMethodInClass($file, $className, $methodName) {
        $source = file_get_contents($file);
        if ($source === false) {
            return null;
        }
        $tokens = token_get_all($source);
        $n = count($tokens);
        $currentLine = 1;

        // find "class ClassName" token sequence
        $classNameTokenIndex = null;
        for ($i = 0; $i < $n; $i++) {
            $tok = $tokens[$i];
            if (is_array($tok)) {
                $currentLine = $tok[2];
                if ($tok[0] === T_CLASS) {
                    $j = $i + 1;
                    while ($j < $n && is_array($tokens[$j])
                        && in_array($tokens[$j][0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true)) {
                        $j++;
                    }
                    if ($j < $n && is_array($tokens[$j]) && $tokens[$j][0] === T_STRING
                        && $tokens[$j][1] === $className) {
                        $classNameTokenIndex = $j;
                        break;
                    }
                }
            }
        }
        if ($classNameTokenIndex === null) {
            return null;
        }

        // scan forward for the class body's opening brace (skips extends/implements)
        $braceStart = null;
        for ($k = $classNameTokenIndex; $k < $n; $k++) {
            $tok = $tokens[$k];
            if (is_array($tok)) {
                $currentLine = $tok[2];
            } elseif ($tok === '{') {
                $braceStart = $k;
                break;
            }
        }
        if ($braceStart === null) {
            return null;
        }

        // walk the class body; only accept a method match at depth 1
        // (i.e. a direct member, not a nested closure/anonymous class)
        $depth = 0;
        $methodDefLine = null;
        for ($k = $braceStart; $k < $n; $k++) {
            $tok = $tokens[$k];
            if (is_array($tok)) {
                $currentLine = $tok[2];
                if ($depth === 1 && $tok[0] === T_STRING && $tok[1] === $methodName) {
                    $j = $k - 1;
                    while ($j >= 0 && is_array($tokens[$j])
                        && in_array($tokens[$j][0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true)) {
                        $j--;
                    }
                    if ($j >= 0 && is_array($tokens[$j]) && $tokens[$j][0] === T_FUNCTION) {
                        $methodDefLine = $currentLine;
                    }
                }
            } elseif ($tok === '{') {
                $depth++;
            } elseif ($tok === '}') {
                $depth--;
                if ($depth === 0) {
                    break; // end of class body
                }
            }
        }

        if ($methodDefLine === null) {
            return null;
        }
        return ['file' => $file, 'line' => $methodDefLine];
    }
}
<?php
/**
 * Step 5: resolves hook handlers registered the modern OOP way, used by
 * v4-scheme addons instead of fn_register_hooks().
 *
 * Mechanism (verified against real CS-Cart source):
 *   - app/addons/<addon>/addon.xml has <addon scheme="4.0"> and a
 *     <bootstrap> tag pointing at a class implementing HookHandlerProviderInterface.
 *   - That class's getHookHandlerMap() returns hookName => [serviceId, method, priority?, addonOverride?]
 *     (or an explicit ['hook'=>..,'handler'=>[serviceId,method],'priority'=>..,'addon'=>..] form,
 *     used when the SAME real hook needs several addon-override entries - PHP array keys must be
 *     unique, so the outer map key becomes a synthetic disambiguator and the real hook name moves
 *     into the 'hook' sub-key instead).
 *   - app/Tygh/Addons/Loader.php::registerHooksFromHookProvider() normalizes both forms and feeds
 *     them into the SAME Registry::get('hooks') structure fn_register_hooks() also populates - so
 *     this isn't a separate dispatch system, just a different way of populating the same table.
 *     Its two activity checks are mirrored exactly here: the declaring (base) addon must be active,
 *     and if there's an addon override, THAT addon must also be active.
 *   - serviceId (e.g. 'addons.product_variations.hook_handlers.products') is a DI container key,
 *     not a class name - resolving it means finding the addon's ServiceProvider (referenced via
 *     $app->register(new X()) in the Bootstrap's boot() method) and locating its
 *     $app['<serviceId>'] = function() { return new SomeClass(...); }; assignment.
 *
 * Resolution is best-effort: the ServiceProvider pattern above is followed reliably in every real
 * example seen, but isn't guaranteed by any hard rule. If it fails, the method name itself - always
 * directly readable from the Bootstrap map with zero ambiguity - is used as a fallback: search the
 * whole addon for a method with that exact name, which in practice is unique to one hook handler.
 *
 * Closures in the map (legal per Loader.php, rare in practice) can't be statically resolved to a
 * file/line and are flagged as such rather than silently skipped.
 */

namespace Tygh\Slx\Tools\FnHookExplorer;

class OopHookHandlerResolver {

    use FindsPhpFilesTrait;

    /** Sentinel marking a map value that's a Closure (or otherwise not a
     *  plain array/string/number we can statically resolve). */
    const UNRESOLVABLE = '__unresolvable__';

    /** @var string */
    private $root;

    /** @var string[]|null */
    private $activeAddons;

    /** @var DefinitionLocator */
    private $definitionLocator;

    /**
     * @param string $root
     * @param string[]|null $activeAddons Same semantics as the other resolvers.
     */
    public function __construct($root, array $activeAddons = null) {
        $this->root = $root;
        $this->activeAddons = $activeAddons;
        $this->definitionLocator = new DefinitionLocator($root);
    }

    /**
     * @param string[] $hookNames
     * @return array hookName => list of handler match records
     */
    public function findHandlers(array $hookNames) {
        $results = array_fill_keys($hookNames, []);
        $addonsDir = $this->root . '/app/addons';
        if (!is_dir($addonsDir) || empty($hookNames)) {
            return $results;
        }

        foreach (scandir($addonsDir) as $addonName) {
            if ($addonName === '.' || $addonName === '..') {
                continue;
            }
            $addonPath = $addonsDir . '/' . $addonName;
            if (!is_dir($addonPath)) {
                continue;
            }

            $xmlFile = $addonPath . '/addon.xml';
            if (!file_exists($xmlFile)) {
                continue;
            }

            $parsed = $this->parseAddonXml($xmlFile);
            if ($parsed === null) {
                continue; // not a v4-scheme addon, or no usable <bootstrap>
            }

            $bootstrapFile = $this->resolveFqcnToFile($parsed['bootstrapClass'], $parsed['psr4'], $addonPath);
            if ($bootstrapFile === null || !file_exists($bootstrapFile)) {
                continue;
            }

            $map = $this->getHookHandlerMapFromBootstrap($bootstrapFile);
            if (!is_array($map)) {
                continue; // no getHookHandlerMap() found, or its body wasn't a parseable array literal
            }

            foreach ($this->normalizeHookEntries($map) as $entry) {
                if (!in_array($entry['hookName'], $hookNames, true)) {
                    continue;
                }

                $results[$entry['hookName']][] = $this->buildRecord($entry, $addonName, $addonPath, $bootstrapFile, $parsed['psr4']);
            }
        }

        return $results;
    }

    private function buildRecord($entry, $baseAddon, $addonPath, $bootstrapFile, array $psr4Map) {
        $classification = $this->classifyEntry($baseAddon, $entry['addon']);

        $record = [
            'baseAddon'      => $baseAddon,
            'effectiveAddon' => $entry['addon'] !== null ? $entry['addon'] : $baseAddon,
            'priority'       => $entry['priority'],
            'fires'          => $classification['fires'],
            'reason'         => $classification['reason'],
            'unresolvable'   => false,
            'serviceId'      => null,
            'method'         => null,
            'file'           => null,
            'line'           => null,
        ];

        if ($entry['handler'] === self::UNRESOLVABLE) {
            $record['unresolvable'] = true;
            $record['reason'] .= ' (handler is a Closure - cannot statically resolve which code runs)';
            return $record;
        }

        list($serviceId, $method) = $entry['handler'];
        $record['serviceId'] = $serviceId;
        $record['method'] = $method;

        // primary strategy: resolve the service id to a real class via ServiceProvider
        foreach ($this->findRegisteredProviderClassNames($bootstrapFile) as $candidateClassName) {
            $candidateFqcn = $this->resolveClassNameToFqcn($bootstrapFile, $candidateClassName);
            $candidateFile = $this->resolveFqcnToFile($candidateFqcn, $psr4Map, $addonPath);
            if ($candidateFile === null || !file_exists($candidateFile)) {
                continue;
            }

            $handlerClassName = $this->findServiceAssignmentClass($candidateFile, $serviceId);
            if ($handlerClassName === null) {
                continue;
            }

            $handlerFqcn = $this->resolveClassNameToFqcn($candidateFile, $handlerClassName);
            $handlerFile = $this->resolveFqcnToFile($handlerFqcn, $psr4Map, $addonPath);
            if ($handlerFile === null || !file_exists($handlerFile)) {
                continue;
            }

            $shortClassName = $handlerClassName;
            $lastSep = strrpos($shortClassName, '\\');
            if ($lastSep !== false) {
                $shortClassName = substr($shortClassName, $lastSep + 1);
            }

            $found = $this->definitionLocator->findMethodInClass($handlerFile, $shortClassName, $method);
            if ($found !== null) {
                $record['file'] = $found['file'];
                $record['line'] = $found['line'];
                return $record;
            }
        }

        // fallback: the method name is directly known from the map with zero ambiguity,
        // even when precise service resolution fails - search the whole addon for it
        $fallback = $this->searchAddonForMethod($addonPath, $method);
        if ($fallback !== null) {
            $record['file'] = $fallback['file'];
            $record['line'] = $fallback['line'];
            $record['reason'] .= ' (method located via addon-wide search, not precise service resolution)';
        }

        return $record;
    }

    /** Mirrors Loader.php's own two-layer activity check exactly: the declaring
     *  (base) addon must be active for its Bootstrap.php to even run, and if
     *  there's an addon override, that addon must ALSO be active. */
    private function classifyEntry($baseAddon, $overrideAddon) {
        if ($this->activeAddons === null) {
            return ['fires' => null, 'reason' => "can't confirm - active-addon status unknown"];
        }

        if (!in_array($baseAddon, $this->activeAddons, true)) {
            return ['fires' => false, 'reason' => "declaring addon '{$baseAddon}' is not active"];
        }

        if ($overrideAddon !== null) {
            if (!in_array($overrideAddon, $this->activeAddons, true)) {
                return ['fires' => false, 'reason' => "registered under addon '{$overrideAddon}', which is not active"];
            }
            return ['fires' => true, 'reason' => "registered under active addon '{$overrideAddon}' (declared in '{$baseAddon}')"];
        }

        return ['fires' => true, 'reason' => "declared in active addon '{$baseAddon}'"];
    }

    // ---------------------------------------------------------------------
    // addon.xml parsing
    // ---------------------------------------------------------------------

    /** @return array{bootstrapClass: string, psr4: array}|null */
    private function parseAddonXml($xmlFile) {
        $content = @file_get_contents($xmlFile);
        if ($content === false) {
            return null;
        }
        $prevSetting = libxml_use_internal_errors(true);
        $xml = simplexml_load_string($content);
        libxml_use_internal_errors($prevSetting);
        if ($xml === false) {
            return null;
        }

        $scheme = (string) $xml['scheme'];
        if ($scheme !== '4.0') {
            return null;
        }

        $bootstrapClass = isset($xml->bootstrap) ? trim((string) $xml->bootstrap) : '';
        if ($bootstrapClass === '') {
            return null;
        }
        $bootstrapClass = ltrim($bootstrapClass, '\\');

        $psr4Map = [];
        if (isset($xml->autoload->psr4)) {
            foreach ($xml->autoload->psr4 as $psr4Node) {
                $prefix = (string) $psr4Node['prefix'];
                $dir = trim((string) $psr4Node);
                if ($prefix !== '' && $dir !== '') {
                    $psr4Map[$prefix] = $dir;
                }
            }
        }

        return ['bootstrapClass' => $bootstrapClass, 'psr4' => $psr4Map];
    }

    // ---------------------------------------------------------------------
    // PSR-4 FQCN <-> file path resolution
    // ---------------------------------------------------------------------

    private function resolveFqcnToFile($fqcn, array $psr4Map, $addonDir) {
        $fqcn = ltrim($fqcn, '\\');
        $bestPrefixLen = -1;
        $bestDir = null;
        foreach ($psr4Map as $prefix => $dir) {
            $normalizedPrefix = trim($prefix, '\\') . '\\';
            if (strpos($fqcn . '\\', $normalizedPrefix) === 0 && strlen($normalizedPrefix) > $bestPrefixLen) {
                $bestPrefixLen = strlen($normalizedPrefix);
                $bestDir = $dir;
                $bestPrefix = $normalizedPrefix;
            }
        }
        if ($bestDir === null) {
            return null;
        }
        $remaining = ltrim(substr($fqcn, strlen(rtrim($bestPrefix, '\\'))), '\\');
        $relativePath = str_replace('\\', '/', $remaining) . '.php';
        return rtrim($addonDir, '/\\') . '/' . trim($bestDir, '/\\') . '/' . $relativePath;
    }

    /** Token types that can appear as part of a class/namespace identifier
     *  path. PHP 8.0+ tokenizes a multi-segment path like "Foo\Bar\Baz" as
     *  ONE T_NAME_QUALIFIED token (T_NAME_FULLY_QUALIFIED if it starts with
     *  a leading backslash); PHP <8.0 tokenizes it as separate T_STRING and
     *  T_NS_SEPARATOR tokens. Supporting both keeps this correct regardless
     *  of which PHP version actually runs the tool. */
    private static function identifierTokenTypes() {
        static $types = null;
        if ($types === null) {
            $types = [T_STRING, T_NS_SEPARATOR];
            if (defined('T_NAME_QUALIFIED')) {
                $types[] = T_NAME_QUALIFIED;
            }
            if (defined('T_NAME_FULLY_QUALIFIED')) {
                $types[] = T_NAME_FULLY_QUALIFIED;
            }
            if (defined('T_NAME_RELATIVE')) {
                $types[] = T_NAME_RELATIVE;
            }
        }
        return $types;
    }

    /** Resolves a (possibly short/aliased) class name referenced inside $file
     *  to a fully-qualified name, using that file's own namespace + use imports. */
    private function resolveClassNameToFqcn($file, $className) {
        if (strpos($className, '\\') === 0) {
            return ltrim($className, '\\');
        }

        $source = file_get_contents($file);
        if ($source === false) {
            return $className;
        }
        $tokens = token_get_all($source);
        $n = count($tokens);

        $namespace = null;
        $uses = [];

        for ($i = 0; $i < $n; $i++) {
            $t = $tokens[$i];
            if (is_array($t) && $t[0] === T_NAMESPACE) {
                $j = $i + 1;
                $parts = [];
                while ($j < $n && $tokens[$j] !== ';' && $tokens[$j] !== '{') {
                    if (is_array($tokens[$j]) && in_array($tokens[$j][0], self::identifierTokenTypes(), true)) {
                        $parts[] = $tokens[$j][1];
                    }
                    $j++;
                }
                $namespace = implode('', $parts);
            } elseif (is_array($t) && $t[0] === T_USE) {
                $j = $i + 1;
                while ($j < $n && is_array($tokens[$j])
                    && in_array($tokens[$j][0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true)) {
                    $j++;
                }
                $parts = [];
                $alias = null;
                while ($j < $n && $tokens[$j] !== ';' && $tokens[$j] !== ',') {
                    if (is_array($tokens[$j]) && in_array($tokens[$j][0], self::identifierTokenTypes(), true)) {
                        $parts[] = $tokens[$j][1];
                    } elseif (is_array($tokens[$j]) && $tokens[$j][0] === T_AS) {
                        $j++;
                        while ($j < $n && is_array($tokens[$j])
                            && in_array($tokens[$j][0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true)) {
                            $j++;
                        }
                        if ($j < $n && is_array($tokens[$j]) && $tokens[$j][0] === T_STRING) {
                            $alias = $tokens[$j][1];
                        }
                        break;
                    }
                    $j++;
                }
                if (!empty($parts)) {
                    $fqcn = implode('', $parts);
                    $short = $alias !== null ? $alias : basename(str_replace('\\', '/', $fqcn));
                    $uses[$short] = $fqcn;
                }
            }
        }

        if (strpos($className, '\\') !== false) {
            $segments = explode('\\', $className);
            $first = $segments[0];
            if (isset($uses[$first])) {
                $segments[0] = $uses[$first];
                return implode('\\', $segments);
            }
            return ($namespace !== null ? $namespace . '\\' : '') . $className;
        }

        if (isset($uses[$className])) {
            return $uses[$className];
        }

        return ($namespace !== null ? $namespace . '\\' : '') . $className;
    }

    // ---------------------------------------------------------------------
    // getHookHandlerMap() body -> PHP-like array value, via tokens
    // ---------------------------------------------------------------------

    /** @return array|null */
    private function getHookHandlerMapFromBootstrap($file) {
        $source = file_get_contents($file);
        if ($source === false) {
            return null;
        }
        $tokens = token_get_all($source);
        $n = count($tokens);

        $defIndex = null;
        for ($i = 0; $i < $n; $i++) {
            $tok = $tokens[$i];
            if (is_array($tok) && $tok[0] === T_STRING && $tok[1] === 'getHookHandlerMap') {
                $j = $i - 1;
                while ($j >= 0 && is_array($tokens[$j])
                    && in_array($tokens[$j][0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true)) {
                    $j--;
                }
                if ($j >= 0 && is_array($tokens[$j]) && $tokens[$j][0] === T_FUNCTION) {
                    $defIndex = $i;
                    break;
                }
            }
        }
        if ($defIndex === null) {
            return null;
        }

        $braceStart = null;
        for ($k = $defIndex; $k < $n; $k++) {
            if ($tokens[$k] === '{') {
                $braceStart = $k;
                break;
            }
        }
        if ($braceStart === null) {
            return null;
        }

        $depth = 0;
        $bodyTokens = [];
        for ($k = $braceStart; $k < $n; $k++) {
            $t = $tokens[$k];
            if ($t === '{') {
                $depth++;
                if ($depth === 1) {
                    continue;
                }
            }
            if ($t === '}') {
                $depth--;
                if ($depth === 0) {
                    break;
                }
            }
            $bodyTokens[] = $t;
        }

        $returnIdx = null;
        foreach ($bodyTokens as $i => $t) {
            if (is_array($t) && $t[0] === T_RETURN) {
                $returnIdx = $i;
                break;
            }
        }
        if ($returnIdx === null) {
            return null;
        }

        $rest = array_slice($bodyTokens, $returnIdx + 1);
        $value = self::parseArrayValue($rest);
        return is_array($value) ? $value : null;
    }

    /** Recursively evaluates a PHP array-literal token sequence into a plain
     *  PHP value (nested arrays, strings, ints, null). Closures resolve to
     *  self::UNRESOLVABLE. Good enough for the Bootstrap hook map's shape -
     *  not a general PHP expression evaluator. */
    private static function parseArrayValue($tokens) {
        $tokens = self::stripTrivia($tokens);
        if (empty($tokens)) {
            return null;
        }

        $idx = 0;
        if (is_array($tokens[0]) && $tokens[0][0] === T_ARRAY) {
            $idx = 1; // old array(...) syntax
        }

        if (isset($tokens[$idx]) && ($tokens[$idx] === '[' || $tokens[$idx] === '(')) {
            $depth = 0;
            $interior = [];
            for ($p = $idx; $p < count($tokens); $p++) {
                $t = $tokens[$p];
                if ($t === '[' || $t === '(') {
                    $depth++;
                    if ($depth === 1) {
                        continue;
                    }
                }
                if ($t === ']' || $t === ')') {
                    $depth--;
                    if ($depth === 0) {
                        break;
                    }
                }
                $interior[] = $t;
            }
            $groups = self::splitTopLevelTokens($interior);
            $result = [];
            $autoIndex = 0;
            foreach ($groups as $group) {
                $group = self::stripTrivia($group);
                if (empty($group)) {
                    continue;
                }
                $arrowPos = self::findTopLevelArrow($group);
                if ($arrowPos !== null) {
                    $keyTokens = array_slice($group, 0, $arrowPos);
                    $valTokens = array_slice($group, $arrowPos + 1);
                    $key = self::firstStringLiteral($keyTokens);
                    $val = self::parseArrayValue($valTokens);
                    if ($key === null) {
                        $result[$autoIndex++] = $val;
                    } else {
                        $result[$key] = $val;
                    }
                } else {
                    $result[$autoIndex++] = self::parseArrayValue($group);
                }
            }
            return $result;
        }

        foreach ($tokens as $t) {
            if (is_array($t) && $t[0] === T_FUNCTION) {
                return self::UNRESOLVABLE;
            }
        }

        foreach ($tokens as $t) {
            if (is_array($t) && $t[0] === T_STRING && strtolower($t[1]) === 'null') {
                return null;
            }
        }

        $str = self::firstStringLiteral($tokens);
        if ($str !== null) {
            return $str;
        }

        foreach ($tokens as $t) {
            if (is_array($t) && $t[0] === T_LNUMBER) {
                return (int) $t[1];
            }
        }

        return null;
    }

    private static function stripTrivia($tokens) {
        return array_values(array_filter($tokens, function ($t) {
            return !(is_array($t) && in_array($t[0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true));
        }));
    }

    private static function splitTopLevelTokens($tokens) {
        $groups = [];
        $current = [];
        $depth = 0;
        foreach ($tokens as $tok) {
            if ($tok === '(' || $tok === '[' || $tok === '{') {
                $depth++;
                $current[] = $tok;
            } elseif ($tok === ')' || $tok === ']' || $tok === '}') {
                $depth--;
                $current[] = $tok;
            } elseif ($tok === ',' && $depth === 0) {
                $groups[] = $current;
                $current = [];
            } else {
                $current[] = $tok;
            }
        }
        if (!empty($current)) {
            $groups[] = $current;
        }
        return $groups;
    }

    private static function findTopLevelArrow($tokens) {
        $depth = 0;
        foreach ($tokens as $i => $t) {
            if ($t === '(' || $t === '[' || $t === '{') {
                $depth++;
                continue;
            }
            if ($t === ')' || $t === ']' || $t === '}') {
                $depth--;
                continue;
            }
            if ($depth === 0 && is_array($t) && $t[0] === T_DOUBLE_ARROW) {
                return $i;
            }
        }
        return null;
    }

    private static function firstStringLiteral($tokens) {
        foreach ($tokens as $t) {
            if (is_array($t) && $t[0] === T_CONSTANT_ENCAPSED_STRING) {
                return trim($t[1], "'\"");
            }
        }
        return null;
    }

    /** Normalizes both map entry shapes into a flat list, mirroring
     *  Loader::registerHooksFromHookProvider()'s own normalization exactly. */
    private function normalizeHookEntries($map) {
        $entries = [];
        foreach ($map as $key => $value) {
            if ($value === self::UNRESOLVABLE) {
                $entries[] = ['hookName' => $key, 'handler' => self::UNRESOLVABLE, 'priority' => null, 'addon' => null];
                continue;
            }
            if (!is_array($value)) {
                continue; // malformed/unparseable entry, skip
            }

            if (!array_key_exists('hook', $value)) {
                // positional form: [serviceId, method, priority?, addonOverride?]
                $hookName = $key;
                $handler = (isset($value[0]) && isset($value[1])) ? [$value[0], $value[1]] : self::UNRESOLVABLE;
                $priority = isset($value[2]) ? $value[2] : null;
                $addon = isset($value[3]) ? $value[3] : null;
            } else {
                // explicit associative form
                $hookName = $value['hook'];
                $handlerVal = isset($value['handler']) ? $value['handler'] : null;
                if (is_array($handlerVal) && isset($handlerVal[0]) && isset($handlerVal[1])) {
                    $handler = [$handlerVal[0], $handlerVal[1]];
                } else {
                    $handler = self::UNRESOLVABLE;
                }
                $priority = isset($value['priority']) ? $value['priority'] : null;
                $addon = isset($value['addon']) ? $value['addon'] : null;
            }

            $entries[] = ['hookName' => $hookName, 'handler' => $handler, 'priority' => $priority, 'addon' => $addon];
        }
        return $entries;
    }

    // ---------------------------------------------------------------------
    // service id -> class resolution via ServiceProvider
    // ---------------------------------------------------------------------

    /** Every "new X(...)" mentioned in the Bootstrap file is a candidate
     *  provider class - broad on purpose; wrong candidates simply won't
     *  contain the target service id and get skipped in buildRecord(). */
    private function findRegisteredProviderClassNames($bootstrapFile) {
        $source = file_get_contents($bootstrapFile);
        if ($source === false) {
            return [];
        }
        $tokens = token_get_all($source);
        $n = count($tokens);
        $names = [];
        for ($i = 0; $i < $n; $i++) {
            if (is_array($tokens[$i]) && $tokens[$i][0] === T_NEW) {
                $j = $i + 1;
                while ($j < $n && is_array($tokens[$j])
                    && in_array($tokens[$j][0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true)) {
                    $j++;
                }
                $classTokens = [];
                while ($j < $n && is_array($tokens[$j]) && in_array($tokens[$j][0], self::identifierTokenTypes(), true)) {
                    $classTokens[] = $tokens[$j][1];
                    $j++;
                }
                if (!empty($classTokens)) {
                    $names[] = implode('', $classTokens);
                }
            }
        }
        return array_values(array_unique($names));
    }

    /** Finds $app['<serviceId>'] = function(...) { return new ClassName(...); };
     *  in a ServiceProvider-like file, and returns ClassName (short/qualified,
     *  not yet resolved to FQCN). Scans forward a bounded distance after the
     *  assignment to stay within that one closure's body without needing to
     *  track its exact brace boundary. */
    private function findServiceAssignmentClass($providerFile, $serviceId) {
        $source = file_get_contents($providerFile);
        if ($source === false) {
            return null;
        }
        $tokens = token_get_all($source);
        $n = count($tokens);

        for ($i = 0; $i < $n; $i++) {
            $t = $tokens[$i];
            if (!is_array($t) || $t[0] !== T_CONSTANT_ENCAPSED_STRING || trim($t[1], "'\"") !== $serviceId) {
                continue;
            }

            $j = $i - 1;
            while ($j >= 0 && is_array($tokens[$j])
                && in_array($tokens[$j][0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true)) {
                $j--;
            }
            if ($j < 0 || $tokens[$j] !== '[') {
                continue; // not an $app['...'] style array access
            }

            $limit = min($n, $i + 400); // generous bound for one closure's body
            for ($k = $i + 1; $k < $limit; $k++) {
                if (is_array($tokens[$k]) && $tokens[$k][0] === T_NEW) {
                    $m = $k + 1;
                    while ($m < $n && is_array($tokens[$m])
                        && in_array($tokens[$m][0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true)) {
                        $m++;
                    }
                    $classTokens = [];
                    while ($m < $n && is_array($tokens[$m]) && in_array($tokens[$m][0], self::identifierTokenTypes(), true)) {
                        $classTokens[] = $tokens[$m][1];
                        $m++;
                    }
                    if (!empty($classTokens)) {
                        return implode('', $classTokens);
                    }
                }
            }
        }
        return null;
    }

    /** Fallback when service-id resolution doesn't land: search the whole
     *  addon for a function/method with this exact name. In practice these
     *  onXxx-style hook handler methods are unique within an addon. */
    private function searchAddonForMethod($addonPath, $methodName) {
        $pattern = '/\bfunction\s+' . preg_quote($methodName, '/') . '\s*\(/';
        foreach ($this->findPhpFiles($addonPath) as $file) {
            $lines = file($file);
            if ($lines === false) {
                continue;
            }
            foreach ($lines as $i => $line) {
                if (preg_match($pattern, $line)) {
                    return ['file' => $file, 'line' => $i + 1];
                }
            }
        }
        return null;
    }
}
<?php
/**
 * @copyright Salix.me
 * @author Panos <panos@salix.gr>
 *
 * Given an area ('backend'|'frontend') and a "controller.mode" string (the
 * same shape visible in the admin/storefront URL, e.g. "products.update"),
 * finds every PHP file that could be handling that mode.
 *
 * Mechanism (verified against real source - app/functions/fn.control.php,
 * fn_dispatch()/fn_init_core_controllers()/fn_init_addon_controllers()):
 *
 *   For controller name X and area A, CS-Cart's dispatcher builds a "cascade"
 *   of files to include, in this order:
 *     addon .pre.php (every active addon, area + common)
 *     core  .pre.php (area + common)
 *     core  main .php (area + common)
 *     addon main .php (every active addon, area + common)
 *     core  .post.php (area + common)
 *     addon .post.php (every active addon, area + common)
 *
 *   Each candidate path checked is:
 *     app/controllers/<area>/<X>[.pre|.post].php
 *     app/controllers/common/<X>[.pre|.post].php
 *     app/addons/<addon>/controllers/<area>/<X>[.pre|.post].php
 *     app/addons/<addon>/controllers/common/<X>[.pre|.post].php
 *
 *   Every file that gets included is a PLAIN PHP file (fn_run_controller()
 *   just include()s it) - there's no per-mode dispatch at the file level,
 *   only within it, via `if ($mode == '...')` / `if ($mode === '...')`
 *   guards. That's exactly what this class searches for.
 *
 *   .pre/.post files are additive - every active addon's (plus core's,
 *   plus common's) all run, for any mode of that controller. The MAIN
 *   (non-pre/post) file is different: fn_dispatch() itself throws
 *   DeveloperException('Duplicate controller ...') if it ever finds more
 *   than one active main controller file for the same name - so at most
 *   one should exist among active candidates. This class checks for that
 *   exact condition and flags it as a conflict if violated.
 *
 * Pure static text search - no CS-Cart bootstrap, no Reflection. Known
 * limitation: matching is line-based regex, not a real PHP parse, so a
 * comment that happens to contain literal matching text (e.g. a code
 * comment mentioning `$mode == 'update'`) would be indistinguishable from
 * real code. Rare in practice, but worth knowing before trusting a
 * "no match found" result completely on a file with unusual commenting.
 */

namespace Tygh\Slx\Tools;

class ControllerModeFinder
{
    /** @var string absolute path to the CS-Cart root, no trailing slash */
    private $root;

    /** @var string[]|null active addon ids, or null if unknown (no DB info available) */
    private $activeAddons;

    /**
     * @param string $root absolute path to the CS-Cart installation root
     * @param string[]|null $activeAddons list of active addon ids. null disables
     *   all active-addon filtering/conflict-detection confidence - results still
     *   list every candidate file, just with addonActive left null (unknown)
     *   rather than true/false, and the "duplicate main controller" check only
     *   fires for candidates we can actually confirm are active (core/common
     *   files, which need no addon to be active at all).
     */
    public function __construct($root, array $activeAddons = null)
    {
        $this->root = rtrim($root, '/\\');
        $this->activeAddons = $activeAddons;
    }

    /**
     * @param string $area 'backend' or 'frontend' - used literally as the folder name
     * @param string $controllerDotMode e.g. "products.update"
     * @return array{
     *     area: string,
     *     controller: string,
     *     mode: string,
     *     files: array,
     *     conflicts: array
     * }
     */
    public function find($area, $controllerDotMode)
    {
        $parts = explode('.', $controllerDotMode, 2);
        $controller = $parts[0];
        $mode = isset($parts[1]) ? $parts[1] : '';

        $candidates = $this->buildCandidateList($area, $controller);

        $files = [];
        foreach ($candidates as $cand) {
            $abs = $this->root . '/' . $cand['relative'];
            if (!is_file($abs)) {
                continue;
            }
            $scan = $this->scanForMode($abs, $mode);
            $files[] = array_merge($cand, [
                'path' => $abs,
                'matches' => $scan['matches'],
                'hasAnyModeCheck' => $scan['hasAnyModeCheck'],
            ]);
        }

        return [
            'area' => $area,
            'controller' => $controller,
            'mode' => $mode,
            'files' => $files,
            'conflicts' => $this->detectConflicts($files),
        ];
    }

    /** @return array[] candidate descriptors, before existence is checked */
    private function buildCandidateList($area, $controller)
    {
        $candidates = [];

        foreach (['', '.pre', '.post'] as $suffix) {
            $kind = $suffix === '' ? 'main' : ltrim($suffix, '.');

            $candidates[] = [
                'relative' => "app/controllers/{$area}/{$controller}{$suffix}.php",
                'location' => 'core/area',
                'kind' => $kind,
                'addon' => null,
                'addonActive' => null,
            ];
            $candidates[] = [
                'relative' => "app/controllers/common/{$controller}{$suffix}.php",
                'location' => 'core/common',
                'kind' => $kind,
                'addon' => null,
                'addonActive' => null,
            ];
        }

        $addonsDir = $this->root . '/app/addons';
        if (is_dir($addonsDir)) {
            foreach (scandir($addonsDir) as $addonName) {
                if ($addonName === '.' || $addonName === '..') {
                    continue;
                }
                if (!is_dir($addonsDir . '/' . $addonName)) {
                    continue;
                }

                $isActive = $this->activeAddons === null
                    ? null
                    : in_array($addonName, $this->activeAddons, true);

                foreach (['', '.pre', '.post'] as $suffix) {
                    $kind = $suffix === '' ? 'main' : ltrim($suffix, '.');

                    $candidates[] = [
                        'relative' => "app/addons/{$addonName}/controllers/{$area}/{$controller}{$suffix}.php",
                        'location' => 'addon/area',
                        'kind' => $kind,
                        'addon' => $addonName,
                        'addonActive' => $isActive,
                    ];
                    $candidates[] = [
                        'relative' => "app/addons/{$addonName}/controllers/common/{$controller}{$suffix}.php",
                        'location' => 'addon/common',
                        'kind' => $kind,
                        'addon' => $addonName,
                        'addonActive' => $isActive,
                    ];
                }
            }
        }

        return $candidates;
    }

    /**
     * Scans one file's lines for $mode checks against the target mode.
     * Primary pattern: `$mode == 'X'` / `$mode === "X"` (either operand
     * order, either quote style, arbitrary whitespace). Secondary/bonus
     * pattern: `in_array($mode, ['X', ...])` / `in_array($mode, array('X', ...))`.
     * `!=`/`!==` are deliberately never matched - they indicate the opposite.
     */
    private function scanForMode($file, $mode)
    {
        $lines = file($file);
        if ($lines === false) {
            return ['matches' => [], 'hasAnyModeCheck' => false];
        }

        $modeQuoted = preg_quote($mode, '/');
        $directPattern = '/\$mode\s*(===|==)\s*([\'"])' . $modeQuoted . '\2/';
        $reversedPattern = '/([\'"])' . $modeQuoted . '\1\s*(===|==)\s*\$mode/';
        $inArrayPattern = '/in_array\s*\(\s*\$mode\s*,\s*(?:\[|array\s*\()[^\]\)]*[\'"]' . $modeQuoted . '[\'"][^\]\)]*(?:\]|\))/';
        $anyModePattern = '/\$mode\b/';

        $matches = [];
        $hasAnyModeCheck = false;

        foreach ($lines as $i => $line) {
            if (preg_match($anyModePattern, $line)) {
                $hasAnyModeCheck = true;
            }
            if (preg_match($directPattern, $line) || preg_match($reversedPattern, $line)) {
                $matches[] = ['line' => $i + 1, 'kind' => 'direct', 'snippet' => trim($line)];
            } elseif (preg_match($inArrayPattern, $line)) {
                $matches[] = ['line' => $i + 1, 'kind' => 'in_array', 'snippet' => trim($line)];
            }
        }

        return ['matches' => $matches, 'hasAnyModeCheck' => $hasAnyModeCheck];
    }

    /**
     * Mirrors fn_dispatch()'s own hard constraint: at most one ACTIVE "main"
     * controller file (core+common+addons combined) may exist for a given
     * controller name - more than one active main file is exactly the
     * condition that throws DeveloperException('Duplicate controller ...')
     * at runtime.
     *
     * @return array[] list of conflict descriptors (empty if none found)
     */
    private function detectConflicts($files)
    {
        $conflicts = [];

        $activeMains = array_values(array_filter($files, function ($f) {
            if ($f['kind'] !== 'main') {
                return false;
            }
            // core/common files need no addon to be active at all
            if ($f['addon'] === null) {
                return true;
            }
            return $f['addonActive'] === true;
        }));

        if (count($activeMains) > 1) {
            $conflicts[] = [
                'type' => 'duplicate_main_controller',
                'message' => 'More than one active main controller file found for this controller name - '
                    . 'this exact situation throws DeveloperException("Duplicate controller ...") in fn_dispatch().',
                'files' => array_map(function ($f) {
                    return $f['relative'];
                }, $activeMains),
            ];
        }

        return $conflicts;
    }
}
<?php
/**
 * @copyright Salix.me
 * @author Panos <panos@salix.gr>
 *
 * Checker part: feed it a raw text/HTML string, get back a flat list of
 * issues (unbalanced tag counts, and improperly nested / "crossed" tags).
 *
 * Pure text-in/array-out — no DB access here, so it's reusable outside
 * the SetupChecker addon too (CLI scripts, unit tests, etc.).
 *
 * Ported from app/addons/as_setup_checker/Arena/SetupChecker/Support/HtmlTagBalanceChecker.php
 * (jcclubbelize-specific location). No logic changes needed — this class
 * has zero CS-Cart runtime dependency to begin with, only the namespace
 * changed (Arena\SetupChecker\Support -> Tygh\Slx\Tools, matching where
 * it actually lives so Composer's PSR-4 autoloader can find it; the SDK's
 * composer.json only registers the Tygh\ prefix).
 */

namespace Tygh\Slx\Tools;

class HtmlTagBalanceChecker {

    const VOID_ELEMENTS = [
        'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
        'link', 'meta', 'param', 'source', 'track', 'wbr',
    ];

    const SNIPPET_RADIUS = 80;

    /** @var string[] only check these tag names; empty = check every tag found */
    private $onlyTags;

    /** @var string[] never check these tag names, even if found */
    private $ignoreTags;

    public function __construct(array $onlyTags = [], array $ignoreTags = []) {
        $this->onlyTags = array_map('strtolower', $onlyTags);
        $this->ignoreTags = array_map('strtolower', $ignoreTags);
    }

    /**
     * @param string $html
     * @return array List of issues, each one of:
     *   [
     *     'type' => 'unbalanced',
     *     'tag' => 'div',
     *     'opens' => 3, 'closes' => 2, 'diff' => 1,
     *     'reason' => 'extra_close'|'unclosed_open',
     *     'snippet' => '...context around the first problem...',
     *   ]
     *   or
     *   [
     *     'type' => 'crossed',
     *     'closing_tag' => 'div', 'expected_tag' => 'span',
     *     'snippet' => '...context around the crossing point...',
     *   ]
     */
    public function check(string $html): array {
        $issues = [];
        $clean = $this->stripNoise($html);
        $events = $this->parseTags($clean);
        $tagNames = $this->discoverTagNames($events);

        if (!empty($this->onlyTags)) {
            $tagNames = array_intersect($tagNames, $this->onlyTags);
        }
        if (!empty($this->ignoreTags)) {
            $tagNames = array_diff($tagNames, $this->ignoreTags);
        }
        $tagNames = array_values($tagNames);

        foreach ($tagNames as $tagName) {
            $result = $this->checkTagBalance($events, $tagName);
            if (!$result['balanced']) {
                $issues[] = [
                    'type' => 'unbalanced',
                    'tag' => $tagName,
                    'opens' => $result['opens'],
                    'closes' => $result['closes'],
                    'diff' => $result['opens'] - $result['closes'],
                    'reason' => $result['first_problem_type'],
                    'snippet' => $result['first_problem_offset'] !== null
                        ? $this->snippet($clean, $result['first_problem_offset'])
                        : null,
                ];
            }
        }

        foreach ($this->checkGlobalNesting($events, $tagNames) as $crossing) {
            $issues[] = [
                'type' => 'crossed',
                'closing_tag' => $crossing['closing_tag'],
                'expected_tag' => $crossing['expected_tag'],
                'snippet' => $this->snippet($clean, $crossing['offset']),
            ];
        }

        return $issues;
    }

    private function stripNoise(string $html): string {
        $html = preg_replace('/<!--.*?-->/s', '', $html);
        $html = preg_replace('/<script\b[^>]*>.*?<\/script>/is', '', $html);
        $html = preg_replace('/<style\b[^>]*>.*?<\/style>/is', '', $html);
        return $html;
    }

    private function parseTags(string $html): array {
        preg_match_all('/<(\/?)([a-zA-Z][a-zA-Z0-9]*)\b([^>]*)>/s', $html, $matches, PREG_OFFSET_CAPTURE);

        $events = [];
        $count = count($matches[0]);

        for ($i = 0; $i < $count; $i++) {
            $isClose = $matches[1][$i][0] === '/';
            $name = strtolower($matches[2][$i][0]);
            $attrs = $matches[3][$i][0];
            $offset = $matches[0][$i][1];
            $selfClosed = (bool) preg_match('/\/\s*$/', $attrs);

            if ($isClose) {
                $kind = 'close';
            } elseif ($selfClosed || in_array($name, self::VOID_ELEMENTS, true)) {
                $kind = 'selfclose';
            } else {
                $kind = 'open';
            }

            $events[] = ['name' => $name, 'kind' => $kind, 'offset' => $offset];
        }

        return $events;
    }

    private function discoverTagNames(array $events): array {
        $names = [];
        foreach ($events as $e) {
            if (!in_array($e['name'], self::VOID_ELEMENTS, true)) {
                $names[$e['name']] = true;
            }
        }
        return array_keys($names);
    }

    private function checkTagBalance(array $events, string $tagName): array {
        $stack = [];
        $opens = 0;
        $closes = 0;
        $firstProblemOffset = null;
        $firstProblemType = null;

        foreach ($events as $e) {
            if ($e['name'] !== $tagName || $e['kind'] === 'selfclose') {
                continue;
            }

            if ($e['kind'] === 'close') {
                $closes++;
                if (empty($stack)) {
                    if ($firstProblemOffset === null) {
                        $firstProblemOffset = $e['offset'];
                        $firstProblemType = 'extra_close';
                    }
                } else {
                    array_pop($stack);
                }
            } else {
                $opens++;
                $stack[] = $e['offset'];
            }
        }

        if ($firstProblemOffset === null && !empty($stack)) {
            $firstProblemOffset = $stack[0];
            $firstProblemType = 'unclosed_open';
        }

        return [
            'opens' => $opens,
            'closes' => $closes,
            'balanced' => $opens === $closes && empty($stack) && $firstProblemOffset === null,
            'first_problem_offset' => $firstProblemOffset,
            'first_problem_type' => $firstProblemType,
        ];
    }

    /**
     * One shared stack across all (allowed) tag types, to catch improperly
     * nested / "crossed" tags that per-tag counting can't see, e.g.
     * <div><span>x</div></span> — both tags individually balance 1-open/
     * 1-close, but the closing order is invalid.
     */
    private function checkGlobalNesting(array $events, array $allowedNames): array {
        $stack = [];
        $problems = [];

        foreach ($events as $e) {
            if ($e['kind'] === 'selfclose' || !in_array($e['name'], $allowedNames, true)) {
                continue;
            }

            if ($e['kind'] === 'open') {
                $stack[] = ['name' => $e['name'], 'offset' => $e['offset']];
                continue;
            }

            if (empty($stack)) {
                continue; // already surfaced by the per-tag check
            }

            $top = end($stack);
            if ($top['name'] === $e['name']) {
                array_pop($stack);
                continue;
            }

            $foundIndex = null;
            for ($i = count($stack) - 1; $i >= 0; $i--) {
                if ($stack[$i]['name'] === $e['name']) {
                    $foundIndex = $i;
                    break;
                }
            }

            if ($foundIndex !== null) {
                $problems[] = [
                    'offset' => $e['offset'],
                    'closing_tag' => $e['name'],
                    'expected_tag' => $top['name'],
                ];
                $stack = array_slice($stack, 0, $foundIndex);
            }
        }

        return $problems;
    }

    private function snippet(string $html, int $offset): string {
        $start = max(0, $offset - self::SNIPPET_RADIUS);
        $length = self::SNIPPET_RADIUS * 2;
        $chunk = substr($html, $start, $length);
        $chunk = preg_replace('/\s+/', ' ', $chunk);
        return trim($chunk);
    }

}
<?php
/**
 * @copyright Salix.me
 * @author Panos <panos@salix.gr>
 *
 * Finder part: discovers TEXT/MEDIUMTEXT/LONGTEXT columns across the
 * CS-Cart schema (via INFORMATION_SCHEMA — no hardcoded table names),
 * applies include/exclude table filters, and yields candidate rows
 * (ones that plausibly contain a tag) to a callback.
 *
 * Ported from app/addons/as_setup_checker/Arena/SetupChecker/Support/HtmlTagBalanceFinder.php
 * (jcclubbelize-specific location, uses CS-Cart's db_get_array()/db_get_field()/
 * db_quote() helpers, only reachable from inside a bootstrapped CS-Cart request).
 * This version takes a connection explicitly and builds its own SQL, so it
 * works standalone from cscart-sdk:
 *   - db_get_array($sql, $params...) with ?s/?i placeholders -> $conn->query($sql)
 *     with values escaped via $conn->quote() up front (prepared-statement bound
 *     params would also work, but this keeps the same "any object exposing
 *     query()->fetch()/quote()" duck-typed pattern already used by
 *     TemplateFileCollector::fromDoctrineConnection() elsewhere in this SDK -
 *     a real \Doctrine\DBAL\Connection or a plain \PDO both satisfy it).
 *   - db_quote('company_id = ?i', $id) -> the value is already an int by the
 *     time it reaches here (cast at the call site), so it's safe to embed
 *     directly - no escaping needed for a value PHP itself has cast to int.
 *   - The INFORMATION_SCHEMA.* queries themselves are unchanged - they're
 *     already portable, plain ANSI-ish SQL, no CS-Cart-specific syntax in them.
 *
 * Added while porting: getPrimaryKeyColumns()/tableHasColumn() results are
 * now memoized per table (not present in the original) - eachRow() is
 * typically called once per TEXT column, and several TEXT columns commonly
 * share the same table (e.g. cscart_product_descriptions), so this avoids
 * re-querying INFORMATION_SCHEMA for facts about a table already looked up.
 *
 * Verified end-to-end against a real MySQL/MariaDB instance (not mocked) -
 * findColumns(), the exclude/limit table filters, eachRow()'s LIKE-based
 * candidate filtering, and the company_id-scoping-with-column-existence-check
 * (including the "table has no company_id column, filter gracefully ignored"
 * path) all confirmed working with real SQL execution before deployment.
 */

namespace Tygh\Slx\Tools;

class HtmlTagBalanceFinder {

    /** @var object Anything exposing query($sql): Statement-with-fetch() and quote($value) */
    private $conn;

    /** @var string[] table names (without prefix) to exclude, lowercase */
    private $excludeTables;

    /** @var string[] table names (without prefix) to exclusively include, lowercase; empty = all */
    private $limitTables;

    /** @var string */
    private $tablePrefix;

    /** @var array table => [primary key column names], memoized */
    private $primaryKeyCache = [];

    /** @var array "table.column" => bool, memoized */
    private $hasColumnCache = [];

    /**
     * @param object $conn A real \Doctrine\DBAL\Connection or a plain \PDO -
     *   not type-hinted against Doctrine specifically so this class has no
     *   hard dependency on it.
     */
    public function __construct($conn, array $excludeTables = [], array $limitTables = [], $tablePrefix = 'cscart_') {
        $this->conn = $conn;
        $this->excludeTables = array_map('strtolower', $excludeTables);
        $this->limitTables = array_map('strtolower', $limitTables);
        $this->tablePrefix = $tablePrefix;
    }

    /**
     * @return array List of ['table' => 'cscart_...', 'column' => '...']
     */
    public function findColumns(): array {
        $sql = sprintf(
            "SELECT TABLE_NAME, COLUMN_NAME
             FROM INFORMATION_SCHEMA.COLUMNS
             WHERE TABLE_SCHEMA = DATABASE()
               AND DATA_TYPE IN ('text', 'mediumtext', 'longtext')
               AND TABLE_NAME LIKE %s
             ORDER BY TABLE_NAME, ORDINAL_POSITION",
            $this->conn->quote($this->tablePrefix . '%')
        );
        $rows = $this->fetchAll($sql);

        $wanted = array_map(function ($t) { return strtolower($this->tablePrefix . $t); }, $this->limitTables);
        $excluded = array_map(function ($t) { return strtolower($this->tablePrefix . $t); }, $this->excludeTables);

        $out = [];
        foreach ($rows as $row) {
            $lower = strtolower($row['TABLE_NAME']);

            if (!empty($wanted) && !in_array($lower, $wanted, true)) {
                continue;
            }
            if (in_array($lower, $excluded, true)) {
                continue;
            }

            $out[] = ['table' => $row['TABLE_NAME'], 'column' => $row['COLUMN_NAME']];
        }

        return $out;
    }

    public function getPrimaryKeyColumns(string $table): array {
        if (isset($this->primaryKeyCache[$table])) {
            return $this->primaryKeyCache[$table];
        }

        $sql = sprintf(
            "SELECT COLUMN_NAME
             FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
             WHERE TABLE_SCHEMA = DATABASE()
               AND TABLE_NAME = %s
               AND CONSTRAINT_NAME = 'PRIMARY'
             ORDER BY ORDINAL_POSITION",
            $this->conn->quote($table)
        );
        $cols = array_column($this->fetchAll($sql), 'COLUMN_NAME');
        $this->primaryKeyCache[$table] = $cols;
        return $cols;
    }

    public function tableHasColumn(string $table, string $columnName): bool {
        $cacheKey = $table . '.' . $columnName;
        if (isset($this->hasColumnCache[$cacheKey])) {
            return $this->hasColumnCache[$cacheKey];
        }

        $sql = sprintf(
            "SELECT 1
             FROM INFORMATION_SCHEMA.COLUMNS
             WHERE TABLE_SCHEMA = DATABASE()
               AND TABLE_NAME = %s
               AND COLUMN_NAME = %s
             LIMIT 1",
            $this->conn->quote($table),
            $this->conn->quote($columnName)
        );
        $found = (bool) $this->fetchOne($sql);
        $this->hasColumnCache[$cacheKey] = $found;
        return $found;
    }

    /**
     * Fetches rows from $table.$column that plausibly contain a tag
     * (contain both '<' and '>'), optionally scoped to $companyId when
     * the table actually has a company_id column, and calls
     * $callback($pkDescription, $content) for each one.
     */
    public function eachRow(string $table, string $column, ?int $companyId, callable $callback): void {
        // Paranoia: table/column names come from INFORMATION_SCHEMA, but
        // never interpolate anything into SQL that doesn't look like a
        // plain identifier.
        if (!preg_match('/^[A-Za-z0-9_]+$/', $table) || !preg_match('/^[A-Za-z0-9_]+$/', $column)) {
            return;
        }

        $pkCols = $this->getPrimaryKeyColumns($table);
        $pkSelect = $pkCols
            ? implode(', ', array_map(function ($c) { return "`$c`"; }, $pkCols))
            : '1 AS row_marker';

        $compCond = '1=1';
        if ($companyId !== null && $this->tableHasColumn($table, 'company_id')) {
            $compCond = 'company_id = ' . (int) $companyId;
        }

        $sql = sprintf(
            "SELECT %s, `%s` AS content
             FROM `%s`
             WHERE %s
               AND `%s` LIKE '%%<%%'
               AND `%s` LIKE '%%>%%'",
            $pkSelect, $column, $table, $compCond, $column, $column
        );

        $rows = $this->fetchAll($sql);

        foreach ($rows as $row) {
            $content = $row['content'];
            unset($row['content']);
            $pkDescription = $pkCols
                ? implode(', ', array_map(function ($c) use ($row) { return "{$c}={$row[$c]}"; }, $pkCols))
                : 'n/a';

            $callback($pkDescription, $content);
        }
    }

    /** @return array[] */
    private function fetchAll($sql): array {
        $stmt = $this->conn->query($sql);
        $out = [];
        while ($row = $stmt->fetch()) {
            $out[] = $row;
        }
        return $out;
    }

    /** @return array|null */
    private function fetchOne($sql) {
        $stmt = $this->conn->query($sql);
        $row = $stmt->fetch();
        return $row === false ? null : $row;
    }

}
<?php
/**
 * @copyright 2019 Kartpay.com.
 * @author Panos <panos@kartpay.com>
 * Date: 6/4/2019
 * Time: 3:21 μμ
 */

namespace Tygh\Slx\Commands\Traits;


trait CartDbConnectionTrait {

    private $conn;
    private $idColumnName;
    private $hasStatusColumn=false;

    private function tryConnectToCartDb($output) {
        $this->getCartDbConnectionParams();
        $output->writeln("DB Connection:");
        //print_r($this->connParams);
        $output->writeln("** DB parameters **");
        $output->writeln("db_host:      ". $this->connParams['db_host']);
        $output->writeln("db_name:      ". $this->connParams['db_name']);
        $output->writeln("db_user:      ". $this->connParams['db_user']);
        $output->writeln("db_password:  ". $this->connParams['db_password']);
        $output->writeln("table_prefix: ". $this->connParams['table_prefix']);
        $output->writeln("---------------------------------------");

        if(!$this->connectToCartDb($this->connParams)) {
            $this->output->writeln("Cannot connect to db!");
            die(1);
        }
    }

    private function connectToCartDb($connParams) {
        $out = false;
        $config = new \Doctrine\DBAL\Configuration();
        $connectionParams = array(
            'dbname' => $connParams['db_name'],
            'user' => $connParams['db_user'],
            'password' => $connParams['db_password'],
            'host' => $connParams['db_host'],
            'driver' => 'pdo_mysql',
            'charset' => 'utf8',
            'driverOptions' => array(
                1002=>'SET NAMES utf8'
            ),
        );
        //print_r($connectionParams);
        try {
            $this->conn = \Doctrine\DBAL\DriverManager::getConnection($connectionParams, $config);
            $out = true;
        }
        catch(\Exception $ex) {
            echo $ex->getMessage();
            $this->conn = false;
        }
        return $out;
    }

    private function dbTableExists($tblName) {
        $out = true;
        $stmt = $this->conn->query(sprintf("SHOW TABLES LIKE '%s'", $tblName));
        if( $stmt->fetch()===false) {
            $out = false;
        }
        return $out;
    }

    /**
     * Returns the list of currently active addon IDs on this installation
     * (cscart_addons.status='A'), or null if it couldn't be determined
     * (missing table, query failure, etc). Callers should treat null as
     * "no info available" and fall back to unfiltered behavior rather
     * than treating it the same as an empty (zero active addons) result.
     */
    private function getActiveAddons() {
        $table = $this->connParams['table_prefix'].'addons';
        if(!$this->dbTableExists($table)) {
            return null;
        }
        $out = [];
        try {
            $stmt = $this->conn->query(sprintf("SELECT addon FROM %s WHERE status='A'", $table));
            while($row = $stmt->fetch()) {
                $addonId = isset($row['addon']) ? $row['addon'] : reset($row);
                if($addonId!==false && $addonId!==null) {
                    $out[] = $addonId;
                }
            }
        }
        catch(\Exception $ex) {
            return null;
        }
        return $out;
    }

    private function getTableFields($tableName, $keepPrimary=true) {
        $sm = $this->conn->getSchemaManager();
        // Get fields list from table 'contact' :
        $columns = $sm->listTableColumns($tableName);
        // Loop over the array to get names and other properties :
        $out = [];
        foreach ($columns as $column) {
            $out[$column->getName()] = (string) $column->getType();
            if($keepPrimary) {
                if ($column->getAutoincrement()) {
                    $this->idColumnName = $column->getName();
                }
                if($column->getName()=='status') {
                    $this->hasStatusColumn = true;
                    $out[$column->getName()] = 'fld_status';
                }
            }

            if($column->getType()=='String') {
                if($column->getLength()==1 && $out[$column->getName()] != 'fld_status') {
                    $out[$column->getName()] = 'fld_choice';
                }
            }
            elseif($column->getType()=='Integer' && strpos($column->getName(), '_id')==strlen($column->getName())-3 && $column->getName()!=$this->idColumnName) {
                $out[$column->getName()] = 'fld_select';
            }
        }
        return $out;
    }
}
<?php
/**
 * @copyright 2019 Kartpay.com.
 * @author Panos <panos@kartpay.com>
 * Date: 6/4/2019
 * Time: 4:56 μμ
 */

namespace Tygh\Slx\Commands\Traits;


trait GetRootDirTrait {
    private function getRoot() {
        $pharFile = \Phar::running(false);
        if('' === $pharFile) {
            $root = realpath( __DIR__.'/../../' );
        }
        else {
            $root = 'phar://'. $pharFile.'/src/Sdk';
        }

        return $root;
    }
}<?php
/**
 * @copyright 2019 Kartpay.com.
 * @author Panos <panos@kartpay.com>
 * Date: 6/4/2019
 * Time: 3:09 μμ
 */

namespace Tygh\Slx\Commands\Traits;


trait CartDbParamsTrait {

    /** Regex-extracts $config['key'] = 'value'; assignments from a config
     *  file's raw source and merges them into $connParams (only for keys
     *  already present there) - overwriting whatever was already set. */
    private function extractConfigParams($file, &$connParams) {
        $m = [];
        preg_match_all('/\$config\[\'(.*)\'\]\s+=\s+\'(.*)\'/', file_get_contents($file), $m);
        if(count($m)) {
            foreach($m[1] as $i => $v) {
                if(isset($connParams[$v])) {
                    if(isset($m[2][$i])) {
                        $connParams[$v] = $m[2][$i];
                    }
                }
            }
        }
    }

    private function getCartDbConnectionParams() {
        $configFile = $this->absCartPath.'/config.local.php';
        $localConfFile = $this->absCartPath.'/local_conf.php';
        $connParams = [
            'db_host' => '',
            'db_name' => '',
            'db_user' => '',
            'db_password' => '',
            'table_prefix' => '',
        ];
        if(file_exists($configFile)) {
            $this->extractConfigParams($configFile, $connParams);
        }
        else {
            $this->output->writeln("config.local.php not found!");
            die(1);
        }

        // local_conf.php is CS-Cart's developer-override file. config.local.php
        // itself includes it (AFTER all its own $config[...] assignments) when
        // it exists, so any db connection params set there take precedence -
        // e.g. jcclubbelize leaves db_name blank in config.local.php and sets
        // the real value only in local_conf.php.
        if(file_exists($localConfFile)) {
            $this->output->writeln("local_conf.php found, using it to override connection params where set");
            $this->extractConfigParams($localConfFile, $connParams);
        }

        $found = 0;
        foreach($connParams as $param) {
            if(!empty($param)) {
                $found++;
            }
        }
        $this->connParams = false;
        if($found==count($connParams)) {
            $this->connParams = $connParams;
        }
        if(!$this->connParams) {
            $this->output->writeln("Connection parameters not found!");
            die(1);
        }
    }

}
<?php
/**
 * @copyright Salix.me
 * @author Panos <panos@salix.gr>
 * Created: 20/Αύγ/2026
 * Time: 08:51
 */

namespace Tygh\Slx\Commands;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Filesystem\Filesystem;
use Tygh\Sdk\Commands\Traits\ValidateCartPathTrait;
use Tygh\Slx\Commands\Traits\CartDbConnectionTrait;
use Tygh\Slx\Commands\Traits\CartDbParamsTrait;
use Tygh\Slx\Commands\Traits\GetRootDirTrait;
use Tygh\Slx\Tools\FnHookExplorer\FnHookExplorer;
use Tygh\Slx\Util\NameConversions;

class FindFnHooksCommand extends Command {

    use ValidateCartPathTrait;
    use CartDbParamsTrait;
    use CartDbConnectionTrait;
    use GetRootDirTrait;

    protected function configure() {
        $this
            ->setName('find:fn-hooks')
            ->setDescription(
                'Given a function name finds its\'s hooks and defined hook handlers.'
            )
            ->addArgument('cart-directory',
                InputArgument::REQUIRED,
                'Path to CS-Cart installation directory'
            )

            ->addArgument('fn',
                InputArgument::REQUIRED,
                'The function name or class method (Class::Method)'
            )
        ;
    }

    private $output;
    private $input;
    private $absCartPath;
    private $root;

    protected function execute(InputInterface $input, OutputInterface $output) {
        $this->output = $output;
        $this->input = $input;
        $this->absCartPath = rtrim(realpath($input->getArgument('cart-directory')), '\\/') . '/';
        $this->validateCartPath($this->absCartPath, $input, $output);
        $output->writeln(sprintf('cscart = %s',$this->absCartPath));
        $this->tryConnectToCartDb($output);

        $activeAddons = $this->getActiveAddons();
        if ($activeAddons === null) {
            $output->writeln('<comment>Could not determine active addons from the DB - results will NOT be filtered by addon status.</comment>');
        } else {
            $output->writeln(sprintf('Active addons on this installation: %d', count($activeAddons)));
        }

        $functionOrMethod = $input->getArgument('fn');
        $this->root = rtrim($this->absCartPath, '\\/');

        $explorer = new FnHookExplorer($this->root, $activeAddons);

        $editionAcronym = $explorer->getEditionAcronym();
        if ($editionAcronym === null) {
            $output->writeln('<comment>Could not determine this install\'s PRODUCT_EDITION - core/edition-tier handlers will be shown for pro, mve, and ult all at once, flagged as unconfirmed.</comment>');
        } else {
            $output->writeln(sprintf('Edition: %s', $editionAcronym));
        }
        $output->writeln('');

        $result = $explorer->analyze($functionOrMethod);

        if ($result['error'] !== null) {
            $output->writeln(sprintf('<error>%s</error>', $result['error']));
            return 1;
        }

        if (count($result['defs']) > 1) {
            $output->writeln(sprintf(
                '<comment>Note: %s() matched in %d places (showing all):</comment>',
                $result['displayName'], count($result['defs'])
            ));
            foreach ($result['defs'] as $d) {
                $output->writeln(sprintf('  - %s:%d', $this->relPath($d['file']), $d['line']));
            }
            $output->writeln('');
        }

        foreach ($result['defs'] as $def) {
            $output->writeln(sprintf('<fg=magenta;options=bold>=== %s() ===</>', $result['displayName']));
            $output->writeln(sprintf('Defined at: %s:%d', $this->relPath($def['file']), $def['line']));

            if ($def['addonActive'] === false) {
                $output->writeln(sprintf(
                    '<fg=yellow;options=bold>WARNING: addon \'%s\' is not active on this installation - none of the below would actually execute right now.</>',
                    $def['sourceAddon']
                ));
            }
            $output->writeln('');

            if ($def['error'] !== null) {
                $output->writeln(sprintf('  (%s)', $def['error']));
                continue;
            }
            if ($def['abstract']) {
                $output->writeln('  (abstract/interface declaration - no body, no hooks)');
                continue;
            }
            if (empty($def['hooks'])) {
                $output->writeln("  No fn_set_hook() calls found in this function's body.");
                $output->writeln('');
                continue;
            }

            $output->writeln(sprintf('  Found %d hook(s):', count($def['hooks'])));
            foreach ($def['hooks'] as $h) {
                $output->writeln(sprintf("    [line %d] '%s'", $h['line'], $h['name']));
            }
            $output->writeln('');

            foreach ($def['hooks'] as $h) {
                $output->writeln(sprintf("  --- Handlers for '%s' ---", $h['name']));
                if (empty($h['handlers'])) {
                    $output->writeln('    (no addon handlers found)');
                } else {
                    foreach ($h['handlers'] as $handler) {
                        $addonLabel = $handler['calleeAddon'];
                        if ($handler['declaredIn'] !== $handler['calleeAddon']) {
                            $addonLabel .= sprintf(' (defined in %s)', $handler['declaredIn']);
                        }
                        $status = $handler['registered']
                            ? '<info>registered</info>'
                            : '<error>NOT REGISTERED - dead code, never runs</error>';
                        $output->writeln(sprintf(
                            '    [%s] %s()  %s:%d  (%s)',
                            $addonLabel, $handler['function'], $this->relPath($handler['file']), $handler['line'], $status
                        ));
                    }
                }

                if (!empty($h['coreEditionHandlers'])) {
                    foreach ($h['coreEditionHandlers'] as $ceh) {
                        $kindLabel = $ceh['kind'] === 'core' ? 'core' : sprintf('edition:%s', $ceh['kind']);
                        if ($ceh['addon'] !== null) {
                            $kindLabel .= sprintf(' (defined in %s)', $ceh['addon']);
                        }
                        if ($ceh['fires'] === true) {
                            $status = sprintf('<info>fires</info> - %s', $ceh['reason']);
                        } elseif ($ceh['fires'] === false) {
                            $status = sprintf('<comment>does not fire</comment> - %s', $ceh['reason']);
                        } else {
                            $status = sprintf('<comment>unconfirmed</comment> - %s', $ceh['reason']);
                        }
                        $output->writeln(sprintf(
                            '    [%s] %s()  %s:%d  (%s)',
                            $kindLabel, $ceh['function'], $this->relPath($ceh['file']), $ceh['line'], $status
                        ));
                    }
                }

                if (!empty($h['oopHandlers'])) {
                    foreach ($h['oopHandlers'] as $oh) {
                        $addonLabel = $oh['effectiveAddon'];
                        if ($oh['baseAddon'] !== $oh['effectiveAddon']) {
                            $addonLabel .= sprintf(' (declared in %s)', $oh['baseAddon']);
                        }
                        if ($oh['fires'] === true) {
                            $status = sprintf('<info>fires</info> - %s', $oh['reason']);
                        } elseif ($oh['fires'] === false) {
                            $status = sprintf('<comment>does not fire</comment> - %s', $oh['reason']);
                        } else {
                            $status = sprintf('<comment>unconfirmed</comment> - %s', $oh['reason']);
                        }
                        if ($oh['unresolvable']) {
                            $output->writeln(sprintf(
                                '    [oop:%s] (Closure)  (%s)',
                                $addonLabel, $status
                            ));
                        } else {
                            $loc = $oh['file'] !== null
                                ? sprintf('%s:%d', $this->relPath($oh['file']), $oh['line'])
                                : '(method not located)';
                            $output->writeln(sprintf(
                                '    [oop:%s] %s::%s()  %s  (%s)',
                                $addonLabel, $oh['serviceId'], $oh['method'], $loc, $status
                            ));
                        }
                    }
                }

                $output->writeln('');
            }
        }

        return 0;
    }

    /** Strips the cart installation's root path off an absolute file path
     *  for display, so lines stay short and readable in the terminal
     *  (e.g. "/var/www/cs4191/app/addons/x/func.php" -> "app/addons/x/func.php"). */
    private function relPath($absolutePath) {
        $normalizedRoot = str_replace('\\', '/', $this->root) . '/';
        $normalizedPath = str_replace('\\', '/', $absolutePath);
        if (strpos($normalizedPath, $normalizedRoot) === 0) {
            return substr($normalizedPath, strlen($normalizedRoot));
        }
        return $absolutePath;
    }

}
<?php
/**
 * @copyright Salix.me
 * @author Panos <panos@salix.gr>
 */

namespace Tygh\Slx\Commands;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Tygh\Sdk\Commands\Traits\ValidateCartPathTrait;
use Tygh\Slx\Commands\Traits\CartDbConnectionTrait;
use Tygh\Slx\Commands\Traits\CartDbParamsTrait;
use Tygh\Slx\Commands\Traits\GetRootDirTrait;
use Tygh\Slx\Tools\HtmlTagBalanceChecker;
use Tygh\Slx\Tools\HtmlTagBalanceFinder;

class FindUnbalancedHtmlTagsCommand extends Command {

    use ValidateCartPathTrait;
    use CartDbParamsTrait;
    use CartDbConnectionTrait;
    use GetRootDirTrait;

    const DEFAULT_EXCLUDE_TABLES = 'logs,log,user_activity,search_words,session_data,change_log,template_snippets,template_emails,original_values';

    protected function configure() {
        $this
            ->setName('find:unbalanced-html-tags')
            ->setDescription(
                'Scans database TEXT/MEDIUMTEXT/LONGTEXT columns (WYSIWYG content, descriptions, static blocks, ...) for unbalanced or improperly nested HTML tags.'
            )
            ->addArgument('cart-directory',
                InputArgument::REQUIRED,
                'Path to CS-Cart installation directory'
            )
            ->addOption('company-id',
                null,
                InputOption::VALUE_REQUIRED,
                'Only scan rows for this company_id (tables without a company_id column are scanned regardless)',
                ''
            )
            ->addOption('exclude-tables',
                null,
                InputOption::VALUE_REQUIRED,
                sprintf('Comma-separated table names (without prefix) to skip, IN ADDITION to the built-in default list (%s)', self::DEFAULT_EXCLUDE_TABLES),
                ''
            )
            ->addOption('exclude-only-tables',
                null,
                InputOption::VALUE_REQUIRED,
                'Comma-separated table names (without prefix) to skip - REPLACES the built-in default list entirely (and --exclude-tables, if also given) rather than adding to it',
                ''
            )
            ->addOption('only-tables',
                null,
                InputOption::VALUE_REQUIRED,
                'Comma-separated table names (without prefix) to exclusively scan - empty means all',
                ''
            )
            ->addOption('only-tags',
                null,
                InputOption::VALUE_REQUIRED,
                'Comma-separated HTML tag names to check - empty means every tag found',
                ''
            )
            ->addOption('ignore-tags',
                null,
                InputOption::VALUE_REQUIRED,
                'Comma-separated HTML tag names to never check',
                ''
            )
        ;
    }

    private $output;
    private $input;
    private $absCartPath;

    protected function execute(InputInterface $input, OutputInterface $output) {
        $this->output = $output;
        $this->input = $input;
        $this->absCartPath = rtrim(realpath($input->getArgument('cart-directory')), '\\/') . '/';
        $this->validateCartPath($this->absCartPath, $input, $output);
        $output->writeln(sprintf('cscart = %s', $this->absCartPath));
        $this->tryConnectToCartDb($output);

        $companyIdOpt = trim($input->getOption('company-id'));
        $companyId = $companyIdOpt !== '' ? (int) $companyIdOpt : null;

        $excludeOnlyOpt = $this->parseList($input->getOption('exclude-only-tables'));
        if (!empty($excludeOnlyOpt)) {
            $excludeTables = $excludeOnlyOpt;
            $excludeMode = 'exact';
        } else {
            $excludeTables = array_values(array_unique(array_merge(
                $this->parseList(self::DEFAULT_EXCLUDE_TABLES),
                $this->parseList($input->getOption('exclude-tables'))
            )));
            $excludeMode = 'default+additions';
        }
        $limitTables = $this->parseList($input->getOption('only-tables'));
        $onlyTags = $this->parseList($input->getOption('only-tags'));
        $ignoreTags = $this->parseList($input->getOption('ignore-tags'));

        if ($companyId !== null) {
            $output->writeln(sprintf('Company ID filter: %d', $companyId));
        }
        if (!empty($excludeTables)) {
            $label = $excludeMode === 'exact' ? 'Excluding tables (exact list)' : 'Excluding tables (default + additions)';
            $output->writeln(sprintf('%s: %s', $label, implode(', ', $excludeTables)));
        }
        if (!empty($limitTables)) {
            $output->writeln(sprintf('Limiting to tables: %s', implode(', ', $limitTables)));
        }
        $output->writeln('');

        $finder = new HtmlTagBalanceFinder($this->conn, $excludeTables, $limitTables, $this->connParams['table_prefix']);
        $checker = new HtmlTagBalanceChecker($onlyTags, $ignoreTags);

        $columns = $finder->findColumns();
        $output->writeln(sprintf('Scanning %d text column(s)...', count($columns)));
        $output->writeln('');

        // Buffered on purpose - Table needs every row before it can compute
        // column widths, so this is a "review when the scan is done" report,
        // not a live progress feed. That's the right trade for this command:
        // it's something a person works through in their own time afterward,
        // not something watched while it runs.
        $rows = [];
        foreach ($columns as $col) {
            $finder->eachRow($col['table'], $col['column'], $companyId, function ($pk, $content) use ($checker, $col, &$rows) {
                foreach ($checker->check((string) $content) as $issue) {
                    $rows[] = $this->buildRow($col, $pk, $issue);
                }
            });
        }

        $output->writeln('');
        if (empty($rows)) {
            $output->writeln('<info>No unbalanced or crossed HTML tags found.</info>');
            return 0;
        }

        $table = new Table($output);
        $table->setHeaders(['Table', 'Column', 'PK', 'Type', 'Details', 'Snippet']);
        // Wraps long cell content across multiple lines within the row, rather
        // than truncating it - available since Console 4.2, confirmed working
        // against the exact pinned v6.4.0 source before deploying.
        $table->setColumnMaxWidth(2, 30); // PK
        $table->setColumnMaxWidth(4, 45); // Details
        $table->setColumnMaxWidth(5, 55); // Snippet
        $table->addRows($rows);
        $table->render();

        $output->writeln('');
        $output->writeln(sprintf('<comment>%d issue(s) found.</comment>', count($rows)));

        return 0;
    }

    private function parseList($optionValue) {
        $optionValue = trim($optionValue);
        return $optionValue === '' ? [] : array_map('trim', explode(',', $optionValue));
    }

    /** @return string[] one Table row */
    private function buildRow($col, $pk, $issue) {
        if ($issue['type'] === 'unbalanced') {
            $details = sprintf('tag=%s opens=%d closes=%d (%s)', $issue['tag'], $issue['opens'], $issue['closes'], $issue['reason']);
        } else {
            $details = sprintf('closing=/%s expected=/%s', $issue['closing_tag'], $issue['expected_tag']);
        }

        return [
            $col['table'],
            $col['column'],
            // PK values are real integers/short codes in every practical case, but
            // still DB-sourced, so escape defensively - Table cells pass through
            // the same OutputFormatter a plain writeln() would, so a stray < or >
            // could otherwise be misread as a style tag.
            OutputFormatter::escape($this->toUtf8($pk)),
            $issue['type'],
            $details,
            OutputFormatter::escape($this->toUtf8($issue['snippet'] ?? '')),
        ];
    }

    /**
     * Guarantees valid UTF-8 output regardless of input. Real CS-Cart WYSIWYG
     * content routinely has mis-encoded bytes - Word/Outlook smart-quote
     * paste-ins stored raw in a nominally-utf8 column, old latin1-era data,
     * a stray non-strict-mode import, etc. Symfony's String component (a real
     * dependency now that this SDK targets Console 6.4, and one that
     * setColumnMaxWidth()'s wrapping exercises directly via Helper::width())
     * throws a hard InvalidArgumentException on invalid UTF-8 rather than
     * degrading gracefully, so this has to run before anything reaches the
     * Table at all - confirmed by constructing a real
     * Symfony\Component\String\UnicodeString directly from several genuinely
     * invalid byte patterns (a lone continuation byte, an overlong encoding,
     * an invalid surrogate, a truncated multi-byte sequence, a lone lead byte)
     * - every one of them threw before this fix, and produced valid,
     * non-throwing output after it.
     */
    private function toUtf8($string) {
        if ($string === null || $string === '') {
            return (string) $string;
        }
        if (mb_check_encoding($string, 'UTF-8')) {
            return $string;
        }
        $converted = @mb_convert_encoding($string, 'UTF-8', 'UTF-8');
        if ($converted !== false && mb_check_encoding($converted, 'UTF-8')) {
            return $converted;
        }
        return preg_replace('/[\x80-\xFF]/', '?', $string);
    }

}
<?php
/**
 * @copyright Salix.me
 * @author Panos <panos@salix.gr>
 */

namespace Tygh\Slx\Commands;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Tygh\Sdk\Commands\Traits\ValidateCartPathTrait;
use Tygh\Slx\Commands\Traits\CartDbConnectionTrait;
use Tygh\Slx\Commands\Traits\CartDbParamsTrait;
use Tygh\Slx\Commands\Traits\GetRootDirTrait;
use Tygh\Slx\Tools\TemplateFileCollector;

class FindTemplateFilesCommand extends Command {

    use ValidateCartPathTrait;
    use CartDbParamsTrait;
    use CartDbConnectionTrait;
    use GetRootDirTrait;

    protected function configure() {
        $this
            ->setName('find:template-files')
            ->setDescription(
                'Given a template path, finds every file (base template, addon overrides, addon hooks, includes) that could contribute to rendering it.'
            )
            ->addArgument('cart-directory',
                InputArgument::REQUIRED,
                'Path to CS-Cart installation directory'
            )
            ->addArgument('template',
                InputArgument::REQUIRED,
                'Template path relative to the templates root, e.g. views/products/update.tpl'
            )
            ->addArgument('area',
                InputArgument::REQUIRED,
                "'backend', or a theme name (e.g. 'responsive')"
            )
            ->addOption('exclude',
                null,
                InputOption::VALUE_REQUIRED,
                'Comma-separated path roots to not expand (still recorded, just not descended into). Pass an empty string to disable.',
                'common,buttons'
            )
        ;
    }

    private $output;
    private $input;
    private $absCartPath;
    private $root;

    protected function execute(InputInterface $input, OutputInterface $output) {
        $this->output = $output;
        $this->input = $input;
        $this->absCartPath = rtrim(realpath($input->getArgument('cart-directory')), '\\/') . '/';
        $this->validateCartPath($this->absCartPath, $input, $output);
        $output->writeln(sprintf('cscart = %s', $this->absCartPath));
        $this->tryConnectToCartDb($output);

        $this->root = rtrim($this->absCartPath, '\\/');
        $templatePath = $input->getArgument('template');
        $area = $input->getArgument('area');

        $excludeOpt = $input->getOption('exclude');
        $excludeRoots = trim($excludeOpt) === '' ? [] : array_map('trim', explode(',', $excludeOpt));

        $output->writeln(sprintf('Area: %s', $area));
        if (!empty($excludeRoots)) {
            $output->writeln(sprintf('Excluding (not expanded): %s', implode(', ', $excludeRoots)));
        }
        $output->writeln('');

        $collector = TemplateFileCollector::fromDoctrineConnection(
            $this->conn,
            $this->connParams['table_prefix'],
            $this->root,
            $area,
            $excludeRoots
        );

        $results = $collector->collect($templatePath);

        $output->writeln(sprintf('<fg=magenta;options=bold>=== %s (%s) ===</>', $templatePath, $area));
        $output->writeln('');

        foreach ($results as $r) {
            $this->renderEntry($r);
        }

        return 0;
    }

    private function renderEntry($r) {
        $indent = '  ';
        $viaSuffix = $r['via'] !== null ? sprintf('  <comment>via</comment> %s', $r['via']) : '';

        if ($r['kind'] === 'excluded') {
            /*$this->output->writeln(sprintf(
                '%s<comment>[excluded]</comment> %s%s',
                $indent, $r['relative'], $viaSuffix
            ));*/
            return;
        }

        if ($r['kind'] === 'include_unresolved') {
            $this->output->writeln(sprintf(
                '%s<comment>[dynamic include]</comment> %s%s',
                $indent, $r['note'], $viaSuffix
            ));
            return;
        }

        if ($r['kind'] === 'hook_override' && $r['path'] === null) {
            // malformed hook name shape - no file lookup happened at all
            $this->output->writeln(sprintf(
                '%s<error>[unrecognized hook]</error> %s%s',
                $indent, $r['note'], $viaSuffix
            ));
            return;
        }

        $addonLabel = $r['addon'] !== null ? $r['addon'] : 'core';
        $pathDisplay = $r['path'] !== null ? $this->relPath($r['path']) : $r['relative'];
        $existsLabel = $r['exists'] ? '' : ' <error>MISSING</error>';
        $winnerLabel = $r['assumed_winner'] === true ? ' <info>[WINNER]</info>' : '';
        $noteLabel = $r['note'] !== null ? sprintf('  <comment>(%s)</comment>', $r['note']) : '';

        $this->output->writeln(sprintf(
            '%s[%s:%s] %s%s%s%s%s',
            $indent, $r['kind'], $addonLabel, $pathDisplay, $existsLabel, $winnerLabel, $noteLabel, $viaSuffix
        ));
    }

    /** Strips the cart installation's root path off an absolute file path
     *  for display, so lines stay short and readable in the terminal. */
    private function relPath($absolutePath) {
        $normalizedRoot = str_replace('\\', '/', $this->root) . '/';
        $normalizedPath = str_replace('\\', '/', $absolutePath);
        if (strpos($normalizedPath, $normalizedRoot) === 0) {
            return substr($normalizedPath, strlen($normalizedRoot));
        }
        return $absolutePath;
    }

}
<?php
/**
 * @copyright Salix.me
 * @author Panos <panos@salix.gr>
 */

namespace Tygh\Slx\Commands;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Tygh\Sdk\Commands\Traits\ValidateCartPathTrait;
use Tygh\Slx\Commands\Traits\CartDbConnectionTrait;
use Tygh\Slx\Commands\Traits\CartDbParamsTrait;
use Tygh\Slx\Commands\Traits\GetRootDirTrait;
use Tygh\Slx\Tools\ControllerModeFinder;

class FindControllerModeCommand extends Command {

    use ValidateCartPathTrait;
    use CartDbParamsTrait;
    use CartDbConnectionTrait;
    use GetRootDirTrait;

    protected function configure() {
        $this
            ->setName('find:controller-mode')
            ->setDescription(
                'Given an area and a controller.mode (as seen in the URL), finds every core/addon controller file that could be handling it.'
            )
            ->addArgument('cart-directory',
                InputArgument::REQUIRED,
                'Path to CS-Cart installation directory'
            )
            ->addArgument('area',
                InputArgument::REQUIRED,
                "'backend' or 'frontend'"
            )
            ->addArgument('controller-mode',
                InputArgument::REQUIRED,
                'e.g. products.update'
            )
        ;
    }

    private $output;
    private $input;
    private $absCartPath;
    private $root;

    protected function execute(InputInterface $input, OutputInterface $output) {
        $this->output = $output;
        $this->input = $input;
        $this->absCartPath = rtrim(realpath($input->getArgument('cart-directory')), '\\/') . '/';
        $this->validateCartPath($this->absCartPath, $input, $output);
        $output->writeln(sprintf('cscart = %s', $this->absCartPath));
        $this->tryConnectToCartDb($output);

        $this->root = rtrim($this->absCartPath, '\\/');
        $area = $input->getArgument('area');
        $controllerMode = $input->getArgument('controller-mode');

        $activeAddons = $this->getActiveAddons();
        if ($activeAddons === null) {
            $output->writeln('<comment>Could not determine active addons from the DB - every candidate file will be listed, but active/inactive status (and the duplicate-main-controller check) will be shown as unconfirmed.</comment>');
        } else {
            $output->writeln(sprintf('Active addons on this installation: %d', count($activeAddons)));
        }
        $output->writeln('');

        $finder = new ControllerModeFinder($this->root, $activeAddons);
        $result = $finder->find($area, $controllerMode);

        $output->writeln(sprintf(
            '<fg=magenta;options=bold>=== %s.%s (%s) ===</>',
            $result['controller'], $result['mode'], $result['area']
        ));
        $output->writeln('');

        if (empty($result['files'])) {
            $output->writeln('<comment>No controller files found at all for this controller name.</comment>');
            return 0;
        }

        foreach ($result['files'] as $f) {
            $this->renderFile($f);
        }

        if (!empty($result['conflicts'])) {
            foreach ($result['conflicts'] as $c) {
                $output->writeln(sprintf('<error>CONFLICT: %s</error>', OutputFormatter::escape($c['message'])));
                foreach ($c['files'] as $path) {
                    $output->writeln(sprintf('  - %s', $this->relPath($this->root . '/' . $path)));
                }
                $output->writeln('');
            }
        }

        return 0;
    }

    private function renderFile($f) {
        $addonLabel = $f['addon'] !== null ? $f['addon'] : 'core';
        $activeLabel = '';
        if ($f['addon'] !== null) {
            $activeLabel = $f['addonActive'] === true ? ' <info>[active]</info>'
                : ($f['addonActive'] === false ? ' <comment>[INACTIVE]</comment>' : ' <comment>[active? unknown]</comment>');
        }

        $this->output->writeln(sprintf(
            '<fg=cyan>[%s:%s]</> %s (%s)%s',
            $f['location'], $f['kind'], $this->relPath($f['path']), $addonLabel, $activeLabel
        ));

        if (empty($f['matches'])) {
            $note = $f['hasAnyModeCheck']
                ? 'no check for this specific mode (file does branch on $mode for other values)'
                : 'no $mode check at all (runs unconditionally whenever this controller is dispatched)';
            $this->output->writeln(sprintf('    <comment>%s</comment>', $note));
        } else {
            foreach ($f['matches'] as $m) {
                $kindLabel = $m['kind'] === 'in_array' ? ' (in_array)' : '';
                $this->output->writeln(sprintf(
                    '    line %d%s: %s',
                    $m['line'], $kindLabel, OutputFormatter::escape($m['snippet'])
                ));
            }
        }
        $this->output->writeln('');
    }

    /** Strips the cart installation's root path off an absolute file path
     *  for display, so lines stay short and readable in the terminal. */
    private function relPath($absolutePath) {
        $normalizedRoot = str_replace('\\', '/', $this->root) . '/';
        $normalizedPath = str_replace('\\', '/', $absolutePath);
        if (strpos($normalizedPath, $normalizedRoot) === 0) {
            return substr($normalizedPath, strlen($normalizedRoot));
        }
        return $absolutePath;
    }

}
MIT License

Copyright (c) 2018 PHP-FIG

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
{
    "name": "psr/event-dispatcher",
    "description": "Standard interfaces for event handling.",
    "type": "library",
    "keywords": ["psr", "psr-14", "events"],
    "license": "MIT",
    "authors": [
        {
            "name": "PHP-FIG",
            "homepage": "http://www.php-fig.org/"
        }
    ],
    "require": {
        "php": ">=7.2.0"
    },
    "autoload": {
        "psr-4": {
            "Psr\\EventDispatcher\\": "src/"
        }
    },
    "extra": {
        "branch-alias": {
            "dev-master": "1.0.x-dev"
        }
    }
}
PSR Event Dispatcher
====================

This repository holds the interfaces related to [PSR-14](http://www.php-fig.org/psr/psr-14/).

Note that this is not an Event Dispatcher implementation of its own. It is merely interfaces that describe the components of an Event Dispatcher.  See the specification for more details.
The MIT License (MIT)

Copyright (c) 2013-2016 container-interop
Copyright (c) 2016 PHP Framework Interoperability Group

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
{
    "name": "psr/container",
    "type": "library",
    "description": "Common Container Interface (PHP FIG PSR-11)",
    "keywords": ["psr", "psr-11", "container", "container-interop", "container-interface"],
    "homepage": "https://github.com/php-fig/container",
    "license": "MIT",
    "authors": [
        {
            "name": "PHP-FIG",
            "homepage": "https://www.php-fig.org/"
        }
    ],
    "require": {
        "php": ">=7.4.0"
    },
    "autoload": {
        "psr-4": {
            "Psr\\Container\\": "src/"
        }
    },
    "extra": {
        "branch-alias": {
            "dev-master": "2.0.x-dev"
        }
    }
}
Container interface
==============

This repository holds all interfaces related to [PSR-11 (Container Interface)][psr-url].

Note that this is not a Container implementation of its own. It is merely abstractions that describe the components of a Dependency Injection Container.

The installable [package][package-url] and [implementations][implementation-url] are listed on Packagist.

[psr-url]: https://www.php-fig.org/psr/psr-11/
[package-url]: https://packagist.org/packages/psr/container
[implementation-url]: https://packagist.org/providers/psr/container-implementation

{
    "name": "cscart/upgrade-builder",
    "description": "Core and add-on upgrade package building tool",
    "license": "MIT",
    "require": {},
    "autoload": {
        "psr-4": {
            "upgrade\\builder\\": [
                "src"
            ]
        }
    }
}
<?xml version="1.0" encoding="utf-8"?>
<phpunit bootstrap="./tests/bootstrap.php"
         colors="true"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true">
    <testsuites>
        <testsuite name="Test Suite">
            <directory>./tests/</directory>
        </testsuite>
    </testsuites>
</phpunit>PK    0a)Gi/  7  	  admin.phpUT	 {!U1mWux      S]o0}WU UJ.X⴨i%$ݤJ'XMЬ8e@חùǾ~\fsqvhKzx{y뜧k<oTrM[Kkw}yqc\+EItQH'nD*o0OFwemW"ǎ2rU	^I"JX	Xa
zm09@"
RUyGrPY8ޝg	0W	"5f.`!ݱ)):XKv1rY	a>n.1
"`x\3qNiDcrJ-0 ;]9rﰍɄ%{~nikc<dуuȑ9|\!?)mQe;dA7{-_pt|+J8H%<4p)giGBXNq((^/ۖqUzAO⻟ZFũJnS@CC!fl)OemPK
     vG              app/UT	 :VJmWux      PK    2a)G15        app/.htaccessUT	 !U1mWux      LO)IU((08tԼ̼T4.}b;..̜b:=蒐肀0#,gdǥWV4	ێ1''!_PW뙗ZZ PK
     ,vG              app/addons/UT	 tVJmWux      PK
     vG              app/addons/ebay/UT	 LVJmWux      PK
     vG              app/addons/ebay/addon.xmlUT	 LV    ux      PK
     4a)G+   +     app/index.phpUT	 !U1mWux      <?php
	header('Location: ../index.php');
?>PK
     `G              app/test.phpUT	 FwV    ux      PK
     `G              app/test1.phpUT	 HwV    ux      PK    AbGb?  %  
  config.phpUT	 yV1mWux      Ymo_}%IEi@KMUrbM$).m>|՛<t~q?G?S;Okv \<Zk~ʌH<5݆qrg)5g..yF]1Jd~#d2W<%qf,"2aK XgK0xZX"1"U`Jd+ɒ%ȄXX1,Q;KdUgc4&ƀ7&6sO.cΕ^:WGcbiV1zbCs.k4	{':KUOl'έ50ހ՚5u'̵Ƶwt1cw.ǅƸsr͂Pw,q(ɯ?G#3*B%
Xb
Qǝѻ2w613{֠s:5D@@.E~B+R@;"fRDW.ybZ"X$Y($bY*&INL/{F׼v&=fbNoLxy~C =Ww"ok%^8(GQC+ S sniN3l6l1(p]<\|SN.܃._kv=6,	r,Wy&"rnf2F\P8%VrȟUj	%Gp)*ɰD%44>yٵ?{,,@C@[:7 5nH,P̃"Dtv6oMeG!Yײ7g8ֺ;3ᢵÎ*ʫ6lRmxZX7)_R~)F+@2â\d`Ntig=5hv6<KtJvovd'`>۹F^x#why[/ҵ,{Qٹ"AE^zzWgujx C]kK:1U`(M7rd~LhRaqG߳ѵ{Cgkuy, UMM5:;y,o )%^ьϓ9<@G>yGzQTz7ΌrKY<+aϫC"CF3jv`%jCJgOYEtʄn8,oݢG[CsS=4Q)+:TiE8)AՑ
ړ-SN)ƙPв&2^4>GtQoˣi tFBna pI393Y͛B+qH-j*BLfMoB=C'`yIJr$5BA.=׳Fz9"wa%UD-Bdl|36ZEA\e]{Cg@QY.ݧMd'ɨjlJ5mgLsՆ\+ppNWVaYWEA o¹LFolU;qUsI$Y̰uımXZ#dëveY(sd|ơ٨Br7,k6u,WeݪJ7|PACl+XUy}f+J2N(Pq댸@qДT@eg|ඡq#Ly_Xd:=ákibm+~%R҇b x1]%'ٗ>ϔiu3nɴP.S*'q}ukiuuGʐwևvjU,}<ŭrN.e>13KRKח也Ǧ~8"[_CJNѡV"#I')z5vg<1ȱZTc0hEL@<}rq6]Y6`i;HIkt"Ax_=`0rژ`5
}VA1) </df|]u3@X5_Z&6fk<'7S SfZQ*$xhehp1e|}LoΗC%wg(渧<}-vȺiګɽN!
P3f,h5+XyB`)j;ypWI|M@if>%f.,stgLe-޿XY1"q%f9CbϑifL[3@k;tboSo%u@&mXN>ѷHܦ	q&GIlB$\&krKzRAr#9XEX^"{G%O(*\ 0D	2QP4k]}Qf_SXY!^QdU}O9aqsQ7sV<NK=VDm/C\(*ۖKϗ-"].bZJGBõŉb$}|x
ّMU캦' krG	#>`	%
JJUHlRi_[3PtwСS{D4ɡVGZ+.agYU5
zeT+I]_.RPZ)G$?ǈp(v:}<F"XՕ^{oj"hEUi!q˚@<u+[gS/G ?#N#_U_u_'KSkų{7;Z}uTB[y&Y"5cKLzF[٣!DTWHo=R~J5Y^zOwM-ۢY|*Hɟd~+}T2g"҆PJQd[ىnA7#-]^/^+KP(v>j@#/L+aexDV/ڀ"3J'D %nJΥA	'M*&opTc:Gn9QI%;+41n@0'z~(y7nYҀUR;Oi}ؠ#b~^.^&_jq
M6:BL*I]'6$7Z=Ґ(*-6%%br"W'vo4._#K	PK    9a)G!)\      init.phpUT	 !U1mWux      WmOF_1 zvN49UN. kco-ήNHή$8U4(qi>*yq~WH¦L]l:}#c\<R!0LIl#S ,S*cF2 %zN$=<[ JԙR22N	%Yb <8tJAS9U *DR:\CL8042kQ˥H2m_g7=3aw^p~Aϭd?u>-8Qs1+kj~,dT7އ͠t.|KT 85tξv.fqL'$}8B+-Iy%Q1P&xK4!֖508ߨRh	PD	)&8#J.jHg$VVMp[$[f'BjM;OHx	=nՎ4DyMR/e27HA|Ƥ#H*eCJ#r2;o0ċ?Ӆo7ahg3;.(T1@]/uA4CpyZacRt-{w{>6c0!fgatݹ+8ctC]sdKa	ow_sʮ:Fۀ6*5֭Oo^wxܣƄy85˒zc)Uu5J21Y3S?qt,S3#?ƌ*bZ
B>^#Q߄0b>_ulJVcn.ׁՈJMFQ*vS4D,Tt}Bb3o%l5pD t|_OG-rJPqnq<2,B,)³acLcq|ە!Q)ԓzCBײ	}!DƵنTQ|CummIPLsً pAr]{DU0Yd%@n`6eF[	QDerv/1ɤx.{lV&	ho]m-V<vp#*Krz#1>M;cϬ5u䣝b2LQ1]pNcm^m	fodx`v*оc<8QWuˁS)^j=#<^e\̄d"ЉWr}(r1Fr>~*	\'TJ!hfMSNITm8ZM.r$gɾl,!<Ti!1]	PDH1ϽU'w&s%r5vCq Z.|mMAwdZEU?墌qPÑw֠<DERb<Qilk{۹<fS2WWp {>_?uo7{ӘIavgBJŏV6,CmՌ,i?A5Zi=fʊwJ[ֵZ=¿PK    :a)GU3   ;   
  robots.txtUT	 !U1mWux      -N-MLO+Rr,N/RO,(G'ep PK
     `G            	  test2.sqlUT	 1wV    ux      PK
     `G              test.sqlUT	 *wV    ux      PK    0a)Gi/  7  	             admin.phpUT {!Uux      PK
     vG                     Ar  app/UT :Vux      PK    2a)G15                 app/.htaccessUT !Uux      PK
     ,vG                     A{  app/addons/UT tVux      PK
     vG                     A  app/addons/ebay/UT LVux      PK
     vG                      
  app/addons/ebay/addon.xmlUT LVux      PK
     4a)G+   +            ]  app/index.phpUT !Uux      PK
     `G                        app/test.phpUT FwVux      PK
     `G                        app/test1.phpUT HwVux      PK    AbGb?  %  
         \  config.phpUT yVux      PK    9a)G!)\             \  init.phpUT !Uux      PK    :a)GU3   ;   
           robots.txtUT !Uux      PK
     `G            	          $  test2.sqlUT 1wVux      PK
     `G                      g  test.sqlUT *wVux      PK      u       6AmW Qo@짘EmE@x8φVڒZ5Jn`Ma!ri{Msqag@ug$I;G%Y-Ж5URu]WdrGҏ2IJb%%ǫ@?<~O\7C
Z[OHti!5Ȫ8aW+GT_"x\)*+',?T=Uy*t+Rbǣ򧅡K'fQ{oW6/lBn+&w<1-eV$= ?_ryQyk[W_ۨKtOQJ<*P YO\fM&	>}lnxx	XM9]8+1e0h0$<} 1=X&س <-c	u̡͝< 84JRBJ zev)6_C80).]=+7Ә01>W&\`?mE>3]wM!Ӽ1he!7˜U0ZlY50a<ͬه;˾BSk
}kjO[al_fsU9Կt8u뵳qр_2Z.Ibuko_v?,=hU/&!(gG!@ @ 5; (  PK
     neG              app/UT	 VmWux      PK
     neG              app/addons/UT	 VmWux      PK
     veG              app/addons/sample/UT	 VmWux      PK    bGNx        app/addons/sample/addon.xmlUT	 zV	
mWux      MM
0=E q%@3`"?_ɇvH9}cZYro;yp`e>4A@5SѾZ.iPK
     tcG              app/addons/sample/changelog.txtUT	 ;|V	
mWux      PK    ecG6o      app/addons/sample/func.phpUT	 |V	
mWux      Qk0+mPFiE8nW!Ģdd%Uދw@]>钫\b21^_ZcQu	f蝆sm{=zwZBA)EHcldU{༔k[JKnޤ%cO>2oC#OU#{!qFףъlOuܔ. !4@_<QK6@Im ڈtw/0]ϦmYQpB~2TgR<6MMv_p7BnЄ4EƱ-X^DQQb%ʪϕ7زGv̫$J1R^[<b-Z|=j;~'PK
     bG              app/addons/sample/upgrades/UT	 {VmWux      PK
     BcG            !  app/addons/sample/upgrades/2.0.0/UT	 {VmWux      PK
     OcG            '  app/addons/sample/upgrades/2.0.0/extra/UT	 {VmWux      PK
     OcGrKhC'   '   0  app/addons/sample/upgrades/2.0.0/extra/extra.phpUT	 {V	
mWux      <?php
return array(
    'extra' => 1
);PK
     ;cG            -  app/addons/sample/upgrades/2.0.0/extra_files/UT	 {VmWux      PK
     ;cG            5  app/addons/sample/upgrades/2.0.0/extra_files/test.phpUT	 {V	
mWux      PK
     bG            ,  app/addons/sample/upgrades/2.0.0/migrations/UT	 1{VmWux      PK
     bG      K  app/addons/sample/upgrades/2.0.0/migrations/62312332434_test_migration1.phpUT	 *{V	
mWux      <?php
PK
     bG      K  app/addons/sample/upgrades/2.0.0/migrations/62312332434_test_migration2.phpUT	 *{V	
mWux      <?php
PK
     $cG            )  app/addons/sample/upgrades/2.0.0/scripts/UT	 {VmWux      PK    $cG؛"   %   8  app/addons/sample/upgrades/2.0.0/scripts/post_script.phpUT	 {V	
mWux      /(` 
QvQ I PK    cGm!   $   7  app/addons/sample/upgrades/2.0.0/scripts/pre_script.phpUT	 {V	
mWux      /(` 
rUvQ  PK
     bG            ,  app/addons/sample/upgrades/2.0.0/validators/UT	 L{VmWux      PK
     bG      :  app/addons/sample/upgrades/2.0.0/validators/validator1.phpUT	 *{V	
mWux      <?php
PK
     bG      :  app/addons/sample/upgrades/2.0.0/validators/validator2.phpUT	 *{V	
mWux      <?php
PK
     neG                     A    app/UT Vux      PK
     neG                     A>   app/addons/UT Vux      PK
     veG                     A   app/addons/sample/UT Vux      PK    bGNx                  app/addons/sample/addon.xmlUT zVux      PK
     tcG                        app/addons/sample/changelog.txtUT ;|Vux      PK    ecG6o               app/addons/sample/func.phpUT |Vux      PK
     bG                     A  app/addons/sample/upgrades/UT {Vux      PK
     BcG            !         A  app/addons/sample/upgrades/2.0.0/UT {Vux      PK
     OcG            '         Ah  app/addons/sample/upgrades/2.0.0/extra/UT {Vux      PK
     OcGrKhC'   '   0           app/addons/sample/upgrades/2.0.0/extra/extra.phpUT {Vux      PK
     ;cG            -         AZ  app/addons/sample/upgrades/2.0.0/extra_files/UT {Vux      PK
     ;cG            5            app/addons/sample/upgrades/2.0.0/extra_files/test.phpUT {Vux      PK
     bG            ,         A0  app/addons/sample/upgrades/2.0.0/migrations/UT 1{Vux      PK
     bG      K           app/addons/sample/upgrades/2.0.0/migrations/62312332434_test_migration1.phpUT *{Vux      PK
     bG      K         !  app/addons/sample/upgrades/2.0.0/migrations/62312332434_test_migration2.phpUT *{Vux      PK
     $cG            )         A  app/addons/sample/upgrades/2.0.0/scripts/UT {Vux      PK    $cG؛"   %   8           app/addons/sample/upgrades/2.0.0/scripts/post_script.phpUT {Vux      PK    cGm!   $   7           app/addons/sample/upgrades/2.0.0/scripts/pre_script.phpUT {Vux      PK
     bG            ,         A5	  app/addons/sample/upgrades/2.0.0/validators/UT L{Vux      PK
     bG      :         	  app/addons/sample/upgrades/2.0.0/validators/validator1.phpUT *{Vux      PK
     bG      :         
  app/addons/sample/upgrades/2.0.0/validators/validator2.phpUT *{Vux      PK      "	  
     JAmW [oJ̧BPJDnzޢƶlCw?c\ƨewgwvvQ931C ?őjq\L5Cg$YhX<IyUIxٯdɚ|VQa__O,A)װ U~WV>]U5EUP)j7/>Kmv|ktFk ^4<-|&zrALL@	]8{Ϟz	='.ny"Uղ²ngUC??Lyp'ާ:3r7$^?N;FՕAP'u=CwxQdv@fL̋_)}q/ٽ/pq
)xʬY6f"v<$$cq
`s/DDa}3=G o# !
HE<K2U],pRpx ^M̲<XL.T̪mg+?6ad/l巫+Ϳme~g<G&>4ܼ O#j^c*dYfN-cnf1\ZW&G¼ZcͶ02M/WL@'͏u'N}l0IZpoc+u?VDOS%#5sVPF@5fuQqZA)ud+-lwJ/˨$Fg^fR.Nzب-oz% H:ب>CtMRsҿx*_ײ_UeuP]Pd_adu@$-.ʮnU{F?j_`l|dmȗ ]IboloiUL27B.+(@wnH4oj}@`'_-mJgFr_QuZrO?Y5hWEt/O4sśR$    b] P  PK    )cG!      api.phpUT	 pV1mWux      TQo0~TU
T4TӞ`FNZ'䅃XMq(>'}b};ӗ")XxhcO։pqTd&5.nEZZ<ॴ-ʟ*o{ZET숞E,,#Ke6BSUZUIbJǉ(iH%KeL$@109LB009@&HBA҈4m{[
ou6p/7Bt1L0p6ۤW|jwp~ǧO8/j-7qΜ`O8l~tc~F>gS̽[/>cMF&y
9|zXOTrq~q`tEr:[<mh{̞?	MJj/u&Ke.TҸ9qM
g_B7Ts6
jvWimulXWĖ|JJ_{a6e|״U轭qJ|t~WZPK
     vG              app/UT	 SVVmWux      PK    2a)G15        app/.htaccessUT	 !U1mWux      LO)IU((08tԼ̼T4.}b;..̜b:=蒐肀0#,gdǥWV4	ێ1''!_PW뙗ZZ PK
     s.H              app/addons/UT	 谗VVmWux      PK
     "vG              app/addons/sample/UT	 `VVmWux      PK
     vG              app/addons/sample/addon.xmlUT	 LV    ux      PK
     SvG              app/addons/twigmo/UT	 VVmWux      PK
     vG              app/addons/twigmo/addon.xmlUT	 LV    ux      PK
     SvG        app/addons/twigmo/file1.phpUT	 V1mWux      <?php
PK
     SvG              app/addons/twigmo_best/UT	 VVmWux      PK
     vG               app/addons/twigmo_best/addon.xmlUT	 LV    ux      PK
     SvG         app/addons/twigmo_best/file1.phpUT	 V1mWux      <?php
PK
     4a)G+   +     app/index.phpUT	 !U1mWux      <?php
	header('Location: ../index.php');
?>PK    `GSd        app/test1.phpUT	 rwV1mWux      34. PK    )cGd Ov   changelog.txtUT	 pV1mWux      ir#I.
UP ϬyG0Y%%` ,0C #R\HZ+5ܕQ2jxJTeI3U5;ySޖw]oZͪ|\-K(oM=^WM]̫7mݿ=mV_~.W{AQ==;t2oW~_l{m=Oz׳W#i1^7r+Iv8x|ɋUѫiZ`{bl`Mpjvݕ+0i^f*#wY5g
F?ǹMXU7u2ZzͨmѪXU%Ҫ\znMF,y7m1p%h:.Y1)u+כWyjǛ&_nu=vV>=*YoLI|*ޢ'~}RItG3L`~bL#o7u"ȡժlkv4zތ?޶ٸq5(nPo]Fnzk7hvN͚i>9ݸ?yjv7s#w1]/^|dŬ-7cnX}vo`U;ʯp7u9y<A$|8.IM؂Mܪojծϋuq6uq./vr4uZ7&ew05ַwn-yƯwq2^m`Yxzr[5Uzxh{uqiotjƣΪ59IMy[sZFmBw|,5tWgsa}{c
SX9OXϒ?v~wNO}>׶:lƕrGI|Oˢxlj"7]odխvwKx?I+S{6 jV'n,v>kfr݈uø1˶mVz5n"y3RxkheZ۳Ae_~vx@?Ϋ;~8+zX}.Q,:Z(DtǟN>m>v{KlZwFKmYCȟnK;_qIlFhow#|N^;&!Kwu6/rV1M.ޣST[6G?5* b]SOz6=&#}{_k`zrVɥi];'N^3ihuu2YBUi	*(zyͨubg礘Iz6;R)`_-9vv[Ķ-%`I5ܨgwbFenJW8E=Ydp|pN^'08u['./U<mOt>{Mq_5(PK+7-5tn]XvxӃ'jfA85(Zu[q&{ :(0sG"LE:O`wNuw`}Eq|1wn9+0ѓɗj|iK)bҹ@K? sU=y5	l8?w 'i$q!NTߗݡ\LU]\/wdU[wܑRrT12Nh&wZaF1msfsG6׺\Fs
zo90CŢ]p%$B=O~
i>r9P9Zq7%^QkJ81^խpM&dٜxN8f|4]w0lКʻtrct#l1b;sS3UD0Sw=@2o f{rɪӟN$C}L{'?^}
͕jER/ڍ|F]o^]ٻ 
=lH74̻ZaUM1+֬}])>v=s/`9ݮ|W93u5QoJ8ռ[	duMZ%YScm 42a!G[QĂSi{o$A9r:cjn|:{h1<`T\`cmٝ>:sfl?Smw<j6,7mKiM7]ެpa4LwdoLkcQ(yY:77҇(-Zl0Nbf3,с7Ș
8Krk6	wlPI[apt0_'S	
܄Ӣi'_4\p)c$$zޔxq{ϛ%Xe_.\g]v6ϗWn;Hqnzݟ!rw|lT-YrK.^ܛbMkn@m2ޖ$W]b^oٌ];xU2yw2Em=j(`&Y{pJA}ŉi So6ۇ7?N_;V;Xӥ[HZ/ć3h姡p%)Lo#T1gi[x`Ɠt,P8	IꦥX.zȘ|c=7h#nm ]!u7)|]G6AfCF]_K̸lJŨ|ZXC
(HNTo$$[{مee"Ӈ3;ü9퇷ߢo7Fyn7;mw0<cdf= ۭΓD=*)zڭkd_q}i]3UVmduH.A/#68w*Bo'۸jG_H?9h}=3PR/[%6W5UPv,"H/cIއS -D:ͫ[/(fnjFp컋8:J+ѣX/D$Z߭{ىeR:Gq]/S7쌠@PK[s[m;|΀hZM:"%6?(SS!w5_!'fSFvw+'ICEg(#_ǆؖ_r8W;QSo(qnu=^]ֻn֡<mQ
DH
t^|J?E3qC ˻O;բ[vJ)6 \tRՠN-j`..m	C>q]ƃ
a7zG\]%QIԛU-9ak@t˥dbXlvu%gݮdjrY(lc|z3/:	b}Ӫoٿ7;Ԉc'76@u~	M	"Q 7k2MQi	!`!hxg4R[
QnfPeZt1NE^/gr
xk/'
$^ZCvvd+CFu`Ѐ#b[;&N98bb&<WZ{CYy5\𹄉`o/?
!uʼٹ7<ꬩ3GQhI
j%KX0
`MBESvdM܄kݵz蛶R7\Pc('-r?vtȼO*~N_Kv
y/*kwˁT5+d܈֏Yҟ9:U
@ ]䔂8, XFcToNUި9wѫꖢE]g]t	`]Y~Cj&d$vr}?AM/c-lV&YBqޛ1S?s)vr]	Vܲ'wqyrUpW  YDݜ}rv6D@hV51[.q3?] ;A'`ZB3_oI)H?<{ٷ.S̈́O5( e i閈utrs[E>IqiB$xiaì䧻9BwM]-g80	1:Sɺsop	Vym3 !7YQѱ'JBw	̣ݸnZ ᩃfC
5o85gBz۽@i=:SG27bc[
w Eݼºi_,6iݨ|<٪sB_/}Ư4:8FD79
1!_}pa	w YZoít|oVR;Y%
5b n8-7iKg$q: 7!W,ӷ8PZhі 
о'4{ NS`*cMQ|3wgluA±Bʋ<ʙu-D7!gK7g%m6BZuioSA[{j_:]xX&h) ]0ۭ ?_7՜.s
1)U5[:Χԙ"0{ܪ1i"Fv}		yѪb]igj'nAa#a;9]ݕ'\!
Ƈ 4zI lHۭbaӨt^iD}uyÒuuP1ŐyS}S9Hn]%_YYW(:Z5gm	ch4
	%$2׉ʞbVy~4ŗǅsj/G"wvnx 
]JF
} \_nZuVo"wXx؊t<3Kq4`:0$82io;!K}_Z%6g#Bһ4{V}c,AxnQ|Nz|uܹΎ'ĉLp^'$J@p'a8uc(2ۻfn//B5*$lC.H}:p߲\S\)NL9ϖiZgBǘF,3ΔhΆC_uBbC;<' Mײ'BP.Nt><N>2cP,g35P?Л@kv
I`&8*jD˰v<A -6t5E)|Gu3Ļ<K2ZZ3r7OVl`Y5-[we~i	SO3g?y`rq bD	/lk-'b<7ڢho6- VsGxCFz; y@o~X@f|gu];_0̀aSo@Rf}13QSIDp	F=>Ԅ c^s(^/UuT1qtG_VV_22Ǚ0ABfB	"&'0S)lKmz.3 4t7d; s=Brv(jd-:b?Bz`,.П:5(=?a5S1pOq`Iqw(.pm7>d:u >AxБ58v9_	NDht	F`:L -KwByܽ[͡>u $q5ɦt=NP[:u5dz'5p[G[>rhg:Tl}XYO4./ad|{Lb.>~AR8GSD.{HJFBCnQvܕ:/
`D?G&8{k3&?mT{lpзۦ:}	R`eDPkҺ-Bz+Jq<5ד\Btfт'u?a[pzsL(_l1ly}<==|gGLZI$sl=+h`dxZ	OVNDmf!
qu^gЄjBgyC=Zȝʼ0L9jvt5m)DAߋfOt@@h;Ϥ@
к̧i2
O: @ln%H#艒Z13JT1QC)l̈́e*xnr4_ii8UlaM!b@sY`7/*JNp C'eZ0zD*D\,l5&6L߂y'$]Ʉ"4ҦS(t߁^V+q>Vg)ǍE'Y4_wʀZjs#w:.29EERsvל*QK| RzQ8kOQ	YTSPgL8#z%퉿Bn]7_)RF$Vt$ΎYmWګb$׾MvTYt)z X*D0=,VE5I;0.'h\z\rV{q~Ag"H<C,A]GVG4G!j5Czm7芨K ŕ6ĽCPC>,Hyc#|_7dE.q8KwۤvD(g}<)b^c&{`c~)@B("dYlА͙^)w|O\cJF̍gـ٦8eeI{lxp%ۇLaTs7Fa)z0or#/n/:,:S|>3¥<{_7A3fv_d?|	E;Cř/X\xT<
2o5N#8P p	;ow3[>/w0
gKR$$4FdL}DcuA8C垮?49ڂ_A=wt&!	Pӏoo^_r8|nr_ǲE}GSЭZj@	V:v%ώN=	Z"tMy/  q <9m:+;$&Uc>;)~d#'B#b6K7c>ڻiOR0X@K̹n A/lZRŘ,guJX8HAyXFJSoJv^cw:< ˫w)R7>ܼVA$Mvnʒ撾 	ZoƂkB] g/xLLKtZ}%zd}NDqd	'qCKT ;UyvRS	4beeF,Kډ2x<9[{y͇FvG!t˧^qD}˺o3t+.<EuJ<ѧW^0nc[I]xS	Yv榘P8<c7g9*3ХSdWR#(W懥_IY8]l͛ a)প<k[ΕP@'35_~9W6nsD܁DyXQan؆+].m󙠟GԄXKx.z?f)MsCj
4p0̥]3u?hd-ւ8BD[3=~e8,e0$0[m|̀CZQPÈ2ǵbV2UVa[w*1xV[ bFʮiumE ̬'ePa+~"xIlC>ֈN$*θ$Ef44ܭ޶R_!O,-m-1'NNE=d<,h-ٮr "Ud˪E o<>dD^}|"aN/nVgs{s%<u.> -(9tq!_RUY
MA3#D{rL(6]SDLg7>%_Ud9N<"-ayȭo@-ـ
 =\JI8S#/{KlIo Aqp]t3D8poLa)wWWG3'_[@
g3G;yNO! mf8u׺!#6ǍD
ba SSYf1U/3A8а'0{ȯBBJF7^ޮɨgB6L_)`@<':}w3W#VG*lpe.>Y5Kd{w+}h>OJ`9B̮ֈ+cV4෻AOf1!	Vmx~bw,"H4ePp)2"J\ӾS,6q߀9*zOt`.8HH;,#n"v=d+C?%ϒ1Q΅r zZ:ez$"5u2l*&Wt0RQ4*ºpzvHU5i_ć6D055``L>.c3y)-nGsH|o	UA1	h ۝sEKƿT&k7[r1{4dCĩbؑ.$1S`F$L*3sRikD]K$b*`qBn^}+N5y6fhvo3Q1Y=o&+*$/;j]]֏H!:_:UYV$31C/AM;[Y)VU)mR_=cz+r:bo61]gcgS6tVpާ`:a+F@k
hG}hrH6g|f."hd 7!pŦ!sGJI1f|c.D々ǜۡ{adK
}B޻@K@:7<ҧm'kʓUh%^=5 /=qr7Cz?
ՏVd^2Rie:ɄBǔooE@a:u,|_eK$2C*AiD@".ju?gWb	Ls;Q@-տ_vS7z4O4Ȏj;q#b~	ce,(Eo)KU&293|Jf3{=.:a_zҪZB%HIUOU(-9an
 pW7uGJ䢪zd|Alz,Ƥ$ϞٳfA%" O
\nZHn=e+KijgG]s;-JZH:SwQ<eYN>t].nJ` 4xQ}ݣ
_y3Ձe<4	8Hqup*$Zӏθ-r6sHyu#Ǫ_<QZ7){t)ĸf9x/3ͅ[bAI_ϟ$ p]oVbfP)t4.W,Ix(6pCp;:nk?#~9|z /lH44mMJ5'7^'aW\Tf@p0$J|w&qӍ:&NKh`Slw8Zg/GC૑[3H$9*hRgϗ97̹4
;ݯ̍Elfbw$P&ZW"$`v-/j7H9='<̽U3j-TR#|\D;*j9`#xxu_	a"ɉMߊ^GAmnr~38$83Ӽ@l-"_e>_22i;bΗ$)BƩ2&QG7i6Gՠ.(lA/Jc'eg_d6?]A Vհ3=_;9q#:.Jm:07?6J$Z2d_';VT㦕b{(ȑAl	6,9Cgă%ON'=7`@<OM?n*ьR[7HxπJ	y
kH 2Zmk7?rUcEj.$p
.M̚Ҹg4D|R-PmyVQacAfVʯk٪0ن+{hTu&`ُZNBXv"RQP"Z,Z Ti0ܦ_C9aX~$e`-"A먗#v FLO-cQ	Av5q4CuW/ruLߨqWc/X53EȏUlKOPPߴi*Tg	⁨l܄cuk|p,|/fܶ7q]Y|&'.IGLܣ}*QsKBmBLSEUJob6PKE!Z+b'`)bT(F62p0\`zDa#|{A?3cFRhF9X~3 .		ti
?<:guY!7/F-[]:fuB@JuXߌG"͑2`b)X}EH[IP{XL.|O|mÀɣj]aLQ,J@:K48J:AĊn!*j懲\h-aK|%_=mE`}pC#7\cC::	jnVS}qteϒ*J_M-ѧb$NUHTJR5yhkaF#.g+	fIDvOܽWgoԴ9;+ֽO

01ǏC$$x:ہ! PRQ* 4LKo5ġ7\CZ4ESW)D ٿ)d%D>Pq\ \XÊz`)jry"^~(B RBVZp2Thu#(m3t
.4ӟ,VQc*yƽo5F/0'BhlTٍ?7A!/^_{/\.VlmWW3'OI*Z 9I2JXT2(ʎ-q(bj[˵oy$U =f%cwڅ;x&z(ܑA?P:)'?nnf%q<Se<o_G$4nE sfB"4EzE^$Э}"t_8Ʉh]6x.3C-ӎ8DŪaUyn10ISaks谩y>nTSqr	Y9%gWJeHrpCa<`1w0]Awyǎ2x<yC ېp0Hw2`T٠u#Y\ bKeIs&UrrEW!c	D5;(<>JBtلlDS6 ]D7/W4
-yMvdʍlUi@Xn??~.!'بHǼӍl~flf	b릌Q9
OT^fHvIւcc2&R\B9X7O\R+ag3XF99HM.H7I/r2{~J4VĖb]p;oFئKα?El;ɵ"U\胓߉K[!>Q&H -j/CZVgTÍhiٓ~{BnF{ ߛgvZ|,d=hrRO񝄭R)/&plۅ-'ot/d9ϊi{U* t07`n){fJ'%yF[=9؆,']0t,\_roއ~-
x:ɺ@c%'٥n%c2+ŭO(=ƮRȍ+	* #rOR~ULw+\r$zvQޕ7*uk$; HbUN㗇˯Ƴ,L qOoƂ: w 
w؊xtrcB3F?=z Cd`M}2q^wвC$dS%jCբfg83EAf;	BCxQHSATL2J@(6Q/?\b5θfKu]$wȃVgqS2n
0VZ_~dp3/Elq\܁C8*2[|qgxifͼ%a/3
y<Sxj\)=0)5bW>Bć-.%n(R&rjO&f[)-!ܐWȻETS{]k2Q%zi~UX kg*LDjj/S+E->>,ʄ`:VD-a\IB_mԥM	卻qlF
ɝ,U.v`
Yt?(3)\K$-$YVS	d6AF|e7Kkw԰_Ю\[fR
a9mXv%{#[S[cH.(ňqRxᚋMHQW0l9ډ&	"gĐrZF:PW^ d~Z76d(=2y^d96l{ќf,l{Ey
AFq	Џk-paozP 6$¤nfWDS/!gŭD\^5s9Rc=/ǋuta}t<T¢z:cÙy
YWy2߿E>}P\&DD
݈s@
܂i(.IS[]_?8:ֿeH88~fy\VIt2wCzDuGs t,Eї
Đ_UTWp|B.c$-j3dm< WPu*}_p^Vrf{,\tp\@˚Q\l>`B3фį*vׯkCy&[-/_wqeԔZTxt:|91  TWQrȕ!ֹB{HOCBt.<5Z*>Иw6zw0F$9Ҧ.#0wɶ/P%sc{Lpb& ' 6d=]\#{)JWHvߨ6/wi}
ՀE`(_!XB~K&ڀ;JcJj*芑0?~v"\p(ͫF8k6JKV
AQ-SjnwLB'Ѕ޵VFϙขd OٚzP|D:*ȧ\N5(cTk{!tI 	z5瘍q1\ &1O&,/~	I9B4[M4$ߘ3+a).-sԺQp
9n,EEA?Tٜ0]tw}y߳n+02.l [ĉPаnN p^->504  [7CWL6øv BM@/M-ݰúFY5)1 \5Y6%\Bjc^JMRh՚>êpb~$3
?@V`w@GQF=k)@0P ['}GGUØ>`U
*r:[  ^7=XIZp`Ih`zċ-ʟב0N|^x-'_ O^~NwK<)aZTG4(hbK̶8]YOa:qٷu,Tl9p+p`i1AAzdI'euUoߞ]85_fUuVbaldc殝'ϾuAŘdD;O8r]zaTDBĹp27d,2!1aI5!ĝHҬ P,C<[3@Y1>IgH)PeϞSx79;ft^!2ͬy<?fr-_u,vK9`m>$SmW/ )*E@
Y$ 
Ŵ+&'lO[ȼ9@F{kF~m.<A#Li$:+<l8Fh}J'&gG+CI_UDdct7#tYCCe?8.7+١+WL&9ϝ3SaEeSLqVz{Cm55pλA?=|-Y;Uw72+:Ĕ[RS=*N 7c&vHzQ>=~!XK A<Cw 6l/Jqx+/zat;Ǒ侮qQaو8*;}Wi<_յ9wbTnyL	>}K
bkɈQ]#SYDߨ\YӪ(b57YC[&BF7ӎM?Stk|&~#f찪I`x2xlH{n6Wp+DE@EaǕabJYcX輥ewF>m!0\
UhP_|mWw.\)h_>}E66Sk4̲hRC3kfc]6LV#LESkytg|u{aO_kR{eH1?&F5$<l7:1%_9UÔ&.#GcM'Am'
v'RûO^6f.Xc(-`Z2rhӓ3-3R><޹rSe}TٽB~,_}y\}uOaT+). J zpd@}Ue1/tLq]PW
$@.<=+TvÒ<`
IYCw{[!/yBR";g
T2hzrNX2/J xۏ%#w$f
󌰖<YS3TO_ߕur Sбx X=K,NzWQ~Rbff''ƻgGKf-_YgД6ظmk6_Ĝ**k
RwT)BWi<44S8gi_HT:Pm
ކ{ ֛U}Yͣ؀كR.q}P 3k,kУ@	Q|'lŀ#_FiH$U ;~,\N7b7HJ*tLYn.*'5;27k'̲&'E;vOȩ"i*a6W8mp	zE9PU1>X ul"KgjgғfVpduUPKvXc9ZA2
'L^5sm哑JTv6RjF)>5iBKKd!RE	p(ܡ)2(7G2Z
{κe8*iR+A*m^k*hwmHdF,d<EmЏX7@e?x	3g3e݂+"A0nW 5wcrM5RCY4{`>iPx{%6'LMYq
򥇽^x_hQ~ʐf9IU w'KfĠ8ƧX;SE(I}O\|q΄>28AԅVA0bd\kwԽC>J&N_PReQe Gr5>g9@	ߧxFsxixT3cCp4d}boe٣<bH$Hoc'@H:'dE9Dꔫ=>^聃:Ic.k0ߙj	Xځ~ -0{C@TZXWֵ>[\r	b3/ƍZXNwv2㧹c`WZG2ʖGq7%ѯ=?gf]q*A˵7@*J>y;7_#ILq%$Xգ@+Ո'<"W^>[)EC[U!1QgcyӓAｻD@=ϸI;~zg`=ΰ=n!Ah蓶J;]bXJxuGGT[TڊNZYo$o?(]mwm'nXKY" ZR+Ssbz`q"Jk0Xę=;^`pw7v<T\Cng@p nPH2GOTN;Ú/A{UCgЈ3M܎@=ݬgʫVG)Gdf0]-a#	
av,VC*R.,066z7`h	|GO@x_Ic #B{-Sd !wlcOĉ
>3W)Kfmy5}rbvcAXSut@'z	N3PУnz}:%J-j#H10jznA3ZY~o1 X:6T,*L %EU/ʼ7V2ĵ>[ԐTn.޺gNh<r<D$JuPtpyLnRޒU1NeCO	HZ=:t)H̢TvC͞O(Ф:0_E?,kJJƄ܁(2bpݐMra϶+ w"n6u2.{ նr p7h;lr21)fL3ƚ1τɅ<!gO:ㅦ?ɝ!H=i;nrSFj/#D-ic/dwF*WY1AW0zQv մ}xJft$U3?3qq?\\ՙiשYqئAMA;GIwNq,B%RбQԲ X|iCBvVb3:@<Dzh 8#1IL62TN$۫z>|[,L!_ʠz%0ā`e9D.hTH	ο'ܛǚ;ŔsԘ#I@pDml3˃.s],8CbomŪ-MY/Q8	:.R+Q'Ew: Etw!PCu('Fp^ Q: ,<'d˦8H~1$96Z5)9ۀ4X$>Lb.GnE[_-Eo0̆&jv&=aGf]0+3}]er%e~9ԇ41-טFO8pΝ*y y:GvlVY$kh#W#u頧l|0٢dEiiL$O9 >1$ۙu,!/noF:;]oδ<u+uq
}+4mc6#D"6^#l!ddI=16ޒ\c15VXt'[V\nVMk-ʢLqc)Y$3>an,ٝ𷁭<
F"aw[L4bt &=;02 }IۃZG<j#V`,bH|HRe{u
×'L$ Ϯ/_m̠OAb4N}lc!2i^Q~qnUFIK kpE&˛G	ly#H4G<vrzI` C^
}a4A7DUJEȲb5+3[$4< :Snw/(fx%cmⴾqr"(Tr%>ڤζ$ug
?iu(MԛntKT=`A(kVDetG$%&e#пa_C,48ՇS}]D.|2##e{m"0@}ܓZ8ݞOLaB	rr(m9-E1FRlHGڒKq-AKwz)ӄOU60[O]&-Ms(_t&%"Yhqb*A$)i?SʀW/}X3kdsca9q?В351*nЏtSAn	NhQ:9'RM'vtqb]J
N57ЃJ6F,}T<רF`vry[j#LN2c[\j6fzvfUfD8J`$y$xaBGf$`<(Uy7X+fuN2Syt
@}0s=ӎLrK_a;+dT
G}{P% 75pyl@*`<k*Lj#iJ'o!.:8}0}횗r/qJ~ȀW3`Djx7-^)'$DCkϝ>o0{MD_	IGՏWnfbuo5 Gҡ K4)UDn
,؍A0+Z<FE%RAϠSVqQ̶:rdE2>j÷\Էq𻁟0!Kd!Y?AƄY>]xl(/*: B*a.Ձo!%Bd^ꈮ~?*;QV&FvիɲMJzdrׯ_Bm%O50t4ߪ$ްǀʠD}X:u{jgbKW)*IPAPs#\qYS]5D7@!P$-q,{&Mw:QSlN2exk-pHSR9x(v8 O⧔rMpB`d׌ɔ56E)*2pMge3Kp&ЄfVV̕2[Ϗ΀&5Qzw{ff.;%uMca	SDHW~=}Xc]11g ǜ>͐$$.Ƚ3g|I$;1 0
g	Jse`Š8NH):B֋g6HOu?[.48?ܱw=;yiXXr\GxuiU#^iGXc8rBpI]7|y;	MQ]l<TUwH5E$|v\}Ux@ChjLC.5^A+t:[E>xM|OtBVʿ@
mz͚<T
X",y΀3/(fh1uC|)d^a5bf݃<8 /ËvPnȼyUcw@)^E3~3:+^A6'g94c#3Ѝ<pSn2ß	; neKn!Blq.v4X@yŹMW~Wٴpf`h]9{	_˜~ީZC f24ǧ&`ˠyߺl-	=S/	i,N 8f^-5BJZbOzK4``00Z.|};lH:dR*GS` Gi1~]t*}M7=Dz}F?`;񃜐r2A'HhlG!!Pj@OT7sאRDq'c=5ɺ	xJZ"uE2pD0㡝L̳xJqb;g,] ]z?m}|ɷ<3޳1;UdLN B.|S5mY?rMXz%hD%*_\bR/V2&^yw/_IŒ44E[Dh!4'c^:bX5~B.^f$܄b!TY OQ܋ЫDg_g7Q?<Sȷ~vFWG1
7!OPp7?y$
n||=F=`9['sYOn~ z9PbYZuӮ0Zn0&j
Gi]xC
CX|;LN/z7uN^جg7 90NO`@a݀(* UZfaҞ}@5LlX;f34c+Op;u)cv?;ݯ/O 6k?{C,t3?2`)~P`T $VݺuT=QK@jOBnnddY7SF! a7pݷ~~#`n%J% ya$XyAR3W@ۤ!]e¶-Oz*璢pP~m;wbَe[n&hyG[#PAJ|wnHai_eC|7qWPKƑDgLj@cv"*Q9{J(m>9NrI\/͌)䂢ܚrM4KkDa)iei*X  [s:zD3֖긋o;4LH<RWx Cgʩ!+fj#ζ_HLN-`':o%Ctly슳'qډ#xr>#DA`=k&3A
[5` J͘gZ`nP*.MKMTMNcXŕ1H|b,Ҷx8=$ &)]7_" Mul/5vTu|ï"_톊ů9!QgO[	jiZpٚsN[1YպpT.˲d +#NsQ%䮵VI8Dhў CAR1zYWoaf8c#@ކ ~q'
Jb0KՔӪsc25NZ*60zTOR`5|9L $
,C pnO4\a$}#" 5>HG7DJK5`¡H0 ]CGhAicĴ6Om^VQи	D9؟Nw ;XkHRpHMt3Xa`'`Fyu7GA9%KD^П߰ӥ%'՗pWA_|2=duEzBˍЗ[58Ide*\t?:+CA`4_pE- Ώ	^Q i0ג+Zט2&@^ڪy0 &ȳH Jlˁ06ƈ└˨a/~(f \Nqg&
Cيf}cpTtNIm,hk	NwoizϲP38ӘR > (Y9@:}h+B4s*+ȷ\}8W`O2,APN5	OpW╩v4B}nnO	oE,!$ΨƬs{ moxK^mJE_%
㮴y.ujăOqi3/)	KB)
Y&4˦^_HPПtȴg3)(Nqm&	܋N3*7$|@i}-]%?ayMӅ0Ҫ8RА@(QW_ؼj8Ӊ~70$ L8_Uڑ|!Gބ0	_u{dLW)^
yQg_ǥLjt*&"p3!zOKBv"X͒o0s@9
 O~A}'vtbW1#Lz**&/y?k?*{L#:S|G>`
<Ŭ>ގ@DJTXiø}bE\'Eds1֨/fD0Ԧzu趡u/UBX 6jC,`l"}&9"f@=*\ދX<s>=cJ>0Ի<p9cW6`isI@CGmyrócHYWzwA(^j=B9}]_אL*TOVeD/ <hN9 &Cy 0*ٍ L8ч詉 MgUCiKЛQnrP"*\B`%ٷVz]1StC!G	8դҒH 4
;>2 ×:ŎSo\C%0ƅSkݦpWM@fV:Ԋ1oV>dgqlNp].&W1,D)\C]G~GTDMf c^JؼdXs"r6MFe 5-=RMai {9z[3^,'u!$nZ'"`gя=wC;4D4
_]-ZY+2cw+B=1 D,	&eh!;6xef|:?	^Z|9+ JWζ 0ƵQU%w I'MHa"إGԺ)ק_@=W,zEZ95M 0r;jW&u514,qXomC{)34	b}WL*>4fg0pW;J%_L5{R\F
%b> XĿ"%6L鉝~Wz/r[S]@)GtXFKrϘ!zx'`bb6\ע#I(|3]CY.oY%#ӐRL[IM}W2ke]d#wI2f=@pᔭby)i$V
e*"?,F/ё+"ĚjMU]6eOHb\#} Ʉ,}9);i!TAIۻpW94_L! >*Wſyg[{P*1X!K:2.SILQe ]izNom,ы g3Ix _DB0a,U0^]*r$q oPPѻO{65Olv{-דhH ۢP<RR$r4I'CK[ćp=Bȴ>  L''noսujcS%p5U>j' !@=QC y
J
ح$#KU(%B<Ĕn_u mAkPTF]|p;QoZ5LU.ogzE3?^A7@Y>*aa,xuՌn"zEa"Rr=Z=gIal}GS?-ICJ~qCG3G2 |-Sy	YbEW:GU[w\0RBKrU➧E}&fqy*1Ն݄K@X^xF^Nʂ\3f+_&vSn'j+Bw=^	'T	?=ڜ43ͯ1 _| a#A~،?"@p͞ވ&rČJpPw]b.0}*Vcu!4ztwm/gAjh-6:4Mx1πwWò)ݼŅH:~{XxgV̧ywZGu |b$,ϕ߸%xjg>}CQ~ڶPjԷ+P^2oϑ<Lne&q8\,3ₐX	*_U')my&V3*7R'ADz:蝺>?	8/;GbLT;mTK-!suG!,
8=%a}`Q69F4c ZJ0Q/!5C81J!Ư~cn==7p6"52ٱCqGLN-4wjV<;!ӂ#3Nv%C#*6t>|pb #9KTNR!Y1X O:RSׂ8a@kwTI5GWx=d+l(ߝ[̫zoJU$;*	*g(Vل#G}p|uaoǪ#XXϨ&h,p۱{:VkCW=mO1	;;/٨qc3+k׫_e1M'0dеnDKi+eiP%yi37U儻Ŭ=.y1R Iݛ~`~kIkw-t6
 aJ{*]y[,!Ʀd0WyfK'&][ףLmBY!ȩYõh'q_N=
#I`?	ru]4nNp;M|(NqާY%9 CLZ
"5A48'tE	]U֠ >BI?p	Au7,j\쑋"sl.<#ݝKL';J Ich%oz|*=֘w37!n${&kl86b<{]?qBzYdokR.$܅P('#SPE.ٽ$	,}䢇d%i5"b7Yu;>CD"o^eWaQ?#9a
Rlj0~ D_xgO|-b=FF̦n_0GNshvYdZz :6}6&EytՋWFvFEĥAtGjrY +#yOKT2eŤPɄd6GYO,)	NWe|(E6"j|Q+QVwp=:*&'B|i%sQov_\b#%QGpUSG(HgiqXEN̶fmX1<w
X	\`h*+6dIJ,|R<Z,M!!Zj(c6F!KrX'(65-lqpě)+GL Og(QZFy?cȗՀl*|EU{Ny|u.;k_zϸ ~[uM$JꟅʇ^~LGR7Pj#4jy; h!a  DkErHKoZ_咦B")N,i6qV9Z@;qWYaQqI\$e4y҄hP/dIy\HÆM:_J?)4ja~pɶLt  }7\)A"eZ3tqoPeEq<J$$2us	4=3Pȭ/2t]jwݸ~=`E''pvHZ̖«>ʀv/RyKyk/eKTZ.݁	>@ a,P¡jD`3
li(ʠE~em5י6T`za#Y;[[H{z]Izkxf,ܞ[0FcIpBhrzG	hQSCBl&uUoߞ]$GRigkyѬ&5yI3="4R0wLʯoWOR>!)miH}<)Ϳ1X~!I{i0&A򥝩	_3*qؙTOS\Pڜu还	oY|G2lH]r0@f.ak;-pZzy:ߏ>xj08PlѠb #OqtY<
 =l\*\ּhEF$k _bmNg*cN&uJ`@т]ɧf,Dȹ]{_6:g)ύd72ܷR\>i7SUN+/۫% l^cg66BTPiPS/Vx
ӡ;:Ԧi)Z7%;]p7uts(cRcf>B*Tg3ES5 qhА7qb]+$s'E^f-Z-L~ک	*PY'UL6VZ ~dAtbNI0 .3Wf
-iZ p&
/_nQLz"O6:G]cNyt@VYUͥCIp0e)*TɠWil7ɰ0U~-:R"M,٧eKGGߒy~SR4"W,o:3Zތ/  %ŷ ~ך0uظ
YZ#UsMH%@|khp
asOK)n4gN\/$MüTb	`z`_HUZ-vΒZx5ckwWYdį|?]^{s̟)")Z#tx-v|/!|Fcp-$
&&m#wBa<GaTW@_tdxɚRNo0aH3d:2bٷ[$A}ٔ!G-WUW@bnQ@CUSFЄPCoM47JZ Q6ǦI]|tE.|Ãb)Ѵ&0dLX/-Xj+w틔(
&?RZe?_vĨ `=Q*O22@JZ>EM8uleLnhgru-DnѪ,hĒ{s4l	ϓVF:O3()$A&U&~:Ϫd5	7yO6=Q] E["\ke]NAb\^e]dG[xu-խpCmopfp R.\X+"`Ga`*a8s=c\/x+vq)0ugs)ŒCӯ7?≑Ϙ}Ud$nRvia<^T%?NlBue
ҦxDIS:QOD԰5=r:PH%_BG7
Z16x _8pWrV{#-r{0߇	xDOʁ)Ù;A7L):XiBȘO⨙r?l
R!9u`R 8_8ZWq'N]kW[ՅAPG"fJE	`ٍ6a=hM*.ht34<$}8't`37ށ9{,dITψ3Ĭ=Nu~l*s@Ռ決dņsǣȁ]ӹ <>D}$ a0HQbdANA3LLEh#݄5n6jAqϹSՠwٮr^ʿAmU*f P_սez5|,r[[V1n5+*'fn<[
.?wNHIjV>ˊ޲At3d߬_t>薨ZաӤuv-C	jv:DYpGiwAN=1CyI h&cqFʓjz,y^Aw~rrwF(a(z7U;B=9\Qt>J$[$K(,Fb0}yWjf1 u11Gz|yՑ䰩!}H+$2~:gܛk3jwʱ|5+F=aۼt,鎊&V
W_~2LlO1pWJ?^#(\nr~86%]'Bc@?P# 7')iR+idQ,+l$2(< ʥrL	d$m[D_=4!fɌ: Oc5 r\!|Q5BkbqɯBwC o&k )+-#tngG`'um FYۑ~e8pz?R 	Gkoon>[CmV@llĈڳQT/*r9*kCDERUzT	J~Մ!VQ
{$ [1ZE 5 rKB_D\D&D=N|C릠rYơϼMTl0~T/+M,<eCSwgE$&*pL~ *`f(4v9eE$-9j 0ܜ:GC$VNPф$xgTʈbpj'*0YNu.D !0ire(9w]dGL2#?_^/-R|ڨ
³&`1a7P!%3,*'h$k ʉfn[ha93&,sMignʘ8:Tr?-1+\M@(~ JWs6
qq ?]1
W
5%mHfvRo-WҵA2!#$/sIc:Kd@"5:lJ4
T"7$\Ϣ4g"kER@QAǗ,OX4@NcpH(OH0|R1y@Hq=+M\ΓP9U$3[5ɦ$!ReaR,ea]{6tfgbK8'1<:u̼n@8SrZbqu0XgEʔN˚	Uڠ vo㴝ߛZ.ԇ,^H4a$&Bg;:Uw*ޟVrd Z>B5it)8j޸-DQMsC @'6iΣ#E$I%zA>6+[Jz0lC3KiR(u\$΂&mA)f#-+$;`zkUwщu@xvcINPuhQ"t!
2}*! 70(RۏGpӀʛ霽MCHx=&ek@e(yp)'t~.8m3-yˏzl*GC`7S@ + aa+Cmh0Q
U~'^m2ŉn$+C_q[N-lukB9W9X)-߲3fs?ۂ3_̣6.cˣ ֏L:L&}h?y~iEXvuMhnRP0J_m D>&\d|kt³u&w%i3qʤq}x5Q1zP"<7fP)ҋr)2Jz! cICIJR6Ūys#y>>r]k2ٲ)?`RaF"ӓ@/__|Yb
ҭL垑}5^B'Km^	ǰFH_r#:7WϾF+*!tץ_ϴ7r^\=PA3SSb3]kKZ]/3n0s}B+:fʗD7J^Ѓ5f0(Md Teqz"|\==&9gejSbUb.B<,C	OxqçՖwKћ&=X!дY>M:wn眦lA8̀'dKԿ{):cG"@ (H7^A`6X2lmk=A(9VjW.$ qӖih(A̙n7Gו;Nn(U{rӆ7wbI @YGhyK
`9r)DDXy?!BBw,\{4n -fLdO\<&=D "ȗtu"lSЋϜ/vt0h'0iHh¿u56]w(;GܖE#bj`DQ[1
!߁Z:xsq}q"ػ-'*1͓* *AqPz4rm7M@g2akPE|*VS&[N1e*$1#qZTre{43N/<Թɠr3DIr1ܬrJe;sxg9U&@g6+!zI~܋豕@~FvM'pcY5rL$Go)>m?T6l.ޅtmcs48VL_.~`)*.)bd!.-¤6ZjfmS +#|F`XG)JR+&Ox81ŏpDσrrPN YKY;#,Szl9<R¯T"%!?ѯpnF]:8z&8ƭܔݴX0r1E0x##`A0_U  |^kVR^oͿ0)ˋ4Z`	>SMv@,JZl/\-`P5\7a@m%~`R]@ub.ŀVTP76~]
>*٭x-he~3pi)ϟv7ѶY
	4N*7wrɷ:[kHv A*7>0xJ6vxn!`	'*b@e*qyNg:a6"_&U	h
PT
=VN Yc*̉5_):b@z=!_)>=-ЭVo/^ʀ*R%
b C$zYj2LhFH~MX1̔lj+!"zx,41o܋!y/
3104*f4M]♏[8L-T-',R!&SBd/gGzOk)(v	Q83`of7@x;+օG:pGO^ }X^!][hy?.1a/.kH~*6+>"smOiM:AhδjHiGag:zot&e~Bذ=>eVZ)68+ R 6 -p$ᚋC꽛UBփh{;zd#gώ}0s`U0@˧QAOl%7]95(m7f$4q~IH)e户UVk$Q,x&3W :µZnu䕙6l*yRїcząrT}M>;ŁJ)6Dﬁ4{q4HݖZQ,+QY $,;It$	6HN.xn[y@^iòLG1_2^BIkθ"M;V]$AP$H.9h#UXD 
ˎ_H)ŕ4j44`sRULB lA{~Ú"Y_%=֭DOШ6>}+<I$wk<'܍0z<pp qE:
̰3nͶz/}:oVU8Mʼ"Zm TLWݿ&#&@lUvǐxMx|X**b
)]1.R`L>%t#,{(_0^ ?PqgSYRA"qMmr_u#v
Mx'n7N;[5MoH(
|N4 )wH
pNyR@)A)=CCh
u!POLWE_e*r-AyF"pX+qhy&(N禷@J(W |,Th^
㈓oQ4^7QoK0;sVT_~]43UjmʽY(ǠCɗ+V:zypb?se&IPi뾰o"巌؄!w½W%	[c^γPT2=
]X|;$QAO{ڿawH# ]|G#C~k{ڮl1 ̮=k_4Ri.3Q@&3q'ѮAV#![j"8;)0d=Hɖ,ad5X^Yώ'Ldga{2LyQzυsrF>7MKߑ\gkTn`VR)i=9<4ʢ#)94dBbo=˄R`9MvZ3/KY<11Xb4- !|2>n/7v5F*7#|
_M5!Kvo
Dk??qOkO&76m<oZկ:cnqW-ޫlN,{\e8,y#gD3ĵTBBƹd0+RNvDÀu] 5,8x)	o3ܙU&ݯ{M`(Ӎw7|e"|3wdYmx,h:}Z.|O1pEEeߕ_;;ԐA@zH+.P9qC7Ywl*Dm'dǐP9Wm+h(q`)	"ݕY`>ᛕ)S8Z1;^,T|Y2$YDo9l]9gXs2l5n@l$yӏQuT'R&l/w>9ԫUKvIy<Ͼ_}(OQHg@V,3s<yz_yḉuZp711!)yIך_u? mT.ر-VIXGbl48S::~r]ft\lOn@}-069[7.ܥX~WT9k#gQPP##1wQ>.QȔ]!Yݰ0bYhK!$џRGm┻ѤUL?NB=i`*~Y(Kà2;RnjU<+C8NVβ5_l!Pf6SJAd/,=TٻZbNو(QljX8n+VF,)XHN?_$%L7ĲFs/
'b!EIi)B0E3}'&IJ,J[gxDjv.6? L:WKZL1e4orw`0x+V_71½c)s#7Ӣ_,	Pi)B <,5+5^O(1XAGݗ7e<$"'n{rŞIx{L|"QfZ,!DfZ an:Hzא5a/yBpEiߠf"Sv2'uJT%
ۂ#À(xuwlG{)ù<f@~MKq	"_f.:o\(/*s mUNk$8<>g4}twndYuh IX*p*N7ǈSJ!Zɰ*WHd,kKs+>}}RR=m4$µ+ b:x53VG
`|c7̌+RgC16jAB9wh1MbZu;ыj]8MI3@/LJx1}k"#@n6yVxhZJ
H8Z`87üq JP:n)ǚQs
9-)52 $ni)QUsDvuz? J¹NƐV}앞^RieL~mѻNю4٪㻲&EϢORRX-x{FpaQ𵲚Z,s,sB%|T$M'Sy"T_l0Omcl]:h-D]qJtna㐩!52d4	W7xp~_	
('tAE玻s.mp~LŶjM{O~6ĤXw>[DpOĘG'N1C5/3B=Q"xȳQGSw-k>1{4KcV#rJod=]2<@mדL@0M?]C%vxP8ow߱ϯ#LD:s+a'_%e>.3;^1ypZZ
XY(IMZda<XHN3XK\%GI?B@^<SR=mDg焚q2sC,c4nvZSO)wy!7h-^+f(q[>D3S}Li 6H>Ǧ-[7{gf3՞}d
&"E ]1w/vU	3k\{uyX&dMf֮ۋ?f
w_ũn:{|]j>c)UH[
kr01^sǵ_٬UܻQ`I}l!;ʙuv6VפzSqa	Q2M|CA'flok^=?|dJ!/NAuا-{~z?x1ܳ{黁X{V%cJgɦK!}]mYaV<Aw|!tG	ن_{$yX䡠M)w tLZIGl`'K?yLfPJf9YSD#	`ԿiZNDty	"l@EJ̦3"ms+t9( >5@ƸfȇÜYù5%ͳWr1|3'?Œz@ݝz6)6Q̴@v#&FT+a_
":1%D-:WT64pfj%Gvu/Zn#c3}jmlF::i^naTrQy]?Q7zL0+who2GZbfL3dKIr(lcȷ֢x	b^	0D퍝֩j5:yXjr22YTQҺp"yq~ոｹ<x7ŻkvW}T'տh{YY.P~jk11\RYW2I_]ٳZ
&Gs=uAT%oi$bJeaޡ&*ƤI~ρ#Z:"
^q#9Ri}[{ax#K@`2b%洺6Z$2O񤾨	'$;"AX(<!81`3Ntm|vtd	RJ!X|F	27;'}fcZP)#I;&e|=
\1^cD${Q9XKD/4 *qx;55Sr;AfۈEKFw9fKgXE}J~Kz=00O308>H8k+o:5FuxZ&PmmeYVeIU"_` ՗uJ <jOW֖)pmխe2~TxKW_g6aqHԔd):XyG)~*a?4PrszSΩgۅf$~|]n)oX9"%'V,jWDb$RXnnf' b"kQ@%X$q]bY-cLNZ6&  S 3F˔QMWst	ZoKAW'/|3ڭܣ$(S޺[xPx/c'/It,-!װA72쏯b^Vq0Lax~ E/PY^0_f+قl6'MYшyTal/E(2D4dgJgYU.WJK$0	#>N'Hs1,*둷V3:G/sg0KՇMd0STP1|d*4[i{잳rE> p00s,ngN:|$$
J:RVP+&=v}vu
Fh1	GpCEA{pUw<[+u@^-fOg´ucEbI*pIr(sl%껃NzMFqUiY)B%0ϞoSZN},W]o\೰B$}[@N8aQFq$@蚊0>040ޙgud;so1k%_>`ARQF컯yԤwt|pt2ax_c[K_C[Tx}'b9SX0U^bE4Q=RObQS Ā&ۗhps5y!)	Fx&%74/ÇAs:˿=z
I9y!yb XY"a*3,%|<&FZvpqvŜW\XIֆԓ+LY~
DAI0p2< #фK禛":]c{z9%-sK9﬊h<be!DZi~0`c~SN׏? B	`r{Z0 [f L~n;qYKdptWۣW6FSqH{L#
ՏAXˁ
V0D6}ulM{C`w&y$\Z3j؆Aer~`?514*ls,.nk&x{"2,յǘSG_<E~xDN=%H$^zRcKxs4dT6@MW닠Yb20dPi&Mp%q#nqdogImB6utD=Z'SC_j 9wvBղTq}|B}޽8ͅ>\e*.͞)iH{6i0^*fkܵ*;8m `i}2PZBA?C732BR:]{o`yIظnW/Q^QobE=u}?SO}@xh|vOAGAֆ`_[X2!pEѱO~ A,޸	ο7R9Ym8	+-Vp`rUPnMvY+IS=e	~K20kA<>H³Xc HA#8I.
y%$Ly֭.wz:d	(%)OX27[cez
!S#r((1bc[XܰϽk}7LAb*SФҬcgXJ\ r5v ]S%ڤE' W3_/C 2!,@Q"kO
!E@
"}|6񕏄Vu۾gm%0pȵ&>/0eM5bY 	ҍ
!L-5q\ӗOYi:ga˧%/14\yJwؠ}+b*`HT{[,>ȯ棛fǽ)h5Gݤ3A̖-HENhxE<{
`dϟ}'	w{8<m<KqWہRw!+$r]:3pQK5 ps'A!&VIԂ'/FǍS:-ԃ[1x1̈́`&]7D_~6hC*c-醃7kAmΨQUcwcSܾUioEN)_*I,hK#cS-X_
Pb4԰Tig-+ISntJMA{2X$/3eA۾RM(/_x=Q_M11yGj#RR:z	˵[A5Bn^p9$h&`bT0ГUoٮ2 ".l;bV4ֆ%1ʄĻSAg8@>X4[ꃇ3+j&\][z34*q<x8xh QZKh~x7tl9L!lfӂ& <t6	?_.cQ&M".ʁw|!Q_}%SBR=obEe͗dt+=eaEDw%PJ܀\o=UsFGlatͨ~csMNK5+<}w(`r-5]7ݼ4'&3LE{͘U7ڈ|it}[Ҏj"daTA`Ymf޶RZ˗ۣxո!
nuWĹ^8:M)ae(X#Xa&Lel,$4k"iQ []-(IL{*%'Z
db]I2#{$|=!u(a}W@H1sjnǎ@ŀ(8-F-wQeRpDSRpL{	rSNjOw;0F!PcK?H^~-]ʖÃ/!CŉreXɰ^bU']0ZRm3	g[ǐ+Cؼ"Z4ҍ5=9r.?\p"mMhAdGNu)$l0!oE{%sJnD̅^K(v+Ŗ2Wl	v>F..;M}(^ɶRhIEYݝlΆÓnޱ+#}Sљ2>D܆(r^|%>dVYN٭&259U,GWZE7Ԛ~iDŭk%aT}J#Ѱdٯ0(0e!_Dǣ,QCwlV%:SaqX+԰E*f"&-ѻ;0ak!$G~^w>fhox`OnU4`4: J/dPFazpZ+ Fk2W,Vg7?I.nx% 1x^[w.-賈o#ЬlaTl)C_C9`z
?bH .]xS?~޽8bJmRjOlYI[K$A>qIS1woz:rӇ^P\Q̒w_ #KNp4֩tz{b!M<TWAŋZ@Ic5d>Rsh)%)8>5Ӡ(tGϻ&_u2,FT8*6L	'zlaͶ7^`u2-ќT܃i/nnZ$aft& 7\\	$XH-D-(zc^&30|;WD(C>x8P`p)نf>ރhh=m0O؝ !iu[,PY`sPo8}+I7$B[MU!p{iB[wz6pEKJ3k<&"i(.0Mh>z>၀opx'b}!jek@ c(5
$&\ ()JLhP7?9O"jx^"`oS*J>mg]=-k=)d	mLO gjRr);A@A,`ڻ IYs\|C2)-
dr=r(aIz˾(d()2J;~_6݃.4YPM32q-hb Z
<}rt+Qh3CX%<;<f0.~LfƋ?F49z<\duL8,=*
nU2i504Di@E(1OU_<-H#z h+[=$-JywORohUr=Z=VX"ADu08se1t=@PZ^Pc	6aVSQn.^bڶ+*1=K0wy^IyjęU&1&jFy)#6e8|2prMg8ݿFڰ`_ڂ*ۖj:phYC]t`iP0vچ
{t	N?d8啳%`ȇF̃r˫{	o/wy=2	Y!TPhu}Y_UJylo)<s93Qv]ə<˼"WbE[b&"BUOiI~RBR#i)O(knYV'nxWM.$hzYt{[`e1×vTtYwqRU4UX;MbF\@8;O^~D,:+0'醻evxDu@<_KU.=2m(Xuq! Z>XK-dS`$=i({55i<Z`]ۀH.wˤt1k`|LI:I@~4(3Vm\Z{Esy6>AƮ	g6LȽ+݀$=6?e=.=em61GoUDgX
p<pux$kGr/!?{^|ٞFjԐsA ȔA<p.ṇ?RvILR_fz_`ڲ
*ُi|AHXdЀm\8z * h7v7^at 8"&3n(ޖa^y"gˋa篅ˆeodUǙ|)a[ȆϘxEySS/3Ϸ;<cEd@E@5ѕT.aJ!Cftg\Zcs!GaK=8(7B^1sX88ɡD 6qqioXV>Q\2}:G&lB"(=d<4e|v䰂ۙ-(D>"l	ڪc怓o) `	nDV	NGQl.#Me'z,4̾c!\*b7#<Y!L##,PY}!YB2\m,H,d{piY8\YM|H{?^!}KX7{{0)Dpct.h Ess\F6=r|fOQ6m$LHd
ӣt:kbSMb}hb#^H.{dvTpC.p3>Kx>'X5Q{a3'RPxgN"ϱHl)- QiƖs*6I}ͲP21󤦶kk{)<U/v >yf2ygqh5@'1(T#+f}.۟/bfz5VEE]$|I6G>~G21Cˀf$WoN-h")FOFPIRdV{Ջ./sx>'Ζ;R';c:zwX,?9Rpj4ly Aߚyb*6>HzȨ50gAK7BӁQ+(G]n[]cNĕ~ic:ٹ WreHA5W	bX,mc{rfڀںg32|wv4nK3$ǫƙ3YeQhZZRtZBxh{P42DJE-AFLxwӵJ*lVx^/?ܜx+/f*zl囋{_x{bA/u3s+-5*Bx׀F1vG%|6K`'OK(lr`==/ڙPAՀrCTț@XueksS4/n{h{X
&O\6F5oqӐj]66g)諰m	2T]Q=x[)xNSoK">V_*-`iǵ~+6SQ'rφjk96`aGvBOK>##٫O#>qeWOvpCqPqĹmFrʩ*FhrYZ2LRШ*+ sі@Y\_sf@\Mʛfz:Ua7HaS+(3Wx2<0PDQ4ܡI>H%i"kF@,)iz_}ɾ3zP	,U*ER+efA5,rq~?(]*[	2'1"2h󲾍lZe0bII&Cʡ;7</v[FGSe8%"J<)#0=:3ճ;& F(0jڋnS$4PIDA֖+SE|EqmLhff{]0>=9ILwg{Sbhcia}b^_IB&yiO%Iu܆l [OW.<u&@aY]xf8\ˆ*Rr5lc(wHd9ܲ ##e8'u2&-#$=pW*q1;xT7.Þ8ȒeI7&^R%T:@Vu@ :ItFys!;jD7)=8cUEgm)&>?xv}㇋O?\\b7K۠k@o螊'l랋_Os5),&5YɮՈߺQnLEw%u*块90
NFn)E)<$w?Nos}@*91򪩶YfO@nZlI?}#\iM䲰(Kek팂s<WqH"?s*Q;)U/!
AųJ{.f͢
!݆S9NҴaid.qNXDWٸOu>^9t(7SF3 ϶
ݶQc6ٴ]b`E<װԃ&k1շZn@bqY3yW!_#a 8#=3RCbƟJH@j/^6rL!XnOUPHoTKnJXB> w#U	4_ev+CaS4+Ut4,wx
ZuÑi~tg{(d0E<)X턱He&aV|ƚ@ 9T%l%JҔwDua#"dD☒|!ka	9w\y}iX;^@eD0%4EաQUG]{(~i xO.{U,ckbd~TsE܇sTO(*09v&0#Mi!](ܮƽBlݝMf#~9}W`:%E9z$7JܔZ)]{cv}Rǫ"G3Kl	HuF0kY1iiW%`FB$7tS(ݨGQ(WcY|ʫ:.KO4G$qJ-Op*ˏY
CHx..˾
ȃx=nMqS2|X^c&02zcd@uΔ?@{>44_},3ڴTȺDDMexM7[InŎ;%Q9+Sw)L	wn.76,4xJ%Dw'~x*扻~œgvɶޏEm?Q}y`ܦ6X]j rGvU3L)Y9̄m=x\L2"rRS\bH7V,zkt[i B9lh83):ݬO˰El;Hb'KaNL;L 6nWG1X|2hi"]$HRD`0cd^Ӷb[="*slCw9Zhdp`ՠ,PᘤK_GemwK2ܪѰF,R}t_7׫}i}P,u3Ӈ}F1$M^_VLS&!EZSfȒGBpn2	ӕW+Je?apʲOߡYb"Q$eduXbY r#98`KO5hJ[I?H2ts RPYH>04x'Cn!Y|Hx85|PQ{gy=Ahd21fP棩c	~뢃dx| 	ŦvFj-Ai#tA[Gw"#N1E&/KERȄBa+.,;úRʕ<R#MxwRIxKt`q1{vG(`\1w@̜wd$ӯu$afC}/1:AMxjgN\y:yp0tr>5hOFb=Q	F#<v`CHC)c,ڒh7aYE=MV`]&}ɯ3gZ7td~;0X`Ǣ>?w,qΔh?Rڕ!4.|8xޑvZIT"9ammdЈ5:w0A9ILf
2_ #9d#$6tx*sp'`=ثq[\mSC噠ibZB#mV)s|.2ىV,IFkK)u{U3z"Գ¾8ƙa91#g93tOK*B1Lg&B6m"6F-X	w4l*ЂJRBU- Ϻݸ֥J*QU[DXq"37~!oiOo/%u"x+$6-iګoS!W [|@;gj)6/3hg.&`H8dRK
?f(:G,J֟wU3Ǝ|8C-f'i <Swrg`9'hG*⹠|{"&|&ix[Aző)	)av2lOAۨ-D0IߘxMRֹrKQ Õ5' 9nΡhrmQ3t+֠u^FVɸE,I Gcϔb v,Yə2TFhjޯQ{9$#V<*$F6O!:a0ʗL #'WAS+WOP78ײ#mޏ(H$74fEx ɺNkB)0F4yΘ:u]0F>tp
=O}Jt,ɤ?yfzřD[xSB=Pi;(hƿkzUadW<JFRe:^w&S9**t-j9`+=G7sPI0<	{ZCo`bPekY'tMq8b;>8̿wF1mIk9#Y"	7?h\@&1Ý=n(\y8v&+QݼYYZ"EԒ;J$1@*#H鍸}Ņ\|MA"0/``3Aox{"ZC(vLC!*$vrNoo܅C03bm_Lu1)z|ye%fRdKMZzXvn!?<Jh}O		_x#bz%*&zymx+TK37	PR큚tG'w=`$~7hz()K'Sޮec ء!VvxǝUah'+frdji<gѾd5gwj.b=0TG+kmfNٱJ2ܭ(/+H?蝮V֓(sYqys܉-3 NyN*kn)3
ԈO,z6Cg,J)7alBl(/)w?^
)Y7ZC(y ^՘of@J36:4Fycs)ޜ&D\Y,hWo{07Egdd7>5}Rg)9 : KI`'x [%`E D̲xr&|^-۪zfdXs;W5%gNk%:/?+zs
?Ⱓf}w$}m!6kr ?YVÖ%W5&w.˗^@=6Z*5@Q&ʝaŰ3+3^
~{3|UIcgrk$E%'9YM|\.aUN@H_9z1j^È_wpx C?А~U74s_zJ 3B3<Z<}-jU Ţ
2,pPb|k|pG8:6c	3]}2	}{$93R8Rn5Y"N-}tLR*ba+o{,ZdOtTV ^|`g` evtќ(L0&9Xkm#VL6G7\qFͨߐDNc942Z%շ7usC\hhGoOcuYƘSgQmL̓rul^݆-B~$n?qph#n؄*Y-ْ%؞tzF؋k-nm6e#s1:JaOK̜|^T3 Rq;r
NR+8\AIçV;ƸwUێhq.E*
f*Vc%vDXkNI`</q1*J4QJf$fj'iK°VE߬b@Q7n8UrMs߉!$8*SZhz)ƿS%͸۩\>Rb}@Q̦Pc@;!lQ5[#vVeQ?hXo#k%&w9iTx4	\ԨOeK<k$_{~Z>tjցN-,ǒ	-C0<#ɊH}+阕t-`UHh&	j7Hd"堰lBrX$֟,'D)!Yi7 IeC7&ܸ\/P͉➴r@]xZ7K	wTUHե!e6Bl*VILAEVIavs
A	n_ftgmd
 #cR;Yr(^9ї>j(јEo*MJ o5%8.MKYTXGتd`ê܇K_
2Ú /jXMTfUQ{8Crl){ry[Ce+>d zb@,f1"E{j ɩ`~Nuk|'VkʴYho	"_17Qβj,|}32MD-
`&!!;C['܄MY&+=\(]|iUY׶Րo±\`)W6]N*B?vΗRG'*05xZjnF6C'o"dle;.bYİՑWY3|`ȲLMe!m%]eüJ  \OS* %Uv.!w-5~l9	'\g֝1'4ǼFEm#*$'?lh;;yHQRB45$|\x}1n􊙺Fia?)rY8^򑩻WTNawͪL{<T;,Sgn%7([]G SQ~6P.ʗb'ŦWG>Rr`<+D4ũQ-&?L@zb&t5C60{#f3	ױaZ%WjN;f0mQ
5]dcKƧ[J%)`56,8lw,rfll<ÝK@,.5\0✨i=)D{+y=*Itu WHsWdp]MP2+D;\~f,EMz7t&xOq/nR$ef }JU|5_(a9EQ_%m_=.7=Y=ăc8 _0pN!4jC2ͦdz+s!PY#},jpD:I1aՉp $.BdؙnggU9JY4$uD2|)ʨ"GJ2Ϸ nB+R˻R}65gr=!qk$UOn.hGY\o vpΘ̉OѪ,Y*dwX+o*vӟ^j%Վ2SFeWِ2\|yzJpCѦiJk8AkYVۜ;sqպwnl"ߨ_ӂ
b2 
~)޲r{Kȇ1l@!L._d5%c:e	
B<X$RXWS+_RKQԶ:ɰO%CzǦRCGi-5pcDݽy2p&kdMDugjpUF]C 	N1v|)>Ýf_qmjhk^NMR}Y<Vژܽ b_J},1\9usI`{.gh2J}H8_	E͛O@&,YsוSQ# [ӒQ-a,`0Ł?Hm3pފSG	5> %AXg'`g)<2&Nk6o{Rۤ$ql4̽U;RisY+L_D(Ivc`)|ؼ@` މ$:;/a\|so| 	8z`^VjIJߗ9? gji[^]rR%}19q>`cVZCẔ$0fLӶ/.1)JYB.n`n;лxR*9FF9Cp_hH1RӖOt;k=[gp
aЋW6I6s:[	)R5h wÿMW.^\_eӻ}`J*j$>6~ݖN3fcd;|@;
@(%$	D%@A]uo(EtL.,0WɗHff_kQVRxE#&pq~Xx#Iһj	/G~5`hJUC}yJu[	yXIy.4#޽NFӤLب{D?5
-{HGWؗ@;8]oAaj	9c?@a) Qf\=]jx0RT`)G{Er,}.N~xˍ83FDKS7H7(NcMw([zV#nN!m7'60-`MPFlx~ @-6c&uC4_7NW hXl^7}V99Okؖ}/Wuj	_OX8±2A]@YAyE/1SԫeYه|
_t3t<"ؚӠu'{CPH=]Yg/>,񑍰 CɹGIt0u\V "1H zi,jp²H(tݘ_YcCD^>f,ł;s9 %iAPJ)t.Caޖ6,~oKI+ah˯ufv Lrk%Ts=EKN9`PJ(b^@CYy5 '!IOE%Uz^__ӝyltӖk}EI	ImP.ib#;5Ye*lV& %]<{b=RlDpa8
^1xW>U'V4à.fD̅:CWa׆Qt7	Cٗv#ĥR]ԨOs2* b2'%к_s:tcI9D z,)=A=՟_$*܍MӽW@[eC*:{9d>i0q(_hIGOtvo-b'\=tǌl1IӅa0ī!<~u̟ךDBr nQc$hO`&$'aAD4ɀ"U:' ?6ܝ0`sw!ji5^2=S޵5Wt/K;si8Z-n>ÓãC~eL4=㍆	2覘R	͊/qpȭgQmWE.VpMyョ;w ]Jk2KzBkcħ_"S⳺nulJ"X/8:xz|EDvLqL_O`Qlˉm:#Qd]s'SDl܊^Gtgf?N"x*>g-TZ Uj/E9eFDYhc$"exX~/ x$3<iT#dBתPR	0-cx4N{ޓ S:76T;-6-BTwǫ>5:{}p)>5:龑hB#w0BQdڱ4'
O'Y@	OBf+g?WoyTjo *T)Β&H7>>8ả\#u\X:p85g"ub"tO\kޭn	 #RSCZi'3qJ#T^b0;J=u->OdqChR\uZjΙ)h/Ew}9 [mpCQvܾ|{$hsu*8epBNTgɥWj.VrN,"q59Q~ 
_.eS-{`qюh("୧dT.(ݕU}=aL45'	10@m&ZY !猛A(gʰu=**>WF8`U-BH.
0Ί/bIHI-ge;q=s-R>juhJa\ Xټ>{!+4qwU6PSQ՞^70YWL1ǵ<uOK|4k."C6#$	IJd¨_slKrveLӘ_ Z!wŊcq3PŒcH2yIנdMi㙱V+\|s
*5ߒRL\\T?|)ycg&ع;nr6B0{{[cJo!=#?Ms;}(K=l$*HJqLT>5YܚCk ,diBPbHZ&H>6?21F<a>UhCyuE63yknyZPI%6?BO_(#`6mUX\hʚFHR:[~Bp׫7@Sv%"9X}zڍ2!dz =;.RJBryRm#5oJf
YWvwF-{~c	.̯KPY!t)@z(uB	g8$!1tQVP)9Ѝ2x¶'SNz?;-"tl/ݫjϟ%- z_ENg]wRdy4maVE7~B>ou#lTziڈ }"VۊQ7wb̀\@b^-*LG<ٯ'ᡫϻp/Ta@דM7Y{PTWVB>ՐXwIt`__<tYCZzaPktDI]j0 Rċ/ QG;&bbr$ Ȇ;(ΫȒUb"CC"8'Fj2;21tȶeMW,QX,O]9>Vwi8fMZ-9㛵4 	b*ݶoP,BmoÖeg7/N?~.?sWL_l%.coiHDq}$:֘񋼭ಠ||O^yo&
6?X
/k@	N7	FNPwG뗒qY-sqn q=L?*<Q%pfBy~+ \ ׳J4QsTD2i$Ct=K胜zX@"+)lBISF̀DlqPWPn`\n 6 Af^ַҞjK7cAly?ȼX&e;uGܢ9aaNr©+M7*|7\@wT'FfrƗƸAIR*ul_RVw3c=G<{c ,Hs.|;٘ltmTD!g$8G$=ςOix$	*=`#?N˫xNa| YUv,5Vp6TR>~⒚ QЋe!kI>Hz
rQX_" Û!]̦U^aB'c*_%/|]7KhSLXd5!+_dNFeoa_TeQ U5oA`M*βqsIp[$yDFXYe;&b̂ApmǞ56ty(L.a4
6IOb ۮtbdE_S	s}G#:g*7Լ1a>՘_l¬u19yGN$Ʈ:;Z#G	oJԡ8}!쿘	0[C]P\xtɎ'OC3]\r!OGk5tU^&B;<HJ"j`U 	;^9ag(5s@0F&6wPNpbls	ࣗBmk/ /Y0ow"]|58OQ	-)zvMKN [@E+$Kù :܂֟+@^o&|TD2c^_y@~DQ_x3H	֝@}grv	uAwQΝX^30z"j
moГI}E)|n%$gCc"6٨{ &B	U42Mȸ
R1EYU68)	0XbC0zseZ5j-)dH s|pbAsCȌV~lne|;EǕMUWۧi#,GBAxK̭97-0O:f~hU!	}A3, $
if*gG.S}}ȶ/swKV{?|OTQ@r.egO"#W)0Ic)@XS؉,5v M[{3RA1"f1!h]nj	0 GϚ.j$Ҷ
UAiH?>JRI	j+
h9[zۊocu35Uݹ~ ֬O9Jn]]>TC癦à4tt[vȗASFTBjJ
>hM;IfB?1~ɼS6, 휿nW0eD~0Ŷ|Y,X7<Y~h8m`azsu*]3at3=6Ϗuʐ=a٩>oBt)?O[$S	pm$uMKo
'gC	!{)xq#]o#T3Ls\lj^bv&b3uNՌwaﱾ{LVŷ1jDEJ<^!-oއX=_AscЮ[YFOSvLK)FVUc[,`G3vAx׀*\Zn]©$E!DN'btRHBG$6>6gD >3I۫orVwj*!8	\؄XE8B->N7P*qe} WvYY%%pY+vB`&CKqK6ϱ{y$ѴnUG[[L%neXpS	\˷H2⪆a}M01Nyh`'dCI2٭m _NM7IH}C$&y,n
6~+|ei
e";D!(qu)%{a.5Amʸl"I)2,O4f@}E)6;6hh˭ĵf
Gb
TGzL3|#mBԏRH@C55d)B:u$ʚn!IRJr	%{"c3aә.ɤ{d$WQIXUՉ9]6:o]wQ X̌am`׍CJwondtm(m59Qܭq@h
DK`rUsLe0՘Z0!@b:egyzhT̋IL9w(fxI^;<B/2edxǂ
h%/=25k :EEE9y%TQ;CA8!YRR/V~
*뱢SWNI
ZĐ׎LlQ!͖inSsb7#eȁ%KQH2Q+VBJ}A)&37EjKp[,*Ä؜e-vYZ0HF1&Jd&E9 X:z{k*GKn[	NARn\.9J#s{YPo	cyNy"'
'Zl:2{r)Ŕ}k>r6t.Ba,9GR
kB^9a%cv]Dɢ2eI{KփSS}M%}:A}:&̒!haBKM~mr{/UF|nӴ Q"FHENxϞzLdL+Tz U;%?{׸gK$8!5<'7fobTγ{>䋤=-xs
 f $]$LeԘp2$ j1-pIjmXhc2=wF[r.#(`tP8gNȰuza+
I0_1[*#Y5t0LcY	*q}=qBN"j<*;7IfFunLpGg<5ofTQy}1K7%Q7cZWXLtN4	K+y?<89**m@+Tj毽F"1_7)lU6emuSh\3r(^vǢ=]\s^;3,z٠tf-E(%q&=!CCC,]d5iD\r)~,03ek"@ 8LZs&oGbQ7<9:`j m6+-0oR cɪGp'1k	+N[{ʫ;DS+`R}|$m@'$JD;І	o.*0D+RDE|Oj*H~n,:a<:SV8
Yt|MI)VxYB:8|WC8S߫.@ȩp"C?mBX2?{Hgݎ'Ҿu-zњggɧΡ,@ybIL!	-cgv/:!2ȸ:*w ˲T4AOF3q@f+jwKJ3?eRHJ~đLt90OyGOf\y@}036+a#8qA留J3W~+4jd[Z>SQ`16~*VUfIl"ȁ^ߒF=i	@)p}1Py]]GiFzonOz{5k >po#gF広rg=Vxh
*_܇蠇= *iPYwW8	jg}ϴ*D- C+2ejmH~dTN7Jͽ
yBIu&:tO;'5\e9w%mrQOMyޟ*BAJ%|7l/ܔ	eDw`w14o1De
/ꁏy ~pJH5XMcWqy0eйiۘ"1iJ!ڣplGx` I(qyi>Ǳ^3(Vp&3޾\"ґa-0%魳(0ʍmt&ja!c!'[rRpDC\Du(gO3*{JM!ȝ( ;UJyp_ޯeUfldLH{cH)E=:Bi0ROn#'Y@*sp*h	1ͼ 4t7=l),ڣRQpU\FWt԰VZFL1I"6M%S+RH>zșC%R@\ONEDkX&;#pm5px \ڞ/<Ѧ¤>v!!(>×^P9?Im]i8QkŊQO	w߉πTo
~cU;jjYd4WSW)v&F⿙aͳ(~J	7IZV,[59]%闋pbPT!uo!?ElS9KMG.ʀ{2e
Dr*]	ڄ}#Javמp-,ZɻBĺ7>9ZH
C(ǜi.Tو!2ImfձBqOhU-c"Uժ1ykᰒʎ}<y#A8? _tE1+A<Q+}CZ}:|3]Kb}(a=$K,RW|-}[a8<#<3SZ^
n8/0?c9=?ΰ`>p,].@2:qK?򺪳VgnE-p܂v̕i+ԈWbmaw`Xe9-3NM.\tRJ'2fG7c?}N	5/VVGGv
0$ٔ';qbXQtiRx/k.98AϔC\*b|{|N#?|wzAeBKsޭ(Φ׺9WPbzr*9*z9?RT6	@`}[l5K0?U(C/eҶZHG¯5qpAtUܪ]/L[Jp1#HP>a}g+OCBՃHY,"'gT@"$~x	(g*Y,ȟ䱚Gsj[!'?a Cw<I23BMǻ;rLx2IPKζWI&_m6fMP\c w[oBwe0WN;'+CQא~+sp!>OP*,B?Ww%}jL1jd,1:z?`D Qca{ɒ a_Pl3ֽ _Y(myr)޼WK0"Y;5T)fS,QU+a)ĥoHD1	.^'wP)(.H]D u);S1
q>hs/$vHŚ '/7'drTגȌzXQrr.'7v,1> 壂&63)?wkרH9rY7q	yJvF'hEZN;3Y_&`*9^KT%H&C"x5$@< {A$l|Z5NXY747A}]kQ`qo$]$):B饲2pPѵ"=d%nPn% A8O(6(f?ji*Rr4A*.^zUH-	W3oЧ㐛^Di#+"#1JgGe5m$Y)1Ei*m2I*:X"Y5k $ @Ӭ;=w ʪ6J
qRftmYx("hE/sp0m%otªqJ|YjE*H|"Ҙ6<pvM}!GR'4̾
ITPE_ɲ$,%&]G0IT+j~J~Mm5=qM+^thiT#1R8jVt3ydTIw UTY?>1WwrR%dK3ޜ
_A'g%-)<[c\`tиE6!^Y{y$1B0E%t<%"R/i>ꨬ`k-V6uKu(Zyhұ–GDRڡI/fn H7 	q68
FЫݕnR}Op$ r@LPMGExs9)Vw5z3]
iiR	Yr	q7Kɞ
iXCxkIQanIi3FE7AEtuqE\\&#mT̀Dq?Щ(0k]55)%]H2Nx),;G|I7c}U͜h>.&|7(A,BK=yA3eoVJyF7n'ޠKI*vʖCdTBK$HϗE8)d>kʰ*AfxUf)PnXzE`+'H̖ěRq}\{{Ql~)Z]ZT29RE&KwC0}iy!-agbՠ#1TX`i 	;bDX~i;=Tr@tAmJErbn iRfbZf*:79JB(5_pԬ!lH]]B"%91VoHTu[K&*CbE"i^(w$BWue.,Ac	_{5"O*);?%'k\`͞iunpmy]Tpj;h@viz\sFZ1N{0to]0!:ebsFfmY7
aek{_oú Knyf`2\ͿU"L5ulN5sNgvn	W]Xm <p>(cYEOX2L$7xeO6oo	ۈ8|no(INtز7Q'AV	$?*ϴQH
sjkTwP̵3
qgrSV"}Q֘qD2fJcTxօWpQ7JT\UlOT,꒾+[3ڻDela[DQV+Q7C`*7Yb݄tTê\ۼ}XHDrTd
誯6i4]Wsse.HQ"F!S>0R_m~Ai#z4mz8K& b:ݑWk	&+"$+~Nzӡa3`h+|G4l*$<ٳI|ޥ×@l|ŏo͚47Ae!05پG&9bܗ8I},*7"427F)! $='Ӥ25l@bOBZ7<||w(	hˆu}C:Nu hV/YD9s|0#ꬕh=|eu"r:'P2~l}&7^AD)>RG
Ϻ})w?% ,/,kpm񞚐V}Z1|0RwoPFĲPv>YH#C<>yY1c28>stCJqyGq3o霺hkf%vk\3'Hp2cӏvgZ<g~Ow6\ftW?npG@1*#/ޫ+Qd9!l߉m)Jߡj"E;*WUZVo@^ \>r;K9Q錡3@ʚ5T髐pZUtﾱVT2]6'DB,坷*W('`jn8lԓJbU[QKN@:Vw"]RP_=1<|K+6 bZwB8{I{"{L<7pF`#ϡPD7{@"X_}-7XQF_7)j}}LC:r?ksLuHH>V fU'Fk)r㍑9ڷucpxea!@%QL8>1:ϛ6Pi7_|.8I.8%ݦ|r-vJXe3!L^J5_rEqC>F>2Yn0Ҫ~0!Pp{;A/.bKEM2!k<ufhqPWu1>ʟJHbm2cbpp~pOvkȄNX끧eV7~wWuwμ\",Z+\/jv
d^BX{f@Sf a^sw_G)f)]k+#1O,z&ʮ1KQ+Fw1(Or6y+x?kI{ӦPHlCT	w0$2eu*D%"	k|=ܘW3O)ɢo@f/	ta-|6K6n/**tr~UL[7KX/ӳp-)c波7W k(I/1s}(ATJT-ԍy?X(
u%
4ʜBdͼp'1Y_"ַM|7$(zEfmW~A/F.wt1^f,~By1Ц}yX=^a*_>&O*`Z?Spd vWI)"<]j(({W$DTHWP1I3g~ c%Jy<=˵	{zDhJyatRI</"!-֣B"BɣatVYXx&hn+\K0b Ohl6!evj4gz,6xqoY'
9R᮹@#t5>8_J^vqm BIw>;
]wa|1-O>)FPaI]m':e.fIز3Xy:#baЬlyҕ#a)<~5m'
 =%v] 3NT5dj<YB_~\oA_'<l2vȗ.\ycIWP|,^<,bL@tcɢ쎁b(l_s~O'Α&#J<_XRkXj%D&Z>Hu~7ι+'M~0U^Y1kFKذ,yp4~1-7X| yaw«,.W),عh	H4:ni5Ѥ]Re%y$Yqc2n)!ﹾfysECq7=PS33DۧtNǢ-夢f'<eqJ40w>Isf݆:LB pmvB!> ^ˋ?zgrL13z1eySS#2՝¿ɺNz/azGjz(Í^QjVS&!U~䡩?@Ѹ֝=
8f4&LZDfPV;JDr*af]-܄H&eRiqh&ǰ]W%u5R}d׍"}-ڭ7ADN	DOJ=͐tT۴Y5 ]v@pͰ+hC*u@hZ2S%S0ˡ-s}n.>%dA5qۼǊf!sm, 26|%iA!-;LC$tUn^f]?bBX$,4PS\Xr"P7&v
Ew̬*ۣZ)I@X}2.ըL{8_T߳$<3S9YEst}4>gh"w%:g4k4bEyB>Pk5ա)ޒf	;p*jT
G"%)q|[-C499Q.i `^DsT&j(qt8}6kRb4mZe)јŧ^
(Ro 4=+[eX,UV.B6i#MI'<DӪeYpG	cXt~ q(JXs9TQqZÎhp"?" :?TAC	O;Bp_p"3'갼ꄃ]JYr>,ÎF
!B֕@6wNWn[lu^6\]FV`$	U;P
!$Ur:'uM8]$¤TSޥ.Dлt6x7	T;	uH" 	;WkGMXoV-+S״	
e^27F	Lv3Xl6{Ԡ^^Y	/L:W2AN:qڎ'5,Bެa+)ć'9"j#&ͧяTɚD*6TP@F
4UN>`K8N8NXE_T 0z|K>5]ؒ.آ$=̱:yMxj>YcM[=g_:!M3 z6_r^nKT-%=1|=?	'#Ǹ0žeņ̴ZÀ~)*=8q=IC NTYifϷIyI
d6M9>A':`X~n^1^8LfK+ +PըjH\
(6Bj&ժd<q0ŐBބm ҟ6b7Ν,LdҞm>ms8\_ruBSzʐ48h0JƓr4A40RpgGWs*3aa</&#F$Hy7ꩭ3{it/8Cx'0mc\1;pc[~1/UdiS&g0OIW DW\S86ϳ_4_ҷHF78+=m%1VQ<%"<hx8RQ>6B5K6uקּ<Wv[,!x׵x%lp2LLΪ8mǙ{StYwJ(UZHbpzkf/fg5LT8~ 	CV'dTiv3S^8ynȻ0!d商~5˫7ɉ8Boc@"+cU*yh9^4m?VSkC@z4m|.6	
JaYʌȮ7{@: {BӿןDI"GXUOƑl@P񬐯	FD<{*"=XlJ+7鳧~VJ,T9wtݜ mv)#~-}g5HXAr*uZ;ߴtp;u
!э4Dk_[>g{8ha+X>nzw1||.W;^7?-N&:*fYo%.TjK&Ug;{>	-`{*϶rZ=#KZpd6ƔPA?\#؏=zMq_Gxy|VnJđf_*dF\ǫǰNoUTH_؈^[֫::tJ,۴HgaZF㏃Ӥ͜B* 7o@$1DVVʤJ@Iɡ0njJgd~XªJ4v=Id!8#y]xAnIAZSJ+'jѐiԋ4bҡOEl8O촁l՗Td)ؽ'KWn| Xl,-kMYo֌w6 'ZZsYxE6NkvI+G+9Q	Tx/;pSq0GĆvӳZ,x*~h7{Nwlھݰ\kɁV#'F0fI! Fᅂy&A?EY; Ap 8T9Q;t(,Öw"K;sZmWa_I:2$ySt-/@?G
6dx̵Q1}6\X'B<[u~~V=c\7d@,((P`7djd*U!	K<G.<f S_A`Qipf&>?siyV? T(4qUVE }]Y{~5RlHtasKUfy㴫\|pu
#c9.Q(@j49A-=)WY*6%A[T{y?
+9 Y]r|&G lbyNPe:q)e.|y
6Rr،HC@]VwGkrN`]ORڵ=7՝
@%jb4<d6xL:!`9M%\#nl}2b6Vm7JwyvÞhLI&%v5'uZYŉOXE@"ne~3fl&cf.xIkQ\ʿXص6c29-FGg*B3qXGJV>=xѫnI$RHnsl%g'aCأD_Z썧GyGu(
n~ n֥;Ukdgt	
-"j*#ÜVu+K
lUTAQ&xn8Q<Vsi"(Y%;:?VNi&E\1<
բOy6sR142?Rfeu vm^ .oSM#*bc	Q5 Lm%W(;>3a:n\(F
I	{K*V9aHCcayI}/%=݆F5fUc/ah+"L k8ઊz='$3Yb݋pax	V lCXz/%x+ݐkFpӛyyԛE,p&mwfExq+M%mdqndC$$!c'Fb?JqC6UvJ{z'>.mc5\ў pf3HL
S3BQ#xSp\N,Eh $ K
YS2)Inq/F_(yR7F-*8!Ი{,{GOvI,5Fշ@hZ/2
GSJ3畻c{!0?L·
ܪ,hTi:7p=S)DtǑM~_?9.{T[QkT%{ŋs,)>1z#{0Su,H餜uL_k5'eceMK")fū7P^g݂~ɵAWz>= Șd:%?i=1My;W>׬.H]kB=^)RNSmĜ/`\wڒUt+fJ"X})tK9"!NXǗDO|$sU)F+%mE<IY'KXy;1@ 7Bvye5^6|ASKFyGĞsa:fK"Mb"]r`; )A;۴>VG;x̒d삷/
%pᾐv }evR-<;jB-Ck3H`db|
aWVY1sqHK[?[ζ"}|Sk1ִ]AjZP$[TvN *3&I<dyLnkeXŮoTޠvDT3:δ84g`A
lY;+^Dki@wv@Z|Ώnb5tE8BND H<979J$q	B	u;oȿOoabWv~l{2kjsSAv@"-1ZA^Q@8luZR 
EIF,NGVZzҵG)$:;V}=A$KUQ_|3y=LȻ닷?}Kո;Ǯ.1T`3C]1	;h5UJI)|MQz?Qx\/pzͷa|Hc/8P|PŢ"~CǏVQPw4T`=6w9%(Nǎş,C̑ }.&!SJrPӑf>bׅF!|F^ˋ׷ot~_H\c:]N«Nk3aaIW?#6:5ug`#qM@΄W8+,p=hZϖSY
_~ĐVv^ck؆`"]|ZþH_Hg7rtO2d5&P	!QyB1tCa =}i)mDS?ފiJJbƕEyR0^tyCW/2ڈs|C="(dL:3~'o$$/(qqaseS"-f@,PYqG1?	י{_3OZ +vֵ9]B8:z>0yA -Rԃx6$/'UjȀ @E+jYDyLaK_ZqRNĶ+I:g]bO	eЈ?fcsX![)XJPZa)b.vL4XAgSŌ]W/{uj]ܵ䴊dRwk_Uգ
+kgSn[K
<c;h[_cl';Q.Z6nDE0TX0< Dy$E
/ޚfh))K2Vpxng >RFi
|R@ƀܣpe+f}5h	t ݬ3#`l&!*Mؾl
G5b]Ι+,!%* KG&Uҭ!3
q"PĴ gdtc&Ʋ$1Z!C(u6^5ěT!].G?ab*Vɫs3"%&DJ)[p?D	Nl!$j)ׅgX+{.*Yws&ϸWH&?=?DTR.<8yIH<Q6,T.f,G	Y:7\).dz2Tty&\sGCѐcnXux+
rb&^l#)jws@'10q{60n2e7ZkǗ j<=R>0FLrxٳgUhX .JsBy7Z⼐Ttgdk|
/>#b# eFe/)m1uiCW7J\S ߟTNU˼h?<a0baܬO+
{2Ǐg?y-t[t-|Nb?	g9pZS!}ojGp	A=gz%	__hM

T<;vn[!J|6+%@[0M7&Y^KAK?d-':t&<إP%<;PQ+h\Oա6.LkOpdٴ}F5"uM>I3'AU:IREN?ݥb]UE~Ιn7JS`Dj2F CPkkw5zD
o68k96XVI¢!揫mtY
l0߽}YN3+c>'^^Z?ʛgS|zt=3a<T%AC	CZ/GjEo-~Ҋ>WWe$XID7BO[ĽŪIaNv?xO+YBpcU,mF +"jC/i,
 =Ϸ0s
{Ky<)C)0vK#-zRT4/+> O$X"GKt;+2iJ<v^+!EӮ_cf+^hHKOuJgMlAC\rD]r5d{#U31jp	XBcxߐl*8	Np8R:A4P_ ){qyrxQuu=+E<bX Nmfz<W~(]p^SDavGLeGEeqc-J@c.i9-4=Ȫ'~QDO7yB[࣍3K6hIPb@/0BŀMb`)
AN,C]1Vt4bNrS+:Y-ѧOK.4vL:~:O9v\HC&}DLl,,PG	MPZ "_`h=o(x'wYh;ћ%WM};m(Ly"s)W-:ӒTFbaLQ%#oaɋXK; ,@.*=FJDI1ėPP}9.x:IZhtCo!uc=RQ1̮>sWyH.ޘ&zw4G1~<Yw!IjrC#Y+lJ0V_Gd؆rjm@a֛ÔܶD?QZ-+0
ū']7Xr[Kg~D· !GAfKD	8!04ޑB$ɨTǿ֋UIcxZwg|c'aD?uYŢBdW8`8
p0#/] Ҡ/Ywҗ,Bz %Nt".p69XA+ClP/O 
e+ToKpGގeh#aG1e:UK;ċV}²SƠk_LWē@nk/
:yߎgLꎲi}7*/QFv[V`Ā+۸q~V'*q,iV9#lL|鮏$Y{(~wwHI1^p:!+U[2[E6l-##F>t7c8hXpM錉Rw1ןnP'ViPrL
)
摒k%Y|T(%ǌ=ꩫ8-	{FVBe)\))@	Hj 	 ak_'g^X3$ʊrkV=qg6)Sp(Ցe&Og.X']<ֱ|?s%,1ʋl't
W8saZ2ϕi#ݷ r$+K51WrүʙarMM{S_㗠(OKS(%4ri|R(oq˷0c=4uy) F]!I_rl+,z>*tB*GVh(c6+'UrS˝z6Él-[XK'Q,|f*BSF;O/tuvI[9$6S,ɩSdk#8#5/JKF1	$z(tbJ@N	դRRlKfeeY8{ͳD5jk8/UjZ0Ǒ-;y;/%8)͋jk1(-[Ct7a`ʋ5N_YmZfiIi6G?\GΎRyq1\~۰XV'S:gzRUOb=䰈!	 Q⢚4%~wO"d3:y^Ug1ED<+rRhPѓ$W?wQAt*hA+C`-offa:yAjNSxXf0FeZP#$mQSgJW⨗)G!(yڄMO:cEY֫-n<y,,jk;ģ'?7>pfp?G܇>qF+@<\0 #'+3TߺpMeE7`eJivG@^C5FT/ aR^@._.ujl6dTZqO!1.kˎӲԶ֡Bck![V	[S4Fb3j#nw&7?tFgj';c)>xStYt2dࠫ*BUa=Zc-zGB#~`Xd{WʶM H"G|6\X>Zg /9LݯV ܮ70^qg$KcL2R㣛 Q7騗qyET9[瀥 E0S5Q~YB
<89N֞xtݬ6D_,y}lH.
1^INSp%,%Uv@{/r8';QӷI`M7Ӈj<T'hnJb<,dCFll>6) Z;`P&n'	XK݋akq4
)_-]4Eu׏uKCpG0y0c.QҿPIU~GbADP47SU<Jū؋q=໳ ZxϚ8X3dPP<n6J\/1vp.rk_w+ї?㎙@hN*^ IE9ZZD;<ۍx7vGeaeоg%@Tb"8aPn6]|TB;Eba8cHt@jY±ᚢ?Ea @/	WNXsve)1k`#X
)7@K
O)FbUxz<ƒSŔ%mF,^|Na5TiF<|'j.pr6ЊE;vlPyX'GĺY6kn2m/{윿u;
B 71$B/v?v *wյV`"!5k4#wP㐮"ϊF/?R7?<R -ǡ#o,
O$v-YبR!V7 +;iiGO:MܘˬC!`s$UbMsÌƃ_8:r7ҹUIJ1ۦQW}mDeoLqN3-+xal=Iߐ׈ИSᎏAE n$"?Ev{POFlQ8gp/ޙֵqfaĈ݃]EN%@ai
N} *
\rI˱cWMbu oj)θNCkm_J+-.ڛ6lFTߎkI^.D),n%]uSg ;Rewgo)Av ^l䑻;6Ѣ!Id/xDU 	C|W,qFv-QE0ܳ&M5;Ţl`M^0ۅi|X;@r>㢼[Yg
c#4ƥأߔIC؇Tme]0iqT]2[~0ȥ~*,d	O5lq`>D剈4heUzI!%NQr%sᶟ4~ZW?.7ztg8}?F?~?~YVKJ/}> \{GWuHx+!CǨroMqRӈ!@|Gb	|ߜPqfPLi`0W'CHֿAzru3̈́WݭdԿ.^1զȋyxNؾfj|#ZX}͔-HGnQG|.9W*{sԘیiBD\yvC(p{_thAܠ!bϫrtf0{HAX[hdT|Y	?*QChsEqUC۷jBB\>b  yehbLAioT[Qg<Urʼ|ab2)AǺv]Ԉ4R[T:pRF)Qwhآ듺o't:}[64L	(wkZeK⽛uҭ1d.^rXYe/%LM^i6|7U$E!'*.=;ae
6E!y*j.d	NCyrWLUwg+hSϭl_`I~A`F^Ki)0҃ۗ%WV])K=R~Û7?Z*?6Xxpb&(Kĳ䆑b.x1g(t5,b6{I1%+bob|܁G{G=E PhnQ2J	ϔOO' |)9ްTlD\Ħ?!Ӈ(gQǱ$zi>GReiX7|z!Ij&p,&=^^Ē_26D.J|"|S/noi$o(7KR8ȿPyUT5qfazrޛ&ƮH/W%$Cx0 #šϷ&h&}>^RU/m'l}wAu:5 1<eeOQ.DT뚨)H+$8Io睬	[@,>Ϧ0QPpE0UqLGST KSd8R_ͱ,4h lEvfeSV2EJnlჼEPF)<Eԅ k.e[o(ˑ`Dx96N:!8'%jGdm] .32j또O$1T%5VGJ`,')_t+<5$r(yl;eQ;Y͔m1[`sEXsx2fj(3)g8PrCތ])>'L-j#O4k$&S퍛^J{>PϙuDTiBSXCt;ؖ"bzp9Y&y\6QRVՕiHF~*MD* g4*-Go0[g@8e"ť,"XhM#9XF+2qIH̯YQ#IG*TIҞs_:7JpՆҪ[U{>ˬG=}|zӱ!+)@8Bl6)0m"Kyqc"Z2Ǉ_?GTz+FQb.wQ]%OܬA,qܙ
!v=;<*nY|%-6S'9qg 7Nٮo6u#%JD^fHCc^%]6P af~&w
4:xw5UamrNqa6 sPuԴaBs w._[-D1!!,ccoC- ڌvH7	|D8?dM%.g	Ft0Fu5
bh(2B*}*{UC,;@ӟmVs0$.j&uU`?/t-xkqⅭZ%kԬ 

/@ ^}
RM)4i>K~U  Rdb@48Am%GX}W©Cs=nO{t#AwS3fg7o4!cSh	IXGM	>$?e-E40AYE.'pģQJ>:͒yE뢺
/[!1WP"%Xz3:\naz2FTcQzmFWJEmC3@|;	vTK~h
kK-5CĠp#h$hx^BbeD֮_@uFǦ2FQg4l$`
u]rHSOWr:J0El@;1I,7PUxǝ3F84/6![?]eb6+bDdĐzKYf<gd-#s>qR9_I3mi0TC->H{/$k-a.ӥLoNxyq{_2Zfŋw/oз7q?_u52F[+FlYmȸ3b?YKg]3.lZ|cЎT3W1z&ggE7QYEq9/P?fFkfUa Bd:Bb=LL8X׆<sž<1
oϱ R5{ǖpJ9"ؑb"}< nT^I+]Z)#y=LBAYN!m0NG`ũy\*RnH23j2lf!NTmxq~8!+	;6Z0sћ<c.TiE&Q%Qāb_\s<Ft䃠9e!W8@Jm"v2<Ϫr3㌌LɊS=Eׅ̓T..~ZYoz"!:nDEh@)5 /n"%OiBg'ֽ WuIjZLE0B.:$odKl).akqt	@1WKs/W*U-Ufx"Y
.;Cr)nNWc5*jTzPK1')\侃vڼ"k
20@NMyB Ɣ`JWmtjna](ikuz0Jzb
.|wa6-k(&ŝ2kzwETjxn\j[?WK(MC):EuF_<_juv0 Ҏt?/oS`Z5['ªuk7a g<9R9#El~΍#iɱwBX܈.&3eGtHvP1d:O|&5}I'pe;'KCO`^ݞ]\ܾ<}/2<c6uqN定Llĸ&&ދ[{:%qNf{NX[렏 aI­jƊo
A$Zbh*ot]LzLZ*O,aE9mSzZzxI_QcK"1LUSMem p@*N咬zEPd69Q9k3o/0{.$h߅'w;t;OkQSdRnuwPnL}<Tk.KbS%j8D`Cj,-Ҵ/ōX"ZōHa`PZ.V<l0T*Q@@هuGJ'!%D3;,
\P6Zn!bʁv:F1b$>xKhS˵"VMM `!رBé'G &3loxKrGy=HIJ{'CF2_ @1؀HT'R'Qw#8bCU U|{Ʈc=Y''v
 6;)dab'A֖xNSd{$V].[
 xH	&>GdUT!1AEh[CE,QZ2JJR*+aK6e#N2xhTD
d74WҊ|MyJ{gV.?ؚ#Б>벭؍@jBL/f.j$
 yѥU4[78wtaGGv)wm	v"5ԢMȨ;Bu\!_-&+GWU-6pHӺ'eI}a-\OQUypQS1rÆC^m  ]{tlKbPR@.<WܭV'ԃ@Y?cCŗjFzMũ^?/P7|xA5r.&cwÁYR*!GiqHyľ.ncN)PҝT/x/h@,ؐ`[jUZ^%kq;;-P)-ᚤPujglΗd1]DVtUۼe4UXKU//NO|u0$&bM	x$u
#(S9P?2|fիR8"K3n@M=#w4عɰ"ti:?##1bUS5Z-~S(H&O	>E--6NP>,A/IRy̡V%ZB"ql~jpe&ݑa-D&,-'6M=jO(eϾv<ilF_l 0yAҠES?$# vv|}4	FϹaTWBDPϹBn]K#\'/T)PǯfCtYy4s,h7"ScJOFcVć/9F?&lV1a#< sMNu rL"R(3	fF|>sZzb?ZhqzӢAe1Irqp:ռniC߿o!>E]w;$;vWpgyX lk^RED i(qD*%۾'[eRa 3.󭘊ρΞ=07ʴwׯ1Bx@5diVQjΖ%R9SQ2bԅay]TU4Zy$&vS=WtD!ɣ M1h9_'zZ'f'&t`); 6A(< هyGځ!{8vmyv2Pi!a7v47O
	hzX@(_s{āH!xUaAjrKʏM^0[$j~I
?4s+"~'?*b́/pQ:'bP6!]*BGG
mdoq(Ywv=B`!Kk6«ªnjr-g/p|THd)$7Mz,>'Juf9J6w*ICG?f^B7Jwקfx@$*^\p꜋EВ,M~4:8"xQ5$ K'9 i3d65
Żw2+IkPT-M#ಋaTJQBAu'K8H^C. 2Hy ZcmSC6Z0IdYlX3p.^ʱ!!1mYD7@j)%Ztˏ`]|XwE>֫\/zG"d쩙,xXk@o 9@sn7bo+| bBa8\Zg6)`5am:Sh;2"Qp*kMEG]R<DGzS¬@n˿icNi4LL'{تQ5vP8f\hXՁS,QXltbͬ*zOld]Ccr;0FyR>Pjo3b
s>ߨfHc#?]A;_>l# (Qp6r,B8}Ȏ9(iYgmG3J[]s?_S{fO	=BSh53'Td$0N4<whF(ba!7xro4]pZQ>qgXś}i뢂ݚ*տVՑលUU(̇kfAk	$;('\<T Wt$vn*M(_ D\E<=P7"{0Q+6bV&BJKKzߌGpEvgԚU:8GW]/<NWI\'*}{}(9N|	i<aتӮ7GEy~"E=dK7LŮ 8}is';޹)gjϽ##OaDCqgt);eDGcaBx_vV frqjf_54gDcg'I>PLD$9IKV|bŋ<p=߉{ydOrc41|hh|R"?$\F+X[i6>:-u9)pƯPzGגv32#(/z}/|fʃ;;x@w]iT*Ԉ8&ArRwge)*	Pҙ420=Wjl7{Nm.MM=y3&/yuW&'ژ0-խAڤ:Rm"<EV	shd5:Z:k})K2u鄤s"upT:67D8*\hP>Pg.f<Y茄c"R1 ĹjX㾾k=K[eD7iئdFJ#Rh
0g|d3	~x8(p9GB"{CJ'lYJa[YJ1:JI
@ԱOV+NzgQa4ոjWpPBTkfT`hTd,]	R!7N"µQ[WvxIxg(ԓ6h@6k<nȿ"H/<:{@J\V0Zy:8)oٺY`՛'6}h}>/Qۧ?QuYzk9V1vx!{16 qޤx<(wgƁx~7$I|YdB"Tpúᇐ<5~Q
JJ&JspRoue1  u#ABM|4HTCbJ'tݜ*C<tIc+h<+vߪc
]/ b5hF=XI(9-阂wtf2o2RN3GVM{B!0Ŵb0~Y);nُ8{}f'扮9mh}L(r-dw5EOzξ5	^0(rc(M6%UCm S=U9i^Tds
U3;FiKKWqt㨂+P.Q[wlĻi rd)6.2k5%S(c~Sw2z攃r-[*+R
ӃD\Oxu9U#7ϣA`ox9a!G=<C%wAG{X"_j7M
Lvm1@_n{@D[P9&Jk
g 14$(^N
/[ʽp3Oр7vaR<-	D=I|#<4579Hƅ2	6[q=|0^?k3H.;_	b#s&OX,(%Jk>[*i(*)Q7J ԐD8[@SGBJGDs/| M)sg\;cwKX@ηTr O5{#
gTߌF(&ΊFy3KۻyqG0ز8ˉ/^-# o bUz0W:t[ήl*ޓ=]0>X 6 2ԁK?ɋw߉K/9.cP|ExY}Hh{YϨ
WAg$AJ,}f5YD~ۋӛsix5Eg2j+L]@wF
!AJc.8)#ѴY*4[ޞHlzƠkSebeSfYcnU!VXOykO
bub\Y1[x
6n}~]ľ|$J*Nɗ-x
C=T>6)SG&HDQ5:QX5ru~PA1GrXiи׶l"ڊ@ҸƩJ@8YJCzcfĥc@5P9y^Y`>bBAu`Σ7uK7R_ƶ _,SO`-%qatQ=),p>u0?-/wkwQ~]gy+ZhA1tМsH5;KC
M4nE|w݂5jmBȆ/MgLV2A)V-r5P@eYjة5׽)qv(9Np '>#
l7GJ+T+JpEx߃v#C9烎#2-?Ȋ$'S0N[L@C*jF)̥IbPrF kJq֧@e-, "dЏ 7_A)_q*Tkrt0յڳN& 'fg*dU*ZJqD֖35>GE?$L1XSD45:/Һq'&à2 	0ɀ`FV7Q-_8ùq~݋wx-voPp6_&Sm'ܨDң	"{P
%.unҿUm/#M-3D~w)]3
XqK	D)'TZޢ^8
{2LnPD1]"d ?B23|Z'ma@߇LCt$^%ćd+qg(9NkGl0aSB&ȷRoىEX\,7'ʟ
0ch_J~]$msȌ:"rd	cBK|Ԁ]A7gzʵ嵐qsYv2'1PbFBY:\ĥ\]3`vp;?.+e,co@ޒJ{[`='Yu=2I7|̺0h*sţ˪ڡ&@fo2n}3陵gTz.,mg7F&Cȏ7j(al[IvQ\"sI>r?lŨN^w Қ:MEQ/;v?sX!S㗝Z}=9OGJTfGfV0 P&VC{%/P}&yQFYԊJ]j^@~\:+yDǘ-F_qt7sUzяzZ_:ﾛ.el.W@#:xDFԊ2WD<m't,c:z0+PcmgF:b	 @$[:0p#0B@QeB_/gHʹy,2z@E3r/V}Af<^1d1 9Z XlT}_dM8f629?D0(l-\Ǌ^]72H|ωoVESmlc'"oԇ.K*ٴ^31[C	{츂|Sϡ|`<^4%U-4NF_ 97"bEs1A|=m7}lG^%|쌓l⁊ {	UJEI-S!y{ZZ'PBb's5!!>{ͷZ
)ܐJRZܐ>9*nr$춶n8XGCOv	!	8ћb>#E3F*Fnqo |\o:͔q(CrK		OVkG 'R&7BȠ=aͭm5GUGs_ET{MًI>udY[yw.S.L}W-p$b(]Ӧ[z3\e9ϦO⣷G#rT0#yޒDK.	"brN
<q$v>^k6xœn/IJ n拮vz	"=PO.XPch;{z=};0ݧ"k	
Ax%w9g*ëv/e"9KuQv4{!p\3UF)]rA?}*\ltIX'd96Eba9b/Cm2GWZdJpHB:/|}{- iTTMy,"c	Tr躚IbkPi	6h9o$Q!Cʿ1,"$|"2/\'hiJy36iR.m8q(\ :0Bǆ`Lg`qMV#RGLsN0v6E48`jI6Al;:LOZCV~HvۻΖPú)98]} 5h0׆G`O6"NNd8@h-y LW^3\y/c)(?-ݭ/vЦY&$
Ǒ]QX$TϠ0٣dv>c! =L,gfX/
@bNi'ɤl}:+er[&eʊ֦̕B腪X	i={(az|V4nӺaU?`t_KAtMiL*P-s:O  g5T6
zd8u}W[pPV߷%gcܫ?̞]xiG ?0JM@k#VĉPV3pn%Erd`vTe,>ߨyp._rʊ
dq^0pؠ>@PF|'*aޘx`P]Dv  2~T義'j{6dI8[V-{zUjۅ:v&-2#⩏/~O征A4Rt6Pe~zG6aV.DƐdDW=>m)5yJ^6URj6Ɍ>??'KJAi#>9kԨJN5|Ӟjװx[% <14U5($Y|oD\o6(w+Q0*x=-O`Q(UoV@^c렭An?.$J_~7|v(|TC"!ˬkYЁa<zvkGW۰v~֋
X֢-iv6Zy9p\ITtyI@kh*2לg172_k`lc ǘW|?z[2IiK,-ޗ	C寔ԍtuF<(_\!?t4Kt
ub掽:Ax="g7.q؂¼IV!h+7B*![&؝D+ބNPE6!	aJ,D?;!//MMB|iN̅*V QqHenFG
|6Q
;cl޽}5ĝMMˠ{e G(/7+@<	N#l CuF<)ЇX~mRG:iX4u&y'6]<;ASV/:=pL,?q|<\3g%sAxHݙmy$o &YPO\Jd3F[H(73ny{(_,XdIc('Du
=Q"&bkkɯvWv9D	<WY<Cx48d,F%
F%?+cVj]|| G3 ,3՟.V]P%ftݝODHB\wrS丬$!㊓G7xWqevtzZC\ϥWfƅn{2:@8*.&Tgy],Kl0ڨ**
9Vv|ہJчq0b7WɊDJmA3+sz"JԐ3Nq2psDH\}~[`-|'c>4Dװ@J{T4;5!(CP5Qsʪ!kQ2}%'ʍJ}:)܆wH2"On:Id zɉ<_svWGOӾn	QzOٿ& 蝱ʂ-D
F\@#+5D7mA.}3R'j%k̓n'd%}O)]!7QDZse/9|.w'G_'S{	ǀH!pDc}(AqBKl~2p"&&	xFҶdЄmTDw8mn	 X?H3`"nUc5%}ETؽYCװ3!Z54.4/U@Q2৾qpϿOo찙E}}p؏MwyGfٹ_+>ʑf?MMX #@q3ɟoΡume0: Rj0d1۬/lf]?PQ#;7VyŬhOkm~Pto3.$$%XכrEw֑F#?l4Tn>w^J&+.WMV]L!Nk<eյ>㫋0&w[Q4gou4 &*a.H@ iд=D9%zκTo_TW8@D{Jx0S~|VM-sei4.#5&<Ec;6IfY,F2}Z"zJ}&)䚘IA;T_qFVHcj", Ԍ-CbV~	CW{ߚePz qTqeN7ǈ+["^8",a)h-J.~N5;84excJP3Q2m}ɫ$EI5'_Y@t@{e<j%pG70DQ F6)Gq~
=YoMI(λ^nkjX]`\Yc
1:PpFIL#ym)|fВ^w=nϼ<87.X#*NE_YʠDj~͢hqmvVCYw%K*݂cfhv	ƀd!Ūb$bpcg56^E=;`b=lܩX[䧞q$dg߅>]pma=s]ڽ$|@,mx׋C_?ox󔄳8qSŖ/|@ڤKa-7Seɷ/^Kwe.PY1IdibT-mH/v${ ،C* FB($^<*ln8cZ_<\!jk>b)o#RgOì(nIB6v%s$ԗL%[-'!g&ӷXg*e$ejE`xZtrЀ238$CzBhlhi|3֖/F_?ɂ:Q~EJSvuٲhLrY{SYn=UV{ɴגvj֑;MԺn-h6ͯ4N3#9sY']\І>tyn%ܓIolp)7BHN9=!yA1T.<
T*#zZv!9MF/17*n8'|`p7hAi%]x[aaX3n;	Q&2Nُxx<%"SʚQ7KN&k4Ӑ4 /ރ
b[W%`ȋJX)x@[7"ɋJ59`{tDk!Wv"*r'3SPMz	׎aOv[֘vx
_/^|PkM.>	S2 8 *q}ZPbݠM-A-7/)
l2Zd2JI/DnQ(N$`q+5#JY<}x#^m@EC{NϜ,ʋоW䛵Rb&R2ܥQ&qa5v=79-o-4̤R-j f	aˢp1˙P#n+=D(LQVrGȀk':7(Yk)oMUEwKӎ9ĵ8]ĶMYxwVH{2_as0c5佹IMli4V>\5 9 6PVp1]i Nم(nD%@nubO8)AQ2,^g riz[ʊfYc]m#;׽dSfZOja!1cRm9vB,7ǟ`%Fj.H4oˋDhwDϊ2WP>| +T']wBMWP8yS ]$!!7ե]ƃi<"X<oJL̲	yLC'0ǞOc0zj`EN#sI:4|49]_DRg*;5of.|&bN !KAE\BU}~!(8SG/y	rM0˚-FG\H6\38$ͧ
QF jRʔu2V,UǨ,gbg>?F<Q04Ľᴫ(,(i"GD\b|B*`4-fdIڦۍ!ZdzW٬z[\W7LL.m3~&3୔^X!2?@^ɃGGkr(cBvJ`=psȃvt7}e
fbc}97Wﯾ}":چ!
X=%q s 9y!oE5 7 x3QD</}?0!/0A}E4
QF3+c(:}ѨJye(obc

DPdUKX&-ud֡x1At؟4pq0z-(}'6<A^Zp?yI/3PUm9-DgBd=peXEFu32*
]ua!b8Qǝ4%Tbdɱv(PMz*D4]ZhdWMhLA憼"¼"FlNJ¼Z\ZAI]'"eDx`lSV<CM^A]Iҁ(WQ;ZX+_ߖќL[fmdmFG*;,++~ǥDɿ浻1	o	
wū"8I38*]s| H!
rX$a ڬy2M'$XCn9g_*YOLê0Mk̈dk	$|\0k$ "PvaLp-l,+NaT̎eaZS;IڽqÜ#nm0<ЉFc2BhIHUX*ƒh ]~?:׃GF5H$^x_ZZBOz̍o}sS3*ؗ۶RǰW/ca
P5Fj5 э%eqbdG	otUy"+FKDQ@[[kv%zWlYn{pqO&SC \vazkď'O>.
dHӴ=%(y,xAm,7зA8Q\|ay/.\m+!ÛM$	!"(?Pֱ~ʠΐdBItɣvѡ<^aա̹xO^2CԬtC]>1Blkeyj D MKQZڇ
:TkDpL3)dσ-1CjzX)7kga3PD0MJah`= (ޙsnw^}5 dqjx#8Ipޥ$դz7z+Ҙ5dFw $aF)|w厰bqm/d@]i
l(BAoΨknT8D烀,{ygbzBR<Ϣe\3b6Ʌ⾄YPmvC{O49!f\t"Hjޏ>PřbOaTs#-rzg+q=:n#9T9*^MB ]
.67ɱ!S\4_IrF|rURM'LY4gfr~)}r0P9=綛=ǑHm텼xX'\3P@K!BïDe)HN)Q9c8o6La]fu1y`͐bnc{">i1H*ZNҏx!Z U+e{DX|w>U'g?:I H#Hر'JBsE.raێvHdIJ(`niV10iT*},"LwW	7}i#=nפ(%lpk3W(L9`7HI}
_2;Pw8x`&X;E("Gb=2Ԋ3rD ]nn&,pxYVŮK߮/ۏClF.3e 86Sw׍(¼v~i->^,P_4CoqR7/􃔇vefB(B;98+rP*>+T/1j~гW~μg~͝3W|*VT(jt7#!1f$+vUao*d<Jyj5ʯexvHYڠR,s^$Qldq+>JF&eLTY d7L[Be`s).wp}{+ъV+GOt3`dcKuɰ|d?Л.rn Db޾,i댍b6D:dü-5X2Գz>wr/Mt]',Tf(ZVN^Zm(*-U3[d(WjvP7՘;lT$eqlqY9Wd=~YV}t21Iy@=`Icَ5RH	`xN}fYLXŻ[Cb^0 tM4IR'cgif}R' #݃:تS
GTW+S6g_$`^v^lxe}1^(f¦@(榾kN!}IQUlC!mY!V20Ѡy`[Zṉ*oh;eו?\Ve 7`FO$q~IC%zC)~8Әn[d#KR6c\RO-?B^iT#ujLlK3i'0*R!od-n={)-@}mw|v%l*j,⃤G8.+HD[	0_ܨבO2:=Bb`pČrh+t)MG&æ\;Nsp0;7.f3|m}Ƨ]]7|
PX:n
<,A~XSzt*	I&"+pQ@82P*x7b<kR?/Jċ	ʹTApGsSCM![QWC%-ss"}	HzY
ŲD}x1G5WRL
ʽ|lDH^Lvщ{_x0x嗁sSb z壖![fBbN
G
vi4m8/o{^Ba}zAI:J=w3Γ9C>^[bv20*bjai3f'H=24z2QG;ě!0<JaY'lGoZE	veLΛhoRtaP!^!|10k>"Tl::l2S?)+;ǸW7Fsn7Cix${Tz(%˧5!(sb LOBz,O!@Ͼ!mDx)@؁Tg-)EY`p=pULYisKƅr#\.0JeE{9~kqyܜi7$1֣8P]	cGŏPF%	9`ikB&+Snr"*ߣo%|D88ts-(>C[N䨷
>~yܡ J_puQqoUK¾#'yF&"eI	'L ߐgtjk8 04%3U8f:^׌lȘnfO=4,尗l-SЊ6״X%i}RI8Qdd#:OUŠ&|P Yҥz՛--l"y^~N5t6:s='t^1;+<C%aQ<vADnaװ(MTL5+cTϻIl:va׊L	w е>o<zoE>Jfz\У<,sl~qse$K=+?	/o_9;4`lӄ!(Td]TL|!m7tޔ7sGz[DZ oIM }4$a%>NLh
>(!dq!>Ao3[b#|7qZݲ`;6`!l ^#DDc^n|Xގ3,)GW _޵aM=}JVڥOXDM뭲+.)kp{҅wA$~i6KB76Vay	tB<<S4ϙmy%1Ob"}s8hD#d3^B';	7^N
JĬNJ">ތmS d=GqiBOM=A!{BCCz8t)3YPȸM>Cn´8M2^ d'ReeH$ⰚN&֋>]ݗx/O
UxoQĝ;ǚ9JXu-.c? lՔPm׫r\O?|(J\+}9gi'4"Q'rj@K*]Tw<FdX_0 u{ :VrT:Ap6NF0jC`>vSj"{4𣲈`֯[>g'(bq|Xot-Dۏ&o"@'Nn:`΀<ǅ*Jh8EjS?JJB_%W?9Qʘ&+l>KU2$ݓi!5t(U,#ܐa}\@GOGg7,SE63<NZm.՛Q6r*&=ܗ6vO1<
moWZ)Gq)=4..1JSNӯXH06ni<1h,hɴ7P¥SYLQޠ].́n?)<:yy$G{dވXRK͈Kf#H: lds&@vZ[/C<HbYE*QVfaitח[Q \j͓٣b-i\03'oeW>C^wf"f`v1,3[nZ0P*_:aMj66=VFA]*vZ7a<G%oSAq񚢓#|˒GiBV0|G|hJ
uC
.<e;bo/xLү	8ObӤaIq?@20(eavCq<
<tX>`skM?P
Xof,	](lO *| Є3Ր݅}8pjɼ!]	_0Fuߥҋf0Dsڕ~#G*^KJ^Qaoh4`zrFIvm}5UR倘n+*b u׉cЪp"3iD>(9*x q4R//~ˋw7N|c#<;JyK] ,vkP39<۰ά`TqQȆ!'pXfHbzW'K4KFKI#	RI0* Mvix0cD`T)j7']dOp^<Oe4˷Aí<6ig5kܬ3puwz^
a!S:y~&)b%XqrPZ!)V~fb 7)JY*u1}MY#/H
x-2E?̯%<q!H%!ps11=G{KZ{[VDOiٰ}uSҜ 2Ռ%F䋜YXa\;[NQŷ#^?c<lg(^.?B=n!Cx'`Xk;zF/b;kPjs6I;Rye`k/*^q1Ck~9,̇_`z#Ɠ1&3al}˸B5./>E8nCbV yOt6{e$k6@^Y9w,ދ$aϋ]?6xOX0@r_#-L]R~iaI2Y,lWFοԆ=lR2T#ҵmI<ͲsR?5־{Hq9&T
-U~$1|7([/Ԫ5*FZx;TPLph*c%cѯd,>
YH|fgUz7n1#?ʼzj=
^pF;tmoP \Ґ|R0H&8N1%.=bi<TƻȟonʦsYaƢIDO7Bc֔/aE?Ye=K8L٦X'BCD&kŹdn;"ꀆ	J8Fa5IashM6#Vy6j*m֎8wv0-Tx3"	qWAdGt1#^;:|IMiAA!A",+RWTXiՄ`Fa/V;#=lsW7Be%ӣ:7}}X/s`ڄ݉7N`f9@uw*l>9Kv`ױN^ZV/Ȁr<1bc,%CDaxPFDX%u+Ԛma*B"(ʱG]xl@]>̘fHEh|RC!&Kfb8τ/<|u'ѨȞ{96M>w͟6J8pS[{󨒻<iPk<= bHR'-(?!`dRx&ǎ!a,Ȁ:)C6٨Mഭ6Us,G)yb2ie&=(z(|Si֠hY"CB:a$p%Y0*7z-d$Z̉}lWnw+p$C!d5]C82y,ڍ
ibpεmBX?;WX+rRMb#~m,QJXagA6+H}='ԃEq84$bŗoo6qP~@-ۥb&FXM	'eBua<7Ng,Ky
u8(xonr%(=/7J_P-|9v6Rgz7OPgc1>S=o^qy
_]c L]y{3 Ez`hiEMglѝ\ON2Kn<V;|sbh!(h80A~V~G(6q7껲22a
50<,'8t!ua$Ev>(0g/r1FH-2!5NK/<öe-~prJ{q3_\"K0}e	xӜ8\a qR*}~ʧ#B:STm QLZAڀ~O*=n)j_vA4q?u}!a9v8`YZd;/eaxI"ox9P>s̜?EtBzbH͊趐!efes\͙ U_?%㜿xVeN^UwttcZ Ppt]Zg?S4Wt^!ZDN6>_X]˗u9Q/2Qh1&SӂKr9 *ϑ2~\'K*_b6+8<-doY%RjH
0{wɕsx	g\p]o=	ҜgTFgw
M00۹]mS%rJn(!;atYB][joر,[;[ߢKHWWQ/*VxMxD^9)qPBH!kX5?hz%q@8yC\%29NNs#I^rKNa#(C,+_.!@h>F x?*0vRRzCߘ>JmٕB$i@&ov^Qu0S8Vc^zu-Qh(Df^].e{y .@	[p߶O*#`lB	,pBTQ,VI+G|43FRID=??-ܡTS5@Fw	H3ÓS!y2Aʆ51GeӖ}aBG02>|J7i L6|t T/ozi܆E]S
5JXcʕdchܵ.//{}qds[`L̦X|Cv1V7-\m,P/vV
tr#GfWNѪһ ."HK&:z`*;k=TC=2%ށ~_b.TE.E1;aR6'cr(0JPD-4%0pكq1"K&Q73%@&#woJYQu? Qfu
|
)JtL~<5+YB)
9f"^&;[ 쬂s+rExݽ{rJa˖Z@J=
HXG
pv#Myq,cmC~ ԯw}vԛ xasY*f7!iϴLJ>SCPfGYp6WH&_ ,Hgj_US/b|7SK4߿&mrJkm6ױ?;ųY$o	Ur.P*c*he:-#6Jfsa	Z#hr))1Bi&r6e9Q]Gom<KIUn^?턒LV]Zw~{8
$)K9q`xϛ;A[Kڄ<w}r353IqkE y-jFjlpvI#-\c!ZUk&1[geY+Cʌs5+'JlVk bn>p<%lV)
|ƓZlTKhQy<j?XHD8@&<0Y4aOTլ`~ +K(	_g De'?>㆔n;@8
[i5!tfseZ$6].ה:@XπF\ho^|ԫc^e^\Lv; dl[ǐH^]iw>Jrf=!TW
G[FPH	C]xO/ιT"N10  +6׊LS6lta3\s+f0RKAqJ+7V>ɌWL<},\=?kH,Y4fB	d.}M;_Meµ=t_	EܮD$,.WW`kR`}[jtWLKȊba~}}[w
kPLW	M'J"@U.>`w'#aii8`|+Q]Â#y١_ҽv1mؗ}c=Ҹz^ I	`6G{cJ12 2
D-T#0lWl <ς'zavCfFbo	'h9t?T_7b`Q}#%.Y,E1WZڔʘy{PԄ!0,nPUacE,]BK!poWӿyU'}6IhBĦ;QhNuԛGFᴝ	,$EhmZi
`}<;-PM!uP#[[%a[GWk*83Cð{yY&߳wGFPǏ̿=ۼءM-"M1_nҚA({hLNu:[C598B(=}h8Xg|ʴ+:$
qnrww0wєyKu,4A7x#bԇ;MI1"d!,ć(XФd㡊/]
!>YEhB7V+W%Q8z`3!kf>˭AuJ-RaF"J-M#6^>z<wjh
fuTcR}c3LT^+jo]1i@XN0ao$m\@P1
79xq-8RTzy;;2&ut&赈h5a#$m%=tV	|d(j)9[ٺea7N[≾
.!MN#|LXnI"h̄^1Spdb8q._f
ߚuǈE?i@"J>N!d 2t>UCPŷBl}:ěc_ϡ*Cs7w?ѷ`,RIi̎lsD*Ydu' -A tG⥇ǛzᄊԈlDc<phrSs^Sr5XlYR1[R|A/yi,:X}kT6$Ӕeaz˯aB2lBZJlPxMl`Xdk}!_F%)s;AA|L]ҬOF,	튁@]Z~
'c;}YA<wKvֺ|bs?4Tb 5W"ƠLK܁`,Z"t`@~SUxBdCYVG4>JrPw IMgѭzsZE}<O2 {9o$y; U*v& P!!ަEPʁ-A''7x8w02تU2zQ;6OF*.YCbTCL0VSg!o9?мLxcxEHƣ;P|Y*-(U|ifv=`w+rwbԠ ݘ*ih8(i"Fꞌ\(6"@㞵 -91?ءMQP>.5Dh%#~;#_z,>_c,K=fBT}3
uzP6!tJbL(vޢZGu?Vnŭ/Ng iӓpjOu{p@b>a"@=GFI;6RΔ'f/*+<
+k`@	9*vG}"Ohxb	cd"|v-z.P1YJM9w	mgY &gTP}xAdzє|h<Y0Iٱ;	o"d!s.",Da@<=6h+eR7|chJ:)dA>< rZ)JOp>حWʴ:U 8IEi
KPm驊$^KY6$*Ӷ4ET SMaa~&gNg'30kW$3r7niar%"٩$0eC<_?{HUm^h{C]ad^kmVɌVG3Y +
{tC
ހ/h]ptm9vJ.(1z51c sN<QXQQM)/3	t` ne.k[M7Б2p+KQ>)	;H@"l4]J)":xؘl*(;6a]Xn,x^vEƹȑ}9bFU_k֕`?4XEzb;E,vW~A5}]XLf!QnS|M)e)ҝՀ0mQ~	jJ*%IުK)CPY*IaL-#6*q=	@
|~]y+?x2TͲ$ĒXgqОdNvn(ۡT,!\$$!?%e(d,F>.+ܻIDǽ+ΖT4'g.dMfOCkհ	Ed
eyEi4MJBr` UDO!^5^_ܔRD#yH4*B}hg gtPDM9=:Xcߛq YHWE5RZ95:ZL>Cv *l {Ie-Nuj*}#r_A8X};QdE* ~7;i(zB+̷%\7BՐ n6W}-l!+vG@t%	*/lGL}F?tChרe&Iׁ3btQv$_*`zݷϏ\abÈDDXڈ[ok-A\A4co!P$@;eOX{' ý"aBu/9%?NKQ;4Bg"E@<	GDZOW?af5д*T]1IbX٬+pvUX{0]ꦰ0Z]WukTTA^v:Ds5+C=-payaZY
2^G@{(gʨInJ#^mHKqz8|MK)|,Q P~R`p-ua4^[p/+:mS!5@8oMV<`Bg4_*.Ao:oxv+3C+YŦ,k;-b3ҟA`3ظH*\xM|{.G,f;QgeaPdu?]ton!st87#1ɜϾ J2hgᠿz"cf
jᩆ	7kDXUZnf`usj6<ͮ7H#('z*V:k!Fv4IP39Z$+(ȠvD$ԝ˂mqVд_:9X( by+_Bu˞V>,I5u{ܟ*PP	p[4DXr{
zf!7ss!$>C,u
)u`i0)+Q0LqR	SSĒ]	B=+2%rv7"\,	.^EwH7W>8JP!a& fPvp[Ue?xQrymb[S572:0Kٳ9uuW{49hϾ^+\n/j&-ST-ܿp%*^}*1,BY)1\dKBBvUCT gF=!(UuSU@*cV9x
h}MZ*YjS	jzW"\Hfd*we2j'!Y-P}e 0v3iSĕA>UW'urZ5:	f]w`~IBSh:)ɀfᓈX5`zc:IXVS>%l"#3gH1X%i;^p*H)I|
srvgl6D.pGyot	H(>o{f	SQPZv5̉pq^tYU%]lEw(BhXSחqݾD{#U0Jlu3׍Lzm콤(.,SGKҊ4y'ˬ)*+$!#:o8H5;V
kB,xKTXCz7j
JN6Hk2'1:B2mXw?B+sDP|Fϡ:{uvyV1šв$S6OXGJc{iC>:F[SP;X 5dXr(v(x"=CL#)#LR]]z8 ISE6tOV!y5tସXz;c><n<]Ny.R=ppG稶$\9"Z^LTMPӡ fJ`U<M5*	<<KHiB ˼Ry](Q`7j5W@\#ф|҄tm-"Y]d"ϼ]KEkZ>}GE>/+HtK.ԎhNh5-g[>Cd[JyҐ&.jM{=En	zUb7,@d>73*g#̲wp4QACbB8CwoчBS"Y| Ll>Y>$ʫgVJTs60VB,wy7l#Ho6!1?N])V(VX}7w1ɞάMdl(dy,c36],/Bv1Y?ngݥ"mڔ"܉<vWpe
A;,G{h8!GERC,dH}-.!ɶ/o㵴ȉ êQ@Z	긄0jI% WQșYK@ƳݖʂL aŇHw6Z!*F%QMT'aDyZF,&j!9{OXwiz*|%Ea\jBw9Ec@xϱu2Ruy.'J":E8\Q̤"v38OIxH[&Ͳ01J	9	R)($@ɀ!)4`h0+.LfQ-%m)oCG$aK;C_b_t{]ABvwP{}Z>P%)}|$^|G8)bҭ3dnkFw`qR?vS*\޿w9`m3Ȥ
bTR8<c
hBh/	+c]_^{)G^`ʸMBXw'|5z{_ѓ0Py/6umwS.[;aRQ[$4z]u}APi(,oI2B?2FU^*:4hKYwFojgxXn;&AN"#	V{NG`7|=T;ӮvWЈ@3x"Yb_V:jvQ8q<36aS<Lh,v6ԟGcX8a(1"2*)·~yRzO.LƍV828Eu[ZB6-?)x P;@ >t, t6[~և39UІ`rzZk+JFE_e,Z2IXKhQ*ӜhC(i]:좂c*XS^>hV 	c4j
̺qG^v@w]I]#V$a5Q'.i6Ym< 5w+&	(L!7:UĤѐ_x~UtrO4~ TװљEixpR%=VM՚j[IP%JTHv
B2b@vXh)&p:RJ:q YJ	?Nt<뇨@jRu"h}<śB N$&8s!o9rĺ8|4+)5È0ΚPqM-I	gD:\y˱gBAfb3^fǃ1VK+Jz-R8O~ȊBlUƹ?xJ!pbL^=G;8r[LS#,$BL<^ ? i^2[3{Dbg~ܲ@URvTSxݣ[
獺Im9ZGFm3h%t_bͿ+1TT xONӗ!sؠzJͻk_Gb.ʰOU="ͨ7]٪`۾"W5vS=G@a
étDlQTfƏUc\.uv-S+`vt,h't-	-T!h
eECFCٴt@jC/*!ǝj*U*CP"W	!<bKs塱V>-83ѳ*䣻}a]$Gtu"#C~{	ö90hˁ=idE[l$v<9wv|RV#}=5KZm"*6JfrCQ Q#	e/2+0j`pqCQPV1&px"%+[T#%??c+?MDYHGi\Nd`NڭSl8/_ٍ7(axtLG_}wnIROQ+$Fua	h,m8h=IRxc(5u&Р1	MG%&+zuc#X!yo6;bEeɲѪ9/ߎC; &&#qû*nѤh	'M-$I'<d+	}|;$b+U	xc<}c(*O),u|ֳqZƑ##.I~x!ضMA`G6E;UZX	=)Hs(ŁLCe@|xUq[zznsC0裺CTXjH!</ǣPK    .cGZ&=  %  
  config.phpUT	 pV1mWux      Ymo_}%IEi@[MUr"WruE{ޜHؐwg988a]*]}uO'(I<}8eV٠&K?NCj.#j cD:~OKxX!x2eKȃ/x|QQg12L|%X:W0O$E~+fbvY.r/6Ʀgɮ\˘sV&ε0uef̸eAh]$_k6hY}>Z-ք;.ȸ`ܘ1{GGќ)(qq\h`:'',qǀBD<s>b?e4Qfukpcӻ;ӑg;܀^Z$&xf{>("#.B2L8d3r~ W,
y^+qe3칷csrؤs
I񜜿-+p{G=F_ɗ,E %sP6[3wiWO3lVl$BΟ2D.N&%߃ jJ~Nla9es#<v+;clCw?(mmlol9Ujs%GK	XB ڟ;h/=PְNM52.a[B.(~_歼t=Mþ>ӬlSsR.Z8찢jojتiOHK5<\(}<bv}sr~O=kxPc] {#Tء`=KTJh32WFYe9yPyzXC܍S;upI-c| ˃t޸Wv.H-"OluhwUU3غdĳ4u.<Kc/{c
YRu@'	|R(e,v\dlsxZ}b^"ɱb4㲕&MXN

i4g4G<lO_!+[gJ9GK<+fϋC4X[!u#|`;B/g$)jCBqPOY'qtF|L\umMDW+*TE8	Ճ<#iʒSN 	SߐG$R^4ޖG+)JvsG!mihojB!!3[dULG̚F{Q".bN{ KVZ&Ȭ!R陟+5K^VeD͙p'?C8z(4o0+H,NiMhJy9N#'9OSrgΈ6sHGm82ᜮ.[l`zch]ADs.dw&؆Tɸ9Wi"49iaO逹ݱcHyeGЏ۵zQ["2u;+`[.ekB:ȵaejz%۸2uM>(fuP *oDYL¦ȣS/tW~%*"f>$lRmWppjeg6n5h7/\E~S8t<[2z[>?/b=ŀG3kcjK7'@~.NWhnGޝ9hq2Cnx|Ro o6zݡ2]AH;*H#fFA;tu'Z.ltW<TGЦ
Tω=FiGrE#MmLb9[f?Zd}y9\{\3 YL7mQ6ծUq Q}T0Pzi\eBMh?u&Tn$Ҳ0՘6^<hoh?Nl9J	AO`#*8j9AW/^5J֓ןQтc~cBkuP;_jN!WByuYgY&:8퇂Pp3YjZE7Qܐvh@ORVŃJz-Pp@ ]kУfBD-׻kT&*j~d(Eؕ8ғ/{LW6|uzoRt@"mXE"x?ѷb&h\_SsGIl\$3Lkr{2A^#p4snIhES^b-D¥*.M-
Y"YXj4-8NT>(^I__h^fqmtU}O9~s7uVo<M$=Vxo/X8]ٖ/՞.JTu1\a)KRV)~8%{靕M=uMozSLGjK,֓Ksl!Sh_3PTSwP3gxG;|:G?T,`&bQ͚U%Yx,z%3j8Z*V1&'+O19|dm԰Jz1ܫsu%~֖kMNP{[\7u<$[z~m{||&O]{z\e|WI[+ϰÈ
ޯ+q:V~~ϥbAOadᔒfg*sSzC FCU=~8}׎#7ZeX0x78 Kr)E|I]II(fHzw+3O>E5	9?C%gz_ɮC÷J=]yi[5ML[#܉h˷%.\ԺN3j(7\"wH)zGQN9WA4	fOnÿ(4XWQMha*|_xEv~bTe,=Q8	#Lq JS3~{4>rs&4otSnɖ*F5aFP
L> [˷oFköEAf&om!BR.FaH{wjYrPK    *cGa4^    	  index.phpUT	 pV1mWux      S]o@|WH!SV>'(M>DB'8ScTw!X'V;ss2/{lrBl|Q"A4mn#~:W#*2Y&<*܈!U u%a3$D
=eڑ2 W)D6`C"
RUyGrPY8^ώkƈn&)"5.^L`Ũi	7+flNp֒$leַ{I8eXpx[ku
Gbe0n~qNiEcrJ-p'7r^Ʉ%{~ 83wU(;!E{#K|\!?)mQeT /v9:	{ՕNhupTlK	\R:r)X"{>/FBxqN$*j}~Ԣ*NeUrd?f=CMz~8q*?ږ}PK    &aG!)\      init.phpUT	 wV1mWux      WmOF_1 zvN49UN. kco-ήNHή$8U4(qi>*yq~WH¦L]l:}#c\<R!0LIl#S ,S*cF2 %zN$=<[ JԙR22N	%Yb <8tJAS9U *DR:\CL8042kQ˥H2m_g7=3aw^p~Aϭd?u>-8Qs1+kj~,dT7އ͠t.|KT 85tξv.fqL'$}8B+-Iy%Q1P&xK4!֖508ߨRh	PD	)&8#J.jHg$VVMp[$[f'BjM;OHx	=nՎ4DyMR/e27HA|Ƥ#H*eCJ#r2;o0ċ?Ӆo7ahg3;.(T1@]/uA4CpyZacRt-{w{>6c0!fgatݹ+8ctC]sdKa	ow_sʮ:Fۀ6*5֭Oo^wxܣƄy85˒zc)Uu5J21Y3S?qt,S3#?ƌ*bZ
B>^#Q߄0b>_ulJVcn.ׁՈJMFQ*vS4D,Tt}Bb3o%l5pD t|_OG-rJPqnq<2,B,)³acLcq|ە!Q)ԓzCBײ	}!DƵنTQ|CummIPLsً pAr]{DU0Yd%@n`6eF[	QDerv/1ɤx.{lV&	ho]m-V<vp#*Krz#1>M;cϬ5u䣝b2LQ1]pNcm^m	fodx`v*оc<8QWuˁS)^j=#<^e\̄d"ЉWr}(r1Fr>~*	\'TJ!hfMSNITm8ZM.r$gɾl,!<Ti!1]	PDH1ϽU'w&s%r5vCq Z.|mMAwdZEU?墌qPÑw֠<DERb<Qilk{۹<fS2WWp {>_?uo7{ӘIavgBJŏV6,CmՌ,i?A5Zi=fʊwJ[ֵZ=¿PK
     nv.H              js/UT	 ﵗVVmWux      PK
     nv.H            
  js/addons/UT	 ﵗVVmWux      PK
     Tu.H              js/addons/twigmo/UT	 ೗VVmWux      PK
     Tu.H              js/addons/twigmo/test.txtUT	 ೗V    ux      PK
     Tu.H              js/addons/twigmo_best/UT	 ೗VVmWux      PK
     Tu.H              js/addons/twigmo_best/test.txtUT	 ೗V    ux      PK
     	wG      
  sample.phpUT	 V1mWux      <?php
PK
     aG            	  test3.sqlUT	 wV    ux      PK
     `G              test.sqlUT	 wV    ux      PK
     mu.H              var/UT	 VVmWux      PK
     ou.H            
  var/langs/UT	 VVmWux      PK
     v.H              var/langs/en/UT	 UVVmWux      PK
     v.H              var/langs/en/addons/UT	 UVVmWux      PK
     tu.H              var/langs/en/addons/twigmo.poUT	 V    ux      PK
     tu.H            "  var/langs/en/addons/twigmo_best.poUT	 V    ux      PK    )cG!                 api.phpUT pVux      PK
     vG                     Ab  app/UT SVux      PK    2a)G15                 app/.htaccessUT !Uux      PK
     s.H                     Ak  app/addons/UT 谗Vux      PK
     "vG                     A  app/addons/sample/UT `Vux      PK
     vG                        app/addons/sample/addon.xmlUT LVux      PK
     SvG                     AQ  app/addons/twigmo/UT Vux      PK
     vG                        app/addons/twigmo/addon.xmlUT LVux      PK
     SvG                 app/addons/twigmo/file1.phpUT Vux      PK
     SvG                     AM  app/addons/twigmo_best/UT Vux      PK
     vG                         app/addons/twigmo_best/addon.xmlUT LVux      PK
     SvG                  app/addons/twigmo_best/file1.phpUT Vux      PK
     4a)G+   +            X  app/index.phpUT !Uux      PK    `GSd                 app/test1.phpUT rwVux      PK    )cGd Ov            changelog.txtUT pVux      PK    .cGZ&=  %  
          config.phpUT pVux      PK    *cGa4^    	           index.phpUT pVux      PK    &aG!)\             ~ init.phpUT wVux      PK
     nv.H                     A js/UT ﵗVux      PK
     nv.H            
         A js/addons/UT ﵗVux      PK
     Tu.H                     AP js/addons/twigmo/UT ೗Vux      PK
     Tu.H                       js/addons/twigmo/test.txtUT ೗Vux      PK
     Tu.H                     A js/addons/twigmo_best/UT ೗Vux      PK
     Tu.H                      > js/addons/twigmo_best/test.txtUT ೗Vux      PK
     	wG      
          sample.phpUT Vux      PK
     aG            	           test3.sqlUT wVux      PK
     `G                      # test.sqlUT wVux      PK
     mu.H                     Ae var/UT Vux      PK
     ou.H            
         A var/langs/UT Vux      PK
     v.H                     A var/langs/en/UT UVux      PK
     v.H                     A. var/langs/en/addons/UT UVux      PK
     tu.H                      | var/langs/en/addons/twigmo.poUT Vux      PK
     tu.H            "           var/langs/en/addons/twigmo_best.poUT Vux      PK    ! ! +  /   PK
     ceG              app/UT	 Vx
mWux      PK
     heG              app/addons/UT	 Vw
mWux      PK
     qeG              app/addons/sample/UT	 Vw
mWux      PK    bG>wu        app/addons/sample/addon.xmlUT	 zV
mWux      MA0E= #^4c1ښHX+p.!]3A}Nd@'qq[EK.2)2-^гL{uq	\wOuCŋA}>vokPK
     tcG              app/addons/sample/changelog.txtUT	 ;|V
mWux      PK    bGfMxn      app/addons/sample/func.phpUT	 zV
mWux      Qk0)K2AQu`^|EeJZ3ݫx@?~:Mtt\b#^_ZcQu	f蝆sn{=zwZBA)EHcO6aeU{༔k[JKnޤ%cO>2oC#OU#{!qFףhE'@:nJQB p/%}ƌ#^X]6L׳ilqVrP=pYz̔zaMw|a?M#v](^p8E+"*JDY[vyy$zR=y+`b_5g,Kߣ&-wPK
     jcG        app/addons/sample/init.phpUT	 '|V
mWux      <?php
PK
     ceG                     A    app/UT Vux      PK
     heG                     A>   app/addons/UT Vux      PK
     qeG                     A   app/addons/sample/UT Vux      PK    bG>wu                  app/addons/sample/addon.xmlUT zVux      PK
     tcG                        app/addons/sample/changelog.txtUT ;|Vux      PK    bGfMxn               app/addons/sample/func.phpUT zVux      PK
     jcG                 app/addons/sample/init.phpUT '|Vux      PK      y       V \k{HWT{$Kl3cpg4O!Ҫ$igst`'϶˩s}Og~õwlcsgwXI{,VpWWVGܲR
xg{{w6vP[[uccks[0rן\N(Ho)s4sm0JciǮ`<UM釿=!h;nw_:Azb
CGCӶO(ǍDn֛A:~vSx<eW$qaEEE0˪DPojiW8D"	;~CQcm*/LxJA('Jl[;cDvRPBwG֘v,?;kl	*F%hy|Y%?{nf~HB
fx8cF9	 j	ޔ	-&B"x-dt_zK^7IJDI"[iTNCgeQ6ȭ1FP$M6R@L=!}ȁwd3Z9hdbc7beVX*aTLI&3ؓ=> ^ol:4\FM}g僌aWp%@Q#1)%㏘(
(s'bdN id@J"to	ƔB9pĉƬ:eܶ_NX*l\%gB(I6|Kz(VkʽOL"}(
d25!?w,F5+:*351 j7OB^iM1hUJ{bvz}uY^?aGc:5~V~g/78Bn+Z}UK/)w>j4[_+s#ˈ;oh#;Ȼ5jbehS+	<]h8btKNgx&A\TX̖ߕFS&1!:tp4b]gPLw՚ѻu'Oo^UzҍXlD};gr5]13|eʫ5vgWYw]_\e#e/0^2*zMx(֘C(3K6 c}6슻M>C1N` #Q(Ft >7d"MBU?ݯuZnwbGG=~ii	f:n@	bXْ8FӨ0~kBvsU}aJ%do6lΏٯ31rT~,j5ք!( `=Hڈ)NcjI߯lv/f?ONVC+A
h	XSW!9a(y	u4x+p@
{<7>`iD.^~![݀UCO@Tg@PʪTI
5EB޳"iac__9^ةonA_}}Ka[ @fʒL\ID~HSa72<bE1QEt}!=ɟa(=*97%ʞS4fK?M&
_[[u:|=G
W4'xjM Iq-Zz=qW/oll֟c\Ggj;;d;;O"_\&pwߛ{]?*O{c})?ƵT=wZ*{;O>ܗݨNoonll<c\ڮ<V3xcM "}fNG{wI(Fv=?UYc+GۃqoolE	ד?u$F·*6<CQs W~i;}whƴިsoδT Bz}U}a	o BQgוmW l0Xw[PfFp3a[Z(F!e4}vdk9P fTԄi|H:V:m8oeVGO4b{ca]")XVgбsn}C0)kv"<tRH[ X*$Ii{Nq߫TeS犇_SlQVL{ p-+#18h(r5>ɍ-1IKԶ gd`A1FhREo2FskN(؊iǾ+Ka7h3hQPO8Pin`5PKnB>iBP7|{kγԠ	.DW'+K+~$>zje6Vh4ĝ~	1bǼj<]sq/"<D_4̀ZǂP􂮙9f:>pB7y`(0t_74:PH3Th+'vZFU6;pޠ  Ȁ	ۡm|n(Oh,<ݱ	Bi:@`>[eDivr&
rL`c6)^a!Rfv}	wDegG8|]I;6 8uS(IKJSCΔ-K~	m'[3x5qLlY׭dbpD2ltKBC@5ﮨ'lLs@f1\8U_a}Uu.~@pFbuZksf:RE̓	4?>m ;=9j=^
r࣡NQGs`_[VMv~>GƊ(
͎C,%PH(
}Zfn+)6p
)H@F5#_h6ÄVtP74	j;ܥkv1Y$+ywޫT@|tLPX0~uWkl nؾx>	XL
R⮒LnB;CveZv4 =V$1lSfjA#GQ¥3KZK6ȍEJo!$Y;[."`rǵMbvrqN V^3w2j,'c)t?g 9Ktط/xNV1EN}Dm}YiA5Zi6Wefߛ0^RM41It袶']{/Ǹ AG1	<K;d-'p]@K')
Qz=!R3~샿|xr ZM~!s׋I>JA'W;`$ (rw99>|X.8zg&{*ڀ	42-fOXҰ8k5j"8Z!E4Ep
c> +`˜D2+p9EWHWvx䶐4IH(ϵvSƅrF@h7YlK D"xZDD2BN1NNW?cƯwh&uS\`2؈Z,΁NuVAN成CcDT0a#vG" ik;HIO &T7lP{9m4657{[>!. G4Deq|[CV6s?0`êc|10\}gilbxC,WIdb֨RgSV6 qNQY؊y 1Nb!+=1jG9 `_	gqXX3gT$Z{B/P jb<![}O,+U0|6怟-N6TRU >zS! .dUC;eEac
l\010OIC<0	 GhgFRrD}bsQP{G0#bb<nt0ƯFzQAKЙsS7Y Ao]vLZR9zs20+#	Wgȁ Bvzk&촱;I]5?AJde4)tg-:i_̭{)GBsG|fڱcRIJaxvټ3<c-"qyv⁉rC3s"Cꦏl,Ӎ=af%@iTנp].B+`8J;W{;  H)C HT:Ȝxwf90,(Sb͒&Rn6	],l|X8pxR`<Gwys_Jvd31B,DR29)7$NnMp$H5OM
ʘq:Qbo:.f@E9Gg>a$p0F=1iU>wQ&XL5+~&T̷-nLs$ly8h%9ӧLL268#t(-Liff99*&S.'-@qH9JL>'I
DR=䕞3|?CX(e_&v<&sM=* BuEs⠲MiEA) 
¿"w6!/rHR(yXgi9lN]Kw>kS%}VLƑKr)qjP{vw`|:CJ-('h;~qM-u 6k$Ws+(OA2	@noœI=-)ipC_-Q+ .SLʖ[ciR҅stVr({FLQKc/\G/SKVhMФz%E?Yⶃ0In4e<iv$ z.><͇T,czm pM0*	z-j@eX)2fKc'Sb~V >^XH&vLf)	46hĖAZSn+*IuJ[ܺNp*Jtt5GFk,׳ɤeFMXo㜞ak
>4Y;ePއf~CgT;븝8&GSF2JApe9,N,H3k?C]U¡A$ࢆhA#li砸|mXLz.Y(X:h89+DNIדC
Y$XyGB>FZC)Quf&mf'Eb{pW6_H4Q]gOtx]}^ F $3kb0 tyDPqFd0+5JFSQUs *n\0SU3y|w74k@c\˖~}SIIU~RO5ո	QgKM)1B_
w`Vn|_ߔ5iY׀ Gw;C7kKsÃ<boFhr?!۞L0.PQf{>S(]O<X3 <3()
7h42kqw	#B^[fN:b8]
Ĩ".i3JDweX!ah	24^OG]o].xp-D@} '9x\qI۰cNQ0h0p4)q5X6LVـAqڤћ13E(E ꍯ8lF ,Aq?]>ͯ Bb*  ִtxga" 5 1W4vlOGxlQW/W҉NxwE-x{f҆,>K4eONzHpGM>&Kf7$%~z>)bM
<NkK<fL޷3TԧC[;`*1*,
\a N<Vή})8q5kgyZUVhD'9dl]ᆻ(<:SM'{'5unǄ"@R"6jqta}^|
q]]]8^>M''_y
<YWHq>FibX.V0v@Vs}Z]'Dfl7D;6٧ c iǔ+껣<`Wsw0c{	v?)|x R`/#[ulj2f|c+H"WJ5^X^^HYJlhWbxfͽrv8-gAA!y7g*|܁ٗW{^^֕/ڣmuj.BAȻ_JҠdPm8_-tPTSTNifIϷwA>UgvۃFaEdDeM1a`2:5_#|ޙY=CZ#a;_.Ñ+^ 
Ƈ1azAjRסqak)跪,˅]&ͼًۺ6kv!/ye[lنexCb(yӄº5ʞdY=_679Qtaւc%L҈($}+4&q_f&{>k E{SwBga]Ԇ_fDr"\ 
]6มO.f	f<pBL>38}?Mµje.2(Xyf>Yۂ;4!Y]Y;/^~_+}5
1D
YMpM8	m%Վ;s	AJf1NPG!J@r:'a:1tc׽-:y00״0>jw"te$Et\6LĳybhJǘV,Fp^JgNO|4 ĆY0D'r$RШ2.%:Rz4%1(3\K^ζ{'Nîบ ]>(9zP I[2Ymmݼw8ua4$'c퀏5]6ad=aŷq^n5^7Ob	o°{gXT[9l"M4xMm͢8x-qs8|Հ2l"@S	Hw;_LTahgMNrC. Fk9;eN"^l60CMf6)DZ?5>}3O#ޝ|Y
1V`(s0!Yl&2')ÒK .A4VDPB]#ztr~)	xPB-cQLa{x'L99ˁwe@W~?0,6܂$@xl71uAW삓PbSR[и`ZGؑlA'tWS9mRW~{7_ݧao< ;Sr1ozPXH7~<|&#w5:C:>KE#y `FP<4\>H2.'Q(db{,b.1`)l. %BRޞe'ǵO)|ᡏotPwhY}Y}b,4OQ	a	:Ϯ~>WP0)mMݟO$XJeA J=CwŹncX	',&g̯}	љ@)L%/LǤqmU_'?3Vt?@!A3]͒V&lD&[#%J >wudFp*:OYT>4Q*hȾ3_;Mд뿧膟f(El[[[øcd+;.ehe1*@L`c/YbmnM/FZ13Ľp%KP3B)&w9 ZHBIy!g^%h
?Df<WFFwD`
{ 2__}o>GCiZ>#կw)yM#T) DQwIY.~}fh1c#=L8(|&/`QW6b6}yΉ"ӏ)}04گnVAe -5܈!O\jZ#_V$<90qsX	Qbv
6xaN,Ȣ봾sH,)p(b^GDfWo*bE@G$qtfgє7#Cf,۩C{9)hĄ\!%Mhm?2\݉ha!3H"x?&5}޺$*ƂR!O|jУJP}Ӧ	V҃'4]PČr+hB;1D兗}E0W9|X~Swdv($(>X~s1o#xƌpj@
Ѓ:I>o [,49+Hho)"йmSFPLφW><'tɒƴ:y.mfBSd#/n:L[jހm/!<G?V2fd?፼3	E;C!Kp!^DgiΚ΀jVc-B`=$f6&Cz[=MYȞ^a̞d0 k0<])/ ~WmE/&FkO,AB"~zf۳7oO>;>==χ_ǲ-jyGS0Ft&t Ha`3@'QF-I:j v <9M:+r8$&8Ï|?1j+%뛱C`]4#~mhA,MN ڃȗo6j-9bLPqس͎N#H )w;lIXQ	7)dwނP+' y_95Ie}{o۫ւkg
:׳{+_&%HF+6]dcВOb/kRfT=||v"R\Eɗ<gχkF3xB웿/&xSmφV(=Dŭas;2D92/,b
@""oSJy0)BUxХ%RdVR#!,Kݿf~.#fzft{36a.jj^dO{1m5_~vwunQ½y{f	b)V7nfǝR7bm>_j2yI%WV:c @|ﻕ09sk+y.Da.
Ϟ~_;fֺwZ0Gh`:hjVv})F
!j.8`Ŀ3-KZ봨Ua`?zwVe383fj$ظrhZC[	 /E(Gc Cǣjb"Ă6*?$8H^6Keȅyk$'RL`y`\"7epn o;NE}c`6I/0EpVs5)N8CJ1A_~.`S$cU$0HF*I͒ә OohHսS`>Dk2n&s&j%<u<Eށ(90\*tתbR)YrCӪ"4Frpv,(]/K$J>}l@i!YMe\x0wHa^r#vd9U*@jR z+M8H%Kbܒ;%>i|;H@09.bO 1͡3KcTÑz+|:m&|`$ k2é8U]'9%m$ۓ]RYS4t{_A8p0s9B"JF7^Ş-٘g"6L_sx_vՌQ06|X}ՌvXs8dL㗱}>'Q%0PkUT:m6a=YP9EȀ[WRۣM exĖE@e5"JܐӡS^l(Ҿ[s\񺺺^`@$GHPX4,~I a	K!OYkB=mIm #p?cvAc#2X_Z;*' Dᰆ
C;bTԾ3EXwN5\B϶;$>,!;\]a}ZR_^QLceS m%>[XJ:'"`"l$nw
!E0.9a2<cn9,操Ŀa]!1`F&Fw䙙TY#;3}O*x8s^^/N5E5Vho;Q<1a=7Ct=F`Ep$ʎ&im'fds/·Zg4c"TRml*f64רX^r9=7unl7lJnF^7)YʓP:Ȓ~:mY)!Sh**cv Yq?j!@gTJO*q׉& ,	̅12ӮnMIa\0zt)_ s%CoC^-'cXVCJH=C 3 óy}O:L,na*HV?2xr*+ӳ9Jt8v>!ܕd匇cz:_WD(9Fb9TtN%' *_]>>>gWb\L3Q@5Ovt4L$[dEafGᵃx= b V)K*u'̐Cs	!mOj0f z$.uuN.5*FUq+47)Jf4_)unNp6 8=}ˠab襁Q#\T6"b\I7T69{PIG*H"A5%pxe%>75jQ\~;MAlgh$O6&SI<eUN>	kyYa#8c+ h!=Gz@x0r.~.fN(=u	eh*dVOS+9ʛ~}Is/T-B=cR\3#A(|bAf}=^ 0np]#3`~
Ehy(9ah.kKEILy,Go>noBx7;ͧRPzDKokRR%9~w2Y\jP}?ZAW7Z8@HZM?:{%Xb}ט"6{A,sn^3sA%-,
;EnJ6}W+1sJ;d2	ɨX]jkex6ys9!&SNC"~6_09scюe7iagEE~BK@4Lzn.+"$R]	uLԕ.{4efU@!PVݓ+81МfLh!|NM
Hh"d**
=f*Gx)\t06{2ZFVQz]#e\D#yrKgTxnpbb(J^2싵[|*5nZI*Fhh}dȖ蓄ʒ#qDDеTuA9ؤWG韫hF@x,p[`>R^!#F#iy<PM!(Ȫpp:Mס$1-W5[v=yƥĬAWHCu'ThdLU4`248^MЗ2ҕ{fl>tjk{GͣD:u*#ʼu:a}eג~L]+ˌ*Rwv3g\2p*_S[%a'X~$e`-"Aˤ
3v`HcqWHAf<,5q63BuE(V/NxYJVչbؗ״p53׻"G<e12$TԺ`9@1{N,Df^8[6!NOqk3?t_ù|[W_(r(knFb&!R?p+ax1zŔR=Xv(PX1*g_fc80Z`Da";~wD?UjЌj~r) ψttH/G +pLS
j\<:"l%hS]:fsB@J3_ߌG"HNјba)z(vB8P{s	RNGI,ܹhkRC=enMbym4+:xN5'ÖkƿF
1xC_(TsC9:Ija'VTX\"g豤j&u#j+2]_K>Ѿ=?mT+؁}Y00`p[uAg[{{elD!d!Oxz[@year)0rB=A})g(i<3iܴ:
O(![=>rhBq4kbF+?:GfMԌZE䛋QeA̹Rj!Iv4PlrN_'Fcs0"-rn\7{
^Vn-7rwcЗyLPP*ƾq(7V@!'<Ҫ
,G,ry*o>7੐E "QvF!ċQt>EQ39hB@[PնbVk:Hq 3HLۅ=XaՍDa:,Ǹa80u|(]ZBn&/gL[-ד	-cbΌ!r),%,+Eؙm`0
y;Uzdb}ֱCJFOBFX̑
3LT zmjF[+%vJ%g'JeeIrtCaO`ނМ{V0]9z]"x<iK [/D8~U1qÈ-` bZ$¤k/M:	{5Z2Ƶ@TGiC TO/$wLW0M4et'!< ҇%t8y(d6g8bЪl/M)h@Xn<~.!/بȮbЍl~aljbLΚq}݈|^⁢Tqϔ?"1b5R;Q+cMvjZP o0NRsI0;fCgRKӘKF)ʍ.v݄={
TJr|b],(p5mHlOι?"ZIP$-pYw~"'Ȕ4iѶLfqYkYTR}'W~j2p:/-tlʽE`77>3َ7ESb'U*ŀBQrʖ>
|n?DSa'^[jGɞ+rXu6|=CË/{ū+pxXo|7f øQpOEI}: c%'vb*#SJX{Q]r!%+ #
W r2o^	ƮbaŘ+Xg,uNg[+ GbUAݗo&\cAE_ZہB%":%1!O!ΟQw3XI2t`l6&᳅7,0%j5`8+EI;OoljԂo"h|ͲܕO<
+PML05f.`¬#.~I=UK55рFT܏s;}؛9	@O"Y>8(2i4;0
p=ninPjd®&-zHY)D[BJPͥ3p%q tlSYd!b܈WȽWURS{]*Op#?nq}( TϡU*";14jv+/`Ut1-	ng>`6ZZԫ}4sYN9ԥ	hMqjF{U{	Z!	8	~( sQp.Y+8Z\mv̳ss.eSÞ~AraZirHu#lq.1R8eKNk\p~¥1E3N
"saJDc՝(4q&47%'Ӛ&"![7r~D;;+DaAܗ	ݯb'Ȳa<Ռ'6)NԤ4*ZdQ*`F4JSMabgD[+&G|:L:@! 4X&E ˵LM3Fj]Q]<[3%^s9Jc;1v^:ӧӳ1܍4z:lc<	YV"΁tIY,o-C>R(.G	pHD
HsDˈQ^
[}ÿ~5q
tP-k@8<n2tݐ1vX	Z9!r +VQ%ᒶP}
X#aWR*Wp|&.cv+f8FE%(@.. )_PD(6GUCp8aS+RMüm-Tc救mL68KHb'y)l"YVƌUzL3ALuSѸT]CvS+AP{%!="uekyB(E
WK;y-ooG%w3Ҧ.#0ŶH%c{Bpj$o  @.=Y)l{lTӗ; E>{3܇ja(_!J,%V bQ1eʅex~UBtHx?q
"~°x%5E['+PW%y7x[~!`r	t!kZ+\I2op\aw4llPdz)ȧpdYL-Hu/| qUْsFlzY\B02DhRF9t}O	7a}8dױTz.:@oBwv<~htjc:"Lٯ7-#q!5,6Ex`YVt̥>5(4-~P,wȓ/'bvWI&&٥/(cM3}%F8`v~]zcW^٣d(yU>Fbu%n̸	 r\*U_$
ayRw~zԔ1	a| ZPȥgwZg G ߋfQӟ ArFӸ/[MpH/p5a:*ZB_~A֟rz/?_xC&=.|H#=n4cNjX_]Ha:TuUT90c|\uhPyYg {/<}p2t`i/_~^Тv?n9}{q&%γ.vBI_k-a`̳:}fT`rN,a(!-ig𩤂ePdϞ׈Dʮ7?f	r 2͢{<n/:P;E%KC7$ΫY UAJ B$=ɴTM1ʪ93q;p-ӇVm#.2u8{2{Nܺ])NA#7HiPC68<Vh}J¯g'+6,CI/*d{(3CVe;C/w~8}֑}EIٻVw|,.|&8G{c#>Ic_]"P[u20OmmFÍ1qɡ+/~4Sʱ+~	`Q	Jj{5a`"E\:	aշ-`zQ"Sy^q0ޠS;NbGCA+&2yWw\ټ~N\~G`aBOgrҥX;Z2,oT,D9u_9/bЪ(b'!)N5TD2yޑFh	s܈;q>^tvYy<z1=7fq{X{WN"e`f˕!V,҂g<}版ef>m!.O/8BU +jϿ˛Y'ZQjnnAWPd`s	mMp9vbGf^j|؆*jŝqH|hZ]|{aO_R-TZƐW#cN5]
mᆋ\s0ߘrhe(}!Ws54{ƶ,y~(wr<j]PH>ٸ9 \b
NGk)jȡ{ZgZ|z:vw꣆7h?yIs_-MJYIwP	{G?j
4V%XWa'ǔֵiiE@T*ӝksa7,.+R L$%k*JĭW86GBDHxMkx`B֝TR*6RMd*4}rNJZ'% mK<'kH;QՒ'1=k*>G\\t"ffiq,mo^5[Jw5%}?c^eƏ^Nr"/SS@kswradd)LHc\Pŧ0IO{Pv4wСS&sΠӡnH	qfF`Z/H4܀hZWiul5w	7'UC^Xר?У 	]Q5vRQ,M4"PBծ StE'&E-S6[
	*Do&q-`'|*kRѱ{#,T<mw&	IB*pɿH ulȋ#|IN3I+kuu+~1z 2
~$\9ٕPx2s|jqE!
CR/rnXCP]+2(7=zؖb8*yR4 	ֺߥ!UteP
E5ƯCld?D7Bc=aEk4nd0 SqM5RCZ3=0OPx-F%&DfBbhޗZDΫ$ OKˤ8:ULU[w׎ݽ_6\K#Xݟa6F߀sv'ܛί1[0ӭj2. UQYFAZ	|;pUcpn99܎p^<Ak"Nj)@ߘsІOlOA61牉/%i`f:bwGQVw~zqXwZAC;0@1cW9EK40⽨M`>Ÿ
st:>--:V>s)~3P7zf	s<`\{bt!Z xދFZrkPkM
̯!@7|fmL)}}U@BcL{pbB%DN郃nzB3Q{B(-רIj%TÝ12,%UãP b,mEe)O7wކ$7`6&teɬVT-Zlsbz`6=lB}­gf2wί=Bp-w;v\@nYx0Cc,8 (moǍtPGdZ?Q	;5_&( <X#j>8F^hLZul-MuZHnNL3f>1XK@7V˧c Vyra1 NPaȇacGkH 8?Q>?=|jQÚAdK6:@Cnh+3DᓰU𚱱 *TF^I5Kb e:0)DSh+ܞD	J--j#B8m0k#6`Ȃ\3wk/-`9G‥QcEŢ<q~a/T QSJLQe+k}`lFL+StI̕.<MDURT'BFQP;dt;Ac Z@l!9J@Ҳɢp	mp`u'`ix&M4  R| Zy*)aJȈ%ht>[@d0i1x\q9>WX/Vm[$],EyVk{k<}Åآ!̼g[O}t?ɭH=i?o]T3u}x	WS4on*]W?tH^UɻLj҇؁F}zHY??Yh7YN߸ClSp 7x=|7>Q3~#UB
:V8ZKl<oChNIn&Ezg P<8JoB-(sD;>d-CԼoy{58s!WV a;pȸ,e͊({X`R:{*yR`TF
J{^@xuqX^K=;C0&pC(_O&V-T$Qo ; 
_1@bN?)9sN;T1E?@qu15a$FqpZC 	 QLز92ߟ& 	և//D6`3M/VNı؇!b

Dƻ6T^AE7v
ŰZ`9!!0aI97J.ǁ^29Ƹ-i\KdūS	`J׈:E'uu"y,}6fκ-!-JslCaNsJ0hr@"1ؙ,Cq_ތfr:);grC(|3dr&Wjzـ8E/boIaE(G'̝M׾vu7')'@L%IcS9_:UӚ{7u5kLIk~*AύO_aʗ`dW,C+ERQOsLv,]Xv 2@cIAI-? L_5ғ[L+լpİh$%֐/OU0TD:ArJhx=AYlfEʋa:8C]KC̩@vz
;Gz7RL^XT=  ,
d!H/P"Eht0dzI`a{]
aٶ*R%.;Ql`BMJ'>) 5NT77Ƅ}3)ؘx9;{p"*\a6w-_J<<ݫQC׼IFht%G	~pXW%kȪ;@Jr.el#пawO_S̼׋.h=p-WDv,Q>_( E`$<}5ȵ5t&4 wGZ)Er4S&*` .($ql%DOU&([KLC&-Mw(_//JVEeiX/.|&TIQ%v^iw'KW3)q?WE%gzjBcV[a	Mh}DjI6	h\C̱KQd[s#`shx~vbUGX
e~ڈٗe,x]*$/eAS7DTI`	y;[B-* yG\ВAk2e	#!ȝ㸩AoY0!WӔNB\;
UXvƟjϭ	q"?af 0H"zu(/״%g֟{Ƽ5	G'vߨՓN 	fY5q~DZ<&9JXTM)0Fp1(`H=]ᢘXud;l%{tc5"óͮq끟B?y[|~8BU>ĠuxP^Uu 0RbMO\<oYKFo/^c
#4|-/ԓ]x"Tf
qmLp7SòIJYzFܾ٧7/c.|B'MxZvʷ8b@eP>#`7q&$UFm8kp\ aP">	e(2ӎ4&rĨ)Ydi_I;;!Z,<SL0k,ԈA}\S4\7Xk{Zɕ5VE))2p&Mb62)%8pw9խ@saL%߇'g@i#"t	o]*uMs갂H4<5ʦ?(64kF2 Ǽ<~ZIBϘn*32]ԙ gE/H+ܕtC9|υJw2u"1F0nqiTB#ŎcxknTBy&ύ
kTNkwHHQa8j;c+SQ:sKn5)$1f˖'|S¶K j.>)qDdWI5WMWW4,BkZ}<`trGo@
j]aYiqj+65u|<NrBV?g㈄6eM

N*TR,		Ӂ<gg A@	E͗Q{L}~?Dn(rciI*̅Y˕P]_#eV=X3'hqLm;(7dټ7bo.*iC1L,bJ(|YgfrFґZfFTt	Q0n1ӟ: /󟾣
l>lO\赭1is+zEh֒Gs _i<'8La@0X?L-!оf1d9Vx_h	ja2 l\BhuT#aJ23Kf1]#AyBKz>mn%'"JZcOz4`bd;0ZHaa|DEO̝AZ6$p 0rCUh
lqDLa?H_y<dCGŁF	ܚBgmHpV*ԽГ:կ<4m\oLwr/{!fHp2AC#|$
6YM$L3n*7,u8<v¹];.,GEwFW)-=;)6iD|ͫ\	+muƗ&X+}ī|¿Z&M&
Ѭ`4V1(1}͆NPf0!6Bbـ¯)UVrzӌ^^Lqvz J1и|Wh~Vꨦn*p( T
8^G'gwWOpGOHښ_xv{לs8_TM77郚TPe=髛vEђp47Qu-.'_<:RW0].G	a1TɞC;j>k(sJAJr8B\R3LOh&炉a|tƟz߁elz%CtRGOp|y	Hh;gtIua!s#/!MP.QIBjL֩v0Ď:tvG<|!1D1LU1e7co<6oO㉀b0稒Cʀ<r1
<zUVd 
h{JpI!ml#Ep
E;Zw׀iضxs>E{>Cꨃzt<>>䩣嫽Cec	n,'I .#!Ԁn	o%GUr{v/zr9K֮~iaDf;E'3r%5"Q4bq=<syYrM,WG5#"48ks%E=&4.Q2HК
fȂZb pﰳO	31 	|[޷C(NmΡ8Zd?au^7$-T{W] sVjpk6/SnWI41ũT:EbaA'F ꉭS>*AerlQ0q)
pTx!N0T,~<n#ji!xO9gExu?a|	_5#O&2r锋
5._X[ܩHӻM	>$}="Gy2G|11
 d8i3>gCjQC0u#͢0rQnvdĄ[\>+1?@ոRi5!K:<fq.y8mG탑l|;ʖ&>'z0֨TZxF&ja$@:XG0F.0Jk&yZ͛pϞI
z7Ɋ%4%)D)sr[L#qWC4ߔuS{-a/]92dpsnEB+0[58!Xp
ϏJoQ S`ն;]뗿hcK_#cLB[^e0&(Hpq '>qg.a (OI$TM_&%Po"[ƚ,j4	:\f'*;
k+xh6ˇ/8AP9,hk	wVբ<+ޖC-`N`M#?eF nG87cj+B66j*oT)W`_P1 `5	WHo<pV+KiB}	nxn󣤷lEg4cuےXԶ;4:%[.VnXE_%
]<!ԈK{i//+	kB)Y4+xg_[ (tg.J)]ٌʽtA	rsLү$=@9T074]Z[}C+4e0IEWvotZ9tbE肁	orв2BkGh!!cG8aL	_u{lL'}t<7)="DMj7ժt*"pbp!{Ovy
Wf;uutnQi3oiCn<XE2k(t /DMT;C+𲒗ƠWY?0W3@mBl
rJ
0x	OOo~D}G`<"*-b,aa>""~4B{k1h:fEwi[ލض>Ā6&cAW|BS,`62$2̠s$>̀z:UT}bA|zM[#>0"p5SWkNv\ASf[zY=ʺcBE,5@N.#dCW5kNdn0ACP=y\'>苋 ̳ЊHrH2T؈>߰Dό+R|<?}[UcAhfMv%/2j*l{{=?4ԣGQtF2v$ -GX@8{*aҌ";Vf6pSNlJCjAy3dEP,,ƆD)9lJ!g}@;'C#=):4#S4%*ҽ0yݶx;ďh-D6U!̌Rwy>N
+sh~RK6i[L}gwep{?t[ 9(qf.hŝgvXcXAd1Hbx#P`sYM;'";FFSidx-@(uY/ôrL[1Ո*Ž8Q/<(DM8yB2`M$:'uSoH8zX Ov,?4}-teܷv75Z=%Wp+#	Ð&fiU$TҼCsvƁʧ5;"̱5 2ֆ\FY1H5	AuI*=R1Q
.`sDXϐF%Y!x b綝0PD_}WҌâγ[/0+sG(tJN9) f;YܮPr #,eWAN pjy2BGX@
IH{rM5٦fD'+XQiƎ.V `b¢D&y朌ݝдQE*Ձmv[?+uHCD:[ôJui2&U9)87# -p}R{
,$ʈ2Ogw蔂C8Z _~&fO՗2JdGL^8Ldk0t `T_e`X3b(oT<L:bwɟ"5O|G罫U7hJmy()pR%`@%$͓H\)SQv+-&0IvENhjjX&
 *Qu3p5'PQ`ƱP1@PQaVߑꪌT$ө<~_u$UIkQԘF;_^]7jkyU)DQq=0W39l+m(IcTItiIՊ.*[_Çe=ulIR'_L3:"I>bqd AN;_!nR͓J;+I]X.	Х	ZK$"¾4ZT)cjElO@X8%C{;/_<+D#/	{I4%ۋŊݡ<(WRd({bw62ͬls8'@HP<9ݫfOGo"/J+pPwb͠.pѴUtRby.u#071^fA939f4͗6tE` ͂swWò)Ni{u"wHu5$xT' }L4YLęl5SJvӧ1|*~EGm+BTI,n@l "3Qg5ũ[{eC u}P!$5VWEKs<k+D72/L1Dz:>9r7H699|3Wk0mgKirf lEcF3EtDj<w؜!((Lu5Ϲ6t@SKk.Js`FXZd4kPV<;!gf!
C'+vbn7X
wdEqOSRk.|4 H=.Mc(|vd+f9n	̭y4_o MTj
$	^~R(?3ۏ8ZZWdYN4%юEO"+L͸`_EŒĐ>Ɵ2j:'><c"ASCgUw=nôÇB
ㆱÚj7ѣ,i>!3p"Zq|Д۪C`䥡«`ƨ&?K KL8HRfAY*ZY8nŭ)u<%'=./	cd0WߒD(M>U8H\OR3525d:Ʌx(Gg#0I
3I຃?cHh ,FƨN	笒BA=dK}̡ ʇÃi~U㟄5!arfz/ >bXSDpUi}
˭8N.ž0¿9+<<(xc1<2KxY+VfLPc&׏K<_J5B϶iNK\H8_P=;sxIbY
1NEKj<$B+aյoS&*\~
;%iYFNa*2lj|ga4|Aa>1( Wb-b
FNjOT3L/|͢F -TA*u,}&&eQytՋWfFeղ^n䲈WW +#EOMT2cvwER0a٠q*|O*)NWe|Tb(E5*תy\JU|ќT==@>deA4
U?q/T.x`VGp5SG(fiQg"E&f[Eh,̯b+-0teOK,|2<:,M-|H5YM͋[*xWpѨjX6ITQR2:(ՂlKYbMM{@Nu|e=-;k/FϤ}[X2MЀb#h(5Gt0h*Zp #Qz좜ҫ.Z'%4=aԗP7i8$ʭwjf"-^20qHa!M8xBOj}THI.ϖKpip-DIW<RO':b*YsX[
OEtCб9}fIrJQ2YJs(ɲO~fzh
yaH-/rt.jqpֆ>Ab1
8D:dKJh>ʈky輣<ɵS
%T.zO: c#)3*Z!#X@o-Z"?riB#ҬiOB<+ɎZ/ـ;<GC&c\⩜	'*F
3]7.!Ni9kfw#ZꁹTvRRp-/Z4^>43 a(XH!K4`;[g2"MֱfWO.jd=ipO3$B>՜uM%Ρfmv(Mvq_q[t\?A?,!2,tL1LYX
AY&chQ -TMoS\u	}#@kO#4%
)5:_C;g DLۂTLƕJnۚWͲ&{#s"w}Mڠuλ<7RߘO+zNv[]kAs^^MU$]5^ak3D5kܷC|wE4mܒWk79Q1j13q9ϑgZtXoޱР%3nŶa_2!#"@/N_&[30`>#$"DeV5*=1y,ÀP63Zܮ(ff(?Q4t(m2
y;^sZjP,MlcJ2F	s|Z2]@ʩbHiJT͇bqW܊8E,YrL˴!\wvgAyqSJYmU*s&
CUux]']wװ @[GZDai6lZx'z{UWMH)@|6p
a@K-D)N2瘌Nn^eh7gP$~՗-VJLqsu2yMVԊ1#1	K&/Iўg?çI6ғjb~@LA>_/[ƐXOEx̾Q0{`n:2? rNlް`=Ȍ3b:*bBX$C|%CB[0h
6nSEn*ѹ#ymR|hL1.fL I.$Q.]e	pPV,V"C&iko'-֋c
Cr K]"%
w)Z0Ƈ1|ɋړ8[Rtk{C,R)icrjLUw-GM聿4^T|e߿"Cܴjă?ᑉΓVFz^N|;l2@XhHMU@?$no"F5
2@E[2\k󢮻 &.+T`fj<Cpk_
'"ZNY/=+Á}`U|:s=Ǹ+vh)b)7+hHg~U$NRiV>K3 ?S*#\L2a]ڔ6ip߰Փ5L*VcF#|̓nNWa y48
GZ	]U.Cjj~Lc-:rz<b'@oi~EdaNr=DM$52.s=8Z&C-<i AG©C7&Hrv7Φ֕i+sɫ*$̔_lg7^]Q14_OAga
_?&!OI:fYoa7?s)O	Xeɒ$Pj䟑fY9{8@'	JC|'Ŷu	)R8@T3r3sWoODPҹ |\^H0y!ң얊dDHj"%Ȋge[Z-HLEhh#	Ϊ9n_^O`8a0hp-zNyz]o|kG l_O)kjU[]WM؟n9"׃CBBT4S$k[X"	;DZ |z`[}hD;諞K:L` `v,oaj&] lp@,~׏D%,[QAN#o0C<Ќ:<F}&g	NsB#<	`8?[9)p# ^=S
{U<s
ң|0IHy)"Qް}m+ZĀ/AG"Zg4j!%W~	:r\||LAuZ͂r?Qbѻ}ܔ#ruUN d~H8";Jn Wg%&+>.'⳩24H4hCEq>,<N$&BEma'ZҞ%]\Pe?A~@B=bf,%Ac*Ă~ݴsGF1&ؘtH~AsvxjfһD Hu+G |һp?[hY.z}Ti6lЉe<t	GkooοwFW@lmD:Qͨ*9r:/D.b/*^0@гXd~LL
UL
#,..RTG& Q\ciey$!'v;=%nJz$!dS񛤚}No/nnri?28A`ALÙxJT*$CUTʤ	eNpYkmKAJӒӐͩS)ٸGh儏5/M -5FlP7yЀ)btʡP_cW?`z.F^S=s@Ʌ- s1]{XE- Ct_jd0Hk'-Tyo	@d08I>eF?"sאnr8]luYtѽO&I΀ܔqUZdt넨p5GV:\Nʦ@*~yC*3[\-(c{Ds*z5ZJ𶁴0좤,oIkdBGHQ5'藨4BjzhiNU
T7E.Dg1f82Q>'sRA  	˽~^~L	 )yuX2ήk$rz
v+U2EiݻI>%ВCtI;4<>ea]M{h6tʚf3F\%TTU.PRnaH4S֍E:7rr=,`0Ld_ԕ+Te']y;>7,DM&}b,T{!*: ()v`#+<l.UQg*OZ{kU1 -V!"X´K%(p/b
Wc-kDm6&¶Vs)'TaW$}ydty)^^_B/tùlC*K4[DkTYA9Hdn_x}H=.C|vbm9_["@% ;XgWC'ܧ<$N!MdmJA61]$etB %>.\>i.h6;~z;7ZTI<TYނ
/6YaL7ïK`aU~ m2A/V`6W#]&{JRPr3UZW$cNfuiϟZ霧~l׺6'k|ws,rcǪ+u¡Z3`TA&\|+U:kNQKht63!&M"*fVO3IFL*4Xzٰ2ER9,ağ8zq,i(Rc@	
Yv?K,Nxe5[Ɣ_Op_ϸ}|XGXVO)|Ye+2U{F
29;{%Ú!)oI"եz=u1JFBh({($MN{3^q
Z/f\obg;{:c+7QG^1p,bIDzxcE
=
|@إF@uX
Y6QBxfk^9;џ`dz3l<S~N_n?6[H(xb D"`z=<75d="p8}2M=قtO
u[=h|g):cO!@ (I0 'U@`6X0lˈ=G9NiW.V`:8~? m~M_$^Hw#̀&ͳa>yjmڴT@vQI6B@FqhiG{6UrZӬ۪fa
]Z oãW#1[\2&ϰDo$"t"lS0Ϛw/vtFb'8B&<0wyrzۊHqDY$j@A~RJ`Kc	mEãGgGGe_%2!<9r	",3W/qRqVNv:	ǚYwիI2ٛϘ(sT!I>fY/l/Ǧ_#a2~E&'V F,HebmZTYa:l'k˩6)tǰ6 ;cB\pK-^DJ[08	4L	N)f	IfHo)?Sljc6LU(T\6* 6ӓKd/~/QҒ>-F[Vav(,ژHka^Km̙%J8X4_S
9FQ.cIܿ{SK!OT<Pis.p΀P[0|带o^7 kuXmg`XY-+㼎%J,Prmέå#C,	_kz*>`Ru!b.")D'évX x_	RP9A5+)E/oɿOskW4Z w{,"R-	o\-AW7z3DI|ZF)!b:1GS@p+ۍM\g9M
꟏k?~93N,p_ϹTOEh[KTt$NzQ{9]{ڴC	A0B\vO*LvG\'$*B)MOzTUŢ0,C.@<;jaje#Kx?J_
	MU J_au4"Wa|*!sRW!XKA7!_)>#-mܭ;*G*2p9^;I&^݌gx-%3e57a`!@3%D{睎V8&hpz`##Q/RE<(aj)'MphB b命=ejc5gDbS
6iWLy#-;{jAkk2ԵkW=o!<%ȏ0c@Ųr`s,2䊘V4Joͅ65^Na䞖(py!;>h0xاRX D}`/&NHK."\S	|n0AZR4'\;ͳy[̜=y]M΁i:a9SLFwe1pp3"7Yyqg\FPV
QHh<pI(>>J+B
zG<Z#mg1#.ޛPxl#nE19WɓKՃOǬ&/Hvam8(RJ6v=8a(+q# HtYPwHLMHN!xn[D@y}YM`&룚.5HVl%+N/{H.Dr95'mJ  ]sTG	c7ⷸrIn"ؐF=f*UU@at9̢`JK,kp~#d4֏x
F՝߫]9~f/V`ܭWQp7u*|uw6.*u$YPknܯf/?Z6U&enU7_w6$@·w~9oMvǔ:1y_**b*)]1Z`L&Qb#&p7(vgS5D!LQ۱w]S@WaౄwZaEfwz@5<-@8aJR'.lgO	Thv'MRj?:}z]ដ7j0QtY(G"$,ln[KrhZ࿎eޠ_'Y6*?`^vux3+jiZ}8
Ǡ!eD<Ϛ0$+5-mU#_0|N87WEM17U(Z*7<=!,^=i{ӿAOs:a;QCfF1eGY#C~ks2FڮbW	 ~]3т:d{iQ h.oGCZ$D:V#!;f5P 83%0S2؆dRK[0,}#q̌bd'
N̘`TE3YYq݆si#҈y#(33ɹy(-߫#Gݑ&ݲK	JܧK4ӓ݋u2)1wQvd"X)~$&^N^[-c>&q.&Io]FޅlT FsM,DC雮-}۵؟hrϵ.Q9:MUPVÄWҿ}н9+ŋKWh6dxXOT90.Rć&%=իRWklS]&/%jb;S(u`Sp2bOLIδv+7r&	s<0Jk/1ŤyWjKS|PC%jG#\p+m6m<,%q]/dǐP!Wi+buX{yv*{¨p('1%~`6VpsEiLU=`2d,ڈ9Dj	q,)/R^Fio|
PޠPG}N*ud CI|NRjۙMhbeCް̗S&%NbJQ#!ykW|xw/ zs3b#b.LeHkgwqat\'HmMA4"Z`l~h%a5P6\smǒ<KBzXGAuXf5b6\ށHH)ųBXaL-"68~YPODC=m.pjlVS;%BiXYsEvVH3;+|C$,m%$XlW0NIǗoTJZ<=؈	"WUCjXuESng[#̱~9KH0F
_N;lޫEIi)B0vN*%MJl,	J[gDDfYl~u2? fwI"e>,rwb0Fx*X\,Dʜ"xMť(tp\U*-E({FYsFgQn3n|*hgIYxF["_K#)-(}QDFjYCeyaj:pKzߒu	ÆEADEiޠe"Sv'sJL%Jۂ#Ny׻ uWqjIf@~KjKbLqx\MA9W[p:pӬp,K! aHR;51G9F
,QdIUJAeV弼<-έD5d5p@
תn 8Vųgp7)$9\~޹'%gmlܥ;&]H*)o;1IbZ3ыZ}8Mj
g )NbTnʷP7|:$_ъsⅰi)+C m+X(۱;襜6l< QѹFv3J9eMS)ọ[$('y#P͙$Ui`"&gJ㢹AFcX+xmJLOx:)z;ZfZU1P@|7azIY/\/1 o3PuJWHL;͂/UT|>2<!T[]6Q$HULru1pk:-%& Iю])u:bB8,?л.T^¼%Cj5vSf &믊o24a~ſxD}m('rAU
ؕN8sVIbNT`C\t1tS01mى}$qNhFh'!@xA9ײ哪763U^x"+tBeG^'`*G
)`xtCw zD@&BU<X^/ ˿?<	GП!r TWxyi)KZc|J$64xʹ#i,1KX,R$!}҇ļeJX q}&LZ̵M3t,ߍ.Q2|0>EKda6:@,*LyfvA#)mowOo"6m>]]׽q0mWڰ7Lb2\`^W:<q׭a)W/g^aZIL
,1J&`
_%nvuzź,]G7}v%pV3gS/UD_S}'V;~K@jU
]_ŹTsڄ&}?D tlWYa]O8ՕTo9UX"1*c$6:gx_;vCkO4l#S06h {G56߇ewNp-x>ހ8ԢfL),Yʴ̊'5t{1(!T_	>`d<)ŗ\%Ӆv#5Sp?жK?y,fЀFf9L̜QFl$0vLnHdw]Fq8Hl'rӦ`Q?B@Wć.>pX3t䒦ŏGbFTN
K9uw&֙J7ҞĨ՟0
	jMtem,>]+)Ya+&Iup틁VHJμϝ%aS/2es+ZU*Ōba#^0>,":œ.ޘb9?J!"[,V( ^[q0yvLBM#,vU8oݺ0큃<:<Ij?=7Vw:?nH/FpVJT!1\ڬRn	JL)0Qgt)[ӰHD^^B)eDL3)=+9TEy*j놓2^x .e^G.ͬhq(xe6dD;Dc77F':x
(#ꊄR#5mAuJr!O0!ߓwi0EL3u.eɳA$J X|F0WW;${3nڕiB}'d R01m\) &Yf#"9H.P]p,fjk^?669a݈mm3.T7=Bd>}9g)<ĔA	h|PI^ ! P3iWnao$xi k=%HeQ\5UׄUm,S4Q:U!\֬l8*<kWo>\m5Rv*$Yj,ށ0)/+"0HECwB _Z(gיT
v?Y_?jThla#(ɉN<jW$`IH=vRv^(4Dԥ;x&
$=4C;l-8[FLZ6/IM0	 7BZ-SLjR%ŵN@ǲSHK7;{}|Fs4D1P 8m8W:U{zH3`i\i}|nVKe1	mc0O;ngV3B#֋TpV/S#3wErECV1[ ;1+q+j; Enl,B#4ޠ&j{GUZ!ٟOs̐(ˉⲰ1һ8(PVTia9vm3xK5MdҰRTXO0bl4[M׶잵qE1糷lA8;ٕ3@-	a"bS!Ry.g'r,ϱ7p|0'ncwU!Dz"ZkE"a|6^c-ko}"T~9"P@7rl%NvMհ(%PcOrY>Z+Y\LX^-P/%b<
3AH]S{ 㰸pҿFeA8P`#Ŭ!ԠbeM-ƃ?q3<H؇֋i i>3nlT7>̐Q 1IhsSBeՂ9,knǋc%VE;K=[FM}0h_!rS;QRpn`jZ~#b0|0J?IAW_I9E9E!Eb] )ָo6rI'Lkm)nc[n*4LdhP$Jd3AQqs'6G[~)1KzQطDa<E}m-UmGKVMA6A=ڙ:o哏PB,\xVz^-T7k~) _$~70`+0kJᒞJ{@ФY~jtni4"/Wxb尻x	Ad>\9X.4q
J_95$Odt4|v*1CPRBڧ*5Z^/4S	?|8hVJڢc)n
.#w<E!Ce)4gbuFu:% .#C3iї,4?I5BFZ%Iz.:5_ 3-lj|:gѧ`6H*f&$M+J+|f*?BpA<DLIGڵa|L`KQFmpuT$fcUq<)mova}H(MgꌌPTk$G[<z%3,o|4/[QQo͊LiWuc8")DqG%K:X=AP,cѦcm>Au%Ӈ/0ч \Td 
2HE3ܛ0R._kF.LXYj%XN+We䐅D2І=P؜gv&u P f) '@)lN{֐#a%n}0r]oxqZz'I -<ahN#l)Ly4QLd!vĉJ#emSa/f~p6>"]0qJZ'B^Ya(ϰ bZt]0O\IB.貂i2Cv\|ɟ\ۇ0(1Ys~bT(2ܝ!Dx%b㋘	mflAb!ַngYfo@)z6-HJ>ৈą %nnj5EIϞE\7u1-9|OV{\PNq"@wt59M`;MYgG.}Cpf樿`h7S4f4O^dX;(g`n$(4WyjJy9,%v)F%p5n/h	s<
96(
;좎ESX58؈|B=Cwq+[ +I`zSc\ѓ~w-*lJ:$9Z!9 (9U5	'6gB]	p(rzbD98H\;,G6`Sj<4y?)1ԨK:6.ݔ؎$،b(^pk1}󵹠, LD(~=a6Ĕ|XҎQnݓeڼz3is|`_N]7P\COVߊ]]*.;"+kEe.4F5$V#|}}p.p~C-L+O`oOI\</af|0MJEhU-nD%R|)_FQt;NS`T3]u`	HH<ݱ%+ra,Ƥ!bXNBy]N3;Ouad._mg[ZX{QUn s(Ni-6TϩKv7d* XPיIa;ذڿ#Lq,vS][x}5>1M{a<Y@Bu6Kwsk\-LUxSGƐ؂T[>Z|FFC?,$N[|8%h4r5HS
>q9Ŝ:R|PFL%MKRc$aM Y
O1DE2_H
1 juW _J"L1(%	i@aɿ)uMX#u!;cKQZZyR~HE1qSbj_f?0E!PsK?/.i-Gy$_:6]ŉ> XƗa<NaLoŮK*YXK$4oNDz|J>Ѣ	~΁bp@KsÍ[r 1A6#^hEbDNm)l;%
7psfTrJygX.K
X]vefXMN3p&b*n)KhI{;6WNyͬL'cpeM(vQ$<s}05UT{h`/oh5B>+)L*èYbGaɲO a/SfSL+@_"ǣ,Q<h]-JOJZ-RZtZbo{
i6 CH?p>af`wNW=Ťɺ3,0j)Յ_; @Z@Aa p%V dX<a,I.a-Bz@iz-;VÈh6><r~xݶ?(B_.-rL@_i-tI={7H+-J*}Y?8;}-vS,.w%H4}'Q)ãbX^t|`}5EPb(z߃q0#K+LNA&X7Zi :l3,A
RQS=B3hhI92
a:iRwҋ˗vq(*ΊFMSc يvf@\{	f{jI _bycR~Ә./DY
[H$w*8"j!ȅڂ"{b{N`f_fX"BΑw'J!	\}vxN &-_\tIiK*j.Bz4c) Bx#u$Xh).u,~SjRf.pM4C\$y.ą?sBNkü-)K݄偑i{JVGp{pP1O\˂A;zV"lK%Ci6?\烂L5\/	00)%hlߩ]-k#9d	mu9d~QsQs)A S8"IYs<<!kJGK|1Y՞Byk$r,Yc2D̹Xy'c= {A:cizk&3mrXmI{sΫ(6GqB	϶wy[%Nz
0څ9J9f7,"q٠X8^#SmA3H:g(xB1ѐgGۃW|U2֣¨Ege Dè\9/$7jJUzt{݇xeBt[sgTs@|{OP*>g!b]/NϷTc&SrGN؄_ُO	]6R@)ӣ)E]/f9ݖýQTKB>k"
x!{7z݁!Ǘ][,<Z\N-kJ<=KXُ4
N2BaCNXJC,syUd0D~.9h{w{<[xUauB(
`̾ba%98{<M-Xu3ga
G{2GY]){w$wKd=a^"L )NB&?⒖	EYpk6gdvI@bxW3WW,Nٔ^3Om:b}PESXЅ*ftMqDQYex[guzy;H놳cuxDu [寥*܎a+ʠ(EC\_K'kM9E,	SJ:3I!pLBk0DqFd/ק1sIbxiwAQ.M9͢K67Nmژ cWrBF0)`hneP^M(/bBQ[j%wjKsߚdKH]WQ|sBKŗ-Pmnpn06(5 XDu	=<K26kWs@SBB@:g2/р6.=
Pl7$;OW^`vY"&4Qgª	ޖa^̼@QsDg&$'2a9R&hkEV-7:83^|/<3|#*ƨP#LOWR2	3rm^Zss^HeDOrSiR_q=9*n8f4.NsG{#hMwIRy҇sgv,$]'g&㡉a3_:0$I|k;-/KEVoڪco) 	nD:7v0ɟܜMU7OXpy*#BQaF6B܇E`+E,n,7a	$A=4rWV/g	.0M!"8@1%&&J݀7_XOUl&$%*vOV頝N9z7zb#^{df pm/obus/HV7Y{]PodCvELB/9InyE[v^1E h4nsǜJ}	iO:Y"L<-5ǵc.͊U/ =3Lv,M:<G[B:rb9od°~(E`zD,0uk]tAQ^ߛϾ
@'I@&C1(MFxmB&2=MaoxjtNMH9%صfW/
`.ltnFCCJkEU3ezS[/}cK90Xc$I,t]3=ͨ2
Ea$ųPg>Cnb{[n_8}jaU<[xv鯱 ఄ:A!RPnV᲍E=_gefe&=lpj)1FfHNmp!g&/yPbQZRmG)U[\%:a@b*\Y
~_!F~QK3tI~׍?m qeYLyҿ;>=|xwrޛΎޝ?;:`u:u+-5Om{BH!pF<kYu_ei5W0-Yˠ};@ikӪ*h#DMq5=}M#^jd+ZԼACu!RېhaOcnD%HPQwFiq[$iܥT[1RFXZq+TqXhpcTx-8H7s=&3ޚ#zQ%3RPNM1G**>&jYk2JOSU0W@ؗ-\_sI3Fln.|7u9-V3Wt
<=0p%0VpbK,c:D-@	PUk0Lí¾âTJŢ)RFj] HTâcAARe|@$6^+xZϮ<KWzQVFy ̔7)8	k`U9g6F֮ŘHS\&AP(j@ `,4Ei0S#:no¾NJB{I?1qRT.K4Qo)EdJ4e]01y8ũ)V}}G0o,"^zdcEIAwRݷ0!8[0|a{`)y9+d@bȗeWFou.%GQ6>.?d1xez5pGȖQIFl2U95An܂^h]#U˰'rYaF#j9_y(UB+TU ]V3̣q%,7'^i>Yel-}xtgg?}<:ø֥9	:̖uE	zgm2k5nRizI]/jyUrrUg?J'l`LzҊUN b"jjmzeYb&j|߽h..3䲨(cUkoixnR
D"v/T:w}(iwULTiC	ų+	8$j"7\7p*zCS9^a}d-\$8S/b Q6g
SZo)@D鍅?1(Kuo-kx3AS̈!lf ՗z?\q_00K#^Z}#nX uuuGge`,h+P0QW@x>V`	ΐIN<_Upjw=)N&^$
eB]q6\@vl~{+VPجE<xE
Tcc*.H!4Pa8%Q4u{U,FQ4ڔ HDDuX2
ђ4rMT>cI  8S	uG~#2ey
eDdBFT()ɧ&^K-_l]Yw=ډtK%R6C4*_i\}raT3G$PF=΃t[hSTS?R"	'OZG\f';h4	.vAyւ.&E ~+:99F'Τ0Jڔl	Z˛\ߢ$3KM|IHFf@ǲbsiK0^&$7rS(G9QT(Wck|6'%:JʦW[>0-v?pO=}t+KᲩ<JkAQYԦeDJFLˇdEI--a->̀  y :eI9-Mk}"7aƽ6/+UѢqS@ٶ7^ٝ;6I'k\_W˰}{1:;h,tR	ݸqJ~:"A3dw1Em?3cyjfa6NՎxբЧ[u9,mx\Lq	9)uդ{#xT/-u fQգ^y*,/%eux-^/W2	4IT2kt34SָdR. yvswZYΙPxAHti"I_ۂ}̱Mݙ~di3Rп;gde.c!Q˺*(exFU.$FylnmrZg^쓥전b>Fez0L'.XzJRPLt8C1Jh]B\EK\]rD_k3wjY!l+u;qcҸZ*.^?2؇LKs]Uibp><LD,W7kHa[?3]fs@׊Ą=Qo*j-#fM ĸl|e>Yź谳 " seǠn{frtnI5KI^RHPP|	'9S.Ic]x)30|^F%J[Et1FNK2px*GJxwRɀḈ#@2݄%<N33Vd0!T; U)tɳkj2'hJsQfDs`N-I+/RFN.r00Kz_I5,1kY"S')|DZ>|y*3t\pLHb->|#0*&Yd}ů'1$61ϴl#	taѱE?>^.OǼ)%^$ڕ$!^쮺w^3u(N8}twC
YVh$d,ݮ7~#5'd̞c0Xh(g".XP!a&SqPQ;$:Q8g30⌨(6}o^c}+6ǜLev'+Ҭxѷ%
m"n.>rՌ/΃130%E̙Fy.V)`IeSt9o2+jioTH[Z3坪nhUi>ҥ
ZIhʱ0ͲoJT}K1ODq9"] noNTxRzREpE2iwTU#V:h8ia#muabcPM&F2.s"
{]5sll;w{L9?ow"ñ:q&&]>y&v]8;/j'`}Fa!}vŋ]w|TJ3/{8ӗ)`4=qYSl
db$k
8Pֹ%	qV(QMH3d'wl'XݚrntҭZY*N_4rUw$&Gس+D~;0WuTfFQo=#}{*kG *@I$:JBhd:k(_nnNh\i]˂_=.`nR	{UįsNG0"PMQD?SЄ&,RT}\w	F@/,̌`n`(|8M1!Ti?C.8{ϒf̌&z
%4%МS{pN;0a_ëWSF(ʕRe@z=3sF]`:GZwxF\)4jz_l
l?	v
&#I'G%ݛ'4 nP5=K=>rvaVy]eՃnxA 8g(2Т}D#s!2s5a]I6SJI脤7%H$uk	d!fpqAQ)[+bRF@M}%8P3	9scC)Ľjx@Îe(Ąa$j"N);q;#ՆO0.pra{.;ڢY	0&T`(yXvn!?Er#ݎdNcRz%*mΟ
":Ǔm(@M ~#N&h8L.8 zJ
)Tkp3T"JvwyxxoPvvh>XILmpq-ڛv,5rZNX#NbpvMXp*!J+A^[Ţ"L60O 27ǝ"1!xNeͭd^ VQ).{G&~qeеez(1s$Feلqaia5P(_2V,w>?(&oQLA:	2[恒f>4FucKo	2עguVk-k^сf'ۉO*A%i <ZGVlu[MBJ*3 +MifU<t
.t̻yVdOq3+)0N7k^~Vrs*?ⴣ(:"&x-)Q ~ݴ1,ި09sAMTtG SaA2\DE5l Z":cE;A_#;&o;C@8xd:e'cćXԍU2-+f݃&BURiY; ~\pz{cӄhHxNV5Ţq/~1X>8&-XB}ͮ!9_7an??-=BUz3U=PnF"EntW<)1
ƺ5.dW%_ n||{#O:q/
ZdH[s<4da1׭3i_GV-@&GBQnCi;ODVpl	MV+='PF	t4̥K6N/a[vbNQC̮!ژ<9'bj_]=B)vmrם`CJtek~='qIj 'ՠj2X
fgz$ae%;7ǃOBÿЭ=@ zp{R\|7PW	h*,>Spl{]D&,K;NRp57`Kfu|:q=/W(Y_4b.TZ5&*ddnj'eK9B`.b\@H+xK[ǋK-6Jω)$X*KZ{4=JEp_S%10Sd}Rb{@PQ.y,C;R [5j,#"1`-tdݽNUM5`G(Nqyl+H![(	QVcLݫYwQd@'c
>VĒ,CсbEdFTLRa)ѽF$ PA!~Jfb-EbjrGDՒOZPX_IDy,P=!PՈjR<9`RړVK؞Ӭ:\=prlDMKŚid)HQp8>`#Ą]흾"P«F<oF 82V`bQ*WB".~bD_ɩdc^+zS.6#XM͠(fqqљi	2
ˌ[,lXԛpP@zX3I$2ʡlмOړdС܇R0]r*{5+1d$cIl@§!RYHpN=e=6 ~jpBk:S%Bs3wNԼIjU7Q$o#iQP3FjhP	C&_LD2ZS%\,aO|)X/M=zȫ!!90WY3V5]&E>Nuӹ)lLr<k-ˤ@Q([8[M*>k(lʑE:*K1pAvunZ[;y2AKt˘úJ yvbh0R̅b7XjU9dWc+hS@vףi;@kpIц>a?HshLԎɌPg;q7E'=x<*#PE8 N[BF45'.\D}FFL]uRZ
o<K12Ċ*v1j=R;,5$7ʘ([]}@ S~fW{sKaY"nW)0bAh7ќ}ƉD:SvMa6Ƽƌ!DulX֢ @5J-Yi'^}`flp{q)}U6 Xy=oA%a{&qOpo$20%D[	#-Dh0V؉{T82X:H
&&(Y]&֕z"1 !B3⢎&냅y2\0QʐƢ^Tq-PJyBI'J4hҨq#~(JplwG? _0q^Bj2%dUW`z'B8FR+XMHtWb&_'v(X:(ݜ*x.ޙ%!kllY4$sDp)_$iUh}$#||@>!CKGa(X`Re,l$ X4m?wqRM_:!:vbNfAg=LfyUuwy@niܵLxɊj'̔Kc+BH~A),2񪙂ShJm8Ck[VٷˌרBehwq펶e,Ehd1|Z?e:f"u~y1<o\ՀN7~!L./Cؚ1sc
~MU,֕z(0kg)nN
SЎvcSQ)!ygi=59s֘7Z?vF&3wOy)CFδ~%O;|"ds6m`9̹*lI܅
,[;)Mۯ8ÛGIR{]?(Ԥ-56hycK"b_?dޢȕ׀noFrZZ N,{JjƓO@&glݨgPiŨU6y+pT2Lq R4WW½g,թ祄 A{όcMlC,XuO&ӥV-@jEgV\M6/so͎|7!yn
7"*z,S`A|a^Œ#{+vq
RIfѷI=6&/3GLZ1)T<RSɗ#$LG7uu_hnDĔ;-{D"&bL-pغ`>`c
1Xo'$0n"s.#2=Y@,дKK4%1&EiD?K@ō.N2!FIσQ:q rL!Tp+/jR5=[gp
aЃ?V6ɯ-Vӈ:[tގ逻ߦ+(.
ݥ<z%evƀfM5*]nwukoc|ȴpCarp2IBmGú71nb&ܕw<s/#t5FaK\LDmD\+c`)<˒¶ptx,<D(턾a:y"T$fGުPw@e_=|m:na}Mѭ0V"Nc}NF%YkQԄZlSc_3w0[sq&Їr^Q||w6 z:p1 RL`)G|Er?=Y,7	8Z8m3;t쌳8N7âoXeX4$5!Gm9f}`ACyIi;Kw@nݾvʿ';n\Q"a[Y82VE/Br9'*s~pL` pC<X_ՀXVb7m2q]bH r[:nDoH
;݅>{adaNlVÈgF	AWMc777a2	08f6$lTP/&3AO}y.: 	NCw
T}W2(xCX:}I\~yϊ'2򭾔
֘jF`e
G*FcZXt6UTd`kV6ڨOt{RReMmAHw*i#ӥA[-'EmR.kͩ#3]*ެMʺx{Z U({^q#T=T=1W`YFB,}	Ryo3)f//J w1C-&S:t<IRB٨@n =+%ȺcY5D z,)=B=՟_$)܍MսQA`uE*rȰ}8gP>вw6RHdv8-jHӕQ0«}ˏǚd0Jrx7ߨ3oaBU"y0OQ*	(-<+TYV-Bŝ.^2SHZ͔_t#K;Ci8[-}d6OULx!m-a)JhxzvIڪ\&Y"g-jnn=އQ;@oeނ[bE|$T6w#|5>%=ꦵM!R`Uz[WDbWe-Wزˉxu.("r}N^؄/
.>`;6)(⩄}*)a)I+ Ujl%yJGRrc$2exD)D/ X<BAAT0t\j$X>*bXZ3"Trg 1<:},`odLc+iJ29,]~\s0)Qꚢi)$5q Xg$$̉P6Yv,gU\$4`_8)wy;G&QEaN1Kp#0?cr]3m獕#/MSsZX()R'.r]kZspJ3!s4B&V씃9P>o"}Xߎg>B뀵͜S&lO)&ّAzwCt:!\#@lS*?ЩB-SH9Ư*,5\-VA,q5_9c8o8..)}rNh)"HEx3ʦ0p~'	1i!!<Y,vG-#ċ!BA(kʰu=*T]@#WfӔW1DSXw%	+hm!R"Bu&.at؇ 4vDW!!`42u8䈖}¡L0e?JQqCmOxlwM6VNdIcv"4mi	B$O-,~#sS%}`Sx4"u=Seu32/i~+CS0De,JrD/wvH)X8U
߄4C$t@C˗7zIt@7Rȝmv_t5綘DNO!=#4\|bսN
RKrh`^=JQ@>h<bCOhAOp۳OQSL!\՚0eL'P{V-~TgoiFJ.{iAYXu=a~#FH] ī !A5FHtJ#	_.nR֌%"Xcz2!zNXHO=AajP-r%JP<m`<Mij!ݝΨgw!mYהvV]DR.pSBǁIGt
]r\cL
b@\
<K^~gZo[
]twn6"x1KЊawQ<Nk]w㧌dy2m`VǏ~9;9;,'ʼMQ=wi34[Ao+FJkoHZL&'1n]Y7чu4DÃl11xw㋒0@/BXRsoĺ+ CfMy鰔On3Y$.u_ MT9(eQRNdAZ,"d t%i,Z^mՐ9'1.&Y꽆IGȌa@3(k:bIc14~o)|ؙcnرkex|5uQL۶[iS*B4LwÞOgo>}<:e>P1}+
οQ~Q# 7Xc/BȂ8`Q1? yo^	L4I~1%j@N	F&k͸k178QE^?*<QWl+m~q \ gu|&ܱrƣrTD2iY tv":.[zVTk3-y$+B-&p$HMd
BڗZb;_zb^lc8Ii]BfiN
ªLoT OKgY,AS6瓯_w'_'M0<4OQׁ>z+l,Go=':v&{/c.m10HC.|3ٸlmlm~TD!$8ѣB:µ)>pD9PBg,6C.}0>Bx
9VY+ MksqIMQ0VT١m9^4tk~{§D]aG̱\gS6-H~"BKسTʫlPdB7ŋvNR~Mq~{i=_bAV(l-As7ud1xP]Ҫ[f'|0~&SgކsIpZr y"C3Zl[ ~L@Gr|gll#|F]Wy¥GIxFO($]l`K]	+`Jws5ՎhW9I4uP].b{kr0'`,-O[Љy|*Mv(!d (H	XbJí%Oݟt[eFES*N~|Rʓ[-+/^`d%dZy<cc@NuXJ-L 0I0;௧((db:1uK!c>EMw	>$𿏦y}K<ADҋW&K 9܊U@71wC&X1o:pLoLAǙ̭- uF1׳pr6~r{!nQ^tgh~M; JH.-bsҲmӳS <&B	U42M&ȸ
ҰEYwYU&m`FźTahcY*ӪDRا!l=g}{RM	6t{ٞ	r3MN/UWf7wi%dxADK՛OOfЫEv8H~$1H/SKY;JHumGOF>]VW>g( bk%UX9$XU),+Fj`m>>@UW	 ]-gfb"hf1R4Eݮ0e8KQ{1YA24B|AwOcVY=(D,V> %t:F1*&|4\5xz<aQ}<!vZ8H)DU:nཡgJgAh][s%B6b§O!Kr\Y	w+tH4K֭d7`*<t
osI񘸹6ẊX*W7<,8dK4w}b;D8x_l݌l:?҈ݝ} "92dNXu*kϛC
i?`cɃl!XwzN`= zϟo'Y?Aa)EϮլѣfyϾ#EKf2EKqY݁3agR%H=M;-XAxx-OoA1Z<_skЮWM=)k^M)	W#!e=XL]QIkV+usޭҀp4	tQD7bU"tFOAdITemXDCGb}>ѻfBl5  oH$w1d9o+RZ.j R+KU#ԭL+Bt̞hhR;/L(碜Ԡ8.
1+Vec|6-!>:֒Ɏu1Q8u:?a=0-q+ZWA-_˘:3T1|Ms0c'>t{*!$G=ݒtrx	oxRkmh+#Ӿ&/AeL\9sPuAPou)
Nb\IcRDk/WDYKKߵA%ں
G `/ o7#}$ىSaHW|"s[=6~BCkdkS)Tu8H-J@RFn&Bdl lzoR(rUP鲹lY#|E23撅Yu	'Xoܔ2	m5[Q39[3Ώ@h
D`ǙqU9&2HVX i!G`zT񞗊|FQ/8pm(C<9x/W_aCU
rAӍ
d%ͯ=u?@`  Bw]QQfW
qKKTܗzZcTPenO}}+ʊI{넂(䵓(DTwŲc=~YԜX-C@FA}$gL8ue+!Yϱؙ"4`ŝք)K0!6皘qAD |ޭo1IF1fTuT}LNq;`-|T{>Qblǭt:\#(š
#7^#w3Şy)^7-V?f"̚+l1"U-}l=]o |p,G25dkb,G/٦bWD)]'.C(,*Sl\>ޒ8qTwOӈG,`9zHäY
k\hɯMUhHdmG=1F(w}yaɜ?tأ eJ[o?<rH%8=)5\'VrոA!}IZP>@:<H
<	;,\S3ƿBB*KH֜?|S-\+/Z^=BoqA:sOi܆uC^xt06K3f˰uxa/
E;D]LҒl\fgXV<y1%bgO<@'\i5V$GP#uo!DQ=
/ ƚwkYx+u+y}K?Q7㲾L3Mz}$.#!33x崋,eΗ7f毽Ef13_7+t
(Ռ͸7"E::g83,z[P}S.$P6!#KnXpN֠Irʵı\{(Zن{@䲣E8gF$}]W#sشv<~Ve{ÄpzpNErK4w2۷iGb:K$z] ؃6y%ܳfvH2=dEia\uZjZekrR!rBZ(*Jqāu5j0ZUDزNY !j/|s}5Ni M_sEg^l]:rKr(Zͯ۸~|H<:/:Oao y;{	цzy J5M^g	F0*shŢZ,)gr=ʢC)H32\
Iv̸s&zdQfTd]<c|nywFWa*=KglfUY{$$zк?W|̔fbjhYuoT->zyE.7dof#Hʲ35S#Fp_}k !v.ʔ*]Wh
*,頋w)@TҠB]YTf7K.&=W!i:DT0la[۷^<P&-T"Ěͽ͊"Iu&v"NiƲ9w:H9Zr(yi~ٜm^ֵ*FJC>Q	?jfBU="D;SQYvz8!`>]B]cXv3+CO8ۮ:x?Ço(B8hwN*X0 .c[,N&b[$Bxhgn'<%R-K"_x=i&'\-j1Sb2T}('Y)M7F4$AT`<pH_n?4!/Ge*J8j\Vw@vmٿ=߸?X3kG?Qtã+#8l~Ut@͢U9S<iѝS"t9J2ݣ⴫O,9^aH1I5IR eVR(>v-ș?)A Q"`'T"&@*I6*ys| ׹\ΟtXOy2MlM&ABk>#^9?NФ>WW2MJ<CRbS:2`b>BA墹jhXd2eWUרveFҝ?KB08%FD$/+\V75ٗ7~"g	ty(ewOThaiOܽp¹}gL%.TmB
վ#fx0+}kOn:]J!b7>9 &P\Kg#F$9Y43
=Ua[4Dw;4&~Tv#|aH%R5SAXbb!0{I\~ew]W#pس:4xFxvfJZ􃸹
n4/09_n??<ި`>pnoe92?:-,2<[`v̑4sqjT+߰pG02B69kv di7V]4]o0F99QHȸUό[^lD-V..)(fs9wRbXR)c)U~@<9AD>|40pOn:l]q>z:w;/B̔4'JH:\Bfk({P0)Qh
ʵ$u b[b|s,=a,Fz)*m[Ai6?_Eu~A<
v"Q%e8&r>!!7Ҹ$ ?[yXQ=\2)r}&$[K0KyRe"VB2{NSm+Dd=]Z:rB0Q~OD3	I
.txY$߅lQRʀ`NZ<,lxgV:xzTz'Qsv	!QQא>+s!?OPY;ۤ F?'|tAS%F'w1^-@y"j/	?:n**O<	6m 	JF)n/\ɋȩi3'w ͺq[Abր6i0
W>40]`5;KԄ}utxzp(C"@N|_)P8S1
)N,i$wW bMzGЏX̑Z=(e%,7m9Oau;?O
.lmn/dt+!]*|+PD&?goۗ-QݢPyge m"L8Q)/QG& U:=TԐDIǳ_цԚq7XA[|Aso-fq[,A$aV$U #EGp?A*VQGv.co_3wYÍZ1=fe#ձ'C	TT:G
e4-*2ll@)1\yy8D>uƐ1Ę'9v0Np\㳭䉲P16,?]mV959pC5DVF
ǥIK9V偔'"INg}v/4o	Hд}0tDPE8u|aE%LG:K?B?ebipi&DqJp9uF\T#1;jV4riz&eQ*'yd,&:DsI!9G7M+tA~VҒ2[ u7v/$ga:!	S4{B'cࣤ`YdzkTLJC/\~+z$ɾEd:4AqٝkN)$Yĝc' )(!gMzיMOI N%	"O""ڔMHMjku5z3m
ihS	Y,{;ae6 Gcg_u^_`yQE~\]BfC*)kk3 8Q?y Z|rk՚.%^C>ĸ`}U͜hccnkGF B3yA3eloJyF&/)'ΠKTmKƸR
ϐ76"y-<O7i_bGUІ'Ք4[rA s8)D:OfeJYg.W1y:Ǣ 5E"'QACC\h(f+=4/X6Ii~&t&Fl!- fGLhs]_e7E#*PZ?&Pw{W-T
Cd`gP$a*`։غPƗ#GI#)4J(N9Pt5c~"Rig(HcOeFUgZ#*dRpZcmɑDeJ  _
b&}F$HJ/j~Wue<.,Ac	/=jtQM
)zJN<Tm	V4MSiD->E!&xD0GLW@®E٥8,#0=-4b. Ӂ{7fTz`29#bMYJߩ=~7aѠ!Z=fPd_7ID6p]`k8	FvZT9]06w9b%\va0BTy2f?cʥ`%(;0lݞQ6XaC5S$$Y"f3keoJ/I\.tlSdEU<9*D!PrT+RjdU;/ANRJC/7
"^ldbjغ
6b6Gk`e!ӣ>e"I,&0JE=wԚ8uq%fL<U,`.ꀗoͯC|M}D49[Ef|ZL¼?2v}
(^-<)*_7)/`&~Ai#z53ij	%.7L@t1Ze.]ߩ%D(@(9ik!CJ1f4VFk%I6Qro/$<mkk@RZ8_]-<":?;ǿ"éWFh:B:LqIma<>'q>`',Hb0w
a^Ѭ.k @s4uߚi8*RV'![=}BįҺ77MrAKz!':ԁ/-C|0#k=|yU"ߧ'P2~^TWL"7&!Ή!Rh>R;2K.~[b9A_J(4XoKԄfKEVGȊaD,al72BT3/89ft;nTMKg~H)6n(nK:UDepZ=G]LpI.*&\;LBCG!K?C*U*Z^ewjPxHQYxj,FpT-YkR-_Q0xI!kӁ\
&w a[//s)U:pYJ}.^+js-E"eaӌ|vRI$W衲1_R
sŽ_U-$:R)WVg%!$uH;.=6<|K+6s/bZwB8{Jz"ә(y*l`Z}S!<gWlElSϞqI)j=n/-Urku"?SsLu<HH>F .jX+k37Ƈ.`Rp㙑y#tnϪ" L%TVM~,®pN~}bti<'ݐOc'g6
}	Ir_cRy,:m%Ɨ\Q!oPz:Yn0Ҫ{aB<v$?_\ ŖxB^ԙy^CYǸ_Ěd0Ǥ%7Ý?M]-@N#JzZD(KҞ>>tzrY :XGIKb/W x$<hqF	[0&>09G7dޅBH{f@RV a^sdZ)@ue3UggU)?
E.Ir
6k!I&PH1lMT	zD_N(Cґ>C2B$aA7=63s4ntZìSrP1򗄚tf9|6K47gq˫!;(L7KX/KPs&-)cֳ7W0ؤ V!P1s}(FzC͗? j՘'ݵePWR33@USl6_kz"ƫp鬝eKA]>A* vi=tbkҨϿ;w{0Y)"Kg{O.I9fڴ/+[m4OMd4;#EFT5<T2q;ᣤɨH$O3Jp3*yLpL[80;^hIV=DHFWGt*E)/gWlpTR6K| k.IhMAPûčc|pY{
 K]]EpZ7xEםT{V}|͂2xLюS&F/o'юyB'`̑
whi+hrTtC4L
։JTrE`vג($|}uY@!봺[L1"+΍6Xԥ:vCuڅl,	Yv+oXg6,9"q	xɺ񪙦&]6Z㧊Zt FZ£Ϻ >	)ўʰ'c?pCē%(U`zΣa}c7O{1рff%sew4o{#dݗ6Ɓ?Éev&EҰך#<FR	~>P-b:Q*'m옒5Tf#ɱOK,a-Yi^Ŵ`ƄCeq'Ja}莝<Iy#2F>[ZM4	DW&d}/N茛gJSysm{
Uws |tX쁘ʈ!>'Sl =mM.'=.zjY,n^!Xo*3nu;{H:LB\"MqP|IR
/iI㣗29J`&=腲)xֈNi3^TIN-wֿx0@92}=+29Eڕ*-$
8\WWzG! BX$RQ
䎅K"9ae~S-܄9kHeRhq'ǰuĐV5R}TE7-7hAD4Nk߉:(L4CSP˦]P QЍWvڐHP3XVTnrii\G2C8McbOsm , 2`[PpY[!g:7`WJ{Y} w	1q+
lDBqNupA'B b@A0X؅#+T.u-Vo#GeR `!wA쨭Fg:ù6S}Β7 V (<ިVT3Jw8[YɨjShŊ0`W	yau(iH7KЄ_^3F!~@QsČ,RyEP'@6e~=m_ ċvBDP{#G|ΦyMr&u־2%ԩ86 =P(+Y!Bߑ~~mްGkQݡr$4A	[!֫F]h7;b1X.	pW%ۚMxd:!'2cx˛.8*_4H;8aiH9}ȺCt_DZ{WNwn[l1r_e8pOj#Yp%ڞtVU_'bJskXnjm*CI:&40)ՔOizk7W&*^F(CB!$5{OZe>w-y:|kY<O(Y&[K&&2ȁ;kz_u}eXz=1h)AmPª	Hu:Wd4OjX^WH&ķ'[9"j\G
dI"OrAW(`fiZWM8>-!;pEuj.j/
=Qxb70%I;<1EIz Y!4ᎢdY[xݤs+NW<:8 [[po]۸|@j:M*I]ER6ݔ&)M
@,@(@=k}=\
ܶ33V2Yuv>y׶%\ҞUÃq1bFeŊDZ)">9q=˵S;C*^5͸X [hkʼ$´X2/EJdď NR&gR@E&.'<VBF7
/V&%@9x:SO5M\
06B*&dqEBG	ބ"];So߫hZ_JH޹eCNO.\蔞҅cuC	m9PI9D﫡)˳13OabM5ҹk^zd3*>FnK)Mv	,Uhva8}kǏE`q&0WIW @WTHS 83s_/t8XKGtߝrEm8z"]am]ZVy<~)hi(ddѭu"C1U-qP`
!q]FYL Ix,Mhr6lvX~IHKS/)@NONk,v7;.)dD/t[͎#ORR>$z>{fh^rw{n2q˖~B2 87@  1 $,}0f3E{L1c8\FCLULM#)aŔ]obRSP~&=Zv7&9= Br0=f-tneGxM(iLDSOcxޮb ::|?
W8"NU~ML[WX[g=ҷk(AL*U3wuG^y4ѓf4Pp?m<{8-o+/a+X>n<1?h2-*ƟO't,wB@'L%I=7UO{>!0{'*WSNRjѼ=<k!gը2p\777#V~뱶F}S{ɢۛ,95f%
Ήz5WHƂxII	ɩg\~_a^=SK*)&3 i?r4sl$R?tZ|QdI|Ad*b~\YT/9E8&XZ=y܈۰*G	úsV:,g+})xPs%AĐdi8zb%ZlXox*D|GBC=O`BV%pHbw)ٽ0'u7> ,P6E5&*ȈzV;az ZP{E6Fk<׏%P^׷\dgHҒZ߬k>jPAn}`y0܍gdΒzt!l&;ZuďN`''LvfX
rV[,!}(JA߾/318F(ӕ`0s[C|]qCNի o
.B.ͣy?FRSّ@a]ǽ6V1nC	sv?UoMKTc)N	20C̜q&[HV萵ULX! x#μ1u/vacx
p,
M,	B`|49b4L`@e`D7S.&
{*2co9o^k"s\#ƆqIQH7e=%iW{yECƎ6h%vZI
T:f9''s:\%dٮx~hΧ,H6 ῅!,Ն;ݹLnb
0s[p_0?1>OvBۿ;ª9~	I+V$kn<dwʌ0JTj4[iyYYIOm%q	CmV	)|$D8{wm|.Ve#?9!f=2oeָ)6VJ\NJhm7 :%3;i2Td#:.b,D'=g83{kC7p3vLشIs'W7LZT|ק7]cfX;a'A\@(DhQb`Zk"&[#OY~!sq{(3ۣBGUm(
nI nV;f6K3x	2[zE+%}<|Ox-w55_m<WvXE)*{E$@w[Ͼs/NM3+0{QT@44VOqnȵӾ^-ꔻ˭\8/^{VT:໙Ykۥ6/&.C"ɂQ+b)c	Q6 Lvތ6H&0̼Rs|@Z"Y3DƷMX[RzᯣipVPPw.aj/k̼3 Mc-ǒF @qN U98|COm$Iy/nsGg%d`&96AXBnk0u	a7/wn_DϬbPE}۱'/nmjb$8ˎiVqo½[L|B+	iV^!c%HF"q]:fr7|piY/̺hzu[i(OF{D1*ONjlEDMjk-TbB;&WͪHO2dPr>Q$o\TqCŘͳ'_?[XCkPGqறmTuMk}w+qs.
#7?ķ4[,Ѩgi<pBG]CRjl|375)>.ഗHIF^z<:7v<7:1?e
IN҉ΘǴ%XA	i&bsmT6ִZ2ZL7xqQ\O]xe ,]sHf.ڰ1
|KPo׬P] 82{UzJ.q;{ڲoolb&׳"V:VWFJ<E}Ts1"!ĎIJDxDsL{s&U܋xDOuN}9M3A.|34q3`i<<}5:+spcBfLM&kVϵǒEvɎ퀨 "l}$wpDѬaP=ᾐv |eW4T-}vld{ #csv5GqLD~(%mlۖmNcԷoTVol5SQ.۽OPќ͌=JGP쓕*1HCBbݒb_(۽D?)[gxȶ2'`A2@]V`ٖ+>$L?د,{8$SKf8p3=}C =h:[9=9TBlWιc5pA.xcfo4
rDq	BqHu+퓦ȿonbUvzl"k*)=	MZ
kQKZEqBWaAPGnK6Jڦgo5[Q4)ZW)v}Iu%ðA$KL K[z5J󗍺W!!y{}zj\cgQF#XPEqn\;Z%&P4&`Jjm?ϲ^*Ar275t&q_ȃ0b\x\w@[uMЌH /WQn{ܓ2VXA#^pNc
xw`ϑ/Gq8]!й;Y	C .0Utx 4eq]w;;o'p룿^^Nήx>yT3sB~LåMxѼӉY0	ʇWl)FEbH
g93x_.. l)9j1KW5Xz^\X^ױlC`ϹlDW$Rk/o+- CFs$pVB8Eca:4!!ar` =za)-S?ޜ);#OOj6.?ߖ|5M𲨿W&5fq/5hLdcxysSD.Z}As]hWV5%mdJb~]gLL؆1*)|4޺s\HfTBGUzM9/ECk͆LU 2 [c,PC\ka<$^JqmW6u="H A= )[1bS$Bl.&8,B")aٌ%?fN(eL6枃Lb4#6eUy^?HS&9ilrⲞR#%UYb6yk*3HB1Y@&QyqW=tTӚZh3"st2 
Rn z E&-2/ފf^i)32V{$@>̡zݧUT,* s\N}z!Gef= `4xo106Tx&hlub#1Ax5jS$s-楐U ȗ4TYLo;)8#0315%$!=Ev_!]³=Q2uU~SM['+(-s3'Ј/=~}Аm۳+8V*\4&OP&џ<7to?z"OY(c;d((,Zdu*yi$>JG Cx
%4z-	te:\CPר1hފ2!/#)WS@0
~S{a_ԌY@.Ku_JX 5QBi?0FL{_x')^|K<Ut=_-t$k^}t/>b=&Cefe+Im0unX]Wt7X>XJTS ߟ*wRoOc	i^b7~§O53['u^|:&𹾾Ytz5Iعnc@Ray-rg;C|[ku>XBw*}mVm!R|Usϫ@[0Ы`*oT^KAKKE-;TƧXP)/[敐Qp\@աo6/=
mF2XlZ>ǽBQx <Dc'qSu?bIJ +p])w'L"̺+7zr
ߟjsF)0yKnbG"Ѫ1}rG?=Z}	J:'}MbCq0~^Ky#8MXBB߼8*ĚӽA=Yh\3$xH}󼹫yn"PXѦj0-64.w#ؠ;`Mfe#I
dG=w'
Km۔|^ݶy#5jC-{(,b㼜g?Y-N[@$h=]#4>1ۂ֋<yyή^}*M$҉D:O`":`9TT<5:g'̐QS1rUzd!b?ap,=;!D:j<UʝI!^.qol=G%_.6.)sGƄ#I9NGJRGHs%R#w/c Ȉ
9_QqOa<k1N"?161롘l2Wv9۫gjﳶ=WۏZsO4,~nEآB]Z5d2 [ܭ4*Hiz7WoO8zt>jOOɞ<wIrk5j/H)z0H" i>ՈG骊z;9X@'qrI@H:tݐ%N}ڪ$x.wSyEocd6#xQT ؎O?;|Mn҇@XGDufcY
gWe$%з>N0MġF `SEkyDC~.9>-L	zՀˇ!@a&qG^|Q􉤫fa
iF
A#Yq7XLa%,G6籖vƇLwn1nYsUīN, ,0H*BA"3U~/V.9}wXYܹ*F`r(_q @ϼ:'>ߘ&|Q6h~M@r7T۸ J_#v#hAWX!&֥c7LmevSP)ڷ풻tඦc#UnY^7/^0lwė3j%Y:`d!G}{r$dF( @Ӑ;tPÚ&ITǎ鼄U<mpg?=혨gȳXlEaP?
p0꩐#/5O iw椻i,B%Ntj\hc0?	Ã8W&*S4:HbCݐ҇3=vBPF݆Mq$(fla#ԻE8mE^gϙB(7:"z== ʸ"mQ1bwM#nӽ)y~Un$nE~xq֯p~80WiV9=lL|nxG{=%ӻ}Z5{yaFCRmb^d֒p6%yD'"@?r. EFӂs,g,6`5E}pgqej%h);s|+l`bH{WAq6gȸB.AN/,6ӧ$kDl̻
Ǧ\If	RL|JfФF F>q4
ª1D臑VO[8>1=4aum}R	^fin }67wx}jkKaE6v 
"j4iq
ә9òp+/SFr4YfQS#Nzφߊ@[m_92`$<~
)h8_1w;TB.-Fw)\vł 8dg\;A}Uv~d("(3kH0t36<ALlyZtyXާ
a*n",ڍkn=Zweq<MA%'ZG8hky|F"͔.t)"gÝPъ ]V#yt
	٦ʔk{*96=]!)#`;s8;,Pju5aA*^sbee8{ͳx%Nռsz0_,
gW˞M;r2Y5|_rDqSQ|c6șG01$}Yz4q9a:7hNTyq14Vu`ܬ`q,StnxWvoẎ ը(kI ~gLZ*uEx ;3~-a-g=11.%=}.# ;tMuL6,[pܮ?мSbMQJ{IRj86tL(Yera6_T)qIγ(%QC<-'FSڱ8f[Emi.iT޶%to}8Qz72|q!]_^șĹ-)P_T!,hs(!;	,WulqM# /MhI4K!d$l`鮔&s)ܴ絈͖&jdZLml24hCL!%<i3iꭞX~M.
ǈDpͷ]ѽ=oXڑ%n8kwyʃC.xEHCVX.&=Sw7Cw6H%[n w=gFZ3BEY.\^AC0Z}A^,aaJn5L4Hnxިsnmh 2)ʀ7YGM3+S] Q:^{]W%}i,qR9A$}TQE\Tm\qKS
pjw@1{lZ8'9QCGWGI4ÁM.+(U' W0^rL!	F'Q%2VU3X)WL	HK݋h#S-'5P]=βuuKJ:Pz@M8DrD%EaA1{'BP*xVqH^@66΄0*=F⤗!gB]~qùQ[^DBct"E3}c	6TIrߙo?g(pEGaGd*ji.!
vafPW%@db"8nd!l%
tdv:+Ȅ6JOb"Bq
1 'zQs'U9F"1 i@IᕣKNc	;bJ?L6QKpڒh.SҪKJ<z¾ۜ(,̯7M	4H7V
: {BC
PA/![B:'2MOU87#Նw
,LݴYpiJõrne7j@PH䆖|nG@鎘ݶ삝,$dn0p~-إkF~"zArX%Nw?$C}<`Yx?[~@1[=VF	I)(h4j,NgSn2K𕴸'&.Xeܠ̐c8YU"lMtFI^LtTn)lA0f۔?t	4{ib27L8	T7(w؇WYלMw{AY`&w"?EvI{POFq6)=\	^W]L8G{7xaA.^$S 49+|W㿊< U.|9Vtl!
^/IbMiR7387!5Ł/IȒVkFmBCvٌRAvCJ
u&r󠩲5eAP<^W 5voZw/wxF]] >3Oq~!;&("bewY&՚,m`gEBy9a3):rX3)Ogbz0&9BQി<5.M0uH5[YV]ΛzZMOPY;,
']eg&nFLTc\^V)G&S+[f5)gQZ"JDb,\
_7=m~y:mm{y±mS;_>*y恕~6Ҁ"Cxa{6 \{{ $pg^Qz좦G	=vx`%&:Lࡼ8iiWr;M!<0M@	ˬŭ"M/hڐOf4%wډmVY7"@Lڂ8yTi6zYQ4H|W^JtoAkmƔja-8C=FqB)ܡp;:G nSO8{'z<D)kK߳͌"o4}Vlo?=Lz4b(hµ(ʿZEX(GL+0ޭ$1m2 M(h3̸b1J2/1_X-J.\9"Ֆ
Qȹr+dv-r0hzz={q	U;s}2%墪2bScۋS5 ^9jbQԔsSl-L{ycFr0
i-Y [P#驖TLpD
œi(}彋?=2a^mP|ꉥMS3Br ٧u\a7\:^sw7[V9R0HI:7P4i[eu|,@Y<LCDVjW"Vqmvw@`Yqd0>KJWVߤ*#vZ@<.4%%z\ģ`¡0@s0)t)ٮap9L3ZfUz[^CN	rG2#ǺO(Vݒ`Lz䵼K~}~.tNh; Ve9U)Hz,Q]*IB jE'|!QyTD# 7\ #_6$̽{qby[lJ>z/1jHml hk b8ic(J B xEIPP+$/;	++@,M>2PpF0eqgg K腖#Xd~sh|ZAHY6)LO[M퀽^Jd9()Sr2Q眬e=mfu˷8 /G5'8详)|RjVHikn*&IL͔IQʮ0I6(O8>`{6[%ѐяwpc̔-1[`LsxXBEs@tP-Gf?|K 5-W<l\n  zDebHd#^t*CfgA=K*uxT&i\SXCt5ؚ<bz%w9^YΜURV?+*E* g(+%G91YπpJOEkX<D$b!FR(Wdiw㔐_hx;Gq͹.RpֆUk>?jͧԃi>>i CƷ^$qF6S*R̆#D2Qb.w@7k G3;\>sv6X-&O[h?LRt	fdji`\KLD^gPc!Km A6U>uq}^7-$ixLx7ە;ea2 E&iÄZJURjաbX)BC%QWDoXF
*8 h)J(IS8&<]BWVLn):łSp	W=Dod}F&KƝS'ѱ;Sz:p
.cuν{>1[lتye/Q]bPegx Sj>[C&Я!"ڽƞrhӐ{'k5(qxLàLoPΘ1ͤ=A؈iې(lX\Ge>$?dÀ<]1ܠ$#f]~`Q(>:ՌyI󢺟qߗɭ1W%,ۿ@.D>
#B{k+dTt\5+؂5M|6**MnmN7k|n%}#%T(fFv}Twnv,CmvU~ִyEI?٩?׍vi=[% "lKNnj}HMǋqF:,\b6*tDjK>51L8M]y
j-p3s2~QW)jLu۸n d&&xQ{)Oe`}֭ClzE/_%Ó髣7=<z}v}}vg*ۆk$;R[F ͬȸ3	cwZp:i+vE{hFaBkS(J\VT_9ߔDgAe\hDY-j 

iŘ~0a~07q?Q]nRS5F-g8 kP3e+!U=qlQU.F9rC\/<4'&dV3(d:%kc>^Ї	lNLXyW!(!e07Rà	
:w5"r2_SZv_qPf,D閲-OO.'d)}bF!D(14O"C(\a@ji>/gv\~VCw}	tA2!㳳XWzUkjFGnSc!CX辏OV~ֈ'xY}I<Hy!{BrU;*7~x_nxEh@* n</iB'^cDfS&E<\,:iDodDKlø:1eK_AnV&vVʴˈtCK~sX]lWAWe@SZݘ{E2ߔ,Ĺۅgn-gL^gύIjrY$
ZgOT*ܚa5t64u rrHTd1ne>;Z0w9qFNz{EUqմ~f?a ։(ԫ042bͳ )V,vQE_G@4JOYopL]14Dmszgѹ(ŅY*2Wtdi'%kC4˸0:~$q^"h8 C=skztAh!:Gǧ77_=4izφR(?L(UriͰY{y밝zNb{A[Ԍ%ߔ1ie2FSzGtΤRKuK9FQPzZ:<װ(~K|-`+O$p91
+9Z=$(l}R1'38HȾ[s!E?	qxCiy8EG(PحܮfG|ǇN&p|PĜL%K{_xmrc3օKw,8'R\^egy{\
scJ(
 ü#%zg s9!s`^)',d碽C;:RdqN!a8є$,DHRwllh8dh_bBP(3Ȧ[KAdV;-5xsHz@1p쉪B`Bp-saXWDb@F;YnwCpT*vPzb.r=-VN!;DO:cpzt%[Zu~?u) lK%a҄
)'.J@wL`"fT
@d+I+τ-ڸ-pr@k&S&+܀ZK)X.?Ysl"/+vZMH)e\iM+o^x)%{ݞDܺ-ʝ|!]\;jD#Ð@.+z"Wa`/tfJ%NiZ̱O|ZgJ4@݂C!ou1T)Z:&=Jd|

-1CQLk`zy,A=34$=U|ɏf^`T ``WO=>?do_rPGDza*I|*d#ei8bSpJT1 Q|<sunOT^u-5+HaN)k~;R;N)JZРMR1CnoVG536{dѧYDVT	{6MV~.RĒ9֣0P}B0 M!7Eb@		1&Ao}vd|`NHxh?ZTn q Tt~6BPd~Uc}zGLW49c4:?!"}ZUa֧k4GPM& O	>-YK(-)dlHl&שϡVmQA0^U~CL#ypf+Z͐M^6,=},h)m?v5zLv yA̠Ec?s4 vV|Mm`/"{yȉȟrTPﺖ$G`;QJr7qGxY_ ! _ -9\r/,lnDl{!H=!bQAg-qx9佤6kuRMGݰ`h8C  #.7ѹhnҭ.x+ P2/7E$QB#I_)fͮYōzY#X)h%~N='d*
vCּiҭwo3Zv<.nhm˨syXAXQ{H"$ACGYn[jpҮV~]>6_x9aQw	p`|xL63=KױhItdCf&m-l-3%`.F5MPE5J^</W&		(5ZH~ҙO$6|K@.PkD,;P:!Xe@Ӂ?T v:Ю2\}^HqӧՅ1|vmoBa݄\<hzX@(_c{H.SUaArr3e^2[DjzIJswiW7D뻲(u0'I]uNgNّR p(#{KZB;.jvP2UF#lemy4_h%4I $ t
AsZWЕţR1F:ɦ~Y)jhOKnRQD#nfrrEew3`hS8{o;dB1:]"#_/-&zc?EQL&H5/z>@<$l*wI/5WW'9Jx2+0tw!h#	R{.2Hyh
G 5¢ cj -e1rdJYxQk_F#]u#WRIK+<ke!7i>p!	WeOMh)6ql-rfOd^1d	vm&9 EGRJnG@VFMT,\BCreAņq"#=%jqaVdMT֗1NTiL4L;؊^vnYq d1	A&ު8\=D2ŚU>̛跺,	m̂'@&zOfC J*!|5s0|gB,cK޻eD 3V\c_){ӫwѱA%,+nhEIr3{i[  k*Ϭ	0kGRmMT_ѧ #Yq}pviK"⠺Xg1TE#4^afpקҠE	7	5Y#)#=j>"̂װHt
(\Bz {kĹIP_6 \IL$NxXoC:)֪d]$ދg鼋jh*\QgCP:m||ike`C~(:4][N#yBT-]oIHTOkqIB`􆉺8H0>@]|'VkJ07Sֻnr/i_幗oaȠ8p383)=q *
`
ȱ0wuV2qEkF8v62pb:8x}r+a`FUrhO8HCzdO@rcm2r0|(h-uL 7tǨۊYL+,ms铳q+u#>G*΃%;pE,J׋ih˘yPwGN>E;!	]nV
#BRRFe**	PҘ206l{4TXnr`ۜ8rز3&!yyWS&'҈dpjmBZ)9 OU1`͐}m_
V27 b\7>:F;g_J8%J]3!?`bGF<蘈}b ^Nvs^^[V=9KtnNLiG~R)`j> &Ccp3Ys/6f(ϙOZN,慄ULn[YJ;zs-W̡]"hq.s:2Y<-Pw%."Pt'H8ËrzS/x{gHh@69s<.I"P/ܗ[J\,xl}㿟U;JxQBg5@}j	>/>OR
K[MֲbBv>yޤhܗHwgƁ.:x^DC>
,b@h	߹_*aQCȽ/Ϗy	)f%9C8 uiR;P !g`<4بbu{*EV!FX6̈́2CuI#K+W*| eiWj3"GQ.(Nj1JiNS:&1vfie>$>h]\Gf{B!QcZ}t>q&L/s΂qir6&D<u9A@P&f:b^n5ȄQPɷ&bA"Snpt,@$KD{h:|d}JР!ѤZU*3	PY?sC&tQHSI=
uX(rfoEf$uߵJk#7l"뾔*)>"㛂^ ]Ձiti <;Ĳ[*k?8Wx')ҍ~09#7dhpG87ћ(d+^>5|3e1R}v]Z#Ż#=,Y	i&Nz1@nz@DP# |,7 bT-HPO<w(n)wB3Iy"mNcooqI|#W%7m8I2w	;Kqư[hՇ6/V?$Q^Oho៦BkיTϵ`*VhQ븪ȤBHSCDF$2"* ε[2r~!sF\k}ķ3XFG
=@JIew!הB(Bi1SXۻyt!>a\miq$	^ZL8ijЃVgHvȽݵ_:{30fC.7?  27s?ы'/).bgKE\ۙq}	ba+Lဠ3ad5YD~D9>MAN13z%._{\%m\zv 1
OޒFXoTIL˓ ~eB0^0)2Ju>dq5Ũ1x{e~ZCŠ!v>(a3V7*	9wZ]Gm,<T#2EWq(OL$d<A-h
H`-V	WDE"DRPb`e;ЖĲmLkAYR+R	^ۜVF jKr Ib$G+q8꤆vL>!fYk:yLfЅ%AЅ{1Xυ&yRAG{C}Y',G%Qata\?(OO0qˆpl|`/=hݠ570_JߣȠ9aS.,nuC
4n݁*1Ao6S*a3.>sliZXZ8B)yob-W7@ngDf%3iª6DpgDre/SCM}~ygQb:Cqn}4O@h\<0A9zDYT< bRcMa-#cXXQC/l`>m	ԎRl:=MfFQ
yNAHtyRI\I@fWYdU
ԠZLG3'>Q/9DSEZ5΅WDo52%;o	pṁOXXfKm]YYՠa45N[v/F!lfeYֶ hB}>߀{P
%H{nϝ]_2K鶍򦚅<Mp)Ucrq"sT!"%T2o/W]>
F7H=Yu`r"Zݝ۔̨>iSmf.mwۘ͝t$^%D8'+qkZ_\^k|XG6[m%7Da2˙ƭɩYJh -<KIGER6/,Qw2Vwe\[ݜ&BفRάl!.s
m̈Zqq.R0ֲDv~{a?!Tim3T!-z0bO{\DztLZKc^Qz69LO<)[;HmPmEbM0};|wVM7=3r_t(_dd`hFۖxR.r;J7ysIBpٸQҝ47}E׷~
Z,;`	A@a(&^KV|j/h! Je|E>596LD;rHkԽRլ8L?.^<O#9?rb5:䫫wMg;fwMz{8gzDpi犐e7REOu1Fo87тI q6()qR_i:nfg&~xI!կrlo<E#t/ԝ(zE	Y1H"jFE+7A҄*XsIfFd5%;caP":Y`M"KX!^]T2"O|7PFDy߉;Ø%Wl/V͘-&@̥4XYsyo2@ypEgVPD;D\Fu@܈9J=DMi<h@;vwVQ{3Nd-2tDRjB$Du^,C:kB	!<tlu5h[S9׼JRɈ膔vہ|.hlubkx]|7$rfQshn d uE4]d!Il=9FƍU]jAyW2ҹgbj6ӎ{TF
Z}zHZ~2]? <BB9r3pAnme$n8j^OϺUzNHɧ]:mRV^_ITC"ҁdK?$| ClDG@lݲi|jYN)ӻxQU*L1A@D$B)\K9D<<Q@''	ݮ嫪M[a3[ۧ$)!^dt7[NSȧ~X=˴ҕi	1^C>Mk`*Eݩ-ygN
$bU/e"	SƯ<Kle5?jb4,4iFl8}
e%@>a'5'G"?	ENM{~9"%.=̝@|,07=0rv[<G1}%WU8p(!f)tBScRIPQ0)cV.&}ְj	T~]C1%k(8
Dפ_kIE	z4*lxs4m4y36iR(/m8Qq-dAu`V&zRGz
K"z+"Yxka60Y 'oIYZ dw)xaL=$.Ѣmg!$GD*a~~oabk#ԠN[Gd8k-q L^sﹾ+!45t$C'U"%	'hNʈ|'aZ]uF!X=idރ0:a0`ALxT&/rbqLO֗380UPpلLI0ڔ_(Pd:7zf9Wv0
Lݲa]7_xݠw_j]
Z [	f< peQAeV0t^|CV7YhT/1_GZ:	uN&ّݧp@0z%6V7׆*-0i
xS㮖F4#ɷc't+yO	ǲu[STH U n ' nȷ~#1g07ha(aC$jϝENf^vlx_ˏt5TaYۼqG@Pʘ\rʙQ!u|{jpQK-YeYGx`1mj'B-mMQk	K*JH|EI&'lp"`j)cڐD`oϳOZ8ճ1SfocʻzSq~ϗ֔{D #Ac`UZ qI"zG )Z4G#if̴h<F&-T\̑t
cAYi'm]bp(<}|/*k7xݗLR>6&V.WnmzW^~'Kxo3E5Z|R&AVm_mf&ys(B RC?܃%_aHr<^cr\ŴJ~^}A84MZ2X&#>k~ر(~d^^ abvb+4u=]t+ćA`:juMk5B;vb蹥&{Zg,Ln	ASДYEno!AЅ&nAzQ&4m4e8'dDax{̇H7.ȳ?pD.kda_*
jU-: D5Buպ&	H\,x)$Xg<)\75-Ŷ O5Je(1$xJwkēo0:9{}]]mqgiC;\3.
 +R3TO3!K mfYdN(/l1ݭٖ@NRkYh8d"V"&m!ynL"_5nI,O[S:Կqc@%ңh<)w}P2zU~'g⮚^R_֔3Q^.
3rD 'g1׬JPuvj&2Nęi} Q,m3ÁS%ft9&&RGڤd=0N.sz׀j<W'e)%etI<b<λ"'wM#pUf%Lw|.YD4{6T[сbڈ1pP<ΡWD6
CA-y!P9!ʚw58}'#^AؾB,1%b
4t#zFkɿ]lchM>zxA꓎GF3L]R΁t0dEQB9,d)"q{vBWN/H MʽRf:'.vmfP?ֻqPP&NEa<LW}l+"xv}Q.WrL~;f";S.}9GfވG)=՞fEVD7'd7x6Pmur{JlQML`L}~W=UѡTQًN5mɞIX-!"o1 B 1> E|dRIM7ZnϸZN8BWčI E,)'aDa@ "VWvض={:P)
/=?"~c2BIӻ:@ǆy+drBt]%m7=W㍝6E>ORNj.Ù|v.n9=G:ИD?3n&}W56|ˮuz!PG@2f,Fy腍*Lx$Q~GEm^Θ5>@M=.mVFӟLOjv"kW*}$\ۭ7YvsPzQ/MDV.26)WlV&*ra}5ĶqZyWDv@JiA;7Aia(30,uTLU:1\9@¡){rrSUtJbu|5wBG)v;GAznD2>u^fcץ"?-OQ07&"ʅ̬2.C#,Lh'8ߋ0y@JeP3అ\r1خߡ,ZrԻ+nVPȊiL.Q%׿䟊QRR"1~H|PdD(THXHqVqfMI`=ĕI-eb~ϝ"LaIh.J.1;8mRfdifI=Bhk>ݢ'_ꙿ@<xګ]+^E٤֝`ޏWT[cv%**AQIm+8=Lk"#)yW6(YJ8bfELvb1BQ( ZPV[<ӌr_-P!W	w JT2h&EnK'v`d4Mwy>ib%2\]";ѾC؋YE({Q)B.ZQA&C$N9XBނm~j	grCqY3jXkTmW\uX$9T{hvc{w>} &2<C_}@&_)gqn4NKY iL:)珟w^Ʀz`(q6dq`w-H/V$S2j#X!HZ?}6ꓬ<Jln,1W/009{εYvICGnUOj(@{we9l$gZL%K-G!gnBo=XԗtPPIVw(ΥK%'_uMN/{fd^0Qn+;Zгc3JϝԖ7ٱ},ڬU|lnOԉ{R:yU+w]<Z%Ty0rL7R򞇇!(AY(=^2h&+nڱtg-4[
GgmQGq#9sY'U\P޲[nŃؓIXol.dYSn
	=.zAѕ.<
*&XTb&\a<>o dq"%NM"h9Y9>Vm9ðhfv"Mi`npS:%-ui؉-)pf&M?I~1A'+PJY&%AЭs`E	-*"d1ѐ+\|];ٚL7Nl']9ZkMg6+rx
r_^WiM)mx͐[2 Y8"qs6Y+P&b@Pn>eELrZd2JnODCQ(ut_"`]Hf6k{ v(Xf6-{$wDH/B}57BbҷdJ<BD1?ƙVR}B+NL22ՂFN
PfK^hU3#a:r$Xm	RχFs}p
xlV)k&r{.:Yloּqgؖ`۴}Ĕ\DM Yx}TH{vaF$rcô4\=RkSX~0@rrAۨm4˄ƐkzLpppv\HF`Ch	%;
Ctf⼞>.;X4+ҵ8\kuFiɞh!	ot!pɴ|q|,!<3T3elF¾Gi9^wCxBGe̘M fW	-4	\AYsxn|xlsӁkVx0U$0 i7(l\'f`ӂrƮ.GBwL`BG(_/"Ea@9,>|QPnDR'$:*;5of}cN i-4rIkre{E/,ΰrZGK8ƽKLR}t0Q#l1) l_;%Sd!Uy<KS5Lo(|"/xԣ`{WsJiUQ0Hi<kD\d|B*`4-Rt`1;m2!J`Q_.M]a.Jī&)vbtQp%#⭔V@Q:46׽)ur'51/1KƐ]oR/A5r]0MþnEfzT
'+c.i0B0{O Ӿ)< i'#F/k@c ބ~طD!-B4vVaB!)) Z)#kǮ=^(>+T}e7kqy}]j}x}|K9^AVDPdUς%7-eu$k[t<D t760or`f	2(}G&2<A^2Wм_(ӻUf|<uF}id=pn%Ұ6]`?}4҅ic_d*=PK67P-4yhЂjTsU""L*0_FLvhy1)(rPȈ0)C]t6ᾉR4^AC
2]~<07)<lA]AҖ(Wc#w>._cz~]#Gs31n<@iyueieE}~=yѶq:`/:p0'ՑM0y*א4>5W[<*"LEȓo4ެ/Rb	e0KBnN&c'Do&E?csTjo	:^ .d	~~@3MWt'aG홊Sܺ'8;IţS>0;rML:وX*&]L} h?:ZJ[sREcQDԁ	~'|tH3DڪdBO̅o}tSד3J˶VRp_8Yp!BE+/z
Aq.mP9G1'v/<Iz=yFʮh
G/yG ^6ڸ2	DY"D8Ow&hi \Vazs1>>xɻbB){g9bEqǄ6tC}S?N4˷--'~~ *I%D#!tm&!lCw cQ,A.D*& %wZPWX՝	V+y'jwKי?T6٘Z`Ab&Vo@T8I><Wk+HP7Í`I.{,!>bR\O]vf<Pdk,JaDarPcC(nv^ /arx380Ip Tc 56{3Wd$Fw%F0Zv;r{[69ٶWbPj+,J!7Cr:y:|ؚr|A
\w&-Flؽ;	h#gL1dʬ_"YP*mvI{NmrkC̼D&&<}6$ 38`Tq#Mrxe+q=.6Fr<tTq({5c3mtIظ&Lsh\.ҔsLR ʣKofzo>؛Ea	723pFvo2Is;FR#۸3qq7Y/"s@r	%	t݆J\Qr3͆Ieml	FɝM,6Ɓ,r!'N[EQ/D
aԾ6k6a6fm-k5wvJ"&;;X	AQ@R9")ȰlG=(CB_EJa`+fKvAX;;Çt<F뗺  IN]#n8}ҰenQdֹv*w \p0炙̋(Yg*)Jd2Y;lftupXk"GTb=U3tD6	 쾞n)zLbEݟ[K{2r`vɩԻJ]a]5k??e-:z˙wv^:pPNVC/A铍8)B}$Aڇve8^̈́Pt[99LU|b:W=:0{lHhV϶	,}0ðs_8ˬo3g<EI}a@뜝tbg_,CZXb*S#ӓw(ƪiճ鈤Fbr*yꝓl dDfwAzs6#uUMh)ۍVktSo솥bShdl.Y) C޲qb'l1c	?]䦛))+H:TwGm:ԅ {ݜx{u`XM[Qřp7hn!
ղ"6u-kO^Zm:p?dZ[$(j`7U;hTE28GH+F2w!~#Veut21{DBG?	
S$1LZ0tf;Lv2Ya,^_"󢭇i46Q%WS$yb!zO6mU;RO fz`WXOɜQ\L^xE%I9Z{Й}>^)jȢQMê%.|QUlA!.Ig0hPR0Vls.ȱJcD[!Śs^Ve `z(mfb?AeLS(0))1*}OIV򧖃8f*5F,}5e'c'\*;Rw.Od^-f1{I-@}aw|9lB5[Aۣ.KHD[`?Q#Ghxt!08bhѺVe{?l܃1ӍzO⻹wspK+7APIfpLv5dX5f\)0Ck+eR77b({T*#aF@hB.䇐#?BF8WI(-R_[f֞K%4qw4<5݄|:闉QďC\v5iDi" e{Ybw|( /_V^!R1R0gC[E e,dS$+T >HEiყ9?]o6C{		9<$T9)5pwUϞ=WlB&>዁rJzt艎RNMdx` 	s~O^v̫ GrLճ@USSAtckS{]:jZit=аy<)?NɈF	dpI=7_'81`Q1Eϱ|q,`^?ˡ8M wc*ݕ]ߗp0ա뽹`7Sڑ{$?(*.%x(yI
Ҙ FE8ٗ/Hw e0y&*jQIE(09]DOx&ɪ
~}厝hF<ދgcRE{5o
j[J.R(zj	)`%4@6!ɗ©r(,a"YCBqeS	/ǡ1\.nA99Rx吝6\P.[2 nLjJ8̨nZqSzR/iyH#~^2~L _fTJpk9VG}_дQkn4<uf1]0?v/P1K$'AeeSiJiuRI8a$hd}Cu0Ͻd/:M7k͎.]+.VdبO+b64^Ce15:<`V"P:F "ahkǐmVQq7iwQ7`\W&D§{Pu@{W=}rQ@>{g 'JYNSͥP$A.0a~zt>88>+i@*_a^	C`jHT|©L&¶5dPzjΝm)#j$UTnyn-PIC_K|dYuvn
C80pNoFziI4`TۅkWCB_l,DkN%@jӃ k%A#G8owv U=ÌbtŊ	EڻStlj>IYtд*<'5SBl9l ҅nĭH3ςvr"pLҖ(9N`XHTr4N3vpϠ	^N
rYqx|k{3U	d${T{ODT]JkÇ^Ky% |tC.KoC()!@q"e.@LS"DVQ5_|Qݦ#T\ˮO{lJމAqjܙs̙dٚq3Zus_VMAxoj1;`}ʆZ9~}M=i!&8`-egH')!rEyG6Tcc-Hg@H܍ۈp?Y):hz>: zg,;5a6(lrtØoLg|ی&m"X@'n<%`@=.PD})W|W@R*q[p*/΁Uƌ,D_H/YeH'iӌQkӡK#YF?gSA6]Vwc^'!9:#ꢵ x`\hs).
l拣7'EM{3m=Qoc+p
8#h%-ݦtO;#8k\c":p}_6`V-mtRc>򛢛U5%]wC=SbLߠ]A9:N&rS؛s0"q,n"@ )[z6,(l뙋Ad_lWzUǑ|}ض
볓 ˁd_Aӈf}dhAX/6"d>hD<N©cYDDskܴ`!UT@M	vWXD
8M`%7XuM]hQgyLu[Ǆq	񂼓ռ#6Hi%u1Ye0uABHS.$ek|ocq)_\Z68oM,PAoJC@C]CBĢ/ `!@)%i+jCsvqC(XiS1%WN:	ж"ι5*F@XK&U	
i4Dw.%ȵx ^44yܟ &)yq'iXKygꨭY%1`VwV|Y3_e6#}Р/S9:AKzC.(.͢ksB}@A1y>Q GߜO;7aL3pgtC H,!cP6˃TD|A<ۦY2Ϭ`Tq休'ް3cHb2Og!,-9pDH逢Q]`Za K%QߨFvafN9:*x4 VܳqҚެp1UwZ6?)(?WREQ<AI2Xq`^e-@RLWDnRl3'e|$~e#HJZezf^K|	cq~&H%q|bB]mz,svJ{
na7Zٝ6RT&a:+*cy馨8Ae3 	H9QE3ZŸ0H%7
倆(>anph3H^N?@>n;!CRx$F}Cя<@gV";|F/b;;PjsAέ)8byi`i3^q2ᇀSׯ=rsd71~M.ˆ:qsܺ l,8;y袢
s0 ʓzl
잖b'Y Y _V63{)F{rEO?M} I|ն)9G#շQ2R1)a"dfQvetkw?^GMrr6|[R?;fI93Oޞ7Ю"glM[\}I$&2?$}7([LUsڅ04/	sؾ֤"'NƇCQ<BV.Hw(I'K"q3kP?˚ٻ9<ҳ̻+g$ף[Ajfjm3Ft4Ttb0HnpK8Oܭa H桩2E̋%~&z\~J;foM~|]-\VKݾ{ -1}Hz+6Q+%qp(h }	]&iǀr,Buȼ3Z-Jᗵv4{tgNZ%I.5>A:	t zEKVߋn<I3B$	4>pKZQ*Ӽr1^7JG.[7By&ӝ2}}XI/GArm;*sHd${;ZݹC"h_xpQy hMq<Yh{J|HdU(nZCmXGR5]7Vv#c!

&5j`z	MOGX*pb	
YЏmM/}čDNΝ6<.k#*w?x`!ó@+4=DsA%R`@DT+F	`qÏodKPE 6(#U2k#8m87(\Nۣy|2i킍j*Zsou3P&&/AG/x4^3W=(x:HL0,ng]g2w%DCEp	/z.p#T,F=(:][&켂iT"Ms~XsXaJ`H_n'0r+ݹuJQ7=2Iq@P?fa
ĭjh17njy㋯7nBUˁo\
y܁jKWԽfΚ,"DʄdQSkybskLSyϙXNJr}WnOo ]u0r[<@KKmQ >A+Bˏ1C zλOG{w?@g p`_0ݝ<v9`Uh04'5t:ϻJ_9N^D.b#]34ͨ銣D-0˾cɱwIZc-Vns!η/Af 1}ghpfdw#+spX
O#$PB9zSBIM$Xyz&2 a?T0ݒOsڽ3Z5kDsW w`Mȉ>Ωp)Ļ1uWPl&!'@'t9ku/oIbx:dR֙AhDWXD6V{A0Nv,=\MIZ>{ґliHaaf>Etnxh)!	9uDiMКsbTu-Bœ\OsUl) >3>قCmj)q_G'"m4i+k@5kَx$ZL.qme&l2~ͽ-$u0.3cBL;dї$a+]GU%H\яr=䲐z둓(zF]ɕBS! vRn)r"n(j$kv­Rg
zaP[k-΂U\>-
dqɉ}up<cEPzٞ?(jHZ^n6YRoG"/S U	5vԀs#dINbNKNFU\0 -bAHiog
n<(Ӈ%e5:Y# [hw3vHͮx2"LB 9,N쿕X xUN]xAZzKÞc@/) 00n+(\mc@ig<	njjŬ;x칂z`jT$`:a'~q3źrTyRِ2-	
1 Xo:_v{dwm*KAD?z'Q7	͹ZΟy|
S3rgOpʛ4FsxP|t&O/C㛲^a(t])e5RX}hc0ߵpmM&30ų/ʚ)db>P5,k/p0C{E׃P 6sB6`kiUȲ4Ѳ4;4]ҡ,RG_$,jcEV|
0~AH~ &}R/LrH0`vg=Lf'˻]ޝjK-BH=0Ha3A/gIrn"Dp?SΟw0B;\F|[(M0{eHaW"ûftpք0$)8A4ׄT$	-0s+Exݽ:߄뼴	!)b²$Rv~$]rp w=My/cm]| Xw񾾪kM༰`VK&7!ZQgTfAG'لyȝ_[
I$`0
3CuN6\j?bƢ`e-'^?&mrD6bKuuw~$}\Nt|o1	0@LιaRe̔cW9Y2CCe]]iajCѤR-<.pi[sA#~mYHye~%6R;,սt;̝<r$ٔ%q`aoM[[SZ7}8.gzOmfyc֒ լTf]C\)?Fg|[jIrA4!JbHqK!I3NT<,(KI4fQOU*$	U9s
=Ѥjo}A5U+"	/#tp$H4aX0p'4RQ>wƸ.[M&{nv \t-iZ$6^]産<gF)Ƹ\`'5bi -Q- 1>,d'j$\j}:r|.pp;.U:3¸
μ'՗Hl_*Rvg7Xk~ekXSm@ِ/if!縧Y&N7(ԗ_
j<6pE]o	f,m&w9mCA~Y+ԆcB%9R sڐׄ˶sؘ/AZ [}5$˕`raA*;ݏ=+%$E+g_5{_CX8"%:DO<,}=P* j|1;NMQ@G	ƚHA:ο^4Fl˓ɦuQWo8sJiX;5ROЕQ|AuLiG`VҶ'Q	CS@7NfAǍI=gp^pGbo	'u T7N10 >"/OA|.(͙1*1bVܧ&^:
r>Uaj1QdմZ7/4Ugv52#IaŢvw 1,N8Jzo$-nn?nI+m<MK \@J7_%dNR.E+vkK(n[F!UpfgSݴŸXB'߾=C(Ç}Gs+4<}FL/&d7qԠPsUI=ulic>QrWʏ=~ٙg|ʴ+
h^g"^L	<C\ZE}0"FC7M!)z#ՅppT-Jn4s<U%3iަNcZ"v W0J`b=)Hl3^*JA|qf*p@:
a%Tdː?˒z<WjhJC	8$MM7S0by-ː^cЀ>oA`t&@$e5T@17)xq!=0a7Uj{ԅL÷vW{fJEO3<g[!l#G^IZKЭR."GS1W%qAҭ1w\͐W	g38 nh%-iGƄ#eg#Xq"+ƑRyw)~#a.}Qq! 2blzGK:b51 wSncJ}C&;,'1k6V)&ձR/Ss|ųo1	/4GB@ئl ;.XBف渚S1[LX.c|%A/y1,ex1_<mgՍôTӄeAڝ|v/S]ݩqx]a*j[Twb<x14,N9KdP0D>6&qօOF,劎؀3?,LO Q;#);|k@SR̩2q iKbAa@R$AwYAtc055v`@~SYxBZd]Qf 9m9;%T^tppZ	}LԽɍ0NC:Hc|l +A9}^N0DJ_E J87[Z%eCl>C,;2vCٿLcWeU{QY=O߇̬cr:08Tc=expV6Y%˨o2&(~2K!/MĬXNC=
R&Qښ`|룣%%n?2W7s".sr ZP_{6 7ȉi)ʧKd_0Ê/ȗKɒY:1k\O҃2at ^-
Zݏk݊[IY_\@i˓plǩf=T1>a:c0jrG|IO(̰_ƣ)TVx%bW	%Ag:JGO,nl]pV/XOY]NC2fK(']t(Y0I	**(>x$I(^ g$]SO N6؅@6D Yݱ qPBQ61nL@ж"vg{TWTM{O64(ч@N\k҇r1#ӄ6O=vwZB(U R/ޣ8d @§#/k5e]lW|\6HVSRC$	$leO?Ix9r5+AIs73jiar)lw+ReC<N/&+dix2[+mZ<m-edVz1X uWE@1-x{~)y&|xd;yg؍2z&v6 #xp'(e,r
: n<	c-2FmtTyt܊:焏R,6 6.O@J)עvĎXl$H;6i17	<I;	Tr͑}-VlX$o;[L,QrX6buU[_@Lߗ˵fBB>ı3px:@Kw\bih2]78
BB.yA]fH&1Ռ"٨- Rxaf>E(xea+*YVقD%YF	i%;T*323EjFE/*J3A#zD@fg]L;D]pgeLѓ3}&jsX]!Q5B2GnҲ}W=Ģ6fFQJHFpNEhbES&[؎?LFE쐙Ҙi1gaf!?1D8 L+ab<_GSZ>4:RD>t
Ö`O)dY~NJteFփpv1:{7]ךD+gk&IA1$*mx[uCtH]Y^?V$$V)|ap&҇oY.Q$P$zFM3H|h<=#
aJT
O6
HKF0HHčd(G#;r"%{[ ^;a)rz(X5<I'H'&Rk?af68]9Ab\Y.(ܸ],u+་7KBdCVUy9{b5\%YgNь>FBK95:T3p_Co=nR'H,!E|LʙՒ.jC"SZ/Jp)TouC	 }׽Ӈ	R{W&6nܖ@ꨍV<`Bc:5h'Tqxr+2CVѬ`QUv~l11$(mMHuW|gf#@*i~kb$laRݛku}^ڍ~)gftw2ㅫ/N4˽Y@8Qrbm%G.&³-M sU5juyKۂQvvsl=M؜GPNL?p멄_uį4		57Tˉx	  2Cy-CE5fQ"cr 1Ecė4ˎLKJܙ:{?ĳJ_,$\ \LWOaY2dG}qn-J-VE9̻@W3MZ&R!Fy;5|[CBXoHvy1e}a-mШP	䝇X џfسQ E H߁gpam:G*ĭΐjVeY:]3)okР=̀%eW{4)h>_Z\8B$6*&%SRy	I9B*Z}ÄIUh $'vǰrn-q2iy	Efp3WGNԄ OT֙OU)<	əMf7ykd4
0B˦יfW9Oٱ6RqqkHm(g^Kؾ| t.i/w~gb|$
ʮOIt5:!f^w`zI~-npuR~Mٓ]#k&6:tTd)yfќig$F+!UKI u5o远݁27a],bk]BIrGxV-B@\A
՞'p-`0R[ٽusj}n[:pY͋Fb1xJ!l~UqoFLn& ^AF]!(݀}Kj}մ"lޑhCCt#*o8 	H5=MVju2]MyS!`k5-7ZwdΦoL0's[Y#$ˆy=D@:!#=Tǯώs5&?udJ	HjlC=Ї.m4֔y(7.̂B2-uB];i`p!FH03'X'MueaZ/rһ/ҷ{<BNC gud惂pI3Ib(883҆g϶$#Z^LTEV萡n3hE|[\x<J HQ8yFK:Qzjʀi-8Gr8#Q8?f"Ϥ]3EkZ>}-|j4[ّ6?V	95ޜ"	Qk$\	ϖ}az)4&**M{=En	5{oZxtGcT(\sKą

rtcuT́ю"#:8DC<|a͒
|N`Ҍ3dm@`]-#؊>"~xڇz XNtեhgB'#94dN6F${k6v #G.+&2<}S
ic)Z_ޯY:љ!}F]m!FM';Z+9!-=P=.NHQ,P~K=R۸(fz^hۗQZNa*33y\J$(ddϰY[Wd20+:DR,UO82X(+Dzq?IC@e&aQ'ctuM.VyH=Duc4ֵX[ME;ܯru8\@;"0 ޓSd])Fq.'EJ<và4`TFM131vG餌?e-2FYpE:Kٵ	b*AI4=	CTh3ah\0+.׷̢\KP"\܆M_A-a`=chmB/WwB;_{K~䤴st(D@k^t#󥛌k}"2`ARhM	[ ^s~ճBm̂5
$w p7-\_rR$ahտ8g%7q;:nawu=Փ*`qҗLۺwJ)Y[+nRRKD4zU')F Pq(L1IT2B?2JU^*:4hKU@Ue~߂2(-DdjyP0p/=z?ݩ@] _:)	f{z 7zv\RR^<0D&N~ԷUP.w~
*jz('/Dq\t|flx4CKO#3-|ڕ+, o<t!|{`T	iL*ٟO9rGzK&F#+'Q938 ;䢑f1S[q݌}X v6(a^G?a-`^GkHAez׆` ,J0.FϞ61VϠ|>tP@|EJifWeܣDΥ62GXrf>R%eƮFQٕVRN@I;Իzxnu\ [E4O.i<=A*BCVD0DC`-\o:YgKꤥِ_x~WyZ6r
8~y`kDӏ47=8(ƔOhnlM-$z(!$(;RV,//XH]Q
hiCa$@[=
lJiI EwwG4nV bݨƖkX"={=2t:Y4qee_+h[/Czb8ٳ1gѾ	YڛX6^qC儷ALdb$?cep>c;mіuшawQe갠?xJ.p䨢OZ-G;8t[L##]Hŕl?'-~%.kGby-f6sm(g9>קd(?&n'HvգY7eIu9{ƐhltO0{JY	Q=S{K	_`A(GG;@A(j(¦_V58mF-Фԝ-+rV#` + 8Dh*7rg|85t%o^OYLubi|l?i'UKBEvpwnAQ b{yv@~jSI_j{)R?JՆm瞌$nl m@<.r1IFT䣻vüJg^\WGJ|m	c`JYі{R߹SmAr-GÂȘu7Q7( *Sfw (ӄr/2+6`xA,S硰dLRw';4`<ʎCޒtH]ɂh 9M=3}MIts(aVSgvc*0?NQ3.vH}G&+3[ǓCH3P.X۰zLޛC6>%8Xѡn="&(V^5VE=Nxzdx_z<|Rr <xwW-Z`+-6ꉽpRBqhVǯOn =$[6M]$6tz:ByH!9Ƌ:,ya5_X_{ n=<^<3	ae^wBu-&b/4K3&?') .JlbG&6cx.3Qн*XŸ@X91݃}_ :@񥆫;֕(=4_oPZk<v?_>՗/?vxϞ?~_'O^<~ǿٟ$H{Ǎk_~۟8_b?4X&帞֋w?C;Tw.r{Bw6y({{dVwvdQs1rSkۥr=l22iPӠ$	h"=*N;CgnTj,)6g9{g<<?=>]n9z{~޻9Mw
s}SΎO߸{NO_qGa0"~O{Wߝ?zv;9:{MG_zy.Gvw^^\ܸ;8y9	;7n<jwWG7oN__]{;_! Sy)#:BMݜ~}q7W\-<y=rOb@Ǭ~|s}PCczm~}Y	P]?SS^+{<nfn=%S0.N2_딵(	O/u;7-w7M)'>ԙ;;?7]NkNҢ9`_U7.Z=훓cs{7go^	_]//_]+Nqg\:9Bc?_v{?MKþdyvJ<2sћs{oGD(_|}fgp}$8FpşO:c9ʝ/Ku08[vGvGcg-|nbzkw<[?
!\RNw{oP[-&	"/]^NN^ЗI1'bR!Ό.zk#c~7oa/;K62˹eW]]se7.^2kJ~>`P])zո~Z/.|v>wn>{}^ݏWOÙ؉܅ykv)D>71[$1[7WWܺ|ѸjA}*VF<HToޜʤyAWղmP	q`V^:/c@3JZm\31ncM>iv&zH]Dl	4T$@X|s͜|ɩq[Ո:G` 0f [4\@6x}6`oWu؜cMX\u}suq~L|g,Ϝ?ݫCqxڃ\E/#ONߜ;lÁ/0fJwfGO;yutvλΏOiOL昛_?__2WIܻ@
wlM+qs3pc]1L}7^{>:k"%wT¯WGoo7a.N_ff}r5]n~?g# j9uUv큒]Zrg/rWC.û1vsnoQL1~.O18_q
Fe3wJ{}~qEb7rDɉ/9ǹm8ۿЙۙ5;ewsݸ_]Œ.pW>aKԳh Xs>l9޿{UW02\s%bL<uJiȺ#_r-D)QGqp@WN9[vqӧ߫B~o砜;|¤uЃ1$M ΝKϡ]Ocy]O]_pstԃLP}HO{_.3͟'/G\ԃ9PIxЁ,vΕcTʟ1Ѻ)Yw88?)2+W?\lFn^v^Eb##]A~j@= 7f`?lХymCCO1cahyc{7y|~9@{/?pW? "v. &!h29gnH~S-&W	yeF?^p͍CI~XCQa~)C }"ΞǂBMJI㢌/n<dxB_QW&d/Rg 5@wÀucФqCh:DoñzzӇ=dIAW!uoI laNn|TjZ.nh|`logn^fəVYUcxr^}!qPe@QDoaryP6 z}  d\-D`aLSAr~x@sdPm݃Cy@}͑?ֲ*UfKH{?7 P;WD{y7micvܞ圌e({;ڎlb'[rfƋdfwlQܚa!>4>xS!EKe0b$~ж,ߋ/"tww#O$I@^SL8⿣@r	f|m =&Cx<~-Qێat\yۼsBF2;"|p|}ȋ,nvY0jݜWW{k?_O_c={3wɃ m6og</`C{$-+P/zik;/쑨X9^;w`#¾s WÂy K7:	j|Q ]Gyvo8בJ}jOζWKi~6'?g_W?e&?|I^67'+	Ae^ʣAyr\-.ߑl839뎐۟ekٸYw1T]7d8.e_|8@rUP#npu@SO;}}q>TO?!l1r|P45iyqA	ɷCLNs,"=`޿\?wCLG<3<̆=d{.џ	Lt@/*B'͝9;zQbw PH?[ c9Q))8)v;p<ӑ$?/铝CM9O'/߸O;k?}WRQKlzHaނy=D?O?(*{9`avMw~;M[,_)C|ۖY()o('H4y@fur_RՓb^Ev(!UAIQl	xwL-3!wv]pEuL{jF#gn=姍~P`Dp@ԧj}Y`Ү=@ؖ#5({=bd9.gL/υp_rQ>ȂA{Z\@.K{w<,1wjFD-c}!]y4_沙aXHfG&a[+ܻ44wKl 󏗹R5@v0|^wG6J`0VqI0=qhDדvkʤㅋ ^e|~̯凪|3a;?v}W.q7dYq6òޮ&*m<_[ܟ=uj+=ud??=y/_={xœ7guOO?iOOOz_=-~2H!]c#?0t5}[t}_|?hKT>ӧ&?T?S?d'5ٗ>~	Iߟ_Oz?ث^S?ook|oc5u|O_@?U*=M~ߟ2A'>~?O~M~}ӿO}?>|?>|?>; 0  yV \k{6W Zή&r/KI;~ X -+u9 /䤻uyvErp9 l}&\77.}nnOs}|<}xҾ&S}Pf	W՜jyBJ;{[{(g;϶wg t He:žaB{LZDY(X$K_CE{QOl_FI
Î'l0`|A7Pߟ񷱌}L'>B-W<{xvO0dQ4NMW5Uw@<jD-M=i&VuoZ{9㥳O-X䁈膄KwIȃV*ީH{."y-(3%R6&8X_@pRCx"C5T?ڛ_N ykM>Ihny?޳X6bwAٟ٣Ѳ"[љ-Ǆ}m5sfeknnAB[-6'`|xD?(HRBy>4ar8KI:W̶@! g!m'BLL+ q/_ V0_.ĐQ:@7JOĘgfuVXfQsJJ*RT$oKTKqe4K?lmo&4^EM}bPp%@Q`z5m^SDɑF<4#sI{n.3Ri bO0d$XQ("fAqߗq&Spp_2lsŞ"PvCP2>A6iMNڭl6k5)Kx<ogڵ<jKՙ} [&گK,AX&x9#OY]t87ã?:iϿv'S5~|wϿw;^9}d$h5?f2}_VxEqs5Ɵ_XYAa}yx]omй%`eעfc^!ܢCRדͣ&
?c$K/Jf_x^#<Sv fHF*ѕlo*bYJ)X0.tp	Xi>))B_z03e"<k=;x[Oal[gې'!]my<W1goX^e`pI<u1H"kꁊ`O3q|
mfSɦRu
g]0i0 "x8x4	`z?rL_رmڙB`DM-hrǳ?VΠλ6<7ly!=wi)4f=B)f8XݓT73%~w=`?Gд7`ym>2[Jr8Lu۹JjVu t
 XRt >bJnؤY3kO:ޓYwӳAw0`4. y$.)^^C|}
@c(y}5x58Y =pchE6|I.'￣_1[݀`uKb
*sO(nz>m`ҰF,ޠY!|c{,/L!>0'\Y߽g7m}~akϞwlno>d{Ͽ/q&.MDSaJS9B_$[уK*0HSafk@/jT* FCnf?g%Aj_2߷ֺX6k6x`6_k [k{֎v)~|k uPN s淶{hp}/qv߳quV%b):b=gFoT6&V{+Ri>knh$R˶7vk4Q Ls64ݾk!nfS6㸥9xD&)>Y{S΂x,ӈc|r*>DJ}~"`2Dc>Y/gqTxWH
vp.`aNla0Jyn@H3T",3GI/3C:Kiv(/=h3s-&SX'wZD`6"/hXѿ`Liڮnsk4類;{ڪzһ~3$/[vG9f#)`=|2(nOEvQ20;ٽt<_1]ٹ>/'B_ebV`R<AT:-v3e,VgQp>p$Sh HFǒTمU9f&>Rm&A|z,@PaP$bH9@)6Unm
Y42`JFؽj@@-eAw49i.R1IP pОNd
Jm#	Y'T	6k(fLKHy$Ȗ
+dKL{s~-:c'&r4>>LdMmq5׾J&4qN-8"Y%uy!Vw=B63q9 B.P+Ј]oL p
9$B0J%2bcxfR!:79m{\{OE0Iȣ3o,ߨ޺R<L6,Eb#Z"M0飒Eޕ=U?xJ#<dLݨfE-6fhHmZ`K6SZ$kG&Y7F'wdmWrĔs[591-O+FNEJP)AȄ +*T6Rv<>FKO`PF0cW K cO%4<{L^9x
(AMADHGZWJܛgfiD2CIaDΗ<}dpXkSKE`\,7tTM,3X:ƿg 9Kt8/EFIYjң aϜv%0-\+2
tC	E8:n9EdEcO7=2<C M Ɠls.u h)S$%I%J/G:B
~? ^;`@x~ho ;z9bS0@P8|%L[	 CQ!,GJu`x18@3!ƢhG(eZ36jn,D>eqHShx0F,bVW9*NWlG ^ ]0vm%i^8P8_$LЁEl˳u	<bb*Zjt3fAp̰IkR."etiF4buvM{
r
GCx"*$܀iL\S>TI篝]\aP '=qSQLzhmjT	2

DK. GTErA37492oэTg'z:*9ϗxʩ<WB0f MӼyz)~t	М*H õA&ILҎ`c@cG}b!+35G9`τ쯸
<lYxU*EbE`w>P(a0j?k Sm'd27* ʩ+&M0|6借7->trU ySdQ8eak
o
6!Ll;̓|r7F41	H Ba\^yIhNy!jF]@ZM3=fGAJ8A478!ǧCo qLzR9zs*K0+#	7@p!;w
3Nyv>?z}W-H7B֋Y=1ΐt6ߘWoJ.6	*9Zk@Z\gݵEgG&Yyqrc[9QP#1(^:=pPx4M(?;]|rt1`9;iDuPaEZ 8w9)D|LA6KrR5	].l|X8Qw؂oR1<O&6ߠR,-6s27M Yen9EI0m#
ʘq:Q?OBSh$ê7ATG環y8($pFSsU`]M,igJЏUW'BEmm`&hd&a;ArfNM28#(LG3Φ\rkU|u7@TA/Pbt߱5Д r䕩jq3%ϬsJ38c ev>GdyG|*Dt/v%.x[4H krgS`m(R,%:Xi}7mASH*>S'@Ǳ-F-X(ߨ$nwU-jϵryY-n`	l0Ԃrґr'o ^X]<,&D}HZ&H8nZR3[] CfJUlF20kt^fr.({VLQ3cU,]c)QU<('hSDb 7̹$u\M$}`jy˳rhUT,5{kQײ0nT`880"D[>+߄7U(+ze;NJ3S&48\Ơ[&yL=,-KMJXܺwK8%j&bxrZ긢eFٝM,Cbؽڬ޲(s[[0-l!.&;dXDV(A2 9V-IxcpಁhC(P\m>36pl*AraùD1nǗDS9_T)NpH$]w/UV6RNuKYy~QuQPJ@BfAW08)rYQڞϏl|65?fɸِ?rn%U5/,l)6|tiŜ9/in^7@i*bJb$->VZoͮb1ˤ G!8wܝ<]ɪ!iY:jyPј>F/U\ET	J'Qp|TbwW1zP`bwpuZߙMvTK uWw]T*#v\$\P8QwG*={dă[" 0Q| 	0Y#-s͝ ""gDf0"YW2zzQYuTe>;7pYpO+p 5јU
'-*?I@KbT5|>/0v#o!|=Cv79	4]Hچ}}W4?uAMƺ}wY`lP&/:C):wQo|a\6/aYS2vi~ p˄P  ٔd;lYx/E01d||?@'`Bo~Nvk.j	^&g\56פ`]bϧA,<|"vjhC
?jѭl϶?0\Z?0Ǽ5&)0YHl/0lUqG\3}GhX1c!\݊1SuWI`)UX0 pa\/wvs}xM$<+YU=_uz·B%?̹ ; 
7UG	j8٣8sC<&0&pR:Tllϟ-VX.)i<9Y]'VoξB{i4J0|'
p [oŭ<ݝR?Y%J5Ǯ`$,Ѷ5>%hp| MK$?\V3ȸCȎ	;OhN
}9ִ٪g{V6[\AphP"FźD5RDz\_ȺVbE˿3kexg%i>B:i̓h9SmO̾u+/L=Z{p\"XFBuR}w'&+"%7$hj1CBEtJ3K݅bɭ:3#X44,"F&Z/|`m&ӪS(,:kL睙34о,ol5>cϚ2RNB`|&uG/k6Ҏ~Z˹\e˞8i6a2T}q;/b6,BŀCa,̛&leQ${z1b0-aF͈F![91D23Q=ޟ^A,;ϷH4/AuCR~yȱ0s(tـ>in$\nO}l\
1iTjO6	ת9K`6|gm@ $guQfx}-zCAKkzx{2(SU`jZc%oI8n/YvܙHB֒	nuB	HNQ?L'pL_E'8fVT&ۇSΕ>g/FUDt\6Lg<6,x3E1Ym%;88=Eo ,6̂aG&xmeuYaxZD@17^\٥އo5M̘Ic-^ζ{O>k]!+L,	7ʎB5%Ն=]F+|G`u.W)w|(d	X;c|ׯMy}۸^n5^7Ob	oT?+/@ϰ_
s`ĲF7t55Vm:7W-*D;U9!nbaq!ß5@;t*|?@0)@Rnѽ1SIDpkF|IY,5Hվ'ϧ#c&U\SqĻ/K/0F
JҮ Y+!q7"LdN`4SZKC/]0 Ŵ肉GG":%5BGgZ,G=PJ0K%x50:yoİ):8`9N2:8jv>6$Ki0 	34MEc],DT4-0amGؑ؂ O`rڤ.=gfo@Ocy w6>clן&{ݑnj`n&a@N/䁸3:NMõ s]?=6Ll@11(
l*<l!)o貓ڧ>Ƿ:;b,4OQ	atl]*ʽ}:)mMݟO$XJeA J=CwŹncXI!NX@M(ƞq}=K,ZpMg</3u^33W}1N6jS*_n4ӵx,imRC{td`dDZ	Vgζ.N%xCC	^:=+BЇF>"Jws<X^q$߆`4i6Y$徨ykkkv}LlegEL2cR,Sh]4_ƋV F۰[Ӌ+#鉊Z13ĽJTugRLs@'ջB:}Kx*>A?׃)G, |~gz8%ǹSj̏6~VDGDR@.I@D%2ps5Cw d	IeA34~j:^cנf\Lbæ@5Xy#_s>CPmW7}2ڈ!O.?P#_V$<90qsXQb5m&
$Ei}	ᑀYRןN8x=Ɠ_;_lREVTt"ΞlW,`~xl2k^1ɑ}H4n+'&
,nEkNNE5);4.'h\lRIb, ħ=շ=mZc=DR
N[a(bOB s4!Əi"e䅗}E0W9|~.)wJNqoڡd\py<l[x,zPWGB |`+jb>oJ%8[
&fi$tn&'gÅ+><'tɔƴ:y.mfR'y+9xq{aR qLD[A~#7X1 -	oO,bؙ@^ׅG%`<NXi5hH*\.aCnfCiΐ?׿$!HhSC2;&`=]:=9h)x s9HBOzKj'G)3ɟ?e	[ڪM!|sNљkp$uqLHt G$E蘊_ uo 40TFˆ/B3b4VK7c&iG0xXs-@9/lZrŘ,gmFX8IAuX+ϋߕ6착&auD& {*OH%pZ9A@ސΫe᤬j.px^\;?eTׁދX	2)/A2W7Xi@aEd:${,8IplZIletʌJQeļ]MǢKco}owC5UG#vX<f!ߗO"J'mφbە[ssT7(?qtZم'8%ʙEyygS JaxÂ}|?4 u9;0E
ԵDJrJj7=ĕaυxwҬW,.uy݆+9Ț>B(S ^xLvm|`9;,De%z$'hZUwK݈-L~9u% G:1߿{-E7ii[^s!sWx:`؁4mgOՂ8BD[aFWzn*tO0`sBZ@_0b,q{ӢVM2]XΘE<!HرqSд =0_62P\KPaW9i)D#xҶY/CJ[#:bO䁃qYhi
1e=NE~cǗ`6IO0sB_>?,k(Sp@,bxz]~HhQϙǲ*H"`ϱcD7KNg>9"iwN\Kp/̙ȕh8"zVwHjBTߤVJa|TV	gG1a,RtL0Y6aM*r*ۆC"->Fxȭߓo@-ـ; tqA+)'҄CO_$V-Io3X'/~	4LtE,	 v19$¡fjKy8rY_98C*|I=Jsv5D
yb+é8֕]'9%n$')WwZrRPDSNG"3*D/d$*n<a",輆EI?jl7_X[hˇՇYhE;Cvp4~ۇn~V"`&\H3vhڂ?5Z >j`N{2+&XD2i<b"Xe55:	Dnz!OCP}渦uuu
֒L\AP8"IfqKH;VXgJ%،aЏ.Ӗ0Rc<f?>"#5)`Ӗ5֠0s\(f(KE;c4(B8p	=&Uw@І,ru5ƥǹ0PG2y9O]$ǖoc)霐@)[ysq_v`1l(87{>b;Pr))š.> yf.*vT'VxY(#ăo˫%DM{͂NrOLuXA5D=}}r$8eGk8_׶E32YC1ˆdxFxKPSm)6K6JUDwߊ|uyzjkd,X_/LGk9=7un±n)!(OɊeVgڏ,rXDxȤ9
dƽԳ72WpxD 1(qב& ,	̅12ӮnMIa\0zt/  s%C߆>b{:ò(Qc #xabQ3z\h/c*Riez6GLc`[!hL:bu"|_O
Hezs*NTZdTEsy!Ƶ0Ej|;ץ1LXgbEYTfv^;"*;pY`y ʩM28g;)d'Li{TK1s #qvuuQa4*We(V$i>RFnNp6 8=}ˠab襁Q#jqlEp9&
><oX))ɳ/lr!IGR$wy Ms݃PQk NS Y+SB͆T}T:_Yc:kZ^V7~ʙ` xOʞ}= /jg%Oy-~.fNRٲ?d82\
EGn1P7?WbOi"0&5.;"~o'i4\Xp?TvF"[0e.kKi>X9
|noBx7;ΧRPzDKo5)I	%iNǝ<=mp%WyV5Ss(>\hqՍ1&(`SnfUgDCS3v'nK'-DsTҢ΢#Y[xM)]wƸ~&cN}\L2Ѣ_UV[!09ZL9]ы?|řv.;PR |\rM໇R٢pzn.+",""9i󻒲1=PWF].`aۣiZiW  [W@YuO.ڏ`Gw4`ڝD1shRXHJ!L`$^Mz~z4hHpgJ[Ѝhn/>KG5vMqŎBp.Γ[:EɢkDPAd_SqJV1DCw'##Wl	>I,9GH]%&=4`FS?Wьx,p[|BFGP=[J	y
RkH 2N*6t𛆯CzI\vj{ǣōK.]iB:"՝<R_,P,L1EV`c
jtXߖ\)gk6_)~hCy6wqtp<zJ8	j[J2*NYG6L\z-i7bH9`SK}o*ǔm[2H6tp)(Z"AueY;FP&/`yp͸+$ |8B
ՋS<^+SUQ{,5-+mQ3x"DYaz\mI*T]M\`9@1{N,A
/I=`1~m瓮;k8oqN"b;Fi<n`)fabN}>/FR_T`bجha_+_Q<4e24p!W%
@3EA&'H/G +pLԸxtD.fͺl%vIة.9! 7sHk$hVQ1|EH;)8à9XL)}O|mɣ$0w.ښqO[DX&w4t	]zNaȄu{<'Кab5_#]"an>!^5g
U.*,ЄNZi	})wY1z,QQ\̈́7z_2vD~Lג"Oo_ш6x>հY00"`p[uA˭2XAa	W"'m<=ͭ{0q,L."\bu{0(>B38FiܴBBHO(![=>ф*hi?EF+ S?:GjF 9"W/?d>v|y)sn!+H-2:*4ӎ1H&~u4i4?#r^-Т,u%n5ro w7~9Ǆ9Be%A0F
?MmP*HQ&cU,}r6nSaD0<";R/)g<&t!%E\mKa6ʭ3
 0dϴ]؃u^݈&
aI`8%Cɏ/EBr4yAe2mUޯ'ZG$7C\S'X$K	Y+EؙmUx0lg뼝vf=ra}ֱCJFOBFX̑
3\d4ɫA:f>nSJBVao|Pe	Ҡ,X.1.}(q	,[sf+WWDh+;i?	_FULz n$*R0Wa_'|BƸjwQx@+)
	C8]ĉl@ak N^Vp̖xo@OF2{H&O.=t+# U[,݈Ʀl-a׍Wy_<P*G$FlXjg1LsBèK!u! -!K1q6:hj %װ4r"U%ϮgO!>EsaX=\M1'⟜m/V>I>(\/Yگ^$=#*rIh[&V3vƸ,ݵThiŕ~><KBa~^Vfug&ƒY[?%v[%)/.`=ݰWOS<9Ph0>p-'MeU@zaon){nJ/%a]9؇Y_"vAWW"\Go3 އq-OEI}:@c%'vb)%I(.YHsJk cTD_\L[e,g~7s˵P*Ky]@3$Ju+F*䌂 7Y
& "/UvP	-lN	xL{!gg`CG֢n~:06Zqd|xZPjq0$G^7PP^ zjiQFe|ͲܕO<
3PML0Pr]ZCyk#?$ό%MM4`iQ}?<g3^_чD.5{"&Js{Nk
nq	㦚*w[MFjbI!$|Rr.w/hkQ@ga6E2D
J;ץ֩!t`ϸiE_m-
vshDs'SN+=X)8nDi|Op8`LD^;+E#~XtqqAZy>F\D3j^5!Z+Q 1u·Xʊ{<jt`Ku}=f26҄ȯGq\bD+qʖBF	kfhKc*F1f.D8RfM7R)}Wwb~sAhpSrb8i"/l5|x	vLwVΉ܃IE/(ŚOQexˉOFK,̛m"NIiTXGH=N ΈVL8_t7 ЀcAt0,
gjy5R+@:	^n'TQY+8tWb#Gjl}'j,9`0a:FxaۘbpelU"΁D:LXBA|oB09B|!p7w4b$©^yo7կ&NҲV\ȳ-O&+Ԟ>Q`c7dG4VjP41,}5(F,g-ԟ!|0mmg<r#դ(+8Fbv>#1;Z3dm#%0@..@)/("%6GUCp8aS+ڝ&ۇy=+m-(^1ը8pyedSf4b	I\qS:ߖ@:y2fTZǔ?cbfVK5TnY	+bzC]7\NiٺXa T6[vs崴e$Q0qpalOY-;x`CB<ȶF5@>~c [W`C7}PC[2a
(HUS\XWW)D$`o/k+Py53^	'GƹhdDARwe2x]oM?."dCâ2zɕ$w#3@J`LQo:⁚+iS&Áb.{j.)/bCa9[2pوMo@>7XHy2f1[WN1е6)A&\p :[ҩF!(qg i(&[)L](gp9*$zyH2GRbS7aeO\iEC}9J
5qg枮oQ6`}.u7BسGH	P0)|N	y1,'Kܘq X*U_$
azRw~zԔ1	a| ZP̠˳; j(˟ ArFӸ/+_8I0|QoD-Fd_m 9=/G!L	\K>$鈺iMj~/
ЃĮl0ey{:iJq*acc|\uXѠ =΀2ݽ;8
_xdZӁ5v_nE~Vbas桝gLX2~?aK"lɝg].d8P1]G֞C[<P]3~e<1c-S;NbClqPLb|d;#WGç8ɞ=9]Yo
~蚣AdEyԂ[x(vK!`o1$$ΫY@ |b%a H!kdM1ʲ93q;p!C`	G^=Ξ"d4)yWJC/ hKH|!^m4>iצf'+6,CI_Td{03CVe;COw~8}rC}Eٻw|,.|"Th#影x1GN:VX9tww6͈cA\"".Џ)\ؕ%0aw0D!Z:	aշ-`zQ"Sy^q0@KK;NbGCAf⼫;j]l^?Z~G`aBOgtҥ;Z2,oTQ"Q纯1hw	HXաv53>p9$9eܩJH";24b؈wa0ǍjgqeLlaE\ǣSs3`&{w%v­0),WV*8ZHcx˄|[!._^p(BU@+괿Mp#H-QjnnAW a&zdspˆb#JMbi`[3?E>FlCeZqg?n(D>r=^ӗTU֣1ИaDdo[".L7Z)%J$vv_9UÜ&͞-#KcJ㝜+zmJRýO_6n.ZyI(`-`Z6rhށL[OGa>Se}ٽ<?O>/qo}eIi4+). J voxGMFUu5vtLMCHCA	 OwՕn(.+S L,JT^,0[VqlyWW˅y5
	h݉R5dEQ;ia: >(y h[i?Gx|_SD[(#'3Z$gsd1r/, 634Kcԯm{z߲(%1\ Y8*QFoY+2L950ZP;w̖I̪0!u[q]OCaNi8EC?XD9N!ɎW'r!h\7@qpvj1;_-I]#e`h%9RßzrFQJ7AlS47Ґ@Ivaw~*RMWtbo~Q2e%]TOu{*L4urfYotd_2_ն;
ڿD0A/(5
t1\/k#MyqHepvAzL
nN~%R</`TPbk~$\9ٕHz
uh9>5k8B b!R/%pakC1;w[?>Ƞ܏f.]b[(-;ķ*bAiQkUӐdF,d<Bm'uȚ}{ rkXbXpZD>/<E`~J	F.-BNb[t
k1(	ߜ05	d%V)T+JGԢ@6|%v^%}rd1L3\uZpqu9yrwi)ėV-#^zLC`6F߀B`MCW-Vr5uH40*˨5H+A^t*j}g5fw!U-'>'hMI-S<@s7)6SMn ǴG^Fkv|Hzr#pCﲬe|*8XځqPW{{ Z6uO>E=mIF+եәgiiߗ,%eQMIمA3LC `k,^O?5ʐ_ZkzTt	tS33oc(Ue
|6^V1!{B	:k#	ͬǆf\>&mJHÝ12,%F(jLh,ɑFtA۰fل,PHՒE\x։8'[Dְgn=7wߝs~	ג#Q%rQj	gq  7/R&?Z&DjDvJ5_"'( <#j>8F^hLZym-Mu娴%f&#|bQK*`oT˧c VY<KYX16h=L0lh	n	vpgO>S>D$Yhcd0`<K$>	[+W%RW*c]I9Kd 2֤.Xo")s
z4nOzVBRhM
ĖڈR65Ōip0d.;k/-`9G‥cEbQp~a/訤^)nl%.eU,-!T3}sa?Oö/LEj%T C(RUcKYxKNA<vX(>uHN {( 7SY6d005{q<~?uB&i _E?@+5O1X"n~΁_P9L烻TLgh0c#jYxRmUA81)jB+1x_BDjdlf޳^8~`cirk-tOϛcT.ꉺx+Q){77ĮlvݟFT$]5a"@WҎFVUWg~_xt&^g`f9~MfRIl_0|#y9Y:7B%RбQԲ XbyCBUC;u'xAy"xpބ&Z(Ī#!d$'p*};۫y1[L!`zeTہ]`e)8D.hVH	 [ĚÔSɛ̰F*Z$[W[,Գ+xd|Cvw۪EWY9/Q럸/-#+S'E:g.Ӏ*Ca.0>MhH>ckh$ΊG5"rIlIqC?zC
*6`3M/Vı؇)|q:=wu3CmEofa8SGpt%H0+lz9H*v*SЇ2qY/*^XpSF䁌-?so[5\e:KE1slI1
٢4v>4DM%R$XR!/NoFM3:ߊ<Y*u`f@P3zިFۯwE-6ޞ!lh脹3?^k7Xxsr}"s4\:Ibսdl},g^'Z5ܻYcT/'S[f,E;o|#bKI=FaJ`c"j+A0JxlO0}eHOVo1WnR{ E+Eb{

×'\F"}OPAQAb4cs!2ݼztFnčI|5͢P#6H$#Ha;$0Cذ=/ްl[Q|EY6LPEBIO"gL}scoLow/*^5T*ØmR[z))^hȍHL]7:%L^[ SWqgn|U1XuKYa:kz|'NiR`{<Ȯ %ZOfvvǽ
๶x'|dwt {M1r	&ݸqV
DQni<	?eϿ Bk1Z:{062MTe"D1-QjH[y=mڿ;@'}yؔ*.r/O|]pqt362N̷@%$%ݗeNeLkJ}_||z} RGs5[T*9ӣ	Ynq$re+8G%؇\H>&Ů<
NA%^#|pb34#0N3C\ff&꣓Pf9}^&EoKYe2%s<0@#qYC`<poGݱuY;/(/nKH93Ȕ(d5vjvȄꡐAx)TpkX a"kv
pʞF:bep(Wa3}n-NSf<>ɘ$'Pǋ1hBpM]p6zz`i1^xtBnz	>a{l73;ZOHct(-J:1<EذhdFCtZOEE1Qud;l%{tc9b-)Օg9]3?0\3~L}-!"685\"4*';Q@yIT `ĚY<	mzs%#1|-^ɮ~?*3qmLp7SSeƳ&sg޼B]2!B'MxZvZo5"fɠ$}X:GjobMH7>)&SF,)pඹD7@⡰E1|d(2ӎ8#&rĨ)Ydi_IJwwBXxJۧs%pX eFS5Eu{E˘\Y#D.L-4qXIqޅVV̅QJ{O΀.5QFE{B̻ez5BU%LuDóWّ<lÈaOa$ÞQ~p,q FԐWE@x\*Z{.V01v!~
Cj&HO;NEUZ<7,}S968 epj;c+)C9%RnS3˖'|SRm:Aj.>)qD5WMWW4U1֠><y~2dGo
0,c485Ǌt?DM]=]U:ڨ8B!hYBJ%!b:lAQ(8(2bzRp,-]0kY	յa5bfۃ<< /Զ!sE5ҿw@\/ӆbYBJh PL6ϓ<5c#UfF$t (ɏόoKw2hDļ!Ali-^֘4 ,cEZqrWŲpf`h}5La@0X?L-!оf*cZ})KPk0ƥB?o#6#8.Šw!4ٝ8|4KNƞ4`bd;0ZX.|;T6$p@1U01"R=w#)Ͼ.g᫞D<'?7`9!rk
I=$4T{5'uqyh(9ߘYShY#z읧X7CCEMH#Pxa*p"aq۾񷼘gÑ!xwte9*3
NrWܟ|`gR1;MpAདuqy55a镠آz*_<1F𯖤vS0IB4+n(=,BF J7@o~	r]9Ws$^[l5 TWIt9UqFq/@[&C8;G>-1	h\RF+Kxa+uTS7]*D.J/Y|;.)~D*5=!u5Η-˦ۛvp{[AMjPMhI827Qu{s+
#]ec\\]zuNZ-*z#` 
RФ5W`u&'`4svO0>:ωhvH=26RץJ=û*(xg-v-`;BG,/	!MP.QJBj\حSG-(`4uv:xC9cc֙'&cd7co<6oO㉀d0ȒCʀ<r1<zUfd Hyz~MIpI!mlcыpJv׀iضxs>E{>ú#FQx?T$m0$/3U& <<wPp$TIV~&$,0Z\xDQ%+OǴ~	MԫiS*ԨmKcb&c$w\rFEcIUq=<syYrMƫ և#{
4.Q2HК
fȂZd pﰳO	31 	,|[޷CK'e[cP]OXNR\ģ!~#νnIbPatk,,]u5	$[SC\m*`;@vE85
TH1,ĸ@>u`
wpy~X;v@ "L-Rv1n~ E1 .ul/;|lCEkNnHpaSM[}rSxV,Tʷp@u^34l +#NkQAŝDqyТ?އv!3B(o]QA^y­JH z;uLL?ٟb+p$!L]RS0{,fn1ȋsO7g;uTamb­r@͉ GL)n`5Ʋ|DM@CY9˅<#Xl|+w%['!XRiL: y(na0(]aJk&yZ͛g$b:Y1ę$(Dg0#̽f(7:㩽\.tEi`(1oJ#r&'#*EcFCu+Bkq~N0t{{mjgK]K/J_cVǘ,u論`/W"H &(H J;,'|DϬ]1<%j28lRz2؏5j0
&HlrmR0*4/`Y n:ALTV&̹& ,piuY-m9L4֧ ԐvT uu15y\J([I*U6ׅ_H! @_5	WHTZ@7n8합4!>7<Q[	6KH"31ۺmI~j|-Z7Z
ZϢ/IyWe.4<C[{X}M(E!5f2
_:~dڳT]vfG`ŠRNcf2>\_K P;`nt!_kmM)/ДH(Q_ٽrTg
.EXLx-ZS|$?$х0~&/wW}i}6Ó>:˞yQgG&j}9C81吽OAuy
Vk`띺:Lk:FQi3oiZ?H};yz2s`:&EooxYƠWY?p -gс0.uZ=`:>5񈜨ՆyĊNL11ŀS霛-^`NK[hpYmChm2:t(5@٨W3!>g#iөRqE|3;#oZ9$	C+s1+=}eKN+Hzd媡-ݳ](YWzA("ssHJ&cm܉o6!$TO^kD}q1_cq,TќS@]dY@`TA
q7L3#X`8GOjl#M\LjVEfw4py͖gSML=*=Bp3EqHmwK""xy, +_54S{oR6LȎrܔg 2@^[-3oc&8
0.[r9`Lw8?k|DthG:jJd^5{)	`mbiw-D6!̌Rwy?N
Kex4[pIhMpZjӿPa2-5(qfiZֳQȌ1z`@rm,.qh);7xш23>e=p: Ke*g[ĿʐSF4U)9Ez@!j2`kvDÁpҁZ7"}C9蹢Qi~)0X~hw-@[c	ʸoqGJj\zK/6Z=mV~100Gn	!iM<ҪX|ߗSJ'*hmQ8{[c|+sڐlP2a5=B&A0a|4.WEG$
0F?GtNiD/}Ut7(t~\$1|f{UfyzY0\;FFS*1&`#D;lg5V۵J<a$Cz,s4تyFHrXAa)@4~ߐ'PmlfMdSqE1#ر(&K ?p!gI9'#w'4mX"Jy (6vۆJj@a*T]gj|qouUN>lJ|)Hr;ڻP*qXJ:2,;rtJA!yEj/֗~&//1d]лzkcɶp0b_m+*;ƒ:̈	"MDrexșA:bwɟbiR{W+	n$9Д* 2EDdFIYHh4O$rHLFϴT>	 $L/vGSSujc5TJkD}= BR9!J{4凒HӰ4/We&aN-Y!ķ3:mk0W:&9,mD1:wp{Z7jkyU)Dq=(Ռm[JJUb";r]Z=gqGϖ)8p54ӡ#Bϙ#@G?tw)YwjdWY=rLHB&j-ɓ,<OBjQA`Պ.ل85P8%C{;/_<+D#/\n+hJp9lCyPQ* m.dq/O ?l!yb as͞ބ^ѕWJǕ%@wDf`hM*Vcm)<twf/WA939f4͗6:4/x1_5.1)^eS;v
?Dӽ4u5$x >w,Å\YK6)ciL6ֈԶC!oV7l B3ߑPϑ:>Lj[7N+ˆ LxCHjT%+b~;圡gbn;y۠H(|#$`?`wq^pPĘCiFDUB̀R2h,z\5O1솖*}*ԹDxF dįy|mk2XZTN19h=^/ 5 y
wB+,lCە
ixssN*&
1'ߠ k%$uuNOwK-]sy#G#.!QZhbC-n]/o-)-7u4_o@MR*$x+<aPX~hi5&9ԫӔD;=2,{F35e@kYV`w~U-%1W91L3	0;;q5>+k׫Te߱L'0dбNDKe+4ejYr4YxU;g	sqɳ	ǖ\,#[F_K8؍8EDGE2blz*[H|ѡs
IA}F.lNjkh_N.tC9:cߗ)	H|PIh8$QlnCE6`7ZO%6Tu)D2=$(逢P .aD_Ls(CAWOԐKarzQ3e=eGE]bwvª68|}l.0BɩaTk	cͫs^r7CIȃ{h/w	/k8Ö	lay'gqB(@ώTQc/	}B#,zH^a1^7WgM(
wK<zdTd"h|d)c"|Q"= ƧE-bFNjO3L/|"%F 
h,z 	Ο-IYD^-uU!YQAl>"&5YJ; GeH6@	J&BObn+u(T*a٠q*|O,)NWe)!PlTUתY*IU|ќT==@>de4U?q/$gr:Q$\M4JB&2`Z$ƫl;s#"֠f[B4WQVƋM2Eҧ%|p>-qS(zHXYME-N
x2+hi5,	b%e5e/%,QoxS}S`SnYυ3is[D[PP&h׿o1ӑJrFM<qE!  y$Z^+(A\zET4M|&w%$8|cԝD5sZ`]ae+
dJ%uK+Y&.i>,IѠOIM
)	r	"Eh7G)GLɠaA6v7bˤ$[Dz:6V9gg$)Kpx?I&x+e6cD4,;.!v ,.XЅCBm?Ct3w\"FIZRxf0Z 2b#E:(1Otvx>|--"k0ހ.$H%,zP+d$0=>TUK\+Cu'8ZgP5Ĉ4sdmFړJ/h֋'n6O||
t1Yx	ATΏ '*F
3^7`zuΚݻLyDJs4$\EhE fDi<?R0JOjS^=)LFiuTEm~`'i`UȇvA͙I84lڡ49yqŷ0,Y}h(!2,tL1LYX
A&cg4Oوs UpAD]B8i;Pif#"pAi1DFhߟ-3n6Sr2iWE365e;gMF_Ar5i4Lc9H~c?uʽ;;ڙnuͩzF{5V tM8xU$jkܷ/9ga}ShTB _#yXFӨJǌ,Im~<S֪3zS@-a?# r}Ʉ\;.25lu=S3B*rG$ ߼XҔuPCYJ+Yy-iYk(p^:/_pDx;Ɯ8|z)AI&/an_O[.l08$950
4aiiQt܉[GtzH5C;Kq2eםzк8)i)lmUˎ9#uAUl^rW6Iם5, o;s4QnAO[4c/
j4?ӗ"=}nw_*qX;c2:a$:nByϠ27H /[TJLqsTdl<hpˌcKjEۃބ%~DOlkb"C1E$S`0d1E'^odD(gwgE~5m\@G&Gd ݐANމCz&̵GqL^FY=^dB/dQhUFMmX[~H%::`$OM-)@wSЬ9	#Rpl^$ݥK\AgBRGQ8\XH+Mk!C&iko'1!^9gEmE뮣He]xaPq@@k0/yQV{rTg+`ݵY*ټnmo}cCJZ>E:MtzƘE]u"zp2`YDC-p>7mIx29<2yRjnW& B5D
hLe,j6pOd<(dYk󢮻 &.+T`fj<Cpk_
'"yǬ×ؕ^G4-H0OgRMQ_E߬Zœ"1c]"ȱIҬ|"^gAKTF@Cd)Qma'"jUX9vaiF̓nNWa y48%#xsncwQͯ`ETN0I%[y_8eXӢPڿgHi2VFE~ǐGd"CV!hHjI0xMo:gѺ2#82ְ2~\].(81S~%8nAchOAga
_?&!O$I:fP,؍9𔋧iGYdZgębrqN΃'Sd>A}'{Rl["D5{!73G{!(r jr._45}<iovKEA2`$HIadEAA3-,LEhh#	Ϊ959B?5p`T[T[='u׎ J+c]\7S>|YՄ-H?]rEpy/@h*.Olfmc5oHN@w&8@&%Lӻ|CCA_4Xa[UclgyːuT3zabs)}*8<Q	VdLPxhF
:<F}&g	NsB#<	`8?[9)p#A1;):
z֧*-g_SJbiTHy)"Qް}m+ZĀ/AG"Zg4㓞"ʇB
, #G%<Ǉm_JTgL(Ǳq@+FۧM;,WWD^@	r H[Ml+z&+#(	P*
~DPdW|6%]fj2F nH·L)iR4(jpl6;2(<,rL	btFˊ-bo-Fx`/5`i{9.(a51ƤW!;'bgf&k ),.Z#>]8]ڪ=>4Бe<t	G77_;Cm[+pZG6y#7rH(re)CX =݌"fbRbR@aqp	(z(<6	HrE$IC$vzJtݔ	(a3O`C"$pz}psK+2cbEtBI2oy0\@Lԛả."aheZp2 a9u*?y$*'|yiB|p*RQcf1X!ȃLSpȄ"7H2`$*5/3\8KJ2Ӆ 8࿇E/\;Je&M0{pT07_1IG5nTXMʜE4XCy$2:JnP)\ͱ*\Nʦ@*~yC*3+|Ӫc{Ds*z5ZJhaEJY.D
M!!Ek"Ԭr_" b59UMU	qXHxc#S+PbN*?= dqrˏI1"" %O}-EG8Aa=BLbʹZI>%%a%ߡYS&}Q6uR4E .
)s_-qah:<rdg՗p~?ӕFj?5`(nW/Za ye+Ti']y;>7U">ԇ|^Ȉ4Re!e 
=ŪHƳ׉B/Z{kU2 +{?0|	
?KXըż|ͦk!C m\FGI;DXdQ:2//旔[J	!N%M4[T:`zSvC>JEV$;azjQ][9_[*% ;XgWCGܧ<$N!MQ)\i@E7)YbhvmhEx	'\`	ϕ炆mcAѢ,_(Og\p|
c
` W~e,F/M-7^VEzrq_V2[7
!ePuap|U	V
~s[}}_umQm=}t(H<gPNk}^~O?7(7v,4N*ԚB?TsYvZR֙ގ7KfFc	}_לSDiFoBRNKK0-H;Tғ%0Go0%e*8PUߎGc'<䲚-c eAA~n^Ǉu0c@)eRݗ/s/s̱ʾL垑}3LNTڌa͐7~Ɍ$_={%#V\!4=tu&ǈX}xfpBW@Wtg/f\obg;{:c37QG^1p,$"=1j"B1Fu frȱ13ѳmmT}j^9;џ`d|3l<c~N_n?6[hZhOMFoX6дزs1MYG~ϓC.S"[3	âPӁw$p]yi_±@Xh`%	*}zĝdA	veEt&yW簤F;vT%QAp7x-<:k<kQPVn+:緋jNF`7$04[FN;ګfV5+߼WR"t7#{~^!`dnILxe,cRHM"|Ng_/V:%{ /ybB(ƟbGäG ].9/RcV 'چ9P>`L%0EXB[jk趬T_&T7OΫ\;R6`BѠ꥝4*)Ng<VE|A)`2GɘirlAW`B0xae^x4b(+.֫EK
`Tva8Y+ۤ 
q]zG/	WVWWͽJ[08	4L	N)f	If'8/L}ol2!0]PrMبvei\"w`*-i1ڲ2v{ʣ0ic"iˇE{-uo3gK$+`|A`X%rL:^jǒ81J!OT<Pis,]/
K3>(mio*5}u^UF=BnQί:+)Y$;'5ڜ[#<"k^h~nb&|KbQb}Eu!b."1D'ñvX x_	SP9A׷'@Ex9ˋ52Z O=UM@**%-	oHޠ{ǍWsfC(oZk|Չ?bP[nml*8iT>9^D8-a~~1>giȟ~7VKX,u$	NzQ{9]{ڴɡ ZvO&
#ғnDn!	'Uz@e0N?Z@LiQgd%Ctp/f* %LW\; 4/W%dNj2x@4YHtS,@qzw$od+ȁ'I4yf<Ĉn)`$ΐHk#o¦VC6*fR~5);pMx$E&?0F^y(P")pˏh)'MphB 
1ELZ命=251rP3NqH1ީwo II|ӝ=Z*^v*ݼ-`8";A#LPU909O=Y2:@\hSeDc$D70A|A[>2P?!%l3+})6v7@ l@Zp$J@Cxw!|Zќp76m1slwIC|49HRLr4,㈁-80+/ZB;3<g?}酏򊐼OTk xLf%轙 eLy-bY6^%O}9-YoOM^(Hvam8(RJ6v2{q6HÖ1zQWFd $X$ܛC!91n1xl>LG5]1BYk.K V_:]Dr9ʱkNڈJ @dW\v2_<a@M-VҪ*FH6d!8sOJ
X@]i|;<uE4(8y߈'Ź#3|ujfb6zW˺
5*Nϡf8_RueQEGAr6ˍv,:ӮeSˤ-b:jfҸk7~?6Ç;?KT9oMvǔ:1y/ ?[%ToRA7Rr0Lc#G(H2 `܎w/
#-x'nV:[mv_O(PD!\XEfK)C%=CG	3BhOfm^W"Me9?g$B;QXJhR,ln[KPB9Y4-*f/#7hnqI7RvSfx(vŨ+W5y]56^~ŕ}ZZ[r}X #~3rĪҠ3_/\l	o{޾-tc&lQs:2 寁K妚gG>'Mbx7~10htN;L.idÐao|̑!T^lB0j1Akf5ZCF0M"P@&ꙄhWWHH}HQ i1؆dTK[0,}#q̌bd'N̘`T΋f`W30a<.GԥF|Sao\OC:{Y;["\aUcHP2>Ͱ]_\N,J{;=ٽHɡY'ci7O&oGBln/:211Xc
,- )21n*njWXk٨»WE0}Pnpjʨc	YB(s0B']	WO.[ri^pk`p֕ptod#yqIziCoAƑ"lZ8gX,K)jM?tRf$چ7Jkn9b
P&^L7|b3moGY8$au"RhL1Q8D8ŗ5d@ЪrUW
lyGkZWdKYb'IBqX6Y
5GEQ)	"ÑU>ᝍ,7za:kG1{P5 #ȊL&/nVRёsCq{} L73w8KE(OҫQ볓x__ |(	IJr;ݑT-گ,+s<7,50<Bpat{8o}IL)j,H*a^bѵW|xw/@zI׹b+ZEp&;\ 2@}-06Y[?yذo~-+n;혲#gIXUulf(H9*fÅ JT QR<+>Q̑<oڒ.B~wOy6ʝhJn>FT9"NgAf3J`2kRnU:+C8,ם5_l!P6`SQ%ScN*u-NՈ	b%bUCjXuESng[#̱~9KX+a$e$(wҍټW.i)B0vN&iwJl,	J[gDDfYl~uQmN	zjpLd>,rwѢdy#<[kT0F8KdN<i&RZe<8N#^QQcl7ƍOɁ<<tu3OhkdP60=2HE
x{L|q"VZ,!D|ƴ<aj:pKzߒu	ÆEBDEiޠe"#;YH9%%ȩ#:r3Gn<hc!ey8i3N-1oiWmI:6~aQ,)7.|)6`Z5Rc.z}݂eID6HXSCZE>rM1b`'KR3D+YHv".7^+έ!k(IE]X Y
 b:ՃilY#BMpa}>tDa2;W炟wn	~R66.H*)o;1IbU-iE	oy`->&	Pn oULJxmSJCFSǗ8'^;2i[Bَ57(Ħasa ΍w</6"aʱ.kҜBN|Kfm-)')@9km^yJTQDtD)|\8W8yl1jEBb@,SaƎ4Vr&//8ߍuq<zD*zXNA7Տ:뫊a&fj*b>OV{hGD{thL%YEϹ`0	;ڱ?[Gպ&-$]iJthacTa!z)d3Y ǛL}<MG_<"Æ㶂[I!]Pv%Cҥ|sWT`C\t1tS01%mى},P#DORaa4\˖Oۀ>ظ՘\<YkOL4O0P+^M}qu_~pO?ahstӘ_OuFs+6`7g{O'.g*}]rd+w`\Z
֘,(IM:d0r.P$g\K#GR"5	GHgC"`AJB\P3i=36?̂gΠַY<O]ʣ(Xe`=8~|CltRiYULi; G$&l6ꛈMOWWuoL_ï6%S1j ?W>.h溝z9Je7W`QZ4[nNT9u-YnJv":f§_- N-w]Ps7Qsڄ&}?D tlWYa]O8յ9*~LFwF1mrcqe|wS;vC%?ӰLtؤim<
(/o?wZ=H}qE͘RYe*ZH_wl0+Ԡ=ڣlSa|~Z^_;{$y;y(S/-@KNGlm'FI43͠ЀFf9\98V?ӳ1:
G83I㻙c!2u_ƑE؊* fp"7m®( >5@Ƹv`aάi3K?Y9v+stw&ֹ:o=1Q?Wla_+":TMtem,>]+MwLv~٬Dm\Gb`fJ&Ҧ"3sgoe0өyQEmbF1iS߰\/oIrMIHoьX=CvX:VRG)DdM"Kps7&6ҺPc&'S,E
󍒾[A.D{ OャG?;zFQngD`5Aww3VT43,W6Ĭx-d)&r,7գw|{	h%SF4x0:ɡB8,;tPQ[7Dĸp .:rtQ{D8bCKQOe6dD;Hc77':x
(#tEBO5mAU[d!OEM ߓwiA"ę`:VɳAV%H%+va4m)%HwHfݴ+=B}'d R01m,e;@,zd;ǈHr0@(^_ri8@PUv355S¿0nǃZm3.񥨛TkN2^EcJ~Kz=017l8>Hu9jF>U{Mp{#{lՆM~`՝UGqQפQ_	X&PS4Q:[-%dñ5i-WG'ȿF
~+awoe	H@|SKzFu|yJ_̈BIiiArsyS)gۅfdq|%S߳qG^eaFnx5hW$`I܈; [ki 5u) RzavqcY-cL#.Z6
ϗ& AA0VeJ+))=dp:C5_u3ڭ$hކSxPxS̇$N:Pk8a1m۹jI,8&aҳmisjFh$ TzQ@g{<5V2X\㐕pV3nkK5{+c)@x)@&@,C}AM<7_QH3$r",l.Nk8UnBKRZyN]|opƴLfꝻ	ZΦrJC4}m{Y;)\$s>{[[F[#i]	;sВC6[l*D*9oDZN+9vvprFM]6Gу x<U\TY[ư_)Hg>Va]{H Ec)Xc+tk:ewMF,)<.EG`ˢ|%0?})	1[)%1ELҹ@ ҂UGa\7:y&h; kw0KF`40Ɓ)f;(soj<7s>|}kVKC0vˌu:DY0Q=El ( a/j⎕[Z8-$no58JLh2}ȁV2񌢐s3<U[ג$Y $2֓B'lwu1 ,Ƶ"ٸv~\TkpeIOV׎R=v3eK +Ґzry%Su߀&Q$;	rnp\=wds52p;c}K([6SVѪ<bhe!d:i~0`c\>%Ȃzj|nA]PM-APl~sV݅YkdWbc &JSsH{,#`
,(x-*L@aX]i v.rd%,k8I/hni>7j)3Plc28-OO!04l5r,U^/4S	?|8hJc)ãn
.#w<E"Ce)4gbu*-4P"lh8T^Idy-N,n-Kw/>H6hBA+l.ܮ^ȧz}
oahaqof"'дT3cbLq5'dJ*i׆1m=.=DQ*p,`aV8v^ǓM.cye#
P&NkrM錌PTk$G[<z%WY5>i^[QQo͊E[:Qe!Gj0Qk	7&
8~8Et?tUbm>dPiCL5 GF<bL0&LpC+ךkD,Vf5irUPnMYH+mi2?۷30KA8Igl#87[S\y%\\Zu㄁M;"x[ǋ38JbhIJFs:fKTJ2z(G1 ؍'*0[NE[H͈7m|}Eù!`
U25|O&Tf>r/4׋Ij	uu|JryħD	^yrqpr}KdDfm"	wdQ!X	U㕰/b&e{نo/)0%  G\kA5 ٧',oS݂t~H\	ҍ!M5i^ӗ߳HӁ4f.%/.<M806зupEjC9ƉLB;Ͽŕ}hbp	6hRuY^}7gnMw,riڋL keC|CLx<!i[paB-U6"BUzJd]7p	P7K5'Z.q{\	 O(BM<N?h
GqO`t[]1x+eRt755nO=qעfV9'q	ւXSQUpbS~&89ENO)_),WK#cs}_
_I.)MxX>=CqDc#钪۱dSqnMp9cзo6/HjSLL9PEv=ij_P]&767		5e 1zjU!_21#VE6\t/QOc[_`M`5ҒoΥo͸Ԙe`CۤTVRʍH)ŗbe)si
aa洫,iX;EeEn8Ř4(ֿP^)4#,ߞ	}%ڋʚ/v0w^)aCDhwKFyYO5}FGlar̨}cMN*T7]#`m$[knFunNpMfu63)7oڈ|i=N$FmsHjE*R0a!qlfҽmÙʗHأ|jk:|R_qs9u7E|ae(0gl/yӒX9	Ee ,t\D2Xs{U$јCN+/45Se&,RInD=!(:l170žI k3r47$bV`yu) 
VK8Q<qV.qsİ_0Rq{q)1pyg̻_"(9Y/.i-GyD_:j(X'bx/Ap/FyU w6ߊ]j*YXK$4oNDz|R>Ѣ	~΁X/nKsÍj:9E!Wx֒G9ł)a3JzQA;v_=%d@oE	v>tTaf./`&b*n)ShI{;6WNyͬL'cpeM(vQ$<s>dV]_s*h"rXԽlp0oh5	B>+!)LJèYH#Ѱd٧0)Q31/lΨSԮ%:=a$Z"sYKSۣBZ#`(IǱN'L?m#]kn}UO1i.e B/~-q[-\y 5+9WrK =D`a>k~ߊA:0b5=5e<nfL/ӖS9z
?aH w]:~ýq~%>լ}5;}-vS,.w%H4}'RG'4ܱ*jxA3 dJ:@.]0q:`Qk t48fY064a:q=R.!'L7!f.7`)}$Rs)e%8>oIQD#@uK/._Y6"N(plqklŊvf@\{	f{j _byc"?iLZ", ;$c!kB\mDQ{b{N=`f_fX"DΑw'J!	\}vxN &-_\tIi[/U\ؙ ="LiF)X}mp!<S:$Xh).u1-~SjEg3|LPf2C\$y.Ņ?C
D׆>,uF"1Q+X]o3APj<IHL8wb`D()JLuӢm
"j^``lS%hl)Fe+k#9d	muBs*f)A c8"9S!kSb=8(Yc2Tfb$Ap,KWX6ijKӽp^B[<n(w3pc`r;{-^wJiǯ ]l"2!K7*i52jA">(/qbN!(ӫeRGQpRe Dè\9/$e'ʻoؔlCR1Aw'"
m8_Ѕn%峗{R78vCumzqzO|/8PuUi΁;/wxMh~Qen4F	tVz4@xQTB3n]z(x*%!	5hvv>yBw lH%i|8Ol5%%LG vgk!M'HJ&,;e@8oak\F>#y{pNay5a;ֹx&UX
A9BUc]09%g.$YؼA\ɣ,N+3}aDXϸjJ/(HI-䏸E0)Q<ZþrL.]2It0P Օ7t45CGUhv0bF\@Gݤ89=7Y;!:s6@^7#ZTvfLb]4UyWVB5u%H"#=C)X(4i\*6H.L@$;oe`z50=f.ɜ:ؓ/M[X8h3ʥI>Ytms$_90v%/?p3eClTv;ᆏ* ҫ׳I)]ZL>J}ꮪ(ɾ;pg$Bݿ5ɖ=:BNQ|sBKŗ-`mn<`l 5  %\4Nd%26k'qD(SBB@:gPe^.GGpHL@!l7$;OW^`vY"&4QgªIIW^t(9po3`Clfi%pkEV.7:83&^|2|#2(PG[9D2V+y8"˵yDUssHevrSiR_q=9E75f4.NsWFњN$ZCτ,$]'g&㡩⳧|!2t`HvZ/KEoUǊo) 	nD:;Iny	2_ț'z,t̼c!\(0#\Y!LÏ"nO@u"$M&wILp!
$b@³uiw&B1Ere	R6Ten@/'ʪK6D`g}dEGtN=`ʛzY/d=}2t3jAx7~1Ň|^R$֜V7Y{]`odCvELB/9InyE[b)- h斳s
Ҕ>dP2qDS[6kk\)"_ {f*GWqhա9*k劙}?z}_P׭5wE2{1|o^>$X+#rg'!hPP~=M|"4197)ʉ/4zeW s	Ggㅤf<$\0v^Ye_Lo?9Spioly1Wo-Nu}
1^
f˞ӍufJ+
aDųPg>Cnb{[n_8}0ê}ҏ-}LO<(p"G;8,A #a?0M{r*\묽llݰd	A|Otf4aJA+$'63<(UQZRJڎ)U[\%<a@b*\
~_a!%J3tI~׍?m`ʲyҿ;>=|xwrޛΎޝ?;:`u:u+-Nt'nN}Q)?Έgx>?+WUb;a|Zn%fKeо 5i]p[4#DMq5=}M#^%j	c'ZhŠ!i]66ZgS*Ad[K=jwE2tx+F_xY}mTҏ~s+㴜(lpIT3^˹;c=ұ=t57}TF{>x樞8qP J+FsSS8ѽodU1LRAT

%PV/̅u05iE}^CVfRuX#*BO"'wI6A@Zl"˘47BÅ)E?M5EV>&pT_â*S%(*aҰ(4~P T9/9a
ֳ,ϖkyQVy@̔7)8	s3y#rBkWbLq)\&AP**Qp@ `,4E
4	7xC7uBa Jf8}q(ȷeEbJQ3پ6&4aq.^d@&585bX@<O!H"G>=Q$:t'龅	l9S0>9+d@b2+#7:vny~̐Ψ\dfr?d1xj\`OEK?ׯ?6Ȅ<B;N`&Yh Sf-	5BR%{Z6oY:X-+ϽR%$X!: X%`\H[&YoO=8fᣳ'?~:8;s/ߟoZ[5 WsOv7uΥ7£-+ws9,2dj+ND |-)^.90NrrUg?J'~#I+V9TzjbPUSk+jܸ3a㳒~VӺ̐}3nI+'V@{!='"!V1zPB8ajvZ4'ẽG~<dp)lX4Un2t΁VoK/b Q6g
SZo)@D鍅?UcPd;b./uo-sxs5-e-!C0"@ҫz?\q_00KF\;nX@uuuGge`,쏙:`6W条X#a <#o<1J=OfhwL$c$Z僗9yV/iw=SM^Xt*aQCF $GDW%4 }]MXow~Ō
ȔXUQA1FC,R4j!8yIgv$;M^V(Q=6 	NK_!;ZsF\i3́3wZqX~Wr!aALc$U1
!0{H.ߕyף4?Nd[ztE/'4ШqYS\!]6U,c8mMR~֊& *0ߓ7v&0#Ki)<](-&Jl\M rr EsO$Ia)Z)={}779Ef>V%"2hFf@ǲbJӾK`LRIn覄QQr(QǊHKWl,aeq!KO4KuM/|`Z~~J0
;ٕU.z!$=WnxeS7y(x#nM[qK2bZ^>$ (Lj!?03zbw)7bihz&^_p>\Vd#E䁲mo.;;)ď'q}]M/"3%܋ѹ%A8Pg%Dw6$&~yiu8>V'/@c8N6tC恃jr=0f`gR8;WJC2kTs$jpT%LS\wIF:H/-u fQ֣^y*L/%SYuݢa^j~.ΎKA ˝D.)XÜ;o"I  {˸SudyW_8gBkIL#-K$If]$26ugHTJϜ$]F!u*HexFUN$FylnmrZg^-A	2|:/aLH0<rcJ=SHAqɤęچB.-T_^"W#J]u_)CYԢcg(v엺9EۉR}w-pYFx`JR0/]#qtUddd rvSF[.9 )+CA<ӕh!>ˡrVD$&g8Q5|󠨽VG5h6JA㺲I|t?谳 " seǠn{frtnI5KI^"hvC740etÔ%{B]_,/IS#
"FN~px,GZ~+qWtb8XQ$S9%>d$oOД$z{/):QMp<3'B<H\8Lp:XYMaa\eXO"|DZ>|y*3t,8&Eb->F`~?h @0*,+"u:5晖=|!< ":رG	O b3R5G*$dRƅiL\$'>:!,+4b,N?LSrIa2Pfϐ10Xh(g".CSG mTT(pTq[vM_ؤ8faF`1<-Sm%ɒ4k8^plm6N7cY5c@(₞y0#A9sQ^p>,lĒXmلWb5JRіjp&6~>uJæ+Ut)<BZrt\fYmDpoHDgN#BŠIvtNP/'Z WHlZ/^}
j* wh!x8ҶؼΠ18>,cLqdrO
n(9G	,*S3vǄ@)!^~/m!=թ373a#H⥤|V"S>2
gw^-캫@V4ruoǷA`0;f' km\C[LdMAoJ&%,w	B;nR8D+都pz;Pl'XݚrntӭZY*N_4JFaIw&G3+D~;0WuXfFQo=#}{S5~#GY$:JBhh 
l`.Qf*	WNh4VeAӯ~w	07x=o*sNG0"PMD?SЄ&,RaL^zYPF5pccBB%Ә.8{%{{{͜-2WMJhJ9p8';0W2jSGHZCRJHvq*`A;V<#a5B/6lv
&#ὠ+%ޛ'\?ЙzӀ fR#N̯- '\<`wk|!:fXzoX+B%AwD?3hk+s5a!m|TKp'ޔDk"]Itk	d!f>c{S[KbTFPқpJM}q-3f PB`)J$-+0E^1옆BLvI"vJA#6'EhfxYuQe˥vͤO-VQDBTSzuȍ~&Ct;E; X8l:*TGoOD큚G'Mp"]8 zJ
)Tkp3T"Jvwyxxo
;d; 4D}FI6ӸMr|h˚- qĽ)VV$$)c~8JJ;H#}kXTwD*,l*LNH$ps*0P#j?e
{_\p2	U(1s$e	Ra5PH_2V,VP|:RxM"hCtz5>e)͸? ]Z-xs -=:,Dh]o{4>N|j}xPr *su,8ۊ4" x+IVH	]e`)@4,nXŝNytBo)?nf%ƝƙӺyKJ.ڠ8U_qt߇ku-)Q ~ݴ1,YyGoL&B,_{I# TA2B *]`] ߝ /Jw[ԑ;&o;C@8xd:e'c!+U=,
P7Wɴ^̰kq[vNd::JW!2"VfY >nz{cӤЗ9Y4v%k/ɞT؂% ~`#lOu36#YxGJY/s??[؀TtZ{L![Xހfՙ{?"Ål@o~]0eZi:Y:\>(M0&YX\FƯ#V-@&GR'iB#v\Mi89%eZ%ͮg92JMa.]ҵvzAߚws:Ri=.zOo(EWp[!bF(w݉؄WZs"R2] (]pKk)s	ivG^ƲdxIhs] xoR*89)a$Br1~yXHz)f6ǽNm;	˒Ʈ\En*+3F9\͍s"ؒr#-Nn$K0d9K܁BB%J1)^MDX9#m4b)[[âŐRk3q
	aUKqhBnE{91K~J}gZI*Q$nk}aT3%KmaE@F1[6@傱 q(vGT׳qFjfYE}Q6l`]!v*IF2?f2.2ίILZ1uf5Dm +XK&z<\4$+"6UQ2KuDj(4E	$2破lFrX$,'wxD ZrB7ŋꕐJ$ke/ѯL!	UX/PɉIiO(P=YuNg-۹${prlH\-ŚidK:5pr~,FHĄ]흾BP«Fy}dqdEQd+x?bD_ɩdc^+zS.6#XM͠(.cqљi	2
ˌJ&2T5Y5 /=jXMRf<U9I{:t[&, w	_N!v%$|[L<1m X/D <j`~uOCh|-VgdZR}s'LM5+ƛo,xFfET?0"X#{rH@z67ajg&*ȕ?URT?g3+u5d<ēp"ƛ 9x8KJ>Ep`wt.?W	SN\Yqr\TúEmDC/秳wo"dlu7bY°SWU3|`ȲMeLВ&2.a^% <V14K̅b7XjCdWc|mThpN0Ч$¦b<+DTaO8'Ҝ"cDkG2#$$l'?: A},$T(a)SV=a.MQ6ą(/VѩQ1+~?oT_ZLIWx)Fឨbnn@35),5$7ʘ(+E> h|XfW{sɰi+)0bAqj4˛hǉ^HO"QiuMa6Ƽƌ!D:6LkQ [5ኖo>J1"G\
1nߡ*QR,dͥa"#)*/-Dlv$.9I/B-SL!+yb.T؉{$I WG
XW.u%XO$ WhB\Ѥ{}0+^sz-]B"JX)V\[	RfPrMJھ4{,yEZ.f#Vt2KHMY&ĔL
!ɻAC,hp)Ṟ luh!	3sQbͩr/u;8d-m*J^$i*(Р}EFO}B>Dyԇ}bъl	8e,l$ X4m?wqRM_:!:vdNfg=Lfy/Uuwy@niܵLxɒj')dWّ2=\|jEFJCwƫf
N%,4p`'"2ܭos!,WF![	b;ڮ-s^F*#Ȱc#~xJtEJ7R+1<o\ـ7~!z|&P/Cؚ1sc
~M)I8彣1
5,Y[mT6ck]ؔTtHmZOFΜ5	׏]fݓhnZW!#gorOMuDtl6۰sʳ&q
+l`N7mV_o6qX?LjhANMRc<Vژ XJ)CEFu{E`{32^z=qbk&PT3|j25UpƖܮэ|ENKFJuoC	׎%Èa|fᴽ=dIZ|=35vOaڮsx2\.*l<D,?3*lɀ̽5;\s.WlD(INaop1JEzSl _d۔$@7?03M)O1y	8`ڌ⎋B3=/r*rDyS[Bs#W'Q
X"bA`r,h#v?--턘\ŜKLO K.4mxi)@)M,4a FgXN2!VW#lQ:q rL!Tp+5D)`G-3A6{B&0jQgۑ"S>ԮExe1?r]*O+)c[40kxv!ߝg\I;<9 ᎇjmD	I{Juo;EtLr/,pWU)_Gjn_Hk&"6(PxY2~RPOCoj~U7O~=C;!*9]t z>_tFX_St+X}i&1|v-lpqm*rkF :p>wai	98@b9P||w6 z:p1`!"G|Er?=Yǅ8Z	9m3;t쌳8N7âoXeX4$P~#6r3ܾy DRݬ&Lv/F ~S;n\a"a[_,W+V_rOX84(Y`.:A^A0y%71SQ_ՀnL_=eDH/!4(tp
"#(;ޅ>{adaNlVÈgF\'u"8˛F777a2
08f6$lX/&3AO<X 	NCwBa+\tR$.gNV/%5&-e6)UJb>=REKA)`ӌPJOZ/
fܒC8_xCv~O$UTF_	q]N<6mh4h<|rrQc<	Me?9bpA|`FKvarµ7sA.?m#PRlDp`*f8	|R'Vt@t "J>,KaV(/*m0쥿}b
r3bqdJ炎'IJH9s1}qsϊ{	nBoV@auR %R)#+ߓ'p76VFHU`7<!NC@˚ӷK|Kنb$ΰhV'P4]C"WxI&	#-!
XuF64q[HrLDY#v
SJHhC
ְpUq{qW`ha˻V3-/uĆ4G>2no{g\oG&eB&C[S	SP)M;%CrnX|TחsCS<\de5kowkwh>0:yDx;KNk
K#ӯ	)Y6W7lJ!
l=b""
.$-]&ү{yZGG9Х3E(@NɫqEa^l0-"x*>g(n-P*WĒ<#NDYirc$2exD)	_ THVDAT#diBךP x-cxtX{7w S}01Ҕp[@g!¤OgЦ<\5 ߓ|ā`.hBp0'Bdڱ'Uq*΀P9
9LCx$<57F*B$&eoWInpr(a&w n<o,xvhz
BH9cw\3k3;[`D45"fB
!Vi&f)rġ}0§򉢾75,%ZUnA=(C`g(D`C	Aӡ|pw}βM3\>\NJlB1~UejqbH+ pGGxxe69Գ_<8xVXc\I-B	uvho' }IBvZb`O6]Q=hHfP&Jl4BVףQB4֪3wU`.Ry"$x]}'--]j?CwBDȣu8`R!P,+͟?ipE]{rDK>Yt&ߧ.قW)=?n`(`	bbI=z9"tbFFNCDBqmGRI!'-,~#׼Ֆe>Lј_ !wBE	8J-ۗ͜sHsuI7`d[i<V+ɥR|CJ*5ߜ"|N&i.6H*-nmT1Գ\_DD`jF
~y]./}s[c*NO!=#?k;3;<s=l\u"HJyL4>/kxA̳Gj!
(1GvM1BCl-	w{hS$%9Ӱ?|WLS	0Ծ?Mfy[er<@(ˤؠ'/yT0R7* 7:F@YiwYi=?!8Ahٚ$Xsԯ(!#Ȯ9j6ɿ'00>p8RREλTO'ƹfY.mtF={c!osȲBr$ږrtIO7%~tПR >KkIA\qa˔Ƿ}q~L~]kqíf\nTv(Er5.Sdy2ma*GS庙MQ=wi34[AoRO%mq7$-M̓r7ѮLIxC2DÃl1UdM_c֧Jͽ]A_8ToF)϶2m8SKOq!XNo}ARԥTN)zsؿj F.!G2 bݴ@-GjHUEpILKz{)^SIGЌa@3(k:bc14~o)QY>L1MW7hm>8lMaI bS|ڔ"Kiｐ$_.uس|p٧G<vz~ǣӟ>=*/uŲ@A77$j(E~0 	{5fh+,,6<Tޛ~MlLyD6	;V8aiZ3.kq\o\HK"rC0L[_s\ %,u	wQkTQ"LHR:胂yXA":.[_h=S?UPtbF`MXr1 I0.7+ֳ+i_%ٍ vȼpT2%ҺfiN
ªLo$R>iK;ayƖv|d	旇ƹAI2*ul
[	O]	^FCEulpWӥZ8|9܂! r:V<߆oGErLSyBʣp-ĒvdHG5PB8BϬٗY0lr]Na| Y)BxUjR*@ӻ/Gm.ԄhEږECǱ} ">숃9
u6Eac؂o]/BT=JJTym N&$/w/^}shSmV|IȊݗYSĲ90ƤrS!UnA*μsIpZr yEDfDA"Gr|gll#|F]Wa҇$
xO($]RA]sJ]	+`Jws5jG૜SPܤ1MG]%^lus;\ұN$sWn:@	oJa8}ҡD}HR_ʄ8ܺ^.(~_wz,3*C;MC\J)OGot?^7kJ0LfxIex0*ǀ,6Z%a#av_OPNtbыfzUJ}W,7m&>2ޓklh.^?m]̬M$"^ļBX0I]V̶GB yᘈ02iƌxCceE}S&w?g:Μ`n=lHOoxՠsT6ڎs0ӰY,/m=vHB=E[$,e$hޕ(TDr1}I˶92ONX2YlD',Wh`d&ȸ
0}DQ֝mz'alA<a.i<l{,K2ZH\}*=`9k3ܗmҜ7MN
~lmDY0ڎxб2K|r4 n.^»H戧Aĺܚ޴쇥|~7^-RkЦ/`@#AD|ĹE!TdѺO׻d}Xk=gRCuI_b,`*MXci`m>>@UW	 e.˖[s31Ahf1R4Eޮ0eUA%YŬ DV_]a*NL +Dcd"(H	j~4o[[G17;5Tݹ| .(NzG#u
QuExohàG4t[."_UHLM1
'%:LA5n7~by/M Xs
O=[\FGxLu^nCTm
4=Vc%;M^"s<hbW/Vga>a53F7C4b|w{H&@yŴEV?`DP[p
6Ԑ;='Pu>-Aǃ=ϟo'Y?A9`SEϮլѣfyϾc!6ZPxv,}˻ež^|w΄IT#Z4X`.=0V
eiz1̯	j4e[vz(2zϰ%t_
3%a4̰;|4yY\UfR7	/X&.FLx!,TF>Y_aI9zhcAU_|Pu9>#[-g<>{{]YJ1_-5R+[GЭL+Ror̞hh&}sN@4(N)Tnm	IUFYMJqrHϱTZqh[5©I%ރEĭk
7u0.<ZBʗ1zR&|PXS>t{$,!HɣnImD950CB21Wj¸69?$5y|.dB]zcGӦLdĸƘ(^8fm]Z-V8Rux9};)D#Ɏƞ
$N#]^/EUжz!G:tOF=%BV(^_)ZdZҭEjtH-Ip!26C6oR¿Ǒ*UPPesjF&cEbf%~z"O=8)7elEl͜;?)-igUVl1QtbՊ	R(Tl5W0V*RQ~(S \#,!\^گ<(2dyJN_%z~=5K @:G:QQ`^	Yԗ2s/%z*1JTxpSC_ߊy:)AKr{I\c"cXb_&5'.@y9Pa)j3e]Y%?9
J19,Mv^iMD	9T	kI`cV񱎬︃Ҟi`1~,]\|'[,MUMW1 Qᒥ<rp/[j-a=t~5t~}'>c]+E}ӢB(XY5W4a|Ԁ_akRI1U_eM{Ά@R(WX%H X,e:۔CxIu2$REe,ǩmF5<M#=:ACZ&R h_BcHM~mT#Bm;*cc$"|'G=>C=*h=R6tAl}}K9Q	$A&ӓRual/7-`ZiPb؇<xՂ= AVI#%``wWHH^e	473ߚ[8Uᒶj]zB=1qrz~F0	~//q-~^:v^1w1UJKq:v3nϱxvcK|Ξx CO\i5VDGP#:ip7Ɍ¨M\Y c;Ux+u+y})K?Q7㲾OXޙ&~*%ޙ+x崋DA2˛o3S毽EͲcfLo&tUVekjSfK^碘\ls83,z['	.=)CCCH<ҫ5hD餜r-q"03g
kaУh0hΙEy_Ǖzbsv׮*0oJ ӝUϤ1k	_&7\|NoΩhV:V2p IчP. RA<Y3{wpPH"U"#}DX:W.2j59)(5neuAV8
yA<{껒R,uq@:s@8V)v d_pW6	!܄5g4%ΆOܥ)D'[ggDQyo <ILױ3jJImȸ: +Tu`$#L+Pي]=WS&eG&f
2`IaG.E@3,D,̌3kإ3GO~g4yҳtl<k[Zwwjַs 7s#h`zyE.7dof%#HVgk8){p\N\1m
.w'-c\A]9)}U.Wh
*,頋w#)@(i{%~RqcUZ@N,e+VB-k[O~dR"Ě{*ts37t&v"NjVs#trŦFELlW7RЯ9#`aӪJ"Cyy!*+5x|aVoVw#s?;§KX\<ep{eyiq۵D⢭#耮ÁE.Å7$D`;', {1-&FO}b[(Bxhgn'<T-S"_x=q&'\-l1Sb,7SY6g4 ѐ7QBaCEZFp+g	a}IM!]|Q6A~b;s뾺_n(߮m/f7돬7$uXO%:tgawM@oC2+.yY
@8;g}g37 >M7sJ.6JH=*NJ(ΒkeR#Hp,6N-{R*lߒ@*-		"9DLhX&3#pm<5ŅY@&s?QOy2dx&v !k(}XG^9?Ф>gW4MZx(bŨunP81'7Es36ddZiQ
y;B08%F؄$y.VMAכ?ef	xy(ewP$4gi	A'C^8Lܾ3YI?6!Er_	fx0+}'B?7]~QM|HCc.406Wو!2Imͱ'*M9ͪ	Efa%ʎC\y`A:=^ tD/I<8$PU!A_s'dǐBWoGQcǐdWv(;p0{vrC'rggDjP nK9wCL ArRFppa*/Yp(c#kI+PfB:N͘
~jǸ{ #d3gBvoyae
!ES[Cl[?_ϽFb5Ѱ8o6qc|'-+_3XJOe~ƒXM+WO4nwa\LyOsѭ.Wu٧@bE^Q 4*R& +x;7GTG$ M0ܳHe襼cPږilZ3_.Օ*RW*)Á0g8#"|%YrJbEz9e"r}&-%%<d`Y~@դP>G(͞Ӥm(lK9bGqP&h&|.dT/["wa7+(1e@y@'A-GptE<J
̊u3k:xzdz'Qsv	!Qא>+s!?OPY;$UX KW>XSf%F'w1-^-@y"l/	?:nJ*O<	"b\m 	JF)n/\ɋȩi3'w ͺq[J1s@J´SM).3LA 5a߅Eby*  T$@L(tb!TNF&Bk1C?NNZaԒ社"=,},9`f)|nhMn25kTo u?qyG*ŸelD~d2rޙ26s|lxHxᘗH**jHMQ Ȩ/hk	5X+ȳbo5hnWH2ViIr[=":	R鵲
pPo2=T.K{a?CK45~ P̠,{:f_c(AS1VǗR)wgJJm66LZy8>uƐĘ/r@`/>"#1Jg[;[eRttiYx( hRh_0$7`\J|\JjUH|"4<p֧=>aB0	M;w	@KW} ˌS:aVla.[IjbV'^Q1SJcP6!J؆K6!SZxˡƧ3bQ6Ey7((Q+|VtMO3 (/JG T~b|~#c/d1e&򥥘K
ɑ8b8i
_Anׅe^>Iga:0!	S4{Bœ1Qb,Vzkrr&	mo@Y+MP:Fpq@N5Nh,̱yrgMzיMOI $	Z9 '"O"")Vu5z3mihS	Y	q5Ki2q@GcgROj
 /`L:O0J!@X>/K,rH5Te_9ZNg=5E4X\Z.`#I!.!XY?,1-!ͮrDG{w3n0_;:7"
]Rj%;e{sx^W3b6uI84uXOR¶}Rϰhol[z|K?ބL!#0YGІ2	ējm@kA 9\͕dVb9Of\Yg.W1y\Iuڏ+ kڋJ"'ACSdP8<|W:zHi^hm!Lt&FT7pCZw'@̎3eHLUwQ`~j6537{@ilC9 qM_U6$,&hgw72IUL<u\Ɨ#G#Den'P(]S1YMQigHPcOKbF,-TI-j}f%G)|MF$HJ/j"BWux\rYD^_8{N"M
);=%GiTd͚it?8uW_"vi#+(®EA=K}qX&Fa>zZh\2(oͨz`rasV8
~fSEh@d_7I6*!ֲ8	FvZTk:`l,s.D%\va0R罴?h0ˈ-^%K-K?عa+wKa<ٺ=$l#j(S$(Y"f3keoJ/I,{:dFU<9*D!i(L9Xjg$%,/-5Dxí-∉ep<*&[UxօWQg=L _Uؔ%}Wއh.ZHg1aAVjͧ(걸֬+a7C`*'晨bQtxz>T<}XHDUd6
Ȫϗ5=$S`2h`R'E>%D&h.1W3 A`pd(it}a"-2 A"礧0R6!w4\+I6{{!!n[[
٥͗@l|=я>Ŋ4^f!p1KmKA8I's8aFRAˍWڏfsY+?i{HᨈZH$z_Cug/
77 pYҾ^uHɤdmb@zԋQ!>Վk=|yUB'P2~^XWLD[	EzǠs"~H)4vmA˫XzF&iے35)Zv3EKr"HQ<"C@iB9:d3nâcb(yITETf-	7\Zkvb程1n9Eł	)WT`c]̿%{C͔*%-u6(<$xɨ-hZDAHے	+F
[b.&U:k[X^2}`r1Z3Z 5KW)kiW-2=j\dLlT"	"z(fyǗ^\a.¸7럀ʽᴅRG,e7^r:XO^gICu[j^{Ժ#!V7PDS~Ng{3
Ai=d=/e
}H'{^G?WNce\׉S{(|N=1u "a<6\IOÍg^u,s{Vk TBvlT2/::'yz1y4&qRqfTpKHMd%vJX&e=!t^SJ5/۹"9C>!F:n0Ҫz0!`pzBu$?_\ Ŷ5<;3}<q+*!5`%Vo;KZ FFxQr=}}T tR1JbΉ+uZN8߄-kѣӛ
]Y݌o!x{f@RV a^sS2hk
;+b @qYuUʏoѢ$AyA^>ZH?*jxG_QS"cV/\BHH!{!0ǛÙ97:0딜,$X)n,<f((E8rɝ{OJ7K^&g.LZbz4gEo(.` V! ?V$#jȡP5IwmF+@êKl6_kz"ƫp鬝e%ޙՐ].b|y	i8yqZO 95E5LV
ҽkS8cVM0z@۽X+E۶,;[O ݄F3QdthLy-_C) >JDt9a =] 7R!A'ϴc c'v'EK05r!BBD4D=SQ_UrRI<O*ą^sI\Gk
D %n;tY3-Jڿn3X_K0b wp,8Պ9hPzl61xǑq'qpWQ Ћ&ME78DN$W*%/+D!$;ӝg2lQ/0bE;7Th`Q3x۩8ifcI Vg0uf#bWx`L7ijҕFPR~TQkN* HK(1'@F:%S6 d짣jܐ)dJU`z	ByΣa>cF'^aB{񀃃hc	Y3EMYśYa%qDpbY	nQBiXkͿ:FR	X|~Pm=7u~=ι*'mA]1%kd'G)(a[805\Ŵ`	(6Nx;'-y ZgYE>[ZN4	DW&d+i_,7ϔӿ.Z{9|tX쁘ʈ+C4}ZO z,Z2\N"z,zjYLn^!Xo错xu7=D !t.MqyIR
/iKLI #zLo+pjTFkDy?m/$;R_<X]O +ݽFk|דF9Eڕ*e
8\WWzG" BX$RQ

%J"9ae~S-܄9kHЂ!POaz!U5T}$׍$}X7moТ4i\lSQT]u~Qpʦ]P aЍΫmdZk
6$RG U,+>[2*bbZz3Fѷ>APLO&ϱ\e`q2`[PpY[)(tnU`W݊>1q+
lDR@+x`i.\TY!ֵ\rȿ]-"կC@5
xgGn5J?s'A)5F)*34lq
V7ԪGhŊ0`'uau(iX-n 1	gH_BbW!Y,IJp")!-t!Oj<l`;}1 Ђ_f/"q~OoSr(	pl$hR7`!S*cz3(HPT{CeX
,QVB6IcN4~yôa9G	c؇wzs8%sCԫFWzovD1X߄ 8ëmMY&<2F#A<aMT/T]$aN0[$fNF(WHǽtsނd#78i=rC+`	FJR=A H_'dZְjm*CωbFs0i)2
!.$oV&*^F(CB!$9{˟0s0|ZV7tfײ4yL Y&%`d5Ov]oY}1^ObJp]X2ɴN3| d?oxb"(Bj44	('=R?R'K"W!x^yolgl	١	{13^tzr{
*wL:{taJv`yb 2!4Ꭲd^7iqBpǣz\ 5	N读;\fjZψ+IE5<:|06!k3j14[(ӛ3שaqj_CC7m{1\ [h}ϷIהyIbi/UYȈAMaK_HAL]KxZ=PyFOgo6_FxPxSMQ/W&I2~>E@ѤP~7!żvl߃HzyUΔۊ)v~ {oFreh\_ACnv(M4IU=,$$($ /DDPZVSĉ3BK;ױr񅚫S|αGІQ5u(C}f
,miLSӹ_ߞo#Bp͋QmVߟ!]ڃM{YdG̎QJlFXӷz{x,p
f )P
9*i
âcy&yqЌJKJn@n@PoqDOz	g[Kcyk7)eND|ꗢ| /ٌV։B6KZu磊<W5CEl)?MAœY6o3r6lvX~IHS)@NOVk,q7;(dD/t[-#ORR>$z1{fhYpwa2q}˖~\2 87@  1 $,2}0f3E{L1c8\F=&z*)*aŔ]obRSX}&=ZNԐ{!9ʞf-4B&4|&R'<o[|bQ^U)SyYz:@"LD{ҷ	g5am RIEN纓\<I3I(_迌<G{8n(a+X>n:1T>p<kƟO't,~IUZcl3S{>2`P.NT /-:HMT^.~nD#\p$ڤTAﮯ/TGC=Mÿdx>)^{m]2iTsd,׏OoecU=5fգ?toYrАrm2鱛VQc#0Ia1-@"yk7 K"kWʢJDI͓š0niJ#2b?lê%VύCG{[$WּɫipxAnAZCJK&l׋4j`S-
<b	YOL!إb´6˃`o)2D7VGF$63	ˀkdj:W+
q8$_gGL_Q?Z;@zw܂r96!N,HKjuR><Dנjqff%ߴ͂B(]Lv9
3"k8OO)`a^(gBYmlǳ(j0( id"eLW	$lypg-vi1"9fWAvotnf)ǟ
"<Q=?/؉\ｩO7ORg,R(>p?
`h9M %:+ўULXU\~`^_t/vacx
X583`˂|ԏ0{&t^@e`$7S.%
M{*
ѣo9o^"s\#ƆqIQH7e=%iWK{yICƎ6h%I
T&f9''s:*\CilW<}OO揂4S$k	PݐNjD͝Lp[pS1}0s[MM`yb|1`3"nQwvB;|?$njVkknܳ;?A%rb*5ᴼ,d6鸌ͶI+>t"^}6>sL]ϜHUhk+%FM.Ovʒ;i2T;zq;jB1Wpqh=Сxߌ]Cy6F12.xIs\ҿܱk{J܌丼xK0j?2 
dZIE HYy
G!HipVCwG2>ZUQE(G %;دܮJwgmdgx	
[zE+%}<|Wx-\tM1_m:Wv2RTKwu 	o	<Ψο:51jjzNxc%Sp;@wZ=%IQ ~LOǂBxS.a㘿T$BuộYkމR5ye|ivʀpJeªn1lz>tތ6H&0̼Rs|@Z"Y3DnWMX[RzI48A++x3HBF(;wW5fV}/ah+&PsՏz='$3Ibދ[n{ѨY,$'A؆01^K-bu,]Fpӛ<[]v"R}f8cmS͊ntb-E%-dMnq0aŮ$lk*?+a%j0RW1P7)3c]CGtҲj_uѣ:fPğ1Ù8Lg+ԫ0>;=[7Y 7X 
5RwM2ΛU=%#d{ɠ}t-HqCŘY֎rOV4I,5nixQ863y^;ns<Da	`Vµ0&K]#,m11+}5$nlv]=9N{TQiT%Z׋Y4T,<Ev=w);$u8I';cΗb!&隄͉Q٘kZ6hkQz3sdόjB+Ѕ|>>Ȉd:%h}cxUy9KPo׬*P] sB=^)RN[s6nɍHեѱO1f_
RH0&%)0ޏGd|?ӱ0EopF=|ĻL{Rbɒqvy_ 9if ;ȅo比]5nؽh9XѸapaq֌)8#bV`vYBH.ٱZĕmOՑIt>5xPbzzC: `lӊf
ώMP̡tR$tdbYn!׮U(ᜱHI[?[ⶕF?>5k24AjӹVIll5SI.}L;dFLD%(*ICBbݒb}V>_R{X[S8mqeϨ%X[g-)SW ?$F<@3ۯ,)%8љ!4-]'*!MkܱD B()Q\BP?RJx_oװT*;d=5{SBv@VBl-1AVQpؤw(@%A[򒀍8]4pfK<J&R<PԶ:N3od*1DN2Ĭ߻ւs~/k/uыo^cȚD&^巗o/ggVqu?EB4b:GV/Jscvr+66SP+yB	2dX9彞?p:^>L$nFF@oޞ	p3h+IQ	 E&c?b:ָbЄSX"<sdKQrbG܇Mp+7w&cu8bUuﭾQp[#2xuzvr58}38>ӧO񜛗c.oSnk+~0=+&!_j-16UQwf(=d$YW
 c5!-%G9_~KLҋ࿜u n6fvq6NB}J"6NnrxO2d=7QkL'h% S4FQԃ.k;KQo!fMIqs"=QZ/:x6ˢ^Ԙm5~шF12Ń֛~0)ʪD̀LYbq/qԭ댨[3OzX[tIJ(㨪w84`HuhRBِ6DpkP|uj(_k0LDEYi	6zE8T7YgCB4O2E#Vp1E"b,p(M2Mf-5sc^zWmZ9"Āy"Gc˪~BrٔpU@BVey7%X/#qtf}kKxL dG㮼{l6jDw4`ɰB`(x@H~2]HL@'T=p^o;ͬRS& eO 0I&G@>̠zçT,* W}p\}f!Ge-f=h3f=%ެ##`l&!*MضlG5l]̙x5jS$Fp-|g wna:w0>НQ{1RLpF,3)5%$!Lz@lj7)CN\<g?a{$d*VɫC#tO֧l&TJ/̴ÆC#YBCIR؈ov|g®\kXUl1Ug>^@Dڏ@TR.<zdo~BA%CAaE`΃<TJ~tt2nS@.1]$\
¿'\sGC]!nXux+
rd&,#)jWs@oa`0>o2e7\sǗ꾔 j|c{D	p<1m^?:UNj,[nro~BLP1y1PE<d_$!ԥ7wC cv]1`?b)RO0I|rܝJ:>=Ӽh?<a2i10.O53['u^b:&𹾽YtrM5#0QpF r-{S9C	|[k×u>XBw*cmVm!R|3	@[0+M7Y* zRťeH*Tb`SHS| E^bRO(|QNM\-T*Ȩ~O6/=
mF2XlZs>G?Bc'	Su?$;
3\)f	cò7O>.}fݕy9A[؟js˅)0yKabG"Ѫ1}jG?=TZ}	nK:';0&"/Y0a8[jM߉KYDpQ_1BBQ޼8*Ěӣ	Y,	.@\D<p>y6kxsDp`Zl3i]FڱAwʼGNJ{gϞ.aw]yXv@WԨ`E Ïjx8oqoI %c0{/F1|i|b)EvM^?^1:TM$qD:OfJ}L6e<*~:̐	_cd3^B%qw"=;!DOn4{Τ#j/_CA7R6Ӟ\ȯ	 }TǣdSI!NGJR'H3%R#(# 6Ȉ
9_P7Ha<+1%E~Pcsb\gs/0 68^"3Nz0|ԆS%|l&d[v+|
{wmhbPl~ӨbD"gڻ|{ѣDzJK6XۿoP|@?3Ā)XA]yn#c'oOr!]҅vC;ipyIÎAgTYIGSN-~w(C1Ia9~ԙM_9\C,I^	;G		84  B_`h-oHx%G'wZ9!v@^qd#vJ1`rQ.v7eMKj2<>@eZ7XLa%G6v	Qpɻ4k'1nY7rU$N, ,0H*BA}"3U^\r$%hizi%͹3U ;pǁ |cE^mћ&Fnv:'~ؕ0;:_
1.}alk[{ J}H.Ma
nj?YZ<R喅,:|Ǜ&z11DZI"{2s 	6.)H 4$,])I22aVA*ٗO읠# 온g9/X}aP?
p0꩐#/5O iwՖiSX!݁d:5.p41XA+BlTp/N e)oSpGގe4h#aE1eg?TI[ċVuFsPZăDW_t9*:FLim7;+/э8X[`W$~oa:@OTRYg1A]I vBr;fD.
32S7Kūjgz[
Wٔ#p Ms:ȹpXX M	Ne9c`-B;.nP%ViP3&wFdas(
>*$	b#1czj*)N0)iK&^?6H^dw9i~Z:&K3%U(A@j@(XG$ZY1]neT;n,b3{ȭsh+Jy$x3^hy$"_%,1؜O)\D h"3s
e1>W^t 5YfQS#N~Nnٿ8ھ7cshY+yR	bv
]Z1R6("Apθ|[aw\}Uv~((kHpf$D|p1]ia{*tB*G\k5;2F8& ̒-wn4Nhky|F"͔.ZB5NE(hEi.Nk<:l[eRr̕=cecGaHvҔQ9AxEN	(I5a㺚]aJWヶXrY.y)wd9A_9K=/YjZгKeϦLQo_b}qQnTfc6T'Jf0&LLyF+w_n6M\N#5u9*/T=NClUF%w?-?&R:91$$J4IQ$AV.UHU	ȋ.;3~-au[zbb\Jz*.R" ;tMuL6,[pܮ?мSfMQJO	15JhmfcFm
9Ry72UJ\:td1{!zs%3O0Iєvlh2jű Vj;cKcBh_7>(=keE}⊐aD\RYB䖔x(/̐}λh-s(KQXjR+RZ&CaBo2T60tW%´nfV&jZLml24hCL!e<i3iꭑX~M.
ǈDpSo{{^Kp1AsR7)V= u$D,BYa{w4n͸	ȶ.-m5IE
kX:ZË\:*8b`Z}A/`AJn}i<IE턝WG(TFdΧa̙!^	
=9N־{N爵-}kӾ`U	Z gۧB/je[SPYe Qt?49ɉ2|,:<9TMlrwz}Ja	i"H/9wHѨԋu
3X)7!mAA(g[܆k7 Bu8®;d[W2)p4@'z.'(]NRUow_~EbADPD[P}UfݙF%GpAX#dTP8+O<97J\S/u적.RkOg1/3杘*[^ I;lNh-vx P[wo*s3KD5*a"`XMfC/)WPxpǯ :U$V~ƍS0D%p8l{*;= HJJ_bvb'E3cGLQ]3)&Jtp	B[~JZ5v) Q<@a1eqiJ`tMJWGdO0^ȴm>N`T0r#R >}y6nzr.k(ŅyͤIwl60OT/b(L9ԯM+{옿U;
BZ 71$D/v?v Jw+.IB*aFo!.t8*I>y̧H.瑄{H,o(r(
]IPfQvb#J)wS^ @=4qa/))8@宒H|ch2^3OzEf̥crKe\81ۦWum&=Hӧ/,3Y@g\VRf{a^-f]sƚ0=eALD~4M}3'ȡN0lR黸Gp믺ޙqFaՃ]INŧ@birm˰*]rBb7<_,ʠdg]Mg\q!5Ł/IȒVkF!8Uķ!	(
u:rsT95eAP<Y8+<rwŦ{h&v/xF ~;S28 aAgN
TkvEl..xyislRrX1)O)һq:0&9BQ༿5.]DL:m,knܬ~uPMvXTߧfO=O3=<Z<ʂ(LMV޹<ZR2N`Lϧ(=5d"Z|jRMH48!DXioj~]k+FTM봵^:mS;^w_?<JziZi@s٦+VS~(N	wz0OU^-w(.jq`.8LL/Yt6eGıL;LYd
䩇izSp|3¼WD=LIy}ŝvbUVȪ1"u:g6 NUpZFVMrߦUlsTLZcVo3T>F@k)rz5MaõႸF6CL?~;Iwaw HAX[1OFg% zOG#+\mT9}b ƻ^62MMv5[l3FR[!E	<"GڲBR2
9ZC6`̮UUnnnpfz={i(vs\eK佋y]­1Omrx4E1SS/٫N0ކ`&[x[EӳAF/`bS-1	q~~*On+Ilpi.ʄy{Aǖ6=0'fe_ODn,5 u<3w[V])K=RߋF/m+߶sa(3႑cigU(ڕiՁ,x6; I2%+Ǵ7*#Z@".2%%z\ģ`¡1ބ#gm?Cs|S]3}ps>xY-*Q=έ!a#K'c]nDרDvE0p7LzK~}q6tNh;~ E5˶xS'Z͈Y~ByU1AN%`C,򪩈FAyo1rEI!aeÓ(r>ߚ`c8T{IyT=ͷ]^uq㌧wLV=EQbRs+jBHX!`p݉O>,4n*<BA­t1MX.Zaa_*jeBʦ}n@LvR]A	77}`jA.Q"V?^"ꂓh	۹c(aDx16x䟔Z;)3m`rMܑQYń|0 iF  ~21;JՕa9	צ_'їC"wl" ?s6x7;Ltn?wWH9}|}Qg2SƓz)q嚀R|ZUF:(|lKNn{2\RA='ޚQ)#MpM	ci印6++ωβ#p椎.LB"p xYRT,TDla=s?)#8,h`0B6-:`]Q҆)!0K13w$ui͹-.kqہ
)J8kÈVJM*&Mg>ҚOgӬ>> CO^$qFv#U>jx,^ŊQ1z»Hܧ@'n@fwfB}Hdc~,~%-舗Mɸjv6nRKLD?:7<Km A6U>u>nZ)HF70oWo>v3D&ke@؋BM,Sӆ	ŵܕ|ϕԲŰރ/R8 K2{K?
OJ<Tq2 fS*(7s(q2Mx{ۄUWVLn9:łS.p	-V#De}F&KƝS{٘'ѱ7#t>\*@'Σox11[lye/I]b]Pe/fx Sj埭IIQrWHRhci(<NEv~1׸m<maPҍ&w(g̘>Aon#vҧCaCƢb*_uR8-J!Q%ie%|>bօUR>XOko1/9X}
sE,]ԛr䓩0S_^!Nx_lwuѻ/*Ҙi
6`cc#G&XeS%ɭ-F |c4"Do$⸄
̈خ.̎Eߐb2oFm	`;ovzUCڸulFg	&/p'F7闌rY5wx0Cbc%%1XK@PSFpR3y|j#ib%eڻROMi3NySWZb~@ C\T:=xm\7uPrV3<AO뽔2I>B15G}}dpqy/w|5v.N._^]Y#<>t[3:|`Mo}F ͬȸ36cZKgⴕvg}Uؒ=0˩(J܏#6o:|S%*	:.B'BL֨tWW*lQPP(,GH[G	-;ܐ|
ا]aRS5F-8ukPHe+!U#qlIU)F9rC\L#ԏpIpO0%kc>^cøz$C߉Sw".R(~M{Z @qj)3w{Ԉɐ)7$Ni4#l(R^,%tPڼBh:oxPy1(	rfg5t׷>&XKM>z!#Ѱ^Z:]S#6:t;H}tDr2lF8kƛ͊SFŽ]D]2-Bt|b)pg'	՞{Zt"2T,2V.btNQNF.	3[󎐥č9[d;ɭM^iKWw!QGǔK~sX]lwWCWe@W D)$Mߺ++9XO9u>	6k6HjhA.zx=uéT5j:mi5:-%A>¿DTd1ne>;Z0w9qFNz{EUj޸j[?gHMC1nla"@uF)yTC @ymT?hwpS	P0-dbaֺ50\c\=#6ÔM
cN49U2:KEfʊ锬dctwٯT#*maA}h&aMpN7M=_B}Bzpv۷ߞd@Poِ2BՇ1*N0|z3]{ykNI=T޽\%AuFa 5c7ADZ51$є |p:gvԥ!bN[ЕޣuSW%z1LU&SMz`%Z=IPd59R1k$38HȾ[s!6D?qߡۉh"#rgv"r<I^o4\sY"2QDǧ
EʩTHѾW&7c]d|Onf9e?Ұ܋,qKanLE}wD<.|w (pPj@aGp	k2b}bLv|.;DHsShQ˕"gMEAB4/uǦCOL+% &k2l7%~<MhhRCWQF2_ xO1XcOT'b	;Ñ7Q?b]e Uxyc=,trA)뉕ȩηl~X9$>DK1<ɚ}lnORjׅ`e -RgO**62gS?ػ(ekp1i)JR)ʮ$i2jndɩ^O7 L$@vJOky&h2sS[cf1g"]ص:@h5!iQ=l*lZA@~K*i6qp$:|mQee	&xjEȱF8
	y)|B.7#V%6(qHӪ%e}amk99 &ct<:@bjq^L%2>[JrrQ5\@nzrKX~P|Of^/;?" "UShxÛTQ+RoB8v7X%9PL"y,G"\*:E,zo#|A9b_8D%UgXRԹ	vf'nG{r"%ߛSI
~0f(
UŨf]}f|J_H\}_tIlE.:XnTag"E,Y]oOϠwB	a
A)n
 Ā4bM	ws:#ZӡhdA>b1kUg)lRF7?FSVn2]	iU+Kژ\lBFB5h<yo$>La&"Jf-xY^ Ӊ4^>^."XъRD}":[Zf[6YjdnE&gYcAKyo޳vz\NӶݭ;> d^q)85hd&ݵz]-}.XUd89SjMגl'Ja[	^.&/k $YCryܡ:iTb󰷃%4N
܍b#":+EhĻ/94'F۬=V	K5	t}0p@G0]%os\'[]*V{e)"IJ1kv*n67Xg.bHMZܬMV7z؝C	Ae	Mtqp"k^54}V;DǷIp:ro74Z	6TpAX~H>ϒMRvRa +.㥈={;0bfZ1FRvȆLL*r[2%Z*0g2J&\jH;^EI%IqaȫJT].J$GlLG$y-K~ME,(dN,Ck ;Dc`hP>PF+vɇp	azop`VB°nBh.ؿ
	@4=, =dD$igUaArrE6^2[DjzIJstiVWJ7DK]Y:q_मUOE"|m3tmmI)C8ڒejzhb9؅A-
Cjh¬j,kZ|^@n*ɐ$ _BHB׹o4u	]Y<orvVSdN=Lf|˨#Og#/i7JWfG@$*^og8:$qvhFYbtEEF&=_ZNSzVc?EQL9&$`Iu1 i3d6
ŻwЋ+IѫT%M"np20tw(!:o	R{8WsRwJcnSC6Z0HhYZ,yj,t]놌F:Ĉ.JW]29 նhәA$|Cg__UZp"d-x57=4]m߈E̋!,p:Mq$HJʨJW+TkuU=LBjgmasHaZ\h3e1/eLI*r=~
;Hכ{|3.4Y@|$bo	LpfUolD]]1m̂'@&z\!@|CiH-_S9BߙUR%2"+	gK۱/UÇ8ܠ]ըҊf-m @<'H!)p4ӚZS-~EdƉ6p]E!,Au>obTM.+*g7Q-NAngwkGW:2s

P{}N#,~;DppbHFvH%.lN"ϯԄrV Jc[x8
*c;j߆,bu(R,U^H^.~{ṭ)5up`?ZqLm#K[&}/@{(tt8iEOcTtef9#	]g^RvʿIopdiD.ȞK2O%&b`}Ϭ`NiZݔ^Ҟs/>ջБAcqf4x[<qzSF@T$J7,g5.cg	`2謆e֌pe8;D7OnVlT%cGR?ɍ᣶işrt5vo%1jb
ûm.}|*Mu9*0{<\Ҫ1Wɢz8MWu14/ЙxǓЕfADCJnT] %SSBfƆm}ϙFM,xwbQݴl+>h.0EHcՔɉ'2#E<Kܽs6Nx?*0׏fHFZ\Ahۗ¸U*HlX>׍/ѹY[ҹNi#fDa~&t*vbS(k=*bY/E\ld'Pmmړ=XFgpmJLvldJ<&r@S sg7b7ӘeCxq)$7+XDy|jffqPXI':rE,27~)w	E˼fl@5V@EF҅_ j"x /]^8@zځ!.|	zMhÆ5cZJ`P-Alsz^I3wV({b݇?%|',_>Jk,mS4Y˾فv@NM}t{f_oph<1lVd[ BK.RZBc]D~H!7+(eInO:tօ1bHA@[@=D?3ϛ0`"5êYmV`1)3,]4Rް\EXyT(KR[ l(Z&TvAqRQJs1	v?CۙxvINp34#B	B
@Gi1`3=@(;nA8{}f-`DWizCZip^ٽjN	Zo}E׃"5rsEXl38ɖt|d}Р!ѸZU*31PE?s!ҽOP\\-LI=`A밯ʅ
zoS0&#UZ+f.YTqMT7^xozpGMgN{P%R^*;=IAa}׏W9?ͩ_p<}M|	!{lsا&	ꝶ SRX _xg'ܥ5T
>rޓɒP;hRdi&˚oLT*얪TN	]p4賐V A>wء"	'q 69]jXG|`$ާ'!p_W$" ?l'XD,n<L5G0Wi7yԵ'oxa#s-#Vh:꥖B JJL
(ԍ>5DdNhhUpU)H}8:גnImBU6sMvNHbQhpPF1&G<T\(Pb
(,ȷwjwC?aZmiq$	^ZL蜴U5D^%!roywF=exlK ;&"5yRzw+Wx ]ĕgE mp/FYPθ,:#P|ď#O&x"2ؔeN''G'R-ird
B`~qFr]N.˟()%ް(
4'œ*:y0v^0bw_2Ԩ~1x{e~ZCŠ!vHkU͌巭.qy{9ĺ|DJ*I'h͡< ͍*#J(TH
JpҊXV]9Msm=(#\j%V
4h%n$$0Hq(g9NjHcxcZeLɳg2.} Ʉy`#uK5B_eՎ541YC -K2
M|\z"+QQ0*c!X9|`/ݵnЍoVr)ZhtМs)HV]:K!c7uAwCtvs?L[pop_#6LPUoi!BPYZ&V2pusxulpLS$6+N2V!ψL]5ϑEŹ"<w'bPp|5xtaˏ"xF	A	8"oĚZIJ`aE)%P;JDЏ 7_A4RHGq(Tip:JY@I\#CgF]*}Ԡڏ:lgNޡ}9v_#,)F;-s
zEZ5yTDo5
%;o s:З2ʻ12A}0i>k[/A!lfeIVhB}؀Q{P
%H'aϝ]/BtyS]ʨ{`U$f~aw2DJ-#
G^!ITS!#@N$S]ksqj-إ.S|nëRd%nM+·HaQ}nW!MV"J~HDf9QnJ(V
0|\$es8ㅜe3cB&>rꮌk3DrnZH8)L-eSI˲Ქ .E?fZ21(0]ߞ[/Dxc{D-)% Z1꘨_qxa}|[Ke^a~69L2)[;mPm"c&;|wV=L7=5r_ts}c/<k2UOx
`h&ۖxR.(عYCP9p1J]7>潾qg^Ysg x,0sJ l?~rjpOx5X5QhY<  *1~[I!}w䮑,*E{Yi6oq}:gȱnt޷:,.]ﺛ.wVO 
pDψՈZ !b	XoVcjqqo- lPtgSR:nfg	?qWtoP96/7Ľzg%ug+?Agg<^esVA>jdEQ}q*LyeBdĹv;3fa܏Ae"qdaO6.cŖ׽:e%5WE)6։5ow1/K*4_#1[C<XYqyo2CyTp%GV5PD;D\Fu@"܈ԏ9J=$Mi<h@;v{RQAx3Nb-2t$RjB$Du^,C:kB	!<Z:6		לk^l)dDtCJIi@>475OGvMIn+}~"ZkSk.&`b<#I;H4߸QcѵmvR7147*@&:WLULvCfqHCr'^dfO9PmA{mE,GS_Eһyd-lbNWRR8Ч@%NGLPZ>͡6QsjٴsKo>E,є^uYkhD*g X"[{h%"[_2Uq_'nU|ͦ^tl7OIR7cz!/:[mE;NR{ 
N\59`,JWch5;z>u+`*Eݩ-ygWN
$GbU2}՘DW(yI{XYS*A}!ͤMm2bgI>	KbN.kODD~B2@"a`0X1]an`>x:ãm%WU8pJ(!]|}{4}N`R4vScba@U7N"_Kvm}r<91k*DR'"jyFofm|(xތjo1T8K0έU\mc$BXsCC{03?4IjlѠi,$ɩރʦH'ޚZ;-GIbRV+]
!^SƱ˿me;uɑ&H%,ZLsm~$j=d(&c\k;a"2{-l\4`(לʈ|'aZ]uF!X=iRރ0:a0`A
p*@uJ91Nn4^/cgt:a%g2eI֦B~!X׹0˹zWL8]jG	Л^Iw-;u[7P5ۀOKø>HGB*O  9ݯ,*,
0Uu[_pPV_gQXk5'9SXîGf۔$Y]"NdsN:=jIƃT"`=vI{x~2JwX|QY2`Xc)*z"-~
a 7[P	ƍ	E6ASXLGvha(fC$jϝEYڱ^W僤=L%rTA?)pIlt7+,]CU|^MrDȷCj a)S%o835\-uLyIX^F68f
rMLyWO{?n!;o=aM K <4V90$1mqp.Mg{鏧33,x-'(3dF /:(k>'R/򫯿sn6»/A<(|vCTa;*0]o`ۘXeww;oN'H֢-) v63Zy9p\ITnltyã@ih*2טW17,3_koc ǘg|eB9HuKO>&j_)<076Kt
4D77
ر#բ疚@YkJc>,ea\'vC2*r{r4{򻆸	V5Qy3zΛ	ӴMҤa\J#X(2"_ OO~EMh?|)-EU77CLEb/TK$rg,?>gYAqԴۂ" (՗	ٻzoO˧E,"s?4:L#tJuл,mf9Ұ(*v&9Gg'6]<;ACkH*R=ϰD,ßt8~<|ȠtEļ.vf[^u8H[85HeGԓKX\2j1OrTӻ95ttC9m}O
Gin N0J>|W>* b*vj_UŝLZ%eM95Ҡ1-HKrsŪqUgwf-Pn.8f)ϔn13*5lM:Iz`ַ]n
in\O((/K¸#V+r8z4R	WeVp;XPx=a|7Eϥ]fƅj{4:bW]LR)5J9TJ1;LHDh#0ܒUc­xW[Jчq2	DLmhBo
*Zp@C88g$>wz&KZxF'B~C^K}hvtkB9L;(*H3"eC_EHF^ӕk+2M2Bf:'.mfP?I׻qPP&N%a<LW4ؘWD^mCO'Bԯ7R3}5BlR=wK 4	l=2sXMF<-1dt5KĴ$:i|$sYj{SRtzlSBk]	WFJsDe/9},Է5'{Q_'ck	ǀ!pH=c}J+~Kʤ8 ,n¬p'p&@Xs`#/i[CO)Ewo67/#ح7ŶUsՁ)}Tؽ(K&3 ୐+KuP&z϶S_;8yok;mF}|fi7m==33\rzDt١)	x(n&}ߴ6|ˮuz!PG@
f,Fy9腍*Lx$QqG"@6`̦>@퓢=.mVZ_LO)IƺڴDڕ+;4zf+bû\Gy|T˩J"++5dGװ>|˚ b۸OYybq3̻ӸCfZФxlPZ866IJ'f"'H~Z84eONnj]Y]
P]=']W7W/*_K+m /}Dұnܩg?>t&䖉4K.=iyڶ7ܰODTsC'YXe1]DIFOd'8ߋ0e@;P3`By5iخߡ,Jj;nVPJiL.Q%~Կ䟊Q9E)f)?dxpX>Q&
9Y{;*)qvE\$HR&idgHDsQrE7tV 8)KW"isK%%Et|g}l +H9x	ܥ}qǞ>uLQ$Mʵy?ZRma񖤷nI( ڳԹ&2|Nqu oz#<.}bT,r`Э3(wk 7B~ΠYʠD%jfRPtb;#h#I+k-YX` +w4K4DK0Da/fc|:|}WnS\ݣf$L,ṟ9d盬BqE3jXkX+Z]:s{=
Pɽ;>kb!VW`܊ޑᯉ7y
tpSŒ/|@ڤSa.7eʷޗOnLMw˂]^/NF`&AQٵ>#!Xg3NeA&`Pp6#Cka'O(q2Lܜ!rk:d)5Rg=_U=n3
Jlܱ
椲IOJZNC2܄Nfa{\':S/)iPPIVw(ΥK%'_uMN/{fd^0Qn+[Zгc5̝ϭԖ7ّ}"ڤUWܞ,Huʪ.KVw]y-ÒI<9Kǚ`iTx,r/Z2_9l:jomY׭ͣyi{vDE8z HnN\IFd7l?BֶC8G}#U8܅,k5P!x8ߛjy~QJuJQzAb&\b<>oYADJTkEЄAs8rN;rXކü4QوJ7au~s:%ui؉-)z܁@3Mf{Pb?`Lyژ ˓Us(	X%"\
Q ͹EHuE
Xei\b[YbMYR~ﵢz375N1^xҚp7h}No%O5' OoMsVJkb+psBYЭ\nLkPʝm>7{CipbK[X,fyec/rPQ3GK"|c!.ʪ.bKFd]`XhwHݳ~CPG83)T*9)@@G.yXz;D2~t&HRF}Tp
DbV)k&6K"z.:YloƼqؖ`۴cbJ\."[&K:*;_as0#9䵱A^MY>RkSXy0@rrAۨm4˄ƐkzR_Q܈Jlͧ]${E	a:6y_5UǷfYc5}Zֲӑ6+^n0Uf'@p9/ЙFGibn9v^Y.o%Ĕj*H$/ˋx(EOT,
Չ`uWB5N KΓdvR}i<'VO310fV(ub-`o{/Ď~va/{Uy!R:a'e pƖ:t}IQ<DDH\uf0:Y
2*%ʱίm4JTK^X:zg/&6b.,1I1<|FFGcQS s2vJ<MBGyL(|&/xԣ`h{3JiUQ0Hi"[D\`|B*`4-FSb	چ˅!J`Q2_,fM]a Jī&+vRtQȯx+%fU3CH:Hh>ʘ`Ȯ X)c.naxYe¡aOڱd(mF!g}Iq^bgFyKѼyF/k@c ބ~طD!y:݆U':E0Dk>/(G<ndصG2=7C݁f9.o^XN`:Ћ Ȫ4
Y妥|zIƱ"y],.fY:AF\ o0քY&V'HK3
tzh?O2ߨ'LLb^QρF0!b+8PŝԹG*{If*12a_HZPMjJD>yA}St/r;l4]5٢=/x!f0ENʓ^X9u?(=MTi&%wxI(*2tħ1jww?#I*/0^F|(]_czͮ]#[Fs31n{nS!`=0̪(Տ~g/>4]y :	&P48WEp2&Fpr1V H
氶H"jcQ9dM7ˇ~,Rmbeٗ{L?ӧ>KCnN&cgDo&G?csTjo:^ .d	~~@ŃL+:MCLz$8;ţS>ov,w<]TM^:0t mI1KU%Q?,tH#kPmlx
o2!I{	V·>9d)w%e++bs3|11\ABjk@ܣ0e<7'TǏިUCa%"(+.@}3_@}5KD=S8mypi \Vazs>::=ɻRB){g9bEqǄ6V#kkoS[LOR|_TޥJ 6'Bڐ&!bCQt cQ,A.D*& %Z^SWX՝dCZ>yu>(t6,0 gU+7S h*_M$Gj+u(F0$-<qL~MU׼-$2E>MqF;2tbVhջ[ ^z+QX03ӝ'q]ڤd{^i1 QQNAj:Qy\b9j#-wm;~%v7 u/1+DȢ
r3-g[rVι[Sxc207~ugb$9e3l(⾄YPlvA{NmrkIC̼D&&<}6$ 38aTq#Mrx;e+q=Φ\	8lxR {c#c/{5cMtIظ&ǂLsh\.HҔs|&	咫ˉޛO3fQX"06\$ɥBg[FR#۸qq7Y/"Km*%J
	 ~tn&R	)3Jgt5ΛL,&1Jٸ	F]L,6Ɓ,r&Cխ$X"{Џ
>j_I[O6㇖ha``|!+VBP~EPThw:E
2,Lʐ#W8a^	9llV;vkKyhRd$5fcpl(E(a[iwB-`>~W09f2/:,g]Sȧ\(pdm`TXb	RM\+HW)$ |桭Ƿ`p3=˴*L[4^o-,f39zw^+OOE\m9ΪaBju-Pd#@t7qxw{T3!!oN܉ev>a9sOl[媗XXf7MցE3?g^ň3̊Jڙqh=s+2XYw;bCb HtOڞeB벟JL}jdzX2xf0$T$e\A饪EzX!H>2Q"Ve׷h3RW5)dJhu*= atͥh?8>V;YgۏS>HQ7LM9$X LYT]gF2]jӅn.$ ث˳Ύp_O7F7oI`j4/n6_+{/M OQQkY{
jӁ;Y ϱ`E6|F`u>`vS3XF*X()Sc{DL̈́ĹR$s<.;׬C;ُ,hu|ttHO"	K$;ɯLPZ-aԚ}t7a
kDwxuy̋ӔxdGM䙅=)+u=jXK>R5R8A2eq<$
kA'!%~oX!;ZA^+T%^Qb.E+	`1ЊM4wc΅9V)qh+D;_q.]sڋ
,RLo[<\WU¬7TL0sp
9#.b)JrLJIKP2ՓEQXc.sk))dQ-|<JZrV%h,j,G8.KHD[`HQ1ZG;5B"`pČrѺVFU{3L7|uS/$+~']`<xJ*6g|5o*dф_WK~M~ŷL7>TƘ `F?ؔ}eJ0Jn?Vpa@ ?2bAhMJGEiab2\*I1!&TǨ?*qѕׄ?G;'	)ҧYWO&"݅zl?0~Y;
IZaw)X{ATSa/I/W~@n#}$Yz+(J||p!aʑ=Zy\#^ƹ)W>{/6!`@C@zrR&aH yr0'"{ߓǶ%iZl#9m` تǠA.u}G4wpx14g'1㼁gX8UI#7(!n;nfF븞"Ek6ڀOP#W{/J("ņcö]q<%we{p7x{c=G߇b8I$*W`D4Cx,OAϾ mDox)@)9*;7SqVk\O:LM#E1v>/dʷ;/>ҍDp}3/crE{5oN
j[UJ.R(zj炄8,Mhee7?U';p-6~>r(p+ᩖCZpC5R玮_wHZlȀ6ɪ)aQ<mI'LqB&Be~q& IN5thKgMga4<um/'1]xhXel%SꓠЊ2ִH%:)$G0Zw4M:_OYE:|` QKWK2lԧUz1{15+ux@Rw$]T؅]dbsLMNTL'O6+머Oob	˪7}n4140p ^]~#]](2ӣ咀%}v qNS/P$AOΏ
vWXi].?qm7T^7pN	@^j0$ᡯ%>ODh
ÿڦ
;wp!XE[֠ڈ0_8-ɀ v/Vl6"5ΒB YTA׈*k%@|0]b|jxl)LP:v65iDL$":hZmU~I^ۓBll ҅n3ĭHA;%C<y%
i;uKd2uE*.qሄE{
\wHpvWpPj̊褄 ⳘXhh|+sdQχHk\}#SvUKPaöi*o[<2^
Č:x/a;2nc]p_PRB3DCLSDVa=[|Q渣Ӕ#TRv{lJىAqjܙs̙dٚt}2[5:ԿNYi޷T6J_+I(1ICc);=
甆Da]n#	Z0 ǺT 	"7n#!âAofm̧1n5YD,7<6G0ca0隞=+o=`89
oS얀F*~S=.PQDs)W|TҶ.T՟8-lb/Gzl(C=I߈Z]e9!첾y,8)Uuޝ}5S<PQZ}Q[BKo؃|q?&=ܕ6=wOc8a{RaS'AEŝh5.1JRN˯H06Y}RS>먊buIKvQN]01Fywst2M!˳%պ7K0"q,n"@ )[E6)	liGAd_lWzTU|}ض
c]O@e2篠iBk>1{A;ka8s;{5F"f`u1,!5[nZ*_*&Є[j+o"e&zY|U|.4G&mRBlpݸxN#|D~m6!Kb<HL>uUABHѺ	8eW+bomq)_ov-B;nK~=1Mi`HQ6>7vww9tvzW<+vbQRKw}7%@)HV5(z(Xg͘PXL'TFhCzTCtܚw5G#%㺂p%|4";}J34Mgoa"D|8L#%/$k)/74	CU9x$f̂}]5Ub n2/e6#Р/S%:AzC.(.̢-ksL}@A1y1Q GGߝN?w	aL3pgA6XBs!+n*x> /v:]0Ϭ`TqQ'p0#Hbj['64KfK#	RQ0*qz70-fb0`tĥR(ou#۰]3DOp}s/b;6rl7(kqoփ3pUwZ6?딩~>+(s $T80OnWY%N+z}gb 7)L2u1Z=̑e$x-2E=L%8N!H%qs>1=G;fS4MJ{}0էZK٭6Q_W&a:+*cE馤8Ae5 	H9SEabqaJ/4D?Yǉf;^`LCxGm'dH
`c6v095ǋXNĩ˹D$>X^cZC!+XڋWLdy_uGn&/`>|1';Ef
&.= D5NO:E8:`T[ I3CČt2{G3DIV/>@w Wtr|Geݽ_y-4\I}s1a>j[ꔜ$)SzÔ0	iRv2:wM(أ&%AOz9B]ߖOcYv./:DɢX=XiIm4It5Ԫ9~0/t~XkRBt3^'á@] +ȏդEG\|%5v(SSןe͊ݜv)<̻&,ף- ӛdfv(Mū 4iةtb0HnpK8Oܭa H桩2E(e~&zR~J;foMy|]-\VKݾ{ -S	W  QvDXGu@Sф %$6I;shM։#Vhx6**mlG\v%TgNZeI65>@:	|:LG "v	qynjE7!x`8g``riV`Dn/fJG. Z7Be&ӭ*}}X/cGAoz+<wfU !pߡt(IIvfwz"h_xpoQy hM{i<I,4=0"E~XD顗Ua#)Ǌv
#c!

&A
Fy4jnLOGW*pb	
YЏ[w6wLq``m"F
}bsOkUbUA<0L giDXho`E4H|P	}$/&Ǌi4Xܧp"T1MʈMNh1.̨jGi&d)T .fLM^%N^|:BA%4$ӁXFrgefq p;T8Q(1'*K|o1ݮf8IB0BaNE-g^CP"Ms~X3XaJaȎ"'%{=F_+,kv6m(%]Q$aGA?6*jNܪb0>즦';0zC?Lz1K7Nj[aQMk<AĩL.LMa-B,Ӂ!A^S!o4uR_eç:حdP[˅6ψ(i +xyǔ	^.&x{pmю?f@g p`_0ݝ<v`Uh30OO;jtw(ݳϝ ]aGHf}iQG$[`t˾cɩ)wIZc-VnsuJo/Af | MG!&b:
ɦF~W2F&lqs ..7H6 	׌	&~Le An3`&4{1gXj֒W = vR38Ә|SQRw)c97َӵ5:LCN@7՟|9՟ju/oIbx>dRQwLJQsR+y̓f]g˶E,$-L/<Pm64^vڰ03f>-e1$q>ntȐ43Ͳ1ZsnXLEhqN_<՞4'[O0v]2[r(m8\>JGWߓm|<C.*`:Mr~v-Ts"_GŬL#pN.ae0_c޺+ep)føĊbQ@vЌb'ڡ& +Čl!w]<zw*A~R\&lU6[O4tujG'W
M00۹Um)r"Jn(i$kv­Rg
fnP[k],΂UZ>-
dqɉ}up"cE/=ȋbOJ4vdMaW7k~^M*^s;iGu	F0MdtsFx^ppJ7vei!(>Bʖִ[vW1RJf\l 7.bɰEeͅi
8Za^e<5jfZ]]>@$zI5@	[p߶Mmߞ!Zx&i/ĿQ
n>Z`jbSIH}'~Sʓ|ʆT hAOP`1 Gz{$$7ئΚTD2i}aۂGʧp?e
1#,}6Sdq0Lă[U3	rlߜҨE:L)(\
>]dR8	S<=zh<-C̢aX
;gPc=5p+鬑V,"-]9@Gޕ !Lp>)"uJ¢Q1VdڧPC{}W69/u.;)wğcfwVäm}+LPR[(M4`-p3$cg7|fY0u2Ldz@!$z0nRؕ(nť53YBۍS@sx{HE<M_wo&=ee\-30:\M`I)C%5{'#*T'?K+iȋ}n;ze=Zoe?f)Mބ\$kEɟaVr))	GY#w6+~ilL"ou_ (>/髎]./~)Ō%k`N˰dYxaH
!<-֭ױl=vųQ +{[Ioer.5*c*hi[F`Ji5N1TM*%PbC	y;ʻGB̛W,3wPiS!+-Ի|E_˰ΣMM_q 8Eȍ~*ڇ3w9{ȍMr[KkaW2R;g7IdącthȷůL~āj-D$f,L2+4XʁXj:1Nz>Py I8ج8?2Mj<R.A^w>ϱ^QEZ, 2BGxHOH#DjAZC>L G,qB#%|#(xgBx;hEWNbj9JbՕrN9{mT>Xι ;|cx@D!p; 2,$ǐtPHvvAƥL?ۧ:(ʙA,wR^*@;Qo9Cy O<{P}vp(!ewFMa1bq8?TiP6Ka37s3YMn:Q4՗_
j<6SpD]ZX73^Y63OpI"|
ЧY,2jbȜ65!쨹6K״PߐJDRy0rxŰn
oIE? U0ï{/rCX8"e:DOX</b=P* _=:XWaS+	
ƊHltl?=Y1eؗu1뚣ԯ^sҰt j
+]Ѡޣ*D>*mN*#0lo <͂N&v}3"_O&7а+AFoc`}#/YLD>WXZZ̘y{P{+S(d^9neqoj1QdCj^h68j#OIaŢ	w!1N8Jzo$-mpN	$yhMZvm%iޏ\@J7_ds.EKvKa[F!UpfSôƸXA'^vzPÇ{oO86+Vhx~y#F"E_nҜAH{hN3 |
T{x@1/3s`˕i9Vyqn}8I|Ny^T BL1mnƛv2R=R&/(IhTŗL!{:YEhA۵'Kof1J`b#)Hl3yg/)},ox
HG`4,Q\=b9,
s4]lLD/x]?=>or+P01-Dw5dLf
2Q&/Υ[rڞ"uiz䱷hT1]y+z.>c+IvzIݪT")q}2sq9[hI6NK	*!uԉ^S/OE ИbpĒq$A]n,g
ߟ0Ҿ(qB CU9e|듋ͮ}!f.mWcȊ=F\`}N)E- wxX
찜Ƭh6'[qyVGJHq`H_OO=>S*^B#i"Sk\
S=`lbezLBS o1a_J,_!b?jKY0)cyQ߃
'O]i.?˧	)u8j	(޷Nvvmg=vEEfo^UE/OiYr<aG(葏SC8)2tAp :6o<Sdg_Vn.r*P ߚO!ߩDmX\40h@R$Z@wYA|c05v`@~SYxBZd]QfwA4?Jrw JE#Vuaz\`}=Ws}BԽ7)0C:HcOcl +A9}^N0DJ_E J87[Z%aCl>ADOU;z1726E٨"^^}NYAoٿL0fSk!ojsZкL4CAnM,d
\Y٫D̊;$k:40pobok*`nR4H\i䮮UAtKk=B~u R#'ƧYw(S(o
"dɨ`X/#>_c$K&-FԂctT*=(C:&hޢ~4=RVNBJ;^N(d{>>p-6#u 	+TsdTc#z-+hnMzBa2Nc,wuL(i:Q/y`4<1wY`04X8!c6&:r5NoM'7e&0=3ܴvA'/$Ir EGܘG<&iwzypY.1,b莂ǺVLpqc*?;:Jl{A>< rBZ[>T	&}[@VA'J/^$'i(.@~L'k5e]bW|Z6HQSRC$	$lU?Yx܍Eƕ ɬ9ћ4D0O9pNY$MtAX!Vkn4<yiUaf6Ζ
V2ksXA,XG+jHV9X=:ݡԂ'7RXQ.iwGĶNwVJ#.h+cjbk`c <[̅;4F`(cIE5TP$Љ ,ֶ_01oiGEJǭh	,SGN$%Pba$r-jY[X9Ȇc#scј(BVjFˑfPl[ٰYm]Iv36LYsd_-6buU[CLߗ˵fBB1ıpIxg:@KwTbi>̖tq?Q
R6KE2)aF<n'Hii_	4<BLTfY
f6HgqP$פPiDWNB΂brJ!?%f(X} <q2d'w~|>;,RȂ+5Vb5YD&X9JtQ -϶i91MRB2Bu,kϛ7'ץx"v`bO6*BŽoF@gtDN9=~:Xcq HWE6rhzm#t|ŕ}B7Jˇp6{J!T`P_-7"ٻ!_kŮ9f'EOT(|mq!a ͢-d4$!J Kbb=$k"65j|Gua􌘛R;TV/Rx2M`G0տ`ÈDHZKosMx p` 9(]̽^a\y/{S~"whP#4GTyk1h-:#7yW1*bO4F&`[Y0]| Ma>kU]/'OK"dSѰ8Pc):GFjmQ7N%Uy<ȟ 	Q9J=^-LSpz<|uK!|+-m{+L V)<  K̛X]-gw
} zuƛSO+`SAt]aj:5h'Ti7xr+2CVѬϢ,XǫE;v~\PMo`6ZwCbWw,~,ov5?3ow}젗ca>x~	X搒G<=j[1GNB,-DLx9	dF.gkن-`7&`z;]syʉn=KFv4QPssLHLאA=숀qA嵬-qT״8Ld,@by+ƈ/iuˎLKJ܅
:{?ĳ_,$B TL8VOa2dG5|qa-JVE9̻@W3M^&r!Ey;|Е[CBToPvy1e}a-mШP	䝇X џ&سQ E H߁udpam;G	*$ΐjk:C3okР=LRlNnisxqMZ/pțQѤdY@?8![Z_+1ORG <ų83lPH*(N-.a"/0s(x8zDMDlT!ثYγ )dVz]LJ`p
KwԲuf_*B";&S*.3NQy<aj)ߦrF资+7@O}w&M~*oCAI!.VF Tݼ1{C;]1X^߭tk8܋D_Sp;=qD$Xa=FǜN*G֫鴟g6x͙wH2X)i4ѽB"s\黐RZ'^_+-3Cbvgl\뢀RO;³o	z$cw7=3N[Q`n7{
ԒpӶq]u࢞KEw(2ChNվ{#e7Jdu3׍2O0hsZv]2.Uӊ4y'fє{Ct#*o8 	H5=MVjuV|]N`݄8NִFhߵ
ª:2̉zne9,ݏ>9` (AP>:=*՘Rh)['L#54FZAζJQ׭)XQȃ]l	̜B2-uB]ihp!H]әSnsig<X=I[|!C|PV:b2GAnĢ|dZTYv;tї8/c>#oxlKR	#$AUK`U6.I̷UPëh$,&C`Wj %*Fȑւs$!\9z8U(Scj)B񌧸bphU+ۧőbOfK ;Ւ*1S$!x6R|>|CR48IM~/?>fJ]~Ey@oz pL+wpQAAbBNq#%P5BS"Y#Ld>>$ҫgV,l&`(C)HROy7Xh#Ho6!1?.]u)?:X7(fY}7S#=Dr˕Qh;S#VkfIS)_B/7u3CLBR6COv(Wb9ա-=6P=.NHQ,P~K=R;QQz^dۗVJNaq	+aTKH,@2Kw4q%Lzd%`2'i%eX!bi}h~YZ)MTaHڴ4ELN[.3H"zj4VX[ME;ܯ乺mK.]E)).Ҍb]Iϼ"\I627Df<I')CmQX6ʊ+)^*-ȮO ͬRA'o ҜH&Q_ōixGqAf Xі6lb(l	{sLCCPoDc#\Ustn~[g.QrСc=Q,V(FK7|<J6gtȀGKa7%(n)Lz{L	= V\DJ"s+tܱݴ^,r1~!ˑiWC"#"`ڷS&u/|R~[@zg_/}Y_2maZps+d:neLHIu,,j!N*VORQc4 d(0~d1"ԽTt3iЖNUU߂2wGe"]c2kR0p/"z?ݩA] _d:|==~=d;(vW!шvg?@Mxۺef(~*jf^qOGq\|flx4CKO #23-|ڕV, qo<t|GaT	iL*N8
o'zK&F#+'ѿfp@v(D#B>oRpwp$FIcɠϫAxw)7z#>$f9hgEaA6Xa7=_<7lb^@}M%dx4Zlm+RZN3*`_.%jt.i_>b-˙hKUqG1g"G[I5%6
SoCw']l;Lso		'oF8k<ddYg{Tڭ4`&Zof8a0f)fC~6[gV7b!g,p c&}]$ZDg|lJӃbLJFnwz=!$:RV(/RXH]Q
hiCf@[=tm6`d+%6ƝN6as7_ˁXm7a-@Q{Gƛ\ N &ʐkƺ8|0ƜaDfg(hFSf5gD:\y˱{DAfEgĵ>Ƙ-[W޶hK:w4"+
2ʵLC$'*}T#HG Ͽ%;K!o47l1҅DX\ɖcWJK^ࢱy4 :k1n >s|>N^q!WI~$QM60N[^TfݢCT7&5	h	ge=g}JY	P=S_x%~բWm6E_+|~@qA(j(¦_V58mF-ФΖ9?CJ'@7hM CS錴OG?V~͛$:4>
PcKδ}pDhaIՒrI][d:D^ܗEYy궥~<K@a;T):A]ڰܓ!A|9>4t)lDU!M>[jO|<̋t._EQ|tb-aL)+r`O	n2xEۧj77-'.݃>q5ً1nԼjzH0]@/zL-@ʠTڀ;L:1I]78yK"t%tGɡ{DKsR	v##ݜED&t*u*n̸Ac,q==VnIRO;($zUaxv@viKpvZ^^&P1qG}yI:Vtymoc5tx9lŊު1O/RvO
@ 5ʸEl&=GJ(I'܁d+	EbMWӉVwC
}<1^Ծ0o!uڽqqqYƙ3#.~x!l۫NGx%$]giQ2`' "EE\ȤbHe&J߻W}V1Emv!ؾ/'DyR7;֕(=齙g}OiMh{4||Ͽx_>}gϾz?Z7d 7
yp|O=|j<4{?Ucnw8?֏C n]WI>wk ˄a"^;] 'S&s.&;[5*28pF+l!ߣ$x3>Py憕6`B!:hsV{w]^.O{ߝ^=;];:uIxiG'oᷗ''OބN0,XLbWg'OO{?^=^]_|{}zwqxoO~On#L[/ϯ^l=y?Roo9<֓__5E*~mMzs˓o/[;oa}[)@<SyhFyGAo/:3gO7 P.IȿA !|N"{?41Cv(qX}3ó3OOs":'{J~kֽ/Ko|=wb~=N^ۣk]f^w^ktewi]>bf.f}A2<	쯃OO~8Mhӱh=>Gw}U{Fc]}s|pLa_=?<7FK9KW:WWp&'\3_\߲o_=8pEa\cǼ;%`{Y8ə=ꔷ#"~ο=}3S]ޜoz>^{'οXa?|yrav
6Ζ]uuQ];edmXװ^<6*!=KA~pv'|j b5}ݭl}"f!輷M6xH9&Gay|wippf)\XƟ|5ZXÕAfM/'~[ʽQ5nW/{{{G:=	G>M`p&v,wa*c#ѠMvm8sw~%*[Wo;>7qVQ-h;(}!
7=7'2iM};E%PC-m7Zfۈu8(i0p7Ąrp#%B7Lۉp uA*b:CK}Й`hoa8㓗o>?>"PGq~O/ rÀ1T%͆7Ƞm)v:r{=4Swx0bs4bq^Y0yl<Ëvӧ+zs> /Wy|k|}7Sݙ>8VW:;<:=2cÆ~^p>|,̓^L'|nPOFѤ\^7#|"чq~a竣^f+"Y~G5/yuzp_`6[hVlܬ.ߟ\^f3{o?
Ϡ3_!Z0.(Iۥ%kl}b1yC\Sty8<4	&; W?+4QHؿ¾>i_ū_=5cfzx|lfW僂\dt_u 4LN󊲻n¯bɳ+^|1ťLY_y{~`?K0wzqQ>u<lO(l8"5*GВ:~[K9z'h6Ϳo	to~^j;{UQpl_'z:T`yp9til;i[5N`z0 i~vX|&6zxeH9z0b=^Y>	׃nչr_3O5vj|{r
!~VDG)N=3qLՔ?'wYk|CxWP@Ǎ#4X.Yǅp0ti'04eu=J_hzlsk~B=5}@=஑/ vB|3@LB~'dqܐ_=.1yem?^v{)!FE8
S!7!P*	e|qM!3
2q8 }嗾}0Qqx[X1MI>9@%x;5e+PtЛ>.vo"cL
*̭qmxHM/e;ørw76̎6ݼ=t2s>@,NδϺTÇnFG@E$i{9WW2LC2ǳϾ[w}=k d('qE6[1e>OJ!ϑ>4a
,@In:tMAS5GCFO|K#~ZzWi7-:o 	rw7z@^n]:t@V:ɍd,@:NB'm&v̹`hh OfzّS@Ͷ`/Z</ˆ#IleUh뿞~^W|Y#${x¸yuE$ObJb ҏָM  x4{{vv{L<~-䃶Qێa|\ynMކ9!}#Omv>8 ھrfE_|7Un-~PnZ+5ǟ_Sx33g'g?&,|anya/jӭ'0bEw<^o7x=v9#ýk`plQc[n \<~|X=d&y?Ap{0/2?
q@<o0vF<~SW!rA(7z6go^|Ͼywy'?|I^NߜKc̯v'@9Lz)B	%$Q=70¾:~GC(gxl8B6xu'|_Fێjbmdx t)4Bko	Jl1 un<o'4eK׷'Cy2yq'?	0.CCmԤŹRlÇG ̝{Y&[EzPr	?7ygp#x {6B9\c<i_=(^m5^T~; 3I=w4EFk
c2!lhdhGl~ӤPxڭ?mING"HFldLvh)yr]KskGÅJ<*=x!MiV_ 2[0#޿V>y׿WKH#FoE0_DoS-1>ɯ!=4lI(N)o(e$4{@&Mv_RՓ`^5A U@IIl	wL-3!wӶ]NpEuoL{jFga=姍qPJ]Q.xwd	>u v¶ DAY܃1d[Q5ar~y.
A29_꾏ثn!|0xG _;`c"jٽ"4
-'Gz7;?7	>J_y$a~XbX~2G;.d&]itc:m6Ja_}=i7鸦LP:^}+>YJ֯嵹PW=&,XpGP0Ǯ}*D~t;=#_}>n4P`XxLEk;`Z TYNz Wwݶ'N?{@=4;_|ϾW )/>|?>|?>|?>|?>C&P   hςV <iWȲ:.369xe	[x[&KYj
 g_UV/w9	rwuUuuu-T~OJE+]W<o^ey0_y05	geR45]=u㿻_݇;ح*{7juT>Mӣxs61ЈS7{/ێXC5|Zҏs?4s&`vttm;7;r?nO2w)iJ"u-S|ӱ_ x2wT~azwEdqEQqcUuxUt]K9+~E[g
?E."ov.CvxT3̹d9zdL)85'65axTımA|}J~o;_Mʻdtssr+b;ElyyVnm͛!('0 r$\=^8Aؔ	&6>
0h(Sǿǡ߻~bMݹ͍%F}R؀!X,̉iw_BmC>=`δ	e|Ljkz_݁/&`4s0`#	,u7A7pJtO6[!{YV;2f6eZ\<lL+W*:xg
ceҳ((O?|`)p8#X(7}§ps' 0J3SB3Egܚ.ñK@tS!r3Bвug6ekCx ' Yrxy	9\Y<&)BM m Ǜf!.Sh)Ak޲\$>
ɥtt(=>Tλ}_uEv?u<+7^a}ס4Pa_fPzI.!һrTnIz[-XYGUruP'0Ҧݎy	'=$v3I|fͬo9.UF<OJ`^x&U$1&]jMMֶ̭Ƃ {h »
҄|QZÀ	4tyxT֌i\#mw Fwɽ#
AH.,w3#%fvMs,0m:m"[sKd0,$Ⱦ#g^$ܳz:ytN۩CK.x)>hFL#`nB;8cBģ=:67d͙xڬY%K&Q%r:l"=%ޗ@hrC 0!$@نs~ɿ7w/$e@i/1 zTznsM#)%GSn@{~ΙcRrzWHI~S)V/Qt1<\u@#{=#<x<OA(Y"mӆÊ",RgN0)g.g1΅<N=P6aGQO=	+BEH`hA_U2pW|l\90i0((tlHbx,6ϼ{݃N]~$vo	 35{'I^0qQga"Cp}c!Sɉ#?|9W.եպ/s^PcS{Nƞ3e\t]IyW!/&rO{x2OGI_K廙h<~xkBI|WG 
#IhnV_gx)oo;x⿃y>gu_)u |j=սusddWCp3
nWoA\d5:3s:GޑN
%d#}TO2KY;r7ۺg|Ӭ\E!@Lh)0jQ'#NJāNϤtY0Qtw5b&uE:?y_w[BRN u;ʟ\NTЭ	9}~Pc80[@hN/2Gjl>S8N\x]~)bfDJ!޵yԒ9OK}
O;s 
T	䍞$~9l(4zG|ג_?<8@CY;sR,{T	nLz!f-xĕoԾYi}R/d$h:nqGƾp5b˦ֺɎ^GJYn04 JBWSPDT9tχn/Rd?!u?n@toJ}nӧ/D*w6XEj&=%QV id'+ NCj<u,n5lꫝ.i	[W42_w_?iq"!swtz֑WUO T3bZeVV֝W/+k*(q Qi5T:4>Ֆ9Sα>N8*v6ܖ1r<f̯pJ:6\<R
9SODX?BހZw~~h<#didjbb-ʵ݄2[x7g_ 5D}A燮64{q&KRB"K0h-e?@ !7+ЄǊTwW⌺$ָ$Y'6mZ7>)K*Mv߆]\mp٦OxܝE1̹w;-#EJ'	O1ފ3pkfo4: ~!v@b J$;`b`7OT7sLVcF78 5<	rcC(	Y<L`"fLmw(&*jm/<V#Bju{We`5CPxlÃLm;0hy7W_>Ӡ *989"3p nm6SIeP>jʃ@a-vlJY#.CR)Yp	Tkk@Nr+VIԪ7$גYt r|	f-FM<vtCD3bAnImAWm3|i:lCQS- şPz|'{jKJx4r/)oj6MFx/$iJaKQ0^_:Cm^Ri7 :R ȥd{ak`fYX Ơ "$4򻥝*1j¹\GY8rUhN2C,ٕ!(#z jF8^\.gs#hOް"R,U[o6S"ySZZ>%TnA4Py80%Tk/%(J;?H9[5 r/VR+&c*7Ejx'V#G^oX~D0QQ&8{p,3s/Idl1@@-`0юƚs@+cb/9.WqWż8~0@lq`mKXh5(T@jO
v̜p (蜗sm>njsְL2s0?TFK`IB[]TCj p'fAV:^=VCɄ]F-4\ۺ~LܔL\݂W-ИL?/4a|aF"Qqm1ae"沩<Na'z $I(įć׎I
"ߊ5ga,7kd723,R1.^C0n%*nbK	َ_ww+3^M: 3zs34Hů]ᮘb(*X4dٔZT?lo+
d+n4ss<(m20 d*\v+DV->7V4<ǘ #K^w]tDq]L{oh^(8ɶt[38U~#ӂ<qfQJyH*; aUIgEK; !U-旿~(BvQKp-6!-yk۹1jl#ף4r:OlrtUGъ;(KUs:&B܇rQ6MB
j  xQ?{|9Lb-h{Q]Sd,!HխbKf rxͣZ1i5A|ZD׬6TRܒi]*K)XjL>bQ{-j,mڂ,.ǖ|>O,AnGB2Dh3G^fOX7!*˹^a!5$ikF0o!Ka˅]e	8>;lhrx;SulA8#"	xOV[2!BLӘFjY7@7:WCV3J/į#r[M! #"/ڶDa*+.~dIa<,N	^lx3F_̀a/ F2R
+[-$-H5XHRPٷ		5F|bmysWWShwboV/J?U+;jr?_y=9{=ddWCHT@Eq[(1h#".JE#o_0u&
8l4Z q$ҋ7"9^9zicJyd*Ly7Bp,)Bsl>GpNT%U
1܍&M5VRBۙz݁]ݦd20[I/cj9\fm(9;qMqzRQ-M *~tqBvO.Q!/>PoA`o[tt[^4ʿ/Ob&R`!XP:h?ƝT3}Jk1m?vM?!~)&ȭ]ղXU z26GB;@?gLQ~lK?T@㊫U1Vj)Z\)9#s!Fm}Y;N:Sƻͱխ.EsPmZۆ.9%]oci$8?/Y,	4-H'NtK&B˭ƥ5SFU>|Xe9a];EE t3eGx4ƢNXTZxYlG]ȣ!cԛ9iѥQhNoPau7LtG:0-G6%ʾ,$=c		(NܴO@(^$4pVn>hqsdbrUWX2Q/ec)
/!5¸XƩJIV`G7.:ߦ9"`e4RK|Ǵ-lD0Tu
tJ[4*Tlld:s̔2x^+pk	V2Pl _<fswp*U"}JϧM;T;ײַroSڅ[7}>ź\IHEa//H=XrF'Ǡ`l2oGP GK;tU<t5V-z˴b7}s1XΨ1B~5Zqy?<vw"״&\zZpJ[`OϺNW靯<xë3>U x   yV <ks۶Z
ǭ\7vFhzU8."!1EewS;ә{̙64], *_=SR+}x^UkT^gիGȓpYN??C)KS_uس>ߴCXJrpT}UVj{H8J=4=m1Wwroq1c[OKi3s=r[1KzƎuSl,\JyHDs]5t'8L}1Mc"qغ>iCE
ʹ3^K۟"K_7L;!Sc|ya}tQ29F
m-MA`#,hq,#d;0L5_gӲ>|6"(mX\N.C~D^z|Z(BjJd8	 Ƞ	4~m[6e,ڃpi@=ySw>dg{4aQԹ.6`I=*-˙91mfK-q{Pӧglڙ6I)eK.NḬoF3É	<R}t7YIZ`dKZy|&zeC.clS\ɖlʹ"HZaYobi'=SؾxlX#.E5FAQ}J[ߴ'd1Ǣ @`u[8UIm%,:cdnS2D3.OʾCF@֝0JfE g!Sw|^n<j9\Y<&[)B[M m Ǜf!;.ӓh)Ak+Y.n_zCZ:TZ*ݾ"w;?:?oP(/I~\\(&׎whM9 w.N-I^_ c,,a۾S*n:?>ica^McY3fΗK-Q3av>خ7N($IddF)>3wIāM{SjÙۖX@ DxW!Vϖ9*9Cz0.84?+5cf(gD_?<J{<߫y#oސod} O$W컙鑫EB%u]sx.F%2jMdߐ3n/_Y=T؟k%¯ f>uTp:giipdS! ACh1=g >(E4B@D80Es&6+v2zkIDRIK[-H)tyˠ{%ܐ: LH/ImPrp0D;"_-+)5{YPy@@gC?H~!o\nhJ@	;'s?c+$$P+U;;oCO:	Bfv 'o	\ ,iCaEgnB)3'm33BVNyhʇI!@·M"$X0Lfr;iѫA>Ud.4MdQazNi$mZ<[pwowg>~j+{x?_ 2s	_$&.,LD~So,e*9qxD\͵ۻZ7ezJq}j/sfxLπNs+ii4Vx?bǹxjWRZ}⬵o91)>eO⿽!|'Y/]_{<csZr^wx^:g/_/'F!*A!ռ($ۤDyC>:qDb-#Wϥ2KE;r[д\E!@)>2jQ'#^%@@'gRxԵ4ù~KF].rٗPT` Lf#)t)=W?47oN߇僧K@r`(-I_x|[ͯ3{v.5Z.@k3"_'FajEĜ~Os T	䃾JnsPmvU2o$y56ytFG+lk,X.'ݙt!f+I,qb!Pڗ4+,Pg<A;7Hcip1A}ʹ eSkdW =,7v O%ժMګve(Z&ۉz~4`?~ޗc\:n
P` &7>o[;n|"5x؎(k`Sr45z!PrZCFǵrGGìjkڅEѕ4lnZ\+/bH7]+]^`ptU~ Vd}Xemo_X% u,]GU,3t0xW[RBĹ3@::AT,q?eRWmJ--+b}(_tl,\<RzU'YK,I!oBT/CDX 046jjbc-ʵ2[X3KpD ǹEw_Gw}K,͞`v	Rnt,|>ZnKYP?JceX̊HMg0xu#hJiKq2nіnD>}PYz(R^h6Z<6-^?qwn0V&)́~@'>DDXfVh@C17΁ػ(-'M&wooR"j=*x 5,NĄ l9}Ǳ14Ʉz-b9Ƅt6P&o-=<^폵`5XVyU\3QEeK6R2] aߘc<;oL8E!fX:@Jjm>Kaի}*G:]Z?
ٔ:$' ]V?R,ҝ6V譒XUoH'o$FC/[O/\|͌[yx떈wH(6ڪ1!tن1Z ?6O&Ֆ:<h^{S:|mFxËf4
Fj/˿6|Z.{rBl/}M,uby4h ZHB#_+ ˕~uSiv0eo:kSQ//*kLApIB SDֱzfJAQ2<~H+]#-*w`vܹN{p~tMiYg`Pz<mY`M`zn=ai$'-?H}eҀK[2;'!āA0sl 't8->yj:>wqU{)
Ժʮhf:yi"~qpeփBGol@)bP0eVAc@G2,G0W[`7g:P6@1PŰxWuu؝W9x7@N<؀.쌁u}r) T{tmӼ&/hL?0~ 0x3Q[swH d
B1$p/F370v	N3^Fa0fE6ƓśHxt'1T8{d\bHhg7~݋߭XP,z4L5*#2v3agpoSE-Գl#R3Oh:p[	*˛gaeqfƄoBj'xKSPA@>?+dXy}79P(8}<'ٖy;<S72-`sge)ܚ_P	]%14ū!U-RH0=jiaF&DbOzk;sƶ:";JS.&gO^|9cIT5̠kYn2whl T6~)rB8ƯvFgwK}1cAop]YG5+kՊɘy%4KY/SK<l/R9>M6P3g¥NhTe/<g	D$ĂCH\m,	2
Dc9s3qS&yfxlbT]ꃱA0tTi!D_3V q{ޒ"% e47;䧟r/H"ԳoQ'CpOS ;8xkBy}SNx}Iq=lN	lu#sep|#]ba)[m@VF`i,K$ZC(ˈDsZb>1J?gShwx bP/=Z5`_</Fm?WR>	99iTS2s!"nJ#l0-0Z@mf+ѻZc-}dT#+Gأ8/x
)i!́\AZMB#F.B(^'Ծ3={l^(R40;(lo au >]H(;0xmJ]!D28N#e|>)zMh|'b5hK"oG-}]^~!!C4Tȋ XƟ@TG8鱼3[WHHcb`;CD8GgmO~:g{ӏ?Q\cGxdxll9V-w9r滿4ÑPOĿI&(KAz?6KХ.*uţ+
cPnɔz@q3ᘼҟuz޼-kpFn{lh+{3Kz)u%.ERl󗚢ܲc/5o-7F?	Z/5\g:Lw--A_AL#RA'іƨyPfcW*|vw;;;G*C_*_&_СiOa,ʄ.rFFTKl(s4NVrcavQn$qoP @_LL3݊AWmt9)!%}C!|E9j;m2oJM"~][Q6Tu>#Yvu*ĬdXӼ!e)
/6= lJGCڈ>YG Ns#NOk/ӿ6i+.Xbۉoi(@JM+-/,	Ԧ?XDe*f
yM?	<r-䴬XN@*YlrGo9%VP5sCSnDpwLJ8e$_rk{ղsuȍrEI{uvsۙ]9۠ǉ]2bigSʓUt;PWkU,whdjzk󋞴q:m]Zc\1<yk.εn/~ި+0P?pwslSaDN"?NV?G@ @ '%? x   zV <kSȲ2)s@tׯe9$ K:	pv${CT fz{z9\yOժyxVx^zrx@	yδtg>JyA{2vzZ{/H8<߲|jPѫ7
7ʤ㚡M55Nl=e?Hӝ{>hP;]w7-/%*þC4{ae?]|n2=϶=\	0'|Wt':_Cﮄ,i*u/kn|$xoqs֝MȀ%/|qgS(_M3:wo(->SFc/CIM`'>mhqm3d;0Tw;g˶>~(%mP^G%a*2yu_icK_(5 J,BY8o@ؔ	&>hA`*S7ǥ{~jM-ýѷӝ%FԄ%]԰m吭W[_JmCAݘ`s}J_:-My`'ɛ"ӄ'lxdaYE'zhdK!yx&zgC.l3\f˖lHVaYlbi'}*;F\ }ꌂ3= )'Lň;fM(v+ ܜ`[!UE9%m:g
fd(nSC쟹39ʁK@10JM  o# *2p8OweYpCNlem6sCo\OCc.-z
t(+R"WH5T=l<j51knȿ䡪ʟwݳ*mr-$Δmt?`̥F۷y]#W_gvu-v"Kd4uF}7`|4v5y:4vC$:L{NTKnTC2sF/.c]f!{خn)s!tA4%mk\vy֌(aM%=Y&WrP.OF:[+SxE#R:\`ws'en/~K6l:ݛ]td8mޤȾ!g_`$<IptAۙKf:KD>F-#Ğ:` R;iGO)FtHoɐ"2āE <;y/ٷճ~[ne2-^lnUM%w[3]c Д`BY]R!-qςrpl%?]*-E)*CR@9aA/stӬ
G@SBJ1܀>A(\&]1%Ay\v1)v<.QtQjSO:>Q3CL JIr3P~M"
&@\`upڐ=il6{2v   
71`Ѵ|GӢÿ *!Lysh(|iFNi,mZ:[pwo@'qK{{^w!%|Ó/Xh!)]hTs+.xC-ߎO
IotV ߍGR?	z}Zlb(]yሔ˩V`fyG@Yt[@4;_{Z_\1\gbMr_c̏ׯԞ<߯Fm?d7y_Q(z*pK;;*#Xd[ԄlY .Wd@>oWRIw^;Lnzpw7M+p
ZTC(652vġ(-@Y >l }pe\S?aPས^E,ԪU0 SwY
8`jU<F Z;7%.kqp(/I[x|5ͯ3	'KͯO|9ﯓxwEcr "bӊx'dfOoA_%A5j]6J@o^MC\QaɊ,;5}jI73IL<ɇD@o@׬kN9&y@Xv\oap8e3$͡ɯA;J/X2n8jU2HL;Le/Fx~l$2Cd?9 t?!L7h_DtXUn5}]5QB Ydk nSng(]<O,+]-hi	GW4r_p8msq\Ĕn껡V8jg^gi*Bg)2hkށ|
J@xT4:FJdᰠ;֖܇DŲ!2n(V:*V$1~M&:xp1IϫdB}FhsNyRay&fԄa-2x65D\}ݍ.ugs%40rJȰ%h#?@uJO֊1nOI sI,N}8ҍȇ`75g)CMv0FS=]ٖ<[& 3m+Js 	a@7	*`5468dELMqn %JȔ.֩&&7y- Z֣/ 4!6.umvp:.?6]lcB ]LK>k^ŃͮF}ǳ5X.vuU\sQE&	<(ȴN	&Xܕ;R.6fAATJqs6E!gX:@nˋ|>8Kaի<|ԫ=Z?ْ,G ;=^?R,75v\h"&o$FA@,[L/Zb͜{6<4;޺%p됅;fR82_<_M5;0TAԡ>4p`RYm @G֛c<nFw!4~xbOK>kڪbj&e߄4뢯]ʃ =Gߚ繎s]<4RdM+IiH:j¹\GYG8`JuhNG
O#,!(֦#bFjN}I/Tqݷt)^ԆJL%RbmZ>X<{t>qmLRgt!mj<Z6=NtzA?⁦lU0%Ÿa:	JKKxza=Qq$*m?HtSej_r hb zCFv1Z@ZB1}__QJ8/}L*J9>!RZO18\'?ےzPH(E_
~̚:ćpu WEs㨨?L6& 3js0/
X:&и2E	W4P^]3eā*	`N!m$C͔#xŌc7$ Ru*vgy	@NB:xd R(k204@(D$,@&M`l&fz,J1^^C;JiأRE"g{v׽NWzG		Ԋ_3R!7SvPixy'])PB}{i̠(7m-1iBٔ߂(PUV Ky4]~}d!WQdJ'(!b*v+Ęevm>m=x]I1EG 
Wcle.J>s/A콡P'or`~cˆFpfqIG(&hU6X	RQE~EQ
եOm=*ؔHI֦j_+j<9泣1kd+A.rRj,5ݳrNlrq<6&!-E~ UbF ^?_~t\x\id ck0ˀferxݧz)㺵@<ZBХЬgḳIKTIR̝r)EZy*˴7YB'ڎ-(`?p&~=~T C2s®5YA-K&#QBnI{v"pNpv 1"v:-+7DǏ8=]xRH2]gZhOdKT Y[7(AH{!޶'fS¿)] 6yqq~ W	f\1w*ݷ)\b|{Ԝَ8̕YEXtUlV-[K"K,k/#/Yx<d+ۤ`kWgKhwaoPN.=ժ{Zzvj?|s|𣏕}[=9+o)B	y[)s94eѦȷoX0u6[`m6>v:*y)WbqQ:(!s.W 8htV>iDbX!UxBwn#h`
$J!}VECkCyp)\P݁)&ptaSvn94Qq1.7¼bf8?Of *f|yޅ7;	$R7 i#IɎrcyg:HzSćXHf;#8 Fgmf̨q$)b9~)&Oȭ_X5$f+~JÑP0N$gLq~ljK?J.*uݪ+
oePɌ;yT@[Qwe*zCS=WV~OgrfcyH\j*,5Y
(eYj,C,6~`QR#uhWVtxΌd|/ ꞏ$wA%DofBo$wkz*ꄏ.rAûFTK쨛@yt4v6zcavQo4qW_	L70ЗwCot+ny۔#S`!>*i;l8Q捝Јé>]ě;뛩~
>3|QN.N%,yZv}E_>UԦFי8B?hHQ'~48Xt2a'ͯxPbYߦ5D)5VǠ_`	P~!ӡ5rxӽځ"BN%r& U/Ϫdk8媏rn7>GJ7ϧMkԛ܈w1xA3)C-\ιԺպsu&)䊒@^,v>GqЋmvy06/'ҕ%K߲0Ty]UT 
hja],0]Myms~2bxoԠ{9ZL
?F|Zuy8"[EXѴ|cv3%I7Q1E@ @ ?O\@< x  # upgrade-builderd

Copyright (c) Nils Adermann, Jordi Boggiano

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

{
    "packages": [
        {
            "name": "cscart/upgrade-builder",
            "version": "v1.0.1",
            "version_normalized": "1.0.1.0",
            "source": {
                "type": "git",
                "url": "https://github.com/cscart/upgrade-builder.git",
                "reference": "b2f8e8ddcf8f7e1ca318469fb9fae714adb94d42"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/cscart/upgrade-builder/zipball/b2f8e8ddcf8f7e1ca318469fb9fae714adb94d42",
                "reference": "b2f8e8ddcf8f7e1ca318469fb9fae714adb94d42",
                "shasum": ""
            },
            "time": "2022-04-20T12:30:48+00:00",
            "type": "library",
            "installation-source": "dist",
            "autoload": {
                "psr-4": {
                    "upgrade\\builder\\": [
                        "src"
                    ]
                }
            },
            "notification-url": "https://packagist.org/downloads/",
            "license": [
                "MIT"
            ],
            "description": "Core and add-on upgrade package building tool",
            "support": {
                "issues": "https://github.com/cscart/upgrade-builder/issues",
                "source": "https://github.com/cscart/upgrade-builder/tree/v1.0.1"
            },
            "install-path": "../cscart/upgrade-builder"
        },
        {
            "name": "doctrine/cache",
            "version": "2.2.0",
            "version_normalized": "2.2.0.0",
            "source": {
                "type": "git",
                "url": "https://github.com/doctrine/cache.git",
                "reference": "1ca8f21980e770095a31456042471a57bc4c68fb"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/doctrine/cache/zipball/1ca8f21980e770095a31456042471a57bc4c68fb",
                "reference": "1ca8f21980e770095a31456042471a57bc4c68fb",
                "shasum": ""
            },
            "require": {
                "php": "~7.1 || ^8.0"
            },
            "conflict": {
                "doctrine/common": ">2.2,<2.4"
            },
            "require-dev": {
                "cache/integration-tests": "dev-master",
                "doctrine/coding-standard": "^9",
                "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5",
                "psr/cache": "^1.0 || ^2.0 || ^3.0",
                "symfony/cache": "^4.4 || ^5.4 || ^6",
                "symfony/var-exporter": "^4.4 || ^5.4 || ^6"
            },
            "time": "2022-05-20T20:07:39+00:00",
            "type": "library",
            "installation-source": "dist",
            "autoload": {
                "psr-4": {
                    "Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache"
                }
            },
            "notification-url": "https://packagist.org/downloads/",
            "license": [
                "MIT"
            ],
            "authors": [
                {
                    "name": "Guilherme Blanco",
                    "email": "guilhermeblanco@gmail.com"
                },
                {
                    "name": "Roman Borschel",
                    "email": "roman@code-factory.org"
                },
                {
                    "name": "Benjamin Eberlei",
                    "email": "kontakt@beberlei.de"
                },
                {
                    "name": "Jonathan Wage",
                    "email": "jonwage@gmail.com"
                },
                {
                    "name": "Johannes Schmitt",
                    "email": "schmittjoh@gmail.com"
                }
            ],
            "description": "PHP Doctrine Cache library is a popular cache implementation that supports many different drivers such as redis, memcache, apc, mongodb and others.",
            "homepage": "https://www.doctrine-project.org/projects/cache.html",
            "keywords": [
                "abstraction",
                "apcu",
                "cache",
                "caching",
                "couchdb",
                "memcached",
                "php",
                "redis",
                "xcache"
            ],
            "support": {
                "issues": "https://github.com/doctrine/cache/issues",
                "source": "https://github.com/doctrine/cache/tree/2.2.0"
            },
            "funding": [
                {
                    "url": "https://www.doctrine-project.org/sponsorship.html",
                    "type": "custom"
                },
                {
                    "url": "https://www.patreon.com/phpdoctrine",
                    "type": "patreon"
                },
                {
                    "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcache",
                    "type": "tidelift"
                }
            ],
            "abandoned": true,
            "install-path": "../doctrine/cache"
        },
        {
            "name": "doctrine/dbal",
            "version": "2.13.9",
            "version_normalized": "2.13.9.0",
            "source": {
                "type": "git",
                "url": "https://github.com/doctrine/dbal.git",
                "reference": "c480849ca3ad6706a39c970cdfe6888fa8a058b8"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/doctrine/dbal/zipball/c480849ca3ad6706a39c970cdfe6888fa8a058b8",
                "reference": "c480849ca3ad6706a39c970cdfe6888fa8a058b8",
                "shasum": ""
            },
            "require": {
                "doctrine/cache": "^1.0|^2.0",
                "doctrine/deprecations": "^0.5.3|^1",
                "doctrine/event-manager": "^1.0",
                "ext-pdo": "*",
                "php": "^7.1 || ^8"
            },
            "require-dev": {
                "doctrine/coding-standard": "9.0.0",
                "jetbrains/phpstorm-stubs": "2021.1",
                "phpstan/phpstan": "1.4.6",
                "phpunit/phpunit": "^7.5.20|^8.5|9.5.16",
                "psalm/plugin-phpunit": "0.16.1",
                "squizlabs/php_codesniffer": "3.6.2",
                "symfony/cache": "^4.4",
                "symfony/console": "^2.0.5|^3.0|^4.0|^5.0",
                "vimeo/psalm": "4.22.0"
            },
            "suggest": {
                "symfony/console": "For helpful console commands such as SQL execution and import of files."
            },
            "time": "2022-05-02T20:28:55+00:00",
            "bin": [
                "bin/doctrine-dbal"
            ],
            "type": "library",
            "installation-source": "dist",
            "autoload": {
                "psr-4": {
                    "Doctrine\\DBAL\\": "lib/Doctrine/DBAL"
                }
            },
            "notification-url": "https://packagist.org/downloads/",
            "license": [
                "MIT"
            ],
            "authors": [
                {
                    "name": "Guilherme Blanco",
                    "email": "guilhermeblanco@gmail.com"
                },
                {
                    "name": "Roman Borschel",
                    "email": "roman@code-factory.org"
                },
                {
                    "name": "Benjamin Eberlei",
                    "email": "kontakt@beberlei.de"
                },
                {
                    "name": "Jonathan Wage",
                    "email": "jonwage@gmail.com"
                }
            ],
            "description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.",
            "homepage": "https://www.doctrine-project.org/projects/dbal.html",
            "keywords": [
                "abstraction",
                "database",
                "db2",
                "dbal",
                "mariadb",
                "mssql",
                "mysql",
                "oci8",
                "oracle",
                "pdo",
                "pgsql",
                "postgresql",
                "queryobject",
                "sasql",
                "sql",
                "sqlanywhere",
                "sqlite",
                "sqlserver",
                "sqlsrv"
            ],
            "support": {
                "issues": "https://github.com/doctrine/dbal/issues",
                "source": "https://github.com/doctrine/dbal/tree/2.13.9"
            },
            "funding": [
                {
                    "url": "https://www.doctrine-project.org/sponsorship.html",
                    "type": "custom"
                },
                {
                    "url": "https://www.patreon.com/phpdoctrine",
                    "type": "patreon"
                },
                {
                    "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdbal",
                    "type": "tidelift"
                }
            ],
            "install-path": "../doctrine/dbal"
        },
        {
            "name": "doctrine/deprecations",
            "version": "1.1.6",
            "version_normalized": "1.1.6.0",
            "source": {
                "type": "git",
                "url": "https://github.com/doctrine/deprecations.git",
                "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca",
                "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca",
                "shasum": ""
            },
            "require": {
                "php": "^7.1 || ^8.0"
            },
            "conflict": {
                "phpunit/phpunit": "<=7.5 || >=14"
            },
            "require-dev": {
                "doctrine/coding-standard": "^9 || ^12 || ^14",
                "phpstan/phpstan": "1.4.10 || 2.1.30",
                "phpstan/phpstan-phpunit": "^1.0 || ^2",
                "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0",
                "psr/log": "^1 || ^2 || ^3"
            },
            "suggest": {
                "psr/log": "Allows logging deprecations via PSR-3 logger implementation"
            },
            "time": "2026-02-07T07:09:04+00:00",
            "type": "library",
            "installation-source": "dist",
            "autoload": {
                "psr-4": {
                    "Doctrine\\Deprecations\\": "src"
                }
            },
            "notification-url": "https://packagist.org/downloads/",
            "license": [
                "MIT"
            ],
            "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.",
            "homepage": "https://www.doctrine-project.org/",
            "support": {
                "issues": "https://github.com/doctrine/deprecations/issues",
                "source": "https://github.com/doctrine/deprecations/tree/1.1.6"
            },
            "install-path": "../doctrine/deprecations"
        },
        {
            "name": "doctrine/event-manager",
            "version": "1.2.0",
            "version_normalized": "1.2.0.0",
            "source": {
                "type": "git",
                "url": "https://github.com/doctrine/event-manager.git",
                "reference": "95aa4cb529f1e96576f3fda9f5705ada4056a520"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/doctrine/event-manager/zipball/95aa4cb529f1e96576f3fda9f5705ada4056a520",
                "reference": "95aa4cb529f1e96576f3fda9f5705ada4056a520",
                "shasum": ""
            },
            "require": {
                "doctrine/deprecations": "^0.5.3 || ^1",
                "php": "^7.1 || ^8.0"
            },
            "conflict": {
                "doctrine/common": "<2.9"
            },
            "require-dev": {
                "doctrine/coding-standard": "^9 || ^10",
                "phpstan/phpstan": "~1.4.10 || ^1.8.8",
                "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5",
                "vimeo/psalm": "^4.24"
            },
            "time": "2022-10-12T20:51:15+00:00",
            "type": "library",
            "installation-source": "dist",
            "autoload": {
                "psr-4": {
                    "Doctrine\\Common\\": "src"
                }
            },
            "notification-url": "https://packagist.org/downloads/",
            "license": [
                "MIT"
            ],
            "authors": [
                {
                    "name": "Guilherme Blanco",
                    "email": "guilhermeblanco@gmail.com"
                },
                {
                    "name": "Roman Borschel",
                    "email": "roman@code-factory.org"
                },
                {
                    "name": "Benjamin Eberlei",
                    "email": "kontakt@beberlei.de"
                },
                {
                    "name": "Jonathan Wage",
                    "email": "jonwage@gmail.com"
                },
                {
                    "name": "Johannes Schmitt",
                    "email": "schmittjoh@gmail.com"
                },
                {
                    "name": "Marco Pivetta",
                    "email": "ocramius@gmail.com"
                }
            ],
            "description": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.",
            "homepage": "https://www.doctrine-project.org/projects/event-manager.html",
            "keywords": [
                "event",
                "event dispatcher",
                "event manager",
                "event system",
                "events"
            ],
            "support": {
                "issues": "https://github.com/doctrine/event-manager/issues",
                "source": "https://github.com/doctrine/event-manager/tree/1.2.0"
            },
            "funding": [
                {
                    "url": "https://www.doctrine-project.org/sponsorship.html",
                    "type": "custom"
                },
                {
                    "url": "https://www.patreon.com/phpdoctrine",
                    "type": "patreon"
                },
                {
                    "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fevent-manager",
                    "type": "tidelift"
                }
            ],
            "install-path": "../doctrine/event-manager"
        },
        {
            "name": "psr/container",
            "version": "2.0.2",
            "version_normalized": "2.0.2.0",
            "source": {
                "type": "git",
                "url": "https://github.com/php-fig/container.git",
                "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963",
                "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963",
                "shasum": ""
            },
            "require": {
                "php": ">=7.4.0"
            },
            "time": "2021-11-05T16:47:00+00:00",
            "type": "library",
            "extra": {
                "branch-alias": {
                    "dev-master": "2.0.x-dev"
                }
            },
            "installation-source": "dist",
            "autoload": {
                "psr-4": {
                    "Psr\\Container\\": "src/"
                }
            },
            "notification-url": "https://packagist.org/downloads/",
            "license": [
                "MIT"
            ],
            "authors": [
                {
                    "name": "PHP-FIG",
                    "homepage": "https://www.php-fig.org/"
                }
            ],
            "description": "Common Container Interface (PHP FIG PSR-11)",
            "homepage": "https://github.com/php-fig/container",
            "keywords": [
                "PSR-11",
                "container",
                "container-interface",
                "container-interop",
                "psr"
            ],
            "support": {
                "issues": "https://github.com/php-fig/container/issues",
                "source": "https://github.com/php-fig/container/tree/2.0.2"
            },
            "install-path": "../psr/container"
        },
        {
            "name": "psr/event-dispatcher",
            "version": "1.0.0",
            "version_normalized": "1.0.0.0",
            "source": {
                "type": "git",
                "url": "https://github.com/php-fig/event-dispatcher.git",
                "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0",
                "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0",
                "shasum": ""
            },
            "require": {
                "php": ">=7.2.0"
            },
            "time": "2019-01-08T18:20:26+00:00",
            "type": "library",
            "extra": {
                "branch-alias": {
                    "dev-master": "1.0.x-dev"
                }
            },
            "installation-source": "dist",
            "autoload": {
                "psr-4": {
                    "Psr\\EventDispatcher\\": "src/"
                }
            },
            "notification-url": "https://packagist.org/downloads/",
            "license": [
                "MIT"
            ],
            "authors": [
                {
                    "name": "PHP-FIG",
                    "homepage": "http://www.php-fig.org/"
                }
            ],
            "description": "Standard interfaces for event handling.",
            "keywords": [
                "events",
                "psr",
                "psr-14"
            ],
            "support": {
                "issues": "https://github.com/php-fig/event-dispatcher/issues",
                "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0"
            },
            "install-path": "../psr/event-dispatcher"
        },
        {
            "name": "symfony/config",
            "version": "v6.4.43",
            "version_normalized": "6.4.43.0",
            "source": {
                "type": "git",
                "url": "https://github.com/symfony/config.git",
                "reference": "e39cdd91b2adf67a117bea29660b73d896937d86"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/symfony/config/zipball/e39cdd91b2adf67a117bea29660b73d896937d86",
                "reference": "e39cdd91b2adf67a117bea29660b73d896937d86",
                "shasum": ""
            },
            "require": {
                "php": ">=8.1",
                "symfony/deprecation-contracts": "^2.5|^3",
                "symfony/filesystem": "^5.4|^6.0|^7.0",
                "symfony/polyfill-ctype": "~1.8"
            },
            "conflict": {
                "symfony/finder": "<5.4",
                "symfony/service-contracts": "<2.5"
            },
            "require-dev": {
                "symfony/event-dispatcher": "^5.4|^6.0|^7.0",
                "symfony/finder": "^5.4|^6.0|^7.0",
                "symfony/messenger": "^5.4|^6.0|^7.0",
                "symfony/service-contracts": "^2.5|^3",
                "symfony/yaml": "^5.4|^6.0|^7.0"
            },
            "time": "2026-07-21T09:46:53+00:00",
            "type": "library",
            "installation-source": "dist",
            "autoload": {
                "psr-4": {
                    "Symfony\\Component\\Config\\": ""
                },
                "exclude-from-classmap": [
                    "/Tests/"
                ]
            },
            "notification-url": "https://packagist.org/downloads/",
            "license": [
                "MIT"
            ],
            "authors": [
                {
                    "name": "Fabien Potencier",
                    "email": "fabien@symfony.com"
                },
                {
                    "name": "Symfony Community",
                    "homepage": "https://symfony.com/contributors"
                }
            ],
            "description": "Helps you find, load, combine, autofill and validate configuration values of any kind",
            "homepage": "https://symfony.com",
            "support": {
                "source": "https://github.com/symfony/config/tree/v6.4.43"
            },
            "funding": [
                {
                    "url": "https://symfony.com/sponsor",
                    "type": "custom"
                },
                {
                    "url": "https://github.com/fabpot",
                    "type": "github"
                },
                {
                    "url": "https://github.com/nicolas-grekas",
                    "type": "github"
                },
                {
                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
                    "type": "tidelift"
                }
            ],
            "install-path": "../symfony/config"
        },
        {
            "name": "symfony/console",
            "version": "v6.4.43",
            "version_normalized": "6.4.43.0",
            "source": {
                "type": "git",
                "url": "https://github.com/symfony/console.git",
                "reference": "3b643aa587acbc42f967a429af088a56ed8f046d"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/symfony/console/zipball/3b643aa587acbc42f967a429af088a56ed8f046d",
                "reference": "3b643aa587acbc42f967a429af088a56ed8f046d",
                "shasum": ""
            },
            "require": {
                "php": ">=8.1",
                "symfony/deprecation-contracts": "^2.5|^3",
                "symfony/polyfill-mbstring": "~1.0",
                "symfony/service-contracts": "^2.5|^3",
                "symfony/string": "^5.4|^6.0|^7.0"
            },
            "conflict": {
                "symfony/dependency-injection": "<5.4",
                "symfony/dotenv": "<5.4",
                "symfony/event-dispatcher": "<5.4",
                "symfony/lock": "<5.4",
                "symfony/process": "<5.4"
            },
            "provide": {
                "psr/log-implementation": "1.0|2.0|3.0"
            },
            "require-dev": {
                "psr/log": "^1|^2|^3",
                "symfony/config": "^5.4|^6.0|^7.0",
                "symfony/dependency-injection": "^5.4|^6.0|^7.0",
                "symfony/event-dispatcher": "^5.4|^6.0|^7.0",
                "symfony/http-foundation": "^6.4|^7.0",
                "symfony/http-kernel": "^6.4|^7.0",
                "symfony/lock": "^5.4|^6.0|^7.0",
                "symfony/messenger": "^5.4|^6.0|^7.0",
                "symfony/process": "^5.4|^6.0|^7.0",
                "symfony/stopwatch": "^5.4|^6.0|^7.0",
                "symfony/var-dumper": "^5.4|^6.0|^7.0"
            },
            "time": "2026-07-26T14:44:19+00:00",
            "type": "library",
            "installation-source": "dist",
            "autoload": {
                "psr-4": {
                    "Symfony\\Component\\Console\\": ""
                },
                "exclude-from-classmap": [
                    "/Tests/"
                ]
            },
            "notification-url": "https://packagist.org/downloads/",
            "license": [
                "MIT"
            ],
            "authors": [
                {
                    "name": "Fabien Potencier",
                    "email": "fabien@symfony.com"
                },
                {
                    "name": "Symfony Community",
                    "homepage": "https://symfony.com/contributors"
                }
            ],
            "description": "Eases the creation of beautiful and testable command line interfaces",
            "homepage": "https://symfony.com",
            "keywords": [
                "cli",
                "command-line",
                "console",
                "terminal"
            ],
            "support": {
                "source": "https://github.com/symfony/console/tree/v6.4.43"
            },
            "funding": [
                {
                    "url": "https://symfony.com/sponsor",
                    "type": "custom"
                },
                {
                    "url": "https://github.com/fabpot",
                    "type": "github"
                },
                {
                    "url": "https://github.com/nicolas-grekas",
                    "type": "github"
                },
                {
                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
                    "type": "tidelift"
                }
            ],
            "install-path": "../symfony/console"
        },
        {
            "name": "symfony/dependency-injection",
            "version": "v6.4.43",
            "version_normalized": "6.4.43.0",
            "source": {
                "type": "git",
                "url": "https://github.com/symfony/dependency-injection.git",
                "reference": "f11815fd2f5f80ce94bf527b5ecfd4bd0b1cc905"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/f11815fd2f5f80ce94bf527b5ecfd4bd0b1cc905",
                "reference": "f11815fd2f5f80ce94bf527b5ecfd4bd0b1cc905",
                "shasum": ""
            },
            "require": {
                "php": ">=8.1",
                "psr/container": "^1.1|^2.0",
                "symfony/deprecation-contracts": "^2.5|^3",
                "symfony/service-contracts": "^2.5|^3.0",
                "symfony/var-exporter": "^6.4.20|^7.2.5"
            },
            "conflict": {
                "ext-psr": "<1.1|>=2",
                "symfony/config": "<6.1",
                "symfony/finder": "<5.4",
                "symfony/proxy-manager-bridge": "<6.3",
                "symfony/yaml": "<5.4"
            },
            "provide": {
                "psr/container-implementation": "1.1|2.0",
                "symfony/service-implementation": "1.1|2.0|3.0"
            },
            "require-dev": {
                "symfony/config": "^6.1|^7.0",
                "symfony/expression-language": "^5.4|^6.0|^7.0",
                "symfony/yaml": "^5.4|^6.0|^7.0"
            },
            "time": "2026-07-20T15:13:12+00:00",
            "type": "library",
            "installation-source": "dist",
            "autoload": {
                "psr-4": {
                    "Symfony\\Component\\DependencyInjection\\": ""
                },
                "exclude-from-classmap": [
                    "/Tests/"
                ]
            },
            "notification-url": "https://packagist.org/downloads/",
            "license": [
                "MIT"
            ],
            "authors": [
                {
                    "name": "Fabien Potencier",
                    "email": "fabien@symfony.com"
                },
                {
                    "name": "Symfony Community",
                    "homepage": "https://symfony.com/contributors"
                }
            ],
            "description": "Allows you to standardize and centralize the way objects are constructed in your application",
            "homepage": "https://symfony.com",
            "support": {
                "source": "https://github.com/symfony/dependency-injection/tree/v6.4.43"
            },
            "funding": [
                {
                    "url": "https://symfony.com/sponsor",
                    "type": "custom"
                },
                {
                    "url": "https://github.com/fabpot",
                    "type": "github"
                },
                {
                    "url": "https://github.com/nicolas-grekas",
                    "type": "github"
                },
                {
                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
                    "type": "tidelift"
                }
            ],
            "install-path": "../symfony/dependency-injection"
        },
        {
            "name": "symfony/deprecation-contracts",
            "version": "v3.7.1",
            "version_normalized": "3.7.1.0",
            "source": {
                "type": "git",
                "url": "https://github.com/symfony/deprecation-contracts.git",
                "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/f3202fa1b5097b0af062dc978b32ecf63404e31d",
                "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d",
                "shasum": ""
            },
            "require": {
                "php": ">=8.1"
            },
            "time": "2026-06-05T06:23:12+00:00",
            "type": "library",
            "extra": {
                "thanks": {
                    "url": "https://github.com/symfony/contracts",
                    "name": "symfony/contracts"
                },
                "branch-alias": {
                    "dev-main": "3.7-dev"
                }
            },
            "installation-source": "dist",
            "autoload": {
                "files": [
                    "function.php"
                ]
            },
            "notification-url": "https://packagist.org/downloads/",
            "license": [
                "MIT"
            ],
            "authors": [
                {
                    "name": "Nicolas Grekas",
                    "email": "p@tchwork.com"
                },
                {
                    "name": "Symfony Community",
                    "homepage": "https://symfony.com/contributors"
                }
            ],
            "description": "A generic function and convention to trigger deprecation notices",
            "homepage": "https://symfony.com",
            "support": {
                "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.1"
            },
            "funding": [
                {
                    "url": "https://symfony.com/sponsor",
                    "type": "custom"
                },
                {
                    "url": "https://github.com/fabpot",
                    "type": "github"
                },
                {
                    "url": "https://github.com/nicolas-grekas",
                    "type": "github"
                },
                {
                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
                    "type": "tidelift"
                }
            ],
            "install-path": "../symfony/deprecation-contracts"
        },
        {
            "name": "symfony/event-dispatcher",
            "version": "v6.4.43",
            "version_normalized": "6.4.43.0",
            "source": {
                "type": "git",
                "url": "https://github.com/symfony/event-dispatcher.git",
                "reference": "ac405d324c10ebbbde6a6e58379bf81db10f1dbf"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/ac405d324c10ebbbde6a6e58379bf81db10f1dbf",
                "reference": "ac405d324c10ebbbde6a6e58379bf81db10f1dbf",
                "shasum": ""
            },
            "require": {
                "php": ">=8.1",
                "symfony/event-dispatcher-contracts": "^2.5|^3"
            },
            "conflict": {
                "symfony/dependency-injection": "<5.4",
                "symfony/service-contracts": "<2.5"
            },
            "provide": {
                "psr/event-dispatcher-implementation": "1.0",
                "symfony/event-dispatcher-implementation": "2.0|3.0"
            },
            "require-dev": {
                "psr/log": "^1|^2|^3",
                "symfony/config": "^5.4|^6.0|^7.0",
                "symfony/dependency-injection": "^5.4|^6.0|^7.0",
                "symfony/error-handler": "^5.4|^6.0|^7.0",
                "symfony/expression-language": "^5.4|^6.0|^7.0",
                "symfony/http-foundation": "^5.4|^6.0|^7.0",
                "symfony/service-contracts": "^2.5|^3",
                "symfony/stopwatch": "^5.4|^6.0|^7.0"
            },
            "time": "2026-07-21T14:00:19+00:00",
            "type": "library",
            "installation-source": "dist",
            "autoload": {
                "psr-4": {
                    "Symfony\\Component\\EventDispatcher\\": ""
                },
                "exclude-from-classmap": [
                    "/Tests/"
                ]
            },
            "notification-url": "https://packagist.org/downloads/",
            "license": [
                "MIT"
            ],
            "authors": [
                {
                    "name": "Fabien Potencier",
                    "email": "fabien@symfony.com"
                },
                {
                    "name": "Symfony Community",
                    "homepage": "https://symfony.com/contributors"
                }
            ],
            "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
            "homepage": "https://symfony.com",
            "support": {
                "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.43"
            },
            "funding": [
                {
                    "url": "https://symfony.com/sponsor",
                    "type": "custom"
                },
                {
                    "url": "https://github.com/fabpot",
                    "type": "github"
                },
                {
                    "url": "https://github.com/nicolas-grekas",
                    "type": "github"
                },
                {
                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
                    "type": "tidelift"
                }
            ],
            "install-path": "../symfony/event-dispatcher"
        },
        {
            "name": "symfony/event-dispatcher-contracts",
            "version": "v3.7.1",
            "version_normalized": "3.7.1.0",
            "source": {
                "type": "git",
                "url": "https://github.com/symfony/event-dispatcher-contracts.git",
                "reference": "c7de7a00ffb67842132da02ea92988a39ccd9f4e"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/c7de7a00ffb67842132da02ea92988a39ccd9f4e",
                "reference": "c7de7a00ffb67842132da02ea92988a39ccd9f4e",
                "shasum": ""
            },
            "require": {
                "php": ">=8.1",
                "psr/event-dispatcher": "^1"
            },
            "time": "2026-06-05T06:23:12+00:00",
            "type": "library",
            "extra": {
                "thanks": {
                    "url": "https://github.com/symfony/contracts",
                    "name": "symfony/contracts"
                },
                "branch-alias": {
                    "dev-main": "3.7-dev"
                }
            },
            "installation-source": "dist",
            "autoload": {
                "psr-4": {
                    "Symfony\\Contracts\\EventDispatcher\\": ""
                }
            },
            "notification-url": "https://packagist.org/downloads/",
            "license": [
                "MIT"
            ],
            "authors": [
                {
                    "name": "Nicolas Grekas",
                    "email": "p@tchwork.com"
                },
                {
                    "name": "Symfony Community",
                    "homepage": "https://symfony.com/contributors"
                }
            ],
            "description": "Generic abstractions related to dispatching event",
            "homepage": "https://symfony.com",
            "keywords": [
                "abstractions",
                "contracts",
                "decoupling",
                "interfaces",
                "interoperability",
                "standards"
            ],
            "support": {
                "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.7.1"
            },
            "funding": [
                {
                    "url": "https://symfony.com/sponsor",
                    "type": "custom"
                },
                {
                    "url": "https://github.com/fabpot",
                    "type": "github"
                },
                {
                    "url": "https://github.com/nicolas-grekas",
                    "type": "github"
                },
                {
                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
                    "type": "tidelift"
                }
            ],
            "install-path": "../symfony/event-dispatcher-contracts"
        },
        {
            "name": "symfony/filesystem",
            "version": "v6.4.43",
            "version_normalized": "6.4.43.0",
            "source": {
                "type": "git",
                "url": "https://github.com/symfony/filesystem.git",
                "reference": "9ff03da12d67649fbd1f34ca95951554624d0a16"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/symfony/filesystem/zipball/9ff03da12d67649fbd1f34ca95951554624d0a16",
                "reference": "9ff03da12d67649fbd1f34ca95951554624d0a16",
                "shasum": ""
            },
            "require": {
                "php": ">=8.1",
                "symfony/polyfill-ctype": "~1.8",
                "symfony/polyfill-mbstring": "~1.8"
            },
            "require-dev": {
                "symfony/process": "^5.4|^6.4|^7.0"
            },
            "time": "2026-06-27T10:13:35+00:00",
            "type": "library",
            "installation-source": "dist",
            "autoload": {
                "psr-4": {
                    "Symfony\\Component\\Filesystem\\": ""
                },
                "exclude-from-classmap": [
                    "/Tests/"
                ]
            },
            "notification-url": "https://packagist.org/downloads/",
            "license": [
                "MIT"
            ],
            "authors": [
                {
                    "name": "Fabien Potencier",
                    "email": "fabien@symfony.com"
                },
                {
                    "name": "Symfony Community",
                    "homepage": "https://symfony.com/contributors"
                }
            ],
            "description": "Provides basic utilities for the filesystem",
            "homepage": "https://symfony.com",
            "support": {
                "source": "https://github.com/symfony/filesystem/tree/v6.4.43"
            },
            "funding": [
                {
                    "url": "https://symfony.com/sponsor",
                    "type": "custom"
                },
                {
                    "url": "https://github.com/fabpot",
                    "type": "github"
                },
                {
                    "url": "https://github.com/nicolas-grekas",
                    "type": "github"
                },
                {
                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
                    "type": "tidelift"
                }
            ],
            "install-path": "../symfony/filesystem"
        },
        {
            "name": "symfony/finder",
            "version": "v6.4.42",
            "version_normalized": "6.4.42.0",
            "source": {
                "type": "git",
                "url": "https://github.com/symfony/finder.git",
                "reference": "0b73dac42493acbadbba644207a715b254e9b029"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/symfony/finder/zipball/0b73dac42493acbadbba644207a715b254e9b029",
                "reference": "0b73dac42493acbadbba644207a715b254e9b029",
                "shasum": ""
            },
            "require": {
                "php": ">=8.1"
            },
            "require-dev": {
                "symfony/filesystem": "^6.0|^7.0"
            },
            "time": "2026-06-26T15:18:24+00:00",
            "type": "library",
            "installation-source": "dist",
            "autoload": {
                "psr-4": {
                    "Symfony\\Component\\Finder\\": ""
                },
                "exclude-from-classmap": [
                    "/Tests/"
                ]
            },
            "notification-url": "https://packagist.org/downloads/",
            "license": [
                "MIT"
            ],
            "authors": [
                {
                    "name": "Fabien Potencier",
                    "email": "fabien@symfony.com"
                },
                {
                    "name": "Symfony Community",
                    "homepage": "https://symfony.com/contributors"
                }
            ],
            "description": "Finds files and directories via an intuitive fluent interface",
            "homepage": "https://symfony.com",
            "support": {
                "source": "https://github.com/symfony/finder/tree/v6.4.42"
            },
            "funding": [
                {
                    "url": "https://symfony.com/sponsor",
                    "type": "custom"
                },
                {
                    "url": "https://github.com/fabpot",
                    "type": "github"
                },
                {
                    "url": "https://github.com/nicolas-grekas",
                    "type": "github"
                },
                {
                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
                    "type": "tidelift"
                }
            ],
            "install-path": "../symfony/finder"
        },
        {
            "name": "symfony/polyfill-ctype",
            "version": "v1.37.0",
            "version_normalized": "1.37.0.0",
            "source": {
                "type": "git",
                "url": "https://github.com/symfony/polyfill-ctype.git",
                "reference": "141046a8f9477948ff284fa65be2095baafb94f2"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2",
                "reference": "141046a8f9477948ff284fa65be2095baafb94f2",
                "shasum": ""
            },
            "require": {
                "php": ">=7.2"
            },
            "provide": {
                "ext-ctype": "*"
            },
            "suggest": {
                "ext-ctype": "For best performance"
            },
            "time": "2026-04-10T16:19:22+00:00",
            "type": "library",
            "extra": {
                "thanks": {
                    "url": "https://github.com/symfony/polyfill",
                    "name": "symfony/polyfill"
                }
            },
            "installation-source": "dist",
            "autoload": {
                "files": [
                    "bootstrap.php"
                ],
                "psr-4": {
                    "Symfony\\Polyfill\\Ctype\\": ""
                }
            },
            "notification-url": "https://packagist.org/downloads/",
            "license": [
                "MIT"
            ],
            "authors": [
                {
                    "name": "Gert de Pagter",
                    "email": "BackEndTea@gmail.com"
                },
                {
                    "name": "Symfony Community",
                    "homepage": "https://symfony.com/contributors"
                }
            ],
            "description": "Symfony polyfill for ctype functions",
            "homepage": "https://symfony.com",
            "keywords": [
                "compatibility",
                "ctype",
                "polyfill",
                "portable"
            ],
            "support": {
                "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0"
            },
            "funding": [
                {
                    "url": "https://symfony.com/sponsor",
                    "type": "custom"
                },
                {
                    "url": "https://github.com/fabpot",
                    "type": "github"
                },
                {
                    "url": "https://github.com/nicolas-grekas",
                    "type": "github"
                },
                {
                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
                    "type": "tidelift"
                }
            ],
            "install-path": "../symfony/polyfill-ctype"
        },
        {
            "name": "symfony/polyfill-intl-grapheme",
            "version": "v1.41.0",
            "version_normalized": "1.41.0.0",
            "source": {
                "type": "git",
                "url": "https://github.com/symfony/polyfill-intl-grapheme.git",
                "reference": "bb899c1db0aa8127dc3afe8cda4a67eb24915f8d"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/bb899c1db0aa8127dc3afe8cda4a67eb24915f8d",
                "reference": "bb899c1db0aa8127dc3afe8cda4a67eb24915f8d",
                "shasum": ""
            },
            "require": {
                "php": ">=7.2"
            },
            "suggest": {
                "ext-intl": "For best performance"
            },
            "time": "2026-07-28T08:25:59+00:00",
            "type": "library",
            "extra": {
                "thanks": {
                    "url": "https://github.com/symfony/polyfill",
                    "name": "symfony/polyfill"
                }
            },
            "installation-source": "dist",
            "autoload": {
                "files": [
                    "bootstrap.php"
                ],
                "psr-4": {
                    "Symfony\\Polyfill\\Intl\\Grapheme\\": ""
                }
            },
            "notification-url": "https://packagist.org/downloads/",
            "license": [
                "MIT"
            ],
            "authors": [
                {
                    "name": "Nicolas Grekas",
                    "email": "p@tchwork.com"
                },
                {
                    "name": "Symfony Community",
                    "homepage": "https://symfony.com/contributors"
                }
            ],
            "description": "Symfony polyfill for intl's grapheme_* functions",
            "homepage": "https://symfony.com",
            "keywords": [
                "compatibility",
                "grapheme",
                "intl",
                "polyfill",
                "portable",
                "shim"
            ],
            "support": {
                "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.41.0"
            },
            "funding": [
                {
                    "url": "https://symfony.com/sponsor",
                    "type": "custom"
                },
                {
                    "url": "https://github.com/fabpot",
                    "type": "github"
                },
                {
                    "url": "https://github.com/nicolas-grekas",
                    "type": "github"
                },
                {
                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
                    "type": "tidelift"
                }
            ],
            "install-path": "../symfony/polyfill-intl-grapheme"
        },
        {
            "name": "symfony/polyfill-intl-normalizer",
            "version": "v1.38.0",
            "version_normalized": "1.38.0.0",
            "source": {
                "type": "git",
                "url": "https://github.com/symfony/polyfill-intl-normalizer.git",
                "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/2d446c214bdbe5b71bde5011b060a05fece3ae6b",
                "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b",
                "shasum": ""
            },
            "require": {
                "php": ">=7.2"
            },
            "suggest": {
                "ext-intl": "For best performance"
            },
            "time": "2026-05-25T13:48:31+00:00",
            "type": "library",
            "extra": {
                "thanks": {
                    "url": "https://github.com/symfony/polyfill",
                    "name": "symfony/polyfill"
                }
            },
            "installation-source": "dist",
            "autoload": {
                "files": [
                    "bootstrap.php"
                ],
                "psr-4": {
                    "Symfony\\Polyfill\\Intl\\Normalizer\\": ""
                },
                "classmap": [
                    "Resources/stubs"
                ]
            },
            "notification-url": "https://packagist.org/downloads/",
            "license": [
                "MIT"
            ],
            "authors": [
                {
                    "name": "Nicolas Grekas",
                    "email": "p@tchwork.com"
                },
                {
                    "name": "Symfony Community",
                    "homepage": "https://symfony.com/contributors"
                }
            ],
            "description": "Symfony polyfill for intl's Normalizer class and related functions",
            "homepage": "https://symfony.com",
            "keywords": [
                "compatibility",
                "intl",
                "normalizer",
                "polyfill",
                "portable",
                "shim"
            ],
            "support": {
                "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.38.0"
            },
            "funding": [
                {
                    "url": "https://symfony.com/sponsor",
                    "type": "custom"
                },
                {
                    "url": "https://github.com/fabpot",
                    "type": "github"
                },
                {
                    "url": "https://github.com/nicolas-grekas",
                    "type": "github"
                },
                {
                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
                    "type": "tidelift"
                }
            ],
            "install-path": "../symfony/polyfill-intl-normalizer"
        },
        {
            "name": "symfony/polyfill-mbstring",
            "version": "v1.38.2",
            "version_normalized": "1.38.2.0",
            "source": {
                "type": "git",
                "url": "https://github.com/symfony/polyfill-mbstring.git",
                "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6",
                "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6",
                "shasum": ""
            },
            "require": {
                "ext-iconv": "*",
                "php": ">=7.2"
            },
            "provide": {
                "ext-mbstring": "*"
            },
            "suggest": {
                "ext-mbstring": "For best performance"
            },
            "time": "2026-05-27T06:59:30+00:00",
            "type": "library",
            "extra": {
                "thanks": {
                    "url": "https://github.com/symfony/polyfill",
                    "name": "symfony/polyfill"
                }
            },
            "installation-source": "dist",
            "autoload": {
                "files": [
                    "bootstrap.php"
                ],
                "psr-4": {
                    "Symfony\\Polyfill\\Mbstring\\": ""
                }
            },
            "notification-url": "https://packagist.org/downloads/",
            "license": [
                "MIT"
            ],
            "authors": [
                {
                    "name": "Nicolas Grekas",
                    "email": "p@tchwork.com"
                },
                {
                    "name": "Symfony Community",
                    "homepage": "https://symfony.com/contributors"
                }
            ],
            "description": "Symfony polyfill for the Mbstring extension",
            "homepage": "https://symfony.com",
            "keywords": [
                "compatibility",
                "mbstring",
                "polyfill",
                "portable",
                "shim"
            ],
            "support": {
                "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2"
            },
            "funding": [
                {
                    "url": "https://symfony.com/sponsor",
                    "type": "custom"
                },
                {
                    "url": "https://github.com/fabpot",
                    "type": "github"
                },
                {
                    "url": "https://github.com/nicolas-grekas",
                    "type": "github"
                },
                {
                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
                    "type": "tidelift"
                }
            ],
            "install-path": "../symfony/polyfill-mbstring"
        },
        {
            "name": "symfony/service-contracts",
            "version": "v3.7.1",
            "version_normalized": "3.7.1.0",
            "source": {
                "type": "git",
                "url": "https://github.com/symfony/service-contracts.git",
                "reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/symfony/service-contracts/zipball/c0a284bab1ed8aa0417e3d69250ab437739563a0",
                "reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0",
                "shasum": ""
            },
            "require": {
                "php": ">=8.1",
                "psr/container": "^1.1|^2.0",
                "symfony/deprecation-contracts": "^2.5|^3"
            },
            "conflict": {
                "ext-psr": "<1.1|>=2"
            },
            "time": "2026-06-16T09:55:08+00:00",
            "type": "library",
            "extra": {
                "thanks": {
                    "url": "https://github.com/symfony/contracts",
                    "name": "symfony/contracts"
                },
                "branch-alias": {
                    "dev-main": "3.7-dev"
                }
            },
            "installation-source": "dist",
            "autoload": {
                "psr-4": {
                    "Symfony\\Contracts\\Service\\": ""
                },
                "exclude-from-classmap": [
                    "/Test/"
                ]
            },
            "notification-url": "https://packagist.org/downloads/",
            "license": [
                "MIT"
            ],
            "authors": [
                {
                    "name": "Nicolas Grekas",
                    "email": "p@tchwork.com"
                },
                {
                    "name": "Symfony Community",
                    "homepage": "https://symfony.com/contributors"
                }
            ],
            "description": "Generic abstractions related to writing services",
            "homepage": "https://symfony.com",
            "keywords": [
                "abstractions",
                "contracts",
                "decoupling",
                "interfaces",
                "interoperability",
                "standards"
            ],
            "support": {
                "source": "https://github.com/symfony/service-contracts/tree/v3.7.1"
            },
            "funding": [
                {
                    "url": "https://symfony.com/sponsor",
                    "type": "custom"
                },
                {
                    "url": "https://github.com/fabpot",
                    "type": "github"
                },
                {
                    "url": "https://github.com/nicolas-grekas",
                    "type": "github"
                },
                {
                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
                    "type": "tidelift"
                }
            ],
            "install-path": "../symfony/service-contracts"
        },
        {
            "name": "symfony/string",
            "version": "v7.4.15",
            "version_normalized": "7.4.15.0",
            "source": {
                "type": "git",
                "url": "https://github.com/symfony/string.git",
                "reference": "e394af32256bf9e7bf80849d95e589167c10097b"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/symfony/string/zipball/e394af32256bf9e7bf80849d95e589167c10097b",
                "reference": "e394af32256bf9e7bf80849d95e589167c10097b",
                "shasum": ""
            },
            "require": {
                "php": ">=8.2",
                "symfony/deprecation-contracts": "^2.5|^3.0",
                "symfony/polyfill-ctype": "~1.8",
                "symfony/polyfill-intl-grapheme": "~1.33",
                "symfony/polyfill-intl-normalizer": "~1.0",
                "symfony/polyfill-mbstring": "~1.0"
            },
            "conflict": {
                "symfony/translation-contracts": "<2.5"
            },
            "require-dev": {
                "symfony/emoji": "^7.1|^8.0",
                "symfony/http-client": "^6.4|^7.0|^8.0",
                "symfony/intl": "^6.4|^7.0|^8.0",
                "symfony/translation-contracts": "^2.5|^3.0",
                "symfony/var-exporter": "^6.4|^7.0|^8.0"
            },
            "time": "2026-07-28T07:33:02+00:00",
            "type": "library",
            "installation-source": "dist",
            "autoload": {
                "files": [
                    "Resources/functions.php"
                ],
                "psr-4": {
                    "Symfony\\Component\\String\\": ""
                },
                "exclude-from-classmap": [
                    "/Tests/"
                ]
            },
            "notification-url": "https://packagist.org/downloads/",
            "license": [
                "MIT"
            ],
            "authors": [
                {
                    "name": "Nicolas Grekas",
                    "email": "p@tchwork.com"
                },
                {
                    "name": "Symfony Community",
                    "homepage": "https://symfony.com/contributors"
                }
            ],
            "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way",
            "homepage": "https://symfony.com",
            "keywords": [
                "grapheme",
                "i18n",
                "string",
                "unicode",
                "utf-8",
                "utf8"
            ],
            "support": {
                "source": "https://github.com/symfony/string/tree/v7.4.15"
            },
            "funding": [
                {
                    "url": "https://symfony.com/sponsor",
                    "type": "custom"
                },
                {
                    "url": "https://github.com/fabpot",
                    "type": "github"
                },
                {
                    "url": "https://github.com/nicolas-grekas",
                    "type": "github"
                },
                {
                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
                    "type": "tidelift"
                }
            ],
            "install-path": "../symfony/string"
        },
        {
            "name": "symfony/var-exporter",
            "version": "v7.4.16",
            "version_normalized": "7.4.16.0",
            "source": {
                "type": "git",
                "url": "https://github.com/symfony/var-exporter.git",
                "reference": "ca31404415670aa3834809005b529df1b84f0790"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/symfony/var-exporter/zipball/ca31404415670aa3834809005b529df1b84f0790",
                "reference": "ca31404415670aa3834809005b529df1b84f0790",
                "shasum": ""
            },
            "require": {
                "php": ">=8.2",
                "symfony/deprecation-contracts": "^2.5|^3"
            },
            "require-dev": {
                "symfony/property-access": "^6.4|^7.0|^8.0",
                "symfony/serializer": "^6.4|^7.0|^8.0",
                "symfony/var-dumper": "^6.4|^7.0|^8.0"
            },
            "time": "2026-07-30T12:37:26+00:00",
            "type": "library",
            "installation-source": "dist",
            "autoload": {
                "psr-4": {
                    "Symfony\\Component\\VarExporter\\": ""
                },
                "exclude-from-classmap": [
                    "/Tests/"
                ]
            },
            "notification-url": "https://packagist.org/downloads/",
            "license": [
                "MIT"
            ],
            "authors": [
                {
                    "name": "Nicolas Grekas",
                    "email": "p@tchwork.com"
                },
                {
                    "name": "Symfony Community",
                    "homepage": "https://symfony.com/contributors"
                }
            ],
            "description": "Allows exporting any serializable PHP data structure to plain PHP code",
            "homepage": "https://symfony.com",
            "keywords": [
                "clone",
                "construct",
                "export",
                "hydrate",
                "instantiate",
                "lazy-loading",
                "proxy",
                "serialize"
            ],
            "support": {
                "source": "https://github.com/symfony/var-exporter/tree/v7.4.16"
            },
            "funding": [
                {
                    "url": "https://symfony.com/sponsor",
                    "type": "custom"
                },
                {
                    "url": "https://github.com/fabpot",
                    "type": "github"
                },
                {
                    "url": "https://github.com/nicolas-grekas",
                    "type": "github"
                },
                {
                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
                    "type": "tidelift"
                }
            ],
            "install-path": "../symfony/var-exporter"
        },
        {
            "name": "symfony/yaml",
            "version": "v6.4.43",
            "version_normalized": "6.4.43.0",
            "source": {
                "type": "git",
                "url": "https://github.com/symfony/yaml.git",
                "reference": "d2ee2daa59be8cd0864835ea918ea209149796a3"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/symfony/yaml/zipball/d2ee2daa59be8cd0864835ea918ea209149796a3",
                "reference": "d2ee2daa59be8cd0864835ea918ea209149796a3",
                "shasum": ""
            },
            "require": {
                "php": ">=8.1",
                "symfony/deprecation-contracts": "^2.5|^3",
                "symfony/polyfill-ctype": "^1.8"
            },
            "conflict": {
                "symfony/console": "<5.4"
            },
            "require-dev": {
                "symfony/console": "^5.4|^6.0|^7.0"
            },
            "time": "2026-07-20T15:18:49+00:00",
            "bin": [
                "Resources/bin/yaml-lint"
            ],
            "type": "library",
            "installation-source": "dist",
            "autoload": {
                "psr-4": {
                    "Symfony\\Component\\Yaml\\": ""
                },
                "exclude-from-classmap": [
                    "/Tests/"
                ]
            },
            "notification-url": "https://packagist.org/downloads/",
            "license": [
                "MIT"
            ],
            "authors": [
                {
                    "name": "Fabien Potencier",
                    "email": "fabien@symfony.com"
                },
                {
                    "name": "Symfony Community",
                    "homepage": "https://symfony.com/contributors"
                }
            ],
            "description": "Loads and dumps YAML files",
            "homepage": "https://symfony.com",
            "support": {
                "source": "https://github.com/symfony/yaml/tree/v6.4.43"
            },
            "funding": [
                {
                    "url": "https://symfony.com/sponsor",
                    "type": "custom"
                },
                {
                    "url": "https://github.com/fabpot",
                    "type": "github"
                },
                {
                    "url": "https://github.com/nicolas-grekas",
                    "type": "github"
                },
                {
                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
                    "type": "tidelift"
                }
            ],
            "install-path": "../symfony/yaml"
        }
    ],
    "dev": true,
    "dev-package-names": []
}
#!/usr/bin/env php
<?php

/**
 * Proxy PHP file generated by Composer
 *
 * This file includes the referenced bin path (../doctrine/dbal/bin/doctrine-dbal)
 * using a stream wrapper to prevent the shebang from being output on PHP<8
 *
 * @generated
 */

namespace Composer;

$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';

if (PHP_VERSION_ID < 80000) {
    if (!class_exists('Composer\BinProxyWrapper')) {
        /**
         * @internal
         */
        final class BinProxyWrapper
        {
            private $handle;
            private $position;
            private $realpath;

            public function stream_open($path, $mode, $options, &$opened_path)
            {
                // get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
                $opened_path = substr($path, 17);
                $this->realpath = realpath($opened_path) ?: $opened_path;
                $opened_path = $this->realpath;
                $this->handle = fopen($this->realpath, $mode);
                $this->position = 0;

                return (bool) $this->handle;
            }

            public function stream_read($count)
            {
                $data = fread($this->handle, $count);

                if ($this->position === 0) {
                    $data = preg_replace('{^#!.*\r?\n}', '', $data);
                }

                $this->position += strlen($data);

                return $data;
            }

            public function stream_cast($castAs)
            {
                return $this->handle;
            }

            public function stream_close()
            {
                fclose($this->handle);
            }

            public function stream_lock($operation)
            {
                return $operation ? flock($this->handle, $operation) : true;
            }

            public function stream_seek($offset, $whence)
            {
                if (0 === fseek($this->handle, $offset, $whence)) {
                    $this->position = ftell($this->handle);
                    return true;
                }

                return false;
            }

            public function stream_tell()
            {
                return $this->position;
            }

            public function stream_eof()
            {
                return feof($this->handle);
            }

            public function stream_stat()
            {
                return array();
            }

            public function stream_set_option($option, $arg1, $arg2)
            {
                return true;
            }

            public function url_stat($path, $flags)
            {
                $path = substr($path, 17);
                if (file_exists($path)) {
                    return stat($path);
                }

                return false;
            }
        }
    }

    if (
        (function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
        || (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
    ) {
        return include("phpvfscomposer://" . __DIR__ . '/..'.'/doctrine/dbal/bin/doctrine-dbal');
    }
}

return include __DIR__ . '/..'.'/doctrine/dbal/bin/doctrine-dbal';
#!/usr/bin/env php
<?php

/**
 * Proxy PHP file generated by Composer
 *
 * This file includes the referenced bin path (../symfony/yaml/Resources/bin/yaml-lint)
 * using a stream wrapper to prevent the shebang from being output on PHP<8
 *
 * @generated
 */

namespace Composer;

$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';

if (PHP_VERSION_ID < 80000) {
    if (!class_exists('Composer\BinProxyWrapper')) {
        /**
         * @internal
         */
        final class BinProxyWrapper
        {
            private $handle;
            private $position;
            private $realpath;

            public function stream_open($path, $mode, $options, &$opened_path)
            {
                // get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
                $opened_path = substr($path, 17);
                $this->realpath = realpath($opened_path) ?: $opened_path;
                $opened_path = $this->realpath;
                $this->handle = fopen($this->realpath, $mode);
                $this->position = 0;

                return (bool) $this->handle;
            }

            public function stream_read($count)
            {
                $data = fread($this->handle, $count);

                if ($this->position === 0) {
                    $data = preg_replace('{^#!.*\r?\n}', '', $data);
                }

                $this->position += strlen($data);

                return $data;
            }

            public function stream_cast($castAs)
            {
                return $this->handle;
            }

            public function stream_close()
            {
                fclose($this->handle);
            }

            public function stream_lock($operation)
            {
                return $operation ? flock($this->handle, $operation) : true;
            }

            public function stream_seek($offset, $whence)
            {
                if (0 === fseek($this->handle, $offset, $whence)) {
                    $this->position = ftell($this->handle);
                    return true;
                }

                return false;
            }

            public function stream_tell()
            {
                return $this->position;
            }

            public function stream_eof()
            {
                return feof($this->handle);
            }

            public function stream_stat()
            {
                return array();
            }

            public function stream_set_option($option, $arg1, $arg2)
            {
                return true;
            }

            public function url_stat($path, $flags)
            {
                $path = substr($path, 17);
                if (file_exists($path)) {
                    return stat($path);
                }

                return false;
            }
        }
    }

    if (
        (function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
        || (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
    ) {
        return include("phpvfscomposer://" . __DIR__ . '/..'.'/symfony/yaml/Resources/bin/yaml-lint');
    }
}

return include __DIR__ . '/..'.'/symfony/yaml/Resources/bin/yaml-lint';
Copyright (c) 2006-2018 Doctrine Project

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
{
    "name": "doctrine/dbal",
    "type": "library",
    "description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.",
    "keywords": [
        "abstraction",
        "database",
        "dbal",
        "db2",
        "mariadb",
        "mssql",
        "mysql",
        "pgsql",
        "postgresql",
        "oci8",
        "oracle",
        "pdo",
        "queryobject",
        "sasql",
        "sql",
        "sqlanywhere",
        "sqlite",
        "sqlserver",
        "sqlsrv"
    ],
    "homepage": "https://www.doctrine-project.org/projects/dbal.html",
    "license": "MIT",
    "authors": [
        {"name": "Guilherme Blanco", "email": "guilhermeblanco@gmail.com"},
        {"name": "Roman Borschel", "email": "roman@code-factory.org"},
        {"name": "Benjamin Eberlei", "email": "kontakt@beberlei.de"},
        {"name": "Jonathan Wage", "email": "jonwage@gmail.com"}
    ],
    "require": {
        "php": "^7.1 || ^8",
        "ext-pdo": "*",
        "doctrine/cache": "^1.0|^2.0",
        "doctrine/deprecations": "^0.5.3|^1",
        "doctrine/event-manager": "^1.0"
    },
    "require-dev": {
        "doctrine/coding-standard": "9.0.0",
        "jetbrains/phpstorm-stubs": "2021.1",
        "phpstan/phpstan": "1.4.6",
        "phpunit/phpunit": "^7.5.20|^8.5|9.5.16",
        "psalm/plugin-phpunit": "0.16.1",
        "squizlabs/php_codesniffer": "3.6.2",
        "symfony/cache": "^4.4",
        "symfony/console": "^2.0.5|^3.0|^4.0|^5.0",
        "vimeo/psalm": "4.22.0"
    },
    "suggest": {
        "symfony/console": "For helpful console commands such as SQL execution and import of files."
    },
    "bin": ["bin/doctrine-dbal"],
    "config": {
        "sort-packages": true,
        "allow-plugins": {
            "dealerdirect/phpcodesniffer-composer-installer": true,
            "composer/package-versions-deprecated": true
        }
    },
    "autoload": {
        "psr-4": { "Doctrine\\DBAL\\": "lib/Doctrine/DBAL" }
    },
    "autoload-dev": {
        "psr-4": { "Doctrine\\Tests\\": "tests/Doctrine/Tests" }
    }
}
# Doctrine DBAL

| [4.0-dev][4.0] | [3.3][3.3] | [2.13][2.13] |
|:----------------:|:----------:|:----------:|
| [![GitHub Actions][GA 4.0 image]][GA 4.0] | [![GitHub Actions][GA 3.3 image]][GA 3.3] | [![GitHub Actions][GA 2.13 image]][GA 2.13] |
| [![AppVeyor][AppVeyor 4.0 image]][AppVeyor 4.0] | [![AppVeyor][AppVeyor 3.3 image]][AppVeyor 3.3] | [![AppVeyor][AppVeyor 2.13 image]][AppVeyor 2.13] |
| [![Code Coverage][Coverage image]][CodeCov 4.0] | [![Code Coverage][Coverage 3.3 image]][CodeCov 3.3] | [![Code Coverage][Coverage 2.13 image]][CodeCov 2.13] |
| N/A | [![Code Coverage][TypeCov 3.3 image]][TypeCov 3.3] | N/A |

Powerful database abstraction layer with many features for database schema introspection, schema management and PDO abstraction.

## More resources:

* [Website](http://www.doctrine-project.org/projects/dbal.html)
* [Documentation](http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/)
* [Issue Tracker](https://github.com/doctrine/dbal/issues)

  [Coverage image]: https://codecov.io/gh/doctrine/dbal/branch/4.0.x/graph/badge.svg
  [4.0]: https://github.com/doctrine/dbal/tree/4.0.x
  [CodeCov 4.0]: https://codecov.io/gh/doctrine/dbal/branch/4.0.x
  [AppVeyor 4.0]: https://ci.appveyor.com/project/doctrine/dbal/branch/4.0.x
  [AppVeyor 4.0 image]: https://ci.appveyor.com/api/projects/status/i88kitq8qpbm0vie/branch/4.0.x?svg=true
  [GA 4.0]: https://github.com/doctrine/dbal/actions?query=workflow%3A%22Continuous+Integration%22+branch%3A4.0.x
  [GA 4.0 image]: https://github.com/doctrine/dbal/workflows/Continuous%20Integration/badge.svg

  [Coverage 3.3 image]: https://codecov.io/gh/doctrine/dbal/branch/3.3.x/graph/badge.svg
  [3.3]: https://github.com/doctrine/dbal/tree/3.3.x
  [CodeCov 3.3]: https://codecov.io/gh/doctrine/dbal/branch/3.3.x
  [AppVeyor 3.3]: https://ci.appveyor.com/project/doctrine/dbal/branch/3.3.x
  [AppVeyor 3.3 image]: https://ci.appveyor.com/api/projects/status/i88kitq8qpbm0vie/branch/3.3.x?svg=true
  [GA 3.3]: https://github.com/doctrine/dbal/actions?query=workflow%3A%22Continuous+Integration%22+branch%3A3.3.x
  [GA 3.3 image]: https://github.com/doctrine/dbal/workflows/Continuous%20Integration/badge.svg?branch=3.3.x
  [TypeCov 3.3]: https://shepherd.dev/github/doctrine/dbal
  [TypeCov 3.3 image]: https://shepherd.dev/github/doctrine/dbal/coverage.svg

  [Coverage 2.13 image]: https://codecov.io/gh/doctrine/dbal/branch/2.13.x/graph/badge.svg
  [2.13]: https://github.com/doctrine/dbal/tree/2.13.x
  [CodeCov 2.13]: https://codecov.io/gh/doctrine/dbal/branch/2.13.x
  [AppVeyor 2.13]: https://ci.appveyor.com/project/doctrine/dbal/branch/2.13.x
  [AppVeyor 2.13 image]: https://ci.appveyor.com/api/projects/status/i88kitq8qpbm0vie/branch/2.13.x?svg=true
  [GA 2.13]: https://github.com/doctrine/dbal/actions?query=workflow%3A%22Continuous+Integration%22+branch%3A2.13.x
  [GA 2.13 image]: https://github.com/doctrine/dbal/workflows/Continuous%20Integration/badge.svg?branch=2.13.x
#!/usr/bin/env php
<?php

require __DIR__ . '/doctrine-dbal.php';
Doctrine has [general contributing guidelines][contributor workflow], make
sure you follow them.

[contributor workflow]: https://www.doctrine-project.org/contribute/index.html
Copyright (c) 2006-2015 Doctrine Project

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
{
    "name": "doctrine/event-manager",
    "description": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.",
    "license": "MIT",
    "type": "library",
    "keywords": [
        "events",
        "event",
        "event dispatcher",
        "event manager",
        "event system"
    ],
    "authors": [
        {
            "name": "Guilherme Blanco",
            "email": "guilhermeblanco@gmail.com"
        },
        {
            "name": "Roman Borschel",
            "email": "roman@code-factory.org"
        },
        {
            "name": "Benjamin Eberlei",
            "email": "kontakt@beberlei.de"
        },
        {
            "name": "Jonathan Wage",
            "email": "jonwage@gmail.com"
        },
        {
            "name": "Johannes Schmitt",
            "email": "schmittjoh@gmail.com"
        },
        {
            "name": "Marco Pivetta",
            "email": "ocramius@gmail.com"
        }
    ],
    "homepage": "https://www.doctrine-project.org/projects/event-manager.html",
    "require": {
        "php": "^7.1 || ^8.0",
        "doctrine/deprecations": "^0.5.3 || ^1"
    },
    "require-dev": {
        "doctrine/coding-standard": "^9 || ^10",
        "phpstan/phpstan": "~1.4.10 || ^1.8.8",
        "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5",
        "vimeo/psalm": "^4.24"
    },
    "conflict": {
        "doctrine/common": "<2.9"
    },
    "autoload": {
        "psr-4": {
            "Doctrine\\Common\\": "src"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "Doctrine\\Tests\\Common\\": "tests"
        }
    },
    "config": {
        "allow-plugins": {
            "dealerdirect/phpcodesniffer-composer-installer": true
        },
        "sort-packages": true
    }
}
parameters:
  level: 9
  paths:
    - src/
    - tests/
# Doctrine Event Manager

[![Build Status](https://github.com/doctrine/event-manager/workflows/Continuous%20Integration/badge.svg)](https://github.com/doctrine/event-manager/actions)
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/doctrine/event-manager/badges/quality-score.png?b=1.2.x)](https://scrutinizer-ci.com/g/doctrine/event-manager/?branch=1.2.x)
[![Code Coverage](https://scrutinizer-ci.com/g/doctrine/event-manager/badges/coverage.png?b=1.2.x)](https://scrutinizer-ci.com/g/doctrine/event-manager/?branch=1.2.x)

The Doctrine Event Manager is a library that provides a simple event system.

## More resources:

* [Website](https://www.doctrine-project.org/)
* [Documentation](https://www.doctrine-project.org/projects/doctrine-event-manager/en/latest/)
* [Downloads](https://github.com/doctrine/event-manager/releases)
# Upgrade to 1.2

## Deprecated calling `EventManager::getListeners()` without an event name

When calling `EventManager::getListeners()` without an event name, all
listeners were returned, keyed by event name. A new method `getAllListeners()`
has been added to provide this functionality. It should be used instead.
<?xml version="1.0"?>
<psalm
    errorLevel="2"
    resolveFromConfigFile="true"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="https://getpsalm.org/schema/config"
    xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
>
    <projectFiles>
        <directory name="src" />
        <ignoreFiles>
            <directory name="vendor" />
        </ignoreFiles>
    </projectFiles>
</psalm>
Copyright (c) 2020-2021 Doctrine Project

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
{
    "name": "doctrine/deprecations",
    "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.",
    "license": "MIT",
    "type": "library",
    "homepage": "https://www.doctrine-project.org/",
    "require": {
        "php": "^7.1 || ^8.0"
    },
    "require-dev": {
        "doctrine/coding-standard": "^9 || ^12 || ^14",
        "phpstan/phpstan": "1.4.10 || 2.1.30",
        "phpstan/phpstan-phpunit": "^1.0 || ^2",
        "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0",
        "psr/log": "^1 || ^2 || ^3"
    },
    "conflict": {
        "phpunit/phpunit": "<=7.5 || >=14"
    },
    "suggest": {
        "psr/log": "Allows logging deprecations via PSR-3 logger implementation"
    },
    "autoload": {
        "psr-4": {
            "Doctrine\\Deprecations\\": "src"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "DeprecationTests\\": "test_fixtures/src",
            "Doctrine\\Foo\\": "test_fixtures/vendor/doctrine/foo"
        }
    },
    "config": {
        "allow-plugins": {
            "dealerdirect/phpcodesniffer-composer-installer": true
        }
    }
}
# Doctrine Deprecations

A small (side-effect free by default) layer on top of
`trigger_error(E_USER_DEPRECATED)` or PSR-3 logging.

- no side-effects by default, making it a perfect fit for libraries that don't know how the error handler works they operate under
- options to avoid having to rely on error handlers global state by using PSR-3 logging
- deduplicate deprecation messages to avoid excessive triggering and reduce overhead

We recommend to collect Deprecations using a PSR logger instead of relying on
the global error handler.

## Usage from consumer perspective:

Enable Doctrine deprecations to be sent to a PSR3 logger:

```php
\Doctrine\Deprecations\Deprecation::enableWithPsrLogger($logger);
```

Enable Doctrine deprecations to be sent as `@trigger_error($message, E_USER_DEPRECATED)`
messages by setting the `DOCTRINE_DEPRECATIONS` environment variable to `trigger`.
Alternatively, call:

```php
\Doctrine\Deprecations\Deprecation::enableWithTriggerError();
```

If you only want to enable deprecation tracking, without logging or calling `trigger_error`
then set the `DOCTRINE_DEPRECATIONS` environment variable to `track`.
Alternatively, call:

```php
\Doctrine\Deprecations\Deprecation::enableTrackingDeprecations();
```

Tracking is enabled with all three modes and provides access to all triggered
deprecations and their individual count:

```php
$deprecations = \Doctrine\Deprecations\Deprecation::getTriggeredDeprecations();

foreach ($deprecations as $identifier => $count) {
    echo $identifier . " was triggered " . $count . " times\n";
}
```

### Suppressing Specific Deprecations

Disable triggering about specific deprecations:

```php
\Doctrine\Deprecations\Deprecation::ignoreDeprecations("https://link/to/deprecations-description-identifier");
```

Disable all deprecations from a package

```php
\Doctrine\Deprecations\Deprecation::ignorePackage("doctrine/orm");
```

### Other Operations

When used within PHPUnit or other tools that could collect multiple instances of the same deprecations
the deduplication can be disabled:

```php
\Doctrine\Deprecations\Deprecation::withoutDeduplication();
```

Disable deprecation tracking again:

```php
\Doctrine\Deprecations\Deprecation::disable();
```

## Usage from a library/producer perspective:

When you want to unconditionally trigger a deprecation even when called
from the library itself then the `trigger` method is the way to go:

```php
\Doctrine\Deprecations\Deprecation::trigger(
    "doctrine/orm",
    "https://link/to/deprecations-description",
    "message"
);
```

If variable arguments are provided at the end, they are used with `sprintf` on
the message.

```php
\Doctrine\Deprecations\Deprecation::trigger(
    "doctrine/orm",
    "https://github.com/doctrine/orm/issue/1234",
    "message %s %d",
    "foo",
    1234
);
```

When you want to trigger a deprecation only when it is called by a function
outside of the current package, but not trigger when the package itself is the cause,
then use:

```php
\Doctrine\Deprecations\Deprecation::triggerIfCalledFromOutside(
    "doctrine/orm",
    "https://link/to/deprecations-description",
    "message"
);
```

Based on the issue link each deprecation message is only triggered once per
request.

A limited stacktrace is included in the deprecation message to find the
offending location.

Note: A producer/library should never call `Deprecation::enableWith` methods
and leave the decision how to handle deprecations to application and
frameworks.

## Usage in PHPUnit tests

There is a `VerifyDeprecations` trait that you can use to make assertions on
the occurrence of deprecations within a test.

```php
use Doctrine\Deprecations\PHPUnit\VerifyDeprecations;

class MyTest extends TestCase
{
    use VerifyDeprecations;

    public function testSomethingDeprecation()
    {
        $this->expectDeprecationWithIdentifier('https://github.com/doctrine/orm/issue/1234');

        triggerTheCodeWithDeprecation();
    }

    public function testSomethingDeprecationFixed()
    {
        $this->expectNoDeprecationWithIdentifier('https://github.com/doctrine/orm/issue/1234');

        triggerTheCodeWithoutDeprecation();
    }
}
```

## Displaying deprecations after running a PHPUnit test suite

It is possible to integrate this library with PHPUnit to display all
deprecations triggered during the test suite execution.

```xml
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
         colors="true"
         bootstrap="vendor/autoload.php"
         displayDetailsOnTestsThatTriggerDeprecations="true"
         failOnDeprecation="true"
    >
    <!-- one attribute to display the deprecations, the other to fail the test suite -->

    <php>
        <!-- ensures native PHP deprecations are used -->
        <server name="DOCTRINE_DEPRECATIONS" value="trigger"/>
    </php>

    <!-- ensures the @ operator in @trigger_error is ignored -->
    <source ignoreSuppressionOfDeprecations="true">
        <include>
            <directory>src</directory>
        </include>
    </source>
</phpunit>
```

Note that you can still trigger Deprecations in your code, provided you use the
`#[IgnoreDeprecations]` to ignore them for tests that call it.

At the moment, it is not possible to disable deduplication with an environment
variable, but you can use a bootstrap file to achieve that:

```php
// tests/bootstrap.php
<?php

declare(strict_types=1);

require dirname(__DIR__) . '/vendor/autoload.php';

use Doctrine\Deprecations\Deprecation;

Deprecation::withoutDeduplication();
```

Then, reference that file in your PHPUnit configuration:

```xml
<phpunit …
        bootstrap="tests/bootstrap.php"
        …
    >
    …
</phpunit>
```

## What is a deprecation identifier?

An identifier for deprecations is just a link to any resource, most often a
Github Issue or Pull Request explaining the deprecation and potentially its
alternative.
Copyright (c) 2006-2015 Doctrine Project

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
{
    "name": "doctrine/cache",
    "type": "library",
    "description": "PHP Doctrine Cache library is a popular cache implementation that supports many different drivers such as redis, memcache, apc, mongodb and others.",
    "keywords": [
        "php",
        "cache",
        "caching",
        "abstraction",
        "redis",
        "memcached",
        "couchdb",
        "xcache",
        "apcu"
    ],
    "homepage": "https://www.doctrine-project.org/projects/cache.html",
    "license": "MIT",
    "authors": [
        {"name": "Guilherme Blanco", "email": "guilhermeblanco@gmail.com"},
        {"name": "Roman Borschel", "email": "roman@code-factory.org"},
        {"name": "Benjamin Eberlei", "email": "kontakt@beberlei.de"},
        {"name": "Jonathan Wage", "email": "jonwage@gmail.com"},
        {"name": "Johannes Schmitt", "email": "schmittjoh@gmail.com"}
    ],
    "require": {
        "php": "~7.1 || ^8.0"
    },
    "require-dev": {
        "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5",
        "doctrine/coding-standard": "^9",
        "psr/cache": "^1.0 || ^2.0 || ^3.0",
        "cache/integration-tests": "dev-master",
        "symfony/cache": "^4.4 || ^5.4 || ^6",
        "symfony/var-exporter": "^4.4 || ^5.4 || ^6"
    },
    "conflict": {
        "doctrine/common": ">2.2,<2.4"
    },
    "autoload": {
        "psr-4": { "Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache" }
    },
    "autoload-dev": {
        "psr-4": { "Doctrine\\Tests\\": "tests/Doctrine/Tests" }
    },
    "config": {
        "allow-plugins": {
            "dealerdirect/phpcodesniffer-composer-installer": true
        }
    }
}
# Doctrine Cache

[![Build Status](https://github.com/doctrine/cache/workflows/Continuous%20Integration/badge.svg)](https://github.com/doctrine/cache/actions)
[![Code Coverage](https://codecov.io/gh/doctrine/cache/branch/1.10.x/graph/badge.svg)](https://codecov.io/gh/doctrine/cache/branch/1.10.x)

[![Latest Stable Version](https://img.shields.io/packagist/v/doctrine/cache.svg?style=flat-square)](https://packagist.org/packages/doctrine/cache)
[![Total Downloads](https://img.shields.io/packagist/dt/doctrine/cache.svg?style=flat-square)](https://packagist.org/packages/doctrine/cache)

Cache component extracted from the Doctrine Common project. [Documentation](https://www.doctrine-project.org/projects/doctrine-cache/en/current/index.html)

This library is deprecated and will no longer receive bug fixes from the
Doctrine Project. Please use a different cache library, preferably PSR-6 or
PSR-16 instead.
# Upgrade to 1.4

## Minor BC Break: `Doctrine\Common\Cache\FileCache#$extension` is now `private`.

If you need to override the value of `Doctrine\Common\Cache\FileCache#$extension`, then use the
second parameter of `Doctrine\Common\Cache\FileCache#__construct()` instead of overriding
the property in your own implementation.

## Minor BC Break: file based caches paths changed

`Doctrine\Common\Cache\FileCache`, `Doctrine\Common\Cache\PhpFileCache` and
`Doctrine\Common\Cache\FilesystemCache` are using a different cache paths structure.

If you rely on warmed up caches for deployments, consider that caches generated
with `doctrine/cache` `<1.4` are not compatible with the new directory structure,
and will be ignored.
# Upgrade to 1.11

doctrine/cache will no longer be maintained and all cache implementations have
been marked as deprecated. These implementations will be removed in 2.0, which
will only contain interfaces to provide a lightweight package for backward
compatibility.

There are two new classes to use in the `Doctrine\Common\Cache\Psr6` namespace:
* The `CacheAdapter` class allows using any Doctrine Cache as PSR-6 cache. This
  is useful to provide a forward compatibility layer in libraries that accept
  Doctrine cache implementations and switch to PSR-6.
* The `DoctrineProvider` class allows using any PSR-6 cache as Doctrine cache.
  This implementation is designed for libraries that leak the cache and want to
  switch to allowing PSR-6 implementations. This class is design to be used
  during the transition phase of sunsetting doctrine/cache support.

A full example to setup a filesystem based PSR-6 cache with symfony/cache
using the `DoctrineProvider` to convert back to Doctrine's `Cache` interface:

```php
use Doctrine\Common\Cache\Psr6\DoctrineProvider;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;

$cachePool = new FilesystemAdapter();
$cache = DoctrineProvider::wrap($cachePool);
// $cache instanceof \Doctrine\Common\Cache\Cache
```
Copyright (c) 2004-present Fabien Potencier

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
{
    "name": "symfony/dependency-injection",
    "type": "library",
    "description": "Allows you to standardize and centralize the way objects are constructed in your application",
    "keywords": [],
    "homepage": "https://symfony.com",
    "license": "MIT",
    "authors": [
        {
            "name": "Fabien Potencier",
            "email": "fabien@symfony.com"
        },
        {
            "name": "Symfony Community",
            "homepage": "https://symfony.com/contributors"
        }
    ],
    "require": {
        "php": ">=8.1",
        "psr/container": "^1.1|^2.0",
        "symfony/deprecation-contracts": "^2.5|^3",
        "symfony/service-contracts": "^2.5|^3.0",
        "symfony/var-exporter": "^6.4.20|^7.2.5"
    },
    "require-dev": {
        "symfony/yaml": "^5.4|^6.0|^7.0",
        "symfony/config": "^6.1|^7.0",
        "symfony/expression-language": "^5.4|^6.0|^7.0"
    },
    "conflict": {
        "ext-psr": "<1.1|>=2",
        "symfony/config": "<6.1",
        "symfony/finder": "<5.4",
        "symfony/proxy-manager-bridge": "<6.3",
        "symfony/yaml": "<5.4"
    },
    "provide": {
        "psr/container-implementation": "1.1|2.0",
        "symfony/service-implementation": "1.1|2.0|3.0"
    },
    "autoload": {
        "psr-4": { "Symfony\\Component\\DependencyInjection\\": "" },
        "exclude-from-classmap": [
            "/Tests/"
        ]
    },
    "minimum-stability": "dev"
}
<?xml version="1.0" encoding="UTF-8" ?>

<xsd:schema xmlns="http://symfony.com/schema/dic/services"
     xmlns:xsd="http://www.w3.org/2001/XMLSchema"
     targetNamespace="http://symfony.com/schema/dic/services"
     elementFormDefault="qualified">

  <xsd:annotation>
    <xsd:documentation><![CDATA[
      Symfony XML Services Schema, version 1.0
      Authors: Fabien Potencier

      This defines a way to describe PHP objects (services) and their
      dependencies.
    ]]></xsd:documentation>
  </xsd:annotation>

  <xsd:element name="container" type="container" />

  <xsd:complexType name="container">
    <xsd:annotation>
      <xsd:documentation><![CDATA[
        The root element of a service file.
      ]]></xsd:documentation>
    </xsd:annotation>
    <xsd:sequence>
      <xsd:group ref="foreign" />
      <xsd:sequence minOccurs="0">
        <xsd:element name="imports" type="imports" />
        <xsd:group ref="foreign" />
      </xsd:sequence>
      <xsd:sequence minOccurs="0">
        <xsd:element name="parameters" type="parameters" />
        <xsd:group ref="foreign" />
      </xsd:sequence>
      <xsd:sequence minOccurs="0">
        <xsd:element name="services" type="services" />
        <xsd:group ref="foreign" />
      </xsd:sequence>
      <xsd:sequence minOccurs="0" maxOccurs="unbounded">
        <xsd:element name="when" type="when" />
      </xsd:sequence>
    </xsd:sequence>
  </xsd:complexType>

  <xsd:complexType name="when">
    <xsd:sequence>
      <xsd:group ref="foreign" />
      <xsd:sequence minOccurs="0">
        <xsd:element name="imports" type="imports" />
        <xsd:group ref="foreign" />
      </xsd:sequence>
      <xsd:sequence minOccurs="0">
        <xsd:element name="parameters" type="parameters" />
        <xsd:group ref="foreign" />
      </xsd:sequence>
      <xsd:sequence minOccurs="0">
        <xsd:element name="services" type="services" />
        <xsd:group ref="foreign" />
      </xsd:sequence>
    </xsd:sequence>
    <xsd:attribute name="env" type="xsd:string" use="required" />
  </xsd:complexType>

  <xsd:group name="foreign">
    <xsd:sequence>
      <xsd:any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded" />
    </xsd:sequence>
  </xsd:group>

  <xsd:complexType name="services">
    <xsd:annotation>
      <xsd:documentation><![CDATA[
        Enclosing element for the definition of all services
      ]]></xsd:documentation>
    </xsd:annotation>
    <xsd:choice maxOccurs="unbounded">
      <xsd:element name="service" type="service" minOccurs="1" />
      <xsd:element name="prototype" type="prototype" minOccurs="0" />
      <xsd:element name="defaults" type="defaults" minOccurs="0" maxOccurs="1" />
      <xsd:element name="instanceof" type="instanceof" minOccurs="0" />
      <xsd:element name="stack" type="stack" minOccurs="0" />
    </xsd:choice>
  </xsd:complexType>

  <xsd:complexType name="imports">
    <xsd:annotation>
      <xsd:documentation><![CDATA[
        Enclosing element for the import elements
      ]]></xsd:documentation>
    </xsd:annotation>
    <xsd:choice minOccurs="1" maxOccurs="unbounded">
      <xsd:element name="import" type="import" />
    </xsd:choice>
  </xsd:complexType>

  <xsd:complexType name="import">
    <xsd:annotation>
      <xsd:documentation><![CDATA[
        Import an external resource defining other services or parameters
      ]]></xsd:documentation>
    </xsd:annotation>
    <xsd:attribute name="resource" type="xsd:string" use="required" />
    <xsd:attribute name="ignore-errors" type="ignore_errors" />
    <xsd:attribute name="type" type="xsd:string" />
  </xsd:complexType>

  <xsd:complexType name="callable">
    <xsd:choice minOccurs="0" maxOccurs="1">
      <xsd:element name="service" type="service" minOccurs="0" maxOccurs="1" />
    </xsd:choice>
    <xsd:attribute name="service" type="xsd:string" />
    <xsd:attribute name="class" type="xsd:string" />
    <xsd:attribute name="method" type="xsd:string" />
    <xsd:attribute name="function" type="xsd:string" />
  </xsd:complexType>

  <xsd:complexType name="factory">
    <xsd:choice minOccurs="0" maxOccurs="1">
      <xsd:element name="service" type="service" minOccurs="0" maxOccurs="1" />
    </xsd:choice>
    <xsd:attribute name="service" type="xsd:string" />
    <xsd:attribute name="class" type="xsd:string" />
    <xsd:attribute name="method" type="xsd:string" />
    <xsd:attribute name="function" type="xsd:string" />
    <xsd:attribute name="expression" type="xsd:string" />
  </xsd:complexType>

  <xsd:complexType name="defaults">
    <xsd:annotation>
      <xsd:documentation><![CDATA[
        Enclosing element for the service definitions' defaults for the current file
      ]]></xsd:documentation>
    </xsd:annotation>
    <xsd:choice maxOccurs="unbounded">
      <xsd:element name="tag" type="tag" minOccurs="0" maxOccurs="unbounded" />
      <xsd:element name="bind" type="bind" minOccurs="0" maxOccurs="unbounded" />
    </xsd:choice>
    <xsd:attribute name="public" type="boolean" />
    <xsd:attribute name="autowire" type="boolean" />
    <xsd:attribute name="autoconfigure" type="boolean" />
  </xsd:complexType>

  <xsd:complexType name="service">
    <xsd:choice maxOccurs="unbounded">
      <xsd:element name="file" type="xsd:string" minOccurs="0" maxOccurs="1" />
      <xsd:element name="argument" type="argument" minOccurs="0" maxOccurs="unbounded" />
      <xsd:element name="configurator" type="callable" minOccurs="0" maxOccurs="1" />
      <xsd:element name="factory" type="factory" minOccurs="0" maxOccurs="1" />
      <xsd:element name="from-callable" type="factory" minOccurs="0" maxOccurs="1" />
      <xsd:element name="deprecated" type="deprecated" minOccurs="0" maxOccurs="1" />
      <xsd:element name="call" type="call" minOccurs="0" maxOccurs="unbounded" />
      <xsd:element name="tag" type="tag" minOccurs="0" maxOccurs="unbounded" />
      <xsd:element name="property" type="property" minOccurs="0" maxOccurs="unbounded" />
      <xsd:element name="bind" type="bind" minOccurs="0" maxOccurs="unbounded" />
    </xsd:choice>
    <xsd:attribute name="id" type="xsd:string" />
    <xsd:attribute name="class" type="xsd:string" />
    <xsd:attribute name="shared" type="boolean" />
    <xsd:attribute name="public" type="boolean" />
    <xsd:attribute name="synthetic" type="boolean" />
    <xsd:attribute name="lazy" type="xsd:string" />
    <xsd:attribute name="abstract" type="boolean" />
    <xsd:attribute name="alias" type="xsd:string" />
    <xsd:attribute name="parent" type="xsd:string" />
    <xsd:attribute name="decorates" type="xsd:string" />
    <xsd:attribute name="decoration-on-invalid" type="invalid_decorated_service_sequence" />
    <xsd:attribute name="decoration-inner-name" type="xsd:string" />
    <xsd:attribute name="decoration-priority" type="xsd:integer" />
    <xsd:attribute name="autowire" type="boolean" />
    <xsd:attribute name="autoconfigure" type="boolean" />
    <xsd:attribute name="constructor" type="xsd:string" />
  </xsd:complexType>

  <xsd:complexType name="instanceof">
    <xsd:choice maxOccurs="unbounded">
      <xsd:element name="configurator" type="callable" minOccurs="0" maxOccurs="1" />
      <xsd:element name="call" type="call" minOccurs="0" maxOccurs="unbounded" />
      <xsd:element name="tag" type="tag" minOccurs="0" maxOccurs="unbounded" />
      <xsd:element name="property" type="property" minOccurs="0" maxOccurs="unbounded" />
      <xsd:element name="bind" type="bind" minOccurs="0" maxOccurs="unbounded" />
    </xsd:choice>
    <xsd:attribute name="id" type="xsd:string" use="required" />
    <xsd:attribute name="shared" type="boolean" />
    <xsd:attribute name="public" type="boolean" />
    <xsd:attribute name="lazy" type="xsd:string" />
    <xsd:attribute name="autowire" type="boolean" />
    <xsd:attribute name="autoconfigure" type="boolean" />
    <xsd:attribute name="constructor" type="xsd:string" />
  </xsd:complexType>

  <xsd:complexType name="prototype">
    <xsd:choice maxOccurs="unbounded">
      <xsd:element name="argument" type="argument" minOccurs="0" maxOccurs="unbounded" />
      <xsd:element name="configurator" type="callable" minOccurs="0" maxOccurs="1" />
      <xsd:element name="factory" type="factory" minOccurs="0" maxOccurs="1" />
      <xsd:element name="deprecated" type="deprecated" minOccurs="0" maxOccurs="1" />
      <xsd:element name="call" type="call" minOccurs="0" maxOccurs="unbounded" />
      <xsd:element name="tag" type="tag" minOccurs="0" maxOccurs="unbounded" />
      <xsd:element name="property" type="property" minOccurs="0" maxOccurs="unbounded" />
      <xsd:element name="bind" type="bind" minOccurs="0" maxOccurs="unbounded" />
      <xsd:element name="exclude" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
    </xsd:choice>
    <xsd:attribute name="namespace" type="xsd:string" use="required" />
    <xsd:attribute name="resource" type="xsd:string" use="required" />
    <xsd:attribute name="exclude" type="xsd:string" />
    <xsd:attribute name="shared" type="boolean" />
    <xsd:attribute name="public" type="boolean" />
    <xsd:attribute name="lazy" type="xsd:string" />
    <xsd:attribute name="abstract" type="boolean" />
    <xsd:attribute name="parent" type="xsd:string" />
    <xsd:attribute name="autowire" type="boolean" />
    <xsd:attribute name="autoconfigure" type="boolean" />
    <xsd:attribute name="constructor" type="xsd:string" />
  </xsd:complexType>

  <xsd:complexType name="stack">
    <xsd:choice maxOccurs="unbounded">
      <xsd:element name="service" type="service" minOccurs="1" />
      <xsd:element name="deprecated" type="deprecated" minOccurs="0" maxOccurs="1" />
    </xsd:choice>
    <xsd:attribute name="id" type="xsd:string" use="required" />
    <xsd:attribute name="public" type="boolean" />
  </xsd:complexType>

  <xsd:complexType name="tag" mixed="true">
    <xsd:choice minOccurs="0">
      <xsd:element name="attribute" type="tag_attribute" maxOccurs="unbounded"/>
    </xsd:choice>
    <xsd:anyAttribute namespace="##any" processContents="lax" />
  </xsd:complexType>

  <xsd:complexType name="deprecated">
    <xsd:simpleContent>
      <xsd:extension base="xsd:string">
        <xsd:attribute name="package" type="xsd:string" use="required" />
        <xsd:attribute name="version" type="xsd:string" use="required" />
      </xsd:extension>
    </xsd:simpleContent>
  </xsd:complexType>

  <xsd:complexType name="tag_attribute" mixed="true">
    <xsd:choice minOccurs="0">
      <xsd:element name="attribute" type="tag_attribute" maxOccurs="unbounded" />
    </xsd:choice>
    <xsd:attribute name="name" type="xsd:string" use="required" />
  </xsd:complexType>

  <xsd:complexType name="parameters">
    <xsd:choice minOccurs="1" maxOccurs="unbounded">
      <xsd:element name="parameter" type="parameter" />
    </xsd:choice>
    <xsd:attribute name="type" type="parameter_type" />
    <xsd:attribute name="key" type="xsd:string" />
  </xsd:complexType>

  <xsd:complexType name="parameter" mixed="true">
    <xsd:choice minOccurs="0" maxOccurs="unbounded">
      <xsd:element name="parameter" type="parameter" />
    </xsd:choice>
    <xsd:attribute name="type" type="parameter_type" />
    <xsd:attribute name="id" type="xsd:string" />
    <xsd:attribute name="key" type="xsd:string" />
    <xsd:attribute name="on-invalid" type="invalid_sequence" />
    <xsd:attribute name="trim" type="xsd:boolean" />
  </xsd:complexType>

  <xsd:complexType name="property" mixed="true">
    <xsd:choice minOccurs="0">
      <xsd:element name="property" type="property" maxOccurs="unbounded" />
      <xsd:element name="service" type="service" />
    </xsd:choice>
    <xsd:attribute name="type" type="argument_type" />
    <xsd:attribute name="id" type="xsd:string" />
    <xsd:attribute name="key" type="xsd:string" />
    <xsd:attribute name="name" type="xsd:string" />
    <xsd:attribute name="on-invalid" type="invalid_sequence" />
    <xsd:attribute name="tag" type="xsd:string" />
  </xsd:complexType>

  <xsd:complexType name="bind" mixed="true">
    <xsd:choice maxOccurs="unbounded">
      <xsd:element name="bind" type="bind_argument" minOccurs="0" maxOccurs="unbounded" />
      <xsd:element name="service" type="service" />
    </xsd:choice>
    <xsd:attribute name="type" type="argument_type" />
    <xsd:attribute name="id" type="xsd:string" />
    <xsd:attribute name="key" type="xsd:string" use="required" />
    <xsd:attribute name="on-invalid" type="invalid_sequence" />
    <xsd:attribute name="method" type="xsd:string" />
    <xsd:attribute name="tag" type="xsd:string" />
  </xsd:complexType>

  <xsd:complexType name="bind_argument" mixed="true">
    <xsd:choice minOccurs="0">
      <xsd:element name="bind" type="bind_argument" maxOccurs="unbounded" />
      <xsd:element name="service" type="service" />
      <xsd:element name="exclude" type="xsd:string" maxOccurs="unbounded" />
    </xsd:choice>
    <xsd:attribute name="type" type="argument_type" />
    <xsd:attribute name="id" type="xsd:string" />
    <xsd:attribute name="key" type="xsd:string" />
    <xsd:attribute name="index" type="xsd:integer" />
    <xsd:attribute name="on-invalid" type="invalid_sequence" />
    <xsd:attribute name="tag" type="xsd:string" />
    <xsd:attribute name="index-by" type="xsd:string" />
    <xsd:attribute name="default-index-method" type="xsd:string" />
    <xsd:attribute name="default-priority-method" type="xsd:string" />
    <xsd:attribute name="exclude" type="xsd:string" />
  </xsd:complexType>

  <xsd:complexType name="argument" mixed="true">
    <xsd:choice minOccurs="0">
      <xsd:element name="argument" type="argument" maxOccurs="unbounded" />
      <xsd:element name="service" type="service" />
      <xsd:element name="exclude" type="xsd:string" maxOccurs="unbounded" />
    </xsd:choice>
    <xsd:attribute name="type" type="argument_type" />
    <xsd:attribute name="id" type="xsd:string" />
    <xsd:attribute name="key" type="xsd:string" />
    <xsd:attribute name="index" type="xsd:integer" />
    <xsd:attribute name="on-invalid" type="invalid_sequence" />
    <xsd:attribute name="tag" type="xsd:string" />
    <xsd:attribute name="index-by" type="xsd:string" />
    <xsd:attribute name="default-index-method" type="xsd:string" />
    <xsd:attribute name="default-priority-method" type="xsd:string" />
    <xsd:attribute name="exclude" type="xsd:string" />
    <xsd:attribute name="exclude-self" type="xsd:boolean" />
  </xsd:complexType>

  <xsd:complexType name="call">
    <xsd:choice minOccurs="0">
      <xsd:element name="argument" type="argument" maxOccurs="unbounded" />
    </xsd:choice>
    <xsd:attribute name="method" type="xsd:string" />
    <xsd:attribute name="returns-clone" type="boolean" />
  </xsd:complexType>

  <xsd:simpleType name="parameter_type">
    <xsd:restriction base="xsd:string">
      <xsd:enumeration value="collection" />
      <xsd:enumeration value="string" />
      <xsd:enumeration value="constant" />
      <xsd:enumeration value="binary" />
    </xsd:restriction>
  </xsd:simpleType>

  <xsd:simpleType name="argument_type">
    <xsd:restriction base="xsd:string">
      <xsd:enumeration value="abstract" />
      <xsd:enumeration value="collection" />
      <xsd:enumeration value="service" />
      <xsd:enumeration value="expression" />
      <xsd:enumeration value="string" />
      <xsd:enumeration value="constant" />
      <xsd:enumeration value="binary" />
      <xsd:enumeration value="iterator" />
      <xsd:enumeration value="closure" />
      <xsd:enumeration value="service_closure" />
      <xsd:enumeration value="service_locator" />
      <!-- "tagged" is an alias of "tagged_iterator", using "tagged_iterator" is preferred. -->
      <xsd:enumeration value="tagged" />
      <xsd:enumeration value="tagged_iterator" />
      <xsd:enumeration value="tagged_locator" />
    </xsd:restriction>
  </xsd:simpleType>

  <xsd:simpleType name="ignore_errors">
    <xsd:restriction base="xsd:string">
      <xsd:pattern value="(true|false|not_found)" />
    </xsd:restriction>
  </xsd:simpleType>

  <xsd:simpleType name="invalid_sequence">
    <xsd:restriction base="xsd:string">
      <xsd:enumeration value="null" />
      <xsd:enumeration value="ignore" />
      <xsd:enumeration value="exception" />
      <xsd:enumeration value="ignore_uninitialized" />
    </xsd:restriction>
  </xsd:simpleType>

  <xsd:simpleType name="invalid_decorated_service_sequence">
    <xsd:restriction base="xsd:string">
      <xsd:enumeration value="null" />
      <xsd:enumeration value="ignore" />
      <xsd:enumeration value="exception" />
    </xsd:restriction>
  </xsd:simpleType>

  <xsd:simpleType name="boolean">
    <xsd:restriction base="xsd:string">
      <xsd:pattern value="(%.+%|true|false)" />
    </xsd:restriction>
  </xsd:simpleType>
</xsd:schema>
DependencyInjection Component
=============================

The DependencyInjection component allows you to standardize and centralize the
way objects are constructed in your application.

Resources
---------

 * [Documentation](https://symfony.com/doc/current/components/dependency_injection.html)
 * [Contributing](https://symfony.com/doc/current/contributing/index.html)
 * [Report issues](https://github.com/symfony/symfony/issues) and
   [send Pull Requests](https://github.com/symfony/symfony/pulls)
   in the [main Symfony repository](https://github.com/symfony/symfony)
CHANGELOG
=========

6.4
---

 * Allow using `#[Target]` with no arguments to state that a parameter must match a named autowiring alias
 * Deprecate `ContainerAwareInterface` and `ContainerAwareTrait`, use dependency injection instead
 * Add `defined` env var processor that returns `true` for defined and neither null nor empty env vars
 * Add `#[AutowireLocator]` and `#[AutowireIterator]` attributes

6.3
---

 * Add options `inline_factories` and `inline_class_loader` to `PhpDumper::dump()`
 * Deprecate `PhpDumper` options `inline_factories_parameter` and `inline_class_loader_parameter`
 * Add `RemoveBuildParametersPass`, which removes parameters starting with a dot during compilation
 * Add support for nesting autowiring-related attributes into `#[Autowire(...)]`
 * Deprecate undefined and numeric keys with `service_locator` config
 * Fail if Target attribute does not exist during compilation
 * Enable deprecating parameters with `ContainerBuilder::deprecateParameter()`
 * Add `#[AsAlias]` attribute to tell under which alias a service should be registered or to use the implemented interface if no parameter is given
 * Allow to trim XML service parameters value by using `trim="true"` attribute
 * Allow extending the `Autowire` attribute
 * Add `#[Exclude]` to skip autoregistering a class
 * Add support for generating lazy closures
 * Add support for autowiring services as closures using `#[AutowireCallable]` or `#[AutowireServiceClosure]`
 * Add support for `#[Autowire(lazy: true|class-string)]`
 * Make it possible to cast callables into single-method interfaces
 * Deprecate `#[MapDecorated]`, use `#[AutowireDecorated]` instead
 * Deprecate the `@required` annotation, use the `Symfony\Contracts\Service\Attribute\Required` attribute instead
 * Add `constructor` option to services declaration and to `#[Autoconfigure]`

6.2
---

 * Use lazy-loading ghost objects and virtual proxies out of the box
 * Add arguments `&$asGhostObject` and `$id` to LazyProxy's `DumperInterface` to allow using ghost objects for lazy loading services
 * Add `enum` env var processor
 * Add `shuffle` env var processor
 * Allow #[When] to be extended
 * Change the signature of `ContainerAwareInterface::setContainer()` to `setContainer(?ContainerInterface)`
 * Deprecate calling `ContainerAwareTrait::setContainer()` without arguments
 * Deprecate using numeric parameter names
 * Add support for tagged iterators/locators `exclude` option to the xml and yaml loaders/dumpers
 * Allow injecting `string $env` into php config closures
 * Add `excludeSelf` parameter to `TaggedIteratorArgument` with default value to `true`
   to control whether the referencing service should be automatically excluded from the iterator

6.1
---

 * Add `#[MapDecorated]` attribute telling to which parameter the decorated service should be mapped in a decorator
 * Add `#[AsDecorator]` attribute to make a service decorates another
 * Add `$exclude` to `TaggedIterator` and `TaggedLocator` attributes
 * Add `$exclude` to `tagged_iterator` and `tagged_locator` configurator
 * Add an `env` function to the expression language provider
 * Add an `Autowire` attribute to tell a parameter how to be autowired
 * Allow using expressions as service factories
 * Add argument type `closure` to help passing closures to services
 * Deprecate `ReferenceSetArgumentTrait`
 * Add `AbstractExtension` class for DI configuration/definition on a single file

6.0
---

 * Remove `Definition::setPrivate()` and `Alias::setPrivate()`, use `setPublic()` instead
 * Remove `inline()` in favor of `inline_service()` and `ref()` in favor of `service()` when using the PHP-DSL
 * Remove `Definition::getDeprecationMessage()`, use `Definition::getDeprecation()` instead
 * Remove `Alias::getDeprecationMessage()`, use `Alias::getDeprecation()` instead
 * Remove the `Psr\Container\ContainerInterface` and `Symfony\Component\DependencyInjection\ContainerInterface` aliases of the `service_container` service

5.4
---
 * Add `$defaultIndexMethod` and `$defaultPriorityMethod` to `TaggedIterator` and `TaggedLocator` attributes
 * Add `service_closure()` to the PHP-DSL
 * Add support for autoconfigurable attributes on methods, properties and parameters
 * Make auto-aliases private by default
 * Add support for autowiring union and intersection types

5.3
---

 * Add `ServicesConfigurator::remove()` in the PHP-DSL
 * Add `%env(not:...)%` processor to negate boolean values
 * Add support for loading autoconfiguration rules via the `#[Autoconfigure]` and `#[AutoconfigureTag]` attributes on PHP 8
 * Add `#[AsTaggedItem]` attribute for defining the index and priority of classes found in tagged iterators/locators
 * Add autoconfigurable attributes
 * Add support for autowiring tagged iterators and locators via attributes on PHP 8
 * Add support for per-env configuration in XML and Yaml loaders
 * Add `ContainerBuilder::willBeAvailable()` to help with conditional configuration
 * Add support an integer return value for default_index_method
 * Add `#[When(env: 'foo')]` to skip autoregistering a class when the env doesn't match
 * Add `env()` and `EnvConfigurator` in the PHP-DSL
 * Add support for `ConfigBuilder` in the `PhpFileLoader`
 * Add `ContainerConfigurator::env()` to get the current environment
 * Add `#[Target]` to tell how a dependency is used and hint named autowiring aliases

5.2.0
-----

 * added `param()` and `abstract_arg()` in the PHP-DSL
 * deprecated `Definition::setPrivate()` and `Alias::setPrivate()`, use `setPublic()` instead
 * added support for the `#[Required]` attribute

5.1.0
-----

 * deprecated `inline()` in favor of `inline_service()` and `ref()` in favor of `service()` when using the PHP-DSL
 * allow decorators to reference their decorated service using the special `.inner` id
 * added support to autowire public typed properties in php 7.4
 * added support for defining method calls, a configurator, and property setters in `InlineServiceConfigurator`
 * added possibility to define abstract service arguments
 * allowed mixing "parent" and instanceof-conditionals/defaults/bindings
 * updated the signature of method `Definition::setDeprecated()` to `Definition::setDeprecation(string $package, string $version, string $message)`
 * updated the signature of method `Alias::setDeprecated()` to `Alias::setDeprecation(string $package, string $version, string $message)`
 * updated the signature of method `DeprecateTrait::deprecate()` to `DeprecateTrait::deprecation(string $package, string $version, string $message)`
 * deprecated the `Psr\Container\ContainerInterface` and `Symfony\Component\DependencyInjection\ContainerInterface` aliases of the `service_container` service,
   configure them explicitly instead
 * added class `Symfony\Component\DependencyInjection\Dumper\Preloader` to help with preloading on PHP 7.4+
 * added tags `container.preload`/`.no_preload` to declare extra classes to preload/services to not preload
 * allowed loading and dumping tags with an attribute named "name"
 * deprecated `Definition::getDeprecationMessage()`, use `Definition::getDeprecation()` instead
 * deprecated `Alias::getDeprecationMessage()`, use `Alias::getDeprecation()` instead
 * added support of PHP8 static return type for withers
 * added `AliasDeprecatedPublicServicesPass` to deprecate public services to private

5.0.0
-----

 * removed support for auto-discovered extension configuration class which does not implement `ConfigurationInterface`
 * removed support for non-string default env() parameters
 * moved `ServiceSubscriberInterface` to the `Symfony\Contracts\Service` namespace
 * removed `RepeatedPass` and `RepeatablePassInterface`
 * removed support for short factory/configurator syntax from `YamlFileLoader`
 * removed `ResettableContainerInterface`, use `ResetInterface` instead
 * added argument `$returnsClone` to `Definition::addMethodCall()`
 * removed `tagged`, use `tagged_iterator` instead

4.4.0
-----

 * added `CheckTypeDeclarationsPass` to check injected parameters type during compilation
 * added support for opcache.preload by generating a preloading script in the cache folder
 * added support for dumping the container in one file instead of many files
 * deprecated support for short factories and short configurators in Yaml
 * added `tagged_iterator` alias for `tagged` which might be deprecated in a future version
 * deprecated passing an instance of `Symfony\Component\DependencyInjection\Parameter` as class name to `Symfony\Component\DependencyInjection\Definition`
 * added support for binding iterable and tagged services
 * made singly-implemented interfaces detection be scoped by file
 * added ability to define a static priority method for tagged service
 * added support for improved syntax to define method calls in Yaml
 * made the `%env(base64:...)%` processor able to decode base64url
 * added ability to choose behavior of decorations on non existent decorated services

4.3.0
-----

 * added `%env(trim:...)%` processor to trim a string value
 * added `%env(default:param_name:...)%` processor to fallback to a parameter or to null when using `%env(default::...)%`
 * added `%env(url:...)%` processor to convert a URL or DNS into an array of components
 * added `%env(query_string:...)%` processor to convert a query string into an array of key values
 * added support for deprecating aliases
 * made `ContainerParametersResource` final and not implement `Serializable` anymore
 * added `ReverseContainer`: a container that turns services back to their ids
 * added ability to define an index for a tagged collection
 * added ability to define an index for services in an injected service locator argument
 * made `ServiceLocator` implement `ServiceProviderInterface`
 * deprecated support for non-string default env() parameters
 * added `%env(require:...)%` processor to `require()` a PHP file and use the value returned from it

4.2.0
-----

 * added `ContainerBuilder::registerAliasForArgument()` to support autowiring by type+name
 * added support for binding by type+name
 * added `ServiceSubscriberTrait` to ease implementing `ServiceSubscriberInterface` using methods' return types
 * added `ServiceLocatorArgument` and `!service_locator` config tag for creating optimized service-locators
 * added support for autoconfiguring bindings
 * added `%env(key:...)%` processor to fetch a specific key from an array
 * deprecated `ServiceSubscriberInterface`, use the same interface from the `Symfony\Contracts\Service` namespace instead
 * deprecated `ResettableContainerInterface`, use `Symfony\Contracts\Service\ResetInterface` instead

4.1.0
-----

 * added support for variadics in named arguments
 * added PSR-11 `ContainerBagInterface` and its `ContainerBag` implementation to access parameters as-a-service
 * added support for service's decorators autowiring
 * deprecated the `TypedReference::canBeAutoregistered()` and  `TypedReference::getRequiringClass()` methods
 * environment variables are validated when used in extension configuration
 * deprecated support for auto-discovered extension configuration class which does not implement `ConfigurationInterface`

4.0.0
-----

 * Relying on service auto-registration while autowiring is not supported anymore.
   Explicitly inject your dependencies or create services whose ids are
   their fully-qualified class name.

   Before:

   ```php
   namespace App\Controller;

   use App\Mailer;

   class DefaultController
   {
       public function __construct(Mailer $mailer) {
           // ...
       }

       // ...
   }
   ```
   ```yml
   services:
       App\Controller\DefaultController:
           autowire: true
   ```

   After:

   ```php
   // same PHP code
   ```
   ```yml
   services:
       App\Controller\DefaultController:
           autowire: true

       # or
       # App\Controller\DefaultController:
       #     arguments: { $mailer: "@App\Mailer" }

       App\Mailer:
           autowire: true
    ```
 * removed autowiring services based on the types they implement
 * added a third `$methodName` argument to the `getProxyFactoryCode()` method
   of the `DumperInterface`
 * removed support for autowiring types
 * removed `Container::isFrozen`
 * removed support for dumping an ucompiled container in `PhpDumper`
 * removed support for generating a dumped `Container` without populating the method map
 * removed support for case insensitive service identifiers
 * removed the `DefinitionDecorator` class, replaced by `ChildDefinition`
 * removed the `AutowireServiceResource` class and related `AutowirePass::createResourceForClass()` method
 * removed `LoggingFormatter`, `Compiler::getLoggingFormatter()` and `addLogMessage()` class and methods, use the `ContainerBuilder::log()` method instead
 * removed `FactoryReturnTypePass`
 * removed `ContainerBuilder::addClassResource()`, use the `addObjectResource()` or the `getReflectionClass()` method instead.
 * removed support for top-level anonymous services
 * removed silent behavior for unused attributes and elements
 * removed support for setting and accessing private services in `Container`
 * removed support for setting pre-defined services in `Container`
 * removed support for case insensitivity of parameter names
 * removed `AutowireExceptionPass` and `AutowirePass::getAutowiringExceptions()`, use `Definition::addError()` and the `DefinitionErrorExceptionPass` instead

3.4.0
-----

 * moved the `ExtensionCompilerPass` to before-optimization passes with priority -1000
 * deprecated "public-by-default" definitions and aliases, the new default will be "private" in 4.0
 * added `EnvVarProcessorInterface` and corresponding "container.env_var_processor" tag for processing env vars
 * added support for ignore-on-uninitialized references
 * deprecated service auto-registration while autowiring
 * deprecated the ability to check for the initialization of a private service with the `Container::initialized()` method
 * deprecated support for top-level anonymous services in XML
 * deprecated case insensitivity of parameter names
 * deprecated the `ResolveDefinitionTemplatesPass` class in favor of `ResolveChildDefinitionsPass`
 * added `TaggedIteratorArgument` with YAML (`!tagged foo`) and XML (`<service type="tagged"/>`) support
 * deprecated `AutowireExceptionPass` and `AutowirePass::getAutowiringExceptions()`, use `Definition::addError()` and the `DefinitionErrorExceptionPass` instead

3.3.0
-----

 * deprecated autowiring services based on the types they implement;
   rename (or alias) your services to their FQCN id to make them autowirable
 * added "ServiceSubscriberInterface" - to allow for per-class explicit service-locator definitions
 * added "container.service_locator" tag for defining service-locator services
 * added anonymous services support in YAML configuration files using the `!service` tag.
 * added "TypedReference" and "ServiceClosureArgument" for creating service-locator services
 * added `ServiceLocator` - a PSR-11 container holding a set of services to be lazily loaded
 * added "instanceof" section for local interface-defined configs
 * added prototype services for PSR4-based discovery and registration
 * added `ContainerBuilder::getReflectionClass()` for retrieving and tracking reflection class info
 * deprecated `ContainerBuilder::getClassResource()`, use `ContainerBuilder::getReflectionClass()` or `ContainerBuilder::addObjectResource()` instead
 * added `ContainerBuilder::fileExists()` for checking and tracking file or directory existence
 * deprecated autowiring-types, use aliases instead
 * added support for omitting the factory class name in a service definition if the definition class is set
 * deprecated case insensitivity of service identifiers
 * added "iterator" argument type for lazy iteration over a set of values and services
 * added file-wide configurable defaults for service attributes "public", "tags",
   "autowire" and "autoconfigure"
 * made the "class" attribute optional, using the "id" as fallback
 * using the `PhpDumper` with an uncompiled `ContainerBuilder` is deprecated and
   will not be supported anymore in 4.0
 * deprecated the `DefinitionDecorator` class in favor of `ChildDefinition`
 * allow config files to be loaded using a glob pattern
 * [BC BREAK] the `NullDumper` class is now final

3.2.0
-----

 * allowed to prioritize compiler passes by introducing a third argument to `PassConfig::addPass()`, to `Compiler::addPass` and to `ContainerBuilder::addCompilerPass()`
 * added support for PHP constants in YAML configuration files
 * deprecated the ability to set or unset a private service with the `Container::set()` method
 * deprecated the ability to check for the existence of a private service with the `Container::has()` method
 * deprecated the ability to request a private service with the `Container::get()` method
 * deprecated support for generating a dumped `Container` without populating the method map

3.0.0
-----

 * removed all deprecated codes from 2.x versions

2.8.0
-----

 * deprecated the abstract ContainerAware class in favor of ContainerAwareTrait
 * deprecated IntrospectableContainerInterface, to be merged with ContainerInterface in 3.0
 * allowed specifying a directory to recursively load all configuration files it contains
 * deprecated the concept of scopes
 * added `Definition::setShared()` and `Definition::isShared()`
 * added ResettableContainerInterface to be able to reset the container to release memory on shutdown
 * added a way to define the priority of service decoration
 * added support for service autowiring

2.7.0
-----

 * deprecated synchronized services

2.6.0
-----

 * added new factory syntax and deprecated the old one

2.5.0
-----

 * added DecoratorServicePass and a way to override a service definition (Definition::setDecoratedService())
 * deprecated SimpleXMLElement class.

2.4.0
-----

 * added support for expressions in service definitions
 * added ContainerAwareTrait to add default container aware behavior to a class

2.2.0
-----

 * added Extension::isConfigEnabled() to ease working with enableable configurations
 * added an Extension base class with sensible defaults to be used in conjunction
   with the Config component.
 * added PrependExtensionInterface (to be able to allow extensions to prepend
   application configuration settings for any Bundle)

2.1.0
-----

 * added IntrospectableContainerInterface (to be able to check if a service
   has been initialized or not)
 * added ConfigurationExtensionInterface
 * added Definition::clearTag()
 * component exceptions that inherit base SPL classes are now used exclusively
   (this includes dumped containers)
 * [BC BREAK] fixed unescaping of class arguments, method
   ParameterBag::unescapeValue() was made public
Copyright (c) 2004-present Fabien Potencier

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
{
    "name": "symfony/event-dispatcher",
    "type": "library",
    "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
    "keywords": [],
    "homepage": "https://symfony.com",
    "license": "MIT",
    "authors": [
        {
            "name": "Fabien Potencier",
            "email": "fabien@symfony.com"
        },
        {
            "name": "Symfony Community",
            "homepage": "https://symfony.com/contributors"
        }
    ],
    "require": {
        "php": ">=8.1",
        "symfony/event-dispatcher-contracts": "^2.5|^3"
    },
    "require-dev": {
        "symfony/dependency-injection": "^5.4|^6.0|^7.0",
        "symfony/expression-language": "^5.4|^6.0|^7.0",
        "symfony/config": "^5.4|^6.0|^7.0",
        "symfony/error-handler": "^5.4|^6.0|^7.0",
        "symfony/http-foundation": "^5.4|^6.0|^7.0",
        "symfony/service-contracts": "^2.5|^3",
        "symfony/stopwatch": "^5.4|^6.0|^7.0",
        "psr/log": "^1|^2|^3"
    },
    "conflict": {
        "symfony/dependency-injection": "<5.4",
        "symfony/service-contracts": "<2.5"
    },
    "provide": {
        "psr/event-dispatcher-implementation": "1.0",
        "symfony/event-dispatcher-implementation": "2.0|3.0"
    },
    "autoload": {
        "psr-4": { "Symfony\\Component\\EventDispatcher\\": "" },
        "exclude-from-classmap": [
            "/Tests/"
        ]
    },
    "minimum-stability": "dev"
}
EventDispatcher Component
=========================

The EventDispatcher component provides tools that allow your application
components to communicate with each other by dispatching events and listening to
them.

Resources
---------

 * [Documentation](https://symfony.com/doc/current/components/event_dispatcher.html)
 * [Contributing](https://symfony.com/doc/current/contributing/index.html)
 * [Report issues](https://github.com/symfony/symfony/issues) and
   [send Pull Requests](https://github.com/symfony/symfony/pulls)
   in the [main Symfony repository](https://github.com/symfony/symfony)
CHANGELOG
=========

6.0
---

 * Remove `LegacyEventDispatcherProxy`

5.4
---

 * Allow `#[AsEventListener]` attribute on methods

5.3
---

 * Add `#[AsEventListener]` attribute for declaring listeners on PHP 8

5.1.0
-----

 * The `LegacyEventDispatcherProxy` class has been deprecated.
 * Added an optional `dispatcher` attribute to the listener and subscriber tags in `RegisterListenerPass`.

5.0.0
-----

 * The signature of the `EventDispatcherInterface::dispatch()` method has been changed to `dispatch($event, string $eventName = null): object`.
 * The `Event` class has been removed in favor of `Symfony\Contracts\EventDispatcher\Event`.
 * The `TraceableEventDispatcherInterface` has been removed.
 * The `WrappedListener` class is now final.

4.4.0
-----

 * `AddEventAliasesPass` has been added, allowing applications and bundles to extend the event alias mapping used by `RegisterListenersPass`.
 * Made the `event` attribute of the `kernel.event_listener` tag optional for FQCN events.

4.3.0
-----

 * The signature of the `EventDispatcherInterface::dispatch()` method should be updated to `dispatch($event, string $eventName = null)`, not doing so is deprecated
 * deprecated the `Event` class, use `Symfony\Contracts\EventDispatcher\Event` instead

4.1.0
-----

 * added support for invokable event listeners tagged with `kernel.event_listener` by default
 * The `TraceableEventDispatcher::getOrphanedEvents()` method has been added.
 * The `TraceableEventDispatcherInterface` has been deprecated.

4.0.0
-----

 * removed the `ContainerAwareEventDispatcher` class
 * added the `reset()` method to the `TraceableEventDispatcherInterface`

3.4.0
-----

 * Implementing `TraceableEventDispatcherInterface` without the `reset()` method has been deprecated.

3.3.0
-----

 * The ContainerAwareEventDispatcher class has been deprecated. Use EventDispatcher with closure factories instead.

3.0.0
-----

 * The method `getListenerPriority($eventName, $listener)` has been added to the
   `EventDispatcherInterface`.
 * The methods `Event::setDispatcher()`, `Event::getDispatcher()`, `Event::setName()`
   and `Event::getName()` have been removed.
   The event dispatcher and the event name are passed to the listener call.

2.5.0
-----

 * added Debug\TraceableEventDispatcher (originally in HttpKernel)
 * changed Debug\TraceableEventDispatcherInterface to extend EventDispatcherInterface
 * added RegisterListenersPass (originally in HttpKernel)

2.1.0
-----

 * added TraceableEventDispatcherInterface
 * added ContainerAwareEventDispatcher
 * added a reference to the EventDispatcher on the Event
 * added a reference to the Event name on the event
 * added fluid interface to the dispatch() method which now returns the Event
   object
 * added GenericEvent event class
 * added the possibility for subscribers to subscribe several times for the
   same event
 * added ImmutableEventDispatcher
Copyright (c) 2004-present Fabien Potencier

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
{
    "name": "symfony/config",
    "type": "library",
    "description": "Helps you find, load, combine, autofill and validate configuration values of any kind",
    "keywords": [],
    "homepage": "https://symfony.com",
    "license": "MIT",
    "authors": [
        {
            "name": "Fabien Potencier",
            "email": "fabien@symfony.com"
        },
        {
            "name": "Symfony Community",
            "homepage": "https://symfony.com/contributors"
        }
    ],
    "require": {
        "php": ">=8.1",
        "symfony/deprecation-contracts": "^2.5|^3",
        "symfony/filesystem": "^5.4|^6.0|^7.0",
        "symfony/polyfill-ctype": "~1.8"
    },
    "require-dev": {
        "symfony/event-dispatcher": "^5.4|^6.0|^7.0",
        "symfony/finder": "^5.4|^6.0|^7.0",
        "symfony/messenger": "^5.4|^6.0|^7.0",
        "symfony/service-contracts": "^2.5|^3",
        "symfony/yaml": "^5.4|^6.0|^7.0"
    },
    "conflict": {
        "symfony/finder": "<5.4",
        "symfony/service-contracts": "<2.5"
    },
    "autoload": {
        "psr-4": { "Symfony\\Component\\Config\\": "" },
        "exclude-from-classmap": [
            "/Tests/"
        ]
    },
    "minimum-stability": "dev"
}
Config Component
================

The Config component helps find, load, combine, autofill and validate
configuration values of any kind, whatever their source may be (YAML, XML, INI
files, or for instance a database).

Resources
---------

 * [Documentation](https://symfony.com/doc/current/components/config.html)
 * [Contributing](https://symfony.com/doc/current/contributing/index.html)
 * [Report issues](https://github.com/symfony/symfony/issues) and
   [send Pull Requests](https://github.com/symfony/symfony/pulls)
   in the [main Symfony repository](https://github.com/symfony/symfony)
CHANGELOG
=========

6.3
---

 * Allow enum values in `EnumNode`

6.2
---

 * Deprecate calling `NodeBuilder::setParent()` without any arguments
 * Add a more accurate typehint in generated PHP config

6.1
---

 * Allow using environment variables in `EnumNode`
 * Add Node's information in generated Config
 * Add `DefinitionFileLoader` class to load a TreeBuilder definition from an external file
 * Add `DefinitionConfigurator` helper

6.0
---

 * Remove `BaseNode::getDeprecationMessage()`

5.3.0
-----

 * Add support for generating `ConfigBuilder` for extensions

5.1.0
-----

 * updated the signature of method `NodeDefinition::setDeprecated()` to `NodeDefinition::setDeprecation(string $package, string $version, string $message)`
 * updated the signature of method `BaseNode::setDeprecated()` to `BaseNode::setDeprecation(string $package, string $version, string $message)`
 * deprecated passing a null message to `BaseNode::setDeprecated()` to un-deprecate a node
 * deprecated `BaseNode::getDeprecationMessage()`, use `BaseNode::getDeprecation()` instead

5.0.0
-----

 * Dropped support for constructing a `TreeBuilder` without passing root node information.
 * Removed the `root()` method in `TreeBuilder`, pass the root node information to the constructor instead
 * Added method `getChildNodeDefinitions()` to ParentNodeDefinitionInterface
 * Removed `FileLoaderLoadException`, use `LoaderLoadException` instead

4.4.0
-----

 * added a way to exclude patterns of resources from being imported by the `import()` method

4.3.0
-----

 * deprecated using environment variables with `cannotBeEmpty()` if the value is validated with `validate()`
 * made `Resource\*` classes final and not implement `Serializable` anymore
 * deprecated the `root()` method in `TreeBuilder`, pass the root node information to the constructor instead

4.2.0
-----

 * deprecated constructing a `TreeBuilder` without passing root node information
 * renamed `FileLoaderLoadException` to `LoaderLoadException`

4.1.0
-----

 * added `setPathSeparator` method to `NodeBuilder` class
 * added third `$pathSeparator` constructor argument to `BaseNode`
 * the `Processor` class has been made final

4.0.0
-----

 * removed `ConfigCachePass`

3.4.0
-----

 * added `setDeprecated()` method to indicate a deprecated node
 * added `XmlUtils::parse()` method to parse an XML string
 * deprecated `ConfigCachePass`

3.3.0
-----

 * added `ReflectionClassResource` class
 * added second `$exists` constructor argument to `ClassExistenceResource`
 * made `ClassExistenceResource` work with interfaces and traits
 * added `ConfigCachePass` (originally in FrameworkBundle)
 * added `castToArray()` helper to turn any config value into an array

3.0.0
-----

 * removed `ReferenceDumper` class
 * removed the `ResourceInterface::isFresh()` method
 * removed `BCResourceInterfaceChecker` class
 * removed `ResourceInterface::getResource()` method

2.8.0
-----

The edge case of defining just one value for nodes of type Enum is now allowed:

```php
$rootNode
    ->children()
        ->enumNode('variable')
            ->values(['value'])
        ->end()
    ->end()
;
```

Before: `InvalidArgumentException` (variable must contain at least two
distinct elements).
After: the code will work as expected and it will restrict the values of the
`variable` option to just `value`.

 * deprecated the `ResourceInterface::isFresh()` method. If you implement custom resource types and they
   can be validated that way, make them implement the new `SelfCheckingResourceInterface`.
 * deprecated the getResource() method in ResourceInterface. You can still call this method
   on concrete classes implementing the interface, but it does not make sense at the interface
   level as you need to know about the particular type of resource at hand to understand the
   semantics of the returned value.

2.7.0
-----

 * added `ConfigCacheInterface`, `ConfigCacheFactoryInterface` and a basic `ConfigCacheFactory`
   implementation to delegate creation of ConfigCache instances

2.2.0
-----

 * added `ArrayNodeDefinition::canBeEnabled()` and `ArrayNodeDefinition::canBeDisabled()`
   to ease configuration when some sections are respectively disabled / enabled
   by default.
 * added a `normalizeKeys()` method for array nodes (to avoid key normalization)
 * added numerical type handling for config definitions
 * added convenience methods for optional configuration sections to `ArrayNodeDefinition`
 * added a utils class for XML manipulations

2.1.0
-----

 * added a way to add documentation on configuration
 * implemented `Serializable` on resources
 * `LoaderResolverInterface` is now used instead of `LoaderResolver` for type
   hinting
Copyright (c) 2015-present Fabien Potencier

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
{
    "name": "symfony/polyfill-intl-normalizer",
    "type": "library",
    "description": "Symfony polyfill for intl's Normalizer class and related functions",
    "keywords": ["polyfill", "shim", "compatibility", "portable", "intl", "normalizer"],
    "homepage": "https://symfony.com",
    "license": "MIT",
    "authors": [
        {
            "name": "Nicolas Grekas",
            "email": "p@tchwork.com"
        },
        {
            "name": "Symfony Community",
            "homepage": "https://symfony.com/contributors"
        }
    ],
    "require": {
        "php": ">=7.2"
    },
    "autoload": {
        "psr-4": { "Symfony\\Polyfill\\Intl\\Normalizer\\": "" },
        "files": [ "bootstrap.php" ],
        "classmap": [ "Resources/stubs" ]
    },
    "suggest": {
        "ext-intl": "For best performance"
    },
    "minimum-stability": "dev",
    "extra": {
        "thanks": {
            "name": "symfony/polyfill",
            "url": "https://github.com/symfony/polyfill"
        }
    }
}
Symfony Polyfill / Intl: Normalizer
===================================

This component provides a fallback implementation for the
[`Normalizer`](https://php.net/Normalizer) class provided
by the [Intl](https://php.net/intl) extension.

More information can be found in the
[main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md).

License
=======

This library is released under the [MIT license](LICENSE).
Copyright (c) 2018-present Fabien Potencier

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
{
    "name": "symfony/var-exporter",
    "type": "library",
    "description": "Allows exporting any serializable PHP data structure to plain PHP code",
    "keywords": ["export", "serialize", "instantiate", "hydrate", "construct", "clone", "lazy-loading", "proxy"],
    "homepage": "https://symfony.com",
    "license": "MIT",
    "authors": [
        {
            "name": "Nicolas Grekas",
            "email": "p@tchwork.com"
        },
        {
            "name": "Symfony Community",
            "homepage": "https://symfony.com/contributors"
        }
    ],
    "require": {
        "php": ">=8.2",
        "symfony/deprecation-contracts": "^2.5|^3"
    },
    "require-dev": {
        "symfony/property-access": "^6.4|^7.0|^8.0",
        "symfony/serializer": "^6.4|^7.0|^8.0",
        "symfony/var-dumper": "^6.4|^7.0|^8.0"
    },
    "autoload": {
        "psr-4": { "Symfony\\Component\\VarExporter\\": "" },
        "exclude-from-classmap": [
            "/Tests/"
        ]
    },
    "minimum-stability": "dev"
}
VarExporter Component
=====================

The VarExporter component provides various tools to deal with the internal state
of objects:

- `VarExporter::export()` allows exporting any serializable PHP data structure to
  plain PHP code. While doing so, it preserves all the semantics associated with
  the serialization mechanism of PHP (`__wakeup`, `__sleep`, `Serializable`,
  `__serialize`, `__unserialize`);
- `Instantiator::instantiate()` creates an object and sets its properties without
  calling its constructor nor any other methods;
- `Hydrator::hydrate()` can set the properties of an existing object;
- `Lazy*Trait` can make a class behave as a lazy-loading ghost or virtual proxy.

VarExporter::export()
---------------------

The reason to use `VarExporter::export()` *vs* `serialize()` or
[igbinary](https://github.com/igbinary/igbinary) is performance: thanks to
OPcache, the resulting code is significantly faster and more memory efficient
than using `unserialize()` or `igbinary_unserialize()`.

Unlike `var_export()`, this works on any serializable PHP value.

It also provides a few improvements over `var_export()`/`serialize()`:

 * the output is PSR-2 compatible;
 * the output can be re-indented without messing up with `\r` or `\n` in the data;
 * missing classes throw a `ClassNotFoundException` instead of being unserialized
   to `PHP_Incomplete_Class` objects;
 * references involving `SplObjectStorage`, `ArrayObject` or `ArrayIterator`
   instances are preserved;
 * `Reflection*`, `IteratorIterator` and `RecursiveIteratorIterator` classes
   throw an exception when being serialized (their unserialized version is broken
   anyway, see https://bugs.php.net/76737).

Instantiator and Hydrator
-------------------------

`Instantiator::instantiate($class)` creates an object of the given class without
calling its constructor nor any other methods.

`Hydrator::hydrate()` sets the properties of an existing object, including
private and protected ones. For example:

```php
// Sets the public or protected $object->propertyName property
Hydrator::hydrate($object, ['propertyName' => $propertyValue]);

// Sets a private property defined on its parent Bar class:
Hydrator::hydrate($object, ["\0Bar\0privateBarProperty" => $propertyValue]);

// Alternative way to set the private $object->privateBarProperty property
Hydrator::hydrate($object, [], [
    Bar::class => ['privateBarProperty' => $propertyValue],
]);
```

Lazy Proxies
------------

Since version 8.4, PHP provides support for lazy objects via the reflection API.
This native API works with concrete classes. It doesn't with abstracts nor with
internal ones.

This components provides helpers to generate lazy objects using the decorator
pattern, which works with abstract or internal classes and with interfaces:

```php
$proxyCode = ProxyHelper::generateLazyProxy(new ReflectionClass(AbstractFoo::class));
// $proxyCode should be dumped into a file in production envs
eval('class FooLazyProxy'.$proxyCode);

$foo = FooLazyProxy::createLazyProxy(initializer: function (): AbstractFoo {
    // [...] Use whatever heavy logic you need here
    // to compute the $dependencies of the $instance
    $instance = new Foo(...$dependencies);
    // [...] Call setters, etc. if needed

    return $instance;
});
// $foo is now a lazy-loading decorator object. The initializer will
// be called only when and if a *method* is called.
```

In addition, this component provides traits and methods to aid in implementing
the ghost and proxy strategies in previous versions of PHP. Those are deprecated
when using PHP 8.4.

Resources
---------

 * [Documentation](https://symfony.com/doc/current/components/var_exporter.html)
 * [Contributing](https://symfony.com/doc/current/contributing/index.html)
 * [Report issues](https://github.com/symfony/symfony/issues) and
   [send Pull Requests](https://github.com/symfony/symfony/pulls)
   in the [main Symfony repository](https://github.com/symfony/symfony)
CHANGELOG
=========

7.4
---

 * Add support for exporting named closures

7.3
---

 * Deprecate using `ProxyHelper::generateLazyProxy()` when native lazy proxies can be used - the method should be used to generate abstraction-based lazy decorators only
 * Deprecate `LazyGhostTrait` and `LazyProxyTrait`, use native lazy objects instead
 * Deprecate `ProxyHelper::generateLazyGhost()`, use native lazy objects instead

7.2
---

 * Allow reinitializing lazy objects with a new initializer

6.4
---

 * Deprecate per-property lazy-initializers

6.2
---

 * Add support for lazy ghost objects and virtual proxies
 * Add `Hydrator::hydrate()`
 * Preserve PHP references also when using `Hydrator::hydrate()` or `Instantiator::instantiate()`
 * Add support for hydrating from native (array) casts

5.1.0
-----

 * added argument `array &$foundClasses` to `VarExporter::export()` to ease with preloading exported values

4.2.0
-----

 * added the component
Copyright (c) 2019-present Fabien Potencier

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
{
    "name": "symfony/string",
    "type": "library",
    "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way",
    "keywords": ["string", "utf8", "utf-8", "grapheme", "i18n", "unicode"],
    "homepage": "https://symfony.com",
    "license": "MIT",
    "authors": [
        {
            "name": "Nicolas Grekas",
            "email": "p@tchwork.com"
        },
        {
            "name": "Symfony Community",
            "homepage": "https://symfony.com/contributors"
        }
    ],
    "require": {
        "php": ">=8.2",
        "symfony/deprecation-contracts": "^2.5|^3.0",
        "symfony/polyfill-ctype": "~1.8",
        "symfony/polyfill-intl-grapheme": "~1.33",
        "symfony/polyfill-intl-normalizer": "~1.0",
        "symfony/polyfill-mbstring": "~1.0"
    },
    "require-dev": {
        "symfony/emoji": "^7.1|^8.0",
        "symfony/http-client": "^6.4|^7.0|^8.0",
        "symfony/intl": "^6.4|^7.0|^8.0",
        "symfony/translation-contracts": "^2.5|^3.0",
        "symfony/var-exporter": "^6.4|^7.0|^8.0"
    },
    "conflict": {
        "symfony/translation-contracts": "<2.5"
    },
    "autoload": {
        "psr-4": { "Symfony\\Component\\String\\": "" },
        "files": [ "Resources/functions.php" ],
        "exclude-from-classmap": [
            "/Tests/"
        ]
    },
    "minimum-stability": "dev"
}
String Component
================

The String component provides an object-oriented API to strings and deals
with bytes, UTF-8 code points and grapheme clusters in a unified way.

Resources
---------

 * [Documentation](https://symfony.com/doc/current/components/string.html)
 * [Contributing](https://symfony.com/doc/current/contributing/index.html)
 * [Report issues](https://github.com/symfony/symfony/issues) and
   [send Pull Requests](https://github.com/symfony/symfony/pulls)
   in the [main Symfony repository](https://github.com/symfony/symfony)
CHANGELOG
=========

7.4
---

 * Deprecate implementing `__sleep/wakeup()` on string implementations

7.3
---

 * Add the `AbstractString::pascal()` method

7.2
---

 * Add `TruncateMode` enum to handle more truncate methods
 * Add the `AbstractString::kebab()` method

7.1
---

 * Add `localeLower()`, `localeUpper()`, `localeTitle()` methods to `AbstractUnicodeString`

6.2
---

  * Add support for emoji in `AsciiSlugger`

5.4
---

 * Add `trimSuffix()` and `trimPrefix()` methods

5.3
---

 * Made `AsciiSlugger` fallback to parent locale's symbolsMap

5.2.0
-----

 * added a `FrenchInflector` class

5.1.0
-----

 * added the `AbstractString::reverse()` method
 * made `AbstractString::width()` follow POSIX.1-2001
 * added `LazyString` which provides memoizing stringable objects
 * The component is not marked as `@experimental` anymore
 * added the `s()` helper method to get either an `UnicodeString` or `ByteString` instance,
   depending of the input string UTF-8 compliance
 * added `$cut` parameter to `Symfony\Component\String\AbstractString::truncate()`
 * added `AbstractString::containsAny()`
 * allow passing a string of custom characters to `ByteString::fromRandom()`

5.0.0
-----

 * added the component as experimental
Copyright (c) 2004-present Fabien Potencier

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
{
    "name": "symfony/filesystem",
    "type": "library",
    "description": "Provides basic utilities for the filesystem",
    "keywords": [],
    "homepage": "https://symfony.com",
    "license": "MIT",
    "authors": [
        {
            "name": "Fabien Potencier",
            "email": "fabien@symfony.com"
        },
        {
            "name": "Symfony Community",
            "homepage": "https://symfony.com/contributors"
        }
    ],
    "require": {
        "php": ">=8.1",
        "symfony/polyfill-ctype": "~1.8",
        "symfony/polyfill-mbstring": "~1.8"
    },
    "require-dev": {
        "symfony/process": "^5.4|^6.4|^7.0"
    },
    "autoload": {
        "psr-4": { "Symfony\\Component\\Filesystem\\": "" },
        "exclude-from-classmap": [
            "/Tests/"
        ]
    },
    "minimum-stability": "dev"
}
Filesystem Component
====================

The Filesystem component provides basic utilities for the filesystem.

Resources
---------

 * [Documentation](https://symfony.com/doc/current/components/filesystem.html)
 * [Contributing](https://symfony.com/doc/current/contributing/index.html)
 * [Report issues](https://github.com/symfony/symfony/issues) and
   [send Pull Requests](https://github.com/symfony/symfony/pulls)
   in the [main Symfony repository](https://github.com/symfony/symfony)
CHANGELOG
=========

5.4
---

 * Add `Path` class
 * Add `$lock` argument to `Filesystem::appendToFile()`

5.0.0
-----

 * `Filesystem::dumpFile()` and `appendToFile()` don't accept arrays anymore

4.4.0
-----

 * support for passing a `null` value to `Filesystem::isAbsolutePath()` is deprecated and will be removed in 5.0
 * `tempnam()` now accepts a third argument `$suffix`.

4.3.0
-----

 * support for passing arrays to `Filesystem::dumpFile()` is deprecated and will be removed in 5.0
 * support for passing arrays to `Filesystem::appendToFile()` is deprecated and will be removed in 5.0

4.0.0
-----

 * removed `LockHandler`
 * Support for passing relative paths to `Filesystem::makePathRelative()` has been removed.

3.4.0
-----

 * support for passing relative paths to `Filesystem::makePathRelative()` is deprecated and will be removed in 4.0

3.3.0
-----

 * added `appendToFile()` to append contents to existing files

3.2.0
-----

 * added `readlink()` as a platform independent method to read links

3.0.0
-----

 * removed `$mode` argument from `Filesystem::dumpFile()`

2.8.0
-----

 * added tempnam() a stream aware version of PHP's native tempnam()

2.6.0
-----

 * added LockHandler

2.3.12
------

 * deprecated dumpFile() file mode argument.

2.3.0
-----

 * added the dumpFile() method to atomically write files

2.2.0
-----

 * added a delete option for the mirror() method

2.1.0
-----

 * 24eb396 : BC Break : mkdir() function now throws exception in case of failure instead of returning Boolean value
 * created the component
Copyright (c) 2020-present Fabien Potencier

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
{
    "name": "symfony/deprecation-contracts",
    "type": "library",
    "description": "A generic function and convention to trigger deprecation notices",
    "homepage": "https://symfony.com",
    "license": "MIT",
    "authors": [
        {
            "name": "Nicolas Grekas",
            "email": "p@tchwork.com"
        },
        {
            "name": "Symfony Community",
            "homepage": "https://symfony.com/contributors"
        }
    ],
    "require": {
        "php": ">=8.1"
    },
    "autoload": {
        "files": [
            "function.php"
        ]
    },
    "minimum-stability": "dev",
    "extra": {
        "branch-alias": {
            "dev-main": "3.7-dev"
        },
        "thanks": {
            "name": "symfony/contracts",
            "url": "https://github.com/symfony/contracts"
        }
    }
}
Symfony Deprecation Contracts
=============================

A generic function and convention to trigger deprecation notices.

This package provides a single global function named `trigger_deprecation()` that triggers silenced deprecation notices.

By using a custom PHP error handler such as the one provided by the Symfony ErrorHandler component,
the triggered deprecations can be caught and logged for later discovery, both on dev and prod environments.

The function requires at least 3 arguments:
 - the name of the Composer package that is triggering the deprecation
 - the version of the package that introduced the deprecation
 - the message of the deprecation
 - more arguments can be provided: they will be inserted in the message using `printf()` formatting

Example:
```php
trigger_deprecation('symfony/blockchain', '8.9', 'Using "%s" is deprecated, use "%s" instead.', 'bitcoin', 'fabcoin');
```

This will generate the following message:
`Since symfony/blockchain 8.9: Using "bitcoin" is deprecated, use "fabcoin" instead.`

While not recommended, the deprecation notices can be completely ignored by declaring an empty
`function trigger_deprecation() {}` in your application.
CHANGELOG
=========

The changelog is maintained for all Symfony contracts at the following URL:
https://github.com/symfony/contracts/blob/main/CHANGELOG.md
Copyright (c) 2004-present Fabien Potencier

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
{
    "name": "symfony/yaml",
    "type": "library",
    "description": "Loads and dumps YAML files",
    "keywords": [],
    "homepage": "https://symfony.com",
    "license": "MIT",
    "authors": [
        {
            "name": "Fabien Potencier",
            "email": "fabien@symfony.com"
        },
        {
            "name": "Symfony Community",
            "homepage": "https://symfony.com/contributors"
        }
    ],
    "require": {
        "php": ">=8.1",
        "symfony/deprecation-contracts": "^2.5|^3",
        "symfony/polyfill-ctype": "^1.8"
    },
    "require-dev": {
        "symfony/console": "^5.4|^6.0|^7.0"
    },
    "conflict": {
        "symfony/console": "<5.4"
    },
    "autoload": {
        "psr-4": { "Symfony\\Component\\Yaml\\": "" },
        "exclude-from-classmap": [
            "/Tests/"
        ]
    },
    "bin": [
        "Resources/bin/yaml-lint"
    ],
    "minimum-stability": "dev"
}
Yaml Component
==============

The Yaml component loads and dumps YAML files.

Resources
---------

 * [Documentation](https://symfony.com/doc/current/components/yaml.html)
 * [Contributing](https://symfony.com/doc/current/contributing/index.html)
 * [Report issues](https://github.com/symfony/symfony/issues) and
   [send Pull Requests](https://github.com/symfony/symfony/pulls)
   in the [main Symfony repository](https://github.com/symfony/symfony)
CHANGELOG
=========

6.3
---

 * Add support to dump int keys as strings by using the `Yaml::DUMP_NUMERIC_KEY_AS_STRING` flag

6.2
---

 * Add support for `!php/enum` and `!php/enum *->value`
 * Deprecate the `!php/const:` tag in key which will be replaced by the `!php/const` tag (without the colon) since 3.4

6.1
---

 * In cases where it will likely improve readability, strings containing single quotes will be double-quoted

5.4
---

 * Add a `$maxNestingLevel` argument to `Parser::__construct()`, `Yaml::parse()` and `Yaml::parseFile()` to bound recursion depth (default 128)
 * Add a `$maxAliasesForCollections` argument to `Parser::__construct()`, `Yaml::parse()` and `Yaml::parseFile()` to bound alias expansion of collection values (default 128)
 * Add `Yaml::PARSE_EXCEPTION_ON_ALIAS` to reject YAML aliases while parsing untrusted input
 * Add new `lint:yaml dirname --exclude=/dirname/foo.yaml --exclude=/dirname/bar.yaml`
   option to exclude one or more specific files from multiple file list
 * Allow negatable for the parse tags option with `--no-parse-tags`

5.3
---

 * Added `github` format support & autodetection to render errors as annotations
   when running the YAML linter command in a Github Action environment.

5.1.0
-----

 * Added support for parsing numbers prefixed with `0o` as octal numbers.
 * Deprecated support for parsing numbers starting with `0` as octal numbers. They will be parsed as strings as of Symfony 6.0. Prefix numbers with `0o`
   so that they are parsed as octal numbers.

   Before:

   ```yaml
   Yaml::parse('072');
   ```

   After:

   ```yaml
   Yaml::parse('0o72');
   ```

 * Added `yaml-lint` binary.
 * Deprecated using the `!php/object` and `!php/const` tags without a value.

5.0.0
-----

 * Removed support for mappings inside multi-line strings.
 * removed support for implicit STDIN usage in the `lint:yaml` command, use `lint:yaml -` (append a dash) instead to make it explicit.

4.4.0
-----

 * Added support for parsing the inline notation spanning multiple lines.
 * Added support to dump `null` as `~` by using the `Yaml::DUMP_NULL_AS_TILDE` flag.
 * deprecated accepting STDIN implicitly when using the `lint:yaml` command, use `lint:yaml -` (append a dash) instead to make it explicit.

4.3.0
-----

 * Using a mapping inside a multi-line string is deprecated and will throw a `ParseException` in 5.0.

4.2.0
-----

 * added support for multiple files or directories in `LintCommand`

4.0.0
-----

 * The behavior of the non-specific tag `!` is changed and now forces
   non-evaluating your values.
 * complex mappings will throw a `ParseException`
 * support for the comma as a group separator for floats has been dropped, use
   the underscore instead
 * support for the `!!php/object` tag has been dropped, use the `!php/object`
   tag instead
 * duplicate mapping keys throw a `ParseException`
 * non-string mapping keys throw a `ParseException`, use the `Yaml::PARSE_KEYS_AS_STRINGS`
   flag to cast them to strings
 * `%` at the beginning of an unquoted string throw a `ParseException`
 * mappings with a colon (`:`) that is not followed by a whitespace throw a
   `ParseException`
 * the `Dumper::setIndentation()` method has been removed
 * being able to pass boolean options to the `Yaml::parse()`, `Yaml::dump()`,
   `Parser::parse()`, and `Dumper::dump()` methods to configure the behavior of
   the parser and dumper is no longer supported, pass bitmask flags instead
 * the constructor arguments of the `Parser` class have been removed
 * the `Inline` class is internal and no longer part of the BC promise
 * removed support for the `!str` tag, use the `!!str` tag instead
 * added support for tagged scalars.

   ```yml
   Yaml::parse('!foo bar', Yaml::PARSE_CUSTOM_TAGS);
   // returns TaggedValue('foo', 'bar');
   ```

3.4.0
-----

 * added support for parsing YAML files using the `Yaml::parseFile()` or `Parser::parseFile()` method

 * the `Dumper`, `Parser`, and `Yaml` classes are marked as final

 * Deprecated the `!php/object:` tag which will be replaced by the
   `!php/object` tag (without the colon) in 4.0.

 * Deprecated the `!php/const:` tag which will be replaced by the
   `!php/const` tag (without the colon) in 4.0.

 * Support for the `!str` tag is deprecated, use the `!!str` tag instead.

 * Deprecated using the non-specific tag `!` as its behavior will change in 4.0.
   It will force non-evaluating your values in 4.0. Use plain integers or `!!float` instead.

3.3.0
-----

 * Starting an unquoted string with a question mark followed by a space is
   deprecated and will throw a `ParseException` in Symfony 4.0.

 * Deprecated support for implicitly parsing non-string mapping keys as strings.
   Mapping keys that are no strings will lead to a `ParseException` in Symfony
   4.0. Use quotes to opt-in for keys to be parsed as strings.

   Before:

   ```php
   $yaml = <<<YAML
   null: null key
   true: boolean true
   2.0: float key
   YAML;

   Yaml::parse($yaml);
   ```

   After:

   ```php

   $yaml = <<<YAML
   "null": null key
   "true": boolean true
   "2.0": float key
   YAML;

   Yaml::parse($yaml);
   ```

 * Omitted mapping values will be parsed as `null`.

 * Omitting the key of a mapping is deprecated and will throw a `ParseException` in Symfony 4.0.

 * Added support for dumping empty PHP arrays as YAML sequences:

   ```php
   Yaml::dump([], 0, 0, Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE);
   ```

3.2.0
-----

 * Mappings with a colon (`:`) that is not followed by a whitespace are deprecated
   when the mapping key is not quoted and will lead to a `ParseException` in
   Symfony 4.0 (e.g. `foo:bar` must be `foo: bar`).

 * Added support for parsing PHP constants:

   ```php
   Yaml::parse('!php/const:PHP_INT_MAX', Yaml::PARSE_CONSTANT);
   ```

 * Support for silently ignoring duplicate mapping keys in YAML has been
   deprecated and will lead to a `ParseException` in Symfony 4.0.

3.1.0
-----

 * Added support to dump `stdClass` and `ArrayAccess` objects as YAML mappings
   through the `Yaml::DUMP_OBJECT_AS_MAP` flag.

 * Strings that are not UTF-8 encoded will be dumped as base64 encoded binary
   data.

 * Added support for dumping multi line strings as literal blocks.

 * Added support for parsing base64 encoded binary data when they are tagged
   with the `!!binary` tag.

 * Added support for parsing timestamps as `\DateTime` objects:

   ```php
   Yaml::parse('2001-12-15 21:59:43.10 -5', Yaml::PARSE_DATETIME);
   ```

 * `\DateTime` and `\DateTimeImmutable` objects are dumped as YAML timestamps.

 * Deprecated usage of `%` at the beginning of an unquoted string.

 * Added support for customizing the YAML parser behavior through an optional bit field:

   ```php
   Yaml::parse('{ "foo": "bar", "fiz": "cat" }', Yaml::PARSE_EXCEPTION_ON_INVALID_TYPE | Yaml::PARSE_OBJECT | Yaml::PARSE_OBJECT_FOR_MAP);
   ```

 * Added support for customizing the dumped YAML string through an optional bit field:

   ```php
   Yaml::dump(['foo' => new A(), 'bar' => 1], 0, 0, Yaml::DUMP_EXCEPTION_ON_INVALID_TYPE | Yaml::DUMP_OBJECT);
   ```

3.0.0
-----

 * Yaml::parse() now throws an exception when a blackslash is not escaped
   in double-quoted strings

2.8.0
-----

 * Deprecated usage of a colon in an unquoted mapping value
 * Deprecated usage of @, \`, | and > at the beginning of an unquoted string
 * When surrounding strings with double-quotes, you must now escape `\` characters. Not
   escaping those characters (when surrounded by double-quotes) is deprecated.

   Before:

   ```yml
   class: "Foo\Var"
   ```

   After:

   ```yml
   class: "Foo\\Var"
   ```

2.1.0
-----

 * Yaml::parse() does not evaluate loaded files as PHP files by default
   anymore (call Yaml::enablePhpParsing() to get back the old behavior)
#!/usr/bin/env php
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

if ('cli' !== \PHP_SAPI) {
    throw new Exception('This script must be run from the command line.');
}

/**
 * Runs the Yaml lint command.
 *
 * @author Jan Schädlich <jan.schaedlich@sensiolabs.de>
 */

use Symfony\Component\Console\Application;
use Symfony\Component\Yaml\Command\LintCommand;

function includeIfExists(string $file): bool
{
    return file_exists($file) && include $file;
}

if (
    !includeIfExists(__DIR__ . '/../../../../autoload.php') &&
    !includeIfExists(__DIR__ . '/../../vendor/autoload.php') &&
    !includeIfExists(__DIR__ . '/../../../../../../vendor/autoload.php')
) {
    fwrite(STDERR, 'Install dependencies using Composer.'.PHP_EOL);
    exit(1);
}

if (!class_exists(Application::class)) {
    fwrite(STDERR, 'You need the "symfony/console" component in order to run the Yaml linter.'.PHP_EOL);
    exit(1);
}

(new Application())->add($command = new LintCommand())
    ->getApplication()
    ->setDefaultCommand($command->getName(), true)
    ->run()
;
Copyright (c) 2018-present Fabien Potencier

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
{
    "name": "symfony/service-contracts",
    "type": "library",
    "description": "Generic abstractions related to writing services",
    "keywords": ["abstractions", "contracts", "decoupling", "interfaces", "interoperability", "standards"],
    "homepage": "https://symfony.com",
    "license": "MIT",
    "authors": [
        {
            "name": "Nicolas Grekas",
            "email": "p@tchwork.com"
        },
        {
            "name": "Symfony Community",
            "homepage": "https://symfony.com/contributors"
        }
    ],
    "require": {
        "php": ">=8.1",
        "psr/container": "^1.1|^2.0",
        "symfony/deprecation-contracts": "^2.5|^3"
    },
    "conflict": {
        "ext-psr": "<1.1|>=2"
    },
    "autoload": {
        "psr-4": { "Symfony\\Contracts\\Service\\": "" },
        "exclude-from-classmap": [
            "/Test/"
        ]
    },
    "minimum-stability": "dev",
    "extra": {
        "branch-alias": {
            "dev-main": "3.7-dev"
        },
        "thanks": {
            "name": "symfony/contracts",
            "url": "https://github.com/symfony/contracts"
        }
    }
}
Symfony Service Contracts
=========================

A set of abstractions extracted out of the Symfony components.

Can be used to build on semantics that the Symfony components proved useful and
that already have battle tested implementations.

See https://github.com/symfony/contracts/blob/main/README.md for more information.
CHANGELOG
=========

The changelog is maintained for all Symfony contracts at the following URL:
https://github.com/symfony/contracts/blob/main/CHANGELOG.md
Copyright (c) 2018-present Fabien Potencier

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
{
    "name": "symfony/event-dispatcher-contracts",
    "type": "library",
    "description": "Generic abstractions related to dispatching event",
    "keywords": ["abstractions", "contracts", "decoupling", "interfaces", "interoperability", "standards"],
    "homepage": "https://symfony.com",
    "license": "MIT",
    "authors": [
        {
            "name": "Nicolas Grekas",
            "email": "p@tchwork.com"
        },
        {
            "name": "Symfony Community",
            "homepage": "https://symfony.com/contributors"
        }
    ],
    "require": {
        "php": ">=8.1",
        "psr/event-dispatcher": "^1"
    },
    "autoload": {
        "psr-4": { "Symfony\\Contracts\\EventDispatcher\\": "" }
    },
    "minimum-stability": "dev",
    "extra": {
        "branch-alias": {
            "dev-main": "3.7-dev"
        },
        "thanks": {
            "name": "symfony/contracts",
            "url": "https://github.com/symfony/contracts"
        }
    }
}
Symfony EventDispatcher Contracts
=================================

A set of abstractions extracted out of the Symfony components.

Can be used to build on semantics that the Symfony components proved useful and
that already have battle tested implementations.

See https://github.com/symfony/contracts/blob/main/README.md for more information.
CHANGELOG
=========

The changelog is maintained for all Symfony contracts at the following URL:
https://github.com/symfony/contracts/blob/main/CHANGELOG.md
Copyright (c) 2015-present Fabien Potencier

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
{
    "name": "symfony/polyfill-mbstring",
    "type": "library",
    "description": "Symfony polyfill for the Mbstring extension",
    "keywords": ["polyfill", "shim", "compatibility", "portable", "mbstring"],
    "homepage": "https://symfony.com",
    "license": "MIT",
    "authors": [
        {
            "name": "Nicolas Grekas",
            "email": "p@tchwork.com"
        },
        {
            "name": "Symfony Community",
            "homepage": "https://symfony.com/contributors"
        }
    ],
    "require": {
        "php": ">=7.2",
        "ext-iconv": "*"
    },
    "provide": {
        "ext-mbstring": "*"
    },
    "autoload": {
        "psr-4": { "Symfony\\Polyfill\\Mbstring\\": "" },
        "files": [ "bootstrap.php" ]
    },
    "suggest": {
        "ext-mbstring": "For best performance"
    },
    "minimum-stability": "dev",
    "extra": {
        "thanks": {
            "name": "symfony/polyfill",
            "url": "https://github.com/symfony/polyfill"
        }
    }
}
Symfony Polyfill / Mbstring
===========================

This component provides a partial, native PHP implementation for the
[Mbstring](https://php.net/mbstring) extension.

More information can be found in the
[main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md).

License
=======

This library is released under the [MIT license](LICENSE).
Copyright (c) 2004-present Fabien Potencier

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
{
    "name": "symfony/finder",
    "type": "library",
    "description": "Finds files and directories via an intuitive fluent interface",
    "keywords": [],
    "homepage": "https://symfony.com",
    "license": "MIT",
    "authors": [
        {
            "name": "Fabien Potencier",
            "email": "fabien@symfony.com"
        },
        {
            "name": "Symfony Community",
            "homepage": "https://symfony.com/contributors"
        }
    ],
    "require": {
        "php": ">=8.1"
    },
    "require-dev": {
        "symfony/filesystem": "^6.0|^7.0"
    },
    "autoload": {
        "psr-4": { "Symfony\\Component\\Finder\\": "" },
        "exclude-from-classmap": [
            "/Tests/"
        ]
    },
    "minimum-stability": "dev"
}
Finder Component
================

The Finder component finds files and directories via an intuitive fluent
interface.

Resources
---------

 * [Documentation](https://symfony.com/doc/current/components/finder.html)
 * [Contributing](https://symfony.com/doc/current/contributing/index.html)
 * [Report issues](https://github.com/symfony/symfony/issues) and
   [send Pull Requests](https://github.com/symfony/symfony/pulls)
   in the [main Symfony repository](https://github.com/symfony/symfony)
CHANGELOG
=========

6.4
---

 * Add early directory pruning to `Finder::filter()`

6.2
---

 * Add `Finder::sortByExtension()` and `Finder::sortBySize()`
 * Add `Finder::sortByCaseInsensitiveName()` to sort by name with case insensitive sorting methods

6.0
---

 * Remove `Comparator::setTarget()` and `Comparator::setOperator()`

5.4.0
-----

 * Deprecate `Comparator::setTarget()` and `Comparator::setOperator()`
 * Add a constructor to `Comparator` that allows setting target and operator
 * Finder's iterator has now `Symfony\Component\Finder\SplFileInfo` inner type specified
 * Add recursive .gitignore files support

5.0.0
-----

 * added `$useNaturalSort` argument to `Finder::sortByName()`

4.3.0
-----

 * added Finder::ignoreVCSIgnored() to ignore files based on rules listed in .gitignore

4.2.0
-----

 * added $useNaturalSort option to Finder::sortByName() method
 * the `Finder::sortByName()` method will have a new `$useNaturalSort`
   argument in version 5.0, not defining it is deprecated
 * added `Finder::reverseSorting()` to reverse the sorting

4.0.0
-----

 * removed `ExceptionInterface`
 * removed `Symfony\Component\Finder\Iterator\FilterIterator`

3.4.0
-----

 * deprecated `Symfony\Component\Finder\Iterator\FilterIterator`
 * added Finder::hasResults() method to check if any results were found

3.3.0
-----

 * added double-star matching to Glob::toRegex()

3.0.0
-----

 * removed deprecated classes

2.8.0
-----

 * deprecated adapters and related classes

2.5.0
-----
 * added support for GLOB_BRACE in the paths passed to Finder::in()

2.3.0
-----

 * added a way to ignore unreadable directories (via Finder::ignoreUnreadableDirs())
 * unified the way subfolders that are not executable are handled by always throwing an AccessDeniedException exception

2.2.0
-----

 * added Finder::path() and Finder::notPath() methods
 * added finder adapters to improve performance on specific platforms
 * added support for wildcard characters (glob patterns) in the paths passed
   to Finder::in()

2.1.0
-----

 * added Finder::sortByAccessedTime(), Finder::sortByChangedTime(), and
   Finder::sortByModifiedTime()
 * added Countable to Finder
 * added support for an array of directories as an argument to
   Finder::exclude()
 * added searching based on the file content via Finder::contains() and
   Finder::notContains()
 * added support for the != operator in the Comparator
 * [BC BREAK] filter expressions (used for file name and content) are no more
   considered as regexps but glob patterns when they are enclosed in '*' or '?'
Copyright (c) 2015-present Fabien Potencier

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
{
    "name": "symfony/polyfill-intl-grapheme",
    "type": "library",
    "description": "Symfony polyfill for intl's grapheme_* functions",
    "keywords": ["polyfill", "shim", "compatibility", "portable", "intl", "grapheme"],
    "homepage": "https://symfony.com",
    "license": "MIT",
    "authors": [
        {
            "name": "Nicolas Grekas",
            "email": "p@tchwork.com"
        },
        {
            "name": "Symfony Community",
            "homepage": "https://symfony.com/contributors"
        }
    ],
    "require": {
        "php": ">=7.2"
    },
    "autoload": {
        "psr-4": { "Symfony\\Polyfill\\Intl\\Grapheme\\": "" },
        "files": [ "bootstrap.php" ]
    },
    "suggest": {
        "ext-intl": "For best performance"
    },
    "minimum-stability": "dev",
    "extra": {
        "thanks": {
            "name": "symfony/polyfill",
            "url": "https://github.com/symfony/polyfill"
        }
    }
}
Symfony Polyfill / Intl: Grapheme
=================================

This component provides a partial, native PHP implementation of the
[Grapheme functions](https://php.net/intl.grapheme) from the
[Intl](https://php.net/intl) extension.

- [`grapheme_extract`](https://php.net/grapheme_extract): Extract a sequence of grapheme
  clusters from a text buffer, which must be encoded in UTF-8
- [`grapheme_stripos`](https://php.net/grapheme_stripos): Find position (in grapheme units)
  of first occurrence of a case-insensitive string
- [`grapheme_stristr`](https://php.net/grapheme_stristr): Returns part of haystack string
  from the first occurrence of case-insensitive needle to the end of haystack
- [`grapheme_strlen`](https://php.net/grapheme_strlen): Get string length in grapheme units
- [`grapheme_strpos`](https://php.net/grapheme_strpos): Find position (in grapheme units)
  of first occurrence of a string
- [`grapheme_strripos`](https://php.net/grapheme_strripos): Find position (in grapheme units)
  of last occurrence of a case-insensitive string
- [`grapheme_strrpos`](https://php.net/grapheme_strrpos): Find position (in grapheme units)
  of last occurrence of a string
- [`grapheme_strstr`](https://php.net/grapheme_strstr): Returns part of haystack string from
  the first occurrence of needle to the end of haystack
- [`grapheme_substr`](https://php.net/grapheme_substr): Return part of a string
- [`grapheme_str_split`](https://php.net/grapheme_str_split): Splits a string into an array of individual or chunks of graphemes

More information can be found in the
[main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md).

License
=======

This library is released under the [MIT license](LICENSE).
Copyright (c) 2004-present Fabien Potencier

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
{
    "name": "symfony/console",
    "type": "library",
    "description": "Eases the creation of beautiful and testable command line interfaces",
    "keywords": ["console", "cli", "command-line", "terminal"],
    "homepage": "https://symfony.com",
    "license": "MIT",
    "authors": [
        {
            "name": "Fabien Potencier",
            "email": "fabien@symfony.com"
        },
        {
            "name": "Symfony Community",
            "homepage": "https://symfony.com/contributors"
        }
    ],
    "require": {
        "php": ">=8.1",
        "symfony/deprecation-contracts": "^2.5|^3",
        "symfony/polyfill-mbstring": "~1.0",
        "symfony/service-contracts": "^2.5|^3",
        "symfony/string": "^5.4|^6.0|^7.0"
    },
    "require-dev": {
        "symfony/config": "^5.4|^6.0|^7.0",
        "symfony/event-dispatcher": "^5.4|^6.0|^7.0",
        "symfony/dependency-injection": "^5.4|^6.0|^7.0",
        "symfony/http-foundation": "^6.4|^7.0",
        "symfony/http-kernel": "^6.4|^7.0",
        "symfony/lock": "^5.4|^6.0|^7.0",
        "symfony/messenger": "^5.4|^6.0|^7.0",
        "symfony/process": "^5.4|^6.0|^7.0",
        "symfony/stopwatch": "^5.4|^6.0|^7.0",
        "symfony/var-dumper": "^5.4|^6.0|^7.0",
        "psr/log": "^1|^2|^3"
    },
    "provide": {
        "psr/log-implementation": "1.0|2.0|3.0"
    },
    "conflict": {
        "symfony/dependency-injection": "<5.4",
        "symfony/dotenv": "<5.4",
        "symfony/event-dispatcher": "<5.4",
        "symfony/lock": "<5.4",
        "symfony/process": "<5.4"
    },
    "autoload": {
        "psr-4": { "Symfony\\Component\\Console\\": "" },
        "exclude-from-classmap": [
            "/Tests/"
        ]
    },
    "minimum-stability": "dev"
}
Console Component
=================

The Console component eases the creation of beautiful and testable command line
interfaces.

Sponsor
-------

The Console component for Symfony 6.4 is [backed][1] by [Les-Tilleuls.coop][2].

Les-Tilleuls.coop is a team of 70+ Symfony experts who can help you design, develop and
fix your projects. They provide a wide range of professional services including development,
consulting, coaching, training and audits. They also are highly skilled in JS, Go and DevOps.
They are a worker cooperative!

Help Symfony by [sponsoring][3] its development!

Resources
---------

 * [Documentation](https://symfony.com/doc/current/components/console.html)
 * [Contributing](https://symfony.com/doc/current/contributing/index.html)
 * [Report issues](https://github.com/symfony/symfony/issues) and
   [send Pull Requests](https://github.com/symfony/symfony/pulls)
   in the [main Symfony repository](https://github.com/symfony/symfony)

Credits
-------

`Resources/bin/hiddeninput.exe` is a third party binary provided within this
component. Find sources and license at https://github.com/Seldaek/hidden-input.

[1]: https://symfony.com/backers
[2]: https://les-tilleuls.coop
[3]: https://symfony.com/sponsor
CHANGELOG
=========

6.4
---

 * Add `SignalMap` to map signal value to its name
 * Multi-line text in vertical tables is aligned properly
 * The application can also catch errors with `Application::setCatchErrors(true)`
 * Add `RunCommandMessage` and `RunCommandMessageHandler`
 * Dispatch `ConsoleTerminateEvent` after an exit on signal handling and add `ConsoleTerminateEvent::getInterruptingSignal()`

6.3
---

 * Add support for choosing exit code while handling signal, or to not exit at all
 * Add `ProgressBar::setPlaceholderFormatter` to set a placeholder attached to a instance, instead of being global.
 * Add `ReStructuredTextDescriptor`

6.2
---

 * Improve truecolor terminal detection in some cases
 * Add support for 256 color terminals (conversion from Ansi24 to Ansi8 if terminal is capable of it)
 * Deprecate calling `*Command::setApplication()`, `*FormatterStyle::setForeground/setBackground()`, `Helper::setHelpSet()`, `Input*::setDefault()`, `Question::setAutocompleterCallback/setValidator()`without any arguments
 * Change the signature of `OutputFormatterStyleInterface::setForeground/setBackground()` to `setForeground/setBackground(?string)`
 * Change the signature of `HelperInterface::setHelperSet()` to `setHelperSet(?HelperSet)`

6.1
---

 * Add support to display table vertically when calling setVertical()
 * Add method `__toString()` to `InputInterface`
 * Added `OutputWrapper` to prevent truncated URL in `SymfonyStyle::createBlock`.
 * Deprecate `Command::$defaultName` and `Command::$defaultDescription`, use the `AsCommand` attribute instead
 * Add suggested values for arguments and options in input definition, for input completion
 * Add `$resumeAt` parameter to `ProgressBar#start()`, so that one can easily 'resume' progress on longer tasks, and still get accurate `getEstimate()` and `getRemaining()` results.

6.0
---

 * `Command::setHidden()` has a default value (`true`) for `$hidden` parameter and is final
 * Remove `Helper::strlen()`, use `Helper::width()` instead
 * Remove `Helper::strlenWithoutDecoration()`, use `Helper::removeDecoration()` instead
 * `AddConsoleCommandPass` can not be configured anymore
 * Remove `HelperSet::setCommand()` and `getCommand()` without replacement

5.4
---

 * Add `TesterTrait::assertCommandIsSuccessful()` to test command
 * Deprecate `HelperSet::setCommand()` and `getCommand()` without replacement

5.3
---

 * Add `GithubActionReporter` to render annotations in a Github Action
 * Add `InputOption::VALUE_NEGATABLE` flag to handle `--foo`/`--no-foo` options
 * Add the `Command::$defaultDescription` static property and the `description` attribute
   on the `console.command` tag to allow the `list` command to instantiate commands lazily
 * Add option `--short` to the `list` command
 * Add support for bright colors
 * Add `#[AsCommand]` attribute for declaring commands on PHP 8
 * Add `Helper::width()` and `Helper::length()`
 * The `--ansi` and `--no-ansi` options now default to `null`.

5.2.0
-----

 * Added `SingleCommandApplication::setAutoExit()` to allow testing via `CommandTester`
 * added support for multiline responses to questions through `Question::setMultiline()`
   and `Question::isMultiline()`
 * Added `SignalRegistry` class to stack signals handlers
 * Added support for signals:
    * Added `Application::getSignalRegistry()` and `Application::setSignalsToDispatchEvent()` methods
    * Added `SignalableCommandInterface` interface
 * Added `TableCellStyle` class to customize table cell
 * Removed `php ` prefix invocation from help messages.

5.1.0
-----

 * `Command::setHidden()` is final since Symfony 5.1
 * Add `SingleCommandApplication`
 * Add `Cursor` class

5.0.0
-----

 * removed support for finding hidden commands using an abbreviation, use the full name instead
 * removed `TableStyle::setCrossingChar()` method in favor of `TableStyle::setDefaultCrossingChar()`
 * removed `TableStyle::setHorizontalBorderChar()` method in favor of `TableStyle::setDefaultCrossingChars()`
 * removed `TableStyle::getHorizontalBorderChar()` method in favor of `TableStyle::getBorderChars()`
 * removed `TableStyle::setVerticalBorderChar()` method in favor of `TableStyle::setVerticalBorderChars()`
 * removed `TableStyle::getVerticalBorderChar()` method in favor of `TableStyle::getBorderChars()`
 * removed support for returning `null` from `Command::execute()`, return `0` instead
 * `ProcessHelper::run()` accepts only `array|Symfony\Component\Process\Process` for its `command` argument
 * `Application::setDispatcher` accepts only `Symfony\Contracts\EventDispatcher\EventDispatcherInterface`
   for its `dispatcher` argument
 * renamed `Application::renderException()` and `Application::doRenderException()`
   to `renderThrowable()` and `doRenderThrowable()` respectively.

4.4.0
-----

 * deprecated finding hidden commands using an abbreviation, use the full name instead
 * added `Question::setTrimmable` default to true to allow the answer to be trimmed
 * added method `minSecondsBetweenRedraws()` and `maxSecondsBetweenRedraws()` on `ProgressBar`
 * `Application` implements `ResetInterface`
 * marked all dispatched event classes as `@final`
 * added support for displaying table horizontally
 * deprecated returning `null` from `Command::execute()`, return `0` instead
 * Deprecated the `Application::renderException()` and `Application::doRenderException()` methods,
   use `renderThrowable()` and `doRenderThrowable()` instead.
 * added support for the `NO_COLOR` env var (https://no-color.org/)

4.3.0
-----

 * added support for hyperlinks
 * added `ProgressBar::iterate()` method that simplify updating the progress bar when iterating
 * added `Question::setAutocompleterCallback()` to provide a callback function
   that dynamically generates suggestions as the user types

4.2.0
-----

 * allowed passing commands as `[$process, 'ENV_VAR' => 'value']` to
   `ProcessHelper::run()` to pass environment variables
 * deprecated passing a command as a string to `ProcessHelper::run()`,
   pass it the command as an array of its arguments instead
 * made the `ProcessHelper` class final
 * added `WrappableOutputFormatterInterface::formatAndWrap()` (implemented in `OutputFormatter`)
 * added `capture_stderr_separately` option to `CommandTester::execute()`

4.1.0
-----

 * added option to run suggested command if command is not found and only 1 alternative is available
 * added option to modify console output and print multiple modifiable sections
 * added support for iterable messages in output `write` and `writeln` methods

4.0.0
-----

 * `OutputFormatter` throws an exception when unknown options are used
 * removed `QuestionHelper::setInputStream()/getInputStream()`
 * removed `Application::getTerminalWidth()/getTerminalHeight()` and
   `Application::setTerminalDimensions()/getTerminalDimensions()`
 * removed `ConsoleExceptionEvent`
 * removed `ConsoleEvents::EXCEPTION`

3.4.0
-----

 * added `SHELL_VERBOSITY` env var to control verbosity
 * added `CommandLoaderInterface`, `FactoryCommandLoader` and PSR-11
   `ContainerCommandLoader` for commands lazy-loading
 * added a case-insensitive command name matching fallback
 * added static `Command::$defaultName/getDefaultName()`, allowing for
   commands to be registered at compile time in the application command loader.
   Setting the `$defaultName` property avoids the need for filling the `command`
   attribute on the `console.command` tag when using `AddConsoleCommandPass`.

3.3.0
-----

 * added `ExceptionListener`
 * added `AddConsoleCommandPass` (originally in FrameworkBundle)
 * [BC BREAK] `Input::getOption()` no longer returns the default value for options
   with value optional explicitly passed empty
 * added console.error event to catch exceptions thrown by other listeners
 * deprecated console.exception event in favor of console.error
 * added ability to handle `CommandNotFoundException` through the
   `console.error` event
 * deprecated default validation in `SymfonyQuestionHelper::ask`

3.2.0
------

 * added `setInputs()` method to CommandTester for ease testing of commands expecting inputs
 * added `setStream()` and `getStream()` methods to Input (implement StreamableInputInterface)
 * added StreamableInputInterface
 * added LockableTrait

3.1.0
-----

 * added truncate method to FormatterHelper
 * added setColumnWidth(s) method to Table

2.8.3
-----

 * remove readline support from the question helper as it caused issues

2.8.0
-----

 * use readline for user input in the question helper when available to allow
   the use of arrow keys

2.6.0
-----

 * added a Process helper
 * added a DebugFormatter helper

2.5.0
-----

 * deprecated the dialog helper (use the question helper instead)
 * deprecated TableHelper in favor of Table
 * deprecated ProgressHelper in favor of ProgressBar
 * added ConsoleLogger
 * added a question helper
 * added a way to set the process name of a command
 * added a way to set a default command instead of `ListCommand`

2.4.0
-----

 * added a way to force terminal dimensions
 * added a convenient method to detect verbosity level
 * [BC BREAK] made descriptors use output instead of returning a string

2.3.0
-----

 * added multiselect support to the select dialog helper
 * added Table Helper for tabular data rendering
 * added support for events in `Application`
 * added a way to normalize EOLs in `ApplicationTester::getDisplay()` and `CommandTester::getDisplay()`
 * added a way to set the progress bar progress via the `setCurrent` method
 * added support for multiple InputOption shortcuts, written as `'-a|-b|-c'`
 * added two additional verbosity levels, VERBOSITY_VERY_VERBOSE and VERBOSITY_DEBUG

2.2.0
-----

 * added support for colorization on Windows via ConEmu
 * add a method to Dialog Helper to ask for a question and hide the response
 * added support for interactive selections in console (DialogHelper::select())
 * added support for autocompletion as you type in Dialog Helper

2.1.0
-----

 * added ConsoleOutputInterface
 * added the possibility to disable a command (Command::isEnabled())
 * added suggestions when a command does not exist
 * added a --raw option to the list command
 * added support for STDERR in the console output class (errors are now sent
   to STDERR)
 * made the defaults (helper set, commands, input definition) in Application
   more easily customizable
 * added support for the shell even if readline is not available
 * added support for process isolation in Symfony shell via
   `--process-isolation` switch
 * added support for `--`, which disables options parsing after that point
   (tokens will be parsed as arguments)
# This file is part of the Symfony package.
#
# (c) Fabien Potencier <fabien@symfony.com>
#
# For the full copyright and license information, please view
# https://symfony.com/doc/current/contributing/code/license.html

_sf_{{ COMMAND_NAME }}() {

    # Use the default completion for shell redirect operators.
    for w in '>' '>>' '&>' '<'; do
        if [[ $w = "${COMP_WORDS[COMP_CWORD-1]}" ]]; then
            compopt -o filenames
            COMPREPLY=($(compgen -f -- "${COMP_WORDS[COMP_CWORD]}"))
            return 0
        fi
    done

    # Use newline as only separator to allow space in completion values
    local IFS=$'\n'
    local sf_cmd="${COMP_WORDS[0]}"

    # for an alias, get the real script behind it
    sf_cmd_type=$(type -t $sf_cmd)
    if [[ $sf_cmd_type == "alias" ]]; then
        sf_cmd=$(alias $sf_cmd | sed -E "s/alias $sf_cmd='(.*)'/\1/")
    elif [[ $sf_cmd_type == "file" ]]; then
        sf_cmd=$(type -p $sf_cmd)
    fi

    if [[ $sf_cmd_type != "function" && ! -x $sf_cmd ]]; then
        return 1
    fi

    local cur prev words cword
    _get_comp_words_by_ref -n := cur prev words cword

    local completecmd=("$sf_cmd" "_complete" "--no-interaction" "-sbash" "-c$cword" "-a{{ VERSION }}")
    for w in ${words[@]}; do
        w="${w//\\\\/\\}"
        # remove quotes from typed values
        quote="${w:0:1}"
        if [ "$quote" == \' ]; then
            w="${w%\'}"
            w="${w#\'}"
        elif [ "$quote" == \" ]; then
            w="${w%\"}"
            w="${w#\"}"
        fi
        # empty values are ignored
        if [ ! -z "$w" ]; then
            completecmd+=("-i$w")
        fi
    done

    local sfcomplete
    if sfcomplete=$(SHELL_VERBOSITY=0 ${completecmd[@]} 2>&1); then
        local quote suggestions
        quote=${cur:0:1}

        # Use single quotes by default if suggestions contains backslash (FQCN)
        if [ "$quote" == '' ] && [[ "$sfcomplete" =~ \\ ]]; then
            quote=\'
        fi

        if [ "$quote" == \' ]; then
            # single quotes: no additional escaping (does not accept ' in values)
            suggestions=$(for s in $sfcomplete; do printf $'%q%q%q\n' "$quote" "$s" "$quote"; done)
        elif [ "$quote" == \" ]; then
            # double quotes: double escaping for \ $ ` "
            suggestions=$(for s in $sfcomplete; do
                s=${s//\\/\\\\}
                s=${s//\$/\\\$}
                s=${s//\`/\\\`}
                s=${s//\"/\\\"}
                printf $'%q%q%q\n' "$quote" "$s" "$quote";
            done)
        else
            # no quotes: double escaping
            suggestions=$(for s in $sfcomplete; do printf $'%q\n' $(printf '%q' "$s"); done)
        fi
        COMPREPLY=($(IFS=$'\n' compgen -W "$suggestions" -- $(printf -- "%q" "$cur")))
        __ltrim_colon_completions "$cur"
    else
        if [[ "$sfcomplete" != *"Command \"_complete\" is not defined."* ]]; then
            >&2 echo
            >&2 echo $sfcomplete
        fi

        return 1
    fi
}

complete -F _sf_{{ COMMAND_NAME }} {{ COMMAND_NAME }}
# This file is part of the Symfony package.
#
# (c) Fabien Potencier <fabien@symfony.com>
#
# For the full copyright and license information, please view
# https://symfony.com/doc/current/contributing/code/license.html

function _sf_{{ COMMAND_NAME }}
    set sf_cmd (commandline -o)
    set c (count (commandline -oc))

    set completecmd "$sf_cmd[1]" "_complete" "--no-interaction" "-sfish" "-a{{ VERSION }}"

    for i in $sf_cmd
        if [ $i != "" ]
            set completecmd $completecmd "-i$i"
        end
    end

    set completecmd $completecmd "-c$c"

    set sfcomplete (env SHELL_VERBOSITY=0 $completecmd)

    for i in $sfcomplete
        echo $i
    end
end

complete -c '{{ COMMAND_NAME }}' -a '(_sf_{{ COMMAND_NAME }})' -f
MZ                @                                       	!L!This program cannot be run in DOS mode.
$       ,;B;B;B2מ:B2-B2ƞ9B2ў?Ba98B;CB2Ȟ:B2֞:B2Ӟ:BRich;B        PE  L MoO         	  
         8           @                      `     ?   @                           "  P    @                      P  p   !                             8!  @                                          .text   	      
                    `.rdata  	       
                 @  @.data      0                    @  .rsrc       @                    @  @.reloc     P      "              @  B                                                                                                                                                                                                                                                                                                                                                        j$@ x  j @ e EPV  @ EЃPV @ MX @ e EP5H @ L @ YY5\ @ EP5` @ D @ YYP @ MMT @ 3H  ; 0@ u  h@   l3@ $40@ 5h3@ 40@ h$0@ h(0@ h 0@  @ 00@ }j  Yjh"@   3ۉ]d   p]俀3@ SVW0 @ ;t;u3Fuh  4 @ 3F|3@ ;u
j\  Y;|3@ u,5|3@ h @ h @   YYtE      5<0@ |3@ ;uh @ h @ l  YY|3@    9]uSW8 @ 93@ th3@   Yt
SjS3@ $0@  @ 5$0@ 5(0@ 5 0@ 80@ 9,0@ u7P @ E	MPQ  YYËeE80@ 39,0@ uPh @ 9<0@ u @ E80@   øMZ  f9  @ t3M< @   @ 8PE  uH  t  uՃ   v39   xtv39   j,0@ p @ jl @ YY3@ 3@  @ t3@  @ p3@  @  x3@ V    =0@  uh@  @ Yg  =0@ u	j @ Y3{  U(  H1@ D1@ @1@ <1@ 581@ =41@ f`1@ fT1@ f01@ f,1@ f%(1@ f-$1@ X1@ E L1@ EP1@ E\1@ 0@   P1@ L0@ @0@ 	 D0@     0@ 0@  @ 0@ j?  Yj   @ h!@ $ @ =0@  uj  Yh	 ( @ P, @ ËUE 8csmu*xu$@= t=!t="t= @u  3] hH@   @ 3% @ jh("@ b  53@ 5 @ YEuu @ Ygj  Ye 53@ ։E53@ YYEEPEPu5l @ YPU  Eu֣3@ uփ3@ E	   E  j  YËUuNYH]ËV!@ !@ W;stЃ;r_^ËV"@ "@ W;stЃ;r_^% @ ̋UMMZ  f9t3]ËA<8PE  u3ҹ  f9H]̋UEH<ASVq3WDv}H;r	X;r
B(;r3_^[]̋UjhH"@ he@ d    PSVW 0@ 1E3PEd    eE    h  @ *tUE-  @ Ph  @ Pt;@$ЃEMd    Y_^[]ËE3=  ËeE3Md    Y_^[]% @ % @ he@ d5    D$l$l$+SVW 0@ 1E3PeuEEEEd    ËMd    Y__^[]QËUuuuuh@ h 0@    ]ËVh   h   3V   tVVVVV   ^3ËU 0@ e e SWN@  ;tt	У0@ `VEP< @ u3u @ 3 @ 3 @ 3EP @ E3E3;uO@u5 0@ ։50@ ^_[%t @ %x @ %| @ % @ % @ % @ % @ % @ % @ Pd5    D$+d$SVW( 0@ 3PEuEEd    ËMd    Y__^[]QËM3M%T @ T$BJ3J3l"@ s                                                                                                                                                                                                                                                     #  #  #  )  r)  b)  H)  4)  )  (  (  (  (  (  (  )      #  $  %  %  &  d&  &  $      ('  '  '  '  '  (  ((  6(  '  H(  Z(  t(  (  '  '   '  '  '  l'  ^'  R'  F'  >'  >(  0'  '  )          @         W@ @                     MoO       l   !    @0@ 0@ bad allocation      H                                                            0@ !@    RSDSьJ!LZ    c:\users\seld\documents\visual studio 2010\Projects\hiddeninp\Release\hiddeninp.pdb     e                            @ @                 :@             @ @ @ "   d"@                        "          #      $#          &  D   H#          (  h                       #  #  #  )  r)  b)  H)  4)  )  (  (  (  (  (  (  )      #  $  %  %  &  d&  &  $      ('  '  '  '  '  (  ((  6(  '  H(  Z(  t(  (  '  '   '  '  '  l'  ^'  R'  F'  >'  >(  0'  '  )      GetConsoleMode  SetConsoleMode  ;GetStdHandle  KERNEL32.dll   ??$?6DU?$char_traits@D@std@@V?$allocator@D@1@@std@@YAAAV?$basic_ostream@DU?$char_traits@D@std@@@0@AAV10@ABV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@0@@Z ?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A  J?cin@std@@3V?$basic_istream@DU?$char_traits@D@std@@@1@A  ??$getline@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@YAAAV?$basic_istream@DU?$char_traits@D@std@@@0@AAV10@AAV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@0@@Z ??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QAEAAV01@P6AAAV01@AAV01@@Z@Z  _??1?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@QAE@XZ  {??0?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@QAE@XZ  ?endl@std@@YAAAV?$basic_ostream@DU?$char_traits@D@std@@@1@AAV21@@Z  MSVCP90.dll _amsg_exit   __getmainargs ,_cexit  |_exit f _XcptFilter exit   __initenv _initterm _initterm_e <_configthreadlocale  __setusermatherr  _adjust_fdiv   __p__commode   __p__fmode  j_encode_pointer  __set_app_type  K_crt_debugger_hook  C ?terminate@@YAXXZ MSVCR90.dll _unlock  __dllonexit v_lock _onexit `_decode_pointer s_except_handler4_common _invoke_watson  ?_controlfp_s  InterlockedExchange !Sleep InterlockedCompareExchange  -TerminateProcess  GetCurrentProcess >UnhandledExceptionFilter  SetUnhandledExceptionFilter IsDebuggerPresent TQueryPerformanceCounter fGetTickCount  GetCurrentThreadId  GetCurrentProcessId OGetSystemTimeAsFileTime s __CxxFrameHandler3                                                    N@D   $!@                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            8                   P                   h                	                   	     @  (        C  V        (4   V S _ V E R S I O N _ I N F O                                                  S t r i n g F i l e I n f o   b   0 4 0 9 0 4 b 0    Q  F i l e D e s c r i p t i o n     R e a d s   f r o m   s t d i n   w i t h o u t   l e a k i n g   i n f o   t o   t h e   t e r m i n a l   a n d   o u t p u t s   b a c k   t o   s t d o u t     6   F i l e V e r s i o n     1 ,   0 ,   0 ,   0     8   I n t e r n a l N a m e   h i d d e n i n p u t   P   L e g a l C o p y r i g h t   J o r d i   B o g g i a n o   -   2 0 1 2   H   O r i g i n a l F i l e n a m e   h i d d e n i n p u t . e x e   :   P r o d u c t N a m e     H i d d e n   I n p u t     :   P r o d u c t V e r s i o n   1 ,   0 ,   0 ,   0     D    V a r F i l e I n f o     $    T r a n s l a t i o n     	<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
    <security>
      <requestedPrivileges>
        <requestedExecutionLevel level="asInvoker" uiAccess="false"></requestedExecutionLevel>
      </requestedPrivileges>
    </security>
  </trustInfo>
  <dependency>
    <dependentAssembly>
      <assemblyIdentity type="win32" name="Microsoft.VC90.CRT" version="9.0.21022.8" processorArchitecture="x86" publicKeyToken="1fc8b3b9a1e18e3b"></assemblyIdentity>
    </dependentAssembly>
  </dependency>
</assembly>PAPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDINGPADDINGXXPADDING   @  00!0/080F0L0T0^0d0n0{000000000000001#1-1@1J1O1T1v1{1111111111111112"2*23292A2M2_2j2p222222222222333%303N3T3Z3`3f3l3s3z333333333333333334444%4;4B444444444445!5^5c5555H6M6_6}666 777*7w7|77777888=8E8P8V8\8b8h8n8t8z88889      $   0001 1t1x12 2@2\2`2h2t2 0     0                                                                                                                                                  #compdef {{ COMMAND_NAME }}

# This file is part of the Symfony package.
#
# (c) Fabien Potencier <fabien@symfony.com>
#
# For the full copyright and license information, please view
# https://symfony.com/doc/current/contributing/code/license.html

#
# zsh completions for {{ COMMAND_NAME }}
#
# References:
#   - https://github.com/spf13/cobra/blob/master/zsh_completions.go
#   - https://github.com/symfony/symfony/blob/5.4/src/Symfony/Component/Console/Resources/completion.bash
#
_sf_{{ COMMAND_NAME }}() {
    local lastParam flagPrefix requestComp out comp
    local -a completions

    # The user could have moved the cursor backwards on the command-line.
    # We need to trigger completion from the $CURRENT location, so we need
    # to truncate the command-line ($words) up to the $CURRENT location.
    # (We cannot use $CURSOR as its value does not work when a command is an alias.)
    words=("${=words[1,CURRENT]}") lastParam=${words[-1]}

    # For zsh, when completing a flag with an = (e.g., {{ COMMAND_NAME }} -n=<TAB>)
    # completions must be prefixed with the flag
    setopt local_options BASH_REMATCH
    if [[ "${lastParam}" =~ '-.*=' ]]; then
        # We are dealing with a flag with an =
        flagPrefix="-P ${BASH_REMATCH}"
    fi

    # Prepare the command to obtain completions
    requestComp="${words[0]} ${words[1]} _complete --no-interaction -szsh -a{{ VERSION }} -c$((CURRENT-1))" i=""
    for w in ${words[@]}; do
        w=$(printf -- '%b' "$w")
        # remove quotes from typed values
        quote="${w:0:1}"
        if [ "$quote" = \' ]; then
            w="${w%\'}"
            w="${w#\'}"
        elif [ "$quote" = \" ]; then
            w="${w%\"}"
            w="${w#\"}"
        fi
        # empty values are ignored
        if [ ! -z "$w" ]; then
            i="${i}-i${w} "
        fi
    done

    # Ensure at least 1 input
    if [ "${i}" = "" ]; then
        requestComp="${requestComp} -i\" \""
    else
        requestComp="${requestComp} ${i}"
    fi

    # Use eval to handle any environment variables and such
    out=$(eval SHELL_VERBOSITY=0 ${requestComp} 2>/dev/null)

    while IFS='\n' read -r comp; do
        if [ -n "$comp" ]; then
            # If requested, completions are returned with a description.
            # The description is preceded by a TAB character.
            # For zsh's _describe, we need to use a : instead of a TAB.
            # We first need to escape any : as part of the completion itself.
            comp=${comp//:/\\:}
            local tab=$(printf '\t')
            comp=${comp//$tab/:}
            completions+=${comp}
        fi
    done < <(printf "%s\n" "${out[@]}")

    # Let inbuilt _describe handle completions
    eval _describe "completions" completions $flagPrefix
    return $?
}

compdef _sf_{{ COMMAND_NAME }} {{ COMMAND_NAME }}
Copyright (c) 2018-present Fabien Potencier

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
{
    "name": "symfony/polyfill-ctype",
    "type": "library",
    "description": "Symfony polyfill for ctype functions",
    "keywords": ["polyfill", "compatibility", "portable", "ctype"],
    "homepage": "https://symfony.com",
    "license": "MIT",
    "authors": [
        {
            "name": "Gert de Pagter",
            "email": "BackEndTea@gmail.com"
        },
        {
            "name": "Symfony Community",
            "homepage": "https://symfony.com/contributors"
        }
    ],
    "require": {
        "php": ">=7.2"
    },
    "provide": {
        "ext-ctype": "*"
    },
    "autoload": {
        "psr-4": { "Symfony\\Polyfill\\Ctype\\": "" },
        "files": [ "bootstrap.php" ]
    },
    "suggest": {
        "ext-ctype": "For best performance"
    },
    "minimum-stability": "dev",
    "extra": {
        "thanks": {
            "name": "symfony/polyfill",
            "url": "https://github.com/symfony/polyfill"
        }
    }
}
Symfony Polyfill / Ctype
========================

This component provides `ctype_*` functions to users who run php versions without the ctype extension.

More information can be found in the
[main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md).

License
=======

This library is released under the [MIT license](LICENSE).
<?php

$autoload_paths = [
    __DIR__ . '/../../../autoload.php',
    __DIR__ . '/../vendor/autoload.php',
    __DIR__ . '/vendor/autoload.php',
];

foreach ($autoload_paths as $file) {
    if (file_exists($file)) {
        require_once $file;
        break;
    }
}

use Symfony\Component\Console\Application;

$application = new Application();
$application->setName('CS-Cart Command-line Tools v.26.08.21');

//$application->add(new \Tygh\Sdk\Commands\AddonSymlinkCommand());
$application->add(new \Tygh\Sdk\Commands\AddonExportCommand());
//$application->add(new \Tygh\Sdk\Commands\AddonSyncCommand());
//$application->add(new \Tygh\Sdk\Commands\AddonBuildUpgradeCommand());

$application->add(new \Tygh\Slx\Commands\FindFnHooksCommand());
$application->add(new \Tygh\Slx\Commands\FindTemplateFilesCommand());
$application->add(new \Tygh\Slx\Commands\FindUnbalancedHtmlTagsCommand());
$application->add(new \Tygh\Slx\Commands\FindControllerModeCommand());

$application->run();
{ BƞA   GBMB