#!/usr/bin/env php
<?php

/*
 * Generated by Humbug Box 3.13.0@275b091.
 *
 * @link https://github.com/humbug/box
 */

Phar::mapPhar('box-auto-generated-alias-06fae881eaf6.phar');

require 'phar://box-auto-generated-alias-06fae881eaf6.phar/bin/insight';

__HALT_COMPILER(); ?>
                   vendor/autoload.php  Qh  Ѥ      ,   vendor/jms/metadata/src/PropertyMetadata.phpM  QhM         4   vendor/jms/metadata/src/MetadataFactoryInterface.php  Qh        2   vendor/jms/metadata/src/ClassHierarchyMetadata.php  Qh  ^K      )   vendor/jms/metadata/src/ClassMetadata.php  Qh  ^Ǥ      2   vendor/jms/metadata/src/MergeableClassMetadata.php5  Qh5  @̮J      .   vendor/jms/metadata/src/MergeableInterface.php   Qh   ;ޤ      +   vendor/jms/metadata/src/MetadataFactory.php  Qh  V      (   vendor/jms/metadata/src/NullMetadata.php   Qh         4   vendor/jms/metadata/src/Driver/LazyLoadingDriver.php  Qh  8      ?   vendor/jms/metadata/src/Driver/AdvancedFileLocatorInterface.phpy  Qhy  ID      5   vendor/jms/metadata/src/Driver/AbstractFileDriver.php  Qh  vY      :   vendor/jms/metadata/src/Driver/AdvancedDriverInterface.phpm  Qhm  5q'      @   vendor/jms/metadata/src/Driver/TraceableFileLocatorInterface.phpK  QhK  9      7   vendor/jms/metadata/src/Driver/FileLocatorInterface.php   Qh   Ai{
      .   vendor/jms/metadata/src/Driver/FileLocator.php  Qh  X .Ӥ      2   vendor/jms/metadata/src/Driver/DriverInterface.php   Qh         .   vendor/jms/metadata/src/Driver/DriverChain.php  Qh  Y0      <   vendor/jms/metadata/src/AdvancedMetadataFactoryInterface.php  Qh  ]2      /   vendor/jms/metadata/src/SerializationHelper.php  Qh  G%7      *   vendor/jms/metadata/src/MethodMetadata.php   Qh   Q#      9   vendor/jms/metadata/src/Cache/ClearableCacheInterface.php  Qh  ˕D      0   vendor/jms/metadata/src/Cache/CacheInterface.php  Qh  CH      +   vendor/jms/metadata/src/Cache/FileCache.php  Qh  p      6   vendor/jms/metadata/src/Cache/DoctrineCacheAdapter.php  Qh  LӤ      1   vendor/jms/metadata/src/Cache/PsrCacheAdapter.php  Qh  +      #   vendor/jms/serializer/phpbench.json   Qh   /v@      (   vendor/jms/serializer/doc/logo-small.pngI  QhI  $      *   vendor/jms/serializer/doc/requirements.txtw   Qhw   %      3   vendor/jms/serializer/doc/reference/annotations.rst:  Qh:  _x      5   vendor/jms/serializer/doc/reference/xml_reference.rst  Qh  v      5   vendor/jms/serializer/doc/reference/yml_reference.rst  Qh  ˧p      &   vendor/jms/serializer/doc/handlers.rst  Qh  G;      '   vendor/jms/serializer/doc/reference.rstV   QhV   /T      "   vendor/jms/serializer/doc/logo.png8  Qh8  j      #   vendor/jms/serializer/doc/index.rst  Qh  $[      &   vendor/jms/serializer/doc/cookbook.rstA   QhA   gȟ      #   vendor/jms/serializer/doc/usage.rst  Qh  ܤ      !   vendor/jms/serializer/doc/conf.py@  Qh@  D      /   vendor/jms/serializer/doc/cookbook/stdclass.rst  Qh  r      -   vendor/jms/serializer/doc/cookbook/arrays.rst^	  Qh^	  NP      9   vendor/jms/serializer/doc/cookbook/object_constructor.rst:  Qh:  Ro      ;   vendor/jms/serializer/doc/cookbook/exclusion_strategies.rst,  Qh,  Kc      +   vendor/jms/serializer/doc/configuration.rstq  Qhq        *   vendor/jms/serializer/doc/event_system.rst  Qh  I      ,   vendor/jms/serializer/src/GraphNavigator.php5  Qh5  8      '   vendor/jms/serializer/src/Type/Type.php   Qh         )   vendor/jms/serializer/src/Type/Parser.php  Qh  PD      (   vendor/jms/serializer/src/Type/Lexer.php   Qh   C      6   vendor/jms/serializer/src/Type/Exception/Exception.php   Qh    F4K      8   vendor/jms/serializer/src/Type/Exception/InvalidNode.php   Qh   P|      8   vendor/jms/serializer/src/Type/Exception/SyntaxError.php   Qh   ibˤ      2   vendor/jms/serializer/src/Type/ParserInterface.php   Qh   CǤ      @   vendor/jms/serializer/src/Handler/ConstraintViolationHandler.phpZ  QhZ  I      9   vendor/jms/serializer/src/Handler/LazyHandlerRegistry.phpe  Qhe  9`      2   vendor/jms/serializer/src/Handler/UnionHandler.php  Qh  }$W      6   vendor/jms/serializer/src/Handler/FormErrorHandler.php  Qh  S6       1   vendor/jms/serializer/src/Handler/DateHandler.php(  Qh(  T/      A   vendor/jms/serializer/src/Handler/SubscribingHandlerInterface.php  Qh  m^      5   vendor/jms/serializer/src/Handler/StdClassHandler.php  Qh  a      5   vendor/jms/serializer/src/Handler/IteratorHandler.php  Qh  <`C      1   vendor/jms/serializer/src/Handler/EnumHandler.php  Qh  {ؤ      7   vendor/jms/serializer/src/Handler/SymfonyUidHandler.php  Qh        >   vendor/jms/serializer/src/Handler/HandlerRegistryInterface.phpn  Qhn  (5`      <   vendor/jms/serializer/src/Handler/ArrayCollectionHandler.php  Qh  F-5      5   vendor/jms/serializer/src/Handler/HandlerRegistry.php0  Qh0  Y
      7   vendor/jms/serializer/src/XmlDeserializationVisitor.phpM  QhM  j      >   vendor/jms/serializer/src/JsonDeserializationStrictVisitor.php  Qh  pK?      7   vendor/jms/serializer/src/Metadata/PropertyMetadata.php>  Qh>  5oJ.      >   vendor/jms/serializer/src/Metadata/VirtualPropertyMetadata.phpr  Qhr  Խ/      4   vendor/jms/serializer/src/Metadata/ClassMetadata.php2  Qh2  Y      A   vendor/jms/serializer/src/Metadata/ExpressionPropertyMetadata.php  Qh  ~      <   vendor/jms/serializer/src/Metadata/Driver/DocBlockDriver.phpA  QhA  [      I   vendor/jms/serializer/src/Metadata/Driver/AnnotationOrAttributeDriver.php?  Qh?  |9      B   vendor/jms/serializer/src/Metadata/Driver/EnumPropertiesDriver.php  Qh  5      M   vendor/jms/serializer/src/Metadata/Driver/AttributeDriver/AttributeReader.php  Qh  Ҥ      E   vendor/jms/serializer/src/Metadata/Driver/ExpressionMetadataTrait.php  Qh  }      Q   vendor/jms/serializer/src/Metadata/Driver/DocBlockDriver/DocBlockTypeResolver.phpAA  QhAA  RҤ      8   vendor/jms/serializer/src/Metadata/Driver/YamlDriver.phpF  QhF  ,?      =   vendor/jms/serializer/src/Metadata/Driver/AttributeDriver.php5  Qh5  6 =      H   vendor/jms/serializer/src/Metadata/Driver/DefaultValuePropertyDriver.phps  Qhs  n      @   vendor/jms/serializer/src/Metadata/Driver/DoctrineTypeDriver.phpZ  QhZ        H   vendor/jms/serializer/src/Metadata/Driver/AbstractDoctrineTypeDriver.php  Qh  hĤ      C   vendor/jms/serializer/src/Metadata/Driver/TypedPropertiesDriver.php  Qh  vH;      7   vendor/jms/serializer/src/Metadata/Driver/XmlDriver.phpH  QhH  id      E   vendor/jms/serializer/src/Metadata/Driver/DoctrinePHPCRTypeDriver.phpI	  QhI	  Iq      >   vendor/jms/serializer/src/Metadata/Driver/AnnotationDriver.phpR  QhR  !B      8   vendor/jms/serializer/src/Metadata/Driver/NullDriver.php  Qh  !n      =   vendor/jms/serializer/src/Metadata/StaticPropertyMetadata.php  Qh  W
\      -   vendor/jms/serializer/src/AbstractVisitor.phpq  Qhq  B      8   vendor/jms/serializer/src/JsonDeserializationVisitor.php  Qh  h賤      G   vendor/jms/serializer/src/Construction/UnserializeObjectConstructor.phpz  Qhz  H}      E   vendor/jms/serializer/src/Construction/ObjectConstructorInterface.php  Qh  hb      D   vendor/jms/serializer/src/Construction/DoctrineObjectConstructor.php  Qh  ZiE      5   vendor/jms/serializer/src/GraphNavigatorInterface.phpP  QhP  d	Ϥ      6   vendor/jms/serializer/src/JsonSerializationVisitor.php  Qh   xg      A   vendor/jms/serializer/src/EventDispatcher/PreDeserializeEvent.php  Qh  ?:.      4   vendor/jms/serializer/src/EventDispatcher/Events.php  Qh  ƚ      A   vendor/jms/serializer/src/EventDispatcher/LazyEventDispatcher.php  Qh  t
      =   vendor/jms/serializer/src/EventDispatcher/EventDispatcher.php  Qh   "F      G   vendor/jms/serializer/src/EventDispatcher/Subscriber/EnumSubscriber.php  Qh  Q      P   vendor/jms/serializer/src/EventDispatcher/Subscriber/DoctrineProxySubscriber.php  Qh  R      \   vendor/jms/serializer/src/EventDispatcher/Subscriber/SymfonyValidatorValidatorSubscriber.php  Qh  _	      9   vendor/jms/serializer/src/EventDispatcher/ObjectEvent.phpu  Qhu  {r=Ҥ      ?   vendor/jms/serializer/src/EventDispatcher/PreSerializeEvent.php  Qh  RM      F   vendor/jms/serializer/src/EventDispatcher/EventSubscriberInterface.php  Qh  Q      F   vendor/jms/serializer/src/EventDispatcher/EventDispatcherInterface.phpj  Qhj  7      3   vendor/jms/serializer/src/EventDispatcher/Event.phpU  QhU   x      3   vendor/jms/serializer/src/Expression/Expression.php  Qh  aϤ      <   vendor/jms/serializer/src/Expression/ExpressionEvaluator.php  Qh  ":      O   vendor/jms/serializer/src/Expression/CompilableExpressionEvaluatorInterface.phps  Qhs  Oё      E   vendor/jms/serializer/src/Expression/ExpressionEvaluatorInterface.php  Qh  ,ӊ      C   vendor/jms/serializer/src/ContextFactory/CallableContextFactory.php   Qh   8      Q   vendor/jms/serializer/src/ContextFactory/SerializationContextFactoryInterface.php$  Qh$  n(      P   vendor/jms/serializer/src/ContextFactory/CallableSerializationContextFactory.php  Qh  z      S   vendor/jms/serializer/src/ContextFactory/DeserializationContextFactoryInterface.php.  Qh.  #      R   vendor/jms/serializer/src/ContextFactory/CallableDeserializationContextFactory.php  Qh  oq      Q   vendor/jms/serializer/src/ContextFactory/DefaultDeserializationContextFactory.php  Qh  )ͷ      O   vendor/jms/serializer/src/ContextFactory/DefaultSerializationContextFactory.php  Qh  4#      @   vendor/jms/serializer/src/Exception/InvalidArgumentException.php  Qh  t      >   vendor/jms/serializer/src/Exception/NotAcceptableException.php   Qh   "W      1   vendor/jms/serializer/src/Exception/Exception.php   Qh   V}      6   vendor/jms/serializer/src/Exception/LogicException.php   Qh   i+      8   vendor/jms/serializer/src/Exception/RuntimeException.php  Qh  ى_      F   vendor/jms/serializer/src/Exception/UninitializedPropertyException.php   Qh   V0      C   vendor/jms/serializer/src/Exception/NonIntCastableTypeException.php  Qh  kɤ      J   vendor/jms/serializer/src/Exception/CircularReferenceDetectedException.php   Qh   Yz      >   vendor/jms/serializer/src/Exception/ExcludedClassException.php   Qh   OФ      9   vendor/jms/serializer/src/Exception/XmlErrorException.php  Qh  Ӆ      <   vendor/jms/serializer/src/Exception/SkipHandlerException.php  Qh        @   vendor/jms/serializer/src/Exception/InvalidMetadataException.php   Qh   ÖaK      B   vendor/jms/serializer/src/Exception/UnsupportedFormatException.php   Qh    |      F   vendor/jms/serializer/src/Exception/NonStringCastableTypeException.php$  Qh$  
h      A   vendor/jms/serializer/src/Exception/NonVisitableTypeException.php  Qh  `      E   vendor/jms/serializer/src/Exception/NonFloatCastableTypeException.php"  Qh"  Y      A   vendor/jms/serializer/src/Exception/ValidationFailedException.phpa  Qha  mv      @   vendor/jms/serializer/src/Exception/NonCastableTypeException.php  Qh  qh      C   vendor/jms/serializer/src/Exception/ObjectConstructionException.php  Qh  Rڤ      K   vendor/jms/serializer/src/Exception/ExpressionLanguageRequiredException.php   Qh   jX      7   vendor/jms/serializer/src/NullAwareVisitorInterface.phpZ  QhZ  Z      5   vendor/jms/serializer/src/XmlSerializationVisitor.php^Q  Qh^Q  O.      K   vendor/jms/serializer/src/Exclusion/ExpressionLanguageExclusionStrategy.php>
  Qh>
  @       @   vendor/jms/serializer/src/Exclusion/VersionExclusionStrategy.php  Qh  K      B   vendor/jms/serializer/src/Exclusion/ExclusionStrategyInterface.php{  Qh{  Ma      >   vendor/jms/serializer/src/Exclusion/DepthExclusionStrategy.php  Qh  e      ?   vendor/jms/serializer/src/Exclusion/GroupsExclusionStrategy.phpp  Qhp  lr      A   vendor/jms/serializer/src/Exclusion/DisjunctExclusionStrategy.php  Qh  Q      '   vendor/jms/serializer/src/Functions.php  Qh  ત      1   vendor/jms/serializer/src/SerializerInterface.php   Qh   ͞R      .   vendor/jms/serializer/src/VisitorInterface.php  Qh   b      >   vendor/jms/serializer/src/Accessor/DefaultAccessorStrategy.phpB  QhB  .      @   vendor/jms/serializer/src/Accessor/AccessorStrategyInterface.phpX  QhX  Pe)      H   vendor/jms/serializer/src/Ordering/IdenticalPropertyOrderingStrategy.php  Qh  p?      K   vendor/jms/serializer/src/Ordering/AlphabeticalPropertyOrderingStrategy.php  Qh  jԤ      @   vendor/jms/serializer/src/Ordering/PropertyOrderingInterface.phpY  QhY  )^      E   vendor/jms/serializer/src/Ordering/CustomPropertyOrderingStrategy.php}  Qh}         2   vendor/jms/serializer/src/SerializationContext.php  Qh  4      7   vendor/jms/serializer/src/ArrayTransformerInterface.php  Qh  h!ۤ      (   vendor/jms/serializer/src/Serializer.phpf#  Qhf#  +醤      C   vendor/jms/serializer/src/Visitor/SerializationVisitorInterface.phpn  Qhn  ,WѤ      M   vendor/jms/serializer/src/Visitor/Factory/JsonSerializationVisitorFactory.php  Qh  b      I   vendor/jms/serializer/src/Visitor/Factory/SerializationVisitorFactory.php'  Qh'  }AǤ      K   vendor/jms/serializer/src/Visitor/Factory/DeserializationVisitorFactory.php-  Qh-  9       N   vendor/jms/serializer/src/Visitor/Factory/XmlDeserializationVisitorFactory.php  Qh        O   vendor/jms/serializer/src/Visitor/Factory/JsonDeserializationVisitorFactory.php  Qh  p|      L   vendor/jms/serializer/src/Visitor/Factory/XmlSerializationVisitorFactory.phpl  Qhl  E      E   vendor/jms/serializer/src/Visitor/DeserializationVisitorInterface.php  Qh  O      H   vendor/jms/serializer/src/GraphNavigator/SerializationGraphNavigator.php1  Qh1  S      J   vendor/jms/serializer/src/GraphNavigator/DeserializationGraphNavigator.php)  Qh)  a Q      Y   vendor/jms/serializer/src/GraphNavigator/Factory/DeserializationGraphNavigatorFactory.php  Qh  W1      S   vendor/jms/serializer/src/GraphNavigator/Factory/GraphNavigatorFactoryInterface.php   Qh   3:      W   vendor/jms/serializer/src/GraphNavigator/Factory/SerializationGraphNavigatorFactory.php  Qh        =   vendor/jms/serializer/src/Twig/SerializerRuntimeExtension.php  Qh  T;      :   vendor/jms/serializer/src/Twig/SerializerBaseExtension.php|  Qh|  Fϸ      6   vendor/jms/serializer/src/Twig/SerializerExtension.php  Qh  =      :   vendor/jms/serializer/src/Twig/SerializerRuntimeHelper.php  Qh  k      D   vendor/jms/serializer/src/Naming/PropertyNamingStrategyInterface.php  Qh        E   vendor/jms/serializer/src/Naming/SerializedNameAnnotationStrategy.php#  Qh#  <ͤ      <   vendor/jms/serializer/src/Naming/CamelCaseNamingStrategy.php  Qh  V!X      D   vendor/jms/serializer/src/Naming/IdenticalPropertyNamingStrategy.php?  Qh?        7   vendor/jms/serializer/src/Annotation/SerializedName.php  Qh  uq      .   vendor/jms/serializer/src/Annotation/Since.php   Qh   ] ~D      /   vendor/jms/serializer/src/Annotation/Inline.php
  Qh
        0   vendor/jms/serializer/src/Annotation/XmlList.php  Qh  U      1   vendor/jms/serializer/src/Annotation/XmlValue.php  Qh  KϤ      0   vendor/jms/serializer/src/Annotation/Version.php  Qh  +h      =   vendor/jms/serializer/src/Annotation/AnnotationUtilsTrait.php  Qh  d4      3   vendor/jms/serializer/src/Annotation/AccessType.php7  Qh7  UR      8   vendor/jms/serializer/src/Annotation/ExclusionPolicy.php#  Qh#  w@      5   vendor/jms/serializer/src/Annotation/XmlNamespace.phpG  QhG  {',      -   vendor/jms/serializer/src/Annotation/Type.php  Qh  7      1   vendor/jms/serializer/src/Annotation/Accessor.phpe  Qhe  ,|      1   vendor/jms/serializer/src/Annotation/ReadOnly.php   Qh   W)      9   vendor/jms/serializer/src/Annotation/XmlDiscriminator.php}  Qh}  y      <   vendor/jms/serializer/src/Annotation/SerializerAttribute.php   Qh   5      8   vendor/jms/serializer/src/Annotation/PostDeserialize.php  Qh  _$^      5   vendor/jms/serializer/src/Annotation/XmlAttribute.php	  Qh	  Gۤ      3   vendor/jms/serializer/src/Annotation/XmlElement.php]  Qh]  m      /   vendor/jms/serializer/src/Annotation/Expose.php  Qh  P      ;   vendor/jms/serializer/src/Annotation/UnionDiscriminator.php  Qh  - B      8   vendor/jms/serializer/src/Annotation/XmlAttributeMap.php  Qh        /   vendor/jms/serializer/src/Annotation/XmlMap.phpi  Qhi  d;      0   vendor/jms/serializer/src/Annotation/Exclude.php  Qh  #_      0   vendor/jms/serializer/src/Annotation/XmlRoot.php  Qh  ߽      8   vendor/jms/serializer/src/Annotation/VirtualProperty.php  Qh  ^H1      5   vendor/jms/serializer/src/Annotation/PreSerialize.php  Qh        6   vendor/jms/serializer/src/Annotation/SkipWhenEmpty.php  Qh  7
      9   vendor/jms/serializer/src/Annotation/ReadOnlyProperty.php
  Qh
  !      6   vendor/jms/serializer/src/Annotation/Discriminator.php  Qh  L_      /   vendor/jms/serializer/src/Annotation/Groups.php  Qh  .      6   vendor/jms/serializer/src/Annotation/XmlCollection.php  Qh  ҽ      ;   vendor/jms/serializer/src/Annotation/DeprecatedReadOnly.php3  Qh3        .   vendor/jms/serializer/src/Annotation/Until.php   Qh   \邤      9   vendor/jms/serializer/src/Annotation/XmlKeyValuePairs.php  Qh  /      6   vendor/jms/serializer/src/Annotation/AccessorOrder.php  Qh  Lu      6   vendor/jms/serializer/src/Annotation/PostSerialize.php   Qh   Q<      1   vendor/jms/serializer/src/Annotation/MaxDepth.php  Qh  6Pdp      ;   vendor/jms/serializer/src/Builder/DocBlockDriverFactory.php  Qh  Fi+      ;   vendor/jms/serializer/src/Builder/CallbackDriverFactory.php  Qh  L=      <   vendor/jms/serializer/src/Builder/DriverFactoryInterface.php!  Qh!  ʗ      :   vendor/jms/serializer/src/Builder/DefaultDriverFactory.php  Qh  [~      4   vendor/jms/serializer/src/DeserializationContext.php  Qh  _[V      %   vendor/jms/serializer/src/Context.phpv  Qhv  )N      /   vendor/jms/serializer/src/SerializerBuilder.phpT  QhT  /          vendor/jms/serializer/rector.php   Qh   DФ      "   vendor/composer/autoload_files.php  Qh  E         vendor/composer/LICENSE.  Qh.            vendor/composer/installed.php3  Qh3  Q&      %   vendor/composer/InstalledVersions.phpC  QhC  <nw         vendor/composer/ClassLoader.php?  Qh?  2@u      !   vendor/composer/autoload_psr4.php  Qh  x
      "   vendor/composer/platform_check.php  Qh  -      %   vendor/composer/autoload_classmap.php>Q Qh>Q       #   vendor/composer/autoload_static.php Qh da      !   vendor/composer/autoload_real.php  Qh  na      '   vendor/composer/autoload_namespaces.php   Qh   /t      0   vendor/doctrine/deprecations/src/Deprecation.phpa%  Qha%  ] :      ?   vendor/doctrine/deprecations/src/PHPUnit/VerifyDeprecations.php  Qh  [=u      +   vendor/doctrine/lexer/src/AbstractLexer.phpS  QhS  p⹤      #   vendor/doctrine/lexer/src/Token.php  Qh  +{ڤ      .   vendor/doctrine/instantiator/docs/en/index.rst  Qh  HX      0   vendor/doctrine/instantiator/docs/en/sidebar.rst&   Qh&   v      &   vendor/doctrine/instantiator/psalm.xml  Qh  w      P   vendor/doctrine/instantiator/src/Doctrine/Instantiator/InstantiatorInterface.php  Qh  .U      ]   vendor/doctrine/instantiator/src/Doctrine/Instantiator/Exception/InvalidArgumentException.php  Qh  $8      ]   vendor/doctrine/instantiator/src/Doctrine/Instantiator/Exception/UnexpectedValueException.phpW  QhW  z.      W   vendor/doctrine/instantiator/src/Doctrine/Instantiator/Exception/ExceptionInterface.php   Qh   f      G   vendor/doctrine/instantiator/src/Doctrine/Instantiator/Instantiator.php  Qh  yaݤ      3   vendor/doctrine/annotations/docs/en/annotations.rst  Qh        .   vendor/doctrine/annotations/docs/en/custom.rstM'  QhM'  5d¤      -   vendor/doctrine/annotations/docs/en/index.rstH  QhH  @      /   vendor/doctrine/annotations/docs/en/sidebar.rstA   QhA   
\      O   vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/PsrCachedReader.php  Qh  z**      S   vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/AnnotationException.php  Qh  Wp.      M   vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/IndexedReader.php  Qh  蕛      I   vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/DocParser.php  Qh  w<NS      P   vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/AnnotationReader.php-  Qh-  Fr,      R   vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/AnnotationRegistry.phpY  QhY  *      I   vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/PhpParser.php&  Qh&  ٺ      F   vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/Reader.php	  Qh	  $Vi      K   vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/TokenParser.php  Qh  	xe      J   vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation.php  Qh  y      `   vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/ImplicitlyIgnoredAnnotationNames.php  Qh  &      T   vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation/Attribute.phpg  Qhg  z	jX      c   vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation/NamedArgumentConstructor.php   Qh   GO      U   vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation/Attributes.php'  Qh'  Վ      Q   vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation/Target.phpJ
  QhJ
  Ǡ      S   vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation/Required.php   Qh   O      O   vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation/Enum.php  Qh  H      [   vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation/IgnoreAnnotation.php  Qh  
/      H   vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/DocLexer.php  Qh  M      @   vendor/symfony/http-client-contracts/ResponseStreamInterface.phpd  Qhd  t5      P   vendor/symfony/http-client-contracts/Exception/RedirectionExceptionInterface.php  Qh  XLB      K   vendor/symfony/http-client-contracts/Exception/ServerExceptionInterface.php  Qh  q      M   vendor/symfony/http-client-contracts/Exception/DecodingExceptionInterface.php  Qh  Z      K   vendor/symfony/http-client-contracts/Exception/ClientExceptionInterface.php  Qh  7UJ      N   vendor/symfony/http-client-contracts/Exception/TransportExceptionInterface.php  Qh  |ݤ      L   vendor/symfony/http-client-contracts/Exception/TimeoutExceptionInterface.php  Qh  V      E   vendor/symfony/http-client-contracts/Exception/ExceptionInterface.php  Qh  ,      I   vendor/symfony/http-client-contracts/Exception/HttpExceptionInterface.php8  Qh8  v      :   vendor/symfony/http-client-contracts/ResponseInterface.php  Qh  ,^      @   vendor/symfony/http-client-contracts/Test/Fixtures/web/index.php  Qh  KԤ      @   vendor/symfony/http-client-contracts/Test/HttpClientTestCase.php  Qh  IА(      <   vendor/symfony/http-client-contracts/Test/TestHttpServer.php  Qh  L{c      7   vendor/symfony/http-client-contracts/ChunkInterface.php   Qh   C@      <   vendor/symfony/http-client-contracts/HttpClientInterface.php  Qh  yJ      =   vendor/symfony/service-contracts/ServiceProviderInterface.php  Qh  mX¤      ;   vendor/symfony/service-contracts/ServiceSubscriberTrait.php  Qh  FM      ?   vendor/symfony/service-contracts/ServiceSubscriberInterface.php  Qh  B6      3   vendor/symfony/service-contracts/ResetInterface.php  Qh  v      8   vendor/symfony/service-contracts/ServiceLocatorTrait.php  Qh  za      7   vendor/symfony/service-contracts/Attribute/Required.php  Qh  e;Z      @   vendor/symfony/service-contracts/Attribute/SubscribedService.php   Qh         <   vendor/symfony/service-contracts/Test/ServiceLocatorTest.php  Qh  _8      @   vendor/symfony/service-contracts/Test/ServiceLocatorTestCase.php  Qh  q#      2   vendor/symfony/expression-language/TokenStream.phpL  QhL        L   vendor/symfony/expression-language/Resources/bin/generate_operator_regex.php  Qh  -      J   vendor/symfony/expression-language/ExpressionFunctionProviderInterface.php  Qh  z;      1   vendor/symfony/expression-language/Expression.php  Qh         9   vendor/symfony/expression-language/ExpressionFunction.php  Qh  ,      A   vendor/symfony/expression-language/SerializedParsedExpression.phpO  QhO  #      -   vendor/symfony/expression-language/Parser.php%A  Qh%A  ND      ,   vendor/symfony/expression-language/Lexer.phpC  QhC  Oߤ      9   vendor/symfony/expression-language/ExpressionLanguage.phpV  QhV  J꾤      2   vendor/symfony/expression-language/SyntaxError.php  Qh  u_'      7   vendor/symfony/expression-language/ParsedExpression.php  Qh  ͤ      9   vendor/symfony/expression-language/Node/ArgumentsNode.php3  Qh3        ;   vendor/symfony/expression-language/Node/ConditionalNode.php  Qh  !      0   vendor/symfony/expression-language/Node/Node.phpb
  Qhb
  D      6   vendor/symfony/expression-language/Node/BinaryNode.php  Qh  C      5   vendor/symfony/expression-language/Node/ArrayNode.php
  Qh
  פ      5   vendor/symfony/expression-language/Node/UnaryNode.php  Qh  o      7   vendor/symfony/expression-language/Node/GetAttrNode.php  Qh  Pͤ      8   vendor/symfony/expression-language/Node/ConstantNode.php  Qh  C      4   vendor/symfony/expression-language/Node/NameNode.php  Qh        8   vendor/symfony/expression-language/Node/FunctionNode.php_  Qh_  8VP-      ,   vendor/symfony/expression-language/Token.phpP  QhP  }s      /   vendor/symfony/expression-language/Compiler.php  Qh  ]C      D   vendor/symfony/http-client/DataCollector/HttpClientDataCollector.php  Qh  ><;      -   vendor/symfony/http-client/MockHttpClient.php  Qh  c|      0   vendor/symfony/http-client/ScopingHttpClient.phpV  QhV        >   vendor/symfony/http-client/Response/TransportResponseTrait.php+  Qh+  K      4   vendor/symfony/http-client/Response/CurlResponse.phpI  QhI  bѤ      5   vendor/symfony/http-client/Response/AsyncResponse.phpC  QhC  %kSY      3   vendor/symfony/http-client/Response/AmpResponse.phpB  QhB  ϼ      ;   vendor/symfony/http-client/Response/StreamableInterface.php  Qh        4   vendor/symfony/http-client/Response/AsyncContext.php  Qh        ;   vendor/symfony/http-client/Response/CommonResponseTrait.php  Qh   0g٤      6   vendor/symfony/http-client/Response/HttplugPromise.php|  Qh|  A_      6   vendor/symfony/http-client/Response/ResponseStream.php}  Qh}  ٧0      4   vendor/symfony/http-client/Response/MockResponse.php/  Qh/  d       5   vendor/symfony/http-client/Response/StreamWrapper.php$  Qh$  J      9   vendor/symfony/http-client/Response/TraceableResponse.phpX  QhX  p~      6   vendor/symfony/http-client/Response/NativeResponse.php5  Qh5  uO      )   vendor/symfony/http-client/HttpClient.php  Qh  Ҹj7      ,   vendor/symfony/http-client/AmpHttpClient.php  Qh  L      /   vendor/symfony/http-client/NativeHttpClient.phpyP  QhyP  @      A   vendor/symfony/http-client/Exception/InvalidArgumentException.php  Qh  
z      6   vendor/symfony/http-client/Exception/JsonException.phpM  QhM        =   vendor/symfony/http-client/Exception/EventSourceException.php   Qh   o      9   vendor/symfony/http-client/Exception/TimeoutException.php  Qh  zk!       ;   vendor/symfony/http-client/Exception/HttpExceptionTrait.php
  Qh
  S#ͤ      =   vendor/symfony/http-client/Exception/RedirectionException.phpC  QhC  	ȳ.      8   vendor/symfony/http-client/Exception/ServerException.php4  Qh4  4      ;   vendor/symfony/http-client/Exception/TransportException.php  Qh        8   vendor/symfony/http-client/Exception/ClientException.php4  Qh4  :դ      -   vendor/symfony/http-client/DecoratorTrait.php  Qh  7S      2   vendor/symfony/http-client/RetryableHttpClient.php  Qh  _6!      4   vendor/symfony/http-client/EventSourceHttpClient.phpn  Qhn        .   vendor/symfony/http-client/HttpClientTrait.phpq  Qhq  ^      A   vendor/symfony/http-client/DependencyInjection/HttpClientPass.php  Qh  lO      2   vendor/symfony/http-client/TraceableHttpClient.php  Qh  "LϤ      ;   vendor/symfony/http-client/Retry/RetryStrategyInterface.phpp  Qhp        9   vendor/symfony/http-client/Retry/GenericRetryStrategy.phpp  Qhp  >      *   vendor/symfony/http-client/Psr18Client.php@  Qh@  !      *   vendor/symfony/http-client/HttpOptions.php  Qh        0   vendor/symfony/http-client/CachingHttpClient.php  Qh  lu      .   vendor/symfony/http-client/Chunk/DataChunk.php  Qh  R      7   vendor/symfony/http-client/Chunk/InformationalChunk.php  Qh        4   vendor/symfony/http-client/Chunk/ServerSentEvent.php  Qh  o      .   vendor/symfony/http-client/Chunk/LastChunk.php  Qh  zx      /   vendor/symfony/http-client/Chunk/FirstChunk.php  Qh  a*      /   vendor/symfony/http-client/Chunk/ErrorChunk.phpj  Qhj  Lϫ      -   vendor/symfony/http-client/CurlHttpClient.php]  Qh]  %      ,   vendor/symfony/http-client/HttplugClient.php'  Qh'  5r      0   vendor/symfony/http-client/Internal/DnsCache.php  Qh  C      6   vendor/symfony/http-client/Internal/AmpClientState.php!  Qh!  t}      .   vendor/symfony/http-client/Internal/Canary.php  Qh  *ā      3   vendor/symfony/http-client/Internal/AmpResolver.php,  Qh,  sd      /   vendor/symfony/http-client/Internal/AmpBody.php  Qh        3   vendor/symfony/http-client/Internal/AmpListener.phpF  QhF  X      9   vendor/symfony/http-client/Internal/NativeClientState.php  Qh  $      6   vendor/symfony/http-client/Internal/PushedResponse.php  Qh  P<      7   vendor/symfony/http-client/Internal/CurlClientState.php   Qh   n77      3   vendor/symfony/http-client/Internal/ClientState.php	  Qh	  ¸      7   vendor/symfony/http-client/Internal/HttplugWaitLoop.php  Qh  `0      9   vendor/symfony/http-client/NoPrivateNetworkHttpClient.php}'  Qh}'  L:      2   vendor/symfony/http-client/AsyncDecoratorTrait.phpw  Qhw  !      5   vendor/symfony/polyfill-intl-grapheme/bootstrap80.phpR  QhR  i`      2   vendor/symfony/polyfill-intl-grapheme/Grapheme.php)  Qh)  Qΐ      3   vendor/symfony/polyfill-intl-grapheme/bootstrap.phpY	  QhY	  IH1      B   vendor/symfony/polyfill-mbstring/Resources/unidata/caseFolding.phpa	  Qha	  |ⳤ      @   vendor/symfony/polyfill-mbstring/Resources/unidata/lowerCase.php_  Qh_  d      @   vendor/symfony/polyfill-mbstring/Resources/unidata/upperCase.phpf  Qhf  P      F   vendor/symfony/polyfill-mbstring/Resources/unidata/titleCaseRegexp.php9  Qh9  >|zK      -   vendor/symfony/polyfill-mbstring/Mbstring.phpK  QhK  WѤ      0   vendor/symfony/polyfill-mbstring/bootstrap80.php'  Qh'  	~
      .   vendor/symfony/polyfill-mbstring/bootstrap.php!  Qh!  d      F   vendor/symfony/var-exporter/Exception/NotInstantiableTypeException.php/  Qh/  :)      <   vendor/symfony/var-exporter/Exception/ExceptionInterface.phpV  QhV  '/I      @   vendor/symfony/var-exporter/Exception/ClassNotFoundException.php"  Qh"  &5      +   vendor/symfony/var-exporter/VarExporter.php2  Qh2  /uZ      ,   vendor/symfony/var-exporter/Instantiator.php^  Qh^  Ai@s      1   vendor/symfony/var-exporter/Internal/Hydrator.php8  Qh8  V      1   vendor/symfony/var-exporter/Internal/Exporter.php\A  Qh\A  u|T      2   vendor/symfony/var-exporter/Internal/Reference.php.  Qh.        1   vendor/symfony/var-exporter/Internal/Registry.php>  Qh>        /   vendor/symfony/var-exporter/Internal/Values.php  Qh  ^      1   vendor/symfony/deprecation-contracts/function.php  Qh  rg      ?   vendor/symfony/polyfill-php73/Resources/stubs/JsonException.phpE  QhE  8S      '   vendor/symfony/polyfill-php73/Php73.phpb  Qhb  J<      +   vendor/symfony/polyfill-php73/bootstrap.php  Qh  |      '   vendor/symfony/polyfill-ctype/Ctype.php  Qh  xs      -   vendor/symfony/polyfill-ctype/bootstrap80.phpr  Qhr  F)      +   vendor/symfony/polyfill-ctype/bootstrap.php@  Qh@  jQ9      6   vendor/symfony/polyfill-intl-normalizer/Normalizer.phpd%  Qhd%  u      R   vendor/symfony/polyfill-intl-normalizer/Resources/unidata/canonicalComposition.phpD  QhD  'CԤ      T   vendor/symfony/polyfill-intl-normalizer/Resources/unidata/canonicalDecomposition.php{  Qh{  je      X   vendor/symfony/polyfill-intl-normalizer/Resources/unidata/compatibilityDecomposition.phpo Qho c,      L   vendor/symfony/polyfill-intl-normalizer/Resources/unidata/combiningClass.phpD5  QhD5        F   vendor/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php  Qh  %      7   vendor/symfony/polyfill-intl-normalizer/bootstrap80.php  Qh  ,      5   vendor/symfony/polyfill-intl-normalizer/bootstrap.php  Qh  #p      <   vendor/symfony/polyfill-php80/Resources/stubs/Stringable.php  Qh  t]\ڤ      ;   vendor/symfony/polyfill-php80/Resources/stubs/Attribute.php  Qh  MK<      <   vendor/symfony/polyfill-php80/Resources/stubs/ValueError.php>  Qh>  g      :   vendor/symfony/polyfill-php80/Resources/stubs/PhpToken.phpw  Qhw  =7T8      E   vendor/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.phpG  QhG  ֈ+      *   vendor/symfony/polyfill-php80/PhpToken.php  Qh  ϴ      '   vendor/symfony/polyfill-php80/Php80.php  Qh  cH      +   vendor/symfony/polyfill-php80/bootstrap.php  Qh  .Ĥ      -   vendor/symfony/cache-contracts/CacheTrait.php
  Qh
  K      9   vendor/symfony/cache-contracts/TagAwareCacheInterface.php  Qh  .&c֤      1   vendor/symfony/cache-contracts/CacheInterface.php[	  Qh[	  i      0   vendor/symfony/cache-contracts/ItemInterface.php  Qh  jϤ      4   vendor/symfony/cache-contracts/CallbackInterface.php+  Qh+  5s      *   vendor/symfony/cache/Traits/ProxyTrait.php1  Qh1  -      *   vendor/symfony/cache/Traits/RedisTrait.phpKp  QhKp  զ      /   vendor/symfony/cache/Traits/FilesystemTrait.php  Qh  sJ%      1   vendor/symfony/cache/Traits/RedisClusterProxy.php  Qh  T%4      5   vendor/symfony/cache/Traits/RedisClusterNodeProxy.phpN  QhN  ʂ      *   vendor/symfony/cache/Traits/RedisProxy.php  Qh        4   vendor/symfony/cache/Traits/AbstractAdapterTrait.phpm1  Qhm1  ^-j      .   vendor/symfony/cache/Traits/ContractsTrait.php/  Qh/  &      5   vendor/symfony/cache/Traits/FilesystemCommonTrait.php  Qh  ˹      "   vendor/symfony/cache/CacheItem.php  Qh  [8      9   vendor/symfony/cache/DataCollector/CacheDataCollector.php  Qh  V      %   vendor/symfony/cache/LockRegistry.php  Qh  T      :   vendor/symfony/cache/Adapter/FilesystemTagAwareAdapter.php  Qh        7   vendor/symfony/cache/Adapter/CouchbaseBucketAdapter.php!  Qh!  0      0   vendor/symfony/cache/Adapter/DoctrineAdapter.php  Qh  f2v      4   vendor/symfony/cache/Adapter/ParameterNormalizer.php  Qh  [$O      -   vendor/symfony/cache/Adapter/ArrayAdapter.phpj,  Qhj,  0      5   vendor/symfony/cache/Adapter/RedisTagAwareAdapter.php0  Qh0  ;      1   vendor/symfony/cache/Adapter/TraceableAdapter.php  Qh  [Ƥ      -   vendor/symfony/cache/Adapter/Psr16Adapter.phpm  Qhm  b@S      -   vendor/symfony/cache/Adapter/ProxyAdapter.php!  Qh!  [q|      8   vendor/symfony/cache/Adapter/AbstractTagAwareAdapter.php2  Qh2  j6      ;   vendor/symfony/cache/Adapter/CouchbaseCollectionAdapter.php  Qh  ;      ,   vendor/symfony/cache/Adapter/NullAdapter.php
  Qh
  ̐      0   vendor/symfony/cache/Adapter/PhpArrayAdapter.phpB1  QhB1  Sj      1   vendor/symfony/cache/Adapter/AdapterInterface.php  Qh  r#$      0   vendor/symfony/cache/Adapter/AbstractAdapter.php!  Qh!  IҤ      4   vendor/symfony/cache/Adapter/DoctrineDbalAdapter.phpD  QhD  Z߅      +   vendor/symfony/cache/Adapter/PdoAdapter.phpS  QhS  1M      9   vendor/symfony/cache/Adapter/TagAwareAdapterInterface.php  Qh  ,      2   vendor/symfony/cache/Adapter/FilesystemAdapter.php  Qh  &
      0   vendor/symfony/cache/Adapter/TagAwareAdapter.php,  Qh,  \)      ,   vendor/symfony/cache/Adapter/ApcuAdapter.php{  Qh{  R%      1   vendor/symfony/cache/Adapter/MemcachedAdapter.php5  Qh5  Şg      0   vendor/symfony/cache/Adapter/PhpFilesAdapter.php(  Qh(  KH      9   vendor/symfony/cache/Adapter/TraceableTagAwareAdapter.php  Qh  )瞤      -   vendor/symfony/cache/Adapter/RedisAdapter.php  Qh  
U      -   vendor/symfony/cache/Adapter/ChainAdapter.php$  Qh$  "{V      +   vendor/symfony/cache/PruneableInterface.php  Qh  3Y      )   vendor/symfony/cache/DoctrineProvider.php	  Qh	  Ӥ      1   vendor/symfony/cache/Exception/CacheException.php  Qh  򹃤      ;   vendor/symfony/cache/Exception/InvalidArgumentException.php  Qh  #      1   vendor/symfony/cache/Exception/LogicException.php  Qh  H      ,   vendor/symfony/cache/ResettableInterface.php  Qh  i      9   vendor/symfony/cache/Messenger/EarlyExpirationMessage.php
  Qh
  ߤ      9   vendor/symfony/cache/Messenger/EarlyExpirationHandler.php	  Qh	  rޤ      <   vendor/symfony/cache/Messenger/EarlyExpirationDispatcher.php	  Qh	  ʳg      @   vendor/symfony/cache/DependencyInjection/CachePoolPrunerPass.php  Qh        ?   vendor/symfony/cache/DependencyInjection/CacheCollectorPass.phpC  QhC  /Έ      A   vendor/symfony/cache/DependencyInjection/CachePoolClearerPass.php  Qh  ӡ_      :   vendor/symfony/cache/DependencyInjection/CachePoolPass.php-  Qh-        #   vendor/symfony/cache/Psr16Cache.php!  Qh!  wf      4   vendor/symfony/cache/Marshaller/SodiumMarshaller.php^	  Qh^	  d      7   vendor/symfony/cache/Marshaller/MarshallerInterface.php9  Qh9  F֤      6   vendor/symfony/cache/Marshaller/TagAwareMarshaller.php#  Qh#  
B      5   vendor/symfony/cache/Marshaller/DefaultMarshaller.php  Qh  ]y{      5   vendor/symfony/cache/Marshaller/DeflateMarshaller.php  Qh  $fş      /   vendor/symfony/string/AbstractUnicodeString.phpk  Qhk  >,1X      (   vendor/symfony/string/AbstractString.phpO  QhO  o~      <   vendor/symfony/string/Resources/data/wcswidth_table_zero.php]<  Qh]<  ?p      <   vendor/symfony/string/Resources/data/wcswidth_table_wide.php2  Qh2  ?Y      -   vendor/symfony/string/Resources/functions.php]  Qh]  ې      )   vendor/symfony/string/CodePointString.php  Qh  ~Ѥ      $   vendor/symfony/string/ByteString.phpT<  QhT<  =      4   vendor/symfony/string/Inflector/EnglishInflector.phpB  QhB  ՞̤      3   vendor/symfony/string/Inflector/FrenchInflector.php  Qh  W      6   vendor/symfony/string/Inflector/InflectorInterface.phpC  QhC  Qcc      <   vendor/symfony/string/Exception/InvalidArgumentException.php  Qh  e      4   vendor/symfony/string/Exception/RuntimeException.phpp  Qhp  0ʤ      6   vendor/symfony/string/Exception/ExceptionInterface.phpQ  QhQ  $դ      $   vendor/symfony/string/LazyString.php^  Qh^  {t      '   vendor/symfony/string/UnicodeString.phpH2  QhH2  ˬ      2   vendor/symfony/string/Slugger/SluggerInterface.php  Qh  ^      .   vendor/symfony/string/Slugger/AsciiSlugger.php  Qh  ,sgĤ      .   vendor/symfony/console/Command/HelpCommand.php(  Qh(  ͚      *   vendor/symfony/console/Command/Command.phpP  QhP  舢g      =   vendor/symfony/console/Command/SignalableCommandInterface.php  Qh  GeM      8   vendor/symfony/console/Command/DumpCompletionCommand.php=  Qh=  "i5      0   vendor/symfony/console/Command/LockableTrait.php  Qh        .   vendor/symfony/console/Command/ListCommand.phpB  QhB  Z$F      .   vendor/symfony/console/Command/LazyCommand.php  Qh  n_D      2   vendor/symfony/console/Command/CompleteCommand.phpV!  QhV!  !=N      &   vendor/symfony/console/Input/Input.php  Qh  ܤ      .   vendor/symfony/console/Input/InputArgument.phpu  Qhu   C      4   vendor/symfony/console/Input/InputAwareInterface.php:  Qh:   '      9   vendor/symfony/console/Input/StreamableInputInterface.phpi  Qhi        0   vendor/symfony/console/Input/InputDefinition.php.  Qh.        /   vendor/symfony/console/Input/InputInterface.phpm  Qhm  Ӿ}      *   vendor/symfony/console/Input/ArgvInput.php0  Qh0  ce      +   vendor/symfony/console/Input/ArrayInput.php  Qh        ,   vendor/symfony/console/Input/InputOption.php  Qh  { (      ,   vendor/symfony/console/Input/StringInput.php
  Qh
  H?      -   vendor/symfony/console/Tester/TesterTrait.php  Qh  C      3   vendor/symfony/console/Tester/ApplicationTester.php\
  Qh\
  !t      /   vendor/symfony/console/Tester/CommandTester.php:	  Qh:	  7)a      @   vendor/symfony/console/Tester/Constraint/CommandIsSuccessful.php  Qh  ѧز      9   vendor/symfony/console/Tester/CommandCompletionTester.php   Qh   t5N      0   vendor/symfony/console/Resources/completion.bash
  Qh
  'r      4   vendor/symfony/console/Resources/bin/hiddeninput.exe $  Qh $  v      6   vendor/symfony/console/EventListener/ErrorListener.php  Qh  ׈      (   vendor/symfony/console/ConsoleEvents.php~  Qh~  ~6      +   vendor/symfony/console/Helper/HelperSet.php
  Qh
  AtW      2   vendor/symfony/console/Helper/InputAwareHelper.php  Qh        '   vendor/symfony/console/Helper/Table.phpt  Qht  f:	      7   vendor/symfony/console/Helper/SymfonyQuestionHelper.php  Qh  3T      1   vendor/symfony/console/Helper/FormatterHelper.phpX	  QhX	  uwY      2   vendor/symfony/console/Helper/DescriptorHelper.php	  Qh	  X^}ߤ      ,   vendor/symfony/console/Helper/TableStyle.php1  Qh1        0   vendor/symfony/console/Helper/TableCellStyle.php  Qh  -7      (   vendor/symfony/console/Helper/Dumper.php  Qh  ڮ?9      1   vendor/symfony/console/Helper/HelperInterface.phpN  QhN  ԎD      6   vendor/symfony/console/Helper/DebugFormatterHelper.phpJ  QhJ  G(      3   vendor/symfony/console/Helper/ProgressIndicator.php  Qh  ܤ      0   vendor/symfony/console/Helper/TableSeparator.php  Qh  &      0   vendor/symfony/console/Helper/QuestionHelper.php5L  Qh5L  M=      +   vendor/symfony/console/Helper/TableRows.phpE  QhE  a      (   vendor/symfony/console/Helper/Helper.phpB  QhB  cE      -   vendor/symfony/console/Helper/ProgressBar.php"I  Qh"I  ez      /   vendor/symfony/console/Helper/ProcessHelper.phpz  Qhz  3      +   vendor/symfony/console/Helper/TableCell.php  Qh  .      2   vendor/symfony/console/Event/ConsoleErrorEvent.php  Qh  !3      4   vendor/symfony/console/Event/ConsoleCommandEvent.php)  Qh)        6   vendor/symfony/console/Event/ConsoleTerminateEvent.php"  Qh"  q      -   vendor/symfony/console/Event/ConsoleEvent.phpt  Qht  :[&      3   vendor/symfony/console/Event/ConsoleSignalEvent.php  Qh  b      &   vendor/symfony/console/Application.php.  Qh.  r&:      0   vendor/symfony/console/Completion/Suggestion.php  Qh  bۋ      F   vendor/symfony/console/Completion/Output/CompletionOutputInterface.php  Qh  3O      A   vendor/symfony/console/Completion/Output/BashCompletionOutput.php  Qh  S      5   vendor/symfony/console/Completion/CompletionInput.php
   Qh
   GY8      ;   vendor/symfony/console/Completion/CompletionSuggestions.phpN  QhN  (      0   vendor/symfony/console/Descriptor/Descriptor.php  Qh  k%9      4   vendor/symfony/console/Descriptor/TextDescriptor.phpL1  QhL1  ቜ      9   vendor/symfony/console/Descriptor/DescriptorInterface.php-  Qh-  0M$      3   vendor/symfony/console/Descriptor/XmlDescriptor.php&  Qh&  5/      <   vendor/symfony/console/Descriptor/ApplicationDescription.php  Qh  !      8   vendor/symfony/console/Descriptor/MarkdownDescriptor.php  Qh  4{      4   vendor/symfony/console/Descriptor/JsonDescriptor.php  Qh  j      1   vendor/symfony/console/Output/OutputInterface.php\  Qh\  t      .   vendor/symfony/console/Output/StreamOutput.php  Qh        5   vendor/symfony/console/Output/TrimmedBufferOutput.php<  Qh<  k      /   vendor/symfony/console/Output/ConsoleOutput.php8  Qh8  S.      ,   vendor/symfony/console/Output/NullOutput.php	  Qh	  Ф      0   vendor/symfony/console/Output/BufferedOutput.phpU  QhU  BE$ޤ      8   vendor/symfony/console/Output/ConsoleOutputInterface.php   Qh   _      (   vendor/symfony/console/Output/Output.php>  Qh>  ~8      6   vendor/symfony/console/Output/ConsoleSectionOutput.phpX  QhX  GRä      8   vendor/symfony/console/Question/ConfirmationQuestion.php  Qh  ?uȤ      2   vendor/symfony/console/Question/ChoiceQuestion.php  Qh        ,   vendor/symfony/console/Question/Question.phpY  QhY  9      #   vendor/symfony/console/Terminal.phpB  QhB  ri      ;   vendor/symfony/console/Exception/InvalidOptionException.php  Qh  /el      =   vendor/symfony/console/Exception/InvalidArgumentException.php  Qh  u i      3   vendor/symfony/console/Exception/LogicException.php  Qh  SML      5   vendor/symfony/console/Exception/RuntimeException.php  Qh  *b      ?   vendor/symfony/console/Exception/NamespaceNotFoundException.php  Qh  BLH      7   vendor/symfony/console/Exception/ExceptionInterface.php  Qh  l      =   vendor/symfony/console/Exception/CommandNotFoundException.php  Qh  *      :   vendor/symfony/console/Exception/MissingInputException.php  Qh  Qg;      ,   vendor/symfony/console/Style/OutputStyle.php  Qh        -   vendor/symfony/console/Style/SymfonyStyle.php(9  Qh(9  1Ϥ      /   vendor/symfony/console/Style/StyleInterface.phpP
  QhP
  p+Ť      .   vendor/symfony/console/Attribute/AsCommand.php]  Qh]  ֚      2   vendor/symfony/console/CI/GithubActionReporter.phpK  QhK  9Ό      D   vendor/symfony/console/DependencyInjection/AddConsoleCommandPass.php"  Qh"  䪴      !   vendor/symfony/console/Cursor.php"  Qh"  і      ?   vendor/symfony/console/CommandLoader/CommandLoaderInterface.phpM  QhM  d5      =   vendor/symfony/console/CommandLoader/FactoryCommandLoader.phpE  QhE  =H      ?   vendor/symfony/console/CommandLoader/ContainerCommandLoader.php  Qh  0w      8   vendor/symfony/console/SignalRegistry/SignalRegistry.php1  Qh1  v.      F   vendor/symfony/console/Formatter/WrappableOutputFormatterInterface.php  Qh  }z      4   vendor/symfony/console/Formatter/OutputFormatter.phpz   Qhz   !      8   vendor/symfony/console/Formatter/NullOutputFormatter.phpY  QhY  pɤ      =   vendor/symfony/console/Formatter/OutputFormatterInterface.php7  Qh7  ?V      B   vendor/symfony/console/Formatter/OutputFormatterStyleInterface.php\  Qh\  !-n      =   vendor/symfony/console/Formatter/NullOutputFormatterStyle.php  Qh  佫٤      9   vendor/symfony/console/Formatter/OutputFormatterStyle.php  Qh  }դ      >   vendor/symfony/console/Formatter/OutputFormatterStyleStack.php	  Qh	  d\      /   vendor/symfony/console/Logger/ConsoleLogger.php  Qh  U      3   vendor/symfony/console/SingleCommandApplication.php  Qh  g          vendor/symfony/console/Color.php  Qh  *<!         vendor/bin/var-dump-serverJ  QhJ  8/         vendor/bin/simple-phpunit5  Qh5  ,      0   vendor/phpstan/phpdoc-parser/src/Lexer/Lexer.php  Qh  N      ;   vendor/phpstan/phpdoc-parser/src/Parser/StringUnescaper.phpX	  QhX	  8z      ;   vendor/phpstan/phpdoc-parser/src/Parser/ParserException.phpo  Qho  Fx      9   vendor/phpstan/phpdoc-parser/src/Parser/TokenIterator.phpY#  QhY#  TF      8   vendor/phpstan/phpdoc-parser/src/Parser/PhpDocParser.phpN  QhN  7f      6   vendor/phpstan/phpdoc-parser/src/Parser/TypeParser.phpE  QhE  _A&      ;   vendor/phpstan/phpdoc-parser/src/Parser/ConstExprParser.php  Qh  ڒ      4   vendor/phpstan/phpdoc-parser/src/Printer/Printer.php{  Qh{  b<3      5   vendor/phpstan/phpdoc-parser/src/Printer/DiffElem.phpk  Qhk  6%      3   vendor/phpstan/phpdoc-parser/src/Printer/Differ.php  Qh  %¤      1   vendor/phpstan/phpdoc-parser/src/ParserConfig.php'  Qh'  #6      C   vendor/phpstan/phpdoc-parser/src/Ast/NodeVisitor/CloningVisitor.php  Qh  rʤ      =   vendor/phpstan/phpdoc-parser/src/Ast/Type/InvalidTypeNode.php!  Qh!  Ά`      ;   vendor/phpstan/phpdoc-parser/src/Ast/Type/ArrayTypeNode.php'  Qh'  }      @   vendor/phpstan/phpdoc-parser/src/Ast/Type/ArrayShapeItemNode.phpi  Qhi  6      M   vendor/phpstan/phpdoc-parser/src/Ast/Type/ConditionalTypeForParameterNode.phpN  QhN  "#      ;   vendor/phpstan/phpdoc-parser/src/Ast/Type/UnionTypeNode.php  Qh  C!$H      >   vendor/phpstan/phpdoc-parser/src/Ast/Type/CallableTypeNode.php  Qh  [      B   vendor/phpstan/phpdoc-parser/src/Ast/Type/IntersectionTypeNode.php  Qh  vׯ      :   vendor/phpstan/phpdoc-parser/src/Ast/Type/ThisTypeNode.php   Qh   P+      B   vendor/phpstan/phpdoc-parser/src/Ast/Type/OffsetAccessTypeNode.phpt  Qht  ̤      @   vendor/phpstan/phpdoc-parser/src/Ast/Type/IdentifierTypeNode.phpf  Qhf  \A      =   vendor/phpstan/phpdoc-parser/src/Ast/Type/ObjectShapeNode.php  Qh  5      >   vendor/phpstan/phpdoc-parser/src/Ast/Type/NullableTypeNode.phpn  Qhn  {      <   vendor/phpstan/phpdoc-parser/src/Ast/Type/ArrayShapeNode.phpd  Qhd  oM      G   vendor/phpstan/phpdoc-parser/src/Ast/Type/CallableTypeParameterNode.php  Qh  W      A   vendor/phpstan/phpdoc-parser/src/Ast/Type/ObjectShapeItemNode.php  Qh  ĝ&      6   vendor/phpstan/phpdoc-parser/src/Ast/Type/TypeNode.php   Qh   7P      ;   vendor/phpstan/phpdoc-parser/src/Ast/Type/ConstTypeNode.php  Qh   ܜj      =   vendor/phpstan/phpdoc-parser/src/Ast/Type/GenericTypeNode.phpe  Qhe  >p      A   vendor/phpstan/phpdoc-parser/src/Ast/Type/ConditionalTypeNode.php<  Qh<  nH}S      H   vendor/phpstan/phpdoc-parser/src/Ast/Type/ArrayShapeUnsealedTypeNode.php  Qh  dص      4   vendor/phpstan/phpdoc-parser/src/Ast/NodeVisitor.php	  Qh	  \\      2   vendor/phpstan/phpdoc-parser/src/Ast/Attribute.phpU  QhU  aa      D   vendor/phpstan/phpdoc-parser/src/Ast/ConstExpr/ConstExprNullNode.php	  Qh	  [      @   vendor/phpstan/phpdoc-parser/src/Ast/ConstExpr/ConstExprNode.php   Qh   wu{      E   vendor/phpstan/phpdoc-parser/src/Ast/ConstExpr/ConstExprFloatNode.phpu  Qhu  9訤      D   vendor/phpstan/phpdoc-parser/src/Ast/ConstExpr/ConstExprTrueNode.php	  Qh	  9]      I   vendor/phpstan/phpdoc-parser/src/Ast/ConstExpr/ConstExprArrayItemNode.phpQ  QhQ        E   vendor/phpstan/phpdoc-parser/src/Ast/ConstExpr/ConstExprArrayNode.php  Qh  iBk      N   vendor/phpstan/phpdoc-parser/src/Ast/ConstExpr/DoctrineConstExprStringNode.php  Qh  N      F   vendor/phpstan/phpdoc-parser/src/Ast/ConstExpr/ConstExprStringNode.php  Qh  Ӥ      A   vendor/phpstan/phpdoc-parser/src/Ast/ConstExpr/ConstFetchNode.phpm  Qhm  y      G   vendor/phpstan/phpdoc-parser/src/Ast/ConstExpr/ConstExprIntegerNode.phpw  Qhw  .      E   vendor/phpstan/phpdoc-parser/src/Ast/ConstExpr/ConstExprFalseNode.php  Qh  1      <   vendor/phpstan/phpdoc-parser/src/Ast/AbstractNodeVisitor.php  Qh  R#      7   vendor/phpstan/phpdoc-parser/src/Ast/NodeAttributes.php  Qh  <&D      0   vendor/phpstan/phpdoc-parser/src/Ast/Comment.php  Qh  t      -   vendor/phpstan/phpdoc-parser/src/Ast/Node.php\  Qh\  ,      C   vendor/phpstan/phpdoc-parser/src/Ast/PhpDoc/InvalidTagValueNode.php  Qh  t-      H   vendor/phpstan/phpdoc-parser/src/Ast/PhpDoc/AssertTagMethodValueNode.php  Qh  ˮE      I   vendor/phpstan/phpdoc-parser/src/Ast/PhpDoc/TypelessParamTagValueNode.phpj  Qhj  '      >   vendor/phpstan/phpdoc-parser/src/Ast/PhpDoc/PhpDocTextNode.phpk  Qhk  N      L   vendor/phpstan/phpdoc-parser/src/Ast/PhpDoc/ParamClosureThisTagValueNode.php  Qh  rI      K   vendor/phpstan/phpdoc-parser/src/Ast/PhpDoc/MethodTagValueParameterNode.php  Qh  IG      B   vendor/phpstan/phpdoc-parser/src/Ast/PhpDoc/AssertTagValueNode.php  Qh  bA      E   vendor/phpstan/phpdoc-parser/src/Ast/PhpDoc/TypeAliasTagValueNode.php  Qh  ˤ      C   vendor/phpstan/phpdoc-parser/src/Ast/PhpDoc/ExtendsTagValueNode.phpe  Qhe  3      D   vendor/phpstan/phpdoc-parser/src/Ast/PhpDoc/PropertyTagValueNode.php  Qh  @6ɤ      ?   vendor/phpstan/phpdoc-parser/src/Ast/PhpDoc/VarTagValueNode.php  Qh  	+      =   vendor/phpstan/phpdoc-parser/src/Ast/PhpDoc/PhpDocTagNode.php  Qh        U   vendor/phpstan/phpdoc-parser/src/Ast/PhpDoc/ParamLaterInvokedCallableTagValueNode.php^  Qh^  l馤      F   vendor/phpstan/phpdoc-parser/src/Ast/PhpDoc/DeprecatedTagValueNode.php  Qh  I[      B   vendor/phpstan/phpdoc-parser/src/Ast/PhpDoc/PhpDocTagValueNode.php   Qh   fc:      F   vendor/phpstan/phpdoc-parser/src/Ast/PhpDoc/ImplementsTagValueNode.phph  Qhh        B   vendor/phpstan/phpdoc-parser/src/Ast/PhpDoc/SealedTagValueNode.phpO  QhO  +y      M   vendor/phpstan/phpdoc-parser/src/Ast/PhpDoc/RequireImplementsTagValueNode.phpZ  QhZ  )      B   vendor/phpstan/phpdoc-parser/src/Ast/PhpDoc/MethodTagValueNode.php   Qh   &Ml      ?   vendor/phpstan/phpdoc-parser/src/Ast/PhpDoc/PhpDocChildNode.php   Qh         A   vendor/phpstan/phpdoc-parser/src/Ast/PhpDoc/MixinTagValueNode.phpN  QhN  k|      J   vendor/phpstan/phpdoc-parser/src/Ast/PhpDoc/RequireExtendsTagValueNode.phpW  QhW  P      I   vendor/phpstan/phpdoc-parser/src/Ast/PhpDoc/Doctrine/DoctrineArgument.phpD  QhD  mj      K   vendor/phpstan/phpdoc-parser/src/Ast/PhpDoc/Doctrine/DoctrineAnnotation.php  Qh  s      M   vendor/phpstan/phpdoc-parser/src/Ast/PhpDoc/Doctrine/DoctrineTagValueNode.php  Qh        J   vendor/phpstan/phpdoc-parser/src/Ast/PhpDoc/Doctrine/DoctrineArrayItem.php  Qh  HL      F   vendor/phpstan/phpdoc-parser/src/Ast/PhpDoc/Doctrine/DoctrineArray.php+  Qh+  >].q      K   vendor/phpstan/phpdoc-parser/src/Ast/PhpDoc/TypeAliasImportTagValueNode.php1  Qh1  :j_      B   vendor/phpstan/phpdoc-parser/src/Ast/PhpDoc/ThrowsTagValueNode.phpO  QhO  '[      J   vendor/phpstan/phpdoc-parser/src/Ast/PhpDoc/AssertTagPropertyValueNode.php(  Qh(  s+      [   vendor/phpstan/phpdoc-parser/src/Ast/PhpDoc/ParamImmediatelyInvokedCallableTagValueNode.phpd  Qhd  H~r      B   vendor/phpstan/phpdoc-parser/src/Ast/PhpDoc/ReturnTagValueNode.phpO  QhO  Cm      A   vendor/phpstan/phpdoc-parser/src/Ast/PhpDoc/ParamTagValueNode.php  Qh  :c'      C   vendor/phpstan/phpdoc-parser/src/Ast/PhpDoc/SelfOutTagValueNode.phpR  QhR  "dZ      D   vendor/phpstan/phpdoc-parser/src/Ast/PhpDoc/ParamOutTagValueNode.php  Qh  6~      :   vendor/phpstan/phpdoc-parser/src/Ast/PhpDoc/PhpDocNode.php(  Qh(  Ӟ,      V   vendor/phpstan/phpdoc-parser/src/Ast/PhpDoc/PureUnlessCallableIsImpureTagValueNode.php_  Qh_  \G      C   vendor/phpstan/phpdoc-parser/src/Ast/PhpDoc/GenericTagValueNode.php  Qh        @   vendor/phpstan/phpdoc-parser/src/Ast/PhpDoc/UsesTagValueNode.phpb  Qhb  'h      D   vendor/phpstan/phpdoc-parser/src/Ast/PhpDoc/TemplateTagValueNode.php  Qh        6   vendor/phpstan/phpdoc-parser/src/Ast/NodeTraverser.php   Qh   ݓf      &   vendor/psr/log/Psr/Log/LoggerTrait.phpW  QhW  Wj      3   vendor/psr/log/Psr/Log/InvalidArgumentException.php`   Qh`    X1      *   vendor/psr/log/Psr/Log/LoggerInterface.php*  Qh*  1b!q      /   vendor/psr/log/Psr/Log/LoggerAwareInterface.php)  Qh)  j      #   vendor/psr/log/Psr/Log/LogLevel.phpP  QhP        )   vendor/psr/log/Psr/Log/AbstractLogger.php   Qh   G      +   vendor/psr/log/Psr/Log/LoggerAwareTrait.php  Qh  Q'      *   vendor/psr/log/Psr/Log/Test/TestLogger.php  Qh         )   vendor/psr/log/Psr/Log/Test/DummyTest.php   Qh   HTg      3   vendor/psr/log/Psr/Log/Test/LoggerInterfaceTest.php)  Qh)  I\      %   vendor/psr/log/Psr/Log/NullLogger.php  Qh  I      7   vendor/psr/container/src/NotFoundExceptionInterface.php   Qh   >悤      /   vendor/psr/container/src/ContainerInterface.php  Qh  j.      8   vendor/psr/container/src/ContainerExceptionInterface.php   Qh   ^33      '   vendor/psr/cache/src/CacheException.php   Qh   K      1   vendor/psr/cache/src/InvalidArgumentException.php+  Qh+  Gb<      /   vendor/psr/cache/src/CacheItemPoolInterface.php%  Qh%  l      +   vendor/psr/cache/src/CacheItemInterface.php  Qh  4z=         Sdk/Parser.phpQ	  QhQ	  q {      $   Sdk/Exception/ExceptionInterface.phpA  QhA  4      $   Sdk/Exception/ApiClientException.php  Qh  o      $   Sdk/Exception/ApiParserException.phpu  Qhu  w{      $   Sdk/Exception/ApiServerException.phpu  Qhu  6K&         Sdk/Api.php #  Qh #  W         Sdk/Model/Analyses.phpa  Qha  Br         Sdk/Model/Violation.php4  Qh4  ƚa         Sdk/Model/Project.phpY  QhY  a*?         Sdk/Model/Link.php-  Qh-  N         Sdk/Model/Violations.php/  Qh/  x         Sdk/Model/Projects.php  Qh  (G         Sdk/Model/Error.php  Qh  P         Sdk/Model/Analysis.php[  Qh[  kx         bin/insight  Qh  搤         Cli/Configuration.php  Qh  ByH,      !   Cli/Command/SelfUpdateCommand.php	  Qh	  |j         Cli/Command/AnalyzeCommand.phpz  Qhz  Es         Cli/Command/AnalysisCommand.php  Qh            Cli/Command/ConfigureCommand.phpe  Qhe  z      *   Cli/Command/NeedConfigurationInterface.phpA  QhA  n         Cli/Command/ProjectsCommand.php!  Qh!  t      "   Cli/Helper/FailConditionHelper.php
  Qh
  I         Cli/Helper/DescriptorHelper.php  Qh  lG¤      "   Cli/Helper/ConfigurationHelper.php  Qh  7d         Cli/Application.php  Qh  Dz9k      !   Cli/Descriptor/TextDescriptor.php  Qh            Cli/Descriptor/XmlDescriptor.php  Qh  q      !   Cli/Descriptor/JsonDescriptor.php  Qh  Lh          Cli/Descriptor/PmdDescriptor.php	  Qh	  ͤ      %   Cli/Descriptor/AbstractDescriptor.php  Qh  K[}      <?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;
        }
    }
    throw new RuntimeException($err);
}

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

return ComposerAutoloaderInitb258b286557a5c443ca34ff495d81cd6::getLoader();
<?php

declare(strict_types=1);

namespace Metadata;

/**
 * Base class for property metadata.
 *
 * This class is intended to be extended to add your application specific
 * properties, and flags.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class PropertyMetadata implements \Serializable
{
    use SerializationHelper;

    /**
     * @var string
     */
    public $class;

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

    public function __construct(string $class, string $name)
    {
        $this->class = $class;
        $this->name = $name;
    }

    protected function serializeToArray(): array
    {
        return [
            $this->class,
            $this->name,
        ];
    }

    protected function unserializeFromArray(array $data): void
    {
        [$this->class, $this->name] = $data;
    }
}
<?php

declare(strict_types=1);

namespace Metadata;

/**
 * Interface for Metadata Factory implementations.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
interface MetadataFactoryInterface
{
    /**
     * Returns the gathered metadata for the given class name.
     *
     * If the drivers return instances of MergeableClassMetadata, these will be
     * merged prior to returning. Otherwise, all metadata for the inheritance
     * hierarchy will be returned as ClassHierarchyMetadata unmerged.
     *
     * If no metadata is available, null is returned.
     *
     * @return ClassHierarchyMetadata|MergeableClassMetadata|null
     *
     * @phpcsSuppress SlevomatCodingStandard.TypeHints.ReturnTypeHint.MissingNativeTypeHint
     * @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.UselessReturnAnnotation
     */
    public function getMetadataForClass(string $className);
}
<?php

declare(strict_types=1);

namespace Metadata;

/**
 * Represents the metadata for the entire class hierarchy.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class ClassHierarchyMetadata
{
    /**
     * @var ClassMetadata[]
     */
    public $classMetadata = [];

    public function addClassMetadata(ClassMetadata $metadata): void
    {
        $this->classMetadata[$metadata->name] = $metadata;
    }

    public function getRootClassMetadata(): ?ClassMetadata
    {
        return reset($this->classMetadata);
    }

    public function getOutsideClassMetadata(): ?ClassMetadata
    {
        return end($this->classMetadata);
    }

    public function isFresh(int $timestamp): bool
    {
        foreach ($this->classMetadata as $metadata) {
            if (!$metadata->isFresh($timestamp)) {
                return false;
            }
        }

        return true;
    }
}
<?php

declare(strict_types=1);

namespace Metadata;

/**
 * Base class for class metadata.
 *
 * This class is intended to be extended to add your own application specific
 * properties, and flags.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class ClassMetadata implements \Serializable
{
    use SerializationHelper;

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

    /**
     * @var MethodMetadata[]
     */
    public $methodMetadata = [];

    /**
     * @var PropertyMetadata[]
     */
    public $propertyMetadata = [];

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

    /**
     * @var int
     */
    public $createdAt;

    public function __construct(string $name)
    {
        $this->name = $name;
        $this->createdAt = time();
    }

    public function addMethodMetadata(MethodMetadata $metadata): void
    {
        $this->methodMetadata[$metadata->name] = $metadata;
    }

    public function addPropertyMetadata(PropertyMetadata $metadata): void
    {
        $this->propertyMetadata[$metadata->name] = $metadata;
    }

    public function isFresh(?int $timestamp = null): bool
    {
        if (null === $timestamp) {
            $timestamp = $this->createdAt;
        }

        foreach ($this->fileResources as $filepath) {
            if (!file_exists($filepath)) {
                return false;
            }

            if ($timestamp < filemtime($filepath)) {
                return false;
            }
        }

        return true;
    }

    protected function serializeToArray(): array
    {
        return [
            $this->name,
            $this->methodMetadata,
            $this->propertyMetadata,
            $this->fileResources,
            $this->createdAt,
        ];
    }

    protected function unserializeFromArray(array $data): void
    {
        [
            $this->name,
            $this->methodMetadata,
            $this->propertyMetadata,
            $this->fileResources,
            $this->createdAt,
        ] = $data;
    }
}
<?php

declare(strict_types=1);

namespace Metadata;

class MergeableClassMetadata extends ClassMetadata implements MergeableInterface
{
    public function merge(MergeableInterface $object): void
    {
        if (!$object instanceof MergeableClassMetadata) {
            throw new \InvalidArgumentException('$object must be an instance of MergeableClassMetadata.');
        }

        $this->name = $object->name;
        $this->methodMetadata = array_merge($this->methodMetadata, $object->methodMetadata);
        $this->propertyMetadata = array_merge($this->propertyMetadata, $object->propertyMetadata);
        $this->fileResources = array_merge($this->fileResources, $object->fileResources);

        if ($object->createdAt < $this->createdAt) {
            $this->createdAt = $object->createdAt;
        }
    }
}
<?php

declare(strict_types=1);

namespace Metadata;

interface MergeableInterface
{
    public function merge(MergeableInterface $object): void;
}
<?php

declare(strict_types=1);

namespace Metadata;

use Metadata\Cache\CacheInterface;
use Metadata\Driver\AdvancedDriverInterface;
use Metadata\Driver\DriverInterface;

class MetadataFactory implements AdvancedMetadataFactoryInterface
{
    /**
     * @var DriverInterface
     */
    private $driver;

    /**
     * @var CacheInterface
     */
    private $cache;

    /**
     * @var ClassMetadata[]
     */
    private $loadedMetadata = [];

    /**
     * @var ClassMetadata[]
     */
    private $loadedClassMetadata = [];

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

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

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

    public function __construct(DriverInterface $driver, ?string $hierarchyMetadataClass = 'Metadata\ClassHierarchyMetadata', bool $debug = false)
    {
        $this->driver = $driver;
        $this->hierarchyMetadataClass = $hierarchyMetadataClass;
        $this->debug = $debug;
    }

    public function setIncludeInterfaces(bool $include): void
    {
        $this->includeInterfaces = $include;
    }

    public function setCache(CacheInterface $cache): void
    {
        $this->cache = $cache;
    }

    /**
     * {@inheritDoc}
     */
    public function getMetadataForClass(string $className)
    {
        if (isset($this->loadedMetadata[$className])) {
            return $this->filterNullMetadata($this->loadedMetadata[$className]);
        }

        $metadata = null;
        foreach ($this->getClassHierarchy($className) as $class) {
            if (isset($this->loadedClassMetadata[$name = $class->getName()])) {
                if (null !== $classMetadata = $this->filterNullMetadata($this->loadedClassMetadata[$name])) {
                    $this->addClassMetadata($metadata, $classMetadata);
                }

                continue;
            }

            // check the cache
            if (null !== $this->cache) {
                if (($classMetadata = $this->cache->load($class->getName())) instanceof NullMetadata) {
                    $this->loadedClassMetadata[$name] = $classMetadata;
                    continue;
                }

                if (null !== $classMetadata) {
                    if (!$classMetadata instanceof ClassMetadata) {
                        throw new \LogicException(sprintf(
                            'The cache must return instances of ClassMetadata for class %s, but got %s.',
                            $className,
                            var_export($classMetadata, true)
                        ));
                    }

                    if ($this->debug && !$classMetadata->isFresh()) {
                        $this->cache->evict($classMetadata->name);
                    } else {
                        $this->loadedClassMetadata[$name] = $classMetadata;
                        $this->addClassMetadata($metadata, $classMetadata);
                        continue;
                    }
                }
            }

            // load from source
            if (null !== $classMetadata = $this->driver->loadMetadataForClass($class)) {
                $this->loadedClassMetadata[$name] = $classMetadata;
                $this->addClassMetadata($metadata, $classMetadata);

                if (null !== $this->cache) {
                    $this->cache->put($classMetadata);
                }

                continue;
            }

            if (null !== $this->cache && !$this->debug) {
                $this->cache->put(new NullMetadata($class->getName()));
            }
        }

        if (null === $metadata) {
            $metadata = new NullMetadata($className);
        }

        return $this->filterNullMetadata($this->loadedMetadata[$className] = $metadata);
    }

    /**
     * {@inheritDoc}
     */
    public function getAllClassNames(): array
    {
        if (!$this->driver instanceof AdvancedDriverInterface) {
            throw new \RuntimeException(
                sprintf('Driver "%s" must be an instance of "AdvancedDriverInterface".', get_class($this->driver))
            );
        }

        return $this->driver->getAllClassNames();
    }

    /**
     * @param MergeableInterface|ClassHierarchyMetadata $metadata
     */
    private function addClassMetadata(&$metadata, ClassMetadata $toAdd): void
    {
        if ($toAdd instanceof MergeableInterface) {
            if (null === $metadata) {
                $metadata = clone $toAdd;
            } else {
                $metadata->merge($toAdd);
            }
        } else {
            if (null === $metadata) {
                $class = $this->hierarchyMetadataClass;
                $metadata = new $class();
            }

            $metadata->addClassMetadata($toAdd);
        }
    }

    /**
     * @return \ReflectionClass[]
     */
    private function getClassHierarchy(string $class): array
    {
        $classes = [];
        $refl = new \ReflectionClass($class);

        do {
            $classes[] = $refl;
            $refl = $refl->getParentClass();
        } while (false !== $refl);

        $classes = array_reverse($classes, false);

        if (!$this->includeInterfaces) {
            return $classes;
        }

        $addedInterfaces = [];
        $newHierarchy = [];

        foreach ($classes as $class) {
            foreach ($class->getInterfaces() as $interface) {
                if (isset($addedInterfaces[$interface->getName()])) {
                    continue;
                }

                $addedInterfaces[$interface->getName()] = true;

                $newHierarchy[] = $interface;
            }

            $newHierarchy[] = $class;
        }

        return $newHierarchy;
    }

    /**
     * @param ClassMetadata|ClassHierarchyMetadata|MergeableInterface $metadata
     *
     * @return ClassMetadata|ClassHierarchyMetadata|MergeableInterface
     */
    private function filterNullMetadata($metadata = null)
    {
        return !$metadata instanceof NullMetadata ? $metadata : null;
    }
}
<?php

declare(strict_types=1);

namespace Metadata;

/**
 * Represents the metadata for a class that has not metadata.
 *
 * @author Adrien Brault <adrien.brault@gmail.com>
 */
class NullMetadata extends ClassMetadata
{
}
<?php

declare(strict_types=1);

namespace Metadata\Driver;

use Metadata\ClassMetadata;
use Psr\Container\ContainerInterface as PsrContainerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

class LazyLoadingDriver implements DriverInterface
{
    /**
     * @var ContainerInterface|PsrContainerInterface
     */
    private $container;

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

    /**
     * @param ContainerInterface|PsrContainerInterface $container
     */
    public function __construct($container, string $realDriverId)
    {
        if (!$container instanceof PsrContainerInterface && !$container instanceof ContainerInterface) {
            throw new \InvalidArgumentException(sprintf('The container must be an instance of %s or %s (%s given).', PsrContainerInterface::class, ContainerInterface::class, \is_object($container) ? \get_class($container) : \gettype($container)));
        }

        $this->container = $container;
        $this->realDriverId = $realDriverId;
    }

    public function loadMetadataForClass(\ReflectionClass $class): ?ClassMetadata
    {
        return $this->container->get($this->realDriverId)->loadMetadataForClass($class);
    }
}
<?php

declare(strict_types=1);

namespace Metadata\Driver;

/**
 * Forces advanced logic on a file locator.
 *
 * @author Jordan Stout <j@jrdn.org>
 */
interface AdvancedFileLocatorInterface extends FileLocatorInterface
{
    /**
     * Finds all possible metadata files.*
     *
     * @return string[]
     */
    public function findAllClasses(string $extension): array;
}
<?php

declare(strict_types=1);

namespace Metadata\Driver;

use Metadata\ClassMetadata;

/**
 * Base file driver implementation.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
abstract class AbstractFileDriver implements AdvancedDriverInterface
{
    /**
     * @var FileLocatorInterface|FileLocator
     */
    private $locator;

    public function __construct(FileLocatorInterface $locator)
    {
        $this->locator = $locator;
    }

    public function loadMetadataForClass(\ReflectionClass $class): ?ClassMetadata
    {
        if (null === $path = $this->locator->findFileForClass($class, $this->getExtension())) {
            return null;
        }

        return $this->loadMetadataFromFile($class, $path);
    }

    /**
     * {@inheritDoc}
     */
    public function getAllClassNames(): array
    {
        if (!$this->locator instanceof AdvancedFileLocatorInterface) {
            throw new \RuntimeException(sprintf('Locator "%s" must be an instance of "AdvancedFileLocatorInterface".', get_class($this->locator)));
        }

        return $this->locator->findAllClasses($this->getExtension());
    }

    /**
     * Parses the content of the file, and converts it to the desired metadata.
     */
    abstract protected function loadMetadataFromFile(\ReflectionClass $class, string $file): ?ClassMetadata;

    /**
     * Returns the extension of the file.
     */
    abstract protected function getExtension(): string;
}
<?php

declare(strict_types=1);

namespace Metadata\Driver;

/**
 * Forces advanced logic to drivers.
 *
 * @author Jordan Stout <j@jrdn.org>
 */
interface AdvancedDriverInterface extends DriverInterface
{
    /**
     * Gets all the metadata class names known to this driver.
     *
     * @return string[]
     */
    public function getAllClassNames(): array;
}
<?php

declare(strict_types=1);

namespace Metadata\Driver;

interface TraceableFileLocatorInterface extends FileLocatorInterface
{
    /**
     * Finds all possible metadata files for a class
     *
     * @return string[]
     */
    public function getPossibleFilesForClass(\ReflectionClass $class, string $extension): array;
}
<?php

declare(strict_types=1);

namespace Metadata\Driver;

interface FileLocatorInterface
{
    public function findFileForClass(\ReflectionClass $class, string $extension): ?string;
}
<?php

declare(strict_types=1);

namespace Metadata\Driver;

class FileLocator implements AdvancedFileLocatorInterface, TraceableFileLocatorInterface
{
    /**
     * @var string[]
     */
    private $dirs;

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

    /**
     * @return array<string, bool>
     */
    public function getPossibleFilesForClass(\ReflectionClass $class, string $extension): array
    {
        $possibleFiles = [];
        foreach ($this->dirs as $prefix => $dir) {
            if ('' !== $prefix && 0 !== strpos($class->getNamespaceName(), $prefix)) {
                continue;
            }

            $len = '' === $prefix ? 0 : strlen($prefix) + 1;
            $path = $dir . '/' . str_replace('\\', '.', substr($class->name, $len)) . '.' . $extension;
            $existsPath = file_exists($path);
            $possibleFiles[$path] = $existsPath;
            if ($existsPath) {
                return $possibleFiles;
            }
        }

        return $possibleFiles;
    }

    public function findFileForClass(\ReflectionClass $class, string $extension): ?string
    {
        foreach ($this->getPossibleFilesForClass($class, $extension) as $path => $existsPath) {
            if ($existsPath) {
                return $path;
            }
        }

        return null;
    }

    /**
     * {@inheritDoc}
     */
    public function findAllClasses(string $extension): array
    {
        $classes = [];
        foreach ($this->dirs as $prefix => $dir) {
            /** @var \RecursiveIteratorIterator|\SplFileInfo[] $iterator */
            $iterator = new \RecursiveIteratorIterator(
                new \RecursiveDirectoryIterator($dir),
                \RecursiveIteratorIterator::LEAVES_ONLY
            );
            $nsPrefix = '' !== $prefix ? $prefix . '\\' : '';
            foreach ($iterator as $file) {
                if (($fileName = $file->getBasename('.' . $extension)) === $file->getBasename()) {
                    continue;
                }

                $classes[] = $nsPrefix . str_replace('.', '\\', $fileName);
            }
        }

        return $classes;
    }
}
<?php

declare(strict_types=1);

namespace Metadata\Driver;

use Metadata\ClassMetadata;

interface DriverInterface
{
    public function loadMetadataForClass(\ReflectionClass $class): ?ClassMetadata;
}
<?php

declare(strict_types=1);

namespace Metadata\Driver;

use Metadata\ClassMetadata;

final class DriverChain implements AdvancedDriverInterface
{
    /**
     * @var DriverInterface[]
     */
    private $drivers;

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

    public function addDriver(DriverInterface $driver): void
    {
        $this->drivers[] = $driver;
    }

    public function loadMetadataForClass(\ReflectionClass $class): ?ClassMetadata
    {
        foreach ($this->drivers as $driver) {
            if (null !== $metadata = $driver->loadMetadataForClass($class)) {
                return $metadata;
            }
        }

        return null;
    }

    /**
     * {@inheritDoc}
     */
    public function getAllClassNames(): array
    {
        $classes = [];
        foreach ($this->drivers as $driver) {
            if (!$driver instanceof AdvancedDriverInterface) {
                throw new \RuntimeException(
                    sprintf(
                        'Driver "%s" must be an instance of "AdvancedDriverInterface" to use ' .
                        '"DriverChain::getAllClassNames()".',
                        get_class($driver)
                    )
                );
            }

            $driverClasses = $driver->getAllClassNames();
            if (!empty($driverClasses)) {
                $classes = array_merge($classes, $driverClasses);
            }
        }

        return $classes;
    }
}
<?php

declare(strict_types=1);

namespace Metadata;

/**
 * Interface for advanced Metadata Factory implementations.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 * @author Jordan Stout <j@jrdn.org>
 */
interface AdvancedMetadataFactoryInterface extends MetadataFactoryInterface
{
    /**
     * Gets all the possible classes.
     *
     * @return string[]
     *
     * @throws \RuntimeException When driver does not an advanced driver.
     */
    public function getAllClassNames(): array;
}
<?php

declare(strict_types=1);

namespace Metadata;

trait SerializationHelper
{
    /**
     * @deprecated Use serializeToArray
     *
     * @return string
     */
    public function serialize()
    {
        return serialize($this->serializeToArray());
    }

    /**
     * @deprecated Use unserializeFromArray
     *
     * @param string $str
     *
     * @return void
     */
    public function unserialize($str)
    {
        $this->unserializeFromArray(unserialize($str));
    }

    public function __serialize(): array
    {
        return [$this->serialize()];
    }

    public function __unserialize(array $data): void
    {
        $this->unserialize($data[0]);
    }
}
<?php

declare(strict_types=1);

namespace Metadata;

/**
 * Base class for method metadata.
 *
 * This class is intended to be extended to add your application specific
 * properties, and flags.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 * @property $reflection
 */
class MethodMetadata implements \Serializable
{
    use SerializationHelper;

    /**
     * @var string
     */
    public $class;

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

    /**
     * @var \ReflectionMethod
     */
    private $reflection;

    public function __construct(string $class, string $name)
    {
        $this->class = $class;
        $this->name = $name;
    }

    /**
     * @param mixed[] $args
     *
     * @return mixed
     */
    public function invoke(object $obj, array $args = [])
    {
        return $this->getReflection()->invokeArgs($obj, $args);
    }

    /**
     * @return mixed
     */
    public function __get(string $propertyName)
    {
        if ('reflection' === $propertyName) {
            return $this->getReflection();
        }

        return $this->$propertyName;
    }

    /**
     * @param mixed $value
     */
    public function __set(string $propertyName, $value): void
    {
        $this->$propertyName = $value;
    }

    private function getReflection(): \ReflectionMethod
    {
        if (null === $this->reflection) {
            $this->reflection = new \ReflectionMethod($this->class, $this->name);
            $this->reflection->setAccessible(true);
        }

        return $this->reflection;
    }

    protected function serializeToArray(): array
    {
        return [$this->class, $this->name];
    }

    protected function unserializeFromArray(array $data): void
    {
        [$this->class, $this->name] = $data;
    }
}
<?php

declare(strict_types=1);

namespace Metadata\Cache;

/**
 * @author Alexander Strizhak <gam6itko@gmail.com>
 */
interface ClearableCacheInterface
{
    /**
     * Clear all classes metadata from the cache.
     */
    public function clear(): bool;
}
<?php

declare(strict_types=1);

namespace Metadata\Cache;

use Metadata\ClassMetadata;

interface CacheInterface
{
    /**
     * Loads a class metadata instance from the cache
     */
    public function load(string $class): ?ClassMetadata;

    /**
     * Puts a class metadata instance into the cache
     */
    public function put(ClassMetadata $metadata): void;

    /**
     * Evicts the class metadata for the given class from the cache.
     */
    public function evict(string $class): void;
}
<?php

declare(strict_types=1);

namespace Metadata\Cache;

use Metadata\ClassMetadata;

class FileCache implements CacheInterface, ClearableCacheInterface
{
    /**
     * @var string
     */
    private $dir;

    public function __construct(string $dir)
    {
        if (!is_dir($dir) && false === @mkdir($dir, 0777, true)) {
            throw new \InvalidArgumentException(sprintf('Can\'t create directory for cache at "%s"', $dir));
        }

        $this->dir = rtrim($dir, '\\/');
    }

    public function load(string $class): ?ClassMetadata
    {
        $path = $this->getCachePath($class);
        if (!is_readable($path)) {
            return null;
        }

        try {
            $metadata = include $path;
            if ($metadata instanceof ClassMetadata) {
                return $metadata;
            }

            // if the file does not return anything, the return value is integer `1`.
        } catch (\Error $e) {
            // ignore corrupted cache
        }

        return null;
    }

    public function put(ClassMetadata $metadata): void
    {
        if (!is_writable($this->dir)) {
            throw new \InvalidArgumentException(sprintf('The directory "%s" is not writable.', $this->dir));
        }

        $path = $this->getCachePath($metadata->name);
        if (!is_writable(dirname($path))) {
            throw new \RuntimeException(sprintf('Cache file "%s" is not writable.', $path));
        }

        $tmpFile = tempnam($this->dir, 'metadata-cache');
        if (false === $tmpFile) {
            $this->evict($metadata->name);

            return;
        }

        $data = '<?php return unserialize(' . var_export(serialize($metadata), true) . ');';
        $bytesWritten = file_put_contents($tmpFile, $data);
        // use strlen and not mb_strlen. if there is utf8 in the code, it also writes more bytes.
        if ($bytesWritten !== strlen($data)) {
            @unlink($tmpFile);
            // also evict the cache to not use an outdated version.
            $this->evict($metadata->name);

            return;
        }

        // Let's not break filesystems which do not support chmod.
        @chmod($tmpFile, 0666 & ~umask());

        $this->renameFile($tmpFile, $path);
    }

    /**
     * Renames a file with fallback for windows
     */
    private function renameFile(string $source, string $target): void
    {
        if (false === @rename($source, $target)) {
            if (defined('PHP_WINDOWS_VERSION_BUILD')) {
                if (false === copy($source, $target)) {
                    throw new \RuntimeException(sprintf('(WIN) Could not write new cache file to %s.', $target));
                }

                if (false === unlink($source)) {
                    throw new \RuntimeException(sprintf('(WIN) Could not delete temp cache file to %s.', $source));
                }
            } else {
                throw new \RuntimeException(sprintf('Could not write new cache file to %s.', $target));
            }
        }
    }

    public function evict(string $class): void
    {
        $path = $this->getCachePath($class);
        if (file_exists($path)) {
            @unlink($path);
        }
    }

    public function clear(): bool
    {
        $result = true;
        $files = glob($this->dir . '/*.cache.php');
        foreach ($files as $file) {
            if (is_file($file)) {
                $result = $result && @unlink($file);
            }
        }

        return $result;
    }

    /**
     * This function computes the cache file path.
     *
     * If anonymous class is to be cached, it contains invalid path characters that need to be removed/replaced
     * Example of anonymous class name: class@anonymous\x00/app/src/Controller/DefaultController.php0x7f82a7e026ec
     */
    private function getCachePath(string $key): string
    {
        $fileName = str_replace(['\\', "\0", '@', '/', '$', '{', '}', ':'], '-', $key);

        return $this->dir . '/' . $fileName . '.cache.php';
    }
}
<?php

declare(strict_types=1);

namespace Metadata\Cache;

use Doctrine\Common\Cache\Cache;
use Metadata\ClassMetadata;

/**
 * @author Henrik Bjornskov <henrik@bjrnskov.dk>
 */
class DoctrineCacheAdapter implements CacheInterface, ClearableCacheInterface
{
    /**
     * @var string
     */
    private $prefix;
    /**
     * @var Cache
     */
    private $cache;

    public function __construct(string $prefix, Cache $cache)
    {
        $this->prefix = $prefix;
        $this->cache = $cache;
    }

    public function load(string $class): ?ClassMetadata
    {
        $cache = $this->cache->fetch($this->prefix . $class);

        return false === $cache ? null : $cache;
    }

    public function put(ClassMetadata $metadata): void
    {
        $this->cache->save($this->prefix . $metadata->name, $metadata);
    }

    public function evict(string $class): void
    {
        $this->cache->delete($this->prefix . $class);
    }

    public function clear(): bool
    {
        if (method_exists($this->cache, 'deleteAll')) { // or $this->cache instanceof ClearableCache
            return call_user_func([$this->cache, 'deleteAll']);
        }

        return false;
    }
}
<?php

declare(strict_types=1);

namespace Metadata\Cache;

use Metadata\ClassMetadata;
use Psr\Cache\CacheItemPoolInterface;

class PsrCacheAdapter implements CacheInterface, ClearableCacheInterface
{
    /**
     * @var string
     */
    private $prefix;

    /**
     * @var CacheItemPoolInterface
     */
    private $pool;

    /**
     * @var CacheItemPoolInterface
     */
    private $lastItem;

    public function __construct(string $prefix, CacheItemPoolInterface $pool)
    {
        $this->prefix = $prefix;
        $this->pool = $pool;
    }

    public function load(string $class): ?ClassMetadata
    {
        $this->lastItem = $this->pool->getItem($this->sanitizeCacheKey($this->prefix . $class));

        return $this->lastItem->get();
    }

    public function put(ClassMetadata $metadata): void
    {
        $key = $this->sanitizeCacheKey($this->prefix . $metadata->name);

        if (null === $this->lastItem || $this->lastItem->getKey() !== $key) {
            $this->lastItem = $this->pool->getItem($key);
        }

        $this->pool->save($this->lastItem->set($metadata));
    }

    public function evict(string $class): void
    {
        $this->pool->deleteItem($this->sanitizeCacheKey($this->prefix . $class));
    }

    public function clear(): bool
    {
        return $this->pool->clear();
    }

    /**
     * If anonymous class is to be cached, it contains invalid path characters that need to be removed/replaced
     * Example of anonymous class name: class@anonymous\x00/app/src/Controller/DefaultController.php0x7f82a7e026ec
     */
    private function sanitizeCacheKey(string $key): string
    {
        return str_replace(['\\', "\0", '@', '/', '$', '{', '}', ':'], '-', $key);
    }
}
{
  "runner.bootstrap": "tests/bootstrap.php",
  "runner.iterations": 3,
  "runner.revs": 1,
  "report.generators": {
    "memory": {
      "extends": "aggregate",
      "cols": [ 
        "benchmark",
        "mem_peak"
      ]
    }
  }

}
PNG

   IHDR            bKGD      	pHYs        tIME

 9  IDATxy\E=3d# H"	H @%a{7YB  (r"Ra^a_BAdU&dO&GU˱tY<dsΩ:~[}}P@
(P@
(P@Q* zFwVh	N>
XjzE[1ѓ/u@`=` 0i$o ?+f &|Ɠ/)л۝f	sBs%u[́#׈c	5zEcR-6`3`p-kd5zuA]c(%=;0/$gEH5k/ uT9`&`'ыIzr$HY_aZcCB`O`Hϲx}qA^dF<k++4F+pw׌fZW5F)5+*4MNk5q$p1p`w6ZiifrvR#+	fl%}wR[s
8(8}o5)<Lk҂ [_Nc_gT8ۀKSsƻ}5FIQBk/z_(R6zr)
yϽ nXX\?#(NT5;BӀ26[ϻ&idl
)۹;IͪP~m>/$im B-9rkq,	v{YR 19B!窔vw{yq5*VV9xE[Oϣ&i;1D$kA)@ 	N$KIPkEp~]YWùVO'0f	'\*:"o$ɵ%< G[k]PF[99|eHk[-9$EָZ0pGٳU<ǸVhR̋/Җ7rx7[ܩZ5zau5zAHPAJ*: XuEn;k	D$VBe83HKQ^ Bqx?f8#iaq	%: u"h8kz0BnF̹wnp%ԩE&WP_ԭA!НNj@VM}GJ?5Z ]a4j ##-Y%{T
6n^y6qqjLII܏֋#ѳ39Dm	ۓtZBYs3O/.U=
yOvnmMcm\Bt#-">W^,εF/`81>Tbj,Y-"!DO(p5FH\m2̀5d ~RF 7Dtə3Aęex-KUV
;AT* FLph+tv\ lld+} `pFi*!g>i:遐D`.q)⟸(a0"{OFϪ`<"-})O`*0>mגrVx}yl u *D(?> F

kt\uXOqy5	Nz$Ba "wF?w4EiLF?qKI	4M, g]XߎpBWZuK	^tદ,Iy 7ͨV&ֆW#-=E۰.5zQi
	t
i|8،8j2O(bjǧ!R!Ddl
gDl@W"gtt4ˈ^tk͝iGQ(	RFGJI[ iX{DpyRӈ[޳u0usLfV 
(_@K
|/K/t$n!+h;7#gqƧ*"ihQ^\g5Τb4b?@gDVb[	){.]DȧJvh!U$#Z$0oE0Ol9Q3lv§B!sF|Y;MlOgG_Ĝ>d~!m5t&ƛ_j	N%<($#ZD'S߉蒛Y;!D%c+;Pѽ/^ێ#[x*YOk"hhSF[tj
䉸q	j0/х{(yWZ/(Odth('1HQL!mO{vؽa4t`L?Bp ٓYoӰƿ{.y {-F>,ԂnB&EqR>/U.yaHV	0]v(ar뇞&b+A.eX?\L]~V1.A;{}z[=+B5Cb#-Ds8+->r%n븵I\~HN^vXCHuAhjhZX	rj:A$v~p;jBo^9GqlCͯ?i/ fbmK,+%x\0,rCob*.sX
9<s7ʬ5Ha^Ն	BDSkr\&mB.@m6x/$I޷FoGu}_R4AʍpjBo\{^!7{$jyۏbR%Y#`ÜP`JlՊϝ߄so޸L=	R[mE|+GmǵhOP$A(vdjj^z/;s*	聸RU$wCuMHq5$Yk19iˬ^FOϾ
x
"%H;5@k90OB߬{MYfe%ZBά"ڮPcFW|*Ry3Z;@ wy?)<K==ѓqDkB52I`&'ŸrDA3g	u\{pwVgf5׸ŝqQi>;4}5 q;!8|:П6 H383`uXv4_T5z0=ne"B\㜕$Zw;
\2OZ?oG\/j&ꯥ<8QcXk{=^oY}z{d*kZӽ5z=a~RX/ݫ]k;DX|oB>nDzL$0؍?FBrMS`!bǕ$}uu f4&_ }zk68/(qj(h]T6hj 0w͙br?8htes?Q@ 3p[qgl:R-A!_0QoFAb~
>&p|2_*G$F\0݂:Smb&|FtS' /M7W_BIY G)wqmL883NG&)_k9ASپ'LR}.动U_HE<"
VA2E1zxrF>i	60oF]RȬ9ՆKVJ^ \!34k/Z
!UOȾ)[;Rc,poŁ3˯w{-RŸ8!Rs.<+0wc!0o^d 8'c\y2zR(u"V#1/u9Qm-zx=
.jWYAB-^a:νa.څT ;ۧ+B87s*:!Cvrk=u!XW
vULߌBH{չ9}ԓ I;wM
o߶FT iCs8Ÿ&ѳqC#d..{Y[)U	R	ܜuW}nBd'}`ybk+q>wDGbmY/JcMf	'
K.5<iRMU/"APNy9}{\NwPE\%qaJ_[2QX å7Z
ܖ3q5$uO OZǥV4Heo;Ӑ,Nd>E3u:wd3\%ɾޯϗ;ʔ
<L|`2^&zp5zFP6}Eb{}pʂz\%zA#ihq6S's}L2BRveF )Z9&2F76%6Ŝì.Ki
4Wd'C:ow0 >`IhpogӑM޸}>1+ƱZĿci~'uroi|undV	25Ȗ=e<+MD&Ix!۠+*eV0y?qhp#U |Vj=UTm~ _|\o5'<۪}B~W9KX/ʞ$̅ܴ.,Z	8.5 }cIQ$2]\FrF號}Ƚ6\l-/V )4p]ZӬSVzş|X,]@Hs~KҤ4F4h4+c$'S+.2x71΀eUiC26SсOƙX	\]pig񟛮C:YW3UVU>.Rv˘L7y-'Bѯq5hp;V|Ҿ1|U@M@kR23"\ZHq*8*`WkSIXHU1Ӥ/F!R놻yxkT(i'`K3R\+6#9J[ \Q?b('A^N+;+y+%\&NXoEbqoqS҃Z$)qMѝ,%\՘=\ky  wN޳F&S{ͬAGHu!oٚ*?wpe~@ɰOZc"_J`r^kѓ3@nčCwd$p`&Ti60ȧ(q{N_>MEq샴$ 5,׌,f#sTwͭN̯E軁に"GRd4.4F?y4pC̷gS܅Tg͸_/Yy)YI샤Zr;<	."\d꿀3TG5܋|
ĉI~mh8#T^k%[q'ֱF_&iIHܕ!iިXRm*@x7RML"MAK[+pj@fVKWbǊ厴	jqIGt1#-HL_Cn)mIh2%1e4N~BahCg7! q9d ϤpmC|wdoaטiGO
@k9|d~:ns#iBޕ{\_wwMc')㥸4oRZ@U|v<.Y'SH>Uv?=ͽD_4/DYT~H,˭zgp5zSV9DxɘHzhF?G`7"9|EIa 嚬iV{׾ E	[]Vi	R|ؘHq\X\'ɖ8A 86<?5:͆EV!H\	p nVӠ3t	ig*[ŅaJ$|769{;p%;"2j\`Uw:}#ki/6Kܪ:r<2x6ǕVo^FI8-l- x1I@(7mg*P@
(P@
(P@
(P@
(P@dD3    IENDB`Sphinx==1.8.5
git+https://github.com/fabpot/sphinx-php.git
sphinx_rtd_theme
Jinja2<3.0
MarkupSafe<2.1
alabaster<0.7.14
Attributes and annotations
==========================

.. warning ::

    Starting from release 3.30.0 [doctrine/annotations](https://github.com/doctrine/annotations) is an optional package. 
    If you still want to use them, please make sure that you require in `composer.json` file.

PHP 8 support
~~~~~~~~~~~~~~~
JMS serializer now supports PHP 8 attributes, with a few caveats:
- Due to the missing support for nested attributes, the syntax for a few attributes has changed
(see the ``VirtualProperty`` ``options`` syntax here below)
- There is an edge case when setting this exact serialization group ``#[Groups(['value' => 'any value here'])]``.
(when there is only one item in th serialization groups array and has as key ``value`` the attribute will not work as expected,
please use the alternative syntax ``#[Groups(groups: ['value' => 'any value here'])]`` that works with no issues),
- Some support for unions exists.  For unions of primitive types, the system will try to resolve them automatically.  For
classes that contain union attributes, the ``#[UnionDiscriminator]`` attribute must be used to specify the type of the union.

Converting your annotations to attributes
-----------------------------------------

Example:

.. code-block :: php

    /**
     * @VirtualProperty(
     *     "classlow",
     *     exp="object.getVirtualValue(1)",
     *     options={@Until("8")}
     * )
     * @VirtualProperty(
     *     "classhigh",
     *     exp="object.getVirtualValue(8)",
     *     options={@Since("6")}
     * )
     */
    #[VirtualProperty('classlow', exp: 'object.getVirtualValue(1)', options: [[Until::class, ['8']]])]
    #[VirtualProperty('classhigh', exp: 'object.getVirtualValue(8)', options: [[Since::class, ['6']]])]
    class ObjectWithVersionedVirtualProperties
    {
        /**
         * @Groups({"versions"})
         * @VirtualProperty
         * @SerializedName("low")
         * @Until("8")
         */
        #[Groups(['versions'])]
        #[VirtualProperty]
        #[SerializedName('low')]
        #[Until('8')]
        public function getVirtualLowValue()
        {
            return 1;
        }
    ...

To automate migration of Annotations to Attributes you can use `rector/rector` with following config:

.. code-block :: php

    <?php

    declare(strict_types=1);

    use Rector\Config\RectorConfig;
    use Rector\Set\ValueObject\LevelSetList;
    use Rector\Symfony\Set\JMSSetList;

    return static function (RectorConfig $rectorConfig) {
        $rectorConfig->paths([
            __DIR__ . '/src',
        ]);
        $rectorConfig->sets([
            JMSSetList::ANNOTATIONS_TO_ATTRIBUTES,
            LevelSetList::UP_TO_PHP_80,
        ]);
    };



#[ExclusionPolicy]
~~~~~~~~~~~~~~~~~~
This attribute can be defined on a class to indicate the exclusion strategy
that should be used for the class.

+----------+----------------------------------------------------------------+
| Policy   | Description                                                    |
+==========+================================================================+
| all      | all properties are excluded by default; only properties marked |
|          | with #Expose will be serialized/unserialized                   |
+----------+----------------------------------------------------------------+
| none     | no properties are excluded by default; all properties except   |
|          | those marked with #Exclude will be serialized/unserialized     |
+----------+----------------------------------------------------------------+

#[Exclude]
~~~~~~~~~~
This attribute can be defined on a property or a class to indicate that the property or class
should not be serialized/unserialized. Works only in combination with NoneExclusionPolicy.

If the ``ExpressionLanguageExclusionStrategy`` exclusion strategy is enabled, it will
be possible to use ``#[Exclude(if:"expression")]`` to exclude dynamically a property
or an object if used on class level.

#[Expose]
~~~~~~~~~
This attribute can be defined on a property to indicate that the property should
be serialized/unserialized. Works only in combination with AllExclusionPolicy.

If the ``ExpressionLanguageExclusionStrategy`` exclusion strategy is enabled, will
be possible to use ``#Expose[if:"expression"]`` to expose dynamically a property.

#[SkipWhenEmpty]
~~~~~~~~~~~~~~~~
This attribute can be defined on a property to indicate that the property should
not be serialized if the result will be "empty".

Works option works only when serializing.

#[SerializedName]
~~~~~~~~~~~~~~~~~
This attribute can be defined on a property to define the serialized name for a
property. If this is not defined, the property will be translated from camel-case
to a lower-cased underscored name, e.g. camelCase -> camel_case.

Note that this attribute is not used when you're using any other naming
strategy than the default configuration (which includes the
``SerializedNameattributeStrategy``). In order to re-enable the attribute, you
will need to wrap your custom strategy with the ``SerializedNameattributeStrategy``.

.. code-block :: php

    <?php
    $serializer = \JMS\Serializer\SerializerBuilder::create()
        ->setPropertyNamingStrategy(
            new \JMS\Serializer\Naming\SerializedNameattributeStrategy(
                new \JMS\Serializer\Naming\IdenticalPropertyNamingStrategy()
            )
        )
        ->build();

XML Specific Usage
------------------

When serializing to or deserializing from XML, ``#[SerializedName]`` supports special
syntax to map PHP properties to XML attributes. This can be useful for more complex
XML structures where ``#[XmlAttribute]`` might not suffice. These syntaxes work for
both serialization and deserialization.

1.  **Attribute on the current element:**
    To map a property as an attribute of the XML element that represents the current
    class instance, prefix the attribute name with ``@``.

    .. code-block:: php

        <?php
        use JMS\Serializer\Annotation as Serializer;

        #[Serializer\XmlRoot("user")]
        class User
        {
            #[Serializer\SerializedName("@id")]
            #[Serializer\Type("integer")]
            private $id; // Becomes an attribute "id" on the <user> element

            #[Serializer\SerializedName("name")]
            #[Serializer\Type("string")]
            private $name; // Becomes a child element <name>

            public function __construct(int $id, string $name)
            {
                $this->id = $id;
                $this->name = $name;
            }
        }

        // Example: $user = new User(1, 'John Doe');
        // Serializes to: <user id="1"><name>John Doe</name></user>

2.  **Attribute on a sibling element:**
    To map a property as an attribute on a *sibling* XML element, use the
    syntax ``"ElementName/@AttributeName"``. The property's value will
    become an attribute named ``AttributeName`` on a sibling XML element named
    ``ElementName``. Your PHP class should typically have one property that defines
    the sibling element's value (e.g., ``ElementName``) and another property that
    defines its attribute (e.g., ``ElementName/@AttributeName``).

    .. code-block:: php

        <?php
        use JMS\Serializer\Annotation as Serializer;

        #[Serializer\XmlRoot("item")]
        class Item
        {
            /**
             * This property defines the <identifier> XML element.
             */
            #[Serializer\SerializedName("identifier")]
            #[Serializer\Type("string")]
            #[Serializer\XmlElement(cdata: false)]
            private $identifierValue;

            /**
             * This property becomes the "scheme" attribute on the <identifier> element.
             */
            #[Serializer\SerializedName("identifier/@scheme")]
            #[Serializer\Type("string")]
            #[Serializer\XmlElement(cdata: false)] // Namespace can be specified here if needed via namespace: "http://..."
            private $identifierScheme;

            public function __construct(string $identifierValue, string $identifierScheme)
            {
                $this->identifierValue = $identifierValue;
                $this->identifierScheme = $identifierScheme;
            }
        }

        // Example: $item = new Item('ABC', 'product_sku');
        // Serializes to: <item><identifier scheme="product_sku">ABC</identifier></item>

    During serialization, if a property mapped to an attribute has a ``null`` value,
    the attribute will not be rendered on the XML element.
    The ``#[XmlElement]`` annotation can be used on properties mapped with these
    syntaxes, for instance, to control the XML namespace of the attribute if it
    differs from the element's namespace (though typically attributes inherit the
    namespace of their element or have no namespace).


#[Since]
~~~~~~~~
This attribute can be defined on a property to specify starting from which
version this property is available. If an earlier version is serialized, then
this property is excluded automatically. The version must be in a format that is
understood by PHP's ``version_compare`` function.

#[Until]
~~~~~~~~
This attribute can be defined on a property to specify until which version this
property was available. If a later version is serialized, then this property is
excluded automatically. The version must be in a format that is understood by
PHP's ``version_compare`` function.

#[Groups]
~~~~~~~~~
This attribute can be defined on a property to specify if the property
should be serialized when only serializing specific groups (see
:doc:`../cookbook/exclusion_strategies`).

#[MaxDepth]
~~~~~~~~~~~
This attribute can be defined on a property to limit the depth to which the
content will be serialized. It is very useful when a property will contain a
large object graph.

#[AccessType]
~~~~~~~~~~~~~
This attribute can be defined on a property, or a class to specify in which way
the properties should be accessed. By default, the serializer will retrieve, or
set the value via reflection, but you may change this to use a public method instead:

.. code-block :: php

    <?php
    use JMS\Serializer\Annotation\AccessType;

    #[AccessType(type: 'public_method')]
    class User
    {
        private $name;

        public function getName()
        {
            return $this->name;
        }

        public function setName($name)
        {
            $this->name = trim($name);
        }
    }

#[Accessor]
~~~~~~~~~~~
This attribute can be defined on a property to specify which public method should
be called to retrieve, or set the value of the given property:

.. code-block :: php

    <?php
    use JMS\Serializer\Annotation\Accessor;

    class User
    {
        private $id;

        #[Accessor(getter: 'getTrimmedName', setter: 'setName')]
        private $name;

        // ...
        public function getTrimmedName()
        {
            return trim($this->name);
        }

        public function setName($name)
        {
            $this->name = $name;
        }
    }

.. note ::

    If you need only to serialize your data, you can avoid providing a setter by
    setting the property as read-only using the ``#[ReadOnlyProperty]`` attribute.

#[AccessorOrder]
~~~~~~~~~~~~~~~~
This attribute can be defined on a class to control the order of properties. By
default the order is undefined, but you may change it to either "alphabetical", or
"custom".

.. code-block :: php

    <?php

    use JMS\Serializer\Annotation\AccessorOrder;
    use JMS\Serializer\Annotation\VirtualProperty;
    use JMS\Serializer\Annotation\SerializedName;

    #[AccessorOrder('alphabetical')]
    class User
    {
        private $id;
        private $name;
    }

    /**
     * Resulting Property Order: name, id
     */
    #[AccessorOrder(order: 'custom', custom: ['name', 'id'])]
    class User
    {
        private $id;
        private $name;
    }

    /**
     * Resulting Property Order: name, mood, id
     */
    #[AccessorOrder(order: 'custom', custom: ['name', 'someMethod', 'id'])]
    class User
    {
        private $id;
        private $name;

        #[VirtualProperty]
        #[SerializedName(name: 'mood')]

        public function getSomeMethod(): string
        {
            return 'happy';
        }
    }


#[VirtualProperty]
~~~~~~~~~~~~~~~~~~
This attribute can be defined on a method to indicate that the data returned by
the method should appear like a property of the object.

A virtual property can be defined for a method of an object to serialize and can be
also defined at class level exposing data using the Symfony Expression Language.

.. code-block :: php

    #[Serializer\VirtualProperty(name: 'firstName', exp: 'object.getFirstName()', options: [[Serializer\SerializedName::class, ['my_first_name']]])]
    class Author
    {
        #[Serializer\Expose]
        private $id;

        #[Serializer\Exclude]
        private $firstName;

        #[Serializer\Exclude]
        private $lastName;

        #[Serializer\VirtualProperty]
        public function getLastName(): string
        {
            return $this->lastName;
        }

        public function getFirstName(): string
        {
            return $this->firstName;
        }
    }

In this example:

- ``id`` is exposed using the object reflection.
- ``lastName`` is exposed using the ``getLastName`` getter method.
- ``firstName`` is exposed using the ``object.getFirstName()`` expression (``exp`` can contain any valid symfony expression).


``#[VirtualProperty]`` can also have an optional property ``name``, used to define the internal property name
(for sorting proposes as example). When not specified, it defaults to the method name with the "get" prefix removed.

.. note ::

    This only works for serialization and is completely ignored during deserialization.

In PHP 8, due to the missing support for nested attributes, in the options array you need to pass an array with the class name and an array with the arguments for its constructor.

.. code-block :: php

    /**
     * @Serializer\VirtualProperty(
     *     "firstName",
     *     exp="object.getFirstName()",
     *     options={@Serializer\SerializedName("my_first_name")}
     *  )
     */
    #[Serializer\VirtualProperty(name: "firstName", exp: "object.getFirstName()", options: [[Serializer\SerializedName::class, ["my_first_name"]]])]
    class Author
    {
    ...

#[Inline]
~~~~~~~~~
This attribute can be defined on a property to indicate that the data of the property
should be inlined.

**Note**: AccessorOrder will be using the name of the property to determine the order.

#[ReadOnlyProperty]
~~~~~~~~~~~~~~~~~~~
This attribute can be defined on a property to indicate that the data of the property
is read only and cannot be set during deserialization.

A property can be marked as non read only with ``#[ReadOnlyProperty(readOnly: false)]`` attribute
(useful when a class is marked as read only).

#[PreSerialize]
~~~~~~~~~~~~~~~
This attribute can be defined on a method which is supposed to be called before
the serialization of the object starts.

#[PostSerialize]
~~~~~~~~~~~~~~~~
This attribute can be defined on a method which is then called directly after the
object has been serialized.

#[PostDeserialize]
~~~~~~~~~~~~~~~~~~
This attribute can be defined on a method which is supposed to be called after
the object has been deserialized.

#[Discriminator]
~~~~~~~~~~~~~~~~

This attribute allows serialization/deserialization of relations which are polymorphic, but
where a common base class exists. The ``#[Discriminator]`` attribute has to be applied
to the least super type:

.. code-block :: php

    #[Serializer\Discriminator(field: 'type', disabled: false, map: ['car' => 'Car', 'moped' => 'Moped'], groups=["foo", "bar"])]
    abstract class Vehicle { }
    class Car extends Vehicle { }
    class Moped extends Vehicle { }
    ...

.. note ::

    `groups` is optional and is used as exclusion policy.

#[UnionDiscriminator]
~~~~~~~~~~~~~~~~~~~~~

This attribute allows deserialization of unions.  The ``#[UnionDiscriminator]`` attribute has to be applied
to an attribute that can be one of many types.

.. code-block :: php

    class Vehicle {
        #[UnionDiscriminator(field: 'typeField', map: ['manual' => 'FullyQualified/Path/Manual', 'automatic' => 'FullyQualified/Path/Automatic'])]
        private Manual|Automatic $transmission;
    }

In the case of this example, both Manual and Automatic should contain a string attribute named `typeField`.  The value of that field will be passed
to the `map` option to determine which class to instantiate.

#[Type]
~~~~~~~
This attribute can be defined on a property to specify the type of that property.
For deserialization, this attribute must be defined.
The ``#[Type]`` attribute can have parameters and parameters can be used by serialization/deserialization
handlers to enhance the serialization or deserialization result; for example, you may want to
force a certain format to be used for serializing DateTime types and specifying at the same time a different format
used when deserializing them.

Available Types:

+------------------------------------------------------------+--------------------------------------------------+
| Type                                                       | Description                                      |
+============================================================+==================================================+
| boolean or bool                                            | Primitive boolean                                |
+------------------------------------------------------------+--------------------------------------------------+
| integer or int                                             | Primitive integer                                |
+------------------------------------------------------------+--------------------------------------------------+
| double or float                                            | Primitive double                                 |
+------------------------------------------------------------+--------------------------------------------------+
| double<2> or float<2>                                      | Primitive double with precision                  |
+------------------------------------------------------------+--------------------------------------------------+
| double<2, 'HALF_DOWN'> or float<2, 'HALF_DOWN'>            | Primitive double with precision and              |
|                                                            | Rounding Mode.                                   |
|                                                            | (HALF_UP, HALF_DOWN, HALF_EVEN HALF_ODD)         |
+------------------------------------------------------------+--------------------------------------------------+
| double<2, 'HALF_DOWN', 2> or float<2, 'HALF_DOWN', 2>      | Primitive double with precision,                 |
| double<2, 'HALF_DOWN', 3> or float<2, 'HALF_DOWN', 3>      | Rounding Mode and decimals padding up to         |
|                                                            | N digits. As example, the float ``1.23456`` when |
|                                                            | specified as  ``double<2, 'HALF_DOWN', 5>`` will |
|                                                            | be serialized as ``1.23000``.                    |
|                                                            | NOTE: this is available only for the XML         |
|                                                            | serializer.                                      |
+------------------------------------------------------------+--------------------------------------------------+
| string                                                     | Primitive string                                 |
+------------------------------------------------------------+--------------------------------------------------+
| array                                                      | An array with arbitrary keys, and values.        |
+------------------------------------------------------------+--------------------------------------------------+
| list                                                       | A list with arbitrary values.                    |
+------------------------------------------------------------+--------------------------------------------------+
| array<T>                                                   | An array of type T (T can be any available type).|
|                                                            | Examples:                                        |
|                                                            | array<string>, array<MyNamespace\MyObject>, etc. |
+------------------------------------------------------------+--------------------------------------------------+
| list<T>                                                    | A list of type T (T can be any available type).  |
|                                                            | Examples:                                        |
|                                                            | list<string>, list<MyNamespace\MyObject>, etc.   |
+------------------------------------------------------------+--------------------------------------------------+
| array<K, V>                                                | A map of keys of type K to values of type V.     |
|                                                            | Examples: array<string, string>,                 |
|                                                            | array<string, MyNamespace\MyObject>, etc.        |
+------------------------------------------------------------+--------------------------------------------------+
| enum<T>                                                    | Enum of type Color, use its case values          |
|                                                            | for serialization and deserialization            |
|                                                            | if the enum is a backed enum,                    |
|                                                            | use its case names if it is not a backed enum.   |
+------------------------------------------------------------+--------------------------------------------------+
| enum<T, 'name'>                                            | Enum of type Color, use its case names           |
|                                                            | (as string) for serialization                    |
|                                                            | and deserialization.                             |
+------------------------------------------------------------+--------------------------------------------------+
| enum<T, 'value'>                                           | Backed Enum of type Color, use its case value    |
|                                                            | for serialization and deserialization.           |
+------------------------------------------------------------+--------------------------------------------------+
| enum<T, 'value', 'integer'>                                | Backed Enum of type Color, use its case value    |
|                                                            | (forced as integer) for serialization            |
|                                                            | and deserialization.                             |
+------------------------------------------------------------+--------------------------------------------------+
| DateTime                                                   | PHP's DateTime object (default format*/timezone) |
+------------------------------------------------------------+--------------------------------------------------+
| DateTime<'format'>                                         | PHP's DateTime object (custom format/default     |
|                                                            | timezone).                                       |
+------------------------------------------------------------+--------------------------------------------------+
| DateTime<'format', 'zone'>                                 | PHP's DateTime object (custom format/timezone)   |
+------------------------------------------------------------+--------------------------------------------------+
| DateTime<'format', 'zone', 'deserializeFormats'>           | PHP's DateTime object (custom format/timezone,   |
|                                                            | deserialize format). If you do not want to       |
|                                                            | specify a specific timezone, use an empty        |
|                                                            | string (''). DeserializeFormats can either be a  |
|                                                            | string or an array of string.                    |
+------------------------------------------------------------+--------------------------------------------------+
| DateTimeImmutable                                          | PHP's DateTimeImmutable object (default format*/ |
|                                                            | timezone).                                       |
+------------------------------------------------------------+--------------------------------------------------+
| DateTimeImmutable<'format'>                                | PHP's DateTimeImmutable object (custom format/   |
|                                                            | default timezone)                                |
+------------------------------------------------------------+--------------------------------------------------+
| DateTimeImmutable<'format', 'zone'>                        | PHP's DateTimeImmutable object (custom format/   |
|                                                            | timezone)                                        |
+------------------------------------------------------------+--------------------------------------------------+
| DateTimeImmutable<'format', 'zone', 'deserializeFormats'>  | PHP's DateTimeImmutable object (custom format/   |
|                                                            | timezone/deserialize format). If you do not want |
|                                                            | to specify a specific timezone, use an empty     |
|                                                            | string (''). DeserializeFormats can either be a  |
|                                                            | string or an array of string.                    |
+------------------------------------------------------------+--------------------------------------------------+
| DateTimeInterface                                          | PHP's DateTimeInterface interface (default       |
|                                                            | format*/timezone).                               |
|                                                            | Data will be always deserialised into            |
|                                                            | `\DateTime` object                               |
+------------------------------------------------------------+--------------------------------------------------+
| DateTimeInterface<'format'>                                | PHP's DateTimeInterface interface (custom        |
|                                                            | format/default timezone)                         |
|                                                            | Data will be deserialised into                   |
|                                                            | `\\DateTime` object                              |
+------------------------------------------------------------+--------------------------------------------------+
| DateTimeInterface<'format', 'zone'>                        | PHP's DateTimeInterface interface (custom        |
|                                                            | format/timezone)                                 |
|                                                            | Data will be deserialised into                   |
|                                                            | `\\DateTime` object                              |
+------------------------------------------------------------+--------------------------------------------------+
| DateTimeInterface<'format', 'zone', 'deserializeFormats'>  | PHP's DateTimeInterface interface (custom        |
|                                                            | format/timezone/deserialize format). If you do   |
|                                                            | not want to specify a specific timezone, use an  |
|                                                            | empty string (''). DeserializeFormats can either |
|                                                            | be a string or an array of string.               |
|                                                            | Data will be deserialised into                   |
|                                                            | `\\DateTime` object                              |
+------------------------------------------------------------+--------------------------------------------------+
| DateInterval                                               | PHP's DateInterval object using ISO 8601 format  |
+------------------------------------------------------------+--------------------------------------------------+
| T                                                          | Where T is a fully qualified class name.         |
+------------------------------------------------------------+--------------------------------------------------+
| iterable                                                   | Similar to array. Will always be deserialized    |
|                                                            | into array as implementation info is lost during |
|                                                            | serialization.                                   |
+------------------------------------------------------------+--------------------------------------------------+
| iterable<T>                                                | Similar to array<T>. Will always be deserialized |
|                                                            | into array as implementation info is lost during |
|                                                            | serialization.                                   |
+------------------------------------------------------------+--------------------------------------------------+
| iterable<K, V>                                             | Similar to array<K, V>. Will always be           |
|                                                            | deserialized into array as implementation info   |
|                                                            | is lost during serialization.                    |
+------------------------------------------------------------+--------------------------------------------------+
| ArrayCollection<T>                                         | Similar to array<T>, but will be deserialized    |
|                                                            | into Doctrine's ArrayCollection class.           |
+------------------------------------------------------------+--------------------------------------------------+
| ArrayCollection<K, V>                                      | Similar to array<K, V>, but will be deserialized |
|                                                            | into Doctrine's ArrayCollection class.           |
+------------------------------------------------------------+--------------------------------------------------+
| Generator                                                  | Similar to array, but will be deserialized       |
|                                                            | into Generator class.                            |
+------------------------------------------------------------+--------------------------------------------------+
| Generator<T>                                               | Similar to array<T>, but will be deserialized    |
|                                                            | into Generator class.                            |
+------------------------------------------------------------+--------------------------------------------------+
| Generator<K, V>                                            | Similar to array<K, V>, but will be deserialized |
|                                                            | into Generator class.                            |
+------------------------------------------------------------+--------------------------------------------------+
| ArrayIterator                                              | Similar to array, but will be deserialized       |
|                                                            | into ArrayIterator class.                        |
+------------------------------------------------------------+--------------------------------------------------+
| ArrayIterator<T>                                           | Similar to array<T>, but will be deserialized    |
|                                                            | into ArrayIterator class.                        |
+------------------------------------------------------------+--------------------------------------------------+
| ArrayIterator<K, V>                                        | Similar to array<K, V>, but will be deserialized |
|                                                            | into ArrayIterator class.                        |
+------------------------------------------------------------+--------------------------------------------------+
| Iterator                                                   | Similar to array, but will be deserialized       |
|                                                            | into ArrayIterator class.                        |
+------------------------------------------------------------+--------------------------------------------------+
| Iterator<T>                                                | Similar to array<T>, but will be deserialized    |
|                                                            | into ArrayIterator class.                        |
+------------------------------------------------------------+--------------------------------------------------+
| Iterator<K, V>                                             | Similar to array<K, V>, but will be deserialized |
|                                                            | into ArrayIterator class.                        |
+------------------------------------------------------------+--------------------------------------------------+

(*) If the standalone jms/serializer is used then default format is `\DateTime::ISO8601` (which is not compatible with ISO-8601 despite the name). For jms/serializer-bundle the default format is `\DateTime::ATOM` (the real ISO-8601 format) but it can be changed in `configuration`_.

(**) The key type K for array-linke formats as ``array``. ``ArrayCollection``, ``iterable``, etc., is only used for deserialization,
for serializaiton is treated as ``string``.

Examples:

.. code-block :: php

    <?php

    namespace MyNamespace;

    use JMS\Serializer\Annotation\Type;

    class BlogPost
    {
        #[Type(name: "ArrayCollection<MyNamespace\Comment>")]
        private $comments;

        #[Type(name: "string")]
        private $title;

        #[Type(name: Author:class)]
        private $author;

        #[Type(name: DateTime:class)]
        private $startAt;

        #[Type(name: 'DateTime<'Y-m-d'>')]
        private $endAt;

        #[Type(name: 'DateTime<'Y-m-d'>')]

        #[Type(name:"DateTime<'Y-m-d', '', ['Y-m-d', 'Y/m/d']>")]
        private $publishedAt;

        #[Type(name:'DateTimeImmutable')]
        private $createdAt;

        #[Type(name:"DateTimeImmutable<'Y-m-d'>")]
        private $updatedAt;

        #[Type(name:"DateTimeImmutable<'Y-m-d', '', ['Y-m-d', 'Y/m/d']>")]
        private $deletedAt;

        #[Type(name:'boolean')]
        private $published;

        #[Type(name:'array<string, string>')]
        private $keyValueStore;
    }

.. note ::

    if you are using ``PHP attributes`` with PHP 8.1 you can pass an object which implements ``__toString()`` method as a value for ``#[Type]`` attribute.

    .. code-block :: php

        <?php

        namespace MyNamespace;

        use JMS\Serializer\Annotation\Type;

        class BlogPost
        {
            #[Type(new ArrayOf(Comment::class))]
            private $comments;
        }

        class ArrayOf implements \Stringable
        {
            public function __construct(private string $className) {}

            public function __toString(): string
            {
                return "array<$className>";
            }
        }

.. _configuration: https://jmsyst.com/bundles/JMSSerializerBundle/master/configuration#configuration-block-2-0

#[XmlRoot]
~~~~~~~~~~
This allows you to specify the name of the top-level element.

.. code-block :: php

    <?php

    use JMS\Serializer\Annotation\XmlRoot;

    #[XmlRoot('user')]
    class User
    {
        private $name = 'Johannes';
    }

Resulting XML:

.. code-block :: xml

    <user>
        <name><![CDATA[Johannes]]></name>
    </user>

.. note ::

    #[XmlRoot] only applies to the root element, but is for example not taken into
    account for collections. You can define the entry name for collections using
    #[XmlList], or #[XmlMap].

#[XmlAttribute]
~~~~~~~~~~~~~~~
This allows you to mark properties which should be set as attributes,
and not as child elements.

.. code-block :: php

    <?php

    use JMS\Serializer\Annotation\XmlAttribute;

    class User
    {
        #[XmlAttribute]
        private $id = 1;
        private $name = 'Johannes';
    }

Resulting XML:

.. code-block :: xml

    <result id="1">
        <name><![CDATA[Johannes]]></name>
    </result>


#[XmlDiscriminator]
~~~~~~~~~~~~~~~~~~~
This attribute allows to modify the behaviour of ``#[Discriminator]`` regarding handling of XML.


Available Options:

+-------------------------------------+--------------------------------------------------+
| Type                                | Description                                      |
+=====================================+==================================================+
| attribute                           | use an attribute instead of a child node         |
+-------------------------------------+--------------------------------------------------+
| cdata                               | render child node content with or without cdata  |
+-------------------------------------+--------------------------------------------------+
| namespace                           | render child node using the specified namespace  |
+-------------------------------------+--------------------------------------------------+

Example for "attribute":

.. code-block :: php

    <?php

    use JMS\Serializer\Annotation\Discriminator;
    use JMS\Serializer\Annotation\XmlDiscriminator;

    #[Discriminator(field: 'type', map: ['car' => 'Car', 'moped' => 'Moped'], groups: ['foo', 'bar'])]
    #[XmlDiscriminator(attribute: true)]
    abstract class Vehicle { }
    class Car extends Vehicle { }

Resulting XML:

.. code-block :: xml

    <vehicle type="car" />


Example for "cdata":

.. code-block :: php

    <?php

    use JMS\Serializer\Annotation\Discriminator;
    use JMS\Serializer\Annotation\XmlDiscriminator;

    #[Discriminator(field: 'type', map: ['car' => 'Car', 'moped' => 'Moped'], groups: ['foo', 'bar'])]
    #[XmlDiscriminator]
    abstract class Vehicle { }
    class Car extends Vehicle { }

Resulting XML:

.. code-block :: xml

    <vehicle><type>car</type></vehicle>


#[XmlValue]
~~~~~~~~~~~
This allows you to mark properties which should be set as the value of the
current element. Note that this has the limitation that any additional
properties of that object must have the #[XmlAttribute] attribute.
XMlValue also has property cdata. Which has the same meaning as the one in
XMLElement.

.. code-block :: php

    <?php

    use JMS\Serializer\Annotation\XmlAttribute;
    use JMS\Serializer\Annotation\XmlValue;
    use JMS\Serializer\Annotation\XmlRoot;

    #[XmlRoot('price')]
    class Price
    {
        #[XmlAttribute]
        private $currency = 'EUR';

        #[XmlValue]
        private $amount = 1.23;
    }


Resulting XML:

.. code-block :: xml

    <price currency="EUR">1.23</price>

#[XmlList]
~~~~~~~~~~
This allows you to define several properties of how arrays should be
serialized. This is very similar to #[XmlMap], and should be used if the
keys of the array are not important.

.. code-block :: php

    <?php

    use JMS\Serializer\Annotation\XmlList;
    use JMS\Serializer\Annotation\XmlRoot;

    #[XmlRoot('post')]
    class Post
    {
        public function __construct(
            #[XmlList(inline: true, entry: 'comment')]
            private array $comments
        )
        {
        }
    }

    class Comment
    {
        public function __construct(private string $text)
        {
        }
    }

Resulting XML:

.. code-block :: xml

    <post>
        <comment>
            <text><![CDATA[Foo]]></text>
        </comment>
        <comment>
            <text><![CDATA[Bar]]></text>
        </comment>
    </post>

You can also specify the entry tag namespace using the ``namespace`` attribute (``#[XmlList(inline: true, entry: 'comment', namespace: 'http://www.example.com/ns')]``).

#[XmlMap]
~~~~~~~~~
Similar to #[XmlList], but the keys of the array are meaningful.

#[XmlKeyValuePairs]
~~~~~~~~~~~~~~~~~~~
This allows you to use the keys of an array as xml tags.

.. note ::

    When a key is an invalid xml tag name (e.g. 1_foo) the tag name *entry* will be used instead of the key.

#[XmlAttributeMap]
~~~~~~~~~~~~~~~~~~

This is similar to the #[XmlKeyValuePairs], but instead of creating child elements, it creates attributes.

.. code-block :: php

    <?php

    use JMS\Serializer\Annotation\XmlAttributeMap;

    class Input
    {
        #[XmlAttributeMap]
        private $id = ['name' => 'firstname', 'value' => 'Adrien'];
    }


Resulting XML:

.. code-block :: xml

    <result name="firstname" value="Adrien"/>

#[XmlElement]
~~~~~~~~~~~~~
This attribute can be defined on a property to add additional xml serialization/deserialization properties.

.. code-block :: php

    <?php

    use JMS\Serializer\Annotation\XmlElement;
    use JMS\Serializer\Annotation\XmlNamespace;

    #[XmlNamespace(uri: 'http://www.w3.org/2005/Atom', prefix: 'atom')]
    class User
    {
        #[XmlElement(cdata: false, namespace: 'http://www.w3.org/2005/Atom')]
        private $id = 'my_id';
    }

Resulting XML:

.. code-block :: xml

    <atom:id>my_id</atom:id>

#[XmlNamespace]
~~~~~~~~~~~~~~~
This attribute allows you to specify Xml namespace/s and prefix used.

.. code-block :: php

    <?php

    use JMS\Serializer\Annotation\Groups;
    use JMS\Serializer\Annotation\SerializedName;
    use JMS\Serializer\Annotation\Type;
    use JMS\Serializer\Annotation\XmlElement;
    use JMS\Serializer\Annotation\XmlNamespace;

    #[XmlNamespace(uri: 'http://example.com/namespace')]
    #[XmlNamespace(uri: 'http://www.w3.org/2005/Atom', prefix: 'atom')]
    class BlogPost
    {
        #[Type(\JMS\Serializer\Tests\Fixtures\Author::class)]
        #[Groups(['post'])]
        #[XmlElement(namespace: 'http://www.w3.org/2005/Atom')]
        private $author;
    }

    class Author
    {
        #[Type('string')]
        #[SerializedName('full_name')]
        private $name;
    }


Resulting XML:

.. code-block :: xml

    <?xml version="1.0" encoding="UTF-8"?>
    <blog-post xmlns="http://example.com/namespace" xmlns:atom="http://www.w3.org/2005/Atom">
        <atom:author>
            <full_name><![CDATA[Foo Bar]]></full_name>
        </atom:author>
    </blog>


Enum support
~~~~~~~~~~~~~~

Enum support is disabled by default, to enable it run:

.. code-block :: php

    $builder = SerializerBuilder::create();
    $builder->enableEnumSupport();

    $serializer = $builder->build();


With the enum support enabled, enums are automatically detected using typed properties typehints.
When typed properties are no available (virtual properties as example), it is necessary to explicitly typehint
the underlying type using the ``#[Type]`` attribute.

- If the enum is a ``BackedEnum``, the case value will be used for serialization and deserialization by default;
- If the enum is not a ``BackedEnum``, the case name will be used for serialization and deserialization by default;
XML Reference
-------------
::

    <!-- MyBundle\Resources\config\serializer\Fully.Qualified.ClassName.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <serializer>
        <class name="Fully\Qualified\ClassName" exclusion-policy="ALL" xml-root-name="foo-bar" exclude="true"
            exclude-if="expr" accessor-order="custom" custom-accessor-order="propertyName1,propertyName2,...,propertyNameN"
            access-type="public_method" discriminator-field-name="type" discriminator-disabled="false" read-only="false">
            <xml-namespace prefix="atom" uri="http://www.w3.org/2005/Atom"/>
            <xml-discriminator attribute="true" cdata="false" namespace=""/>
            <discriminator-class value="some-value">ClassName</discriminator-class>
            <discriminator-groups>
                <group>foo</group>
            </discriminator-groups>
            <property name="some-property"
                      exclude="true"
                      expose="true"
                      exclude-if="expr"
                      expose-if="expr"
                      skip-when-empty="false"
                      type="string"
                      serialized-name="foo"
                      since-version="1.0"
                      until-version="1.1"
                      xml-attribute="true"
                      xml-value="true"
                      access-type="public_method"
                      accessor-getter="getSomeProperty"
                      accessor-setter="setSomeProperty"
                      inline="true"
                      read-only="true"
                      groups="foo,bar"
                      xml-key-value-pairs="true"
                      xml-attribute-map="true"
                      max-depth="2"
            >
                <!-- You can also specify the type as element which is necessary if
                     your type contains "<" or ">" characters. -->
                <type><![CDATA[]]></type>
                <xml-list inline="true" entry-name="foobar" namespace="http://www.w3.org/2005/Atom" skip-when-empty="true" />
                <xml-map inline="true" key-attribute-name="foo" entry-name="bar" namespace="http://www.w3.org/2005/Atom" />
                <xml-element cdata="false" namespace="http://www.w3.org/2005/Atom"/>
                <groups>
                    <value>foo</value>
                    <value>bar</value>
                </groups>
                <union-discriminator field="foo">
                    <map>
                       <class key="a">SomeClassFQCN1</class>
                       <class key="b">SomeClassFQCN2</class>
                       <class key="c">SomeClassFQCN3</class>
                    </map>
                </union-discriminator>
            </property>
            <callback-method name="foo" type="pre-serialize" />
            <callback-method name="bar" type="post-serialize" />
            <callback-method name="baz" type="post-deserialize" />

            <virtual-property method="public_method"
                      name="some-property"
                      exclude="true"
                      expose="true"
                      skip-when-empty="false"
                      type="string"
                      serialized-name="foo"
                      since-version="1.0"
                      until-version="1.1"
                      xml-attribute="true"
                      access-type="public_method"
                      accessor-getter="getSomeProperty"
                      accessor-setter="setSomeProperty"
                      inline="true"
                      read-only="true"
                      groups="foo,bar"
                      xml-key-value-pairs="true"
                      xml-attribute-map="true"
                      max-depth="2"
            >
            <virtual-property expression="object.getName()"
                      name="some-property"
                      exclude="true"
                      expose="true"
                      type="string"
                      serialized-name="foo"
                      since-version="1.0"
                      until-version="1.1"
                      xml-attribute="true"
                      access-type="public_method"
                      accessor-getter="getSomeProperty"
                      accessor-setter="setSomeProperty"
                      inline="true"
                      read-only="true"
                      groups="foo,bar"
                      xml-key-value-pairs="true"
                      xml-attribute-map="true"
                      max-depth="2"
            >
                <!-- You can also specify the type as element which is necessary if
                     your type contains "<" or ">" characters. -->
                <type><![CDATA[]]></type>
                <groups>
                    <value>foo</value>
                    <value>bar</value>
                </groups>
                <xml-list inline="true" entry-name="foobar" namespace="http://www.w3.org/2005/Atom" skip-when-empty="true" />
                <xml-map inline="true" key-attribute-name="foo" entry-name="bar" namespace="http://www.w3.org/2005/Atom" />
            </virtual-property>
            
        </class>
    </serializer>
YAML Reference
--------------

.. code-block :: yaml

    # Vendor\MyBundle\Resources\config\serializer\Model.ClassName.yml
    Vendor\MyBundle\Model\ClassName:
        exclusion_policy: ALL
        xml_root_name: foobar
        xml_root_namespace: http://your.default.namespace
        exclude: true
        exclude_if: expr
        read_only: false
        access_type: public_method # defaults to property
        accessor_order: custom
        custom_accessor_order: [propertyName1, propertyName2, ..., propertyNameN]
        discriminator:
            field_name: type
            disabled: false
            map:
                some-value: ClassName
            groups: [foo, bar]
            xml_attribute: true
            xml_element:
                cdata: false
                namespace: http://www.w3.org/2005/Atom
        virtual_properties:
            getSomeProperty:
                name: optional-prop-name
                serialized_name: foo
                type: integer
            expression_prop:
                name: optional-prop-name
                exp: object.getName()
                serialized_name: foo
                type: integer
        xml_namespaces:
            "": http://your.default.namespace
            atom: http://www.w3.org/2005/Atom
        properties:
            some-property:
                exclude: true
                expose: true
                exclude_if: expr
                expose_if: expr
                skip_when_empty: false
                access_type: public_method # defaults to property
                accessor: # access_type must be set to public_method
                    getter: getSomeOtherProperty
                    setter: setSomeOtherProperty
                type: string
                serialized_name: foo
                since_version: 1.0
                until_version: 1.1
                groups: [foo, bar]
                xml_attribute: true
                xml_value: true
                inline: true
                read_only: true
                xml_key_value_pairs: true
                xml_list:
                    inline: true
                    entry_name: foo
                    namespace: http://www.w3.org/2005/Atom
                xml_map:
                    inline: true
                    key_attribute_name: foo
                    entry_name: bar
                    namespace: http://www.w3.org/2005/Atom
                xml_attribute_map: true
                xml_element:
                    cdata: false
                    namespace: http://www.w3.org/2005/Atom
                max_depth: 2
                union_discriminator:
                    filed: foo
                    map:
                     a: SomeClassFQCN1
                     b: SomeClassFQCN2
                     c: SomeClassFQCN3

        callback_methods:
            pre_serialize: [foo, bar]
            post_serialize: [foo, bar]
            post_deserialize: [foo, bar]

Constants
---------

In some cases, it may be helpful to reference constants in your YAML files.
You can do this by prefixing the constant with the special `!php/const` syntax.

.. code-block :: yaml

    Vendor\MyBundle\Model\ClassName:
        properties:
            some-property:
                serialized_name: !php/const Vendor\MyBundle\Model\ClassName::SOME_CONSTANT
Handlers
========

Introduction
------------
Handlers allow you to change the serialization, or deserialization process
for a single type/format combination.

Handlers are simple callback which receive three arguments: the visitor,
the data, and the type.

Simple Callables
----------------
You can register simple callables on the builder object::

    $builder
        ->configureHandlers(function(JMS\Serializer\Handler\HandlerRegistry $registry) {
            $registry->registerHandler('serialization', 'MyObject', 'json',
                function($visitor, MyObject $obj, array $type) {
                    return $obj->getName();
                }
            );
        })
    ;

.. note ::

        Be aware that when you call `configureHandlers` default handlers (like `DateHandler`)
        won't be added and you will have to call `addDefaultHandlers` on the Builder

Subscribing Handlers
--------------------
Subscribing handlers contain the configuration themselves which makes them easier to share with other users,
and easier to set-up in general::

    use JMS\Serializer\Handler\SubscribingHandlerInterface;
    use JMS\Serializer\GraphNavigator;
    use JMS\Serializer\JsonSerializationVisitor;
    use JMS\Serializer\JsonDeserializationVisitor;
    use JMS\Serializer\Context;

    class MyHandler implements SubscribingHandlerInterface
    {
        public static function getSubscribingMethods()
        {
            return [
                [
                    'direction' => GraphNavigator::DIRECTION_SERIALIZATION,
                    'format' => 'json',
                    'type' => 'DateTime',
                    'method' => 'serializeDateTimeToJson',
                ],
                [
                    'direction' => GraphNavigator::DIRECTION_DESERIALIZATION,
                    'format' => 'json',
                    'type' => 'DateTime',
                    'method' => 'deserializeDateTimeToJson',
                ],
            ];
        }

        public function serializeDateTimeToJson(JsonSerializationVisitor $visitor, \DateTime $date, array $type, Context $context)
        {
            return $date->format($type['params'][0]);
        }

        public function deserializeDateTimeToJson(JsonDeserializationVisitor $visitor, $dateAsString, array $type, Context $context)
        {
            return new \DateTime($dateAsString);
        }
    }

Also, this type of handler is registered via the builder object::

    $builder
        ->configureHandlers(function(JMS\Serializer\Handler\HandlerRegistry $registry) {
            $registry->registerSubscribingHandler(new MyHandler());
        })
    ;

Skippable Subscribing Handlers
-------------------------------

In case you need to be able to fall back to the default deserialization behavior instead of using your custom
handler, you can simply throw a `SkipHandlerException` from you custom handler method to do so.
Reference
=========

.. toctree ::
    :glob:
    :maxdepth: 1

    reference/*PNG

   IHDR  %     d   bKGD      	pHYs        tIME(2x˖    IDATxw|$ EQ':먶Xۊmm:jVP 7B!@)	3ɹCp}u]s]mx<     hj      E	     %     OP      >AQ     E	     %     OP      >AQ     E	     %     OP      >AQ     E	     %     OP      >AQ     E	     %     OP      >AQ     E	     %     OP      >AQ     E	     %     OP      >AQ     E	     %     OP      >AQ     D!  ?x<RUu*UQeWy]UvU~_UvN9\.]n9n9]n\nn9].\nN\.9]n)E"[cHAtА~Ծu  ?fx<D  W;[XW.$|[do
LE]F4z@=tW.0  ~  PZQmX%J[Pʲү~<v~0rn  E	  Yqir
K[Xeb*X^/~.zW}u  (J  p	N2rSPV5+'kU:(Zl7\{u (J  pٿǣbe)-;_)YyJSJVrk'?n>  + (J  JNg+-'_JSjVҲrY%5~a3j" U;  r[<f}o_\Vq=jO_wpQ >E	 ٫:m!;_JWzNK8hr MsڡQ j>E	 UTu<5SǓ3Ld8:HSNѳK{}  S  IiE'gxJ%+!)S{$	*ŏncw""  X( 0*G:ǓuI%$e*+*iޱ/B/e0 U:z  p:	:	ɪ__Cf/s~"g<VUCUժWPZjUWڡN}uhm"ծUl |S  T܉T:m"61UUWNА`bt*#G9-Tzn2sSb5Ɛ5 6-ԩm+uZ;Smխc{Ԗ3L ):y RQUdLHcI?.}s"c_L|7DҮSi=9v1+IZGwЧ鮁uWx. 4d( h,eU:d=y'jYN1lez祟Iב=#)Jr.jφfS4o]٧[[GpqR2E	 @C)-TLBbu)H;Ik"@"b`eUy8A$W^>;_];Ѱ+i>jI%  ^$=rBɴM,wOjxmG=
6MW/MCoN pIQ p޳t<5S$j:"eIL0n9sޥo@y]j͎:|>Iųֱn1Xw^7D];f*E	 jo\IÉ*:?JFs0DGNvNК1 ǹ/Ϧwu`1b:&	 LnG1	IsL{$*9#|(
l7j DU*j-XCťsܧuMoqnv%g {<|  ӥ}GOj8mQϔ4_C+$/\X~Qx?ZF\75 ~ ){Cck8IPEBڀτ&p<%K37jA^8Y@Q^iג{dAGQP` Q7ؾ ~Үi#dH)Oiҍy8㺴mRqƏPo  !R  k+-ڸǟuG1'ݤh`q'S5cFM$?4=][xXƏn^m"[% M% z*@6=}'v3TOԞ#'17\ݬmxj &? `%ڲ?N$
mkQN%/ԨuZ#3#>7ՓnVV- $= `^e]!"&!II0d}KDUjMZ~N{m' X.Q  sq8]z ^+޸"w4xG7ӂ5a}e0"	 CBJVn;5;bTVQuιU'ei]}Eo"rNiA0{L-ߪF@6|ɍ WIY>Tzι~Ozuix\ é/nܕ[;kaÙwjWC"B ̕(J x<9[hs=$჈yRoO_bguM=Xul˓: $Q  c+*ղ{yJ$nīk6oeD\Mf6&{@L ;Q  6GO6veIgrqI_Di[GهǰE	 hzv~@RzN~$&YMxN"r,^w7]ٷ^z>Ҟ 0^Ң( M'15KQh;gQ3X}{s


$*HHԫW$紀 y~v?`EQ ҆ZabOGI@	Uhp>ީQ+ixong߬P\r
Wl?snҟ 0F( #TQh=*.8뼂huO^#5t QCaIޘ@{ WtA8VM }( d-ZK[q$_'Αߞ#zu<-8Ī	 IN% NuHVoWrF~ǤiHz	ѤoVkު\F?f %% p2a7SYEYD8RV-51AҮ?J"Ǡol!׋ hڄDQ .,\A[Y(		RP`>/ԯGgrԬ|-$ϠQ{dԽ7'fKD% &hֲMx#9)OQÈI>ZUv>gUdpE	 8'5+zMGH%Nn1H3|b>_Vu9h$hOx ?P <iK6(!)!0	ֱ+}{忦,ڝ9{Rv;ݢ'Y h ԞH[izJϩǈ0@}ځHr8]z9um#Iߛp>a8I |7HѴJ̫ǈаK~=QTQU߿N{`iV-g֠>=M6% @y8A}@OvL""L/g2r?0{Uw$2 .P '&kҼՊ;Vߏ$FDחF-8?#PϿ5EyE ={ڈA}/VPғE	 XR,Z=GN#$G'7AIoߞJL&R>م BQ?9ijˁIhIo9KUT`$G5t p񉅢 _Qf!ՓH@Mfzui: q'STA.)˒dI?vx("
E	 VVTZ6i=ruIhILPP>۳ݥ_d3dw8G0Zn>H VTQUiުm?dOb叾"'<:0~O
Oex3༱R8.EmڣK7uhc_El~m1Gv`$unZoO|R:%2 /P `GkvE[PR)7@}ߨK~}'g
	X.IBkpߞDE	 fp>J)yDeCw17m bwg3$wI

Ow^5pAQYeKȉ~`__wmĝLՋ|){5Oـ%鱻o][ Ε4(J 0jSsVlќ[j`0ؤ$$8H^n8ޞJ{59
~$gQH0g#aP `&c9˕_To MH{.=x~iz)*y@Yo<ZG4'2 j& [5Hd'"m#=@ϾJ+U |}}SVDE	 FVp꫕[5;z3[5 M@lսS;BAq&6Y%*VaUzϨgDI y8Aϊf`Cwԯw;dZ6
#7HRfz祟芞]  'T]{m`IGdpy7 x<_>[9D`5I]uyO B (n۩'	Ğ}x$$k)H g}?TԮDs `	)zkK&O_._] :>_@.0g诿xP ~JXNR=5RI&6 97."ws:wn:yWQ 39*.oDƎ>:U3gr-Lx<ҿX éoJd ?CQ@-([i_) JEx3MxNj|ZVTÀS0/$g(J hR6sWop~mj̯yJ'(J h%ה:x,	 sֱ<ܯ?c&<SBac% 4(#-ݴG_&<|l~}'4=j=h!j&NƏNd (dϿё [اno[2ӂg@èQxo2b8 ~DoإWp7 `?>ojvOTy|40Bl 
E	 *T}6_1	4 wկGgiS(5-&@a#% 4-Ū)"w: ~i5۵e_<w t(L ~KVQi_Ek ,[~m[E[Sق6Q  E	 hR^tL *fzf-~hӥ}N
Q pQnfDo̥v{@ I-E[XNI97Wk^Id (eOѤ ŻJyXc_4:dFaϾQD0L. w虿}TWA#
0GQL+^;_>)YD09 Kéwf,S^] u]C޿a!z|܂`<bݡw2r
`b% SV^&6Yћ50`~^գ_OSzhHXci\pcJi*,)'*IQ pVbg|S5z1 Yy?ek1rX ݿa!z-J=m10>2X/g*
`B% t飯W~*k\ꄮ}0o,^
`2I9j?	P =9z_iڝfiԮUKSݺ	#wZOj͎C\y<jI$Oa	E	 5=c%~4kzЍllۀIw"m;ה=|jO;3DXGє5;z9;|бmVnaҁBP;| c{oٯm"`! PZ^?J1	uu0AfOܫ-
L{u5_%vrsQ2j:(J ~xJYʫ	`M4t@omԬۭ>F sQx%l'2q:_>D:k¶>veѹ;+z3ruxG|:O҈
``% ?pΌ%zrv游\ͦW#o/*լeЀx2NK|o2s
`P% ?WT_3EoWWNAwp׋m0iZҍtڅYr `siyLUګ
`@% ?r)qHT`j~\W;ubЀy7iǧsk (J ~bû3TZ^YoRc'ƩUpS	=Y\B.̮D}p /٠N#oƽa
	lCWܩtr!j^UJ
0,r/h;_<ѹw36`&=xut=\hxTSSVлEW⿧QhĹ/4qhFT`yչ}k6t]/&'^w8]UP\FT (XPNA&6YSp:ۃm+%QQi\dyAť3p
c% 9_:IivĸdC.Gm"\7\߲훽rJ+ȋ $I)YzcE	Bv:߾9Eew߶ M|b"ZX㞾sՖ]SRVkwrm}DsVn%*Q ,bˇ_Rh$9RpHcZm_ڪ*މ	 Rł5{ >BQK7Qx<#;ѻnTP 6`{_.ֱ%*	 u<SjV>Q|`_FFA'oAzqj6`!8Բm|s] *3U^i'*@(Té??K+쯷ExԿw
m#d
a7@}B퓯j<(J &TZ^߼5EbXѠjlx莑lۀ뛆TPK6r- pǟg	Q L&D^ڿ@'n!AAzlۀu5lWAqlp.5P_ت-
D(J &r2=G^³vh?gL `)m"[ځ}-ٶ+t+dzQ E	$%g쬝(Lг:6a{{A٬jEo˕p!c*wgwW1L d&=UeUvhI[hH^zj5#*[ҒZi%ysENA^+ݤ1Q nI骲;jwM{nRn,uDsz3
G߬pbɼhD% {\|o&5=*[GmoΚݰ7VyEM FXv:DTڴ    IDATF(0m/̑f`	[`x>*֍[ .U1ӣt*# `@kw-/=5~.ց@ڷ˺[]'Rt2-	!xGé?#{ `06տX àyWI] $y2|%8v2ˈmh.gRլGPhZxPĽ
oƶXhn+*ՖGɟ m~a4 A,ZKY^}nQ.	,}rZngUf j/1-J9Dh %  sUCԸmo*	ۭ- w\fv/͑"*@`U(Hd,T?>Na!DvU,צ<
1yJrF> CsWmӤд~mٹ~-׮rq4%h8\"P0*a.7^öX^1lj\AqƝ h
5˛S) 7_.HA@aƲm~ẫ\o-ηЄZ|5f,ۤQ}xztjK FVQ(z3\S@ ЄߥiiQhzU[GfB^]"Rmy8AeT MFY1	ID&zGO0oAe01jLTCucŶ\a y<ҫ竸 כSۑ7{.uІ@o\?Z;@^K޼SP\NODDQhdOO> ox?1+ٶ"\Wb6<tLN@㾓f\ F@#IH}0G.oSۙ	ɶk\60cDZ6QE	KߛjNcԅmbܐ{YaNK%0
ϙ汒8O%Fޙ.{&f7Ru5 kޱ^jV>_ 灢J^g'&>h_m~ex3Rmڼ m=B s(4j4m
#wSV~-ۣɳ cB9U
@-%rf*5+`wkm~y_}O+b7g΂=Gz:RoMDЄ(mS-v\T fǡ8E	L^Z|o3	wCH>:XM#0魩TP\FT:P .5w嶳vDhrUJ#\kBRzJ ;,%K-rI|;j:B?lۀ_ڣ@csu4%*z>BQHGxj (Hom"ڀ˺YkP8Lj䩏^bprJgdpl,OyFm{WjثIH0??/FpbSoK+l35mDT׺vhPvIM`Ju$1UVo#"w(J қxN\aP6MvZ~:[=s kb:d@%2mzm9V|l{Uwp_RO .ЯGK5l!Γ `j޼tr
E	<m{D3ns'`FoUp6 IuVJ(P V;L֗K7=yHLҿXPo$l6^x^lATe'o9`9sۂ(J PP\?7SoS0q7ٶ|cH5L{H#
oszo8~pKp
й]k==m gZ\RxǓ'R4wV"EQ87.Ѥa<ނDͦW#!NpQXԨJ) K%z,ް[vmVI}א~ض|F[G4*61Exǿ>F:#@Oïqq7+8( 2m9r2
˸SZ~'ߡ(RP\?0C$ƶOSl ׹Թ]k˴'.1|/|e׸ tgl ѭc[Xqx2yǚvSL[LDW(J gxgFS1ygX=:l գS;˴%=@U\T V-L?zJv""%DmܭUA M||"-T8w>j*E	@ґ`[w\A}{mdzy
M^/P++`KLzw=m9thlmګ)GQ~W^Qߢ a۶OSDpe:Z-'r IqrXE	ykwl"0G}"[+,4mqJ7DjVFD`i%෎&o	缫$vGQAl Υk6iKRF<2 6-jrJ,RYE^+515oA"8(P?>N-yp^:L[8OKNޙED`Y%8G<݅@ ]H˴O  IDmG `I%w9VcCwdp:Jl.( VhNˡ(r,)C:kOy!AAzl .P{%s
 wZP\XE	{C|:c(Hԏ~=:xz,T/Yr(JowReR0褪_zH.T)QXRj)@1X
#~-z{0.oA"4$X/=5^Ú"mg [=XE	X^naޜcybMӭ .B
Fמ 'sW@(J<תm"c(Uzw`p:@3nąXrh{*Mf5%0MڠSMIR`x5%*E~/4DB,o}:8GQ*M76SGnp	"[6L[8SWfSVף]=W

l08X==JF0 B +*?+O,M6 T4ow7ze0˴%~ԥGvggM{uU]Z|()TZn2r
S:Jm0{dZmޫ{o(JޟL%bah`M||mT--ԚR.mD{hpߞ/E要~Jfvu*=lLktAjΓ`N%`9bk$Vʶ!hn#U6{ >Pw0T6VN=:Ӄw\L-ۼW+QiRC/xS(K)-ԛSՕaA~=uײmh -í}iѼZhp4LuڷhүGgĽz[4ovEmح*ke Ӊ޼On3X{o104Y|BC
@ti/I6e~EMZ8S1o]0a5ƹz	(z ^gvhIU%e\Uݛ4wW}szqje'Ŵh7'>?79[8E	XBiyޞUo\mh`-ю
r7lOUݥ!_Wk= 3bZ0#%=}Jk.ek$EX3l eVJ}^뭉O{n6o&B}( Vo {K+45j=Pmx1ѩ- UVJVp1ͩcHMy6=}h
	m-ZK9ɽ fVZ^\V۬0 tl ApP`<N)J9KR4ägGj٢6}r"`f S{wRmdڲy}x	@	
L[m\/IC"[{N;yI$"00aaS:!@#	NQtrAͣ$.o:/<zvyL^-mA7\ݟm@#J	ԫK{şX qZng=;N)ВM<"U**-3Нl0u2AZ)t&-/2OccG~.Z#BAQhht^a><F]:mȓ	^5XkE	吗ڵjite\y/7XQCkT9QGm";r.E
%;G.~f響zL#U1Emحq:mE0`H%`*SlPv~b `1/ZG4ׄxچ=p4oC~4%J	Yr}6?o:}xޜ F9^uܚ4oDDQq2=G_J LGVvT䍮EX3E0ct&/}M不[ v>ƌZRBRoZF-Ll)ԧQp̑Q=SN0'g϶	YS7L믻rmuO>&DQ|>K<[$:HmB{$`tV:SrsAM?o޷{GqUux	)Z0PUTi$~6@ hi_Gv`<5n4%`:止á(Û`*$:H}U9
m > 3$EG}vh;G?;v+*ռ5ۉXR7%Ucx;6 +)J:wNMD$= tfGoViy%aPay<[ٖ0S6VJ$C{5;{wS	L1>EE]SE	ֲ{u*=d
C~j6f1rm >J`VJ7ۤoJ 0v:FȖlܣ"C(C*gKOѾu~m
	"*9)JQ0.Ȗ7H`n&[ID`%`HY&`ƪ]17ЈhҽS;oF `1}JH$"90,-߲@`pzhH*ep`iNKm`Q]}E/Pk!0g/#90X>tlg6GmP~qWfRB4oQ4wlw"M[E	ƽG*Τ	$l6^x^las,I[vsai.VJ^ңs:K&Fn7Cn=ty	immeUzw2Uګh~PQ"yԀwnKѵxiZy/Pa|jr
L9oszz-$suJ/ثx6#[t#֩=AG<6-j(C((.-jlx{:_Zj[T^iD[X)aL]ڵ"gѩ]k u(*-ע;|aղW;L0F7ŶJ?;m;lZ0f,7!0˷Xs(KL1Շ{o6ŶW+GTTZ-k@cM|`E+횻zA({wƒz#|;4qj6Ӻ݇-eY5Y)aIw(nzJ*	E	ԖqFhH^ضU~hA#%>*6,y<lZaLC=7T^6(rZdD%gO5,^[eM|fJgi<ނD`@&>>۲?^űm~*]l6qє3>*WyP\POT;b:apqame4Op߳AgYhhYirJE	u;UTZxՇ]:w2ŶZ̼"'0)J	Iܮ`2jYԒ[H`vє(>@DӠ(&WQih1m'ƙⰹ{hxm JJ+,ӖZsA&-+ #X@RVo؆&AQMn֊-b6i09Rt3,,)Upr?
K-Ӗ.%'-' #93 *o_CD(JIkx$vGQA߶.(#QNuv}<nQiE=q%f4*hRSSm</zݮ~63*o	Δ0jS'Dğ$}%и(Jh}3T-Xj'.LN[ehKl0GOZ<)x2n_<t\(HP@NnIض#mYaP'lhB!A\PMNǝ ĝLUA `i"h4%$2sz{0oA"$(H/>5^-Mmc>|#"E	Mؖ-Fsx-aO,AUy|(FBQMb⵵ɶx(3D_,\˶,<vɹ<ض@|rkIZ]jV>Bm,Uzvփw\@cǣwg-S^Q)pVzG.|𝝇@ʼcy8Q@hOH0oA"4$X/=5^ÚEv@{$Ib˴岮]$5+O{jcxn 2mzGQ*5+O14'ޤ>:ufir8]\4
RVQ2=j !%Sj>
1s}OQoVc~8wW=popߙ˔_̶|dZ-}Q0$dveܳ7qG/X AQ@9m14oAYH^|rEGoٯG$
8_YyEYUpQyj?oLіM'{<D$4h43Ө?Zu`י[iQl .TzverYwVKubveޝD6}1|4hP >]%1ow7zm*,)>.4gZ-+alӢ̯֫ڼjAŞHo?Zb͕E(F15jA06&>>N͌mcƽl .rex﵊j|Y5PXRO ē8p(Jh>Y3ݪ]m#-;__.ȶ"RM$^x<sT\Vj$"KFQnTqHw0opzovJٶ\<˴[Ƕ
	xb"ISסɖnw"+a-TzNe2iނDxP=omṞ$!XKGÐQf~%naYcIDԬMp[ճs;ÿ\͊,m    IDAT[A8+aIiE~/UQiTw:}v-4.h09Z#a.QCmrYHΕOw.ysuzNٲW;,ѰݱO7WViQ@bKN-Hk_?6Va!Tlb
>JϱL[ۓj"1	z/T\ZavuH`5]ojx6\4hUZe?<˷mh/dZQa٥=W$$H/_sؙd^|+$|߯yزgʠQ@X~WOU[G6ӥwg-Uiy%?%$[A}{pQM4AbZyi`w8֗K6m jA<āFQtiѺ]cћzN1ɴ9]n˴gPu;onRc_f,Ul8ŚIq(mPc/*%"`%pV8r:oֱ_gbJ^U.M Ì=%ZgǠ>0$X`끣jWqýX˭k}o7aL{׫\0A4c(#\amޛLeUFr<:En*e8dÄ3QXR?7KoL]hC0w/ORqܖnܣҚc;.Ů	Jʧ7	&ٶm:ν4D&I	:R~08t6VoѦ=Gt17}눦}AixXQ)Yf >;Zn~rhFQU|STBrζY3.?(sMX3vST=|a#[#RTZ;iɦ=?PHУwݨ`8!:ǒwf3YpU;k*ٶ4Ex<㺫'0ﭚpZ+VvŪ}~]uy]٧Z7ѩ\O֮	yxG|.RmP@j%1̼B}<_?:VFY>w@#q:;Y=B5WWŝJ{rZEnAZE_}ե}uF]ڷQDpE4S07Q?uTN^	+[~ 0E	4rm}Nx=$Wj:>]Osƌ&$ghẝrٶ4cI)JHY\	k(s
S6Fjݮú!DD
eц]L,=RzN>J<_y%>}aUjgWMOTFյi>D̊v;`.[&IvĎ4ӴzG^:]	@wR߫ra߇ =,IJ׎CǈΉ.݇UZ^DBө}2w^4WMĝLY]@~Q-M~H*] M[	Ή.U	o
8xL/3]kw5c*zwRUTm𕘄Sjψ `$pbNeE	\Si:ĝCө|}
5?<cFLck+1rHrf-DpV%pAI=Ajb8M|{6m061EK6S 9jBjx 0޸Si_JEQǀZv8gG7*wRUګg -T`0 0xJΖ@(JEmB+gߙA $Kk(48 &iJ+DQ7xt^a˪Z&3ߙKִ*)8Xg` 1+Jy
 |)I;ڸN%p^::ax<UhÞX=TmYYEޝm-v$2- `"8/ћO\Wͮv\)5+I IWna4bp?p ǘrCD=%pNey8L?8<J˵aO^|g;wB+d`PcZ=Aq؀3 0|E	{aOjdz==JTeRTR^gGm01	k? [J3r-w.E	=x%?@NX#F?{^uyqs2HB 	I$) C6"CpV[[<>U[[W	VF3Ny IH I;_%}WoO40ƽ@jl>Kz*c g +6S4@(ں\ҙS&^WzEQwҊ-{Q\Nۛdc.mp3 lSJ8܂*B	u,p/ժr-YSOVn٫~T5My ƲiM6fLD JU߰=(!@J*q!:87>^B$iӮCN;uԥz?DD p6NG(f-ٰCu@ժⲊ6$ -,Qr	\S!K+Aq
hTFK\>|8  JIg*-;q' l:݇pwb
 ]GS%дwR pRIwVr
S0  u-߼!FI Ye}.U% xmz@%TJvTYE  'j>\ QgUpu8vQ pNN%&DA}q*,8!FK }r;8B	4P\Z{7ـ 8;g2Lu% lS
rh`=Z! kt
ǌqCU% YWF(A E$p)^1~' 8#TEJ&9ㄒr|P  .xyrw )m[P pya?oCL  Vlޫ
*%`l&  uxҲͺj' DŢESD(IҾTP pA78zB	1=WC  nV!$i} \ٷdyk'pAG'
TJ@U71u \Uqi6>䔟gLS% ["B	h$VTQ pax}N~(#N]5G(-]7 qUϻE?h	 0*}z+p!.\gѺl \hN<Zb$Ld `W,ZMuq.n  _i?[!4H( J*z~
"%\*v  WgTv^v>WͭAA0 5^
B	fhS֙ .nNٺvˇrl=GRq B	i!Uך) \eJ+$x?h	 0Vl.P @C:^<G\; eʿ߸K:*%\΢;6y \7+6juGk- TQYewS'G(~w l)DaIlwULz)q p ,x%\ Ьԟo^Э!FK q؂C)Y:t<81B	trƁ&oz  @I{4UGR>y 0V%\жģ fsz-2L׌Ɖ c6VEe5qR.h΃ pF{z{PIoD?h	 0ZsVlK!Z#'  lsgOk|` fIJC3URVI!  -Պ2;A8 `<8<*%\=( ō2-\?Sw\#_o- 
#WD(b6:d ]GKd,o%.!p!e:ۻ ,(.Ӓ;_3~E?h	 0
F~;!B	an  .Gתbq?\/w#	 0%XJ	%  6Z"'XK]`Dtxv| `݇U\ZAE0-1 pK}8ѕ dZ]J-:"e iMN~md2鹇nQO\
 `3ÙJ 6oZX<,$PƇ	& ^>)JJϡ"NPElu if(p8$iĀ]{e} 04%\@ƉP ]*Ԛ]>ru ~l!wbg΀Pla }G÷ӣa	 hgEږxB8B	( N \QyeK|֨.zbƇ	& /B	'gXq
 []QYs~<r W  ]nUVPG(v>Z
 mRaI|_5SQ]b ZVoO!۲yV  o7|pOwl?L0 h&p8:B	'a(z  Zv\v	ҳ$S(` ږ_1SF(
˔C!  Xz];fo|` d߸B80B	'`2E  Z;oVoMԾ.q*  X.P8x"  -Յ&	LҳRР :Ii9J̥"Pmz jfk.|£Y0 %Tv^rJ( *rEzV|I0 mؼǥF9B	'z nVT_-so0 :uGD(d 6h6Mv}aL @Z%  [mJ~8%m4ol F5[\g"P	f婨B  ڥQxXnr0L÷)&W mwB8B	'z /~PEU}noO]
	 FVnKb=	 @;sK+?d'T_	& ?Pu8B	'  [qqQ]/W ʪkڴp N&-; JzyUnz;Y0 2p8B	'h
E  7[0_[鲅]^L @+{6:9*B	'HJ7&  6Hj~	cýa	 h%:<@!s  CU/KY\w!}o! 6)P)+B  LEk]#.IsΒ[ {g[b2S8s8E  ּkt,3ץk0zPomd *Nv$R@(DI  Ԫb_ڥqH=wǛ]9 5޺"8 B	'^,r	 0
;L*/Ȁ3׻a	 ;gLp NZIi9 `x/^4Cnz)߷a' .,lcJ8) l?Z**](qQ]\:4 *pHL&  --(>H9Ds~s6 \fdWR#pIiMބ  ꭉa.
!)SGյsH/L EXZ.F($y  [B")cߟ){d/L EX E00B	'[ȼ\ #UյzngZ'IA~zi^D0 ٖZ31(B	'
  Gn,Nһlj?=|n4& uڸ 0(B	'hK~ p8-ZRSL&j=ytF png]	"p) pH	_('sa÷ӣa' 3v\GEPԚM!  `,oH4=}

k%	 hZ&2"ψ%\rFu
 pd`"9=G[*Hzw*:<& d]	#"ppGS( ,۴Gt	?sK <[aٓJ8C	n1 7%yCUKOܡۧk%֙ s()ޣ)`%FJ  -5o~*҈d}3'n_B мQ!p䖛ժ#L  8orǷ?M9C3 nٸu%PcK Sۺ?IB4#2,XC9  MHWv^0B	v45"  QWm?l*ԋϮ$` Y3"cK k^kgsԱz;K `
J	cK 4 VIiT,{|mC	' @Ҟ#)*A(% 5ؑ>O'
YvKOܩo"3z;Xڼ00B	W
= kU+Gcn<Rs~`ssx޵{a*)="  \-/3UuC|t~k\@;߰{M8䌜&o.  \znΧzrs}~6ޞzrt$AyƣLL%vkhXM;DbhZ}q,k"WV4팑*  Ӡy彯Jmq@hrdD]o=u_a!C3/m{" JJ;A  A0leT;oOݥ_;S|[Q`7Mc##\}{D5L*=n4o8 sE9 J[%A7MEeZhʨK5dVoMlİp~Buϵ4e L-Wh?p85Kť

툑(9#GVVe /~mMUCp`7ϏVHS(FM (O*}z-$N5RvF(XO  Ic~1 A>_>ŝ* gtয়8Yi4y<<//@h^p7B	~ UރUS8NUS_o=}*",{ @'볿>ۦ͠/(.v|wl۟֠P%1R ڸy!_C;4.kjz浹:b@nOg7Nn. #nCqGUThr*hG(5;" @;0&tfL`Jx_^Ow6e>92qMk6x+FHҾT<xٶdЎ%LN% ڮ)IfOӳCWLVTQoTciqpw~H~qUOF5:E}[
}Ҟ%LrFE ktɤgOSB5z{5aFfqit?>tԷGw '}/4F<|}Si(y987(-;.΁cPӰ6waܧ}gnQsF0q\^#o?>|"_kΦōC;[֧/='%`jF+X-    IDATP4%HGNӣOk+OߥQtЌA0݌~}LwxN&na3KO#Z58oZ3Vĺǃ8,FJ @4DO&}:thxWH[t.]F5>z|鱗O];PfҔQjZanJK躉50{~CK6IV~OlcWvC(`y &47	ipǧK޸KǬgHeMU#i횻p
˚qI
Ӵˇ++c@c'
5\C+DA:Q6b] 6ӘN=my˭?j⾙fH{5TŎ<<5_7Oint og/^~vݤv	$:7}G ъ`
GC(/RB @+nnzbt_b51q7?Y)Y?4*۞mDQiw|#/O8yfoW/mR>:`g}c|uȁvemotΗ?(%3@;$їRB	47 m8yǜstVQzZv>Z~LU5zߨR;WFj}x:;st;0K{qCthE+[bzf\qEhѪἼ=";붫e5uww7u'>ǧ0 [0QkLOyBeZIhu>~6=,{mF15cPu	5Yyz_6B++(.SjVb":QB	4%3" @+6Zx6ΦFGn50H0aJ/0_'
Kt5WPV4WCibF-]i@nnqIOM7D#.I8)(./_@Uլ#^vL& @ĉ|  +Go\Tn!}b'Ӹ!MóXw^>Z9E3/j-:<TO>CϜlѢە_|%.8Ȱ`M;DS>֬_rK$#1I3J) VkG)c޺;_|v}Y ~.Vx@_1r>r<Ekk23zv"|4nh?M~aUjV:t\VPM9 hzzm^
mWe5a`bӞz'l- ]F4IjpXVTiZf[Sc6rB4AcѸ}5w!g4jĦOhcUJJQ|t8 @}el
 )cԻGdOc-|FFM 8qBaC-	u݄:wh=*>W'E@Wр?ܞhJ a {JJ  MYWji;9$HM}c'{qDQi7iT^"WkZ}6<H@!I}{Di$;K7Ի߬ 0{`ϑnp*B(B	 hF}|Ѡ^'W7ediDv&jj?>]^g:]Ҩdh$ٞUZQՒ-?^#/Ie$( l{شw1@(-r
 `qJ0KԨ;_a h`AGVhJ}`<i:={jxzulOm*.hi!$e4xvEwDϾV+צU*Dv8- `p]uärww37yz_C_,ۤo̵O8a^6:;+qwsӐ>q'NOΞC)ږh?BHa 4?/$hĀ^
tlz_b:4݇JJ"  vj{{yۧq 5ѩc g4&f?џE}zDQ#&zwݻTU]]i{b?8Al B==ԫ{'t>qg7y0$?ыJHpHLE%pZv~1E  ;s劋2h'
M04P%)T),f`>ޞ'~Im{XҔ[x$_lQhKmH8݇S6B(!Ɇ) 5seQbѫs*g`\g/C o֙0 M3S:c:GTTZ~1qgX.	1Vh _Z|W
QB	H }>^zbtx^vvLj`f{eKߡNҠ^=4W۱83\e(Ң[^9MaY=c"3&Bweݕz:=ϴa!	H^ڇBJ ;" sgfnޟJ:N1	[g.5+O=~4z`oWCYgQFNROYJSJvkj۬ߨPxyz(:<Ta..ą*)ԯ_PgH8Ii)C ZxM/"77G@%<$D7O_?Ynn&*,ew7uSȰU*(.UV^2sWe8RY׶څfRQa
N먈N!

f闯}܂	~?J5% % \l[;ȴVomX6Z\g-ݠoR'?1@?>振Ԛ_WUZժQyu**UQuU'Wըԟ`Cޞ<==!o/OvUH@+4_A
@Ѧ=ǷPUu-;x<C:#%v p19A":Lׇ\Ǵ'l݇St㳧iTEIܱZO.y:(RMY򒷧<N^^'j;Ya`_J_ܳ\gѡM1%\	B	 8_N>:vmYʇ @#j߯nkKp~0%?/RPQYOj ?)P:% Σ=7>z֩27h*YuZڲXjV~9NLOLJPG= \f]p1%+W-Z˴h6Vׯ+sn w?Svj{
WgB @FI$A_bi:>o*h:9$imڞP!U~-6/RQi:t6B :|Ьioe5[tkTpP;$u=cJ\^1 oRQ]B}8cچskAZzf718Zt~V^sI<NZ7 mAbF٬-Piy%ygm|멗ו[*χ*v(%Sze(8={v$`2Dk"08suMW:ĴؤĆ.=퇍#z5q *΢Gj6uO$J*o\^1 CRdgO8OW]zܡ|g^PlFRz|m[@ŕWV7
P
) gc}u~r3ԚGDqηOҝ{C_T@[-0GIiMݻܠ.()-"B	c 4˖>v2mӥtxY;pgtnk'Kth_R*muo/mbt#gT֔02  W+SGS2$?#RH?UZQVn^;hzW()Pn P7aXmTyUVɴlM{aAua1'Ū/m;߬Pl&\#%ZS hi	n,ou:ECrF8QQU9kۧkpXѴl߯t<E@[P*XEef=rdpJ^Yuh3:Dizܜϔ[.5WSYyj&Eh02n @#O1@ri|8_gpMhrJǚڴ簮21m:R)6>|DyEg&MfZ+ 02n @=YXp~CLۘprhbNԚp6xHrhzS)iyz}8la
%"<,Go@fm*k4w}rnz8Bޞ4J+UnY,+EXU20o #6e~IO>mF]*mQZ^\/m/qCap]u]E}9Ǹ`3seZq4V Р%4H͜ /IW+9ih5MeGɒ	4|h<õlsXo~Xgwf6)-;_1(J#% dcMSh jꭲXi5N((֟F}JMf^jV^%gJJ Oa?dZ]hKMEzu}`n2Z3.ƚp:ezZnV֍@=cm]^1?#JVVQI 2[*Sv<=zJ̥1l87$)To}Tլ+GiZph5쇍d:UUמ~ X:;pWTS gOSp`ӻעu;#h2(.^OukQZp(UK6;߬PaIٮOP;#0* ]3~aieUGi2(k5y@4iCoÚJ;5<`gV^A(ufwi~R)YLۀa5NTUjmZz.]3'טA}رsx|E,,rL1P)Lz`itNm4NH΃ǵquˇ:疙[w^[LZNJX	o paM	bƛ,aMkMWT]Vkྚ92]ߍMÅk4VY,纆zn$)-'_IEP kn \ULD'>m<=KyzRp\gʭr>FuѴuA,VUY]ӗ6+-;"A W'fOW{kŖLۀhvjGrzd|ꭩckh8Ïd04V?J+v}.#.P YOa1|gRo}T54la6iZmuK5}u	jjղͻ+%$(JTq9I p==";kԱ1mO(^4}S<#%-,ѼkѢ5]Sָ!I-Vپ_~lCy
eJhd N]O>]0mcZ=QVm~ojعv:]hhX[VPC5f-XM_Q9- ñX*(.Uh ahw pz7MqQ>*/~`\YEuM6:ew-R#֨N\XAqۮWlVqiEK-в	%P % }z6Re҈^gbZ3uxv5r`/} ӴprK~.9
ǻ?.BA1R0+iL ݩlrunaުSCЈ4zPoSA'SZ^w\גpgIY^%} Lo3mhi3BZm}Xw֫s(.F襑76F܆wQY[-(vB(aP  CǧKSPL8E}h}51 A'7t izn29:g&J^a1EB	bM	 0[jL .9J-۴G6푛IQ$&tӠ^=*غ]$N2R^%5% KTSk9yX,VN,}|$):=c4w,%R^a)EB	b d,	m mr6Ҳ󔖝kIi@B7Q(G͍5)KQ.\:<(E"0Z|muB m(N/M$oO%tThPn9T6.Cݻva.!erJڑb\$B	 ЎrK̴FlP]kޣ{4vd@ѣah%z~7`NP%P ڭcꍏ+"V4ݦMk6a)}8v]];+.&B#Ρ,/OcpX2e>Y9
gy$jO\jbb @~.m{\  (4lPQkӡ,Jj EuQlt⢺(.:\qQp-tuuXv<|<(*%B	n8\Eڴ2rZ_h<mc:B:9O<TEFVL&uSap%tTǀT	nn÷Ԭ<
T\ZNP )Ղ5:ryU5'Ġ,VbSq5wO#X	8U[ێWH]zPϘ֪8[/?u~%4aUC(سi0f@`ߡmr }_4	CAq69M{:c4w..]԰@zwTU][<_)aUK(G/{߮%լGyI?c@$=
Uϼ6W~y?á0R.% f,^i ZAEQioޫͤ{h5L.4cH8=sLo׌g-VM-OAr@YvvL>W Φsbj[b%&+:<T]7Q2j eV5\8>(11| Ffnޟϴ vgizn皻hKua2(R@`e@?k-:w
Kw$ MJ-ٰӥ>kLtdK`{ 0(+ VoC<~
 ;05լ.:x,C3wFT G%I!vH	 h]9p`Js|ԧGWQ]?.F|]Xnn&=zh*WRh`eJ @ki .Y7>=JwD<Oo    IDAToϿ5yp
ag%0&FJ @z's<tY3^Gݍg뭿>qxmW֯1e޿'հB	bM	 h)YhZm HSi;.&+#`=,ei0m`'H	 ?s]^HeМfㅐ Ms^"E;VO1C/0qyFpσ!A8*b'Ś `_-̴ Sc֔їjpXaWQr	}|ss2{XfAY  vt,>Zi l	]"_o/*ՊyӲv̀a:hTĎ%z՛L+ vRkk_DM'o u6ss3i^npKڰnϾg6;U#B	rwsO >aq):nnrQvU?#@r@MHioZ3 \tz1-s{?/]{e

Jolq>bϋӨH+ 0(NWSk-TYEUpE}~>qH8i:zS%kxmwH7FK]z3UJ; \Onј`H:+Gۓ
L{Ͼ)ihEh {յsi%H	 8S2йh ,8P1CbuϖnhE02P % UyUVɴIQsbd#	Z=Q/tBfGoJx% p>YNGR
pbĀziBC&8&`g$w^1JmP ) pJZYum 'fO׈	TU3'7+~gCSt˔ѺnpFS1B	b Z|WV?O px{zit1v"\s߸K?=-s>pWhW\FN% p-\h.;e	?_.0]ԩc-Rzh@Bwy{%nob t}r6 퐄Ww_a⩎q}l
_.vG).&\Q]nM5P¨' b5zmUT1mp$?Lx|z<=tE1@qQ].
WLD'L&*(}_J`LL `j%3mpK =MMu\ȴ1Cu-(mw\<=-2Lኋ
Wlt%Dχj:8B	ޥL w4UWoJ{pdG&1;0wݖ
=5!.:\qኍ
WNrscSӔ' pNzuUV?L:8qkg7o(&]_pW0Ǆ+>	r}_J`U p._hN1LʑoDy1:幻鞙W0Z fK+>:\?dLD@(aT> gq-ZitzBR8֎O&y*DA놗B+ދ-`z[D'E!([W0]3||( 4Jh6  gpwsW֫sPVZ"$xF?J 4oW*%i3w<Wk!Tͺzzە**-N8{EFw7u;䩵zuT@_F(aP> ДIZa'6 '$EGf+:<wͺrrq;~8FEt;cJ`Lބ XYEd	6 '8g%oB\{p[VU5)+G-)&"Lq'gAJ |Ҳ" e2Iw>^~H[䨇/=2L~@#0(B	 h`Ze/6 'XzgiXx*2Q5׿ƬtsNŞz3$ȟE(aPL Wϖa		:ZQT֏ьMOTPm͇.F?tfA% O߇^" UWlT]
@epf]9J_-,Gԝw NL% p҆f{ γ36_bAKGXpO+YpT1?C~s#0(V BsikA1~=]&n:~(.wK):<Tq1'k@0P |?hp9l2sY8{;*hĤhG_P{rCtN۵S% \L fw&@UЪf]9JnnK)*<ԶCl)8)YJ WVXR93mppN^xVGU@ԿNG@_ێq'Fv;' 8OC(ӥ)(@p,^^J*hdSF,il)77*>*\1qjN~ P Ze<מW="è
C7+TQUsuٜ ?#O.<}rOF?8reÂIר83Z
e/\08l3):<y{yjCI];*>"Eq
pb*-;OIJJQRJ2󊔕W(૮aEzuץc9A(a`|	% |D'8*[ ѩc}*h77NvGdgD(.Q~pD%e:%()5[ə'SZ^:x<SK$EkCub$E܂
%ܲWwc!A0@}PSg(-+OIF?g~Q_zezŏgM5H&%E rKNN03mp@?-j@@V
NGSyB:˅u-m7joMzڑӆ%,(PZzL q~uv* 0YSCe|GS0IJNq16B(a`( ]ڲ ڎ1顗SqQ]

Ku4IJJQJf,:BY?J+KNB	b '[XwYx 5#ol4U\\YyJNϲ~8ʋCpbJE}8Nv+#0 FJ p^U[W1J0>[ a2I>x*+(.k0hZRre\kzkg;_΢g"hEH	 Nlڶ4R `O׸}
$ju:zdb:A0wԻ[FHi%h; g_]-Ok.FU WTiY:VoCZv3~c{/4υ
%, bѫs*iiHL=H͜HU PSkֱ'^e+9=[ST^Y}1vW`&*k/w
%#7 8kkdWl A qYx=sLPnAQ'ÇejeZyn8B}zDQ;#0@p.z>6 G?=|L& QuY2ri'~HV4fS;#00I iTXRN#pt	+O-/O@[/VRz~Hׅ~{53IҦ=u<ㄺwLU숷J p
߭ު݇0-kGTh%UյJѱzeb:ӰEwᛧP;"0~- ZzN>\F:m HL&m*X'w:9!GIiYJJQfn|N?mM(agb []EHEL D#a.FUPY]Sk?g+)䯪V(-;O(Jۂpt߬i/NMC!sZB,%(+NhvD(ap:R +%+We8L?AHEUӳ=5N6#)=ju& uuz}"U-pw&?ZiCҩyEөHʥvD(ap&f W7kTcchăRnT.Ԗ9_Tך/}J+)J\'FJ p@2N똶8zꆉ#(ޖ}G4VMVN~5.·iE% MN[҆?EmDdX0Hu.|Ivj=777yxTL&dHH!*ln(.u߫gmu.vڪ{mmŵn '@$섬CI0	;~=8	w=Ϝsn֓[TxlM Wi*:rIeGh]W{U_Z+{P/9]ڄN'FF)X XZO-yW--0A7\{5Z4t@	 "(B, `Mz/#t S*)ΈA舡PcI //՚?D W,P}T~iLIE@Lf+B	pDQ ~kye6 u%.`$
^a["0o 0&?YЬixq_;ZMf"#B	%% CywՖ" ሉeS)J)z,بp
я%L jݖk﫥e	t`sl*{	H\IIPSilS?,	ƕlhg=ҍF(a1QᲐ06Tl`\m={9{ڙ>LqQ.D6o&`*6ۂ0l0Ozh Uga	s,d2SJDJ|E `#, ֙MOҩ'"3ƕ0+B[ғ)B?#0X כXŲ$:|{Y
t!jmW.!`%Ĳ_#08 Vߪ#:! 2DYF|t~x<Y:w$'Q7 0XF ^uAE74t0=(P̛FE1h?wvW.[JDJ7 xן_mV!	$Λ3-^QQE&t~"	eJI:% xgk7o/g`N#&Rs'"@/?DݰxBA/	?&@(a
 <#_ͲSO-.p<fTO?BL8Gl ep;Pw
e", ڲcW$ ck$'i!Tĸ(u9k>hH~m%Lۂ7wWl0O. "TQqLn?0[/J(yp[P tɗXЏ"Ctid4}JXҫs6"	-( 3/z7%.?" nbH40;Mܢu[kknUӶݪSZ546v5^x%/#IITM%L$C x~$4 T40']sҏZݾնӶ_U;k.	%0To K:C0O9" بp8{VU;kUUSmջ	.jrojk#FX
N&7P ng_Ѷ::5I;Й)aC6#;UVj,wk[MjؿC$$_{JLZBnA! k_ɲΛ3" f[$GL1*+5ө]{3VU;}g*kjN-xwB'ch҈%L&%P{;xX}em &KzRƗSOxE	QJRyQN9NUӶ_5Vuxc^t<8 0x -~mߵǟ:ϙ7eӌ?fXhi`n:vכn|涝YG}_,^[/?]AT@(a29	O , L,48HF \Z-Jv(&Tjg>UzߊoE;jWvWd$R j3, L3=cPك^ZQ#是5jg^U~XaQUS{{Qc[/_R٣%L&#9^O3_?D;s xz鴉#)0XԄcܢfz;|sg^p>(:e8i[ go-_wW|I	[p/#PO8I[ғ┞wך[}U:USݪY]{y#g*w#xSqQIK  WNI%K?UcSs_. ` e$+#؍mm֦uXSy[Ϟq"+|R% YH 
H?0@SNq󵆦f8z;oݪS}~nEQSFkxqoJP6w  ߵ}B8sPY( a)+5AY<ƦgU|XTi7Eݾ]̘0y1h 4Pr  ןͬJ! {PrvxFUVV}C
	QDh"BClDFF(aBɎljnn  i9J" ^hp
R(	Y)91[  wҸr  05B	&  4B  LP¤\lv	  |_5bHY 07B	JI  G  #0To  BA9(B  LP¤#b  '$[ 8 
2SɚM  ?3.q:rS>]Yuz<TG%Iq1JOWzRR┑{#0TB	  fа0 SzwjJ}j7GG)51VqJMSZB㔞/;_k
RE  ږn+ʑ@E /jhlҒe+˴cמ9{j}1_
o+R㔞x%rfJ  %lp	x+ݏ<=um}Xh;}ٺ-ETZBofY&Zt%L,9.Za!^O  5$ 8p^z?zϻZS`tJ׮=bbQblض%!ɎX C(arŹpz
 HW\t <lG^]}ڵg˜Sյ{U]WkVEIqJ;$iJjፆWJ\~VjP}%  =m#lp	x\en]u#{PowS`]{}Ӈ cY    IDATԄËhY+F&iK  )Av{GzxCVUUZI_uZ-@Ɏ')-=,RYI`>#0B6 /X,Ұy&':fܧE[wv=ЦX%v	JOWlT8zP#j:  !
RCzyUެX445kӶmVsׂJMSzRR:n6>``N}B Dw ӷ?e'x+}7?^u#MPY|-4Įoe
䖦~Pd%?|]Ȥ465wϾu=Ko-;vˎbQvj5$_E2du>.wd3KN	Ͽ?5vjcN͏bcp؃"r3(зjǆ1x$(+-QY)e%'(5!{xѾ?VXU/T=9lbb74ߢntovAzu񼩚QQN}+!6J5u(paD#Fc
58OY
bL`%ƹ}bVPWmPe	Va6"Oxwŗ:\Ӂ4>|.˝=|nJ ݏai m0lfT0
(M1XF$ۺC^{sWw-"a-nxk(18]ߖQ{ݹMKJrs:{^}O>ՉЩ$6*\s̊rER!?/7%/7u~sU^Dx
SssN545s-0x_@SL	1ͯoI1
:~jhxqe^ҁG:;tFa̔ oM	}I7IWVDu/!?35@GqՙHicۯ֤\Cl
KMeKVo:`lNU?hz*cr2XT2p :mۮ*"BuwW`{`i2)C
O[D@a$mDkSog;d%|,:@cj&kQtD;PJqty\fnGg߻@"MQm	'-w{*bb>:	Vl=c,tIqzK5$d{fꖋ;ռb`o/R@!LPIWt-ndQ,<aX)Y)	ĸ(]`z]2#pmR?JUL	
 +Bu\}/HUgמuxڸY,JKصpX=}u;sU_ڂm5uzk**bB>fp~&g]6y0.VnL尦^␅5h!X4Dz~vb*Q	/y?R3(o)Iෆꊅ)U=P^{1;e`DeٺmW	hpm6[bCe6mEELP)Ȧ'm(蒅|
-%+Y:'黋f{׹s&*2<
ud%|LhpR~Ɩ]qBTn3(7ChO5 Y'{VjBlW6{͖pWTd%|P	JϜ~R3)LIq%`&A67T~,fun"b_	xR|Ο=#CuR10cG"*Ϯ?ORM] >D?^{&B(჆gQMR[4LS;w(~P7 #I7]0WO}N WF8яv&B(#䈍Umd0l->D bM: rDyS7ӧwS~Do&B(FS;iPn:,*%/
izK+?+շ)fmvYH5LPW;s)|Qۅ97=ISx͢Y(*%M.Y-:a Buqgs^ÂYf&6J"99ln	1h3n!Z][:_LmsDM]\kExXE0fJ`{s)|1T^w)܆IMgԳ]NЮD̜FRl4E0B	6tP`vgTFƔ**<B=8B	_EܽNQfշgJ+l%`jzEY"բ
uƖ?B\HtN<J໊L!SGVP 8N7"%E3#@Ëtu[/Cdvg֣(d%|ygrRL]J`(iqѹCIR\E RNjwީz꬙N~NX,9HLPg_	0Ȣ0ɣYR&m OAW5SC8e(&2d%|< LUtf~E(:eOWq^zW)(N0!f('ũb&0$GL2Sڲ}@  [Eʊ4H7UWꭏ<[^ѿ'jL)V>H?bWqna<}(A[>uSCٳ_ɲ5Nӏ.^o^Ʀ^]p:IJyz(Lm\nFg^^F!/c)CsKmݮ/mꍕZRu}E*N4oʽ
u'izkvc^75[83[ZB67+B	?0 [VZZ[)̦b3l mm~:D1gI@Zf,[>^j1NozhѥZ8e2S{:}zM~|]mVeK(~W[UW5%@=PC
ˍOb\l CZ-OlبpXًO[XӧܢD/Fӂic4(| *ՔQdF=2W?f;ebYff~}(K!`xB	`H zb;zշ>f+Õ땛S%/H,/Qyaحg^]}斮^gY<k"qeEUZI`
s(,M.>Yy=d@OOt*w·X7^z'(2< {un<9MPOEG 3BL.:ufQNg	8nha?XU.ÉڽsjWOLY07{z]hVWT-^@9O<J#o1eEZeGi0">}:tIqѪK1pc)X>Z[t5H>Ф^x++'SAVhiքa:y0Z=R}nKw!'j=(G/PTO/A!`F*
(I!@:, z<9p`e8[W5(7]UŐ"bƔhLinٮg_]~UpC2+Δʽ}7HNjd&r(>ayw~F?S7i.XR<ξ=jzMC+LSw]*48?c~޸E>>%"o@&$`.hԛ|J-CoqNlߵG|?{_Wݾ#6R̛gA-&u߉cm-@̟\zL5~PϽi%RJF"YEEPGz/<&0K;>m=RM9XOPfA?eN4Jo-_'_zWVw02(+瞢dG"3%b7-7.L`.胄(ӭ;_^| H\-ZS-Yj60tCժ։u˴󵝃o\E'OШ|xG(o#=TQ^~ھq.0x S	()>Zջ~@#|Sw<7]w50h}jS`MYjJT]{r65u[@Zㆪ4?OJC
ۇaG(Ȇ@WjoŦm5^I4gM9b"5oh͛<Z-jvٴMۢ=K#*"T9iI6C~hDql;K`J1lTCto@F.[8M>DfrDݾz*ʵpX%V TSOjDqn0T64IJx|v&֔%6s;44?~ۧjC%o>|tkP)rpKGN`@ܸTfm]NSo/RWG]~Czr:POU	[bsSآN,{J =aꮫ)6*[(Voҏ~κz #SQGFO%`N,JccQE|O2رk~4􇿿}9 `Z~lҨ Cc3A(@[  z!/=I?d~}6āGם][⠀Jq;'ˀEFPspBkq, B-5þ:e8ҪW}ŷZ7Q}j=LPmSV7L*6:",C)z%);'jli1ͱd$}zn:Gꥥ+C#sF +M̋PmfH?d2SһstM1aLA0PpwY=Ц{9s֟f'o~Ywx%
P?_n0&<=vMdO	㪳dZ2^Y? &L`8Iv@?~2xOt]G8X`Pi~b"*c0B	N7~3sP͙8:#pb?jC,:B	b&*)LHcHqnqrVmSA\u&{ϖt}VC#`»A4i`=2
a=>̍PG`
['k6jmm5.'+>&RYUiB؃<Zyٺ'UG[i	} i[noɥspk% I*LQjBQBx3%`n!vB	\ZR<E47hZUݤk6C:TK[[WfЈ\MUDkE?>L0.xgz4ohzxL]G_kc@D`j̔@/'2SԬu[ij}um١-k/?E>[eӟԍhP^G^[ZbnlrwK0.sjLiR18B	>zHP*OO7mDpWzsjXwݺuꤑl4lnËtᩓ_L?M8B	I֐,}v35.P&ǦDosWVOG|-<Z~nL9b#Ϝ1N_UnכV߉`BZRs7q`g@_`-=<V8)oh]z^H8]1'[=R.$.(<hobrx2M.ĸhmrVw˯Ob;txuݽj?q-@\HQxU:T	(:O9F^h#/2C,݀{Gv.txަ}U۟86*\w]};eA-U\G!>%@g3*XĦW[/.D yu(tEԍ<?qaVnpn	&$Z (:+LQnz"40 .V۵5;5j-mݡ΁7gG|պ<\'+ւic<7L1W[9yQqC)*~ez(;Mu/ƦfO%͏V/y#O|ɼ*+igg>VL LU*BxQ@ 7`rP=`Xw5=`Vkgk<r38~*Wqcu¥`M6S\T.-1TykǶnݳڲ}۟44Į;W!v"ߨM(*.ͨ`	b!9	%9'7tߵY7<>'MO/?s:#m?p	C(.()>ڟfx̮P]7qGƝ7пu[\488U_?_ꁾo.[0՟xQfP(Az̔@J=oeZRy<׼)5et1b;BALB	tkfE.01L((N7-zy٧2sCw߸Tf!愂J[qSRಱzUd<o
~*ԡ7SKb&?-^بp'pBc%MMq|kjn֠t~;ݑé|RU;kQE?=(PIqPB	|!MO^J䚚[(\it볎|BZ7膟?nҼ$]þj@~?v̖ ^i&(NWk&w>0Ɩ't~4$	E(4D1ao[8ѽ,fJo^5맗xΟsv95r2%#VN4BcН8)͡ﯽWOjH^v2SZ׶%+5AY	P(B	)'P-B3:B	\c]k)jR;\ObҽלΚ#@Eh!>8%`vM]7KdQ&
B/_UI㪳dZ|?';b4ah1'<P` hii05fJ;E9k<pnx174IeE؛Lsfb3Gxz%#9F?\pnkhb\SjB, і<}5fw;7CK5yS=(܏Mavp=`]~!h<W1Sy>&8bOhJFx_skb@L<wǫ?ZtUgi>ql  }IDATmW{GPe>%V}c#EKRu&ߴO{9`+T JL=D>~K$xW#7`rGX2S@k&[Wݾ9ol:i~q_@܂JmZ0;q}X;-Ab0ڂ}u{dy:I݆ ^n:.Y)=r(+∁!J͝4Zil$5Jyպ<\gtxSXS[.U#A(S&pF1S']%h^y.$#dpP%'&=ֹ01L(NC
 !3/k=n@[f"B7>)գ],$*"T3'w̔o`B@ x}?#OXa!np~)4"@9BV%/>e55/Lhӈ𬶆xyIssu:wܱz춫4 H}e6@oJ̎44Rt,y# {o#ϕ_|W p☟\3;/#P⬙eE44JZL p%KWxsRMQNZ/29(Ov&,]J_$Ejl`P:vl UQ w<|)=t˥⌓leK?bN],RLdo2LPd	6n`KoPQVfKصGU;k=;Z4w(=q:?ǈ詻ѥ0-7Y	SZm߰{J:)A;fN dF%yyc"tStzk*J޴W?hȒ8|Ɣ(>j'%F(3cO	fBl
s}Σ55{ĸ(-:FѮ='VӮT8`EGy*Lx7S%Я
R4bP>\}ǌO	A7`bj8G0wqDIxSS(AL.!IVfT?H !@IMDā>jdL~7~X"C`0-:|B A(贩Z-.;i@O46J4\F! ")>Z33eǯPBS[ QYa6" A(9ooSl	Xs3:3}hx XtDE PnMǇO0`fjm@BE XL!  [5cBC.;mib`lΙ5AVXQFEP@b­B:{8
v+,;v5}llp~E Pn7whDiq:)Lc_;fI ,+`ZHjmZ<g"@9._tb5;[TYaE PqaJb	vlΞ9^6[ W؃)B)8b` ́f;s'S
wY@"Ns'qg8pcEJ O9q`g(L)Ʊ:\î9d2K^u[."Jn\|:.y$@04B#o̲n5s<\ "G%j	#(ULdXM ӈ>>;@[{*CH]a%q:I./Q٩	G'>yP: Oay`P`]`@)THJvT1r <k|bFE9^[C(L+)>Zq:7;-a(	Qy`N.[000B	x^+A6dQZyQ6?He3cP~ڜDseg`hܴDpёR{MYB|
BE`Hǖue,JϮ_L0B	xs%q$^'GL$xŢ_0 UgTO1mQ_P%Uh
Ű9IE`bѹO>>IҢYh`x'Ȱ.c8']Qy~/iJΜD_p+NAE3-SVҾXQv:B6tܼˬE׳CfOoZF  LP޿XnzN1LqP U?T65%_CuewVUVO1mĸ(:aZ]{Ɋ`Y`JlEC(JѢY?|QrR'E3KپehzzR7OߙyΛ=QWMی8$407M%*T%\>t;=Hbe$SV]~v7OtꤑBStPnz5K#A*)֐Le$o'/0uOg'<)X;>[#2w=o&j-ҏ/YȲڎڽ?c^0Հ$d$?SYiZ9 JhvewA U=gN(2Uw?о9(O_u|:;pnyI}v/;	[U)	LV^z25 #Y!vb ~{%=pH71ZݟN/Ia5(/H'm޾mԨ3.CϿ^zos %)/3Yғ+ B	UcS{z{sф;48Wy
BKG/奟ҾڴxD-6%Y7UgS*wό
W~Fr35 #IyJqĊ^	%`tK?[?ݷq 
ԥj	# U^i߼>IRya?w1T
>>^^|#}jzJv(3%AHv(;%Aa!]	%`4WWp}1 U+ʴxDGGP>\M~)ʉ_>b/5jՆtfZUM

)7#YAJVFC~ %`*[_nqϋѤ5l`d+,NQn:᪯x"u_bQfrk\YFS  u]&YjˎPC[vV]OMÂmrF)!6J	1JIՠv)mܶS7T곯6kw~rEavF*9.ZY)*N^  r&      ǘ     +%     WJ             ^A(     P     x     
B	          +%     WJ             ^A(     P     x     
B	          +%     WJ             ^A(     P     x     
B	          +%     WJ             ^A(     P     x     
B	          +%     WJ             ^A(     P     x     
B	          +%     WJ             ^A(     P     x     
B	          +%     WJ             ^A(     P     x     
B	          +%     WJ             ^A(     P     x     
B	     :ƼqZ*    IENDB`Serializer
==========

.. image:: logo-small.png

Introduction
------------
This library allows you to (de-)serialize data of any complexity. Currently, it supports XML and JSON.

It also provides you with a rich tool-set to adapt the output to your specific needs.

Built-in features include:

- (De-)serialize data of any complexity; circular references are handled gracefully.
- Supports many built-in PHP types (such as dates)
- Integrates with Doctrine ORM, et. al.
- Supports versioning, e.g. for APIs
- Configurable via XML, YAML, or Doctrine Annotations

Installation
------------
This library can be easily installed via composer

.. code-block :: bash

    composer require jms/serializer

or just add it to your ``composer.json`` file directly.

Usage
-----
For standalone projects usage of the provided builder is encouraged::

    $serializer = JMS\Serializer\SerializerBuilder::create()->build();
    $jsonContent = $serializer->serialize($data, 'json');
    echo $jsonContent; // or return it in a Response


Documentation
-------------

.. toctree ::
    :maxdepth: 2

    configuration
    usage
    event_system
    handlers
    reference
    cookbook

License
-------

The code is released under the business-friendly `MIT license`_.

Documentation is subject to the `Attribution-NonCommercial-NoDerivs 3.0 Unported
license`_.

.. _MIT license: https://opensource.org/licenses/MIT
.. _Attribution-NonCommercial-NoDerivs 3.0 Unported license: http://creativecommons.org/licenses/by-nc-nd/3.0/

Cookbook
========

.. toctree ::
    :glob:

    cookbook/*Usage
=====

Serializing Objects
-------------------
Most common usage is probably to serialize objects. This can be achieved
very easily:

.. configuration-block ::

    .. code-block :: php

        <?php

        $serializer = JMS\Serializer\SerializerBuilder::create()->build();
        $serializer->serialize($object, 'json');
        $serializer->serialize($object, 'xml');

    .. code-block :: jinja

        {{ object | serialize }} {# uses JSON #}
        {{ object | serialize('json') }}
        {{ object | serialize('xml') }}

Deserializing Objects
---------------------
You can also deserialize objects from their XML, or JSON representation. For
example, when accepting data via an API.

.. code-block :: php

    <?php

    $serializer = JMS\Serializer\SerializerBuilder::create()->build();
    $object = $serializer->deserialize($jsonData, \MyNamespace\MyObject::class, 'json');

# -*- coding: utf-8 -*-

import sys, os

# -- General configuration -----------------------------------------------------

# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sensio.sphinx.configurationblock', 'sensio.sphinx.phpcode']

# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']

# The suffix of source filenames.
source_suffix = '.rst'

# The master toctree document.
master_doc = 'index'

# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['_build']

# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'

# This will be used when using the shorthand notation
highlight_language = 'php'

# -- Options for HTML output ---------------------------------------------------
import sphinx_rtd_theme

# The theme to use for HTML and HTML Help pages.  See the documentation for
# a list of builtin themes.
html_theme = 'sphinx_rtd_theme'

# Add any paths that contain custom themes here, relative to this directory.
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]

# Output file base name for HTML help builder.
htmlhelp_basename = 'doc'
stdClass
========

The serializer offers support for serializing ``stdClass`` objects, however the use of
``stdClass`` objects is discouraged.

The current implementation serializes all the properties of a ``stdClass`` object in
the order they appear.

There are many known limitations when dealing with ``stdClass`` objects.
More in detail, it is not possible to:

- change serialization order of properties
- apply per-property exclusion policies
- specify any extra serialization information for properties that are part of the ``stdClass`` object, as serialization name, type, xml structure and so on
- deserialize data into ``stdClass`` objects
Serializing arrays and hashes
=============================

Introduction
------------
Serializing arrays and hashes (a concept that in PHP has not explicit boundaries)
can be challenging. The serializer offers via ``@Type`` annotation different options
to configure its behavior, but if we try to serialize directly an array
(not as a property of an object), we need to use context information to determine the
array "type"

Examples
--------

In case of a JSON serialization:

.. code-block :: php

    <?php

    // default (let the PHP's json_encode function decide)
    $serializer->serialize([1, 2]); //  [1, 2]
    $serializer->serialize(['a', 'b']); //  ['a', 'b']
    $serializer->serialize(['c' => 'd']); //  {"c" => "d"}

    // same as default (let the PHP's json_encode function decide)
    $serializer->serialize([1, 2], SerializationContext::create()->setInitialType('array')); //  [1, 2]
    $serializer->serialize([1 => 2], SerializationContext::create()->setInitialType('array')); //  {"1": 2}
    $serializer->serialize(['a', 'b'], SerializationContext::create()->setInitialType('array')); //  ['a', 'b']
    $serializer->serialize(['c' => 'd'], SerializationContext::create()->setInitialType('array')); //  {"c" => "d"}

    // typehint as strict array, keys will be always discarded
    $serializer->serialize([], SerializationContext::create()->setInitialType('array<integer>')); //  []
    $serializer->serialize([1, 2], SerializationContext::create()->setInitialType('array<integer>')); //  [1, 2]
    $serializer->serialize(['a', 'b'], SerializationContext::create()->setInitialType('array<integer>')); //  ['a', 'b']
    $serializer->serialize(['c' => 'd'], SerializationContext::create()->setInitialType('array<string>')); //  ["d"]

    // typehint as hash, keys will be always considered
    $serializer->serialize([], SerializationContext::create()->setInitialType('array<integer,integer>')); //  {}
    $serializer->serialize([1, 2], SerializationContext::create()->setInitialType('array<integer,integer>')); //  {"0" : 1, "1" : 2}
    $serializer->serialize(['a', 'b'], SerializationContext::create()->setInitialType('array<integer,integer>')); //  {"0" : "a", "1" : "b"}
    $serializer->serialize(['c' => 'd'], SerializationContext::create()->setInitialType('array<string,string>')); //  {"c" : "d"}


.. note ::

    This applies only for the JSON serialization.
Object constructor
==================

Deserialize on existing objects
-------------------------------

By default, a brand new instance of target class is created during deserialization. To deserialize into an existing object, you need to perform the following steps.


1. Create new class which implements ObjectConstructorInterface

.. code-block:: php

    <?php declare(strict_types=1);

    namespace Acme\ObjectConstructor;

    use JMS\Serializer\Construction\ObjectConstructorInterface;
    use JMS\Serializer\DeserializationContext;
    use JMS\Serializer\Metadata\ClassMetadata;
    use JMS\Serializer\Visitor\DeserializationVisitorInterface;

    class ExistingObjectConstructor implements ObjectConstructorInterface
    {
        public const ATTRIBUTE = 'deserialization-constructor-target';

        private $fallbackConstructor;

        public function __construct(ObjectConstructorInterface $fallbackConstructor)
        {
            $this->fallbackConstructor = $fallbackConstructor;
        }

        public function construct(DeserializationVisitorInterface $visitor, ClassMetadata $metadata, $data, array $type, DeserializationContext $context): ?object
        {
            if ($context->hasAttribute(self::ATTRIBUTE)) {
                return $context->getAttribute(self::ATTRIBUTE);
            }

            return $this->fallbackConstructor->construct($visitor, $metadata, $data, $type, $context);
        }
    }


2. Register ExistingObjectConstructor.

You should pass ExistingObjectConstructor to DeserializationGraphNavigatorFactory constructor.


3. Add special attribute to DeserializationContext

.. code-block:: php

    $context = DeserializationContext::create();
    $context->setAttribute('deserialization-constructor-target', $document);
    $serializer->deserialize($data, get_class($document), 'json');
Exclusion Strategies
====================

Introduction
------------
The serializer supports different exclusion strategies. Each strategy allows
you to define which properties of your objects should be serialized.

General Exclusion Strategies
----------------------------
If you would like to always expose, or exclude certain properties. Then, you can
do this with the annotations ``@ExclusionPolicy``, ``@Exclude``, and ``@Expose``.

The default exclusion policy is to exclude nothing. That is, all properties of the
object will be serialized. If you only want to expose a few of the properties,
then it is easier to change the exclusion policy, and only mark these few properties:

.. code-block :: php

    <?php

    use JMS\Serializer\Annotation\ExclusionPolicy;
    use JMS\Serializer\Annotation\Expose;

    /**
     * The following annotations tells the serializer to skip all properties which
     * have not marked with @Expose.
     *
     * @ExclusionPolicy("all")
     */
    class MyObject
    {
        private $foo;
        private $bar;

        /**
         * @Expose
         */
        private $name;
    }

.. note ::

    A property that is excluded by ``@Exclude`` cannot be exposed anymore by any
    of the following strategies, but is always hidden.

Versioning Objects
------------------
JMSSerializerBundle comes by default with a very neat feature which allows
you to add versioning support to your objects, e.g. if you want to
expose them via an API that is consumed by a third-party:

.. code-block :: php

    <?php

    class VersionedObject
    {
        /**
         * @Until("1.0.x")
         */
        private $name;

        /**
         * @Since("1.1")
         * @SerializedName("name")
         */
        private $name2;
    }

.. note ::

    ``@Until``, and ``@Since`` both accept a standardized PHP version number.

If you have annotated your objects like above, you can serializing different
versions like this::

    use JMS\Serializer\SerializationContext;

    $serializer->serialize(new VersionObject(), 'json', SerializationContext::create()->setVersion(1));


Creating Different Views of Your Objects
----------------------------------------
Another default exclusion strategy is to create different views of your objects.
Let's say you would like to serialize your object in a different view depending
whether it is displayed in a list view or in a details view.

You can achieve that by using the ``@Groups`` annotation on your properties. Any
property without an explicit ``@Groups`` annotation will be included in a
``Default`` group, which can be used when specifying groups in the serialization
context.

.. code-block :: php

    use JMS\Serializer\Annotation\Groups;

    class BlogPost
    {
        /** @Groups({"list", "details"}) */
        private $id;

        /** @Groups({"list", "details"}) */
        private $title;

        /** @Groups({"list"}) */
        private $nbComments;

        /** @Groups({"details"}) */
        private $comments;

        private $createdAt;
    }

You can then tell the serializer which groups to serialize in your controller::

    use JMS\Serializer\SerializationContext;

    $serializer->serialize(new BlogPost(), 'json', SerializationContext::create()->setGroups(array('list')));

    //will output $id, $title and $nbComments.

    $serializer->serialize(new BlogPost(), 'json', SerializationContext::create()->setGroups(array('Default', 'list')));

    //will output $id, $title, $nbComments and $createdAt.

Overriding Groups of Deeper Branches of the Graph
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In some cases you want to control more precisely what is serialized because you may have the same class at different
depths of the object graph.

For example if you have a User that has a manager and friends::

    use JMS\Serializer\Annotation\Groups;

    class User
    {
        private $name;

        /** @Groups({"manager_group"}) */
        private $manager;

        /** @Groups({"friends_group"}) */
        private $friends;

        public function __construct($name, User $manager = null, array $friends = null)
        {
            $this->name = $name;
            $this->manager = $manager;
            $this->friends = $friends;
        }
    }

And the following object graph::

    $john = new User(
        'John',
        new User(
            'John Manager',
            new User('The boss'),
            array(
                new User('John Manager friend 1'),
            )
        ),
        array(
            new User(
                'John friend 1',
                new User('John friend 1 manager')
            ),
            new User(
                'John friend 2',
                new User('John friend 2 manager')
            ),
        )
    );

You can override groups on specific paths::

    use JMS\Serializer\SerializationContext;

    $context = SerializationContext::create()->setGroups(array(
        'Default', // Serialize John's name
        'manager_group', // Serialize John's manager
        'friends_group', // Serialize John's friends

        'manager' => array( // Override the groups for the manager of John
            'Default', // Serialize John manager's name
            'friends_group', // Serialize John manager's friends. If you do not override the groups for the friends, it will default to Default.
        ),

        'friends' => array( // Override the groups for the friends of John
            'manager_group' // Serialize John friends' managers.

            'manager' => array( // Override the groups for the John friends' manager
                'Default', // This would be the default if you did not override the groups of the manager property.
            ),
        ),
    ));
    $serializer->serialize($john, 'json', $context);

This would result in the following json::

    {
        "name": "John",
        "manager": {
            "name": "John Manager",
            "friends": [
                {
                    "name": "John Manager friend 1"
                }
            ]
        },
        "friends": [
            {
                "manager": {
                    "name": "John friend 1 manager"
                },
            },
            {
                "manager": {
                    "name": "John friend 2 manager"
                },
            },
        ]
    }

Deserialization Exclusion Strategy with Groups
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
You can use ``@Groups`` to cut off unwanted properties while deserialization.

.. code-block:: php

    use JMS\Serializer\Annotation\Groups;

    class GroupsObject
    {
        /**
         * @Groups({"foo"})
         */
        public $foo;

        /**
         * @Groups({"foo","bar"})
         */
        public $foobar;

        /**
         * @Groups({"bar", "Default"})
         */
        public $bar;

        /**
         * @Type("string")
         */
        public $none;
    }

.. code-block:: php

    $data = [
        'foo'    => 'foo',
        'foobar' => 'foobar',
        'bar'    => 'bar',
        'none'   => 'none',
    ];
    $context = DeserializationContext::create()->setGroups(['foo']);
    $object = $serializer->fromArray($data, GroupsObject::class, $context);
    // $object->foo is 'foo'
    // $object->foobar is 'foobar'
    // $object->bar is null
    // $object->none is null

Limiting serialization depth of some properties
-----------------------------------------------
You can limit the depth of what will be serialized in a property with the
``@MaxDepth`` annotation.
This exclusion strategy is a bit different from the others, because it will
affect the serialized content of others classes than the one you apply the
annotation to.

.. code-block :: php

    use JMS\Serializer\Annotation\MaxDepth;

    class User
    {
        private $username;

        /** @MaxDepth(1) */
        private $friends;

        /** @MaxDepth(2) */
        private $posts;
    }

    class Post
    {
        private $title;

        private $author;
    }

In this example, serializing a user, because the max depth of the ``$friends``
property is 1, the user friends would be serialized, but not their friends;
and because the the max depth of the ``$posts`` property is 2, the posts would
be serialized, and their author would also be serialized.

You need to tell the serializer to take into account MaxDepth checks::

    use JMS\Serializer\SerializationContext;

    $serializer->serialize($data, 'json', SerializationContext::create()->enableMaxDepthChecks());


Dynamic exclusion strategy
--------------------------

If the previous exclusion strategies are not enough, is possible to use the ``ExpressionLanguageExclusionStrategy``
that uses the `symfony expression language`_ to
allow a more sophisticated exclusion strategies using ``@Exclude(if="expression")`` and ``@Expose(if="expression")`` methods.
This also works on class level, but is only evaluated during ``serialze`` and does not have any effect during ``deserialze``.


.. code-block :: php

    <?php

    /**
     * @Exclude(if="true")
    */
    class MyObject
    {
        /**
         * @Exclude(if="true")
         */
        private $name;

       /**
         * @Expose(if="true")
         */
        private $name2;
    }

.. note ::

    ``true`` is just a generic expression, you can use any expression allowed by the Symfony Expression Language

To enable this feature you have to set the Expression Evaluator when initializing the serializer. 

.. code-block :: php

    <?php
    use JMS\Serializer\Expression\ExpressionEvaluator;
    use JMS\Serializer\Expression\SerializerBuilder;
    use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
    
    $serializer = SerializerBuilder::create()
        ->setExpressionEvaluator(new ExpressionEvaluator(new ExpressionLanguage()))
        ->build();

.. _symfony expression language: https://github.com/symfony/expression-language

By default the serializer exposes three variables (`object`, `context` and `property_metadata` for use in an expression. This enables you to create custom exclusion strategies similar to i.e. the `GroupExclusionStrategy`_. In the below example, `someMethod` would receive all three variables.

.. code-block :: php

    <?php

    class MyObject
    {
        /**
         * @Exclude(if="someMethod(object, context, property_metadata)")
         */
        private $name;

       /**
         * @Expose(if="someMethod(object, context, property_metadata)")
         */
        private $name2;
    }

.. _GroupExclusionStrategy: https://github.com/schmittjoh/serializer/blob/master/src/Exclusion/GroupsExclusionStrategy.php

Using dynamic excludes on class level is also handy when you need to filter out certain objects in a collection, for example based on user permissions.
The following example shows how to exclude `Account` objects when serializing the `Person` object, if the `Account` is either expired or the user does not have the permission to view the account by calling `is_granted` with the `Account` object.

.. code-block :: php

    <?php

    class Person
    {
        /**
         * @Type("array<Account>")
         */
        public $accounts;
    }

    /**
     * @Exclude(if="object.expired || !is_granted('view',object)")
     */
    class Account
    {
        /**
         * @Type("boolean")
         */
        public $expired;
    }
Configuration
=============

.. note ::

    If you are using Symfony2, this section is mostly irrelevant for you as the entire integration is provided by
    JMSSerializerBundle; please see `its documentation <http://jmsyst.com/bundles/JMSSerializerBundle>`_. If you are
    using another framework, there also might be a module, or other special integration. Please check packagist, or
    whatever registry usually holds such information for your framework.


If you are using the standalone library and you want to use annotations, the annotation registry must be initialized::

    Doctrine\Common\Annotations\AnnotationRegistry::registerLoader('class_exists');


Constructing a Serializer
-------------------------

This library provides a special builder object which makes constructing serializer instances a breeze in any PHP
project. In its shortest version, it's just a single line of code::

    $serializer = JMS\Serializer\SerializerBuilder::create()->build();

This serializer is fully functional, but you might want to tweak it a bit for example to configure a cache directory.

Configuring a Cache Directory
-----------------------------
The serializer collects several metadata about your objects from various sources such as YML, XML, or annotations. In
order to make this process as efficient as possible, it is encourage to let the serializer cache that information. For
that, you can configure a cache directory::

    $builder = new JMS\Serializer\SerializerBuilder();

    $serializer =
        JMS\Serializer\SerializerBuilder::create()
        ->setCacheDir($someWritableDir)
        ->setDebug($trueOrFalse)
        ->build();

As you can see, we also added a call to the ``setDebug`` method. In debug mode, the serializer will perform a bit more
filesystem checks to see whether the data that it has cached is still valid. These checks are useful during development
so that you do not need to manually clear cache folders, however in production they are just unnecessary overhead. The
debug setting allows you to make the behavior environment specific.

Adding Custom Handlers
----------------------
If you have created custom handlers, you can add them to the serializer easily::

    $serializer =
        JMS\Serializer\SerializerBuilder::create()
            ->addDefaultHandlers()
            ->configureHandlers(function(JMS\Serializer\Handler\HandlerRegistry $registry) {
                $registry->registerHandler(JMS\Serializer\GraphNavigatorInterface::DIRECTION_SERIALIZATION, 'MyObject', 'json',
                    function($visitor, MyObject $obj, array $type) {
                        return $obj->getName();
                    }
                );
            })
            ->build();

For more complex handlers, it is advisable to extract them to dedicated classes,
see :doc:`handlers documentation <handlers>`.

Configuring Metadata Locations
------------------------------
This library supports several metadata sources. By default, it uses Doctrine annotations, but you may also store
metadata in XML, or YML files. For the latter, it is necessary to configure a metadata directory where those files
are located::

    $serializer =
        JMS\Serializer\SerializerBuilder::create()
            ->addMetadataDir($someDir)
            ->build();

The serializer would expect the metadata files to be named like the fully qualified class names where all ``\`` are
replaced with ``.``. So, if you class would be named ``Vendor\Package\Foo``, the metadata file would need to be located
at ``$someDir/Vendor.Package.Foo.(xml|yml)``. If not found, ``$someDir/Vendor.Package.(xml|yml)`` will be tried, then ``$someDir/Vendor.Package.(xml|yml)`` and so on. For more information, see the :doc:`reference <reference>`.

Setting a default SerializationContext factory
----------------------------------------------
To avoid to pass an instance of SerializationContext
every time you call method ``serialize()`` (or ``toArray()``),
you can set a ``SerializationContextFactory`` to the Serializer.

Example using the SerializerBuilder::

    use JMS\Serializer\SerializationContext;

    $serializer = JMS\Serializer\SerializerBuilder::create()
        ->setSerializationContextFactory(function () {
            return SerializationContext::create()
                ->setSerializeNull(true)
            ;
        })
        ->build()
    ;

Then, calling ``$serializer->serialize($data, 'json');`` will generate
a serialization context from your callable and use it.

.. note ::

    You can also set a default DeserializationContextFactory with
    ``->setDeserializationContextFactory(function () { /* ... */ })``
    to be used with methods ``deserialize()`` and ``fromArray()``.
Event System
============

The serializer dispatches different events during the serialization, and
deserialization process which you can use to hook in and alter the default
behavior.

Register an Event Listener, or Subscriber
-----------------------------------------
The difference between listeners, and subscribers is that listener do not know to which events they listen
while subscribers contain that information. Thus, subscribers are easier to share, and re-use. Listeners
on the other hand, can be simple callables and do not require a dedicated class.

.. code-block :: php

    class MyEventSubscriber implements JMS\Serializer\EventDispatcher\EventSubscriberInterface
    {
        public static function getSubscribedEvents()
        {
            return array(
                array(
                    'event' => 'serializer.pre_serialize',
                    'method' => 'onPreSerialize',
                    'class' => 'AppBundle\\Entity\\SpecificClass', // if no class, subscribe to every serialization
                    'format' => 'json', // optional format
                    'priority' => 0, // optional priority
                ),
            );
        }

        public function onPreSerialize(JMS\Serializer\EventDispatcher\PreSerializeEvent $event)
        {
            // do something
        }
    }

    $builder
        ->configureListeners(function(JMS\Serializer\EventDispatcher\EventDispatcher $dispatcher) {
            $dispatcher->addListener('serializer.pre_serialize',
                function(JMS\Serializer\EventDispatcher\PreSerializeEvent $event) {
                    // do something
                }
            );

            $dispatcher->addSubscriber(new MyEventSubscriber());
        })
    ;

Events
------

serializer.pre_serialize
~~~~~~~~~~~~~~~~~~~~~~~~
This is dispatched before a type is visited. You have access to the visitor,
data, and type. Listeners may modify the type that is being used for
serialization.

**Event Object**: ``JMS\Serializer\EventDispatcher\PreSerializeEvent``

serializer.post_serialize
~~~~~~~~~~~~~~~~~~~~~~~~~
This is dispatched right before a type is left. You can for example use this
to add additional data for an object that you normally do not save inside
objects such as links.

**Event Object**: ``JMS\Serializer\EventDispatcher\ObjectEvent``

serializer.pre_deserialize
~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. versionadded : 0.12
    Event was added

This is dispatched before an object is deserialized. You can use this to
modify submitted data, or modify the type that is being used for deserialization.

**Event Object**: ``JMS\Serializer\EventDispatcher\PreDeserializeEvent``

serializer.post_deserialize
~~~~~~~~~~~~~~~~~~~~~~~~~~~
This is dispatched after a type is processed. You can use it to normalize
submitted data if you require external services for example, or also to
perform validation of the submitted data.

**Event Object**: ``JMS\Serializer\EventDispatcher\ObjectEvent``
<?php

declare(strict_types=1);

namespace JMS\Serializer;

use JMS\Serializer\Exclusion\ExclusionStrategyInterface;

/**
 * Handles traversal along the object graph.
 *
 * This class handles traversal along the graph, and calls different methods
 * on visitors, or custom handlers to process its nodes.
 *
 * @internal
 *
 * @author Asmir Mustafic <goetas@gmail.com>
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
abstract class GraphNavigator implements GraphNavigatorInterface
{
    /**
     * @var VisitorInterface
     */
    protected $visitor;
    /**
     * @var Context
     */
    protected $context;
    /***
     * @var string
     */
    protected $format;
    /**
     * @var ExclusionStrategyInterface
     */
    protected $exclusionStrategy;

    public function initialize(VisitorInterface $visitor, Context $context): void
    {
        $this->visitor = $visitor;
        $this->context = $context;

        // cache value
        $this->format = $context->getFormat();
        $this->exclusionStrategy = $context->getExclusionStrategy();
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Type;

/**
 * @internal
 *
 * @phpstan-type TypeArray array{name: string, params: array}
 */
final class Type
{
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Type;

use JMS\Serializer\Type\Exception\SyntaxError;

/**
 * @internal
 *
 * @phpstan-import-type TypeArray from Type
 */
final class Parser implements ParserInterface
{
    /**
     * @var Lexer
     */
    private $lexer;

    /**
     * @var bool
     */
    private $root = true;

    public function parse(string $string): array
    {
        $this->lexer = new Lexer();
        $this->lexer->setInput($string);
        $this->lexer->moveNext();

        return $this->visit();
    }

    /**
     * @return mixed
     */
    private function visit()
    {
        $this->lexer->moveNext();

        if (!$this->lexer->token) {
            throw new SyntaxError(
                'Syntax error, unexpected end of stream',
            );
        }

        if (Lexer::T_FLOAT === $this->lexer->token->type) {
            return floatval($this->lexer->token->value);
        } elseif (Lexer::T_INTEGER === $this->lexer->token->type) {
            return intval($this->lexer->token->value);
        } elseif (Lexer::T_NULL === $this->lexer->token->type) {
            return null;
        } elseif (Lexer::T_STRING === $this->lexer->token->type) {
            return $this->lexer->token->value;
        } elseif (Lexer::T_IDENTIFIER === $this->lexer->token->type) {
            if ($this->lexer->isNextToken(Lexer::T_TYPE_START)) {
                return $this->visitCompoundType();
            } elseif ($this->lexer->isNextToken(Lexer::T_ARRAY_START)) {
                return $this->visitArrayType();
            }

            return $this->visitSimpleType();
        } elseif (!$this->root && Lexer::T_ARRAY_START === $this->lexer->token->type) {
            return $this->visitArrayType();
        }

        throw new SyntaxError(sprintf(
            'Syntax error, unexpected "%s" (%s)',
            $this->lexer->token->value,
            $this->getConstant($this->lexer->token->type),
        ));
    }

    /**
     * @return TypeArray
     */
    private function visitSimpleType(): array
    {
        $value = $this->lexer->token->value;

        return ['name' => $value, 'params' => []];
    }

    /**
     * @return TypeArray
     */
    private function visitCompoundType(): array
    {
        $this->root = false;
        $name = $this->lexer->token->value;
        $this->match(Lexer::T_TYPE_START);

        $params = [];
        if (!$this->lexer->isNextToken(Lexer::T_TYPE_END)) {
            while (true) {
                $params[] = $this->visit();

                if ($this->lexer->isNextToken(Lexer::T_TYPE_END)) {
                    break;
                }

                $this->match(Lexer::T_COMMA);
            }
        }

        $this->match(Lexer::T_TYPE_END);

        return [
            'name' => $name,
            'params' => $params,
        ];
    }

    private function visitArrayType(): array
    {
        /*
         * Here we should call $this->match(Lexer::T_ARRAY_START); to make it clean
         * but the token has already been consumed by moveNext() in visit()
         */

        $params = [];
        if (!$this->lexer->isNextToken(Lexer::T_ARRAY_END)) {
            while (true) {
                $params[] = $this->visit();
                if ($this->lexer->isNextToken(Lexer::T_ARRAY_END)) {
                    break;
                }

                $this->match(Lexer::T_COMMA);
            }
        }

        $this->match(Lexer::T_ARRAY_END);

        return $params;
    }

    private function match(int $token): void
    {
        if (!$this->lexer->lookahead) {
            throw new SyntaxError(
                sprintf('Syntax error, unexpected end of stream, expected %s', $this->getConstant($token)),
            );
        }

        if ($this->lexer->lookahead->type === $token) {
            $this->lexer->moveNext();

            return;
        }

        throw new SyntaxError(sprintf(
            'Syntax error, unexpected "%s" (%s), expected was %s',
            $this->lexer->lookahead->value,
            $this->getConstant($this->lexer->lookahead->type),
            $this->getConstant($token),
        ));
    }

    private function getConstant(int $value): string
    {
        $oClass = new \ReflectionClass(Lexer::class);

        return array_search($value, $oClass->getConstants());
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Type;

use Doctrine\Common\Lexer\AbstractLexer;
use JMS\Serializer\Type\Exception\SyntaxError;

/**
 * @internal
 */
final class Lexer extends AbstractLexer
{
    public const T_UNKNOWN = 0;
    public const T_INTEGER = 1;
    public const T_STRING = 2;
    public const T_FLOAT = 3;
    public const T_ARRAY_START = 4;
    public const T_ARRAY_END = 5;
    public const T_COMMA = 6;
    public const T_TYPE_START = 7;
    public const T_TYPE_END = 8;
    public const T_IDENTIFIER = 9;
    public const T_NULL = 10;

    public function parse(string $type)
    {
        try {
            return $this->getType($type);
        } catch (\Throwable $e) {
            throw new SyntaxError($e->getMessage(), 0, $e);
        }
    }

    protected function getCatchablePatterns(): array
    {
        return [
            '[a-z][a-z_\\\\0-9]*', // identifier or qualified name
            "'(?:[^']|'')*'", // single quoted strings
            '(?:[0-9]+(?:[\.][0-9]+)*)(?:e[+-]?[0-9]+)?', // numbers
            '"(?:[^"]|"")*"', // double quoted strings
            '<',
            '>',
            '\\[',
            '\\]',
        ];
    }

    protected function getNonCatchablePatterns(): array
    {
        return ['\s+'];
    }

    /**
     * {@inheritDoc}
     *
     * @return int|string|null
     */
    protected function getType(&$value)
    {
        $type = self::T_UNKNOWN;

        switch (true) {
            // Recognize numeric values
            case is_numeric($value):
                if (false !== strpos($value, '.') || false !== stripos($value, 'e')) {
                    return self::T_FLOAT;
                }

                return self::T_INTEGER;

            // Recognize quoted strings
            case "'" === $value[0]:
                $value = str_replace("''", "'", substr($value, 1, strlen($value) - 2));

                return self::T_STRING;

            case '"' === $value[0]:
                $value = str_replace('""', '"', substr($value, 1, strlen($value) - 2));

                return self::T_STRING;

            case 'null' === $value:
                return self::T_NULL;

            // Recognize identifiers, aliased or qualified names
            case ctype_alpha($value[0]) || '\\' === $value[0]:
                return self::T_IDENTIFIER;

            case ',' === $value:
                return self::T_COMMA;

            case '>' === $value:
                return self::T_TYPE_END;

            case '<' === $value:
                return self::T_TYPE_START;

            case ']' === $value:
                return self::T_ARRAY_END;

            case '[' === $value:
                return self::T_ARRAY_START;

            // Default
            default:
                // Do nothing
        }

        return $type;
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Type\Exception;

use JMS\Serializer\Exception\Exception as BaseException;

interface Exception extends BaseException
{
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Type\Exception;

final class InvalidNode extends \LogicException implements Exception
{
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Type\Exception;

final class SyntaxError extends \RuntimeException implements Exception
{
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Type;

/**
 * @phpstan-import-type TypeArray from Type
 */
interface ParserInterface
{
    /**
     * @return TypeArray
     */
    public function parse(string $type): array;
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Handler;

use JMS\Serializer\GraphNavigatorInterface;
use JMS\Serializer\SerializationContext;
use JMS\Serializer\Type\Type;
use JMS\Serializer\Visitor\SerializationVisitorInterface;
use JMS\Serializer\XmlSerializationVisitor;
use Symfony\Component\Validator\ConstraintViolation;
use Symfony\Component\Validator\ConstraintViolationList;

/**
 * @phpstan-import-type TypeArray from Type
 */
final class ConstraintViolationHandler implements SubscribingHandlerInterface
{
    /**
     * {@inheritdoc}
     */
    public static function getSubscribingMethods()
    {
        $methods = [];
        $formats = ['xml', 'json'];
        $types = [ConstraintViolationList::class => 'serializeList', ConstraintViolation::class => 'serializeViolation'];

        foreach ($types as $type => $method) {
            foreach ($formats as $format) {
                $methods[] = [
                    'direction' => GraphNavigatorInterface::DIRECTION_SERIALIZATION,
                    'type' => $type,
                    'format' => $format,
                    'method' => $method . 'To' . $format,
                ];
            }
        }

        return $methods;
    }

    public function serializeListToXml(XmlSerializationVisitor $visitor, ConstraintViolationList $list): void
    {
        $currentNode = $visitor->getCurrentNode();
        if (!$currentNode) {
            $visitor->createRoot();
        }

        foreach ($list as $violation) {
            $this->serializeViolationToXml($visitor, $violation);
        }
    }

    /**
     * @param TypeArray $type
     *
     * @return array|\ArrayObject
     */
    public function serializeListToJson(SerializationVisitorInterface $visitor, ConstraintViolationList $list, array $type, SerializationContext $context)
    {
        return $visitor->visitArray(iterator_to_array($list), $type);
    }

    public function serializeViolationToXml(XmlSerializationVisitor $visitor, ConstraintViolation $violation): void
    {
        $violationNode = $visitor->getDocument()->createElement('violation');

        $parent = $visitor->getCurrentNode();
        if (!$parent) {
            $visitor->setCurrentAndRootNode($violationNode);
        } else {
            $parent->appendChild($violationNode);
        }

        $violationNode->setAttribute('property_path', $violation->getPropertyPath());
        $violationNode->appendChild($messageNode = $visitor->getDocument()->createElement('message'));

        $messageNode->appendChild($visitor->getDocument()->createCDATASection($violation->getMessage()));
    }

    public function serializeViolationToJson(SerializationVisitorInterface $visitor, ConstraintViolation $violation): array
    {
        return [
            'property_path' => $violation->getPropertyPath(),
            'message' => $violation->getMessage(),
        ];
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Handler;

use JMS\Serializer\Exception\InvalidArgumentException;
use Psr\Container\ContainerInterface as PsrContainerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

final class LazyHandlerRegistry extends HandlerRegistry
{
    /**
     * @var PsrContainerInterface|ContainerInterface
     */
    private $container;

    /**
     * @var array
     */
    private $initializedHandlers = [];

    /**
     * @param PsrContainerInterface|ContainerInterface $container
     * @param array $handlers
     */
    public function __construct($container, array $handlers = [])
    {
        if (!$container instanceof PsrContainerInterface && !$container instanceof ContainerInterface) {
            throw new InvalidArgumentException(sprintf('The container must be an instance of %s or %s (%s given).', PsrContainerInterface::class, ContainerInterface::class, \is_object($container) ? \get_class($container) : \gettype($container)));
        }

        parent::__construct($handlers);

        $this->container = $container;
    }

    /**
     * {@inheritdoc}
     */
    public function registerHandler(int $direction, string $typeName, string $format, $handler): void
    {
        parent::registerHandler($direction, $typeName, $format, $handler);

        unset($this->initializedHandlers[$direction][$typeName][$format]);
    }

    /**
     * {@inheritdoc}
     */
    public function getHandler(int $direction, string $typeName, string $format)
    {
        if (isset($this->initializedHandlers[$direction][$typeName][$format])) {
            return $this->initializedHandlers[$direction][$typeName][$format];
        }

        if (!isset($this->handlers[$direction][$typeName][$format])) {
            return null;
        }

        $handler = $this->handlers[$direction][$typeName][$format];
        if (\is_array($handler) && \is_string($handler[0]) && $this->container->has($handler[0])) {
            $handler[0] = $this->container->get($handler[0]);
        }

        return $this->initializedHandlers[$direction][$typeName][$format] = $handler;
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Handler;

use JMS\Serializer\DeserializationContext;
use JMS\Serializer\Exception\NonVisitableTypeException;
use JMS\Serializer\Exception\NotAcceptableException;
use JMS\Serializer\Exception\RuntimeException;
use JMS\Serializer\GraphNavigatorInterface;
use JMS\Serializer\SerializationContext;
use JMS\Serializer\Type\Type;
use JMS\Serializer\Visitor\DeserializationVisitorInterface;
use JMS\Serializer\Visitor\SerializationVisitorInterface;

/**
 * @phpstan-import-type TypeArray from Type
 */
final class UnionHandler implements SubscribingHandlerInterface
{
    private static $aliases = ['boolean' => 'bool', 'integer' => 'int', 'double' => 'float'];

    /**
     * {@inheritdoc}
     */
    public static function getSubscribingMethods()
    {
        $methods = [];
        $formats = ['json', 'xml'];

        foreach ($formats as $format) {
            $methods[] = [
                'type' => 'union',
                'format' => $format,
                'direction' => GraphNavigatorInterface::DIRECTION_DESERIALIZATION,
                'method' => 'deserializeUnion',
            ];
            $methods[] = [
                'type' => 'union',
                'format' => $format,
                'direction' => GraphNavigatorInterface::DIRECTION_SERIALIZATION,
                'method' => 'serializeUnion',
            ];
        }

        return $methods;
    }

    /**
     * @param TypeArray $type
     */
    public function serializeUnion(
        SerializationVisitorInterface $visitor,
        mixed $data,
        array $type,
        SerializationContext $context
    ): mixed {
        if ($this->isPrimitiveType(gettype($data))) {
            $resolvedType = [
                'name' => gettype($data),
                'params' => [],
            ];
        } else {
            $resolvedType = [
                'name' => get_class($data),
                'params' => [],
            ];
        }

        return $context->getNavigator()->accept($data, $resolvedType);
    }

    /**
     * @param TypeArray $type
     */
    public function deserializeUnion(DeserializationVisitorInterface $visitor, mixed $data, array $type, DeserializationContext $context): mixed
    {
        if ($data instanceof \SimpleXMLElement) {
            throw new RuntimeException('XML deserialisation into union types is not supported yet.');
        }

        if (3 === count($type['params'])) {
            $lookupField = $type['params'][1];
            if (empty($data[$lookupField])) {
                throw new NonVisitableTypeException(sprintf('Union Discriminator Field "%s" not found in data', $lookupField));
            }

            $unionMap = $type['params'][2];
            $lookupValue = $data[$lookupField];
            if (empty($unionMap[$lookupValue])) {
                throw new NonVisitableTypeException(sprintf('Union Discriminator Map does not contain key "%s"', $lookupValue));
            }

            $finalType = [
                'name' => $unionMap[$lookupValue],
                'params' => [],
            ];

            return $context->getNavigator()->accept($data, $finalType);
        }

        $dataType = gettype($data);

        if (
            array_filter(
                $type['params'][0],
                static fn (array $type): bool => $type['name'] === $dataType || (isset(self::$aliases[$dataType]) && $type['name'] === self::$aliases[$dataType]),
            )
        ) {
            return $context->getNavigator()->accept($data, [
                'name' => $dataType,
                'params' => [],
            ]);
        }

        foreach ($type['params'][0] as $possibleType) {
            if ($this->isPrimitiveType($possibleType['name']) && $this->testPrimitive($data, $possibleType['name'])) {
                return $context->getNavigator()->accept($data, $possibleType);
            }
        }

        throw new NotAcceptableException();
    }

    private function isPrimitiveType(string $type): bool
    {
        return in_array($type, ['int', 'integer', 'float', 'double', 'bool', 'boolean', 'true', 'false', 'string', 'array'], true);
    }

    private function testPrimitive(mixed $data, string $type): bool
    {
        switch ($type) {
            case 'array':
                return is_array($data);

            case 'integer':
            case 'int':
                return (string) (int) $data === (string) $data;

            case 'double':
            case 'float':
                return (string) (float) $data === (string) $data;

            case 'bool':
            case 'boolean':
                return !is_array($data) && (string) (bool) $data === (string) $data;

            case 'true':
                return true === $data;

            case 'false':
                return false === $data;

            case 'string':
                return !is_array($data) && !is_object($data);
        }

        return false;
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Handler;

use JMS\Serializer\GraphNavigatorInterface;
use JMS\Serializer\Visitor\SerializationVisitorInterface;
use JMS\Serializer\XmlSerializationVisitor;
use Symfony\Component\Form\Form;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Translation\TranslatorInterface;
use Symfony\Contracts\Translation\TranslatorInterface as TranslatorContract;

use function get_class;

final class FormErrorHandler implements SubscribingHandlerInterface
{
    /**
     * @var TranslatorInterface|TranslatorContract|null
     */
    private $translator;

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

    /**
     * {@inheritdoc}
     */
    public static function getSubscribingMethods()
    {
        $methods = [];
        foreach (['xml', 'json'] as $format) {
            $methods[] = [
                'direction' => GraphNavigatorInterface::DIRECTION_SERIALIZATION,
                'type' => Form::class,
                'format' => $format,
            ];
            $methods[] = [
                'direction' => GraphNavigatorInterface::DIRECTION_SERIALIZATION,
                'type' => FormInterface::class,
                'format' => $format,
                'method' => 'serializeFormTo' . ucfirst($format),
            ];
            $methods[] = [
                'direction' => GraphNavigatorInterface::DIRECTION_SERIALIZATION,
                'type' => FormError::class,
                'format' => $format,
            ];
        }

        return $methods;
    }

    public function __construct(?object $translator = null, string $translationDomain = 'validators')
    {
        if (null !== $translator && (!$translator instanceof TranslatorInterface && !$translator instanceof TranslatorContract)) {
            throw new \InvalidArgumentException(sprintf(
                'The first argument passed to %s must be instance of %s or %s, %s given',
                self::class,
                TranslatorInterface::class,
                TranslatorContract::class,
                get_class($translator),
            ));
        }

        $this->translator = $translator;
        $this->translationDomain = $translationDomain;
    }

    public function serializeFormToXml(XmlSerializationVisitor $visitor, FormInterface $form): \DOMElement
    {
        $formNode = $visitor->getDocument()->createElement('form');

        $formNode->setAttribute('name', $form->getName());

        $formNode->appendChild($errorsNode = $visitor->getDocument()->createElement('errors'));
        foreach ($form->getErrors() as $error) {
            $errorNode = $visitor->getDocument()->createElement('entry');
            $errorNode->appendChild($this->serializeFormErrorToXml($visitor, $error));
            $errorsNode->appendChild($errorNode);
        }

        foreach ($form->all() as $child) {
            if ($child instanceof Form) {
                if (null !== $node = $this->serializeFormToXml($visitor, $child)) {
                    $formNode->appendChild($node);
                }
            }
        }

        return $formNode;
    }

    public function serializeFormToJson(SerializationVisitorInterface $visitor, FormInterface $form): \ArrayObject
    {
        return $this->convertFormToArray($visitor, $form);
    }

    public function serializeFormErrorToXml(XmlSerializationVisitor $visitor, FormError $formError): \DOMCdataSection
    {
        return $visitor->getDocument()->createCDATASection($this->getErrorMessage($formError));
    }

    public function serializeFormErrorToJson(SerializationVisitorInterface $visitor, FormError $formError): string
    {
        return $this->getErrorMessage($formError);
    }

    private function getErrorMessage(FormError $error): ?string
    {
        if (null === $this->translator) {
            return $error->getMessage();
        }

        if (null !== $error->getMessagePluralization()) {
            if ($this->translator instanceof TranslatorContract) {
                return $this->translator->trans($error->getMessageTemplate(), ['%count%' => $error->getMessagePluralization()] + $error->getMessageParameters(), $this->translationDomain);
            } else {
                return $this->translator->transChoice($error->getMessageTemplate(), $error->getMessagePluralization(), $error->getMessageParameters(), $this->translationDomain);
            }
        }

        return $this->translator->trans($error->getMessageTemplate(), $error->getMessageParameters(), $this->translationDomain);
    }

    private function convertFormToArray(SerializationVisitorInterface $visitor, FormInterface $data): \ArrayObject
    {
        $form = new \ArrayObject();
        $errors = [];
        foreach ($data->getErrors() as $error) {
            $errors[] = $this->getErrorMessage($error);
        }

        if ($errors) {
            $form['errors'] = $errors;
        }

        $children = [];
        foreach ($data->all() as $child) {
            if ($child instanceof FormInterface) {
                $children[$child->getName()] = $this->convertFormToArray($visitor, $child);
            }
        }

        if ($children) {
            $form['children'] = $children;
        }

        return $form;
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Handler;

use JMS\Serializer\Exception\RuntimeException;
use JMS\Serializer\GraphNavigatorInterface;
use JMS\Serializer\SerializationContext;
use JMS\Serializer\Type\Type;
use JMS\Serializer\Visitor\DeserializationVisitorInterface;
use JMS\Serializer\Visitor\SerializationVisitorInterface;
use JMS\Serializer\XmlSerializationVisitor;

/**
 * @phpstan-import-type TypeArray from Type
 */
final class DateHandler implements SubscribingHandlerInterface
{
    /**
     * @var string
     */
    private $defaultSerializationFormat;

    /**
     * @var array<string>
     */
    private $defaultDeserializationFormats;

    /**
     * @var \DateTimeZone
     */
    private $defaultTimezone;

    /**
     * @var bool
     */
    private $xmlCData;

    /**
     * {@inheritdoc}
     */
    public static function getSubscribingMethods()
    {
        $methods = [];
        $types = [\DateTime::class, \DateTimeImmutable::class, \DateInterval::class];

        foreach (['json', 'xml'] as $format) {
            foreach ($types as $type) {
                $methods[] = [
                    'type' => $type,
                    'direction' => GraphNavigatorInterface::DIRECTION_DESERIALIZATION,
                    'format' => $format,
                ];
                $methods[] = [
                    'type' => $type,
                    'format' => $format,
                    'direction' => GraphNavigatorInterface::DIRECTION_SERIALIZATION,
                    'method' => 'serialize' . $type,
                ];
            }

            $methods[] = [
                'type' => \DateTimeInterface::class,
                'direction' => GraphNavigatorInterface::DIRECTION_DESERIALIZATION,
                'format' => $format,
                'method' => 'deserializeDateTimeFrom' . ucfirst($format),
            ];

            $methods[] = [
                'type' => \DateTimeInterface::class,
                'direction' => GraphNavigatorInterface::DIRECTION_SERIALIZATION,
                'format' => $format,
                'method' => 'serializeDateTimeInterface',
            ];
        }

        return $methods;
    }

    /**
     * @param array<string> $defaultDeserializationFormats
     */
    public function __construct(
        string $defaultFormat = \DateTime::ATOM,
        string $defaultTimezone = 'UTC',
        bool $xmlCData = true,
        array $defaultDeserializationFormats = []
    ) {
        $this->defaultSerializationFormat = $defaultFormat;
        $this->defaultDeserializationFormats = [] === $defaultDeserializationFormats ? [$defaultFormat] : $defaultDeserializationFormats;
        $this->defaultTimezone = new \DateTimeZone($defaultTimezone);
        $this->xmlCData = $xmlCData;
    }

    /**
     * @param TypeArray $type
     *
     * @return \DOMCdataSection|\DOMText|mixed
     */
    public function serializeDateTimeInterface(
        SerializationVisitorInterface $visitor,
        \DateTimeInterface $date,
        array $type,
        SerializationContext $context
    ) {
        if ($visitor instanceof XmlSerializationVisitor && false === $this->xmlCData) {
            return $visitor->visitSimpleString($date->format($this->getSerializationFormat($type)), $type);
        }

        $format = $this->getSerializationFormat($type);
        if ('U' === $format) {
            return $visitor->visitInteger((int) $date->format($format), $type);
        }

        return $visitor->visitString($date->format($this->getSerializationFormat($type)), $type);
    }

    /**
     * @param TypeArray $type
     *
     * @return \DOMCdataSection|\DOMText|mixed
     */
    public function serializeDateTime(SerializationVisitorInterface $visitor, \DateTime $date, array $type, SerializationContext $context)
    {
        return $this->serializeDateTimeInterface($visitor, $date, $type, $context);
    }

    /**
     * @param TypeArray $type
     *
     * @return \DOMCdataSection|\DOMText|mixed
     */
    public function serializeDateTimeImmutable(
        SerializationVisitorInterface $visitor,
        \DateTimeImmutable $date,
        array $type,
        SerializationContext $context
    ) {
        return $this->serializeDateTimeInterface($visitor, $date, $type, $context);
    }

    /**
     * @param TypeArray $type
     *
     * @return \DOMCdataSection|\DOMText|mixed
     */
    public function serializeDateInterval(SerializationVisitorInterface $visitor, \DateInterval $date, array $type, SerializationContext $context)
    {
        $iso8601DateIntervalString = $this->format($date);

        if ($visitor instanceof XmlSerializationVisitor && false === $this->xmlCData) {
            return $visitor->visitSimpleString($iso8601DateIntervalString, $type);
        }

        return $visitor->visitString($iso8601DateIntervalString, $type);
    }

    /**
     * @param mixed $data
     */
    private function isDataXmlNull($data): bool
    {
        $attributes = $data->attributes('xsi', true);

        return isset($attributes['nil'][0]) && 'true' === (string) $attributes['nil'][0];
    }

    /**
     * @param mixed $data
     * @param TypeArray $type
     */
    public function deserializeDateTimeFromXml(DeserializationVisitorInterface $visitor, $data, array $type): ?\DateTimeInterface
    {
        if ($this->isDataXmlNull($data)) {
            return null;
        }

        return $this->parseDateTime($data, $type);
    }

    /**
     * @param mixed $data
     * @param TypeArray $type
     */
    public function deserializeDateTimeImmutableFromXml(DeserializationVisitorInterface $visitor, $data, array $type): ?\DateTimeInterface
    {
        if ($this->isDataXmlNull($data)) {
            return null;
        }

        return $this->parseDateTime($data, $type, true);
    }

    /**
     * @param mixed $data
     * @param TypeArray $type
     */
    public function deserializeDateIntervalFromXml(DeserializationVisitorInterface $visitor, $data, array $type): ?\DateInterval
    {
        if ($this->isDataXmlNull($data)) {
            return null;
        }

        return $this->parseDateInterval((string) $data);
    }

    /**
     * @param mixed $data
     * @param TypeArray $type
     */
    public function deserializeDateTimeFromJson(DeserializationVisitorInterface $visitor, $data, array $type): ?\DateTimeInterface
    {
        if (null === $data) {
            return null;
        }

        return $this->parseDateTime($data, $type);
    }

    /**
     * @param mixed $data
     * @param TypeArray $type
     */
    public function deserializeDateTimeImmutableFromJson(DeserializationVisitorInterface $visitor, $data, array $type): ?\DateTimeInterface
    {
        if (null === $data) {
            return null;
        }

        return $this->parseDateTime($data, $type, true);
    }

    /**
     * @param mixed $data
     * @param TypeArray $type
     */
    public function deserializeDateIntervalFromJson(DeserializationVisitorInterface $visitor, $data, array $type): ?\DateInterval
    {
        if (null === $data) {
            return null;
        }

        return $this->parseDateInterval($data);
    }

    /**
     * @param mixed $data
     * @param TypeArray $type
     */
    private function parseDateTime($data, array $type, bool $immutable = false): \DateTimeInterface
    {
        $timezone = !empty($type['params'][1]) ? new \DateTimeZone($type['params'][1]) : $this->defaultTimezone;
        $formats = $this->getDeserializationFormats($type);

        $formatTried = [];
        foreach ($formats as $format) {
            if ($immutable) {
                $datetime = \DateTimeImmutable::createFromFormat($format, (string) $data, $timezone);
            } else {
                $datetime = \DateTime::createFromFormat($format, (string) $data, $timezone);
            }

            if (false !== $datetime) {
                if ('U' === $format) {
                    $datetime = $datetime->setTimezone($timezone);
                }

                return $datetime;
            }

            $formatTried[] = $format;
        }

        throw new RuntimeException(sprintf(
            'Invalid datetime "%s", expected one of the format %s.',
            $data,
            '"' . implode('", "', $formatTried) . '"',
        ));
    }

    private function parseDateInterval(string $data): \DateInterval
    {
        $dateInterval = null;
        try {
            $f = 0.0;
            if (preg_match('~\.\d+~', $data, $match)) {
                $data = str_replace($match[0], '', $data);
                $f = (float) $match[0];
            }

            $dateInterval = new \DateInterval($data);
            $dateInterval->f = $f;
        } catch (\Throwable $e) {
            throw new RuntimeException(sprintf('Invalid dateinterval "%s", expected ISO 8601 format', $data), 0, $e);
        }

        return $dateInterval;
    }

    /**
     * @param TypeArray $type
     */
    private function getDeserializationFormats(array $type): array
    {
        if (isset($type['params'][2])) {
            return is_array($type['params'][2]) ? $type['params'][2] : [$type['params'][2]];
        }

        if (isset($type['params'][0])) {
            return [$type['params'][0]];
        }

        return $this->defaultDeserializationFormats;
    }

    /**
     * @param TypeArray $type
     */
    private function getSerializationFormat(array $type): string
    {
        return $type['params'][0] ?? $this->defaultSerializationFormat;
    }

    public function format(\DateInterval $dateInterval): string
    {
        $format = 'P';

        if (0 < $dateInterval->y) {
            $format .= $dateInterval->y . 'Y';
        }

        if (0 < $dateInterval->m) {
            $format .= $dateInterval->m . 'M';
        }

        if (0 < $dateInterval->d) {
            $format .= $dateInterval->d . 'D';
        }

        if (0 < $dateInterval->h || 0 < $dateInterval->i || 0 < $dateInterval->s) {
            $format .= 'T';
        }

        if (0 < $dateInterval->h) {
            $format .= $dateInterval->h . 'H';
        }

        if (0 < $dateInterval->i) {
            $format .= $dateInterval->i . 'M';
        }

        if (0 < $dateInterval->s) {
            $format .= $dateInterval->s . 'S';
        }

        if ('P' === $format) {
            $format = 'P0DT0S';
        }

        return $format;
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Handler;

interface SubscribingHandlerInterface
{
    /**
     * Return format:
     *
     *      array(
     *          array(
     *              'direction' => GraphNavigatorInterface::DIRECTION_SERIALIZATION,
     *              'format' => 'json',
     *              'type' => 'DateTime',
     *              'method' => 'serializeDateTimeToJson',
     *          ),
     *      )
     *
     * The direction and method keys can be omitted.
     *
     * @return array
     *
     * @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingReturnTypeHint
     */
    public static function getSubscribingMethods();
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Handler;

use JMS\Serializer\GraphNavigatorInterface;
use JMS\Serializer\Metadata\StaticPropertyMetadata;
use JMS\Serializer\SerializationContext;
use JMS\Serializer\Type\Type;
use JMS\Serializer\Visitor\SerializationVisitorInterface;

/**
 * @author Asmir Mustafic <goetas@gmail.com>
 *
 * @phpstan-import-type TypeArray from Type
 */
final class StdClassHandler implements SubscribingHandlerInterface
{
    /**
     * {@inheritdoc}
     */
    public static function getSubscribingMethods()
    {
        $methods = [];
        $formats = ['json', 'xml'];

        foreach ($formats as $format) {
            $methods[] = [
                'direction' => GraphNavigatorInterface::DIRECTION_SERIALIZATION,
                'type' => \stdClass::class,
                'format' => $format,
                'method' => 'serializeStdClass',
            ];
        }

        return $methods;
    }

    /**
     * @param TypeArray $type
     *
     * @return mixed
     */
    public function serializeStdClass(SerializationVisitorInterface $visitor, \stdClass $stdClass, array $type, SerializationContext $context)
    {
        $classMetadata = $context->getMetadataFactory()->getMetadataForClass('stdClass');
        $visitor->startVisitingObject($classMetadata, $stdClass, ['name' => 'stdClass']);

        foreach ((array) $stdClass as $name => $value) {
            $metadata = new StaticPropertyMetadata('stdClass', $name, $value);
            $visitor->visitProperty($metadata, $value);
        }

        return $visitor->endVisitingObject($classMetadata, $stdClass, ['name' => 'stdClass']);
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Handler;

use ArrayIterator;
use Generator;
use Iterator;
use JMS\Serializer\DeserializationContext;
use JMS\Serializer\Functions;
use JMS\Serializer\GraphNavigatorInterface;
use JMS\Serializer\SerializationContext;
use JMS\Serializer\Type\Type;
use JMS\Serializer\Visitor\DeserializationVisitorInterface;
use JMS\Serializer\Visitor\SerializationVisitorInterface;

/**
 * @phpstan-import-type TypeArray from Type
 */
final class IteratorHandler implements SubscribingHandlerInterface
{
    private const SUPPORTED_FORMATS = ['json', 'xml'];

    /**
     * {@inheritdoc}
     */
    public static function getSubscribingMethods()
    {
        $methods = [];

        foreach (self::SUPPORTED_FORMATS as $format) {
            $methods[] = [
                'direction' => GraphNavigatorInterface::DIRECTION_SERIALIZATION,
                'type' => Iterator::class,
                'format' => $format,
                'method' => 'serializeIterable',
            ];

            $methods[] = [
                'direction' => GraphNavigatorInterface::DIRECTION_DESERIALIZATION,
                'type' => Iterator::class,
                'format' => $format,
                'method' => 'deserializeIterator',
            ];
        }

        foreach (self::SUPPORTED_FORMATS as $format) {
            $methods[] = [
                'direction' => GraphNavigatorInterface::DIRECTION_SERIALIZATION,
                'type' => ArrayIterator::class,
                'format' => $format,
                'method' => 'serializeIterable',
            ];

            $methods[] = [
                'direction' => GraphNavigatorInterface::DIRECTION_DESERIALIZATION,
                'type' => ArrayIterator::class,
                'format' => $format,
                'method' => 'deserializeIterator',
            ];
        }

        foreach (self::SUPPORTED_FORMATS as $format) {
            $methods[] = [
                'direction' => GraphNavigatorInterface::DIRECTION_SERIALIZATION,
                'type' => Generator::class,
                'format' => $format,
                'method' => 'serializeIterable',
            ];

            $methods[] = [
                'direction' => GraphNavigatorInterface::DIRECTION_DESERIALIZATION,
                'type' => Generator::class,
                'format' => $format,
                'method' => 'deserializeGenerator',
            ];
        }

        return $methods;
    }

    /**
     * @param TypeArray $type
     *
     * @return array|\ArrayObject|null
     */
    public function serializeIterable(
        SerializationVisitorInterface $visitor,
        iterable $iterable,
        array $type,
        SerializationContext $context
    ): ?iterable {
        $type['name'] = 'array';

        $context->stopVisiting($iterable);
        $result = $visitor->visitArray(Functions::iterableToArray($iterable), $type);
        $context->startVisiting($iterable);

        return $result;
    }

    /**
     * @param TypeArray $type
     * @param mixed $data
     */
    public function deserializeIterator(
        DeserializationVisitorInterface $visitor,
        $data,
        array $type,
        DeserializationContext $context
    ): \Iterator {
        $type['name'] = 'array';

        return new ArrayIterator($visitor->visitArray($data, $type));
    }

    /**
     * @param mixed $data
     * @param TypeArray $type
     */
    public function deserializeGenerator(
        DeserializationVisitorInterface $visitor,
        $data,
        array $type,
        DeserializationContext $context
    ): Generator {
        return (static function () use (&$visitor, &$data, &$type): Generator {
            $type['name'] = 'array';
            yield from $visitor->visitArray($data, $type);
        })();
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Handler;

use JMS\Serializer\Exception\InvalidMetadataException;
use JMS\Serializer\Exception\RuntimeException;
use JMS\Serializer\GraphNavigatorInterface;
use JMS\Serializer\SerializationContext;
use JMS\Serializer\Type\Type;
use JMS\Serializer\Visitor\DeserializationVisitorInterface;
use JMS\Serializer\Visitor\SerializationVisitorInterface;

/**
 * @phpstan-import-type TypeArray from Type
 */
final class EnumHandler implements SubscribingHandlerInterface
{
    /**
     * {@inheritdoc}
     */
    public static function getSubscribingMethods()
    {
        $methods = [];

        foreach (['json', 'xml'] as $format) {
            $methods[] = [
                'type' => 'enum',
                'direction' => GraphNavigatorInterface::DIRECTION_DESERIALIZATION,
                'format' => $format,
                'method' => 'deserializeEnum',
            ];
            $methods[] = [
                'type' => 'enum',
                'format' => $format,
                'direction' => GraphNavigatorInterface::DIRECTION_SERIALIZATION,
                'method' => 'serializeEnum',
            ];
        }

        return $methods;
    }

    /**
     * @param TypeArray $type
     */
    public function serializeEnum(
        SerializationVisitorInterface $visitor,
        \UnitEnum $enum,
        array $type,
        SerializationContext $context
    ) {
        if ((isset($type['params'][1]) && 'value' === $type['params'][1]) || (!isset($type['params'][1]) && $enum instanceof \BackedEnum)) {
            if (!$enum instanceof \BackedEnum) {
                throw new InvalidMetadataException(sprintf('The type "%s" is not a backed enum, thus you can not use "value" as serialization mode for its value.', get_class($enum)));
            }

            $valueType = isset($type['params'][2]) ? ['name' => $type['params'][2]] : null;

            return $context->getNavigator()->accept($enum->value, $valueType);
        } else {
            return $context->getNavigator()->accept($enum->name);
        }
    }

    /**
     * @param int|string|\SimpleXMLElement $data
     * @param TypeArray $type
     */
    public function deserializeEnum(DeserializationVisitorInterface $visitor, $data, array $type): ?\UnitEnum
    {
        $enumType = $type['params'][0];
        if (isset($enumType['name'])) {
            $enumType = $enumType['name'];
        } else {
            trigger_deprecation('jms/serializer', '3.31', "Using enum<'Type'> or similar is deprecated, use enum<Type> instead.");
        }

        $caseValue = (string) $data;

        $ref = new \ReflectionEnum($enumType);
        if (isset($type['params'][1]) && 'value' === $type['params'][1] || (!isset($type['params'][1]) && is_a($enumType, \BackedEnum::class, true))) {
            if (!is_a($enumType, \BackedEnum::class, true)) {
                throw new InvalidMetadataException(sprintf('The type "%s" is not a backed enum, thus you can not use "value" as serialization mode for its value.', $enumType));
            }

            if ('int' === $ref->getBackingType()->getName()) {
                if (!is_numeric($caseValue)) {
                    throw new RuntimeException(sprintf('"%s" is not a valid backing value for enum "%s"', $caseValue, $enumType));
                }

                $caseValue = (int) $caseValue;
            }

            return $enumType::from($caseValue);
        } else {
            if (!$ref->hasCase($caseValue)) {
                throw new InvalidMetadataException(sprintf('The type "%s" does not have the case "%s"', $ref->getName(), $caseValue));
            }

            return $ref->getCase($caseValue)->getValue();
        }
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Handler;

use JMS\Serializer\DeserializationContext;
use JMS\Serializer\Exception\InvalidArgumentException;
use JMS\Serializer\GraphNavigatorInterface;
use JMS\Serializer\SerializationContext;
use JMS\Serializer\Visitor\DeserializationVisitorInterface;
use JMS\Serializer\Visitor\SerializationVisitorInterface;
use JMS\Serializer\XmlSerializationVisitor;
use Symfony\Component\Uid\AbstractUid;
use Symfony\Component\Uid\Ulid;
use Symfony\Component\Uid\Uuid;
use Symfony\Component\Uid\UuidV1;
use Symfony\Component\Uid\UuidV3;
use Symfony\Component\Uid\UuidV4;
use Symfony\Component\Uid\UuidV5;
use Symfony\Component\Uid\UuidV6;
use Symfony\Component\Uid\UuidV7;
use Symfony\Component\Uid\UuidV8;

final class SymfonyUidHandler implements SubscribingHandlerInterface
{
    public const FORMAT_BASE32    = 'base32';
    public const FORMAT_BASE58    = 'base58';
    public const FORMAT_CANONICAL = 'canonical';
    public const FORMAT_RFC4122   = 'rfc4122';

    private const UID_CLASSES = [
        Ulid::class,
        Uuid::class,
        UuidV1::class,
        UuidV3::class,
        UuidV4::class,
        UuidV5::class,
        UuidV6::class,
        UuidV7::class,
        UuidV8::class,
    ];

    /**
     * @var string
     * @phpstan-var self::FORMAT_*
     */
    private $defaultFormat;

    /**
     * @var bool
     */
    private $xmlCData;

    public function __construct(string $defaultFormat = self::FORMAT_CANONICAL, bool $xmlCData = true)
    {
        $this->defaultFormat = $defaultFormat;
        $this->xmlCData = $xmlCData;
    }

    public static function getSubscribingMethods(): array
    {
        $methods = [];
        $formats = ['json', 'xml'];

        foreach ($formats as $format) {
            foreach (self::UID_CLASSES as $class) {
                $methods[] = [
                    'direction' => GraphNavigatorInterface::DIRECTION_SERIALIZATION,
                    'format'    => $format,
                    'type'      => $class,
                    'method'    => 'serializeUid',
                ];

                $methods[] = [
                    'direction' => GraphNavigatorInterface::DIRECTION_DESERIALIZATION,
                    'format'    => $format,
                    'type'      => $class,
                    'method'    => 'deserializeUidFrom' . ucfirst($format),
                ];
            }
        }

        return $methods;
    }

    /**
     * @phpstan-param array{name: class-string<AbstractUid>, params: array} $type
     */
    public function deserializeUidFromJson(DeserializationVisitorInterface $visitor, ?string $data, array $type, DeserializationContext $context): ?AbstractUid
    {
        if (null === $data) {
            return null;
        }

        return $this->deserializeUid($data, $type);
    }

    /**
     * @phpstan-param array{name: class-string<AbstractUid>, params: array} $type
     */
    public function deserializeUidFromXml(DeserializationVisitorInterface $visitor, \SimpleXMLElement $data, array $type, DeserializationContext $context): ?AbstractUid
    {
        if ($this->isDataXmlNull($data)) {
            return null;
        }

        return $this->deserializeUid((string) $data, $type);
    }

    /**
     * @phpstan-param array{name: class-string<AbstractUid>, params: array} $type
     */
    private function deserializeUid(string $data, array $type): ?AbstractUid
    {
        /** @var class-string<AbstractUid> $uidClass */
        $uidClass = $type['name'];

        try {
            return $uidClass::fromString($data);
        } catch (\InvalidArgumentException | \TypeError $exception) {
            throw new InvalidArgumentException(sprintf('"%s" is not a valid UID string.', $data), 0, $exception);
        }
    }

    /**
     * @return \DOMText|string
     *
     * @phpstan-param array{name: class-string<AbstractUid>, params: array} $type
     */
    public function serializeUid(SerializationVisitorInterface $visitor, AbstractUid $uid, array $type, SerializationContext $context)
    {
        /** @phpstan-var self::FORMAT_* $format */
        $format = $type['params'][0]['name'] ?? $this->defaultFormat;

        switch ($format) {
            case self::FORMAT_BASE32:
                $serialized = $uid->toBase32();
                break;

            case self::FORMAT_BASE58:
                $serialized = $uid->toBase58();
                break;

            case self::FORMAT_CANONICAL:
                $serialized = (string) $uid;
                break;

            case self::FORMAT_RFC4122:
                $serialized = $uid->toRfc4122();
                break;

            default:
                throw new InvalidArgumentException(sprintf('The "%s" format is not valid.', $format));
        }

        if ($visitor instanceof XmlSerializationVisitor && false === $this->xmlCData) {
            return $visitor->visitSimpleString($serialized, $type);
        }

        return $visitor->visitString($serialized, $type);
    }

    /**
     * @param mixed $data
     */
    private function isDataXmlNull($data): bool
    {
        $attributes = $data->attributes('xsi', true);

        return isset($attributes['nil'][0]) && 'true' === (string) $attributes['nil'][0];
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Handler;

/**
 * Handler Registry Interface.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
interface HandlerRegistryInterface
{
    public function registerSubscribingHandler(SubscribingHandlerInterface $handler): void;

    /**
     * Registers a handler in the registry.
     *
     * @param int $direction one of the GraphNavigatorInterface::DIRECTION_??? constants
     * @param object|callable $handler   function(visitor, mixed $data, array $type): mixed
     */
    public function registerHandler(int $direction, string $typeName, string $format, $handler): void;

    /**
     * @param int $direction one of the GraphNavigatorInterface::DIRECTION_??? constants
     *
     * @return callable|object|null
     */
    public function getHandler(int $direction, string $typeName, string $format);
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Handler;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ODM\MongoDB\PersistentCollection as MongoPersistentCollection;
use Doctrine\ODM\PHPCR\PersistentCollection as PhpcrPersistentCollection;
use Doctrine\ORM\PersistentCollection as OrmPersistentCollection;
use Doctrine\Persistence\ManagerRegistry;
use JMS\Serializer\DeserializationContext;
use JMS\Serializer\GraphNavigatorInterface;
use JMS\Serializer\Metadata\PropertyMetadata;
use JMS\Serializer\SerializationContext;
use JMS\Serializer\Type\Type;
use JMS\Serializer\Visitor\DeserializationVisitorInterface;
use JMS\Serializer\Visitor\SerializationVisitorInterface;

/**
 * @phpstan-import-type TypeArray from Type
 */
final class ArrayCollectionHandler implements SubscribingHandlerInterface
{
    public const COLLECTION_TYPES = [
        'ArrayCollection',
        ArrayCollection::class,
        OrmPersistentCollection::class,
        MongoPersistentCollection::class,
        PhpcrPersistentCollection::class,
    ];

    /**
     * @var bool
     */
    private $initializeExcluded;

    /**
     * @var ManagerRegistry|null
     */
    private $managerRegistry;

    public function __construct(
        bool $initializeExcluded = true,
        ?ManagerRegistry $managerRegistry = null
    ) {
        $this->initializeExcluded = $initializeExcluded;
        $this->managerRegistry = $managerRegistry;
    }

    /**
     * {@inheritdoc}
     */
    public static function getSubscribingMethods()
    {
        $methods = [];
        $formats = ['json', 'xml'];

        foreach (self::COLLECTION_TYPES as $type) {
            foreach ($formats as $format) {
                $methods[] = [
                    'direction' => GraphNavigatorInterface::DIRECTION_SERIALIZATION,
                    'type' => $type,
                    'format' => $format,
                    'method' => 'serializeCollection',
                ];

                $methods[] = [
                    'direction' => GraphNavigatorInterface::DIRECTION_DESERIALIZATION,
                    'type' => $type,
                    'format' => $format,
                    'method' => 'deserializeCollection',
                ];
            }
        }

        return $methods;
    }

    /**
     * @param TypeArray $type
     *
     * @return array|\ArrayObject
     */
    public function serializeCollection(SerializationVisitorInterface $visitor, Collection $collection, array $type, SerializationContext $context)
    {
        // We change the base type, and pass through possible parameters.
        $type['name'] = 'array';

        $context->stopVisiting($collection);

        if (false === $this->initializeExcluded) {
            $exclusionStrategy = $context->getExclusionStrategy();
            if (null !== $exclusionStrategy && $exclusionStrategy->shouldSkipClass($context->getMetadataFactory()->getMetadataForClass(\get_class($collection)), $context)) {
                $context->startVisiting($collection);

                return $visitor->visitArray([], $type);
            }
        }

        $result = $visitor->visitArray($collection->toArray(), $type);

        $context->startVisiting($collection);

        return $result;
    }

    /**
     * @param mixed $data
     * @param TypeArray $type
     */
    public function deserializeCollection(
        DeserializationVisitorInterface $visitor,
        $data,
        array $type,
        DeserializationContext $context
    ): Collection {
        // See above.
        $type['name'] = 'array';

        $elements = new ArrayCollection($visitor->visitArray($data, $type));

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

        $propertyMetadata = $context->getMetadataStack()->top();
        if (!$propertyMetadata instanceof PropertyMetadata) {
            return $elements;
        }

        $objectManager = $this->managerRegistry->getManagerForClass($propertyMetadata->class);
        if (null === $objectManager) {
            return $elements;
        }

        $classMetadata = $objectManager->getClassMetadata($propertyMetadata->class);
        $currentObject = $visitor->getCurrentObject();

        if (
            array_key_exists('name', $propertyMetadata->type)
            && in_array($propertyMetadata->type['name'], self::COLLECTION_TYPES)
            && $classMetadata->isCollectionValuedAssociation($propertyMetadata->name)
        ) {
            $existingCollection = $classMetadata->getFieldValue($currentObject, $propertyMetadata->name);
            if (!$existingCollection instanceof OrmPersistentCollection) {
                return $elements;
            }

            foreach ($elements as $element) {
                if (!$existingCollection->contains($element)) {
                    $existingCollection->add($element);
                }
            }

            foreach ($existingCollection as $collectionElement) {
                if (!$elements->contains($collectionElement)) {
                    $existingCollection->removeElement($collectionElement);
                }
            }

            return $existingCollection;
        }

        return $elements;
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Handler;

use JMS\Serializer\Exception\LogicException;
use JMS\Serializer\Exception\RuntimeException;
use JMS\Serializer\GraphNavigatorInterface;

class HandlerRegistry implements HandlerRegistryInterface
{
    /**
     * @var callable[]
     */
    protected $handlers;

    public static function getDefaultMethod(int $direction, string $type, string $format): string
    {
        if (false !== $pos = strrpos($type, '\\')) {
            $type = substr($type, $pos + 1);
        }

        switch ($direction) {
            case GraphNavigatorInterface::DIRECTION_DESERIALIZATION:
                return 'deserialize' . $type . 'From' . $format;

            case GraphNavigatorInterface::DIRECTION_SERIALIZATION:
                return 'serialize' . $type . 'To' . $format;

            default:
                throw new LogicException(sprintf('The direction %s does not exist; see GraphNavigatorInterface::DIRECTION_??? constants.', json_encode($direction)));
        }
    }

    public function __construct(array $handlers = [])
    {
        $this->handlers = $handlers;
    }

    public function registerSubscribingHandler(SubscribingHandlerInterface $handler): void
    {
        foreach ($handler->getSubscribingMethods() as $methodData) {
            if (!isset($methodData['type'], $methodData['format'])) {
                throw new RuntimeException(sprintf('For each subscribing method a "type" and "format" attribute must be given, but only got "%s" for %s.', implode('" and "', array_keys($methodData)), \get_class($handler)));
            }

            $directions = [GraphNavigatorInterface::DIRECTION_DESERIALIZATION, GraphNavigatorInterface::DIRECTION_SERIALIZATION];
            if (isset($methodData['direction'])) {
                $directions = [$methodData['direction']];
            }

            foreach ($directions as $direction) {
                $method = $methodData['method'] ?? self::getDefaultMethod($direction, $methodData['type'], $methodData['format']);
                $this->registerHandler($direction, $methodData['type'], $methodData['format'], [$handler, $method]);
            }
        }
    }

    /**
     * {@inheritdoc}
     */
    public function registerHandler(int $direction, string $typeName, string $format, $handler): void
    {
        $this->handlers[$direction][$typeName][$format] = $handler;
    }

    /**
     * {@inheritdoc}
     */
    public function getHandler(int $direction, string $typeName, string $format)
    {
        if (!isset($this->handlers[$direction][$typeName][$format])) {
            return null;
        }

        return $this->handlers[$direction][$typeName][$format];
    }

    /**
     * @internal Used for profiling
     */
    public function getHandlers(): array
    {
        return $this->handlers;
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer;

use JMS\Serializer\Exception\InvalidArgumentException;
use JMS\Serializer\Exception\LogicException;
use JMS\Serializer\Exception\NotAcceptableException;
use JMS\Serializer\Exception\RuntimeException;
use JMS\Serializer\Exception\XmlErrorException;
use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\Metadata\PropertyMetadata;
use JMS\Serializer\Visitor\DeserializationVisitorInterface;

final class XmlDeserializationVisitor extends AbstractVisitor implements NullAwareVisitorInterface, DeserializationVisitorInterface
{
    /**
     * @var \SplStack
     */
    private $objectStack;

    /**
     * @var \SplStack
     */
    private $metadataStack;

    /**
     * @var \SplStack
     */
    private $objectMetadataStack;

    /**
     * @var object|null
     */
    private $currentObject;

    /**
     * @var ClassMetadata|PropertyMetadata|null
     */
    private $currentMetadata;

    /**
     * @var bool
     */
    private $disableExternalEntities;

    /**
     * @var string[]
     */
    private $doctypeAllowList;
    /**
     * @var int
     */
    private $options;

    public function __construct(
        bool $disableExternalEntities = true,
        array $doctypeAllowList = [],
        int $options = 0
    ) {
        $this->objectStack = new \SplStack();
        $this->metadataStack = new \SplStack();
        $this->objectMetadataStack = new \SplStack();
        $this->disableExternalEntities = $disableExternalEntities;
        $this->doctypeAllowList = $doctypeAllowList;
        $this->options = $options;
    }

    /**
     * {@inheritdoc}
     */
    public function prepare($data)
    {
        $data = $this->emptyStringToSpaceCharacter($data);

        $previous = libxml_use_internal_errors(true);
        libxml_clear_errors();

        $previousEntityLoaderState = null;
        if (\LIBXML_VERSION < 20900) {
            // phpcs:ignore Generic.PHP.DeprecatedFunctions.Deprecated
            $previousEntityLoaderState = libxml_disable_entity_loader($this->disableExternalEntities);
        }

        if (false !== stripos($data, '<!doctype')) {
            $internalSubset = $this->getDomDocumentTypeEntitySubset($data);
            if (!in_array($internalSubset, $this->doctypeAllowList, true)) {
                throw new InvalidArgumentException(sprintf(
                    'The document type "%s" is not allowed. If it is safe, you may add it to the allowlist configuration.',
                    $internalSubset,
                ));
            }
        }

        $doc = simplexml_load_string($data, 'SimpleXMLElement', $this->options);

        libxml_use_internal_errors($previous);

        if (\LIBXML_VERSION < 20900) {
            // phpcs:ignore Generic.PHP.DeprecatedFunctions.Deprecated
            libxml_disable_entity_loader($previousEntityLoaderState);
        }

        if (false === $doc) {
            throw new XmlErrorException(libxml_get_last_error());
        }

        return $doc;
    }

    /**
     * @param mixed $data
     */
    private function emptyStringToSpaceCharacter($data): string
    {
        return '' === $data ? ' ' : (string) $data;
    }

    /**
     * {@inheritdoc}
     */
    public function visitNull($data, array $type)
    {
        return null;
    }

    /**
     * {@inheritdoc}
     */
    public function visitString($data, array $type): string
    {
        $this->assertValueCanBeCastToString($data);

        return (string) $data;
    }

    /**
     * {@inheritdoc}
     */
    public function visitBoolean($data, array $type): bool
    {
        $this->assertValueCanBeCastToString($data);

        $data = (string) $data;

        if ('true' === $data || '1' === $data) {
            return true;
        } elseif ('false' === $data || '0' === $data) {
            return false;
        } else {
            throw new RuntimeException(sprintf('Could not convert data to boolean. Expected "true", "false", "1" or "0", but got %s.', json_encode($data)));
        }
    }

    /**
     * {@inheritdoc}
     */
    public function visitInteger($data, array $type): int
    {
        $this->assertValueCanBeCastToInt($data);

        return (int) $data;
    }

    /**
     * {@inheritdoc}
     */
    public function visitDouble($data, array $type): float
    {
        $this->assertValueCanCastToFloat($data);

        return (float) $data;
    }

    /**
     * {@inheritdoc}
     */
    public function visitArray($data, array $type): array
    {
        // handle key-value-pairs
        if (null !== $this->currentMetadata && $this->currentMetadata->xmlKeyValuePairs) {
            if (2 !== count($type['params'])) {
                throw new RuntimeException('The array type must be specified as "array<K,V>" for Key-Value-Pairs.');
            }

            $this->revertCurrentMetadata();

            [$keyType, $entryType] = $type['params'];

            $result = [];
            foreach ($data as $key => $v) {
                $k = $this->navigator->accept($key, $keyType);
                $result[$k] = $this->navigator->accept($v, $entryType);
            }

            return $result;
        }

        $entryName = null !== $this->currentMetadata && $this->currentMetadata->xmlEntryName ? $this->currentMetadata->xmlEntryName : 'entry';
        $namespace = null !== $this->currentMetadata && $this->currentMetadata->xmlEntryNamespace ? $this->currentMetadata->xmlEntryNamespace : null;

        if (null === $namespace && $this->objectMetadataStack->count()) {
            $classMetadata = $this->objectMetadataStack->top();
            $namespace = $classMetadata->xmlNamespaces[''] ?? $namespace;
            if (null === $namespace) {
                $namespaces = $data->getDocNamespaces();
                if (isset($namespaces[''])) {
                    $namespace = $namespaces[''];
                }
            }
        }

        if (null !== $namespace) {
            $prefix = uniqid('ns-');
            $data->registerXPathNamespace($prefix, $namespace);
            $nodes = $data->xpath(sprintf('%s:%s', $prefix, $entryName));
        } else {
            $nodes = $data->xpath($entryName);
        }

        if (null === $nodes || !\count($nodes)) {
            return [];
        }

        switch (\count($type['params'])) {
            case 0:
                throw new RuntimeException(sprintf('The array type must be specified either as "array<T>", or "array<K,V>".'));

            case 1:
                $result = [];

                foreach ($nodes as $v) {
                    $result[] = $this->navigator->accept($v, $type['params'][0]);
                }

                return $result;

            case 2:
                if (null === $this->currentMetadata) {
                    throw new RuntimeException('Maps are not supported on top-level without metadata.');
                }

                [$keyType, $entryType] = $type['params'];
                $result = [];

                $nodes = $data->children($namespace)->$entryName;
                foreach ($nodes as $v) {
                    $attrs = $v->attributes();
                    if (!isset($attrs[$this->currentMetadata->xmlKeyAttribute])) {
                        throw new RuntimeException(sprintf('The key attribute "%s" must be set for each entry of the map.', $this->currentMetadata->xmlKeyAttribute));
                    }

                    $k = $this->navigator->accept($attrs[$this->currentMetadata->xmlKeyAttribute], $keyType);
                    $result[$k] = $this->navigator->accept($v, $entryType);
                }

                return $result;

            default:
                throw new LogicException(sprintf('The array type does not support more than 2 parameters, but got %s.', json_encode($type['params'])));
        }
    }

    /**
     * {@inheritdoc}
     */
    public function visitDiscriminatorMapProperty($data, ClassMetadata $metadata): string
    {
        switch (true) {
            // Check XML attribute without namespace for discriminatorFieldName
            case $metadata->xmlDiscriminatorAttribute && null === $metadata->xmlDiscriminatorNamespace && isset($data->attributes()->{$metadata->discriminatorFieldName}):
                return (string) $data->attributes()->{$metadata->discriminatorFieldName};

            // Check XML attribute with namespace for discriminatorFieldName
            case $metadata->xmlDiscriminatorAttribute && null !== $metadata->xmlDiscriminatorNamespace && isset($data->attributes($metadata->xmlDiscriminatorNamespace)->{$metadata->discriminatorFieldName}):
                return (string) $data->attributes($metadata->xmlDiscriminatorNamespace)->{$metadata->discriminatorFieldName};

            // Check XML element with namespace for discriminatorFieldName
            case !$metadata->xmlDiscriminatorAttribute && null !== $metadata->xmlDiscriminatorNamespace && isset($data->children($metadata->xmlDiscriminatorNamespace)->{$metadata->discriminatorFieldName}):
                return (string) $data->children($metadata->xmlDiscriminatorNamespace)->{$metadata->discriminatorFieldName};

            // Check XML element for discriminatorFieldName
            case isset($data->{$metadata->discriminatorFieldName}):
                return (string) $data->{$metadata->discriminatorFieldName};

            default:
                throw new LogicException(sprintf(
                    'The discriminator field name "%s" for base-class "%s" was not found in input data.',
                    $metadata->discriminatorFieldName,
                    $metadata->name,
                ));
        }
    }

    public function startVisitingObject(ClassMetadata $metadata, object $object, array $type): void
    {
        $this->setCurrentObject($object);
        $this->objectMetadataStack->push($metadata);
    }

    /**
     * {@inheritdoc}
     */
    public function visitProperty(PropertyMetadata $metadata, $data)
    {
        $name = $metadata->serializedName;

        if (true === $metadata->inline) {
            if (!$metadata->type) {
                throw RuntimeException::noMetadataForProperty($metadata->class, $metadata->name);
            }

            return $this->navigator->accept($data, $metadata->type);
        }

        if ($metadata->xmlAttribute) {
            $attributes = $data->attributes($metadata->xmlNamespace);
            if (isset($attributes[$name])) {
                if (!$metadata->type) {
                    throw RuntimeException::noMetadataForProperty($metadata->class, $metadata->name);
                }

                return $this->navigator->accept($attributes[$name], $metadata->type);
            }

            throw new NotAcceptableException();
        }

        if (0 === strpos($name, '@')) {
            $attributeName = substr($name, 1);
            $attributes = $data->attributes($metadata->xmlNamespace);

            if (isset($attributes[$attributeName])) {
                if (!$metadata->type) {
                    throw RuntimeException::noMetadataForProperty($metadata->class, $metadata->name);
                }

                return $this->navigator->accept($attributes[$attributeName], $metadata->type);
            }

            throw new NotAcceptableException(sprintf('Attribute "%s" (derived from serializedName "%s") in namespace "%s" not found for property %s::$%s. XML: %s', $attributeName, $name, $metadata->xmlNamespace ?? '[none]', $metadata->class, $metadata->name, $data->asXML()));
        }

        if (false !== strpos($name, '/@')) {
            [$elementName, $attributeName] = explode('/@', $name, 2);

            $childDataNode = null;
            if ('' === $metadata->xmlNamespace) {
                // Element explicitly in NO namespace
                $xpathQuery = "./*[local-name()='" . $elementName . "' and (namespace-uri()='' or not(namespace-uri()))]";
                $matchingNodes = $data->xpath($xpathQuery);
                if (!empty($matchingNodes)) {
                    $childDataNode = $matchingNodes[0];
                }
            } elseif ($metadata->xmlNamespace) {
                // Element in a specific namespace URI
                $childrenInNs = $data->children($metadata->xmlNamespace);
                if (isset($childrenInNs->$elementName)) {
                    $childDataNode = $childrenInNs->$elementName;
                }
            } else {
                // xmlNamespace is null: element in default namespace (or no namespace if no default is active)
                $childrenInDefaultOrNoNs = $data->children(null);
                if (isset($childrenInDefaultOrNoNs->$elementName)) {
                    $childDataNode = $childrenInDefaultOrNoNs->$elementName;
                }
            }

            if (!$childDataNode || !$childDataNode->getName()) {
                if (null === $metadata->xmlNamespace) {
                    $ns = '[default/none]';
                } else {
                    $ns = '' === $metadata->xmlNamespace ? '[none]' : $metadata->xmlNamespace;
                }

                throw new NotAcceptableException(sprintf('Child element "%s" for attribute access not found (element namespace: %s). Property %s::$%s. XML: %s', $elementName, $ns, $metadata->class, $metadata->name, $data->asXML()));
            }

            $attributeTargetNs = $metadata->xmlNamespace && '' !== $metadata->xmlNamespace ? $metadata->xmlNamespace : null;
            $attributes = $childDataNode->attributes($attributeTargetNs);

            if (isset($attributes[$attributeName])) {
                if (!$metadata->type) {
                    throw RuntimeException::noMetadataForProperty($metadata->class, $metadata->name);
                }

                return $this->navigator->accept($attributes[$attributeName], $metadata->type);
            }

            throw new NotAcceptableException(sprintf('Attribute "%s" on element "%s" not found (attribute namespace: %s). Property %s::$%s. XML: %s', $attributeName, $elementName, $attributeTargetNs ?? '[none]', $metadata->class, $metadata->name, $data->asXML()));
        }

        if ($metadata->xmlValue) {
            if (!$metadata->type) {
                throw RuntimeException::noMetadataForProperty($metadata->class, $metadata->name);
            }

            return $this->navigator->accept($data, $metadata->type);
        }

        if ($metadata->xmlCollection) {
            $enclosingElem = $data;
            if (!$metadata->xmlCollectionInline) {
                $enclosingElem = $data->children($metadata->xmlNamespace)->$name;
            }

            $this->setCurrentMetadata($metadata);
            if (!$metadata->type) {
                throw RuntimeException::noMetadataForProperty($metadata->class, $metadata->name);
            }

            $v = $this->navigator->accept($enclosingElem, $metadata->type);
            $this->revertCurrentMetadata();

            return $v;
        }

        if ($metadata->xmlNamespace) {
            $node = $data->children($metadata->xmlNamespace)->$name;
            if (!$node->count()) {
                throw new NotAcceptableException();
            }
        } elseif ('' === $metadata->xmlNamespace) {
            // See #1087 - element must be like: <element xmlns="" /> - https://www.w3.org/TR/REC-xml-names/#iri-use
            // Use of an empty string in a namespace declaration turns it into an "undeclaration".
            $nodes = $data->xpath('./' . $name);
            if (empty($nodes)) {
                throw new NotAcceptableException();
            }

            $node = reset($nodes);
        } else {
            $namespaces = $data->getDocNamespaces();
            if (isset($namespaces[''])) {
                $prefix = uniqid('ns-');
                $data->registerXPathNamespace($prefix, $namespaces['']);
                $nodes = $data->xpath('./' . $prefix . ':' . $name);
            } else {
                $nodes = $data->xpath('./' . $name);
            }

            if (empty($nodes)) {
                throw new NotAcceptableException();
            }

            $node = reset($nodes);
        }

        if ($metadata->xmlKeyValuePairs) {
            $this->setCurrentMetadata($metadata);
        }

        if (!$metadata->type) {
            throw RuntimeException::noMetadataForProperty($metadata->class, $metadata->name);
        }

        return $this->navigator->accept($node, $metadata->type);
    }

    /**
     * {@inheritdoc}
     */
    public function endVisitingObject(ClassMetadata $metadata, $data, array $type): object
    {
        $rs = $this->currentObject;
        $this->objectMetadataStack->pop();
        $this->revertCurrentObject();

        return $rs;
    }

    public function setCurrentObject(object $object): void
    {
        $this->objectStack->push($this->currentObject);
        $this->currentObject = $object;
    }

    public function getCurrentObject(): ?object
    {
        return $this->currentObject;
    }

    public function revertCurrentObject(): ?object
    {
        return $this->currentObject = $this->objectStack->pop();
    }

    public function setCurrentMetadata(PropertyMetadata $metadata): void
    {
        $this->metadataStack->push($this->currentMetadata);
        $this->currentMetadata = $metadata;
    }

    /**
     * @return ClassMetadata|PropertyMetadata|null
     */
    public function getCurrentMetadata()
    {
        return $this->currentMetadata;
    }

    /**
     * @return ClassMetadata|PropertyMetadata|null
     */
    public function revertCurrentMetadata()
    {
        return $this->currentMetadata = $this->metadataStack->pop();
    }

    /**
     * {@inheritdoc}
     */
    public function getResult($data)
    {
        $this->navigator = null;

        return $data;
    }

    /**
     * Retrieves internalSubset even in bugfixed php versions
     */
    private function getDomDocumentTypeEntitySubset(string $data): string
    {
        $startPos = $endPos = stripos($data, '<!doctype');
        $braces = 0;
        do {
            $char = $data[$endPos++];
            if ('<' === $char) {
                ++$braces;
            }

            if ('>' === $char) {
                --$braces;
            }
        } while ($braces > 0);

        $internalSubset = substr($data, $startPos, $endPos - $startPos);
        $internalSubset = str_replace(["\n", "\r"], '', $internalSubset);
        $internalSubset = preg_replace('/\s{2,}/', ' ', $internalSubset);
        $internalSubset = str_replace(['[ <!', '> ]>'], ['[<!', '>]>'], $internalSubset);

        return $internalSubset;
    }

    /**
     * {@inheritdoc}
     */
    public function isNull($value): bool
    {
        if ($value instanceof \SimpleXMLElement) {
            // Workaround for https://bugs.php.net/bug.php?id=75168 and https://github.com/schmittjoh/serializer/issues/817
            // If the "name" is empty means that we are on an not-existent node and subsequent operations on the object will trigger the warning:
            // "Node no longer exists"
            if ('' === $value->getName()) {
                // @todo should be "true", but for collections needs a default collection value. maybe something for the 2.0
                return false;
            }

            $xsiAttributes = $value->attributes('http://www.w3.org/2001/XMLSchema-instance');
            if (
                isset($xsiAttributes['nil'])
                && ('true' === (string) $xsiAttributes['nil'] || '1' === (string) $xsiAttributes['nil'])
            ) {
                return true;
            }
        }

        return null === $value;
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer;

use JMS\Serializer\Exception\NonVisitableTypeException;
use JMS\Serializer\Exception\RuntimeException;
use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\Metadata\PropertyMetadata;
use JMS\Serializer\Visitor\DeserializationVisitorInterface;

use function is_float;
use function is_int;
use function is_string;

final class JsonDeserializationStrictVisitor extends AbstractVisitor implements DeserializationVisitorInterface
{
    /** @var JsonDeserializationVisitor */
    private $wrappedDeserializationVisitor;

    public function __construct(
        int $options = 0,
        int $depth = 512
    ) {
        $this->wrappedDeserializationVisitor = new JsonDeserializationVisitor($options, $depth);
    }

    public function setNavigator(GraphNavigatorInterface $navigator): void
    {
        parent::setNavigator($navigator);

        $this->wrappedDeserializationVisitor->setNavigator($navigator);
    }

    /**
     * {@inheritdoc}
     */
    public function visitNull($data, array $type)
    {
        return null;
    }

    /**
     * {@inheritdoc}
     */
    public function visitString($data, array $type): ?string
    {
        if (null === $data) {
            return null;
        }

        if (! is_string($data)) {
            throw NonVisitableTypeException::fromDataAndType($data, $type);
        }

        return $data;
    }

    /**
     * {@inheritdoc}
     */
    public function visitBoolean($data, array $type): ?bool
    {
        if (null === $data) {
            return null;
        }

        if (! is_bool($data)) {
            throw NonVisitableTypeException::fromDataAndType($data, $type);
        }

        return $data;
    }

    /**
     * {@inheritdoc}
     */
    public function visitInteger($data, array $type): ?int
    {
        if (null === $data) {
            return null;
        }

        if (! is_int($data)) {
            throw NonVisitableTypeException::fromDataAndType($data, $type);
        }

        return $data;
    }

    /**
     * {@inheritdoc}
     */
    public function visitDouble($data, array $type): ?float
    {
        if (null === $data) {
            return null;
        }

        if (! is_float($data)) {
            throw NonVisitableTypeException::fromDataAndType($data, $type);
        }

        return $data;
    }

    /**
     * {@inheritdoc}
     */
    public function visitArray($data, array $type): array
    {
        try {
            return $this->wrappedDeserializationVisitor->visitArray($data, $type);
        } catch (RuntimeException $e) {
            throw NonVisitableTypeException::fromDataAndType($data, $type, $e);
        }
    }

    /**
     * {@inheritdoc}
     */
    public function visitDiscriminatorMapProperty($data, ClassMetadata $metadata): string
    {
        return $this->wrappedDeserializationVisitor->visitDiscriminatorMapProperty($data, $metadata);
    }

    /**
     * {@inheritdoc}
     */
    public function startVisitingObject(ClassMetadata $metadata, object $object, array $type): void
    {
        $this->wrappedDeserializationVisitor->startVisitingObject($metadata, $object, $type);
    }

    /**
     * {@inheritdoc}
     */
    public function visitProperty(PropertyMetadata $metadata, $data)
    {
        return $this->wrappedDeserializationVisitor->visitProperty($metadata, $data);
    }

    /**
     * {@inheritdoc}
     */
    public function endVisitingObject(ClassMetadata $metadata, $data, array $type): object
    {
        return $this->wrappedDeserializationVisitor->endVisitingObject($metadata, $data, $type);
    }

    /**
     * {@inheritdoc}
     */
    public function getResult($data)
    {
        return $this->wrappedDeserializationVisitor->getResult($data);
    }

    public function getCurrentObject(): ?object
    {
        return $this->wrappedDeserializationVisitor->getCurrentObject();
    }

    /**
     * {@inheritdoc}
     */
    public function prepare($data)
    {
        return $this->wrappedDeserializationVisitor->prepare($data);
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Metadata;

use JMS\Serializer\Exception\InvalidMetadataException;
use JMS\Serializer\Expression\Expression;
use JMS\Serializer\Type\Type;
use Metadata\PropertyMetadata as BasePropertyMetadata;

/**
 * @phpstan-import-type TypeArray from Type
 */
class PropertyMetadata extends BasePropertyMetadata
{
    public const ACCESS_TYPE_PROPERTY = 'property';
    public const ACCESS_TYPE_PUBLIC_METHOD = 'public_method';

    /**
     * @var string|null
     */
    public $sinceVersion;

    /**
     * @var string|null
     */
    public $untilVersion;

    /**
     * @var string[]|null
     */
    public $groups;

    /**
     * @var string|null
     */
    public $serializedName;

    /**
     * @var TypeArray|null
     */
    public $type;

    /**
     * @var bool
     */
    public $xmlCollection = false;

    /**
     * @var bool
     */
    public $xmlCollectionInline = false;

    /**
     * @var bool
     */
    public $xmlCollectionSkipWhenEmpty = true;

    /**
     * @var string|null
     */
    public $xmlEntryName;

    /**
     * @var string|null
     */
    public $xmlEntryNamespace;

    /**
     * @var string|null
     */
    public $xmlKeyAttribute;

    /**
     * @var bool
     */
    public $xmlAttribute = false;

    /**
     * @var bool
     */
    public $xmlValue = false;

    /**
     * @var string|null
     */
    public $xmlNamespace;

    /**
     * @var bool
     */
    public $xmlKeyValuePairs = false;

    /**
     * @var bool
     */
    public $xmlElementCData = true;

    /**
     * @var string|null
     */
    public $getter;

    /**
     * @var string|null
     */
    public $setter;

    /**
     * @var bool
     */
    public $inline = false;

    /**
     * @var bool
     */
    public $skipWhenEmpty = false;

    /**
     * @var bool
     */
    public $readOnly = false;

    /**
     * @var bool
     */
    public $xmlAttributeMap = false;

    /**
     * @var int|null
     */
    public $maxDepth = null;

    /**
     * @var string|Expression|null
     */
    public $excludeIf = null;

    /**
     * @var bool|null
     */
    public $hasDefault;

    /**
     * @var mixed|null
     */
    public $defaultValue;

    /**
     * @internal
     *
     * @var bool
     */
    public $forceReflectionAccess = false;

    public function __construct(string $class, string $name)
    {
        parent::__construct($class, $name);

        try {
            $class = $this->getReflection()->getDeclaringClass();
            $this->forceReflectionAccess = $class->isInternal() || $class->getProperty($name)->isStatic();
        } catch (\ReflectionException $e) {
        }
    }

    private function getReflection(): \ReflectionProperty
    {
        return new \ReflectionProperty($this->class, $this->name);
    }

    public function setAccessor(string $type, ?string $getter = null, ?string $setter = null): void
    {
        if (self::ACCESS_TYPE_PUBLIC_METHOD === $type) {
            $class = $this->getReflection()->getDeclaringClass();

            if (empty($getter)) {
                if ($class->hasMethod('get' . $this->name) && $class->getMethod('get' . $this->name)->isPublic()) {
                    $getter = 'get' . $this->name;
                } elseif ($class->hasMethod('is' . $this->name) && $class->getMethod('is' . $this->name)->isPublic()) {
                    $getter = 'is' . $this->name;
                } elseif ($class->hasMethod('has' . $this->name) && $class->getMethod('has' . $this->name)->isPublic()) {
                    $getter = 'has' . $this->name;
                } else {
                    throw new InvalidMetadataException(sprintf('There is neither a public %s method, nor a public %s method, nor a public %s method in class %s. Please specify which public method should be used for retrieving the value of the property %s.', 'get' . ucfirst($this->name), 'is' . ucfirst($this->name), 'has' . ucfirst($this->name), $this->class, $this->name));
                }
            }

            if (empty($setter) && !$this->readOnly) {
                if ($class->hasMethod('set' . $this->name) && $class->getMethod('set' . $this->name)->isPublic()) {
                    $setter = 'set' . $this->name;
                } else {
                    throw new InvalidMetadataException(sprintf('There is no public %s method in class %s. Please specify which public method should be used for setting the value of the property %s.', 'set' . ucfirst($this->name), $this->class, $this->name));
                }
            }
        }

        $this->getter = $getter;
        $this->setter = $setter;
    }

    /**
     * @param TypeArray $type
     */
    public function setType(array $type): void
    {
        $this->type = $type;
    }

    /**
     * @param TypeArray|null $type
     */
    public static function isCollectionList(?array $type = null): bool
    {
        return is_array($type)
            && 'array' === $type['name']
            && isset($type['params'][0])
            && !isset($type['params'][1]);
    }

    /**
     * @param TypeArray|null $type
     */
    public static function isCollectionMap(?array $type = null): bool
    {
        return is_array($type)
            && 'array' === $type['name']
            && isset($type['params'][0])
            && isset($type['params'][1]);
    }

    protected function serializeToArray(): array
    {
        return [
            $this->sinceVersion,
            $this->untilVersion,
            $this->groups,
            $this->serializedName,
            $this->type,
            $this->xmlCollection,
            $this->xmlCollectionInline,
            $this->xmlEntryName,
            $this->xmlKeyAttribute,
            $this->xmlAttribute,
            $this->xmlValue,
            $this->xmlNamespace,
            $this->xmlKeyValuePairs,
            $this->xmlElementCData,
            $this->getter,
            $this->setter,
            $this->inline,
            $this->readOnly,
            $this->xmlAttributeMap,
            $this->maxDepth,
            $this->xmlEntryNamespace,
            $this->xmlCollectionSkipWhenEmpty,
            $this->excludeIf,
            $this->skipWhenEmpty,
            $this->forceReflectionAccess,
            $this->hasDefault,
            $this->defaultValue,
            parent::serializeToArray(),
        ];
    }

    protected function unserializeFromArray(array $data): void
    {
        [
            $this->sinceVersion,
            $this->untilVersion,
            $this->groups,
            $this->serializedName,
            $this->type,
            $this->xmlCollection,
            $this->xmlCollectionInline,
            $this->xmlEntryName,
            $this->xmlKeyAttribute,
            $this->xmlAttribute,
            $this->xmlValue,
            $this->xmlNamespace,
            $this->xmlKeyValuePairs,
            $this->xmlElementCData,
            $this->getter,
            $this->setter,
            $this->inline,
            $this->readOnly,
            $this->xmlAttributeMap,
            $this->maxDepth,
            $this->xmlEntryNamespace,
            $this->xmlCollectionSkipWhenEmpty,
            $this->excludeIf,
            $this->skipWhenEmpty,
            $this->forceReflectionAccess,
            $this->hasDefault,
            $this->defaultValue,
            $parentData,
        ] = $data;

        parent::unserializeFromArray($parentData);
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Metadata;

class VirtualPropertyMetadata extends PropertyMetadata
{
    public function __construct(string $class, string $methodName)
    {
        if (0 === strpos($methodName, 'get')) {
            $fieldName = lcfirst(substr($methodName, 3));
        } else {
            $fieldName = $methodName;
        }

        $this->class = $class;
        $this->name = $fieldName;
        $this->getter = $methodName;
        $this->readOnly = true;
    }

    public function setAccessor(string $type, ?string $getter = null, ?string $setter = null): void
    {
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Metadata;

use JMS\Serializer\Exception\InvalidMetadataException;
use JMS\Serializer\Expression\Expression;
use JMS\Serializer\Ordering\AlphabeticalPropertyOrderingStrategy;
use JMS\Serializer\Ordering\CustomPropertyOrderingStrategy;
use JMS\Serializer\Ordering\IdenticalPropertyOrderingStrategy;
use Metadata\MergeableClassMetadata;
use Metadata\MergeableInterface;
use Metadata\MethodMetadata;
use Metadata\PropertyMetadata as BasePropertyMetadata;

/**
 * Class Metadata used to customize the serialization process.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 *
 * @property PropertyMetadata[] $propertyMetadata
 */
class ClassMetadata extends MergeableClassMetadata
{
    public const ACCESSOR_ORDER_UNDEFINED = 'undefined';
    public const ACCESSOR_ORDER_ALPHABETICAL = 'alphabetical';
    public const ACCESSOR_ORDER_CUSTOM = 'custom';

    /** @var \ReflectionMethod[] */
    public $preSerializeMethods = [];

    /** @var \ReflectionMethod[] */
    public $postSerializeMethods = [];

    /** @var \ReflectionMethod[] */
    public $postDeserializeMethods = [];

    /**
     * @var string
     */
    public $xmlRootName;

    /**
     * @var string
     */
    public $xmlRootNamespace;

    /**
     * @var string
     */
    public $xmlRootPrefix;
    /**
     * @var string[]
     */
    public $xmlNamespaces = [];

    /**
     * @var string
     */
    public $accessorOrder;

    /**
     * @var string[]
     */
    public $customOrder;

    /**
     * @internal
     *
     * @var bool
     */
    public $usingExpression = false;

    /**
     * @internal
     *
     * @var bool
     */
    public $isList = false;

    /**
     * @internal
     *
     * @var bool
     */
    public $isMap = false;

    /**
     * @var bool
     */
    public $discriminatorDisabled = false;

    /**
     * @var string
     */
    public $discriminatorBaseClass;
    /**
     * @var string
     */
    public $discriminatorFieldName;
    /**
     * @var string
     */
    public $discriminatorValue;

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

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

    /**
     * @var bool
     */
    public $xmlDiscriminatorAttribute = false;

    /**
     * @var bool
     */
    public $xmlDiscriminatorCData = true;

    /**
     * @var string
     */
    public $xmlDiscriminatorNamespace;

    /**
     * @var string|Expression
     */
    public $excludeIf;

    public function setDiscriminator(string $fieldName, array $map, array $groups = []): void
    {
        if (empty($fieldName)) {
            throw new InvalidMetadataException('The $fieldName cannot be empty.');
        }

        if (empty($map)) {
            throw new InvalidMetadataException('The discriminator map cannot be empty.');
        }

        $this->discriminatorBaseClass = $this->name;
        $this->discriminatorFieldName = $fieldName;
        $this->discriminatorMap = $map;
        $this->discriminatorGroups = $groups;

        $this->handleDiscriminatorProperty();
    }

    private function getReflection(): \ReflectionClass
    {
        return new \ReflectionClass($this->name);
    }

    /**
     * Sets the order of properties in the class.
     *
     * @param array $customOrder
     *
     * @throws InvalidMetadataException When the accessor order is not valid.
     * @throws InvalidMetadataException When the custom order is not valid.
     */
    public function setAccessorOrder(string $order, array $customOrder = []): void
    {
        if (!in_array($order, [self::ACCESSOR_ORDER_UNDEFINED, self::ACCESSOR_ORDER_ALPHABETICAL, self::ACCESSOR_ORDER_CUSTOM], true)) {
            throw new InvalidMetadataException(sprintf('The accessor order "%s" is invalid.', $order));
        }

        foreach ($customOrder as $name) {
            if (!\is_string($name)) {
                throw new InvalidMetadataException(sprintf('$customOrder is expected to be a list of strings, but got element of value %s.', json_encode($name)));
            }
        }

        $this->accessorOrder = $order;
        $this->customOrder = array_flip($customOrder);
        $this->sortProperties();
    }

    public function addPropertyMetadata(BasePropertyMetadata $metadata): void
    {
        parent::addPropertyMetadata($metadata);

        $this->sortProperties();
        if ($metadata instanceof PropertyMetadata && $metadata->excludeIf) {
            $this->usingExpression = true;
        }
    }

    public function addPreSerializeMethod(MethodMetadata $method): void
    {
        $this->preSerializeMethods[] = $method;
    }

    public function addPostSerializeMethod(MethodMetadata $method): void
    {
        $this->postSerializeMethods[] = $method;
    }

    public function addPostDeserializeMethod(MethodMetadata $method): void
    {
        $this->postDeserializeMethods[] = $method;
    }

    public function merge(MergeableInterface $object): void
    {
        if (!$object instanceof ClassMetadata) {
            throw new InvalidMetadataException('$object must be an instance of ClassMetadata.');
        }

        parent::merge($object);

        $this->preSerializeMethods = array_merge($this->preSerializeMethods, $object->preSerializeMethods);
        $this->postSerializeMethods = array_merge($this->postSerializeMethods, $object->postSerializeMethods);
        $this->postDeserializeMethods = array_merge($this->postDeserializeMethods, $object->postDeserializeMethods);
        $this->xmlRootName = $object->xmlRootName;
        $this->xmlRootPrefix = $object->xmlRootPrefix;
        $this->xmlRootNamespace = $object->xmlRootNamespace;
        if (null !== $object->excludeIf) {
            $this->excludeIf = $object->excludeIf;
        }

        $this->isMap = $object->isMap;
        $this->isList = $object->isList;

        $this->xmlNamespaces = array_merge($this->xmlNamespaces, $object->xmlNamespaces);

        if ($object->accessorOrder) {
            $this->accessorOrder = $object->accessorOrder;
            $this->customOrder = $object->customOrder;
        }

        if ($object->discriminatorFieldName && $this->discriminatorFieldName && $object->discriminatorFieldName !== $this->discriminatorFieldName) {
            throw new InvalidMetadataException(sprintf(
                'The discriminator of class "%s" would overwrite the discriminator of the parent class "%s". Please define all possible sub-classes in the discriminator of %s.',
                $object->name,
                $this->discriminatorBaseClass,
                $this->discriminatorBaseClass,
            ));
        } elseif (!$this->discriminatorFieldName && $object->discriminatorFieldName) {
            $this->discriminatorFieldName = $object->discriminatorFieldName;
            $this->discriminatorMap = $object->discriminatorMap;
        }

        if (null !== $object->discriminatorDisabled) {
            $this->discriminatorDisabled = $object->discriminatorDisabled;
        }

        if ($object->discriminatorMap) {
            $this->discriminatorFieldName = $object->discriminatorFieldName;
            $this->discriminatorMap = $object->discriminatorMap;
            $this->discriminatorBaseClass = $object->discriminatorBaseClass;
            $this->discriminatorGroups = $object->discriminatorGroups;

            $this->xmlDiscriminatorCData = $object->xmlDiscriminatorCData;
            $this->xmlDiscriminatorAttribute = $object->xmlDiscriminatorAttribute;
            $this->xmlDiscriminatorNamespace = $object->xmlDiscriminatorNamespace;
        }

        $this->handleDiscriminatorProperty();

        $this->sortProperties();
    }

    public function registerNamespace(string $uri, ?string $prefix = null): void
    {
        if (!\is_string($uri)) {
            throw new InvalidMetadataException(sprintf('$uri is expected to be a strings, but got value %s.', json_encode($uri)));
        }

        if (null !== $prefix) {
            if (!\is_string($prefix)) {
                throw new InvalidMetadataException(sprintf('$prefix is expected to be a strings, but got value %s.', json_encode($prefix)));
            }
        } else {
            $prefix = '';
        }

        $this->xmlNamespaces[$prefix] = $uri;
    }

    protected function serializeToArray(): array
    {
        $this->sortProperties();

        return [
            $this->preSerializeMethods,
            $this->postSerializeMethods,
            $this->postDeserializeMethods,
            $this->xmlRootName,
            $this->xmlRootNamespace,
            $this->xmlNamespaces,
            $this->accessorOrder,
            $this->customOrder,
            $this->discriminatorDisabled,
            $this->discriminatorBaseClass,
            $this->discriminatorFieldName,
            $this->discriminatorValue,
            $this->discriminatorMap,
            $this->discriminatorGroups,
            $this->excludeIf,
            $this->discriminatorGroups,
            $this->xmlDiscriminatorAttribute,
            $this->xmlDiscriminatorCData,
            $this->usingExpression,
            $this->xmlDiscriminatorNamespace,
            $this->xmlRootPrefix,
            $this->isList,
            $this->isMap,
            parent::serializeToArray(),
        ];
    }

    protected function unserializeFromArray(array $data): void
    {
        [
            $this->preSerializeMethods,
            $this->postSerializeMethods,
            $this->postDeserializeMethods,
            $this->xmlRootName,
            $this->xmlRootNamespace,
            $this->xmlNamespaces,
            $this->accessorOrder,
            $this->customOrder,
            $this->discriminatorDisabled,
            $this->discriminatorBaseClass,
            $this->discriminatorFieldName,
            $this->discriminatorValue,
            $this->discriminatorMap,
            $this->discriminatorGroups,
            $this->excludeIf,
            $this->discriminatorGroups,
            $this->xmlDiscriminatorAttribute,
            $this->xmlDiscriminatorCData,
            $this->usingExpression,
            $this->xmlDiscriminatorNamespace,
            $this->xmlRootPrefix,
            $this->isList,
            $this->isMap,
            $parentData,
        ] = $data;

        parent::unserializeFromArray($parentData);
    }

    private function handleDiscriminatorProperty(): void
    {
        if (
            $this->discriminatorMap
            && !$this->getReflection()->isAbstract()
            && !$this->getReflection()->isInterface()
        ) {
            if (false === $typeValue = array_search($this->name, $this->discriminatorMap, true)) {
                throw new InvalidMetadataException(sprintf(
                    'The sub-class "%s" is not listed in the discriminator of the base class "%s".',
                    $this->name,
                    $this->discriminatorBaseClass,
                ));
            }

            $this->discriminatorValue = $typeValue;

            if (
                isset($this->propertyMetadata[$this->discriminatorFieldName])
                && !$this->propertyMetadata[$this->discriminatorFieldName] instanceof StaticPropertyMetadata
            ) {
                throw new InvalidMetadataException(sprintf(
                    'The discriminator field name "%s" of the base-class "%s" conflicts with a regular property of the sub-class "%s".',
                    $this->discriminatorFieldName,
                    $this->discriminatorBaseClass,
                    $this->name,
                ));
            }

            $discriminatorProperty = new StaticPropertyMetadata(
                $this->name,
                $this->discriminatorFieldName,
                $typeValue,
                $this->discriminatorGroups,
            );
            $discriminatorProperty->serializedName = $this->discriminatorFieldName;
            $discriminatorProperty->xmlAttribute = $this->xmlDiscriminatorAttribute;
            $discriminatorProperty->xmlElementCData = $this->xmlDiscriminatorCData;
            $discriminatorProperty->xmlNamespace = $this->xmlDiscriminatorNamespace;
            $this->propertyMetadata[$this->discriminatorFieldName] = $discriminatorProperty;
        }
    }

    private function sortProperties(): void
    {
        switch ($this->accessorOrder) {
            case self::ACCESSOR_ORDER_UNDEFINED:
                $this->propertyMetadata = (new IdenticalPropertyOrderingStrategy())->order($this->propertyMetadata);
                break;

            case self::ACCESSOR_ORDER_ALPHABETICAL:
                $this->propertyMetadata = (new AlphabeticalPropertyOrderingStrategy())->order($this->propertyMetadata);
                break;

            case self::ACCESSOR_ORDER_CUSTOM:
                $this->propertyMetadata = (new CustomPropertyOrderingStrategy($this->customOrder))->order($this->propertyMetadata);
                break;
        }
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Metadata;

use JMS\Serializer\Expression\Expression;

/**
 * @Annotation
 * @Target("METHOD")
 *
 * @author Asmir Mustafic <goetas@gmail.com>
 */
#[\Attribute(\Attribute::TARGET_METHOD)]
class ExpressionPropertyMetadata extends PropertyMetadata
{
    /**
     * @var string|Expression
     */
    public $expression;

    /**
     * @param string|Expression $expression
     */
    public function __construct(string $class, string $fieldName, $expression)
    {
        $this->class = $class;
        $this->name = $fieldName;
        $this->expression = $expression;
        $this->readOnly = true;
    }

    public function setAccessor(string $type, ?string $getter = null, ?string $setter = null): void
    {
    }

    /**
     * {@inheritdoc}
     */
    protected function serializeToArray(): array
    {
        return [
            $this->expression,
            parent::serializeToArray(),
        ];
    }

    protected function unserializeFromArray(array $data): void
    {
        [
            $this->expression,
            $parentData,
        ] = $data;

        parent::unserializeFromArray($parentData);
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Metadata\Driver;

use JMS\Serializer\Metadata\ClassMetadata as SerializerClassMetadata;
use JMS\Serializer\Metadata\Driver\DocBlockDriver\DocBlockTypeResolver;
use JMS\Serializer\Metadata\ExpressionPropertyMetadata;
use JMS\Serializer\Metadata\PropertyMetadata;
use JMS\Serializer\Metadata\StaticPropertyMetadata;
use JMS\Serializer\Metadata\VirtualPropertyMetadata;
use JMS\Serializer\Type\Parser;
use JMS\Serializer\Type\ParserInterface;
use Metadata\ClassMetadata;
use Metadata\Driver\DriverInterface;
use ReflectionClass;
use ReflectionException;
use ReflectionMethod;
use ReflectionProperty;

class DocBlockDriver implements DriverInterface
{
    /**
     * @var DriverInterface
     */
    protected $delegate;

    /**
     * @var ParserInterface
     */
    protected $typeParser;
    /**
     * @var DocBlockTypeResolver
     */
    private $docBlockTypeResolver;

    public function __construct(DriverInterface $delegate, ?ParserInterface $typeParser = null)
    {
        $this->delegate = $delegate;
        $this->typeParser = $typeParser ?: new Parser();
        $this->docBlockTypeResolver = new DocBlockTypeResolver();
    }

    /**
     * @return SerializerClassMetadata|null
     */
    public function loadMetadataForClass(ReflectionClass $class): ?ClassMetadata
    {
        $classMetadata = $this->delegate->loadMetadataForClass($class);

        if (null === $classMetadata) {
            return null;
        }

        \assert($classMetadata instanceof SerializerClassMetadata);

        // We base our scan on the internal driver's property list so that we
        // respect any internal allow/blocklist like in the AnnotationDriver
        foreach ($classMetadata->propertyMetadata as $key => $propertyMetadata) {
            // If the inner driver provides a type, don't guess anymore.
            if ($propertyMetadata->type) {
                continue;
            }

            if ($this->isNotSupportedVirtualProperty($propertyMetadata)) {
                continue;
            }

            try {
                if ($propertyMetadata instanceof VirtualPropertyMetadata) {
                    $type = $this->docBlockTypeResolver->getMethodDocblockTypeHint(
                        new ReflectionMethod($propertyMetadata->class, $propertyMetadata->getter),
                    );
                } else {
                    $type = $this->docBlockTypeResolver->getPropertyDocblockTypeHint(
                        new ReflectionProperty($propertyMetadata->class, $propertyMetadata->name),
                    );
                }

                if ($type) {
                    $propertyMetadata->setType($this->typeParser->parse($type));
                }
            } catch (ReflectionException $e) {
                continue;
            }
        }

        return $classMetadata;
    }

    private function isNotSupportedVirtualProperty(PropertyMetadata $propertyMetadata): bool
    {
        return $propertyMetadata instanceof StaticPropertyMetadata
            || $propertyMetadata instanceof ExpressionPropertyMetadata;
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Metadata\Driver;

use Doctrine\Common\Annotations\Reader;
use JMS\Serializer\Annotation\Accessor;
use JMS\Serializer\Annotation\AccessorOrder;
use JMS\Serializer\Annotation\AccessType;
use JMS\Serializer\Annotation\Discriminator;
use JMS\Serializer\Annotation\Exclude;
use JMS\Serializer\Annotation\ExclusionPolicy;
use JMS\Serializer\Annotation\Expose;
use JMS\Serializer\Annotation\Groups;
use JMS\Serializer\Annotation\Inline;
use JMS\Serializer\Annotation\MaxDepth;
use JMS\Serializer\Annotation\PostDeserialize;
use JMS\Serializer\Annotation\PostSerialize;
use JMS\Serializer\Annotation\PreSerialize;
use JMS\Serializer\Annotation\ReadOnlyProperty;
use JMS\Serializer\Annotation\SerializedName;
use JMS\Serializer\Annotation\SerializerAttribute;
use JMS\Serializer\Annotation\Since;
use JMS\Serializer\Annotation\SkipWhenEmpty;
use JMS\Serializer\Annotation\Type;
use JMS\Serializer\Annotation\UnionDiscriminator;
use JMS\Serializer\Annotation\Until;
use JMS\Serializer\Annotation\VirtualProperty;
use JMS\Serializer\Annotation\XmlAttribute;
use JMS\Serializer\Annotation\XmlAttributeMap;
use JMS\Serializer\Annotation\XmlDiscriminator;
use JMS\Serializer\Annotation\XmlElement;
use JMS\Serializer\Annotation\XmlKeyValuePairs;
use JMS\Serializer\Annotation\XmlList;
use JMS\Serializer\Annotation\XmlMap;
use JMS\Serializer\Annotation\XmlNamespace;
use JMS\Serializer\Annotation\XmlRoot;
use JMS\Serializer\Annotation\XmlValue;
use JMS\Serializer\Exception\InvalidMetadataException;
use JMS\Serializer\Expression\CompilableExpressionEvaluatorInterface;
use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\Metadata\ExpressionPropertyMetadata;
use JMS\Serializer\Metadata\PropertyMetadata;
use JMS\Serializer\Metadata\VirtualPropertyMetadata;
use JMS\Serializer\Naming\PropertyNamingStrategyInterface;
use JMS\Serializer\Type\Parser;
use JMS\Serializer\Type\ParserInterface;
use Metadata\ClassMetadata as BaseClassMetadata;
use Metadata\Driver\DriverInterface;
use Metadata\MethodMetadata;

class AnnotationOrAttributeDriver implements DriverInterface
{
    use ExpressionMetadataTrait;

    /**
     * @var ParserInterface
     */
    private $typeParser;

    /**
     * @var PropertyNamingStrategyInterface
     */
    private $namingStrategy;

    /**
     * @var Reader
     */
    private $reader;

    public function __construct(PropertyNamingStrategyInterface $namingStrategy, ?ParserInterface $typeParser = null, ?CompilableExpressionEvaluatorInterface $expressionEvaluator = null, ?Reader $reader = null)
    {
        $this->typeParser = $typeParser ?: new Parser();
        $this->namingStrategy = $namingStrategy;
        $this->expressionEvaluator = $expressionEvaluator;
        $this->reader = $reader;
    }

    public function loadMetadataForClass(\ReflectionClass $class): ?BaseClassMetadata
    {
        $configured = false;

        $classMetadata = new ClassMetadata($name = $class->name);
        $fileResource =  $class->getFilename();

        if (false !== $fileResource) {
            $classMetadata->fileResources[] = $fileResource;
        }

        $propertiesMetadata = [];
        $propertiesAnnotations = [];

        $exclusionPolicy = ExclusionPolicy::NONE;
        $excludeAll = false;
        $classAccessType = PropertyMetadata::ACCESS_TYPE_PROPERTY;
        $readOnlyClass = false;

        foreach ($this->getClassAnnotations($class) as $annot) {
            $configured = true;

            if ($annot instanceof ExclusionPolicy) {
                $exclusionPolicy = $annot->policy;
            } elseif ($annot instanceof XmlRoot) {
                $classMetadata->xmlRootName = $annot->name;
                $classMetadata->xmlRootNamespace = $annot->namespace;
                $classMetadata->xmlRootPrefix = $annot->prefix;
            } elseif ($annot instanceof XmlNamespace) {
                $classMetadata->registerNamespace($annot->uri, $annot->prefix);
            } elseif ($annot instanceof Exclude) {
                if (null !== $annot->if) {
                    $classMetadata->excludeIf = $this->parseExpression($annot->if);
                } else {
                    $excludeAll = true;
                }
            } elseif ($annot instanceof AccessType) {
                $classAccessType = $annot->type;
            } elseif ($annot instanceof ReadOnlyProperty) {
                $readOnlyClass = true;
            } elseif ($annot instanceof AccessorOrder) {
                $classMetadata->setAccessorOrder($annot->order, $annot->custom);
            } elseif ($annot instanceof Discriminator) {
                if ($annot->disabled) {
                    $classMetadata->discriminatorDisabled = true;
                } else {
                    $classMetadata->setDiscriminator($annot->field, $annot->map, $annot->groups);
                }
            } elseif ($annot instanceof XmlDiscriminator) {
                $classMetadata->xmlDiscriminatorAttribute = (bool) $annot->attribute;
                $classMetadata->xmlDiscriminatorCData = (bool) $annot->cdata;
                $classMetadata->xmlDiscriminatorNamespace = $annot->namespace ? (string) $annot->namespace : null;
            } elseif ($annot instanceof VirtualProperty) {
                $virtualPropertyMetadata = new ExpressionPropertyMetadata(
                    $name,
                    $annot->name,
                    $this->parseExpression($annot->exp),
                );
                $propertiesMetadata[] = $virtualPropertyMetadata;
                $propertiesAnnotations[] = $annot->options;
            }
        }

        foreach ($class->getMethods() as $method) {
            if ($method->class !== $name) {
                continue;
            }

            $methodAnnotations = $this->getMethodAnnotations($method);

            foreach ($methodAnnotations as $annot) {
                $configured = true;

                if ($annot instanceof PreSerialize) {
                    $classMetadata->addPreSerializeMethod(new MethodMetadata($name, $method->name));
                    continue 2;
                } elseif ($annot instanceof PostDeserialize) {
                    $classMetadata->addPostDeserializeMethod(new MethodMetadata($name, $method->name));
                    continue 2;
                } elseif ($annot instanceof PostSerialize) {
                    $classMetadata->addPostSerializeMethod(new MethodMetadata($name, $method->name));
                    continue 2;
                } elseif ($annot instanceof VirtualProperty) {
                    $virtualPropertyMetadata = new VirtualPropertyMetadata($name, $method->name);
                    $propertiesMetadata[] = $virtualPropertyMetadata;
                    $propertiesAnnotations[] = $methodAnnotations;
                    continue 2;
                }
            }
        }

        if (!$excludeAll) {
            foreach ($class->getProperties() as $property) {
                if ($property->class !== $name || (isset($property->info) && $property->info['class'] !== $name)) {
                    continue;
                }

                $propertiesMetadata[] = new PropertyMetadata($name, $property->getName());
                $propertiesAnnotations[] = $this->getPropertyAnnotations($property);
            }

            foreach ($propertiesMetadata as $propertyKey => $propertyMetadata) {
                $isExclude = false;
                $isExpose = $propertyMetadata instanceof VirtualPropertyMetadata
                    || $propertyMetadata instanceof ExpressionPropertyMetadata;
                $propertyMetadata->readOnly = $propertyMetadata->readOnly || $readOnlyClass;
                $accessType = $classAccessType;
                $accessor = [null, null];

                $propertyAnnotations = $propertiesAnnotations[$propertyKey];

                foreach ($propertyAnnotations as $annot) {
                    $configured = true;

                    if ($annot instanceof Since) {
                        $propertyMetadata->sinceVersion = $annot->version;
                    } elseif ($annot instanceof Until) {
                        $propertyMetadata->untilVersion = $annot->version;
                    } elseif ($annot instanceof SerializedName) {
                        $propertyMetadata->serializedName = $annot->name;
                    } elseif ($annot instanceof SkipWhenEmpty) {
                        $propertyMetadata->skipWhenEmpty = true;
                    } elseif ($annot instanceof Expose) {
                        $isExpose = true;
                        if (null !== $annot->if) {
                            $propertyMetadata->excludeIf = $this->parseExpression('!(' . $annot->if . ')');
                        }
                    } elseif ($annot instanceof Exclude) {
                        if (null !== $annot->if) {
                            $propertyMetadata->excludeIf = $this->parseExpression($annot->if);
                        } else {
                            $isExclude = true;
                        }
                    } elseif ($annot instanceof Type) {
                        $propertyMetadata->setType($this->typeParser->parse($annot->name));
                    } elseif ($annot instanceof XmlElement) {
                        $propertyMetadata->xmlAttribute = false;
                        $propertyMetadata->xmlElementCData = $annot->cdata;
                        $propertyMetadata->xmlNamespace = $annot->namespace;
                    } elseif ($annot instanceof XmlList) {
                        $propertyMetadata->xmlCollection = true;
                        $propertyMetadata->xmlCollectionInline = $annot->inline;
                        $propertyMetadata->xmlEntryName = $annot->entry;
                        $propertyMetadata->xmlEntryNamespace = $annot->namespace;
                        $propertyMetadata->xmlCollectionSkipWhenEmpty = $annot->skipWhenEmpty;
                    } elseif ($annot instanceof XmlMap) {
                        $propertyMetadata->xmlCollection = true;
                        $propertyMetadata->xmlCollectionInline = $annot->inline;
                        $propertyMetadata->xmlEntryName = $annot->entry;
                        $propertyMetadata->xmlEntryNamespace = $annot->namespace;
                        $propertyMetadata->xmlKeyAttribute = $annot->keyAttribute;
                    } elseif ($annot instanceof XmlKeyValuePairs) {
                        $propertyMetadata->xmlKeyValuePairs = true;
                    } elseif ($annot instanceof XmlAttribute) {
                        $propertyMetadata->xmlAttribute = true;
                        $propertyMetadata->xmlNamespace = $annot->namespace;
                    } elseif ($annot instanceof XmlValue) {
                        $propertyMetadata->xmlValue = true;
                        $propertyMetadata->xmlElementCData = $annot->cdata;
                    } elseif ($annot instanceof AccessType) {
                        $accessType = $annot->type;
                    } elseif ($annot instanceof ReadOnlyProperty) {
                        $propertyMetadata->readOnly = $annot->readOnly;
                    } elseif ($annot instanceof Accessor) {
                        $accessor = [$annot->getter, $annot->setter];
                    } elseif ($annot instanceof Groups) {
                        $propertyMetadata->groups = $annot->groups;
                        foreach ((array) $propertyMetadata->groups as $groupName) {
                            if (false !== strpos($groupName, ',')) {
                                throw new InvalidMetadataException(sprintf(
                                    'Invalid group name "%s" on "%s", did you mean to create multiple groups?',
                                    implode(', ', $propertyMetadata->groups),
                                    $propertyMetadata->class . '->' . $propertyMetadata->name,
                                ));
                            }
                        }
                    } elseif ($annot instanceof Inline) {
                        $propertyMetadata->inline = true;
                    } elseif ($annot instanceof XmlAttributeMap) {
                        $propertyMetadata->xmlAttributeMap = true;
                    } elseif ($annot instanceof MaxDepth) {
                        $propertyMetadata->maxDepth = $annot->depth;
                    } elseif ($annot instanceof UnionDiscriminator) {
                        $propertyMetadata->setType([
                            'name' => 'union',
                            'params' => [null, $annot->field, $annot->map],
                        ]);
                    }
                }

                if ($propertyMetadata->inline) {
                    $classMetadata->isList = $classMetadata->isList || PropertyMetadata::isCollectionList($propertyMetadata->type);
                    $classMetadata->isMap = $classMetadata->isMap || PropertyMetadata::isCollectionMap($propertyMetadata->type);

                    if ($classMetadata->isMap && $classMetadata->isList) {
                        throw new InvalidMetadataException('Can not have an inline map and and inline map on the same class');
                    }
                }

                if (!$propertyMetadata->serializedName) {
                    $propertyMetadata->serializedName = $this->namingStrategy->translateName($propertyMetadata);
                }

                foreach ($propertyAnnotations as $annot) {
                    if ($annot instanceof VirtualProperty && null !== $annot->name) {
                        $propertyMetadata->name = $annot->name;
                    }
                }

                if (
                    (ExclusionPolicy::NONE === $exclusionPolicy && !$isExclude)
                    || (ExclusionPolicy::ALL === $exclusionPolicy && $isExpose)
                ) {
                    $propertyMetadata->setAccessor($accessType, $accessor[0], $accessor[1]);
                    $classMetadata->addPropertyMetadata($propertyMetadata);
                }
            }
        }

        // if (!$configured) {
            // return null;
            // uncomment the above line afetr a couple of months
        // }

        return $classMetadata;
    }

    /**
     * @return list<SerializerAttribute>
     */
    protected function getClassAnnotations(\ReflectionClass $class): array
    {
        $annotations = [];

        if (PHP_VERSION_ID >= 80000) {
            $annotations = array_map(
                static fn (\ReflectionAttribute $attribute): object => $attribute->newInstance(),
                $class->getAttributes(SerializerAttribute::class, \ReflectionAttribute::IS_INSTANCEOF),
            );
        }

        if (null !== $this->reader) {
            $annotations = array_merge($annotations, $this->reader->getClassAnnotations($class));
        }

        return $annotations;
    }

    /**
     * @return list<SerializerAttribute>
     */
    protected function getMethodAnnotations(\ReflectionMethod $method): array
    {
        $annotations = [];

        if (PHP_VERSION_ID >= 80000) {
            $annotations = array_map(
                static fn (\ReflectionAttribute $attribute): object => $attribute->newInstance(),
                $method->getAttributes(SerializerAttribute::class, \ReflectionAttribute::IS_INSTANCEOF),
            );
        }

        if (null !== $this->reader) {
            $annotations = array_merge($annotations, $this->reader->getMethodAnnotations($method));
        }

        return $annotations;
    }

    /**
     * @return list<SerializerAttribute>
     */
    protected function getPropertyAnnotations(\ReflectionProperty $property): array
    {
        $annotations = [];

        if (PHP_VERSION_ID >= 80000) {
            $annotations = array_map(
                static fn (\ReflectionAttribute $attribute): object => $attribute->newInstance(),
                $property->getAttributes(SerializerAttribute::class, \ReflectionAttribute::IS_INSTANCEOF),
            );
        }

        if (null !== $this->reader) {
            $annotations = array_merge($annotations, $this->reader->getPropertyAnnotations($property));
        }

        return $annotations;
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Metadata\Driver;

use JMS\Serializer\Metadata\ClassMetadata as SerializerClassMetadata;
use JMS\Serializer\Metadata\ExpressionPropertyMetadata;
use JMS\Serializer\Metadata\PropertyMetadata;
use JMS\Serializer\Metadata\StaticPropertyMetadata;
use JMS\Serializer\Metadata\VirtualPropertyMetadata;
use Metadata\ClassMetadata;
use Metadata\Driver\DriverInterface;
use ReflectionClass;
use ReflectionException;
use ReflectionProperty;

class EnumPropertiesDriver implements DriverInterface
{
    /**
     * @var DriverInterface
     */
    protected $delegate;

    public function __construct(DriverInterface $delegate)
    {
        $this->delegate = $delegate;
    }

    public function loadMetadataForClass(ReflectionClass $class): ?ClassMetadata
    {
        $classMetadata = $this->delegate->loadMetadataForClass($class);

        if (null === $classMetadata) {
            return null;
        }

        \assert($classMetadata instanceof SerializerClassMetadata);

        // We base our scan on the internal driver's property list so that we
        // respect any internal allow/blocklist like in the AnnotationDriver
        foreach ($classMetadata->propertyMetadata as $propertyMetadata) {
            // If the inner driver provides a type, don't guess anymore.
            if ($propertyMetadata->type || $this->isVirtualProperty($propertyMetadata)) {
                continue;
            }

            try {
                $propertyReflection = $this->getReflection($propertyMetadata);
                if ($enum = $this->getEnumReflection($propertyReflection)) {
                    $serializerType = [
                        'name' => 'enum',
                        'params' => [
                            ['name' => $enum->getName(), 'params' => []],
                            $enum->isBacked() ? 'value' : 'name',
                        ],
                    ];
                    $propertyMetadata->setType($serializerType);
                }
            } catch (ReflectionException $e) {
                continue;
            }
        }

        return $classMetadata;
    }

    private function isVirtualProperty(PropertyMetadata $propertyMetadata): bool
    {
        return $propertyMetadata instanceof VirtualPropertyMetadata
            || $propertyMetadata instanceof StaticPropertyMetadata
            || $propertyMetadata instanceof ExpressionPropertyMetadata;
    }

    private function getReflection(PropertyMetadata $propertyMetadata): ReflectionProperty
    {
        return new ReflectionProperty($propertyMetadata->class, $propertyMetadata->name);
    }

    private function getEnumReflection(ReflectionProperty $propertyReflection): ?\ReflectionEnum
    {
        $reflectionType = $propertyReflection->getType();

        if (!($reflectionType instanceof \ReflectionNamedType)) {
            return null;
        }

        return enum_exists($reflectionType->getName()) ? new \ReflectionEnum($reflectionType->getName()) : null;
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Metadata\Driver\AttributeDriver;

use Doctrine\Common\Annotations\Reader;
use JMS\Serializer\Annotation\SerializerAttribute;
use ReflectionClass;
use ReflectionMethod;
use ReflectionProperty;

/**
 * @deprecated use {@see \JMS\Serializer\Metadata\Driver\AttributeDriver} instead
 */
class AttributeReader implements Reader
{
    /**
     * @var Reader
     */
    private $reader;

    public function __construct(Reader $reader)
    {
        $this->reader = $reader;
    }

    public function getClassAnnotations(ReflectionClass $class): array
    {
        $attributes = $class->getAttributes(SerializerAttribute::class, \ReflectionAttribute::IS_INSTANCEOF);

        return array_merge($this->reader->getClassAnnotations($class), $this->buildAnnotations($attributes));
    }

    public function getClassAnnotation(ReflectionClass $class, $annotationName): ?object
    {
        $attributes = $class->getAttributes($annotationName, \ReflectionAttribute::IS_INSTANCEOF);

        return $this->reader->getClassAnnotation($class, $annotationName) ?? $this->buildAnnotation($attributes);
    }

    public function getMethodAnnotations(ReflectionMethod $method): array
    {
        $attributes = $method->getAttributes(SerializerAttribute::class, \ReflectionAttribute::IS_INSTANCEOF);

        return array_merge($this->reader->getMethodAnnotations($method), $this->buildAnnotations($attributes));
    }

    public function getMethodAnnotation(ReflectionMethod $method, $annotationName): ?object
    {
        $attributes = $method->getAttributes($annotationName, \ReflectionAttribute::IS_INSTANCEOF);

        return $this->reader->getClassAnnotation($method, $annotationName) ?? $this->buildAnnotation($attributes);
    }

    public function getPropertyAnnotations(ReflectionProperty $property): array
    {
        $attributes = $property->getAttributes(SerializerAttribute::class, \ReflectionAttribute::IS_INSTANCEOF);

        return array_merge($this->reader->getPropertyAnnotations($property), $this->buildAnnotations($attributes));
    }

    public function getPropertyAnnotation(ReflectionProperty $property, $annotationName): ?object
    {
        $attributes = $property->getAttributes($annotationName, \ReflectionAttribute::IS_INSTANCEOF);

        return $this->reader->getClassAnnotation($property, $annotationName) ?? $this->buildAnnotation($attributes);
    }

    /**
     * @param list<\ReflectionAttribute<SerializerAttribute>> $attributes
     */
    private function buildAnnotation(array $attributes): ?SerializerAttribute
    {
        if (!isset($attributes[0])) {
            return null;
        }

        return $attributes[0]->newInstance();
    }

    /**
     * @return list<SerializerAttribute>
     */
    private function buildAnnotations(array $attributes): array
    {
        return array_map(
            static fn (\ReflectionAttribute $attribute): object => $attribute->newInstance(),
            $attributes,
        );
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Metadata\Driver;

use JMS\Serializer\Exception\InvalidMetadataException;
use JMS\Serializer\Expression\CompilableExpressionEvaluatorInterface;
use JMS\Serializer\Expression\Expression;

trait ExpressionMetadataTrait
{
    /**
     * @var CompilableExpressionEvaluatorInterface
     */
    private $expressionEvaluator;

    /**
     * @return Expression|string
     *
     * @throws InvalidMetadataException
     */
    private function parseExpression(string $expression, array $names = [])
    {
        if (null === $this->expressionEvaluator) {
            return $expression;
        }

        try {
            return $this->expressionEvaluator->parse($expression, array_merge(['context', 'property_metadata', 'object'], $names));
        } catch (\LogicException $e) {
            throw new InvalidMetadataException(sprintf('Can not parse the expression "%s"', $expression), 0, $e);
        }
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Metadata\Driver\DocBlockDriver;

use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\TypeAliasImportTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\TypeAliasTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
use PHPStan\PhpDocParser\Lexer\Lexer;
use PHPStan\PhpDocParser\Parser\ConstExprParser;
use PHPStan\PhpDocParser\Parser\PhpDocParser;
use PHPStan\PhpDocParser\Parser\TokenIterator;
use PHPStan\PhpDocParser\Parser\TypeParser;
use PHPStan\PhpDocParser\ParserConfig;

/**
 * @internal
 */
final class DocBlockTypeResolver
{
    /** resolve single use statements */
    private const SINGLE_USE_STATEMENTS_REGEX = '/^[^\S\r\n]*use[\s]*([^;\n]*)[\s]*;$/m';

    /** resolve group use statements */
    private const GROUP_USE_STATEMENTS_REGEX = '/^[^\S\r\n]*use[[\s]*([^;\n]*)[\s]*{([a-zA-Z0-9\s\n\r,]*)};$/m';
    private const GLOBAL_NAMESPACE_PREFIX = '\\';
    private const PHPSTAN_ARRAY_SHAPE = '/^([^\s]*) array{.*/m';
    private const PHPSTAN_ARRAY_TYPE = '/^([^\s]*) array<(.*)>/m';

    /**
     * @var PhpDocParser
     */
    protected $phpDocParser;

    /**
     * @var Lexer
     */
    protected $lexer;

    public function __construct()
    {
        // PHPStan PHPDoc Parser 2
        if (class_exists(ParserConfig::class)) {
            $config = new ParserConfig(['lines' => true, 'indexes' => true]);

            $constExprParser = new ConstExprParser($config);
            $typeParser = new TypeParser($config, $constExprParser);

            $this->phpDocParser = new PhpDocParser($config, $typeParser, $constExprParser);
            $this->lexer = new Lexer($config);
        } else {
            // @phpstan-ignore arguments.count
            $constExprParser = new ConstExprParser();
            // @phpstan-ignore arguments.count
            $typeParser = new TypeParser($constExprParser);
            // @phpstan-ignore arguments.count
            $this->phpDocParser = new PhpDocParser($typeParser, $constExprParser);
            // @phpstan-ignore arguments.count
            $this->lexer = new Lexer();
        }
    }

    /**
     * Attempts to retrieve additional type information from a PhpDoc block. Throws in case of ambiguous type
     * information and will return null if no helpful type information could be retrieved.
     *
     * @param \ReflectionMethod $reflectionMethod
     *
     * @return string|null
     */
    public function getMethodDocblockTypeHint(\ReflectionMethod $reflectionMethod): ?string
    {
        return $this->getDocBlocTypeHint($reflectionMethod);
    }

    /**
     * Attempts to retrieve additional type information from a PhpDoc block. Throws in case of ambiguous type
     * information and will return null if no helpful type information could be retrieved.
     *
     * @param \ReflectionProperty $reflectionProperty
     *
     * @return string|null
     */
    public function getPropertyDocblockTypeHint(\ReflectionProperty $reflectionProperty): ?string
    {
        return $this->getDocBlocTypeHint($reflectionProperty);
    }

    /**
     * @param \ReflectionMethod|\ReflectionProperty $reflector
     *
     * @return string|null
     */
    private function getDocBlocTypeHint($reflector): ?string
    {
        $types = $this->resolveTypeFromDocblock($reflector);

        // The PhpDoc does not contain additional type information.
        if (0 === count($types)) {
            return null;
        }

        // The PhpDoc contains multiple non-null types which produces ambiguity when deserializing.
        if (count($types) > 1) {
            return null;
        }

        // Only one type is left, so we only need to differentiate between arrays, generics and other types.
        $type = $types[0];

        // Simple array without concrete type: array
        if ($this->isSimpleType($type, 'array') || $this->isSimpleType($type, 'list')) {
            return null;
        }

        // Normal array syntax: Product[] | \Foo\Bar\Product[]
        if ($type instanceof ArrayTypeNode) {
            $resolvedType = $this->resolveTypeFromTypeNode($type->type, $reflector);

            return 'array<' . $resolvedType . '>';
        }

        // Generic array syntax: array<Product> | array<\Foo\Bar\Product> | array<int,Product>
        if ($type instanceof GenericTypeNode) {
            if ($this->isSimpleType($type->type, 'array')) {
                $resolvedTypes = array_map(fn (TypeNode $node) => $this->resolveTypeFromTypeNode($node, $reflector), $type->genericTypes);

                return 'array<' . implode(',', $resolvedTypes) . '>';
            }

            if ($this->isSimpleType($type->type, 'list')) {
                $resolvedTypes = array_map(fn (TypeNode $node) => $this->resolveTypeFromTypeNode($node, $reflector), $type->genericTypes);

                return 'list<' . implode(',', $resolvedTypes) . '>';
            }

            throw new \InvalidArgumentException(sprintf("Can't use non-array generic type %s for collection in %s:%s", (string) $type->type, $reflector->getDeclaringClass()->getName(), $reflector->getName()));
        }

        // Primitives and class names: Collection | \Foo\Bar\Product | string
        return $this->resolveTypeFromTypeNode($type, $reflector);
    }

    /**
     * Returns a flat list of types of the given var tags. Union types are flattened as well.
     *
     * @param ReturnTagValueNode[]|VarTagValueNode[] $tagValues
     *
     * @return TypeNode[]
     */
    private function flattenTagValueTypes(array $tagValues): array
    {
        if ([] === $tagValues) {
            return [];
        }

        return array_merge(...array_map(static function ($node) {
            if ($node->type instanceof UnionTypeNode) {
                return $node->type->types;
            }

            return [$node->type];
        }, $tagValues));
    }

    /**
     * Returns a flat list of types of the given param tags. Union types are flattened as well.
     *
     * @param ParamTagValueNode[] $varTagValues
     *
     * @return TypeNode[]
     */
    private function flattenParamTagValueTypes(string $parameterName, array $varTagValues): array
    {
        if ([] === $varTagValues) {
            return [];
        }

        $parameterName = sprintf('$%s', $parameterName);
        $types = [];
        foreach ($varTagValues as $node) {
            if ($parameterName !== $node->parameterName) {
                continue;
            }

            $types[] = $node->type;
        }

        return $types;
    }

    /**
     * Filters the null type from the given types array. If no null type is found, the array is returned unchanged.
     *
     * @param TypeNode[] $types
     *
     * @return TypeNode[]
     */
    private function filterNullFromTypes(array $types): array
    {
        return array_values(array_filter(array_map(fn (TypeNode $node) => $this->isNullType($node) ? null : $node, $types)));
    }

    /**
     * Determines if the given type is a null type.
     *
     * @param TypeNode $typeNode
     *
     * @return bool
     */
    private function isNullType(TypeNode $typeNode): bool
    {
        return $this->isSimpleType($typeNode, 'null');
    }

    /**
     * Determines if the given node represents a simple type.
     *
     * @param TypeNode $typeNode
     * @param string $simpleType
     *
     * @return bool
     */
    private function isSimpleType(TypeNode $typeNode, string $simpleType): bool
    {
        return $typeNode instanceof IdentifierTypeNode && $typeNode->name === $simpleType;
    }

    /**
     * Attempts to resolve the fully qualified type from the given node. If the node is not suitable for type
     * retrieval, an exception is thrown.
     *
     * @param TypeNode $typeNode
     * @param \ReflectionMethod|\ReflectionProperty $reflector
     *
     * @return string
     *
     * @throws \InvalidArgumentException
     */
    private function resolveTypeFromTypeNode(TypeNode $typeNode, $reflector): string
    {
        if (!($typeNode instanceof IdentifierTypeNode)) {
            throw new \InvalidArgumentException(sprintf("Can't use unsupported type %s for collection in %s:%s", (string) $typeNode, $reflector->getDeclaringClass()->getName(), $reflector->getName()));
        }

        return $this->resolveType($typeNode->name, $reflector);
    }

    /**
     * @param \ReflectionMethod|\ReflectionProperty $reflector
     */
    private function expandClassNameUsingUseStatements(string $typeHint, \ReflectionClass $declaringClass, $reflector): string
    {
        $expandedClassName = $declaringClass->getNamespaceName() . '\\' . $typeHint;
        if ($this->isClassOrInterface($expandedClassName)) {
            return $expandedClassName;
        }

        $classContents = file_get_contents($declaringClass->getFileName());
        $foundUseStatements = $this->gatherGroupUseStatements($classContents);
        $foundUseStatements = array_merge($this->gatherSingleUseStatements($classContents), $foundUseStatements);

        foreach ($foundUseStatements as $statementClassName) {
            if ($alias = explode('as', $statementClassName)) {
                if (array_key_exists(1, $alias) && trim($alias[1]) === $typeHint) {
                    return trim($alias[0]);
                }
            }

            if ($this->endsWith($statementClassName, $typeHint)) {
                return $statementClassName;
            }
        }

        if ($declaringClass->getDocComment()) {
            $phpstanArrayType = $this->getPhpstanArrayType($declaringClass, $typeHint, $reflector);

            if ($phpstanArrayType) {
                return $phpstanArrayType;
            }
        }

        if ($this->isClassOrInterface($typeHint)) {
            return $typeHint;
        }

        throw new \InvalidArgumentException(sprintf("Can't use incorrect type %s for collection in %s:%s", $typeHint, $declaringClass->getName(), $reflector->getName()));
    }

    private function endsWith(string $statementClassToCheck, string $typeHintToSearchFor): bool
    {
        $typeHintToSearchFor = '\\' . $typeHintToSearchFor;

        return substr($statementClassToCheck, -strlen($typeHintToSearchFor)) === $typeHintToSearchFor;
    }

    private function isPrimitiveType(string $type): bool
    {
        return in_array($type, ['int', 'integer', 'float', 'bool', 'boolean', 'double', 'string']);
    }

    private function hasGlobalNamespacePrefix(string $typeHint): bool
    {
        return self::GLOBAL_NAMESPACE_PREFIX === $typeHint[0];
    }

    private function gatherGroupUseStatements(string $classContents): array
    {
        $foundUseStatements = [];
        preg_match_all(self::GROUP_USE_STATEMENTS_REGEX, $classContents, $foundGroupUseStatements);
        for ($useStatementIndex = 0; $useStatementIndex < count($foundGroupUseStatements[0]); $useStatementIndex++) {
            foreach (explode(',', $foundGroupUseStatements[2][$useStatementIndex]) as $singleUseStatement) {
                $foundUseStatements[] = trim($foundGroupUseStatements[1][$useStatementIndex]) . trim($singleUseStatement);
            }
        }

        return $foundUseStatements;
    }

    private function gatherSingleUseStatements(string $classContents): array
    {
        $foundUseStatements = [];
        preg_match_all(self::SINGLE_USE_STATEMENTS_REGEX, $classContents, $foundSingleUseStatements);
        for ($useStatementIndex = 0; $useStatementIndex < count($foundSingleUseStatements[0]); $useStatementIndex++) {
            $foundUseStatements[] = trim($foundSingleUseStatements[1][$useStatementIndex]);
        }

        return $foundUseStatements;
    }

    /**
     * @param \ReflectionMethod|\ReflectionProperty $reflector
     */
    private function getDeclaringClassOrTrait($reflector): \ReflectionClass
    {
        foreach ($reflector->getDeclaringClass()->getTraits() as $trait) {
            foreach ($trait->getProperties() as $traitProperty) {
                if ($traitProperty->getName() === $reflector->getName()) {
                    return $this->getDeclaringClassOrTrait($traitProperty);
                }
            }
        }

        return $reflector->getDeclaringClass();
    }

    /**
     * @param \ReflectionMethod|\ReflectionProperty $reflector
     */
    private function resolveType(string $typeHint, $reflector): string
    {
        if (!$this->hasGlobalNamespacePrefix($typeHint) && !$this->isPrimitiveType($typeHint)) {
            $typeHint = $this->expandClassNameUsingUseStatements($typeHint, $this->getDeclaringClassOrTrait($reflector), $reflector);
        }

        return ltrim($typeHint, '\\');
    }

    private function isClassOrInterface(string $typeHint): bool
    {
        return class_exists($typeHint) || interface_exists($typeHint);
    }

    /**
     * @param \ReflectionMethod|\ReflectionProperty $reflector
     */
    private function resolveTypeFromDocblock($reflector): array
    {
        $docComment = $reflector->getDocComment();
        if (!$docComment && PHP_VERSION_ID >= 80000 && $reflector instanceof \ReflectionProperty && $reflector->isPromoted()) {
            $constructor = $reflector->getDeclaringClass()->getConstructor();
            if (!$constructor) {
                return [];
            }

            $docComment = $constructor->getDocComment();

            if (!$docComment) {
                return [];
            }

            $tokens = $this->lexer->tokenize($docComment);
            $phpDocNode = $this->phpDocParser->parse(new TokenIterator($tokens));

            return $this->flattenParamTagValueTypes($reflector->getName(), $phpDocNode->getParamTagValues());
        }

        if (!$docComment) {
            return [];
        }

        // First we tokenize the PhpDoc comment and parse the tokens into a PhpDocNode.
        $tokens = $this->lexer->tokenize($docComment);
        $phpDocNode = $this->phpDocParser->parse(new TokenIterator($tokens));

        if ($reflector instanceof \ReflectionProperty) {
            // Then we retrieve a flattened list of annotated types excluding null.
            $tagValues = $phpDocNode->getVarTagValues();
        } else {
            // Then we retrieve a flattened list of annotated types including null.
            $tagValues = $phpDocNode->getReturnTagValues();
        }

        $types = $this->flattenTagValueTypes($tagValues);

        return $this->filterNullFromTypes($types);
    }

    /**
     * @param \ReflectionMethod|\ReflectionProperty $reflector
     */
    private function getPhpstanArrayType(\ReflectionClass $declaringClass, string $typeHint, $reflector): ?string
    {
        $tokens = $this->lexer->tokenize($declaringClass->getDocComment());
        $phpDocNode = $this->phpDocParser->parse(new TokenIterator($tokens));
        $self = $this;

        foreach ($phpDocNode->children as $node) {
            if (
                $node instanceof PhpDocTagNode
                && $node->value instanceof TypeAliasTagValueNode
                && $node->value->alias === $typeHint
            ) {
                $phpstanType = $node->value->__toString();
                preg_match(self::PHPSTAN_ARRAY_SHAPE, $phpstanType, $foundPhpstanArray);
                if (isset($foundPhpstanArray[0])) {
                    return 'array';
                }

                preg_match(self::PHPSTAN_ARRAY_TYPE, $phpstanType, $foundPhpstanArray);
                if (isset($foundPhpstanArray[0])) {
                    $types = explode(',', $foundPhpstanArray[2]);

                    return sprintf('array<%s>', implode(
                        ',',
                        array_map(static fn (string $type) => $self->resolveType(trim($type), $reflector), $types),
                    ));
                }
            } elseif ($node instanceof PhpDocTagNode && $node->value instanceof TypeAliasImportTagValueNode) {
                $importedFromFqn = $this->resolveType($node->value->importedFrom->name, $reflector);

                return $this->getPhpstanArrayType(
                    new \ReflectionClass($importedFromFqn),
                    $node->value->importedAlias,
                    $reflector,
                );
            }
        }

        return null;
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Metadata\Driver;

use JMS\Serializer\Annotation\ExclusionPolicy;
use JMS\Serializer\Exception\InvalidMetadataException;
use JMS\Serializer\Expression\CompilableExpressionEvaluatorInterface;
use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\Metadata\ExpressionPropertyMetadata;
use JMS\Serializer\Metadata\PropertyMetadata;
use JMS\Serializer\Metadata\VirtualPropertyMetadata;
use JMS\Serializer\Naming\PropertyNamingStrategyInterface;
use JMS\Serializer\Type\Parser;
use JMS\Serializer\Type\ParserInterface;
use Metadata\ClassMetadata as BaseClassMetadata;
use Metadata\Driver\AbstractFileDriver;
use Metadata\Driver\AdvancedFileLocatorInterface;
use Metadata\Driver\FileLocatorInterface;
use Metadata\MethodMetadata;
use ReflectionClass;
use RuntimeException;
use Symfony\Component\Yaml\Yaml;

class YamlDriver extends AbstractFileDriver
{
    use ExpressionMetadataTrait;

    /**
     * @var ParserInterface
     */
    private $typeParser;
    /**
     * @var PropertyNamingStrategyInterface
     */
    private $namingStrategy;
    /**
     * @var FileLocatorInterface
     */
    private $locator;

    public function __construct(FileLocatorInterface $locator, PropertyNamingStrategyInterface $namingStrategy, ?ParserInterface $typeParser = null, ?CompilableExpressionEvaluatorInterface $expressionEvaluator = null)
    {
        $this->locator = $locator;
        $this->typeParser = $typeParser ?? new Parser();
        $this->namingStrategy = $namingStrategy;
        $this->expressionEvaluator = $expressionEvaluator;
    }

    public function loadMetadataForClass(ReflectionClass $class): ?BaseClassMetadata
    {
        $path = null;
        foreach ($this->getExtensions() as $extension) {
            $path = $this->locator->findFileForClass($class, $extension);
            if (null !== $path) {
                break;
            }
        }

        if (null === $path) {
            return null;
        }

        return $this->loadMetadataFromFile($class, $path);
    }

    /**
     * {@inheritDoc}
     */
    public function getAllClassNames(): array
    {
        if (!$this->locator instanceof AdvancedFileLocatorInterface) {
            throw new RuntimeException(
                sprintf(
                    'Locator "%s" must be an instance of "AdvancedFileLocatorInterface".',
                    get_class($this->locator),
                ),
            );
        }

        $classes = [];
        foreach ($this->getExtensions() as $extension) {
            foreach ($this->locator->findAllClasses($extension) as $class) {
                $classes[$class] = $class;
            }
        }

        return array_values($classes);
    }

    protected function loadMetadataFromFile(ReflectionClass $class, string $file): ?BaseClassMetadata
    {
        $config = Yaml::parseFile($file, Yaml::PARSE_CONSTANT);

        if (!isset($config[$name = $class->name])) {
            throw new InvalidMetadataException(
                sprintf('Expected metadata for class %s to be defined in %s.', $class->name, $file),
            );
        }

        $config = $config[$name];
        $metadata = new ClassMetadata($name);
        $metadata->fileResources[] = $file;
        $fileResource = $class->getFilename();
        if (false !== $fileResource) {
            $metadata->fileResources[] = $fileResource;
        }

        $exclusionPolicy = isset($config['exclusion_policy']) ? strtoupper($config['exclusion_policy']) : 'NONE';
        $excludeAll = isset($config['exclude']) ? (bool) $config['exclude'] : false;

        if (isset($config['exclude_if'])) {
            $metadata->excludeIf = $this->parseExpression((string) $config['exclude_if']);
        }

        $classAccessType = $config['access_type'] ?? PropertyMetadata::ACCESS_TYPE_PROPERTY;
        $readOnlyClass = isset($config['read_only']) ? (bool) $config['read_only'] : false;
        $this->addClassProperties($metadata, $config);

        $propertiesMetadata = [];
        $propertiesData = [];
        if (array_key_exists('virtual_properties', $config)) {
            foreach ($config['virtual_properties'] as $methodName => $propertySettings) {
                if (isset($propertySettings['exp'])) {
                    $virtualPropertyMetadata = new ExpressionPropertyMetadata(
                        $name,
                        $methodName,
                        $this->parseExpression($propertySettings['exp']),
                    );
                    unset($propertySettings['exp']);
                } else {
                    if (!$class->hasMethod($methodName)) {
                        throw new InvalidMetadataException(
                            'The method ' . $methodName . ' not found in class ' . $class->name,
                        );
                    }

                    $virtualPropertyMetadata = new VirtualPropertyMetadata($name, $methodName);
                }

                $propertiesMetadata[] = $virtualPropertyMetadata;
                $propertiesData[] = $propertySettings;
            }
        }

        if (!$excludeAll) {
            foreach ($class->getProperties() as $property) {
                if ($property->class !== $name || (isset($property->info) && $property->info['class'] !== $name)) {
                    continue;
                }

                $pName = $property->getName();
                $propertiesMetadata[] = new PropertyMetadata($name, $pName);
                $propertiesData[] =  !empty($config['properties']) && true === array_key_exists($pName, $config['properties'])
                    ? (array) $config['properties'][$pName]
                    : null;
            }

            foreach ($propertiesMetadata as $propertyKey => $pMetadata) {
                $isExclude = false;
                $isExpose = $pMetadata instanceof VirtualPropertyMetadata
                    || $pMetadata instanceof ExpressionPropertyMetadata
                    || isset($propertiesData[$propertyKey]);

                $pConfig = $propertiesData[$propertyKey];
                if (!empty($pConfig)) {
                    if (isset($pConfig['exclude'])) {
                        $isExclude = (bool) $pConfig['exclude'];
                    }

                    if ($isExclude) {
                        continue;
                    }

                    if (isset($pConfig['expose'])) {
                        $isExpose = (bool) $pConfig['expose'];
                    }

                    if (isset($pConfig['skip_when_empty'])) {
                        $pMetadata->skipWhenEmpty = (bool) $pConfig['skip_when_empty'];
                    }

                    if (isset($pConfig['since_version'])) {
                        $pMetadata->sinceVersion = (string) $pConfig['since_version'];
                    }

                    if (isset($pConfig['until_version'])) {
                        $pMetadata->untilVersion = (string) $pConfig['until_version'];
                    }

                    if (isset($pConfig['exclude_if'])) {
                        $pMetadata->excludeIf = $this->parseExpression((string) $pConfig['exclude_if']);
                    }

                    if (isset($pConfig['expose_if'])) {
                        $pMetadata->excludeIf = $this->parseExpression('!(' . $pConfig['expose_if'] . ')');
                    }

                    if (isset($pConfig['serialized_name'])) {
                        $pMetadata->serializedName = (string) $pConfig['serialized_name'];
                    }

                    if (isset($pConfig['type'])) {
                        $pMetadata->setType($this->typeParser->parse((string) $pConfig['type']));
                    }

                    if (isset($pConfig['groups'])) {
                        $pMetadata->groups = $pConfig['groups'];
                    }

                    if (isset($pConfig['xml_list'])) {
                        $pMetadata->xmlCollection = true;

                        $colConfig = $pConfig['xml_list'];
                        if (isset($colConfig['inline'])) {
                            $pMetadata->xmlCollectionInline = (bool) $colConfig['inline'];
                        }

                        if (isset($colConfig['entry_name'])) {
                            $pMetadata->xmlEntryName = (string) $colConfig['entry_name'];
                        }

                        if (isset($colConfig['skip_when_empty'])) {
                            $pMetadata->xmlCollectionSkipWhenEmpty = (bool) $colConfig['skip_when_empty'];
                        } else {
                            $pMetadata->xmlCollectionSkipWhenEmpty = true;
                        }

                        if (isset($colConfig['namespace'])) {
                            $pMetadata->xmlEntryNamespace = (string) $colConfig['namespace'];
                        }
                    }

                    if (isset($pConfig['xml_map'])) {
                        $pMetadata->xmlCollection = true;

                        $colConfig = $pConfig['xml_map'];
                        if (isset($colConfig['inline'])) {
                            $pMetadata->xmlCollectionInline = (bool) $colConfig['inline'];
                        }

                        if (isset($colConfig['entry_name'])) {
                            $pMetadata->xmlEntryName = (string) $colConfig['entry_name'];
                        }

                        if (isset($colConfig['namespace'])) {
                            $pMetadata->xmlEntryNamespace = (string) $colConfig['namespace'];
                        }

                        if (isset($colConfig['key_attribute_name'])) {
                            $pMetadata->xmlKeyAttribute = $colConfig['key_attribute_name'];
                        }
                    }

                    if (isset($pConfig['xml_element'])) {
                        $colConfig = $pConfig['xml_element'];
                        if (isset($colConfig['cdata'])) {
                            $pMetadata->xmlElementCData = (bool) $colConfig['cdata'];
                        }

                        if (isset($colConfig['namespace'])) {
                            $pMetadata->xmlNamespace = (string) $colConfig['namespace'];
                        }
                    }

                    if (isset($pConfig['xml_attribute'])) {
                        $pMetadata->xmlAttribute = (bool) $pConfig['xml_attribute'];
                    }

                    if (isset($pConfig['xml_attribute_map'])) {
                        $pMetadata->xmlAttributeMap = (bool) $pConfig['xml_attribute_map'];
                    }

                    if (isset($pConfig['xml_value'])) {
                        $pMetadata->xmlValue = (bool) $pConfig['xml_value'];
                    }

                    if (isset($pConfig['xml_key_value_pairs'])) {
                        $pMetadata->xmlKeyValuePairs = (bool) $pConfig['xml_key_value_pairs'];
                    }

                    //we need read_only before setter and getter set, because that method depends on flag being set
                    if (isset($pConfig['read_only'])) {
                        $pMetadata->readOnly = (bool) $pConfig['read_only'];
                    } else {
                        $pMetadata->readOnly = $pMetadata->readOnly || $readOnlyClass;
                    }

                    $pMetadata->setAccessor(
                        $pConfig['access_type'] ?? $classAccessType,
                        $pConfig['accessor']['getter'] ?? null,
                        $pConfig['accessor']['setter'] ?? null,
                    );

                    if (isset($pConfig['inline'])) {
                        $pMetadata->inline = (bool) $pConfig['inline'];
                    }

                    if (isset($pConfig['max_depth'])) {
                        $pMetadata->maxDepth = (int) $pConfig['max_depth'];
                    }

                    if (isset($pConfig['union_discriminator'])) {
                        $pMetadata->setType([
                            'name' => 'union',
                            'params' => [null, $pConfig['union_discriminator']['field'], $pConfig['union_discriminator']['map']],
                        ]);
                    }
                }

                if (!$pMetadata->serializedName) {
                    $pMetadata->serializedName = $this->namingStrategy->translateName($pMetadata);
                }

                if ($pMetadata->inline) {
                    $metadata->isList = $metadata->isList || PropertyMetadata::isCollectionList($pMetadata->type);
                    $metadata->isMap = $metadata->isMap || PropertyMetadata::isCollectionMap($pMetadata->type);
                }

                if (!empty($pConfig) && !empty($pConfig['name'])) {
                    $pMetadata->name = (string) $pConfig['name'];
                }

                if (
                    (ExclusionPolicy::NONE === $exclusionPolicy && !$isExclude)
                    || (ExclusionPolicy::ALL === $exclusionPolicy && $isExpose)
                ) {
                    $metadata->addPropertyMetadata($pMetadata);
                }
            }
        }

        if (isset($config['callback_methods'])) {
            $cConfig = $config['callback_methods'];

            if (isset($cConfig['pre_serialize'])) {
                $metadata->preSerializeMethods = $this->getCallbackMetadata($class, $cConfig['pre_serialize']);
            }

            if (isset($cConfig['post_serialize'])) {
                $metadata->postSerializeMethods = $this->getCallbackMetadata($class, $cConfig['post_serialize']);
            }

            if (isset($cConfig['post_deserialize'])) {
                $metadata->postDeserializeMethods = $this->getCallbackMetadata($class, $cConfig['post_deserialize']);
            }
        }

        return $metadata;
    }

    /**
     * @return string[]
     */
    protected function getExtensions(): array
    {
        return array_unique([$this->getExtension(), 'yaml', 'yml']);
    }

    /**
     * @deprecated use getExtensions instead.
     */
    protected function getExtension(): string
    {
        return 'yml';
    }

    private function addClassProperties(ClassMetadata $metadata, array $config): void
    {
        if (isset($config['custom_accessor_order']) && !isset($config['accessor_order'])) {
            $config['accessor_order'] = 'custom';
        }

        if (isset($config['accessor_order'])) {
            $metadata->setAccessorOrder($config['accessor_order'], $config['custom_accessor_order'] ?? []);
        }

        if (isset($config['xml_root_name'])) {
            $metadata->xmlRootName = (string) $config['xml_root_name'];
        }

        if (isset($config['xml_root_prefix'])) {
            $metadata->xmlRootPrefix = (string) $config['xml_root_prefix'];
        }

        if (isset($config['xml_root_namespace'])) {
            $metadata->xmlRootNamespace = (string) $config['xml_root_namespace'];
        }

        if (array_key_exists('xml_namespaces', $config)) {
            foreach ($config['xml_namespaces'] as $prefix => $uri) {
                $metadata->registerNamespace($uri, $prefix);
            }
        }

        if (isset($config['discriminator'])) {
            if (isset($config['discriminator']['disabled']) && true === $config['discriminator']['disabled']) {
                $metadata->discriminatorDisabled = true;
            } else {
                if (!isset($config['discriminator']['field_name'])) {
                    throw new InvalidMetadataException('The "field_name" attribute must be set for discriminators.');
                }

                if (!isset($config['discriminator']['map']) || !is_array($config['discriminator']['map'])) {
                    throw new InvalidMetadataException(
                        'The "map" attribute must be set, and be an array for discriminators.',
                    );
                }

                $groups = $config['discriminator']['groups'] ?? [];
                $metadata->setDiscriminator(
                    $config['discriminator']['field_name'],
                    $config['discriminator']['map'],
                    $groups,
                );

                if (isset($config['discriminator']['xml_attribute'])) {
                    $metadata->xmlDiscriminatorAttribute = (bool) $config['discriminator']['xml_attribute'];
                }

                if (isset($config['discriminator']['xml_element'])) {
                    if (isset($config['discriminator']['xml_element']['cdata'])) {
                        $metadata->xmlDiscriminatorCData = (bool) $config['discriminator']['xml_element']['cdata'];
                    }

                    if (isset($config['discriminator']['xml_element']['namespace'])) {
                        $metadata->xmlDiscriminatorNamespace = (string) $config['discriminator']['xml_element']['namespace'];
                    }
                }
            }
        }
    }

    /**
     * @param string|string[] $config
     */
    private function getCallbackMetadata(ReflectionClass $class, $config): array
    {
        if (is_string($config)) {
            $config = [$config];
        } elseif (!is_array($config)) {
            throw new InvalidMetadataException(
                sprintf(
                    'callback methods expects a string, or an array of strings that represent method names, but got %s.',
                    json_encode($config['pre_serialize']),
                ),
            );
        }

        $methods = [];
        foreach ($config as $name) {
            if (!$class->hasMethod($name)) {
                throw new InvalidMetadataException(
                    sprintf('The method %s does not exist in class %s.', $name, $class->name),
                );
            }

            $methods[] = new MethodMetadata($class->name, $name);
        }

        return $methods;
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Metadata\Driver;

use JMS\Serializer\Annotation\SerializerAttribute;

class AttributeDriver extends AnnotationOrAttributeDriver
{
    /**
     * @return list<SerializerAttribute>
     */
    protected function getClassAnnotations(\ReflectionClass $class): array
    {
        return array_map(
            static fn (\ReflectionAttribute $attribute): object => $attribute->newInstance(),
            $class->getAttributes(SerializerAttribute::class, \ReflectionAttribute::IS_INSTANCEOF),
        );
    }

    /**
     * @return list<SerializerAttribute>
     */
    protected function getMethodAnnotations(\ReflectionMethod $method): array
    {
        return array_map(
            static fn (\ReflectionAttribute $attribute): object => $attribute->newInstance(),
            $method->getAttributes(SerializerAttribute::class, \ReflectionAttribute::IS_INSTANCEOF),
        );
    }

    /**
     * @return list<SerializerAttribute>
     */
    protected function getPropertyAnnotations(\ReflectionProperty $property): array
    {
        return array_map(
            static fn (\ReflectionAttribute $attribute): object => $attribute->newInstance(),
            $property->getAttributes(SerializerAttribute::class, \ReflectionAttribute::IS_INSTANCEOF),
        );
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Metadata\Driver;

use JMS\Serializer\Metadata\ClassMetadata as SerializerClassMetadata;
use JMS\Serializer\Metadata\PropertyMetadata;
use Metadata\ClassMetadata;
use Metadata\Driver\DriverInterface;
use ReflectionClass;
use ReflectionException;
use ReflectionProperty;

class DefaultValuePropertyDriver implements DriverInterface
{
    /**
     * @var DriverInterface
     */
    protected $delegate;

    public function __construct(DriverInterface $delegate)
    {
        $this->delegate = $delegate;
    }

    /**
     * @return SerializerClassMetadata|null
     */
    public function loadMetadataForClass(ReflectionClass $class): ?ClassMetadata
    {
        $classMetadata = $this->delegate->loadMetadataForClass($class);

        if (null === $classMetadata) {
            return null;
        }

        \assert($classMetadata instanceof SerializerClassMetadata);

        foreach ($classMetadata->propertyMetadata as $key => $propertyMetadata) {
            \assert($propertyMetadata instanceof PropertyMetadata);
            if (null !== $propertyMetadata->hasDefault) {
                continue;
            }

            try {
                $propertyReflection = $this->getPropertyReflection($propertyMetadata);
                $propertyMetadata->hasDefault = false;
                if ($propertyReflection->hasDefaultValue() && $propertyReflection->hasType()) {
                    $propertyMetadata->hasDefault = true;
                    $propertyMetadata->defaultValue = $propertyReflection->getDefaultValue();
                } elseif ($propertyReflection->isPromoted()) {
                    // need to get the parameter in the constructor to check for default values
                    $classReflection = $this->getClassReflection($propertyMetadata);
                    $params = $classReflection->getConstructor()->getParameters();
                    foreach ($params as $parameter) {
                        if ($parameter->getName() === $propertyMetadata->name) {
                            if ($parameter->isDefaultValueAvailable()) {
                                $propertyMetadata->hasDefault = true;
                                $propertyMetadata->defaultValue = $parameter->getDefaultValue();
                            }

                            break;
                        }
                    }
                }
            } catch (ReflectionException $e) {
                continue;
            }
        }

        return $classMetadata;
    }

    private function getPropertyReflection(PropertyMetadata $propertyMetadata): ReflectionProperty
    {
        return new ReflectionProperty($propertyMetadata->class, $propertyMetadata->name);
    }

    private function getClassReflection(PropertyMetadata $propertyMetadata): ReflectionClass
    {
        return new ReflectionClass($propertyMetadata->class);
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Metadata\Driver;

use Doctrine\ODM\PHPCR\Mapping\ClassMetadata as ORMClassMetadata;
use Doctrine\ORM\Mapping\ClassMetadata as ODMClassMetadata;
use Doctrine\ORM\Mapping\DiscriminatorColumnMapping;
use Doctrine\Persistence\Mapping\ClassMetadata as DoctrineClassMetadata;
use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\Metadata\PropertyMetadata;

/**
 * This class decorates any other driver. If the inner driver does not provide a
 * a property type, the decorator will guess based on Doctrine 2 metadata.
 */
class DoctrineTypeDriver extends AbstractDoctrineTypeDriver
{
    protected function setDiscriminator(DoctrineClassMetadata $doctrineMetadata, ClassMetadata $classMetadata): void
    {
        assert($doctrineMetadata instanceof ORMClassMetadata || $doctrineMetadata instanceof ODMClassMetadata);
        if (
            empty($classMetadata->discriminatorMap) && !$classMetadata->discriminatorDisabled
            && !empty($doctrineMetadata->discriminatorMap) && $doctrineMetadata->isRootEntity()
        ) {
            if ($doctrineMetadata->discriminatorColumn instanceof DiscriminatorColumnMapping) {
                // Doctrine 3.1+
                $classMetadata->setDiscriminator(
                    $doctrineMetadata->discriminatorColumn->name,
                    $doctrineMetadata->discriminatorMap,
                );
            } else {
                // Doctrine up to 3.1
                $classMetadata->setDiscriminator(
                    $doctrineMetadata->discriminatorColumn['name'],
                    $doctrineMetadata->discriminatorMap,
                );
            }
        }
    }

    protected function setPropertyType(DoctrineClassMetadata $doctrineMetadata, PropertyMetadata $propertyMetadata): void
    {
        $propertyName = $propertyMetadata->name;
        if (
            $doctrineMetadata->hasField($propertyName)
            && ($typeOfFiled = $doctrineMetadata->getTypeOfField($propertyName))
            && ($fieldType = $this->normalizeFieldType($typeOfFiled))
        ) {
            $propertyMetadata->setType($this->typeParser->parse($fieldType));
        } elseif ($doctrineMetadata->hasAssociation($propertyName)) {
            $targetEntity = $doctrineMetadata->getAssociationTargetClass($propertyName);

            if (null === $targetMetadata = $this->tryLoadingDoctrineMetadata($targetEntity)) {
                return;
            }

            // For inheritance schemes, we cannot add any type as we would only add the super-type of the hierarchy.
            // On serialization, this would lead to only the supertype being serialized, and properties of subtypes
            // being ignored.
            if ($targetMetadata instanceof ODMClassMetadata && !$targetMetadata->isInheritanceTypeNone()) {
                return;
            }

            if (!$doctrineMetadata->isSingleValuedAssociation($propertyName)) {
                $targetEntity = sprintf('ArrayCollection<%s>', $targetEntity);
            }

            $propertyMetadata->setType($this->typeParser->parse($targetEntity));
        }
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Metadata\Driver;

use Doctrine\Persistence\ManagerRegistry;
use Doctrine\Persistence\Mapping\ClassMetadata as DoctrineClassMetadata;
use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\Metadata\ExpressionPropertyMetadata;
use JMS\Serializer\Metadata\PropertyMetadata;
use JMS\Serializer\Metadata\StaticPropertyMetadata;
use JMS\Serializer\Metadata\VirtualPropertyMetadata;
use JMS\Serializer\Type\Parser;
use JMS\Serializer\Type\ParserInterface;
use Metadata\ClassMetadata as BaseClassMetadata;
use Metadata\Driver\DriverInterface;

/**
 * This class decorates any other driver. If the inner driver does not provide a
 * a property type, the decorator will guess based on Doctrine 2 metadata.
 */
abstract class AbstractDoctrineTypeDriver implements DriverInterface
{
    /**
     * Map of doctrine 2 field types to JMS\Serializer types
     *
     * @var array
     */
    protected $fieldMapping = [
        'string' => 'string',
        'ascii_string' => 'string',
        'text' => 'string',
        'blob' => 'string',
        'guid' => 'string',
        'decimal' => 'string',

        'integer' => 'integer',
        'smallint' => 'integer',
        'bigint' => 'integer',

        'datetime' => 'DateTime',
        'datetimetz' => 'DateTime',
        'time' => 'DateTime',
        'date' => 'DateTime',

        'datetime_immutable' => 'DateTimeImmutable',
        'datetimetz_immutable' => 'DateTimeImmutable',
        'time_immutable' => 'DateTimeImmutable',
        'date_immutable' => 'DateTimeImmutable',

        'dateinterval' => 'DateInterval',

        'float' => 'float',

        'boolean' => 'boolean',

        'array' => 'array',
        'json_array' => 'array',
        'simple_array' => 'array<string>',
    ];
    /**
     * @var DriverInterface
     */
    protected $delegate;
    /**
     * @var ManagerRegistry
     */
    protected $registry;

    /**
     * @var ParserInterface
     */
    protected $typeParser;

    public function __construct(DriverInterface $delegate, ManagerRegistry $registry, ?ParserInterface $typeParser = null)
    {
        $this->delegate = $delegate;
        $this->registry = $registry;
        $this->typeParser = $typeParser ?: new Parser();
    }

    public function loadMetadataForClass(\ReflectionClass $class): ?BaseClassMetadata
    {
        $classMetadata = $this->delegate->loadMetadataForClass($class);

        if (null === $classMetadata) {
            return null;
        }

        \assert($classMetadata instanceof ClassMetadata);
        // Abort if the given class is not a mapped entity
        if (!$doctrineMetadata = $this->tryLoadingDoctrineMetadata($class->name)) {
            return $classMetadata;
        }

        $this->setDiscriminator($doctrineMetadata, $classMetadata);

        // We base our scan on the internal driver's property list so that we
        // respect any internal allow/blocklist like in the AnnotationDriver
        foreach ($classMetadata->propertyMetadata as $key => $propertyMetadata) {
            // If the inner driver provides a type, don't guess anymore.
            if ($propertyMetadata->type || $this->isVirtualProperty($propertyMetadata)) {
                continue;
            }

            if ($this->hideProperty($doctrineMetadata, $propertyMetadata)) {
                unset($classMetadata->propertyMetadata[$key]);
            }

            $this->setPropertyType($doctrineMetadata, $propertyMetadata);
        }

        return $classMetadata;
    }

    private function isVirtualProperty(PropertyMetadata $propertyMetadata): bool
    {
        return $propertyMetadata instanceof VirtualPropertyMetadata
            || $propertyMetadata instanceof StaticPropertyMetadata
            || $propertyMetadata instanceof ExpressionPropertyMetadata;
    }

    protected function setDiscriminator(DoctrineClassMetadata $doctrineMetadata, ClassMetadata $classMetadata): void
    {
    }

    protected function hideProperty(DoctrineClassMetadata $doctrineMetadata, PropertyMetadata $propertyMetadata): bool
    {
        return false;
    }

    protected function setPropertyType(DoctrineClassMetadata $doctrineMetadata, PropertyMetadata $propertyMetadata): void
    {
    }

    protected function tryLoadingDoctrineMetadata(string $className): ?DoctrineClassMetadata
    {
        if (!$manager = $this->registry->getManagerForClass($className)) {
            return null;
        }

        if ($manager->getMetadataFactory()->isTransient($className)) {
            return null;
        }

        return $manager->getClassMetadata($className);
    }

    protected function normalizeFieldType(string $type): ?string
    {
        if (!isset($this->fieldMapping[$type])) {
            return null;
        }

        return $this->fieldMapping[$type];
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Metadata\Driver;

use JMS\Serializer\Metadata\ClassMetadata as SerializerClassMetadata;
use JMS\Serializer\Metadata\ExpressionPropertyMetadata;
use JMS\Serializer\Metadata\PropertyMetadata;
use JMS\Serializer\Metadata\StaticPropertyMetadata;
use JMS\Serializer\Metadata\VirtualPropertyMetadata;
use JMS\Serializer\Type\Parser;
use JMS\Serializer\Type\ParserInterface;
use Metadata\ClassMetadata;
use Metadata\Driver\DriverInterface;
use ReflectionClass;
use ReflectionException;
use ReflectionMethod;
use ReflectionNamedType;
use ReflectionProperty;
use ReflectionType;

class TypedPropertiesDriver implements DriverInterface
{
    /**
     * @var DriverInterface
     */
    protected $delegate;

    /**
     * @var ParserInterface
     */
    protected $typeParser;

    /**
     * @var string[]
     */
    private $allowList;

    /**
     * @param string[] $allowList
     */
    public function __construct(DriverInterface $delegate, ?ParserInterface $typeParser = null, array $allowList = [])
    {
        $this->delegate = $delegate;
        $this->typeParser = $typeParser ?: new Parser();
        $this->allowList = array_merge($allowList, $this->getDefaultWhiteList());
    }

    /**
     *  ReflectionUnionType::getTypes() returns the types sorted according to these rules:
     * - Classes, interfaces, traits, iterable (replaced by Traversable), ReflectionIntersectionType objects, parent and self:
     *     these types will be returned first, in the order in which they were declared.
     * - static and all built-in types (iterable replaced by array) will come next. They will always be returned in this order:
     *     static, callable, array, string, int, float, bool (or false or true), null.
     *
     * For determining types of primitives, it is necessary to reorder primitives so that they are tested from lowest specificity to highest:
     * i.e. null, true, false, int, float, bool, string
     */
    private function reorderTypes(array $types): array
    {
        uasort($types, static function ($a, $b) {
            $order = ['null' => 0, 'true' => 1, 'false' => 2, 'bool' => 3, 'int' => 4, 'float' => 5, 'array' => 6, 'string' => 7];

            return ($order[$a['name']] ?? 8) <=> ($order[$b['name']] ?? 8);
        });

        return $types;
    }

    private function getDefaultWhiteList(): array
    {
        return [
            'int',
            'float',
            'bool',
            'boolean',
            'true',
            'false',
            'string',
            'double',
            'iterable',
            'resource',
        ];
    }

    /**
     * @return SerializerClassMetadata|null
     */
    public function loadMetadataForClass(ReflectionClass $class): ?ClassMetadata
    {
        $classMetadata = $this->delegate->loadMetadataForClass($class);

        if (null === $classMetadata) {
            return null;
        }

        \assert($classMetadata instanceof SerializerClassMetadata);

        // We base our scan on the internal driver's property list so that we
        // respect any internal allow/blocklist like in the AnnotationDriver
        foreach ($classMetadata->propertyMetadata as $propertyMetadata) {
            // If the inner driver provides a type, don't guess anymore.
            if ($propertyMetadata->type) {
                continue;
            }

            try {
                $reflectionType = $this->getReflectionType($propertyMetadata);

                if ($this->shouldTypeHint($reflectionType)) {
                    $type = $reflectionType->getName();

                    $propertyMetadata->setType($this->typeParser->parse($type));
                } elseif ($this->shouldTypeHintUnion($reflectionType)) {
                    $propertyMetadata->setType([
                        'name' => 'union',
                        'params' => [
                            $this->reorderTypes(
                                array_map(
                                    fn (string $type) => $this->typeParser->parse($type),
                                    array_filter($reflectionType->getTypes(), [$this, 'shouldTypeHintInsideUnion']),
                                ),
                            ),
                        ],
                    ]);
                }
            } catch (ReflectionException $e) {
                continue;
            }
        }

        return $classMetadata;
    }

    private function getReflectionType(PropertyMetadata $propertyMetadata): ?ReflectionType
    {
        if ($this->isNotSupportedVirtualProperty($propertyMetadata)) {
            return null;
        }

        if ($propertyMetadata instanceof VirtualPropertyMetadata) {
            return (new ReflectionMethod($propertyMetadata->class, $propertyMetadata->getter))
                ->getReturnType();
        }

        return (new ReflectionProperty($propertyMetadata->class, $propertyMetadata->name))
            ->getType();
    }

    private function isNotSupportedVirtualProperty(PropertyMetadata $propertyMetadata): bool
    {
        return $propertyMetadata instanceof StaticPropertyMetadata
            || $propertyMetadata instanceof ExpressionPropertyMetadata;
    }

    /**
     * @phpstan-assert-if-true \ReflectionNamedType $reflectionType
     */
    private function shouldTypeHint(?ReflectionType $reflectionType): bool
    {
        if (!$reflectionType instanceof ReflectionNamedType) {
            return false;
        }

        if (in_array($reflectionType->getName(), $this->allowList, true)) {
            return true;
        }

        return class_exists($reflectionType->getName())
            || interface_exists($reflectionType->getName());
    }

    /**
     * @phpstan-assert-if-true \ReflectionUnionType $reflectionType
     */
    private function shouldTypeHintUnion(?ReflectionType $reflectionType)
    {
        if (!$reflectionType instanceof \ReflectionUnionType) {
            return false;
        }

        $types = $reflectionType->getTypes();

        foreach ($types as $type) {
            if ($type instanceof \ReflectionIntersectionType) {
                return false;
            }
        }

        foreach ($types as $type) {
            if ($this->shouldTypeHintInsideUnion($type)) {
                return true;
            }
        }

        return false;
    }

    private function shouldTypeHintInsideUnion(ReflectionNamedType $reflectionType)
    {
        return $this->shouldTypeHint($reflectionType) || 'array' === $reflectionType->getName();
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Metadata\Driver;

use JMS\Serializer\Annotation\ExclusionPolicy;
use JMS\Serializer\Exception\InvalidMetadataException;
use JMS\Serializer\Exception\XmlErrorException;
use JMS\Serializer\Expression\CompilableExpressionEvaluatorInterface;
use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\Metadata\ExpressionPropertyMetadata;
use JMS\Serializer\Metadata\PropertyMetadata;
use JMS\Serializer\Metadata\VirtualPropertyMetadata;
use JMS\Serializer\Naming\PropertyNamingStrategyInterface;
use JMS\Serializer\Type\Parser;
use JMS\Serializer\Type\ParserInterface;
use Metadata\ClassMetadata as BaseClassMetadata;
use Metadata\Driver\AbstractFileDriver;
use Metadata\Driver\FileLocatorInterface;
use Metadata\MethodMetadata;

/**
 * @method ClassMetadata|null loadMetadataForClass(\ReflectionClass $class)
 */
class XmlDriver extends AbstractFileDriver
{
    use ExpressionMetadataTrait;

    /**
     * @var ParserInterface
     */
    private $typeParser;
    /**
     * @var PropertyNamingStrategyInterface
     */
    private $namingStrategy;

    public function __construct(FileLocatorInterface $locator, PropertyNamingStrategyInterface $namingStrategy, ?ParserInterface $typeParser = null, ?CompilableExpressionEvaluatorInterface $expressionEvaluator = null)
    {
        parent::__construct($locator);

        $this->typeParser = $typeParser ?? new Parser();
        $this->namingStrategy = $namingStrategy;
        $this->expressionEvaluator = $expressionEvaluator;
    }

    protected function loadMetadataFromFile(\ReflectionClass $class, string $path): ?BaseClassMetadata
    {
        $previous = libxml_use_internal_errors(true);
        libxml_clear_errors();

        $elem = simplexml_load_file($path);
        libxml_use_internal_errors($previous);

        if (false === $elem) {
            throw new InvalidMetadataException('Invalid XML content for metadata', 0, new XmlErrorException(libxml_get_last_error()));
        }

        $metadata = new ClassMetadata($name = $class->name);
        if (!$elems = $elem->xpath("./class[@name = '" . $name . "']")) {
            throw new InvalidMetadataException(sprintf('Could not find class %s inside XML element.', $name));
        }

        $elem = reset($elems);

        $metadata->fileResources[] = $path;
        $fileResource =  $class->getFilename();
        if (false !== $fileResource) {
            $metadata->fileResources[] = $fileResource;
        }

        $exclusionPolicy = strtoupper((string) $elem->attributes()->{'exclusion-policy'}) ?: 'NONE';
        $exclude = $elem->attributes()->exclude;
        $excludeAll = null !== $exclude ? 'true' === strtolower((string) $exclude) : false;

        if (null !== $excludeIf = $elem->attributes()->{'exclude-if'}) {
            $metadata->excludeIf = $this->parseExpression((string) $excludeIf);
        }

        $classAccessType = (string) ($elem->attributes()->{'access-type'} ?: PropertyMetadata::ACCESS_TYPE_PROPERTY);

        $propertiesMetadata = [];
        $propertiesNodes = [];

        if (null !== $accessorOrder = $elem->attributes()->{'accessor-order'}) {
            $metadata->setAccessorOrder((string) $accessorOrder, preg_split('/\s*,\s*/', (string) $elem->attributes()->{'custom-accessor-order'}));
        }

        if (null !== $xmlRootName = $elem->attributes()->{'xml-root-name'}) {
            $metadata->xmlRootName = (string) $xmlRootName;
        }

        if (null !== $xmlRootNamespace = $elem->attributes()->{'xml-root-namespace'}) {
            $metadata->xmlRootNamespace = (string) $xmlRootNamespace;
        }

        if (null !== $xmlRootPrefix = $elem->attributes()->{'xml-root-prefix'}) {
            $metadata->xmlRootPrefix = (string) $xmlRootPrefix;
        }

        $readOnlyClass = 'true' === strtolower((string) $elem->attributes()->{'read-only'});

        $discriminatorFieldName = (string) $elem->attributes()->{'discriminator-field-name'};
        $discriminatorMap = [];
        foreach ($elem->xpath('./discriminator-class') as $entry) {
            if (!isset($entry->attributes()->value)) {
                throw new InvalidMetadataException('Each discriminator-class element must have a "value" attribute.');
            }

            $discriminatorMap[(string) $entry->attributes()->value] = (string) $entry;
        }

        if ('true' === (string) $elem->attributes()->{'discriminator-disabled'}) {
            $metadata->discriminatorDisabled = true;
        } elseif (!empty($discriminatorFieldName) || !empty($discriminatorMap)) {
            $discriminatorGroups = [];
            foreach ($elem->xpath('./discriminator-groups/group') as $entry) {
                $discriminatorGroups[] = (string) $entry;
            }

            $metadata->setDiscriminator($discriminatorFieldName, $discriminatorMap, $discriminatorGroups);
        }

        foreach ($elem->xpath('./xml-namespace') as $xmlNamespace) {
            if (!isset($xmlNamespace->attributes()->uri)) {
                throw new InvalidMetadataException('The prefix attribute must be set for all xml-namespace elements.');
            }

            if (isset($xmlNamespace->attributes()->prefix)) {
                $prefix = (string) $xmlNamespace->attributes()->prefix;
            } else {
                $prefix = null;
            }

            $metadata->registerNamespace((string) $xmlNamespace->attributes()->uri, $prefix);
        }

        foreach ($elem->xpath('./xml-discriminator') as $xmlDiscriminator) {
            if (isset($xmlDiscriminator->attributes()->attribute)) {
                $metadata->xmlDiscriminatorAttribute = 'true' === (string) $xmlDiscriminator->attributes()->attribute;
            }

            if (isset($xmlDiscriminator->attributes()->cdata)) {
                $metadata->xmlDiscriminatorCData = 'true' === (string) $xmlDiscriminator->attributes()->cdata;
            }

            if (isset($xmlDiscriminator->attributes()->namespace)) {
                $metadata->xmlDiscriminatorNamespace = (string) $xmlDiscriminator->attributes()->namespace;
            }
        }

        foreach ($elem->xpath('./virtual-property') as $method) {
            if (isset($method->attributes()->expression)) {
                $virtualPropertyMetadata = new ExpressionPropertyMetadata(
                    $name,
                    (string) $method->attributes()->name,
                    $this->parseExpression((string) $method->attributes()->expression),
                );
            } else {
                if (!isset($method->attributes()->method)) {
                    throw new InvalidMetadataException('The method attribute must be set for all virtual-property elements.');
                }

                $virtualPropertyMetadata = new VirtualPropertyMetadata($name, (string) $method->attributes()->method);
            }

            $propertiesMetadata[] = $virtualPropertyMetadata;
            $propertiesNodes[] = $method;
        }

        if (!$excludeAll) {
            foreach ($class->getProperties() as $property) {
                if ($property->class !== $name || (isset($property->info) && $property->info['class'] !== $name)) {
                    continue;
                }

                $pName = $property->getName();
                $propertiesMetadata[] = new PropertyMetadata($name, $pName);

                $pElems = $elem->xpath("./property[@name = '" . $pName . "']");
                $propertiesNodes[] = $pElems ? reset($pElems) : null;
            }

            foreach ($propertiesMetadata as $propertyKey => $pMetadata) {
                $isExclude = false;
                $isExpose = $pMetadata instanceof VirtualPropertyMetadata
                    || $pMetadata instanceof ExpressionPropertyMetadata
                    || isset($propertiesNodes[$propertyKey]);

                $pElem = $propertiesNodes[$propertyKey];
                if (!empty($pElem)) {
                    if (null !== $exclude = $pElem->attributes()->exclude) {
                        $isExclude = 'true' === strtolower((string) $exclude);
                    }

                    if ($isExclude) {
                        continue;
                    }

                    if (null !== $expose = $pElem->attributes()->expose) {
                        $isExpose = 'true' === strtolower((string) $expose);
                    }

                    if (null !== $excludeIf = $pElem->attributes()->{'exclude-if'}) {
                        $pMetadata->excludeIf = $this->parseExpression((string) $excludeIf);
                    }

                    if (null !== $skip = $pElem->attributes()->{'skip-when-empty'}) {
                        $pMetadata->skipWhenEmpty = 'true' === strtolower((string) $skip);
                    }

                    if (null !== $excludeIf = $pElem->attributes()->{'expose-if'}) {
                        $pMetadata->excludeIf = $this->parseExpression('!(' . (string) $excludeIf . ')');
                        $isExpose = true;
                    }

                    if (null !== $version = $pElem->attributes()->{'since-version'}) {
                        $pMetadata->sinceVersion = (string) $version;
                    }

                    if (null !== $version = $pElem->attributes()->{'until-version'}) {
                        $pMetadata->untilVersion = (string) $version;
                    }

                    if (null !== $serializedName = $pElem->attributes()->{'serialized-name'}) {
                        $pMetadata->serializedName = (string) $serializedName;
                    }

                    if (null !== $type = $pElem->attributes()->type) {
                        $pMetadata->setType($this->typeParser->parse((string) $type));
                    } elseif (isset($pElem->type)) {
                        $pMetadata->setType($this->typeParser->parse((string) $pElem->type));
                    }

                    if (null !== $groups = $pElem->attributes()->groups) {
                        $pMetadata->groups = preg_split('/\s*,\s*/', trim((string) $groups));
                    } elseif (isset($pElem->groups)) {
                        $pMetadata->groups = (array) $pElem->groups->value;
                    }

                    if (isset($pElem->{'xml-list'})) {
                        $pMetadata->xmlCollection = true;

                        $colConfig = $pElem->{'xml-list'};
                        if (isset($colConfig->attributes()->inline)) {
                            $pMetadata->xmlCollectionInline = 'true' === (string) $colConfig->attributes()->inline;
                        }

                        if (isset($colConfig->attributes()->{'entry-name'})) {
                            $pMetadata->xmlEntryName = (string) $colConfig->attributes()->{'entry-name'};
                        }

                        if (isset($colConfig->attributes()->{'skip-when-empty'})) {
                            $pMetadata->xmlCollectionSkipWhenEmpty = 'true' === (string) $colConfig->attributes()->{'skip-when-empty'};
                        } else {
                            $pMetadata->xmlCollectionSkipWhenEmpty = true;
                        }

                        if (isset($colConfig->attributes()->namespace)) {
                            $pMetadata->xmlEntryNamespace = (string) $colConfig->attributes()->namespace;
                        }
                    }

                    if (isset($pElem->{'xml-map'})) {
                        $pMetadata->xmlCollection = true;

                        $colConfig = $pElem->{'xml-map'};
                        if (isset($colConfig->attributes()->inline)) {
                            $pMetadata->xmlCollectionInline = 'true' === (string) $colConfig->attributes()->inline;
                        }

                        if (isset($colConfig->attributes()->{'entry-name'})) {
                            $pMetadata->xmlEntryName = (string) $colConfig->attributes()->{'entry-name'};
                        }

                        if (isset($colConfig->attributes()->namespace)) {
                            $pMetadata->xmlEntryNamespace = (string) $colConfig->attributes()->namespace;
                        }

                        if (isset($colConfig->attributes()->{'key-attribute-name'})) {
                            $pMetadata->xmlKeyAttribute = (string) $colConfig->attributes()->{'key-attribute-name'};
                        }
                    }

                    if (isset($pElem->{'xml-element'})) {
                        $colConfig = $pElem->{'xml-element'};
                        if (isset($colConfig->attributes()->cdata)) {
                            $pMetadata->xmlElementCData = 'true' === (string) $colConfig->attributes()->cdata;
                        }

                        if (isset($colConfig->attributes()->namespace)) {
                            $pMetadata->xmlNamespace = (string) $colConfig->attributes()->namespace;
                        }
                    }

                    if (isset($pElem->attributes()->{'xml-attribute'})) {
                        $pMetadata->xmlAttribute = 'true' === (string) $pElem->attributes()->{'xml-attribute'};
                    }

                    if (isset($pElem->attributes()->{'xml-attribute-map'})) {
                        $pMetadata->xmlAttributeMap = 'true' === (string) $pElem->attributes()->{'xml-attribute-map'};
                    }

                    if (isset($pElem->attributes()->{'xml-value'})) {
                        $pMetadata->xmlValue = 'true' === (string) $pElem->attributes()->{'xml-value'};
                    }

                    if (isset($pElem->attributes()->{'xml-key-value-pairs'})) {
                        $pMetadata->xmlKeyValuePairs = 'true' === (string) $pElem->attributes()->{'xml-key-value-pairs'};
                    }

                    if (isset($pElem->attributes()->{'max-depth'})) {
                        $pMetadata->maxDepth = (int) $pElem->attributes()->{'max-depth'};
                    }

                    //we need read-only before setter and getter set, because that method depends on flag being set
                    if (null !== $readOnly = $pElem->attributes()->{'read-only'}) {
                        $pMetadata->readOnly = 'true' === strtolower((string) $readOnly);
                    } else {
                        $pMetadata->readOnly = $pMetadata->readOnly || $readOnlyClass;
                    }

                    if (isset($pElem->{'union-discriminator'})) {
                        $colConfig = $pElem->{'union-discriminator'};

                        $map = [];
                        foreach ($pElem->xpath('./union-discriminator/map/class') as $entry) {
                            $map[(string) $entry->attributes()->key] = (string) $entry;
                        }

                        $pMetadata->setType([
                            'name' => 'union',
                            'params' => [null, (string) $colConfig->attributes()->field, $map],
                        ]);
                    }

                    $getter = $pElem->attributes()->{'accessor-getter'};
                    $setter = $pElem->attributes()->{'accessor-setter'};
                    $pMetadata->setAccessor(
                        (string) ($pElem->attributes()->{'access-type'} ?: $classAccessType),
                        $getter ? (string) $getter : null,
                        $setter ? (string) $setter : null,
                    );

                    if (null !== $inline = $pElem->attributes()->inline) {
                        $pMetadata->inline = 'true' === strtolower((string) $inline);
                    }
                }

                if ($pMetadata->inline) {
                    $metadata->isList = $metadata->isList || PropertyMetadata::isCollectionList($pMetadata->type);
                    $metadata->isMap = $metadata->isMap || PropertyMetadata::isCollectionMap($pMetadata->type);
                }

                if (!$pMetadata->serializedName) {
                    $pMetadata->serializedName = $this->namingStrategy->translateName($pMetadata);
                }

                if (!empty($pElem) && null !== $name = $pElem->attributes()->name) {
                    $pMetadata->name = (string) $name;
                }

                if (
                    (ExclusionPolicy::NONE === (string) $exclusionPolicy && !$isExclude)
                    || (ExclusionPolicy::ALL === (string) $exclusionPolicy && $isExpose)
                ) {
                    $metadata->addPropertyMetadata($pMetadata);
                }
            }
        }

        foreach ($elem->xpath('./callback-method') as $method) {
            if (!isset($method->attributes()->type)) {
                throw new InvalidMetadataException('The type attribute must be set for all callback-method elements.');
            }

            if (!isset($method->attributes()->name)) {
                throw new InvalidMetadataException('The name attribute must be set for all callback-method elements.');
            }

            switch ((string) $method->attributes()->type) {
                case 'pre-serialize':
                    $metadata->addPreSerializeMethod(new MethodMetadata($class->name, (string) $method->attributes()->name));
                    break;

                case 'post-serialize':
                    $metadata->addPostSerializeMethod(new MethodMetadata($class->name, (string) $method->attributes()->name));
                    break;

                case 'post-deserialize':
                    $metadata->addPostDeserializeMethod(new MethodMetadata($class->name, (string) $method->attributes()->name));
                    break;

                case 'handler':
                    if (!isset($method->attributes()->format)) {
                        throw new InvalidMetadataException('The format attribute must be set for "handler" callback methods.');
                    }

                    if (!isset($method->attributes()->direction)) {
                        throw new InvalidMetadataException('The direction attribute must be set for "handler" callback methods.');
                    }

                    break;

                default:
                    throw new InvalidMetadataException(sprintf('The type "%s" is not supported.', $method->attributes()->name));
            }
        }

        return $metadata;
    }

    protected function getExtension(): string
    {
        return 'xml';
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Metadata\Driver;

use Doctrine\ODM\PHPCR\Mapping\ClassMetadata as PHPCRClassMetadata;
use Doctrine\Persistence\Mapping\ClassMetadata as DoctrineClassMetadata;
use JMS\Serializer\Metadata\PropertyMetadata;

/**
 * This class decorates any other driver. If the inner driver does not provide a
 * a property type, the decorator will guess based on Doctrine 2 metadata.
 */
class DoctrinePHPCRTypeDriver extends AbstractDoctrineTypeDriver
{
    /**
     * @param PHPCRClassMetadata $doctrineMetadata
     * @param PropertyMetadata $propertyMetadata
     */
    protected function hideProperty(DoctrineClassMetadata $doctrineMetadata, PropertyMetadata $propertyMetadata): bool
    {
        return 'lazyPropertiesDefaults' === $propertyMetadata->name
            || $doctrineMetadata->parentMapping === $propertyMetadata->name
            || $doctrineMetadata->node === $propertyMetadata->name;
    }

    /**
     * @param PHPCRClassMetadata $doctrineMetadata
     * @param PropertyMetadata $propertyMetadata
     */
    protected function setPropertyType(DoctrineClassMetadata $doctrineMetadata, PropertyMetadata $propertyMetadata): void
    {
        $propertyName = $propertyMetadata->name;
        if (
            $doctrineMetadata->hasField($propertyName)
            && ($typeOfFiled = $doctrineMetadata->getTypeOfField($propertyName))
            && ($fieldType = $this->normalizeFieldType($typeOfFiled))
        ) {
            $field = $doctrineMetadata->getFieldMapping($propertyName);
            if (!empty($field['multivalue'])) {
                $fieldType = 'array';
            }

            $propertyMetadata->setType($this->typeParser->parse($fieldType));
        } elseif ($doctrineMetadata->hasAssociation($propertyName)) {
            try {
                $targetEntity = $doctrineMetadata->getAssociationTargetClass($propertyName);
            } catch (\Throwable $e) {
                return;
            }

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

            if (!$doctrineMetadata->isSingleValuedAssociation($propertyName)) {
                $targetEntity = sprintf('ArrayCollection<%s>', $targetEntity);
            }

            $propertyMetadata->setType($this->typeParser->parse($targetEntity));
        }
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Metadata\Driver;

use Doctrine\Common\Annotations\Reader;
use JMS\Serializer\Expression\CompilableExpressionEvaluatorInterface;
use JMS\Serializer\Naming\PropertyNamingStrategyInterface;
use JMS\Serializer\Type\ParserInterface;

/**
 * @deprecated
 */
class AnnotationDriver extends AnnotationOrAttributeDriver
{
    /**
     * @var Reader
     */
    private $reader;

    public function __construct(Reader $reader, PropertyNamingStrategyInterface $namingStrategy, ?ParserInterface $typeParser = null, ?CompilableExpressionEvaluatorInterface $expressionEvaluator = null)
    {
        parent::__construct($namingStrategy, $typeParser, $expressionEvaluator, $reader);

        $this->reader = $reader;
    }

    /**
     * @return list<object>
     */
    protected function getClassAnnotations(\ReflectionClass $class): array
    {
        return $this->reader->getClassAnnotations($class);
    }

    /**
     * @return list<object>
     */
    protected function getMethodAnnotations(\ReflectionMethod $method): array
    {
        return $this->reader->getMethodAnnotations($method);
    }

    /**
     * @return list<object>
     */
    protected function getPropertyAnnotations(\ReflectionProperty $property): array
    {
        return $this->reader->getPropertyAnnotations($property);
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Metadata\Driver;

use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\Metadata\PropertyMetadata;
use JMS\Serializer\Naming\PropertyNamingStrategyInterface;
use Metadata\ClassMetadata as BaseClassMetadata;
use Metadata\Driver\DriverInterface;

class NullDriver implements DriverInterface
{
    /**
     * @var PropertyNamingStrategyInterface
     */
    private $namingStrategy;

    public function __construct(PropertyNamingStrategyInterface $namingStrategy)
    {
        $this->namingStrategy = $namingStrategy;
    }

    public function loadMetadataForClass(\ReflectionClass $class): ?BaseClassMetadata
    {
        $classMetadata = new ClassMetadata($name = $class->name);
        $fileResource =  $class->getFilename();
        if (false !== $fileResource) {
            $classMetadata->fileResources[] = $fileResource;
        }

        foreach ($class->getProperties() as $property) {
            if ($property->class !== $name || (isset($property->info) && $property->info['class'] !== $name)) {
                continue;
            }

            $propertyMetadata = new PropertyMetadata($name, $property->getName());

            if (!$propertyMetadata->serializedName) {
                $propertyMetadata->serializedName = $this->namingStrategy->translateName($propertyMetadata);
            }

            $classMetadata->addPropertyMetadata($propertyMetadata);
        }

        return $classMetadata;
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Metadata;

class StaticPropertyMetadata extends PropertyMetadata
{
    /**
     * @var mixed
     */
    private $value;

    /**
     * StaticPropertyMetadata constructor.
     *
     * @param mixed $fieldValue
     * @param array $groups
     */
    public function __construct(string $className, string $fieldName, $fieldValue, array $groups = [])
    {
        $this->class = $className;
        $this->name = $fieldName;
        $this->serializedName = $fieldName;
        $this->value = $fieldValue;
        $this->readOnly = true;
        $this->groups = $groups;
    }

    /**
     * @return mixed
     */
    public function getValue()
    {
        return $this->value;
    }

    public function setAccessor(string $type, ?string $getter = null, ?string $setter = null): void
    {
    }

    protected function serializeToArray(): array
    {
        return [
            $this->value,
            parent::serializeToArray(),
        ];
    }

    protected function unserializeFromArray(array $data): void
    {
        [
            $this->value,
            $parentData,
        ] = $data;

        parent::unserializeFromArray($parentData);
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer;

use JMS\Serializer\Exception\NonFloatCastableTypeException;
use JMS\Serializer\Exception\NonIntCastableTypeException;
use JMS\Serializer\Exception\NonStringCastableTypeException;
use JMS\Serializer\Type\Type;

/**
 * @internal
 *
 * @phpstan-import-type TypeArray from Type
 */
abstract class AbstractVisitor implements VisitorInterface
{
    /**
     * @var GraphNavigatorInterface|null
     */
    protected $navigator;

    public function setNavigator(GraphNavigatorInterface $navigator): void
    {
        $this->navigator = $navigator;
    }

    /**
     * {@inheritdoc}
     */
    public function prepare($data)
    {
        return $data;
    }

    /**
     * @param TypeArray $typeArray
     */
    protected function getElementType(array $typeArray): ?array
    {
        if (false === isset($typeArray['params'][0])) {
            return null;
        }

        if (isset($typeArray['params'][1]) && \is_array($typeArray['params'][1])) {
            return $typeArray['params'][1];
        } else {
            return $typeArray['params'][0];
        }
    }

    /**
     * logic according to strval https://www.php.net/manual/en/function.strval.php
     * "You cannot use strval() on arrays or on objects that do not implement the __toString() method."
     *
     * @param mixed $value
     */
    protected function assertValueCanBeCastToString($value): void
    {
        if (is_array($value)) {
            throw new NonStringCastableTypeException($value);
        }

        if (is_object($value) && !method_exists($value, '__toString')) {
            throw new NonStringCastableTypeException($value);
        }
    }

    /**
     * logic according to intval https://www.php.net/manual/en/function.intval.php
     * "intval() should not be used on objects, as doing so will emit an E_NOTICE level error and return 1."
     *
     * @param mixed $value
     */
    protected function assertValueCanBeCastToInt($value): void
    {
        if (is_object($value) && !$value instanceof \SimpleXMLElement) {
            throw new NonIntCastableTypeException($value);
        }
    }

    /**
     *  logic according to floatval https://www.php.net/manual/en/function.floatval.php
     * "floatval() should not be used on objects, as doing so will emit an E_NOTICE level error and return 1."
     *
     * @param mixed $value
     */
    protected function assertValueCanCastToFloat($value): void
    {
        if (is_object($value) && !$value instanceof \SimpleXMLElement) {
            throw new NonFloatCastableTypeException($value);
        }
    }

    protected function mapRoundMode(?string $roundMode = null): int
    {
        switch ($roundMode) {
            case 'HALF_DOWN':
                $roundMode = PHP_ROUND_HALF_DOWN;
                break;
            case 'HALF_EVEN':
                $roundMode = PHP_ROUND_HALF_EVEN;
                break;
            case 'HALF_ODD':
                $roundMode = PHP_ROUND_HALF_ODD;
                break;
            case 'HALF_UP':
            default:
                $roundMode = PHP_ROUND_HALF_UP;
        }

        return $roundMode;
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer;

use JMS\Serializer\Exception\LogicException;
use JMS\Serializer\Exception\NotAcceptableException;
use JMS\Serializer\Exception\RuntimeException;
use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\Metadata\PropertyMetadata;
use JMS\Serializer\Visitor\DeserializationVisitorInterface;

final class JsonDeserializationVisitor extends AbstractVisitor implements DeserializationVisitorInterface
{
    /**
     * @var int
     */
    private $options;

    /**
     * @var int
     */
    private $depth;

    /**
     * @var \SplStack
     */
    private $objectStack;

    /**
     * @var object|null
     */
    private $currentObject;

    public function __construct(
        int $options = 0,
        int $depth = 512
    ) {
        $this->objectStack = new \SplStack();
        $this->options = $options;
        $this->depth = $depth;
    }

    /**
     * {@inheritdoc}
     */
    public function visitNull($data, array $type)
    {
        return null;
    }

    /**
     * {@inheritdoc}
     */
    public function visitString($data, array $type): string
    {
        $this->assertValueCanBeCastToString($data);

        return (string) $data;
    }

    /**
     * {@inheritdoc}
     */
    public function visitBoolean($data, array $type): bool
    {
        return (bool) $data;
    }

    /**
     * {@inheritdoc}
     */
    public function visitInteger($data, array $type): int
    {
        $this->assertValueCanBeCastToInt($data);

        return (int) $data;
    }

    /**
     * {@inheritdoc}
     */
    public function visitDouble($data, array $type): float
    {
        $this->assertValueCanCastToFloat($data);

        return (float) $data;
    }

    /**
     * {@inheritdoc}
     */
    public function visitArray($data, array $type): array
    {
        if (!\is_array($data)) {
            throw new RuntimeException(sprintf('Expected array, but got %s: %s', \gettype($data), json_encode($data)));
        }

        // If no further parameters were given, keys/values are just passed as is.
        if (!$type['params']) {
            return $data;
        }

        switch (\count($type['params'])) {
            case 1: // Array is a list.
                $listType = $type['params'][0];

                $result = [];

                foreach ($data as $v) {
                    $result[] = $this->navigator->accept($v, $listType);
                }

                return $result;

            case 2: // Array is a map.
                [$keyType, $entryType] = $type['params'];

                $result = [];

                foreach ($data as $k => $v) {
                    $result[$this->navigator->accept($k, $keyType)] = $this->navigator->accept($v, $entryType);
                }

                return $result;

            default:
                throw new RuntimeException(sprintf('Array type cannot have more than 2 parameters, but got %s.', json_encode($type['params'])));
        }
    }

    /**
     * {@inheritdoc}
     */
    public function visitDiscriminatorMapProperty($data, ClassMetadata $metadata): string
    {
        if (isset($data[$metadata->discriminatorFieldName])) {
            return (string) $data[$metadata->discriminatorFieldName];
        }

        throw new LogicException(sprintf(
            'The discriminator field name "%s" for base-class "%s" was not found in input data.',
            $metadata->discriminatorFieldName,
            $metadata->name,
        ));
    }

    /**
     * {@inheritdoc}
     */
    public function startVisitingObject(ClassMetadata $metadata, object $object, array $type): void
    {
        $this->setCurrentObject($object);
    }

    /**
     * {@inheritdoc}
     */
    public function visitProperty(PropertyMetadata $metadata, $data)
    {
        $name = $metadata->serializedName;

        if (null === $data) {
            return;
        }

        if (!\is_array($data)) {
            throw new RuntimeException(sprintf('Invalid data %s (%s), expected "%s".', json_encode($data), $metadata->type['name'], $metadata->class));
        }

        if (true === $metadata->inline) {
            if (!$metadata->type) {
                throw RuntimeException::noMetadataForProperty($metadata->class, $metadata->name);
            }

            return $this->navigator->accept($data, $metadata->type);
        }

        if (!array_key_exists($name, $data)) {
            throw new NotAcceptableException();
        }

        if (!$metadata->type) {
            throw RuntimeException::noMetadataForProperty($metadata->class, $metadata->name);
        }

        return null !== $data[$name] ? $this->navigator->accept($data[$name], $metadata->type) : null;
    }

    /**
     * {@inheritdoc}
     */
    public function endVisitingObject(ClassMetadata $metadata, $data, array $type): object
    {
        $obj = $this->currentObject;
        $this->revertCurrentObject();

        return $obj;
    }

    /**
     * {@inheritdoc}
     */
    public function getResult($data)
    {
        $this->navigator = null;

        return $data;
    }

    public function setCurrentObject(object $object): void
    {
        $this->objectStack->push($this->currentObject);
        $this->currentObject = $object;
    }

    public function getCurrentObject(): ?object
    {
        return $this->currentObject;
    }

    public function revertCurrentObject(): ?object
    {
        return $this->currentObject = $this->objectStack->pop();
    }

    /**
     * {@inheritdoc}
     */
    public function prepare($str)
    {
        $decoded = json_decode($str, true, $this->depth, $this->options);

        switch (json_last_error()) {
            case JSON_ERROR_NONE:
                return $decoded;

            case JSON_ERROR_DEPTH:
                throw new RuntimeException('Could not decode JSON, maximum stack depth exceeded.');

            case JSON_ERROR_STATE_MISMATCH:
                throw new RuntimeException('Could not decode JSON, underflow or the nodes mismatch.');

            case JSON_ERROR_CTRL_CHAR:
                throw new RuntimeException('Could not decode JSON, unexpected control character found.');

            case JSON_ERROR_SYNTAX:
                throw new RuntimeException('Could not decode JSON, syntax error - malformed JSON.');

            case JSON_ERROR_UTF8:
                throw new RuntimeException('Could not decode JSON, malformed UTF-8 characters (incorrectly encoded?)');

            default:
                throw new RuntimeException('Could not decode JSON.');
        }
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Construction;

use Doctrine\Instantiator\Instantiator;
use JMS\Serializer\DeserializationContext;
use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\Visitor\DeserializationVisitorInterface;

final class UnserializeObjectConstructor implements ObjectConstructorInterface
{
    /** @var Instantiator */
    private $instantiator;

    /**
     * {@inheritdoc}
     */
    public function construct(DeserializationVisitorInterface $visitor, ClassMetadata $metadata, $data, array $type, DeserializationContext $context): ?object
    {
        return $this->getInstantiator()->instantiate($metadata->name);
    }

    private function getInstantiator(): Instantiator
    {
        if (null === $this->instantiator) {
            $this->instantiator = new Instantiator();
        }

        return $this->instantiator;
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Construction;

use JMS\Serializer\DeserializationContext;
use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\Type\Type;
use JMS\Serializer\Visitor\DeserializationVisitorInterface;

/**
 * Implementations of this interface construct new objects during deserialization.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 *
 * @phpstan-import-type TypeArray from Type
 */
interface ObjectConstructorInterface
{
    /**
     * Constructs a new object.
     *
     * Implementations could for example create a new object calling "new", use
     * "unserialize" techniques, reflection, or other means.
     *
     * @param mixed $data
     * @param TypeArray $type
     */
    public function construct(DeserializationVisitorInterface $visitor, ClassMetadata $metadata, $data, array $type, DeserializationContext $context): ?object;
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Construction;

use Doctrine\ODM\PHPCR\DocumentManagerInterface;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Persistence\ManagerRegistry;
use JMS\Serializer\DeserializationContext;
use JMS\Serializer\Exception\InvalidArgumentException;
use JMS\Serializer\Exception\ObjectConstructionException;
use JMS\Serializer\Exclusion\ExpressionLanguageExclusionStrategy;
use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\Metadata\PropertyMetadata;
use JMS\Serializer\Visitor\DeserializationVisitorInterface;

use function is_array;

/**
 * Doctrine object constructor for new (or existing) objects during deserialization.
 */
final class DoctrineObjectConstructor implements ObjectConstructorInterface
{
    public const ON_MISSING_NULL = 'null';
    public const ON_MISSING_EXCEPTION = 'exception';
    public const ON_MISSING_FALLBACK = 'fallback';
    /**
     * @var string
     */
    private $fallbackStrategy;

    /**
     * @var ManagerRegistry
     */
    private $managerRegistry;

    /**
     * @var ObjectConstructorInterface
     */
    private $fallbackConstructor;

    /**
     * @var ExpressionLanguageExclusionStrategy|null
     */
    private $expressionLanguageExclusionStrategy;

    /**
     * @param ManagerRegistry $managerRegistry     Manager registry
     * @param ObjectConstructorInterface $fallbackConstructor Fallback object constructor
     */
    public function __construct(
        ManagerRegistry $managerRegistry,
        ObjectConstructorInterface $fallbackConstructor,
        string $fallbackStrategy = self::ON_MISSING_NULL,
        ?ExpressionLanguageExclusionStrategy $expressionLanguageExclusionStrategy = null
    ) {
        $this->managerRegistry = $managerRegistry;
        $this->fallbackConstructor = $fallbackConstructor;
        $this->fallbackStrategy = $fallbackStrategy;
        $this->expressionLanguageExclusionStrategy = $expressionLanguageExclusionStrategy;
    }

    /**
     * {@inheritdoc}
     */
    public function construct(DeserializationVisitorInterface $visitor, ClassMetadata $metadata, $data, array $type, DeserializationContext $context): ?object
    {
        $objectManager = $this->managerRegistry->getManagerForClass($metadata->name);

        if (!$objectManager) {
            // No ObjectManager found, proceed with normal deserialization
            return $this->fallbackConstructor->construct($visitor, $metadata, $data, $type, $context);
        }

        // Locate possible ClassMetadata
        $classMetadataFactory = $objectManager->getMetadataFactory();

        if ($classMetadataFactory->isTransient($metadata->name)) {
            // No ClassMetadata found, proceed with normal deserialization
            return $this->fallbackConstructor->construct($visitor, $metadata, $data, $type, $context);
        }

        // Managed entity, check for proxy load
        if (!is_array($data) && !(is_object($data) && 'SimpleXMLElement' === get_class($data))) {
            \assert($objectManager instanceof EntityManagerInterface || $objectManager instanceof DocumentManagerInterface);

            // Single identifier, load proxy
            return $objectManager->getReference($metadata->name, $data);
        }

        // Fallback to default constructor if missing identifier(s)
        $classMetadata = $objectManager->getClassMetadata($metadata->name);
        $identifierList = [];

        foreach ($classMetadata->getIdentifierFieldNames() as $name) {
            // Avoid calling objectManager->find if some identification properties are excluded
            if (!isset($metadata->propertyMetadata[$name])) {
                return $this->fallbackConstructor->construct($visitor, $metadata, $data, $type, $context);
            }

            $propertyMetadata = $metadata->propertyMetadata[$name];

            // Avoid calling objectManager->find if some identification properties are excluded by some exclusion strategy
            if ($this->isIdentifierFieldExcluded($propertyMetadata, $context)) {
                return $this->fallbackConstructor->construct($visitor, $metadata, $data, $type, $context);
            }

            if (is_array($data) && !array_key_exists($propertyMetadata->serializedName, $data)) {
                return $this->fallbackConstructor->construct($visitor, $metadata, $data, $type, $context);
            } elseif (is_object($data) && !property_exists($data, $propertyMetadata->serializedName)) {
                return $this->fallbackConstructor->construct($visitor, $metadata, $data, $type, $context);
            }

            if (is_object($data) && 'SimpleXMLElement' === get_class($data)) {
                $identifierList[$name] = (string) $data->{$propertyMetadata->serializedName};
            } else {
                $identifierList[$name] = $data[$propertyMetadata->serializedName];
            }
        }

        if (empty($identifierList)) {
            // $classMetadataFactory->isTransient() fails on embeddable class with file metadata driver
            // https://github.com/doctrine/persistence/issues/37
            return $this->fallbackConstructor->construct($visitor, $metadata, $data, $type, $context);
        }

        // Entity update, load it from database
        $object = $objectManager->find($metadata->name, $identifierList);

        if (null === $object) {
            switch ($this->fallbackStrategy) {
                case self::ON_MISSING_NULL:
                    return null;

                case self::ON_MISSING_EXCEPTION:
                    throw new ObjectConstructionException(sprintf('Entity %s can not be found', $metadata->name));

                case self::ON_MISSING_FALLBACK:
                    return $this->fallbackConstructor->construct($visitor, $metadata, $data, $type, $context);

                default:
                    throw new InvalidArgumentException('The provided fallback strategy for the object constructor is not valid');
            }
        }

        $objectManager->initializeObject($object);

        return $object;
    }

    private function isIdentifierFieldExcluded(PropertyMetadata $propertyMetadata, DeserializationContext $context): bool
    {
        $exclusionStrategy = $context->getExclusionStrategy();
        if (null !== $exclusionStrategy && $exclusionStrategy->shouldSkipProperty($propertyMetadata, $context)) {
            return true;
        }

        return null !== $this->expressionLanguageExclusionStrategy && $this->expressionLanguageExclusionStrategy->shouldSkipProperty($propertyMetadata, $context);
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer;

use JMS\Serializer\Exception\NotAcceptableException;
use JMS\Serializer\Type\Type;

/**
 * @phpstan-import-type TypeArray from Type
 */
interface GraphNavigatorInterface
{
    public const DIRECTION_SERIALIZATION = 1;
    public const DIRECTION_DESERIALIZATION = 2;

    /**
     * Called at the beginning of the serialization process. The navigator should use the traverse the object graph
     * and pass to the $visitor the value of found nodes (following the rules obtained from $context).
     */
    public function initialize(VisitorInterface $visitor, Context $context): void;

    /**
     * Called for each node of the graph that is being traversed.
     *
     * @param mixed $data the data depends on the direction, and type of visitor
     * @param TypeArray|null $type array has the format ["name" => string, "params" => array]
     *
     * @return mixed the return value depends on the direction, and type of visitor
     *
     * @throws NotAcceptableException
     */
    public function accept($data, ?array $type = null);
}
<?php

declare(strict_types=1);

namespace JMS\Serializer;

use JMS\Serializer\Exception\NotAcceptableException;
use JMS\Serializer\Exception\RuntimeException;
use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\Metadata\PropertyMetadata;
use JMS\Serializer\Visitor\SerializationVisitorInterface;

final class JsonSerializationVisitor extends AbstractVisitor implements SerializationVisitorInterface
{
    /**
     * @var int
     */
    private $options;

    /**
     * @var array
     */
    private $dataStack;
    /**
     * @var \ArrayObject|array
     */
    private $data;

    public function __construct(
        int $options = JSON_PRESERVE_ZERO_FRACTION
    ) {
        $this->dataStack = [];
        $this->options = $options;
    }

    /**
     * {@inheritdoc}
     */
    public function visitNull($data, array $type)
    {
        return null;
    }

    /**
     * {@inheritdoc}
     */
    public function visitString(string $data, array $type)
    {
        return $data;
    }

    /**
     * {@inheritdoc}
     */
    public function visitBoolean(bool $data, array $type)
    {
        return $data;
    }

    /**
     * {@inheritdoc}
     */
    public function visitInteger(int $data, array $type)
    {
        return $data;
    }

    /**
     * {@inheritdoc}
     */
    public function visitDouble(float $data, array $type)
    {
        $precision = $type['params'][0] ?? null;
        if (!is_int($precision)) {
            return $data;
        }

        $roundMode = $type['params'][1] ?? null;
        $roundMode = $this->mapRoundMode($roundMode);

        return round($data, $precision, $roundMode);
    }

    public function visitArray(array $data, array $type)
    {
        \array_push($this->dataStack, $data);

        $rs = isset($type['params'][1]) ? new \ArrayObject() : [];

        $isList = isset($type['params'][0]) && !isset($type['params'][1]);

        $elType = $this->getElementType($type);
        foreach ($data as $k => $v) {
            try {
                $v = $this->navigator->accept($v, $elType);
            } catch (NotAcceptableException $e) {
                continue;
            }

            if ($isList) {
                $rs[] = $v;
            } else {
                $rs[$k] = $v;
            }
        }

        \array_pop($this->dataStack);

        return $rs;
    }

    public function startVisitingObject(ClassMetadata $metadata, object $data, array $type): void
    {
        \array_push($this->dataStack, $this->data);
        $this->data = true === $metadata->isMap ? new \ArrayObject() : [];
    }

    /**
     * @return array|\ArrayObject
     */
    public function endVisitingObject(ClassMetadata $metadata, object $data, array $type)
    {
        $rs = $this->data;
        $this->data = \array_pop($this->dataStack);

        if (true !== $metadata->isList && empty($rs)) {
            return new \ArrayObject();
        }

        return $rs;
    }

    /**
     * {@inheritdoc}
     */
    public function visitProperty(PropertyMetadata $metadata, $v): void
    {
        try {
            $v = $this->navigator->accept($v, $metadata->type);
        } catch (NotAcceptableException $e) {
            return;
        }

        if (true === $metadata->skipWhenEmpty && ($v instanceof \ArrayObject || \is_array($v)) && 0 === count($v)) {
            return;
        }

        if ($metadata->inline) {
            if (\is_array($v) || ($v instanceof \ArrayObject)) {
                // concatenate the two array-like structures
                // is there anything faster?
                foreach ($v as $key => $value) {
                    $this->data[$key] = $value;
                }
            }
        } else {
            $this->data[$metadata->serializedName] = $v;
        }
    }

    /**
     * Checks if some data key exists.
     */
    public function hasData(string $key): bool
    {
        return isset($this->data[$key]);
    }

    /**
     * @deprecated Use `::visitProperty(new StaticPropertyMetadata('', 'name', 'value'), 'value')` instead
     *
     * Allows you to replace existing data on the current object element.
     *
     * @param mixed $value This value must either be a regular scalar, or an array.
     *                                                       It must not contain any objects anymore.
     */
    public function setData(string $key, $value): void
    {
        $this->data[$key] = $value;
    }

    /**
     * {@inheritdoc}
     */
    public function getResult($data)
    {
        $this->navigator = null;

        $result = @json_encode($data, $this->options);

        switch (json_last_error()) {
            case JSON_ERROR_NONE:
                return $result;

            case JSON_ERROR_UTF8:
                throw new RuntimeException('Your data could not be encoded because it contains invalid UTF8 characters.');

            default:
                throw new RuntimeException(sprintf('An error occurred while encoding your data (error code %d).', json_last_error()));
        }
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\EventDispatcher;

use JMS\Serializer\DeserializationContext;
use JMS\Serializer\Type\Type;

/**
 * @phpstan-import-type TypeArray from Type
 */
class PreDeserializeEvent extends Event
{
    /**
     * @var mixed
     */
    private $data;

    /**
     * @param mixed $data
     * @param TypeArray $type
     */
    public function __construct(DeserializationContext $context, $data, array $type)
    {
        parent::__construct($context, $type);

        $this->data = $data;
    }

    public function setType(string $name, array $params = []): void
    {
        $this->type = ['name' => $name, 'params' => $params];
    }

    /**
     * @return mixed
     */
    public function getData()
    {
        return $this->data;
    }

    /**
     * @param mixed $data
     */
    public function setData($data): void
    {
        $this->data = $data;
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\EventDispatcher;

abstract class Events
{
    public const PRE_SERIALIZE = 'serializer.pre_serialize';
    public const POST_SERIALIZE = 'serializer.post_serialize';
    public const PRE_DESERIALIZE = 'serializer.pre_deserialize';
    public const POST_DESERIALIZE = 'serializer.post_deserialize';

    final private function __construct()
    {
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\EventDispatcher;

use JMS\Serializer\Exception\InvalidArgumentException;
use Psr\Container\ContainerInterface as PsrContainerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

class LazyEventDispatcher extends EventDispatcher
{
    /**
     * @var PsrContainerInterface|ContainerInterface
     */
    private $container;

    /**
     * @param PsrContainerInterface|ContainerInterface $container
     */
    public function __construct($container)
    {
        if (!$container instanceof PsrContainerInterface && !$container instanceof ContainerInterface) {
            throw new InvalidArgumentException(sprintf('The container must be an instance of %s or %s (%s given).', PsrContainerInterface::class, ContainerInterface::class, \is_object($container) ? \get_class($container) : \gettype($container)));
        }

        $this->container = $container;
    }

    /**
     * {@inheritdoc}
     */
    protected function initializeListeners(string $eventName, string $loweredClass, string $format): array
    {
        $listeners = parent::initializeListeners($eventName, $loweredClass, $format);

        foreach ($listeners as &$listener) {
            if (!\is_array($listener[0]) || !\is_string($listener[0][0])) {
                continue;
            }

            if (!$this->container->has($listener[0][0])) {
                continue;
            }

            $listener[0][0] = $this->container->get($listener[0][0]);
        }

        return $listeners;
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\EventDispatcher;

use JMS\Serializer\Exception\InvalidArgumentException;

/**
 * Light-weight event dispatcher.
 *
 * This implementation focuses primarily on performance, and dispatching
 * events for certain classes. It is not a general purpose event dispatcher.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class EventDispatcher implements EventDispatcherInterface
{
    /**
     * @var array
     */
    private $listeners = [];

    /**
     * ClassListeners cache
     *
     * @var array
     */
    private $classListeners = [];

    public static function getDefaultMethodName(string $eventName): string
    {
        return 'on' . str_replace(['_', '.'], '', $eventName);
    }

    /**
     * Sets the listeners.
     *
     * @param array $listeners
     */
    public function setListeners(array $listeners): void
    {
        $this->listeners = $listeners;
        $this->classListeners = [];
    }

    /**
     * @internal Used for profiling
     */
    public function getListeners(): array
    {
        return $this->listeners;
    }

    /**
     * {@inheritdoc}
     */
    public function addListener(string $eventName, $callable, ?string $class = null, ?string $format = null, ?string $interface = null): void
    {
        $this->listeners[$eventName][] = [$callable, $class, $format, $interface];
        unset($this->classListeners[$eventName]);
    }

    public function addSubscriber(EventSubscriberInterface $subscriber): void
    {
        foreach ($subscriber->getSubscribedEvents() as $eventData) {
            if (!isset($eventData['event'])) {
                throw new InvalidArgumentException(sprintf('Each event must have a "event" key.'));
            }

            $method = $eventData['method'] ?? self::getDefaultMethodName($eventData['event']);
            $class = $eventData['class'] ?? null;
            $format = $eventData['format'] ?? null;
            $interface = $eventData['interface'] ?? null;
            $this->listeners[$eventData['event']][] = [[$subscriber, $method], $class, $format, $interface];
            unset($this->classListeners[$eventData['event']]);
        }
    }

    public function hasListeners(string $eventName, string $class, string $format): bool
    {
        if (!isset($this->listeners[$eventName])) {
            return false;
        }

        if (!isset($this->classListeners[$eventName][$class][$format])) {
            $this->classListeners[$eventName][$class][$format] = $this->initializeListeners($eventName, $class, $format);
        }

        return !!$this->classListeners[$eventName][$class][$format];
    }

    public function dispatch(string $eventName, string $class, string $format, Event $event): void
    {
        if (!isset($this->listeners[$eventName])) {
            return;
        }

        $object = $event instanceof ObjectEvent ? $event->getObject() : null;
        $realClass = is_object($object) ? get_class($object) : '';
        $objectClass = $realClass !== $class ? $realClass . $class : $class;

        if (!isset($this->classListeners[$eventName][$objectClass][$format])) {
            $this->classListeners[$eventName][$objectClass][$format] = $this->initializeListeners($eventName, $class, $format);
        }

        foreach ($this->classListeners[$eventName][$objectClass][$format] as $listener) {
            if (!empty($listener[3]) && !($object instanceof $listener[3])) {
                continue;
            }

            \call_user_func($listener[0], $event, $eventName, $class, $format, $this);

            if ($event->isPropagationStopped()) {
                break;
            }
        }
    }

    /**
     * @return array An array of listeners
     */
    protected function initializeListeners(string $eventName, string $loweredClass, string $format): array
    {
        $listeners = [];
        foreach ($this->listeners[$eventName] as $listener) {
            if (null !== $listener[1] && $loweredClass !== $listener[1]) {
                continue;
            }

            if (null !== $listener[2] && $format !== $listener[2]) {
                continue;
            }

            $listeners[] = $listener;
        }

        return $listeners;
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\EventDispatcher\Subscriber;

use JMS\Serializer\EventDispatcher\EventSubscriberInterface;
use JMS\Serializer\EventDispatcher\PreSerializeEvent;

final class EnumSubscriber implements EventSubscriberInterface
{
    public function onPreSerializeEnum(PreSerializeEvent $event): void
    {
        $type = $event->getType();

        if (isset($type['name']) && ('enum' === $type['name'] || !is_a($type['name'], \UnitEnum::class, true))) {
            return;
        }

        $object = $event->getObject();
        $params = [get_class($object), $object instanceof \BackedEnum ? 'value' : 'name'];
        $event->setType('enum', $params);
    }

    /**
     * {@inheritdoc}
     */
    public static function getSubscribedEvents()
    {
        return [
            ['event' => 'serializer.pre_serialize', 'method' => 'onPreSerializeEnum', 'interface' => \UnitEnum::class],
        ];
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\EventDispatcher\Subscriber;

use Doctrine\Common\Persistence\Proxy as LegacyProxy;
use Doctrine\ODM\MongoDB\PersistentCollection as MongoDBPersistentCollection;
use Doctrine\ODM\PHPCR\PersistentCollection as PHPCRPersistentCollection;
use Doctrine\ORM\PersistentCollection;
use Doctrine\Persistence\Proxy;
use JMS\Serializer\EventDispatcher\EventDispatcherInterface;
use JMS\Serializer\EventDispatcher\EventSubscriberInterface;
use JMS\Serializer\EventDispatcher\PreSerializeEvent;
use ProxyManager\Proxy\LazyLoadingInterface;

final class DoctrineProxySubscriber implements EventSubscriberInterface
{
    /**
     * @var bool
     */
    private $skipVirtualTypeInit;

    /**
     * @var bool
     */
    private $initializeExcluded;

    public function __construct(bool $skipVirtualTypeInit = true, bool $initializeExcluded = false)
    {
        $this->skipVirtualTypeInit = $skipVirtualTypeInit;
        $this->initializeExcluded = $initializeExcluded;
    }

    public function onPreSerialize(PreSerializeEvent $event): void
    {
        $object = $event->getObject();
        $type = $event->getType();

        // If the set type name is not an actual class, but a faked type for which a custom handler exists, we do not
        // modify it with this subscriber. Also, we forgo autoloading here as an instance of this type is already created,
        // so it must be loaded if its a real class.
        $virtualType = !class_exists($type['name'], false);

        if (
            $object instanceof PersistentCollection
            || $object instanceof MongoDBPersistentCollection
            || $object instanceof PHPCRPersistentCollection
        ) {
            if (!$virtualType) {
                $event->setType('ArrayCollection');
            }

            return;
        }

        if (
            ($this->skipVirtualTypeInit && $virtualType) ||
            (!$object instanceof Proxy && !$object instanceof LazyLoadingInterface)
        ) {
            return;
        }

        // do not initialize the proxy if is going to be excluded by-class by some exclusion strategy
        if (false === $this->initializeExcluded && !$virtualType) {
            $context = $event->getContext();
            $exclusionStrategy = $context->getExclusionStrategy();
            $metadata = $context->getMetadataFactory()->getMetadataForClass(get_parent_class($object));
            if (null !== $metadata && null !== $exclusionStrategy && $exclusionStrategy->shouldSkipClass($metadata, $context)) {
                return;
            }
        }

        if ($object instanceof LazyLoadingInterface) {
            $object->initializeProxy();
        } else {
            $object->__load();
        }

        if (!$virtualType) {
            $event->setType(get_parent_class($object), $type['params']);
        }
    }

    public function onPreSerializeTypedProxy(PreSerializeEvent $event, string $eventName, string $class, string $format, EventDispatcherInterface $dispatcher): void
    {
        $type = $event->getType();
        // is a virtual type? then there is no need to change the event name
        if (!class_exists($type['name'], false)) {
            return;
        }

        $object = $event->getObject();
        if ($object instanceof Proxy) {
            $parentClassName = get_parent_class($object);

            // check if this is already a re-dispatch
            if (strtolower($class) !== strtolower($parentClassName)) {
                $event->stopPropagation();
                $newEvent = new PreSerializeEvent($event->getContext(), $object, ['name' => $parentClassName, 'params' => $type['params']]);
                $dispatcher->dispatch($eventName, $parentClassName, $format, $newEvent);

                // update the type in case some listener changed it
                $newType = $newEvent->getType();
                $event->setType($newType['name'], $newType['params']);
            }
        }
    }

    /**
     * {@inheritdoc}
     */
    public static function getSubscribedEvents()
    {
        return [
            ['event' => 'serializer.pre_serialize', 'method' => 'onPreSerializeTypedProxy', 'interface' => Proxy::class],
            ['event' => 'serializer.pre_serialize', 'method' => 'onPreSerializeTypedProxy', 'interface' => LegacyProxy::class],
            ['event' => 'serializer.pre_serialize', 'method' => 'onPreSerialize', 'interface' => PersistentCollection::class],
            ['event' => 'serializer.pre_serialize', 'method' => 'onPreSerialize', 'interface' => MongoDBPersistentCollection::class],
            ['event' => 'serializer.pre_serialize', 'method' => 'onPreSerialize', 'interface' => PHPCRPersistentCollection::class],
            ['event' => 'serializer.pre_serialize', 'method' => 'onPreSerialize', 'interface' => Proxy::class],
            ['event' => 'serializer.pre_serialize', 'method' => 'onPreSerialize', 'interface' => LegacyProxy::class],
            ['event' => 'serializer.pre_serialize', 'method' => 'onPreSerialize', 'interface' => LazyLoadingInterface::class],
        ];
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\EventDispatcher\Subscriber;

use JMS\Serializer\EventDispatcher\EventSubscriberInterface;
use JMS\Serializer\EventDispatcher\ObjectEvent;
use JMS\Serializer\Exception\ValidationFailedException;
use Symfony\Component\Validator\Validator\ValidatorInterface;

final class SymfonyValidatorValidatorSubscriber implements EventSubscriberInterface
{
    /**
     * @var ValidatorInterface
     */
    private $validator;

    public function __construct(ValidatorInterface $validator)
    {
        $this->validator = $validator;
    }

    /**
     * {@inheritdoc}
     */
    public static function getSubscribedEvents()
    {
        return [
            ['event' => 'serializer.post_deserialize', 'method' => 'onPostDeserialize'],
        ];
    }

    public function onPostDeserialize(ObjectEvent $event): void
    {
        $context = $event->getContext();

        if ($context->getDepth() > 0) {
            return;
        }

        $validator = $this->validator;
        $groups = $context->hasAttribute('validation_groups') ? $context->getAttribute('validation_groups') : null;

        if (!$groups) {
            return;
        }

        $constraints = $context->hasAttribute('validation_constraints') ? $context->getAttribute('validation_constraints') : null;

        $list = $validator->validate($event->getObject(), $constraints, $groups);

        if ($list->count() > 0) {
            throw new ValidationFailedException($list);
        }
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\EventDispatcher;

use JMS\Serializer\Context;
use JMS\Serializer\Type\Type;

/**
 * @phpstan-import-type TypeArray from Type
 */
class ObjectEvent extends Event
{
    /**
     * @var mixed
     */
    private $object;

    /**
     * @param mixed $object
     * @param TypeArray $type
     */
    public function __construct(Context $context, $object, array $type)
    {
        parent::__construct($context, $type);

        $this->object = $object;
    }

    /**
     * @return mixed
     */
    public function getObject()
    {
        return $this->object;
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\EventDispatcher;

class PreSerializeEvent extends ObjectEvent
{
    public function setType(string $typeName, array $params = []): void
    {
        $this->type = ['name' => $typeName, 'params' => $params];
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\EventDispatcher;

interface EventSubscriberInterface
{
    /**
     * Returns the events to which this class has subscribed.
     *
     * Return format:
     *     array(
     *         array('event' => 'the-event-name', 'method' => 'onEventName', 'class' => 'some-class', 'format' => 'json'),
     *         array(...),
     *     )
     *
     * The class may be omitted if the class wants to subscribe to events of all classes.
     * Same goes for the format key.
     *
     * @return array
     *
     * @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingReturnTypeHint
     */
    public static function getSubscribedEvents();
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\EventDispatcher;

interface EventDispatcherInterface
{
    /**
     * Returns whether there are listeners.
     */
    public function hasListeners(string $eventName, string $class, string $format): bool;

    /**
     * Dispatches an event.
     *
     * The listeners/subscribers are called in the same order in which they
     * were added to the dispatcher.
     */
    public function dispatch(string $eventName, string $class, string $format, Event $event): void;

    /**
     * Adds a listener.
     *
     * @param mixed $callable
     */
    public function addListener(string $eventName, $callable, ?string $class = null, ?string $format = null, ?string $interface = null): void;

    /**
     * Adds a subscribers.
     */
    public function addSubscriber(EventSubscriberInterface $subscriber): void;
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\EventDispatcher;

use JMS\Serializer\Context;
use JMS\Serializer\Type\Type;
use JMS\Serializer\VisitorInterface;

/**
 * @phpstan-import-type TypeArray from Type
 */
class Event
{
    /**
     * @var bool Whether no further event listeners should be triggered
     */
    private $propagationStopped = false;

    /**
     * @var TypeArray
     */
    protected $type;

    /**
     * @var Context
     */
    private $context;

    /**
     * @param TypeArray $type
     */
    public function __construct(Context $context, array $type)
    {
        $this->context = $context;
        $this->type = $type;
    }

    public function getVisitor(): VisitorInterface
    {
        return $this->context->getVisitor();
    }

    public function getContext(): Context
    {
        return $this->context;
    }

    public function getType(): array
    {
        return $this->type;
    }

    /**
     * Returns whether further event listeners should be triggered.
     *
     * @see Event::stopPropagation()
     *
     * @return bool Whether propagation was already stopped for this event
     */
    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

declare(strict_types=1);

namespace JMS\Serializer\Expression;

use Symfony\Component\ExpressionLanguage\ParsedExpression as BaseExpression;
use Symfony\Component\ExpressionLanguage\SerializedParsedExpression;

/**
 * @author Asmir Mustafic <goetas@gmail.com>
 */
class Expression implements \Serializable
{
    /**
     * @var BaseExpression
     */
    private $expression;

    public function __construct(BaseExpression $expression)
    {
        $this->expression = $expression;
    }

    public function getExpression(): BaseExpression
    {
        return $this->expression;
    }

    /**
     * @return string
     *
     * @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingReturnTypeHint
     * @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.UselessReturnAnnotation
     */
    public function __toString()
    {
        return (string) $this->expression;
    }

    /**
     * @return string
     *
     * @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingParameterTypeHint
     * @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingReturnTypeHint
     * @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.UselessReturnAnnotation
     */
    public function serialize()
    {
        return serialize([(string) $this->expression, serialize($this->expression->getNodes())]);
    }

    /**
     * @param string $str
     *
     * @return void
     *
     * @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingParameterTypeHint
     * @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingReturnTypeHint
     * @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.UselessReturnAnnotation
     */
    public function unserialize($str): void
    {
        $this->expression = new SerializedParsedExpression(...unserialize($str));
    }

    public function __serialize(): array
    {
        return [(string) $this->expression, $this->expression->getNodes()];
    }

    public function __unserialize(array $data): void
    {
        [$expression, $nodes] = $data;
        $this->expression = new BaseExpression($expression, $nodes);
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Expression;

use Symfony\Component\ExpressionLanguage\ExpressionLanguage;

/**
 * @author Asmir Mustafic <goetas@gmail.com>
 */
class ExpressionEvaluator implements CompilableExpressionEvaluatorInterface, ExpressionEvaluatorInterface
{
    /**
     * @var ExpressionLanguage
     */
    private $expressionLanguage;

    /**
     * @var array
     */
    private $context;

    public function __construct(ExpressionLanguage $expressionLanguage, array $context = [])
    {
        $this->expressionLanguage = $expressionLanguage;
        $this->context = $context;
    }

    /**
     * @param mixed $value
     */
    public function setContextVariable(string $name, $value): void
    {
        $this->context[$name] = $value;
    }

    /**
     * @return mixed
     */
    public function evaluate(string $expression, array $data = [])
    {
        return $this->expressionLanguage->evaluate($expression, $data + $this->context);
    }

    /**
     * @return mixed
     */
    public function evaluateParsed(Expression $expression, array $data = [])
    {
        return $this->expressionLanguage->evaluate($expression->getExpression(), $data + $this->context);
    }

    public function parse(string $expression, array $names = []): Expression
    {
        return new Expression($this->expressionLanguage->parse($expression, array_merge(array_keys($this->context), $names)));
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Expression;

/**
 * @author Asmir Mustafic <goetas@gmail.com>
 */
interface CompilableExpressionEvaluatorInterface
{
    public function parse(string $expression, array $names = []): Expression;

    /**
     * @return mixed
     */
    public function evaluateParsed(Expression $expression, array $data = []);
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Expression;

/**
 * @author Asmir Mustafic <goetas@gmail.com>
 */
interface ExpressionEvaluatorInterface
{
    /**
     * @return mixed
     */
    public function evaluate(string $expression, array $data = []);
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\ContextFactory;

/**
 * @deprecated
 */
abstract class CallableContextFactory
{
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\ContextFactory;

use JMS\Serializer\SerializationContext;

/**
 * Serialization Context Factory Interface.
 */
interface SerializationContextFactoryInterface
{
    public function createSerializationContext(): SerializationContext;
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\ContextFactory;

use JMS\Serializer\SerializationContext;

/**
 * Serialization Context Factory using a callable.
 */
final class CallableSerializationContextFactory implements SerializationContextFactoryInterface
{
    /**
     * @var callable():SerializationContext
     */
    private $callable;

    /**
     * @param callable():SerializationContext $callable
     */
    public function __construct(callable $callable)
    {
        $this->callable = $callable;
    }

    public function createSerializationContext(): SerializationContext
    {
        $callable = $this->callable;

        return $callable();
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\ContextFactory;

use JMS\Serializer\DeserializationContext;

/**
 * Deserialization Context Factory Interface.
 */
interface DeserializationContextFactoryInterface
{
    public function createDeserializationContext(): DeserializationContext;
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\ContextFactory;

use JMS\Serializer\DeserializationContext;

final class CallableDeserializationContextFactory implements
    DeserializationContextFactoryInterface
{
    /**
     * @var callable():DeserializationContext
     */
    private $callable;

    /**
     * @param callable():DeserializationContext $callable
     */
    public function __construct(callable $callable)
    {
        $this->callable = $callable;
    }

    public function createDeserializationContext(): DeserializationContext
    {
        $callable = $this->callable;

        return $callable();
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\ContextFactory;

use JMS\Serializer\DeserializationContext;

/**
 * Default Deserialization Context Factory.
 */
final class DefaultDeserializationContextFactory implements DeserializationContextFactoryInterface
{
    public function createDeserializationContext(): DeserializationContext
    {
        return new DeserializationContext();
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\ContextFactory;

use JMS\Serializer\SerializationContext;

/**
 * Default Serialization Context Factory.
 */
final class DefaultSerializationContextFactory implements SerializationContextFactoryInterface
{
    public function createSerializationContext(): SerializationContext
    {
        return new SerializationContext();
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Exception;

/**
 * InvalidArgumentException for the Serializer.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class InvalidArgumentException extends \InvalidArgumentException implements Exception
{
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Exception;

/**
 * @author Asmir Mustafic <goetas@gmail.com>
 */
class NotAcceptableException extends LogicException
{
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Exception;

/**
 * Base exception for the Serializer.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
interface Exception extends \Throwable
{
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Exception;

/**
 * LogicException for the Serializer.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class LogicException extends \LogicException implements Exception
{
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Exception;

/**
 * RuntimeException for the Serializer.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class RuntimeException extends \RuntimeException implements Exception
{
    public static function noMetadataForProperty(string $class, string $prop): self
    {
        return new RuntimeException(sprintf(
            'You must define a type for %s::$%s.',
            $class,
            $prop,
        ));
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Exception;

class UninitializedPropertyException extends RuntimeException implements Exception
{
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Exception;

final class NonIntCastableTypeException extends NonCastableTypeException
{
    /**
     * @param mixed $value
     */
    public function __construct($value)
    {
        parent::__construct('int', $value);
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Exception;

/**
 * @author Asmir Mustafic <goetas@gmail.com>
 */
class CircularReferenceDetectedException extends NotAcceptableException
{
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Exception;

/**
 * @author Asmir Mustafic <goetas@gmail.com>
 */
class ExcludedClassException extends NotAcceptableException
{
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Exception;

class XmlErrorException extends RuntimeException
{
    /**
     * @var \LibXMLError
     */
    private $xmlError;

    public function __construct(\LibXMLError $error)
    {
        switch ($error->level) {
            case LIBXML_ERR_WARNING:
                $level = 'WARNING';
                break;

            case LIBXML_ERR_FATAL:
                $level = 'FATAL';
                break;

            case LIBXML_ERR_ERROR:
                $level = 'ERROR';
                break;

            default:
                $level = 'UNKNOWN';
        }

        parent::__construct(sprintf('[%s] %s in %s (line: %d, column: %d)', $level, $error->message, $error->file, $error->line, $error->column));

        $this->xmlError = $error;
    }

    public function getXmlError(): \LibXMLError
    {
        return $this->xmlError;
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Exception;

/**
 * Throw this exception from you custom (de)serialization handler
 * in order to fallback to the default (de)serialization behavior.
 */
class SkipHandlerException extends RuntimeException
{
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Exception;

/**
 * InvalidArgumentException for the Serializer.
 *
 * @author Asmir Mustafic <goetas@gmail.com>
 */
class InvalidMetadataException extends \Exception implements Exception
{
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Exception;

class UnsupportedFormatException extends InvalidArgumentException
{
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Exception;

final class NonStringCastableTypeException extends NonCastableTypeException
{
    /**
     * @param mixed $value
     */
    public function __construct($value)
    {
        parent::__construct('string', $value);
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Exception;

use JMS\Serializer\Type\Type;

use function get_debug_type;

/**
 * @phpstan-import-type TypeArray from Type
 */
final class NonVisitableTypeException extends RuntimeException
{
    /**
     * @param mixed $data
     * @param TypeArray $type
     * @param RuntimeException|null $previous
     *
     * @return NonVisitableTypeException
     */
    public static function fromDataAndType($data, array $type, ?RuntimeException $previous = null): self
    {
        return new self(
            sprintf('Type %s cannot be visited as %s', get_debug_type($data), $type['name']),
            0,
            $previous,
        );
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Exception;

final class NonFloatCastableTypeException extends NonCastableTypeException
{
    /**
     * @param mixed $value
     */
    public function __construct($value)
    {
        parent::__construct('float', $value);
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Exception;

use Symfony\Component\Validator\ConstraintViolationListInterface;

class ValidationFailedException extends RuntimeException
{
    /**
     * @var ConstraintViolationListInterface
     */
    private $list;

    public function __construct(ConstraintViolationListInterface $list)
    {
        parent::__construct(sprintf('Validation failed with %d error(s).', \count($list)));

        $this->list = $list;
    }

    public function getConstraintViolationList(): ConstraintViolationListInterface
    {
        return $this->list;
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Exception;

abstract class NonCastableTypeException extends RuntimeException
{
    /**
     * @var mixed
     */
    private $value;

    /**
     * @param mixed $value
     */
    public function __construct(string $expectedType, $value)
    {
        $this->value = $value;

        parent::__construct(
            sprintf(
                'Cannot convert value of type "%s" to %s',
                gettype($value),
                $expectedType,
            ),
        );
    }

    /**
     * @return mixed
     */
    public function getValue()
    {
        return $this->value;
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Exception;

/**
 * InvalidArgumentException for the Serializer.
 *
 * @author Asmir Mustafic <goetas@gmail.com>
 */
class ObjectConstructionException extends RuntimeException implements Exception
{
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Exception;

/**
 * @author Asmir Mustafic <goetas@gmail.com>
 */
class ExpressionLanguageRequiredException extends LogicException
{
}
<?php

declare(strict_types=1);

namespace JMS\Serializer;

interface NullAwareVisitorInterface
{
    /**
     * Determine if a value conveys a null value.
     * An example could be an xml element (Dom, SimpleXml, ...) that is tagged with a xsi:nil attribute
     *
     * @param mixed $value
     */
    public function isNull($value): bool;
}
<?php

declare(strict_types=1);

namespace JMS\Serializer;

use JMS\Serializer\Exception\NotAcceptableException;
use JMS\Serializer\Exception\RuntimeException;
use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\Metadata\PropertyMetadata;
use JMS\Serializer\Type\Type;
use JMS\Serializer\Visitor\SerializationVisitorInterface;

/**
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 *
 * @phpstan-import-type TypeArray from Type
 */
final class XmlSerializationVisitor extends AbstractVisitor implements SerializationVisitorInterface
{
    /**
     * @var \DOMDocument
     */
    private $document;

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

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

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

    /**
     * @var \SplStack
     */
    private $stack;

    /**
     * @var \SplStack
     */
    private $metadataStack;

    /**
     * @var \DOMNode|\DOMElement|null
     */
    private $currentNode;

    /**
     * @var ClassMetadata|PropertyMetadata|null
     */
    private $currentMetadata;

    /**
     * @var bool
     */
    private $hasValue;

    /**
     * @var bool
     */
    private $nullWasVisited;

    /**
     * @var \SplStack
     */
    private $objectMetadataStack;

    public function __construct(
        bool $formatOutput = true,
        string $defaultEncoding = 'UTF-8',
        string $defaultVersion = '1.0',
        string $defaultRootName = 'result',
        ?string $defaultRootNamespace = null,
        ?string $defaultRootPrefix = null
    ) {
        $this->objectMetadataStack = new \SplStack();
        $this->stack = new \SplStack();
        $this->metadataStack = new \SplStack();

        $this->currentNode = null;
        $this->nullWasVisited = false;

        $this->document = $this->createDocument($formatOutput, $defaultVersion, $defaultEncoding);

        $this->defaultRootName = $defaultRootName;
        $this->defaultRootNamespace = $defaultRootNamespace;
        $this->defaultRootPrefix = $defaultRootPrefix;
    }

    private function createDocument(bool $formatOutput, string $defaultVersion, string $defaultEncoding): \DOMDocument
    {
        $document = new \DOMDocument($defaultVersion, $defaultEncoding);
        $document->formatOutput = $formatOutput;

        return $document;
    }

    public function createRoot(?ClassMetadata $metadata = null, ?string $rootName = null, ?string $rootNamespace = null, ?string $rootPrefix = null): \DOMElement
    {
        if (null !== $metadata && !empty($metadata->xmlRootName)) {
            $rootPrefix = $metadata->xmlRootPrefix;
            $rootName = $metadata->xmlRootName;
            $rootNamespace = $metadata->xmlRootNamespace ?: $this->getClassDefaultNamespace($metadata);
        } else {
            $rootName = $rootName ?: $this->defaultRootName;
            $rootNamespace = $rootNamespace ?: $this->defaultRootNamespace;
            $rootPrefix = $rootPrefix ?: $this->defaultRootPrefix;
        }

        $document = $this->getDocument();
        if ($rootNamespace) {
            $rootNode = $document->createElementNS($rootNamespace, (null !== $rootPrefix ? $rootPrefix . ':' : '') . $rootName);
        } else {
            $rootNode = $document->createElement($rootName);
        }

        $document->appendChild($rootNode);
        $this->setCurrentNode($rootNode);

        return $rootNode;
    }

    /**
     * {@inheritdoc}
     */
    public function visitNull($data, array $type)
    {
        $node = $this->document->createAttribute('xsi:nil');
        $node->value = 'true';
        $this->nullWasVisited = true;

        return $node;
    }

    /**
     * {@inheritdoc}
     */
    public function visitString(string $data, array $type)
    {
        $doCData = null !== $this->currentMetadata ? $this->currentMetadata->xmlElementCData : true;

        return $doCData ? $this->document->createCDATASection($data) : $this->document->createTextNode((string) $data);
    }

    /**
     * @param mixed $data
     * @param TypeArray $type
     */
    public function visitSimpleString($data, array $type): \DOMText
    {
        return $this->document->createTextNode((string) $data);
    }

    /**
     * {@inheritdoc}
     */
    public function visitBoolean(bool $data, array $type)
    {
        return $this->document->createTextNode($data ? 'true' : 'false');
    }

    /**
     * {@inheritdoc}
     */
    public function visitInteger(int $data, array $type)
    {
        return $this->document->createTextNode((string) $data);
    }

    /**
     * {@inheritdoc}
     */
    public function visitDouble(float $data, array $type)
    {
        $dataResult = $data;
        $precision = $type['params'][0] ?? null;
        if (is_int($precision)) {
            $roundMode = $type['params'][1] ?? null;
            $roundMode = $this->mapRoundMode($roundMode);
            $dataResult = round($dataResult, $precision, $roundMode);
        }

        $decimalsNumbers = $type['params'][2] ?? null;
        if (null === $decimalsNumbers) {
            $parts = explode('.', (string) $dataResult);
            if (count($parts) < 2 || !$parts[1]) {
                $decimalsNumbers = 1;
            }
        }

        if (null !== $decimalsNumbers) {
            $dataResult = number_format($dataResult, $decimalsNumbers, '.', '');
        }

        return $this->document->createTextNode((string) $dataResult);
    }

    /**
     * {@inheritdoc}
     */
    public function visitArray(array $data, array $type): void
    {
        if (null === $this->currentNode) {
            $this->createRoot();
        }

        $entryName = null !== $this->currentMetadata && null !== $this->currentMetadata->xmlEntryName ? $this->currentMetadata->xmlEntryName : 'entry';
        $keyAttributeName = null !== $this->currentMetadata && null !== $this->currentMetadata->xmlKeyAttribute ? $this->currentMetadata->xmlKeyAttribute : null;
        $namespace = null !== $this->currentMetadata && null !== $this->currentMetadata->xmlEntryNamespace ? $this->currentMetadata->xmlEntryNamespace : null;

        $elType = $this->getElementType($type);
        foreach ($data as $k => $v) {
            $tagName = null !== $this->currentMetadata && $this->currentMetadata->xmlKeyValuePairs && $this->isElementNameValid((string) $k) ? $k : $entryName;

            $entryNode = $this->createElement($tagName, $namespace);
            $this->currentNode->appendChild($entryNode);
            $this->setCurrentNode($entryNode);

            if (null !== $keyAttributeName) {
                $entryNode->setAttribute($keyAttributeName, (string) $k);
            }

            try {
                if (null !== $node = $this->navigator->accept($v, $elType)) {
                    $this->currentNode->appendChild($node);
                }
            } catch (NotAcceptableException $e) {
                $this->currentNode->parentNode->removeChild($this->currentNode);
            }

            $this->revertCurrentNode();
        }
    }

    /**
     * {@inheritdoc}
     */
    public function startVisitingObject(ClassMetadata $metadata, object $data, array $type): void
    {
        $this->objectMetadataStack->push($metadata);

        if (null === $this->currentNode) {
            $this->createRoot($metadata);
        }

        $this->addNamespaceAttributes($metadata, $this->currentNode);

        $this->hasValue = false;
    }

    /**
     * {@inheritdoc}
     */
    public function visitProperty(PropertyMetadata $metadata, $v): void
    {
        if ($metadata->xmlAttribute) {
            $this->setCurrentMetadata($metadata);
            $node = $this->navigator->accept($v, $metadata->type);
            $this->revertCurrentMetadata();

            if (!$node instanceof \DOMCharacterData) {
                throw new RuntimeException(sprintf('Unsupported value for XML attribute for %s. Expected character data, but got %s.', $metadata->name, json_encode($v)));
            }

            $this->setAttributeOnNode($this->currentNode, $metadata->serializedName, $node->nodeValue, $metadata->xmlNamespace);

            return;
        }

        if (
            ($metadata->xmlValue && $this->currentNode->childNodes->length > 0)
            || (!$metadata->xmlValue && $this->hasValue)
        ) {
            throw new RuntimeException(sprintf('If you make use of @XmlValue, all other properties in the class must have the @XmlAttribute annotation. Invalid usage detected in class %s.', $metadata->class));
        }

        if ($metadata->xmlValue) {
            $this->hasValue = true;

            $this->setCurrentMetadata($metadata);
            $node = $this->navigator->accept($v, $metadata->type);
            $this->revertCurrentMetadata();

            if (!$node instanceof \DOMCharacterData) {
                throw new RuntimeException(sprintf('Unsupported value for property %s::$%s. Expected character data, but got %s.', $metadata->class, $metadata->name, \is_object($node) ? \get_class($node) : \gettype($node)));
            }

            $this->currentNode->appendChild($node);

            return;
        }

        if ($metadata->xmlAttributeMap) {
            if (!\is_array($v)) {
                throw new RuntimeException(sprintf('Unsupported value type for XML attribute map. Expected array but got %s.', \gettype($v)));
            }

            foreach ($v as $key => $value) {
                $this->setCurrentMetadata($metadata);
                $node = $this->navigator->accept($value, null);
                $this->revertCurrentMetadata();

                if (!$node instanceof \DOMCharacterData) {
                    throw new RuntimeException(sprintf('Unsupported value for a XML attribute map value. Expected character data, but got %s.', json_encode($v)));
                }

                $this->setAttributeOnNode($this->currentNode, $key, $node->nodeValue, $metadata->xmlNamespace);
            }

            return;
        }

        if (false !== strpos($metadata->serializedName, '/@') && $this->trySerializePropertyAsAttributeOnSiblingElement($metadata, $v)) {
            return;
        }

        if (0 === strpos($metadata->serializedName, '@')) {
            [$attributeValue, $processedNode] = $this->processValueForXmlAttribute($v, $metadata->type, $metadata);

            if (null === $v && null === $processedNode) {
                return;
            }

            $attributeName = substr($metadata->serializedName, 1);

            if ($this->currentNode instanceof \DOMElement) {
                $this->setAttributeOnNode($this->currentNode, $attributeName, $attributeValue, $metadata->xmlNamespace);
            } else {
                throw new RuntimeException('Cannot set attribute on a non-element node.');
            }

            return;
        }

        if ($addEnclosingElement = !$this->isInLineCollection($metadata) && !$metadata->inline) {
            $namespace = $metadata->xmlNamespace ?? $this->getClassDefaultNamespace($this->objectMetadataStack->top());

            $element = $this->createElement($metadata->serializedName, $namespace);
            $this->currentNode->appendChild($element);
            $this->setCurrentNode($element);
        }

        $this->setCurrentMetadata($metadata);

        try {
            if (null !== $node = $this->navigator->accept($v, $metadata->type)) {
                $this->currentNode->appendChild($node);
            }
        } catch (NotAcceptableException $e) {
            $this->currentNode->parentNode->removeChild($this->currentNode);
            $this->revertCurrentMetadata();
            $this->revertCurrentNode();
            $this->hasValue = false;

            return;
        }

        $this->revertCurrentMetadata();

        if ($addEnclosingElement) {
            $this->revertCurrentNode();

            if ($this->isElementEmpty($element) && (null === $v || $this->isSkippableCollection($metadata) || $this->isSkippableEmptyObject($node, $metadata))) {
                $this->currentNode->removeChild($element);
            }
        }

        $this->hasValue = false;
    }

    private function trySerializePropertyAsAttributeOnSiblingElement(PropertyMetadata $metadata, $v): bool
    {
        [$elementName, $attributeName] = explode('/@', $metadata->serializedName, 2);
        $namespace = $metadata->xmlNamespace ?? $this->getClassDefaultNamespace($this->objectMetadataStack->top());
        $targetElement = null;

        if ($this->currentNode instanceof \DOMElement) {
            foreach ($this->currentNode->childNodes as $childNode) {
                if ($childNode instanceof \DOMElement && $childNode->localName === $elementName) {
                    $isNamespaceMatch = false;
                    // Case 1: Expected a specific namespace, and child node has it.
                    // Case 2 (else): Expected no namespace
                    if (null !== $namespace && $childNode->namespaceURI === $namespace) {
                        $isNamespaceMatch = true;
                    } elseif ((null === $namespace || '' === $namespace) && (null === $childNode->namespaceURI || '' === $childNode->namespaceURI)) {
                        $isNamespaceMatch = true;
                    }

                    if ($isNamespaceMatch) {
                        $targetElement = $childNode;
                        break;
                    }
                }
            }
        }

        if (!$targetElement) {
            return false;
        }

        if (null === $v) {
            return true;
        }

        [$attributeStringValue, $attributeValueNode] = $this->processValueForXmlAttribute($v, $metadata->type, $metadata);

        if (null !== $attributeValueNode || is_scalar($v)) {
            $this->setAttributeOnNode($targetElement, $attributeName, $attributeStringValue, $metadata->xmlNamespace);
        }

        return true;
    }

    /**
     * @return array{0:string, 1:\DOMNode|null} string value for attribute, and the processed DOMNode/null.
     *
     * @throws RuntimeException If the value is unsuitable for an XML attribute.
     */
    private function processValueForXmlAttribute($inputValue, ?array $valueType, PropertyMetadata $metadataForNavigatorContext): array
    {
        $this->setCurrentMetadata($metadataForNavigatorContext);
        $processedNode = $this->navigator->accept($inputValue, $valueType);
        $this->revertCurrentMetadata();

        if ($processedNode instanceof \DOMCharacterData) {
            $stringValue = $processedNode->nodeValue;
        } elseif (null === $processedNode) {
            $stringValue = is_scalar($inputValue) ? (string) $inputValue : '';
        } else {
            throw new RuntimeException(sprintf(
                'Unsupported value for XML attribute for property "%s". Expected character data or scalar, but got %s.',
                $metadataForNavigatorContext->name,
                \is_object($processedNode) ? \get_class($processedNode) : \gettype($processedNode),
            ));
        }

        return [$stringValue, $processedNode];
    }

    private function isInLineCollection(PropertyMetadata $metadata): bool
    {
        return $metadata->xmlCollection && $metadata->xmlCollectionInline;
    }

    private function isSkippableEmptyObject(?\DOMElement $node, PropertyMetadata $metadata): bool
    {
        return null === $node && !$metadata->xmlCollection && $metadata->skipWhenEmpty;
    }

    private function isSkippableCollection(PropertyMetadata $metadata): bool
    {
        return $metadata->xmlCollection && $metadata->xmlCollectionSkipWhenEmpty;
    }

    private function isElementEmpty(\DOMElement $element): bool
    {
        return !$element->hasChildNodes() && !$element->hasAttributes();
    }

    public function endVisitingObject(ClassMetadata $metadata, object $data, array $type): void
    {
        $this->objectMetadataStack->pop();
    }

    /**
     * {@inheritdoc}
     */
    public function getResult($node)
    {
        $this->navigator = null;
        if (null === $this->document->documentElement) {
            if ($node instanceof \DOMElement) {
                $this->document->appendChild($node);
            } else {
                $this->createRoot();
                if ($node) {
                    $this->document->documentElement->appendChild($node);
                }
            }
        }

        if ($this->nullWasVisited) {
            $this->document->documentElement->setAttributeNS(
                'http://www.w3.org/2000/xmlns/',
                'xmlns:xsi',
                'http://www.w3.org/2001/XMLSchema-instance',
            );
        }

        return $this->document->saveXML();
    }

    public function getCurrentNode(): ?\DOMNode
    {
        return $this->currentNode;
    }

    public function getCurrentMetadata(): ?PropertyMetadata
    {
        return $this->currentMetadata;
    }

    public function getDocument(): \DOMDocument
    {
        return $this->document;
    }

    public function setCurrentMetadata(PropertyMetadata $metadata): void
    {
        $this->metadataStack->push($this->currentMetadata);
        $this->currentMetadata = $metadata;
    }

    public function setCurrentNode(\DOMNode $node): void
    {
        $this->stack->push($this->currentNode);
        $this->currentNode = $node;
    }

    public function setCurrentAndRootNode(\DOMNode $node): void
    {
        $this->setCurrentNode($node);
        $this->document->appendChild($node);
    }

    public function revertCurrentNode(): ?\DOMNode
    {
        return $this->currentNode = $this->stack->pop();
    }

    public function revertCurrentMetadata(): ?PropertyMetadata
    {
        return $this->currentMetadata = $this->metadataStack->pop();
    }

    /**
     * {@inheritdoc}
     */
    public function prepare($data)
    {
        $this->nullWasVisited = false;

        return $data;
    }

    /**
     * Checks that the name is a valid XML element name.
     */
    private function isElementNameValid(string $name): bool
    {
        return $name && false === strpos($name, ' ') && preg_match('#^[\pL_][\pL0-9._-]*$#ui', $name);
    }

    /**
     * Adds namespace attributes to the XML root element
     */
    private function addNamespaceAttributes(ClassMetadata $metadata, \DOMElement $element): void
    {
        foreach ($metadata->xmlNamespaces as $prefix => $uri) {
            $attribute = 'xmlns';
            if ('' !== $prefix) {
                $attribute .= ':' . $prefix;
            } elseif ($element->namespaceURI === $uri) {
                continue;
            }

            $element->setAttributeNS('http://www.w3.org/2000/xmlns/', $attribute, $uri);
        }
    }

    private function createElement(string $tagName, ?string $namespace = null): \DOMElement
    {
        // See #1087 - element must be like: <element xmlns="" /> - https://www.w3.org/TR/REC-xml-names/#iri-use
        // Use of an empty string in a namespace declaration turns it into an "undeclaration".
        if ('' === $namespace) {
            // If we have a default namespace, we need to create namespaced.
            if ($this->parentHasNonEmptyDefaultNs()) {
                return $this->document->createElementNS($namespace, $tagName);
            }

            return $this->document->createElement($tagName);
        }

        if (null === $namespace) {
            return $this->document->createElement($tagName);
        }

        if ($this->currentNode->isDefaultNamespace($namespace)) {
            return $this->document->createElementNS($namespace, $tagName);
        }

        if (!($prefix = $this->currentNode->lookupPrefix($namespace)) && !($prefix = $this->document->lookupPrefix($namespace))) {
            $prefix = 'ns-' . substr(sha1($namespace), 0, 8);
        }

        return $this->document->createElementNS($namespace, $prefix . ':' . $tagName);
    }

    private function setAttributeOnNode(\DOMElement $node, string $name, string $value, ?string $namespace = null): void
    {
        if (null !== $namespace) {
            if (!$prefix = $node->lookupPrefix($namespace)) {
                $prefix = 'ns-' . substr(sha1($namespace), 0, 8);
            }

            $node->setAttributeNS($namespace, $prefix . ':' . $name, $value);
        } else {
            $node->setAttribute($name, $value);
        }
    }

    private function getClassDefaultNamespace(ClassMetadata $metadata): ?string
    {
        return $metadata->xmlNamespaces[''] ?? null;
    }

    private function parentHasNonEmptyDefaultNs(): bool
    {
        return null !== ($uri = $this->currentNode->lookupNamespaceUri(null)) && ('' !== $uri);
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Exclusion;

use JMS\Serializer\Context;
use JMS\Serializer\Expression\CompilableExpressionEvaluatorInterface;
use JMS\Serializer\Expression\Expression;
use JMS\Serializer\Expression\ExpressionEvaluatorInterface;
use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\Metadata\PropertyMetadata;
use JMS\Serializer\SerializationContext;

/**
 * Exposes an exclusion strategy based on the Symfony's expression language.
 * This is not a standard exclusion strategy and can not be used in user applications.
 *
 * @internal
 *
 * @author Asmir Mustafic <goetas@gmail.com>
 */
final class ExpressionLanguageExclusionStrategy
{
    /**
     * @var ExpressionEvaluatorInterface
     */
    private $expressionEvaluator;

    public function __construct(ExpressionEvaluatorInterface $expressionEvaluator)
    {
        $this->expressionEvaluator = $expressionEvaluator;
    }

    public function shouldSkipClass(ClassMetadata $class, Context $navigatorContext): bool
    {
        if (null === $class->excludeIf) {
            return false;
        }

        $variables = [
            'context' => $navigatorContext,
            'class_metadata' => $class,
        ];
        if ($navigatorContext instanceof SerializationContext) {
            $variables['object'] = $navigatorContext->getObject();
        } else {
            $variables['object'] = null;
        }

        if (($class->excludeIf instanceof Expression) && ($this->expressionEvaluator instanceof CompilableExpressionEvaluatorInterface)) {
            return $this->expressionEvaluator->evaluateParsed($class->excludeIf, $variables);
        }

        return $this->expressionEvaluator->evaluate($class->excludeIf, $variables);
    }

    public function shouldSkipProperty(PropertyMetadata $property, Context $navigatorContext): bool
    {
        if (null === $property->excludeIf) {
            return false;
        }

        $variables = [
            'context' => $navigatorContext,
            'property_metadata' => $property,
        ];
        if ($navigatorContext instanceof SerializationContext) {
            $variables['object'] = $navigatorContext->getObject();
        } else {
            $variables['object'] = null;
        }

        if (($property->excludeIf instanceof Expression) && ($this->expressionEvaluator instanceof CompilableExpressionEvaluatorInterface)) {
            return $this->expressionEvaluator->evaluateParsed($property->excludeIf, $variables);
        }

        return $this->expressionEvaluator->evaluate($property->excludeIf, $variables);
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Exclusion;

use JMS\Serializer\Context;
use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\Metadata\PropertyMetadata;

final class VersionExclusionStrategy implements ExclusionStrategyInterface
{
    /**
     * @var string
     */
    private $version;

    public function __construct(string $version)
    {
        $this->version = $version;
    }

    public function shouldSkipClass(ClassMetadata $metadata, Context $navigatorContext): bool
    {
        return false;
    }

    public function shouldSkipProperty(PropertyMetadata $property, Context $navigatorContext): bool
    {
        if ((null !== $version = $property->sinceVersion) && version_compare($this->version, $version, '<')) {
            return true;
        }

        return (null !== $version = $property->untilVersion) && version_compare($this->version, $version, '>');
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Exclusion;

use JMS\Serializer\Context;
use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\Metadata\PropertyMetadata;

/**
 * Interface for exclusion strategies.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
interface ExclusionStrategyInterface
{
    /**
     * Whether the class should be skipped.
     */
    public function shouldSkipClass(ClassMetadata $metadata, Context $context): bool;

    /**
     * Whether the property should be skipped.
     */
    public function shouldSkipProperty(PropertyMetadata $property, Context $context): bool;
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Exclusion;

use JMS\Serializer\Context;
use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\Metadata\PropertyMetadata;

/**
 * @author Adrien Brault <adrien.brault@gmail.com>
 */
final class DepthExclusionStrategy implements ExclusionStrategyInterface
{
    public function shouldSkipClass(ClassMetadata $metadata, Context $context): bool
    {
        return $this->isTooDeep($context);
    }

    public function shouldSkipProperty(PropertyMetadata $property, Context $context): bool
    {
        return $this->isTooDeep($context);
    }

    private function isTooDeep(Context $context): bool
    {
        $relativeDepth = 0;

        foreach ($context->getMetadataStack() as $metadata) {
            if (!$metadata instanceof PropertyMetadata) {
                continue;
            }

            $relativeDepth++;

            if (0 === $metadata->maxDepth && $context->getMetadataStack()->top() === $metadata) {
                continue;
            }

            if (null !== $metadata->maxDepth && $relativeDepth > $metadata->maxDepth) {
                return true;
            }
        }

        return false;
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Exclusion;

use JMS\Serializer\Context;
use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\Metadata\PropertyMetadata;

final class GroupsExclusionStrategy implements ExclusionStrategyInterface
{
    public const DEFAULT_GROUP = 'Default';

    /**
     * @var array
     */
    private $groups = [];

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

    public function __construct(array $groups)
    {
        if (empty($groups)) {
            $groups = [self::DEFAULT_GROUP];
        }

        foreach ($groups as $group) {
            if (is_array($group)) {
                $this->nestedGroups = true;
                break;
            }
        }

        if ($this->nestedGroups) {
            $this->groups = $groups;
        } else {
            foreach ($groups as $group) {
                $this->groups[$group] = true;
            }
        }
    }

    public function shouldSkipClass(ClassMetadata $metadata, Context $navigatorContext): bool
    {
        return false;
    }

    public function shouldSkipProperty(PropertyMetadata $property, Context $navigatorContext): bool
    {
        if ($this->nestedGroups) {
            $groups = $this->getGroupsFor($navigatorContext);

            if (!$property->groups) {
                return !in_array(self::DEFAULT_GROUP, $groups);
            }

            return $this->shouldSkipUsingGroups($property, $groups);
        } else {
            if (!$property->groups) {
                return !isset($this->groups[self::DEFAULT_GROUP]);
            }

            foreach ($property->groups as $group) {
                if (is_scalar($group) && isset($this->groups[$group])) {
                    return false;
                }
            }

            return true;
        }
    }

    private function shouldSkipUsingGroups(PropertyMetadata $property, array $groups): bool
    {
        foreach ($property->groups as $group) {
            if (in_array($group, $groups)) {
                return false;
            }
        }

        return true;
    }

    public function getGroupsFor(Context $navigatorContext): array
    {
        if (!$this->nestedGroups) {
            return array_keys($this->groups);
        }

        $paths = $navigatorContext->getCurrentPath();
        $groups = $this->groups;
        foreach ($paths as $index => $path) {
            if (!array_key_exists($path, $groups)) {
                if ($index > 0) {
                    $groups = [self::DEFAULT_GROUP];
                } else {
                    $groups = array_filter($groups, 'is_string') ?: [self::DEFAULT_GROUP];
                }

                break;
            }

            $groups = $groups[$path];
            if (!array_filter($groups, 'is_string')) {
                $groups += [self::DEFAULT_GROUP];
            }
        }

        return $groups;
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Exclusion;

use JMS\Serializer\Context;
use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\Metadata\PropertyMetadata;

/**
 * Disjunct Exclusion Strategy.
 *
 * This strategy is short-circuiting and will skip a class, or property as soon as one of the delegates skips it.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
final class DisjunctExclusionStrategy implements ExclusionStrategyInterface
{
    /**
     * @var ExclusionStrategyInterface[]
     */
    private $delegates;

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

    public function addStrategy(ExclusionStrategyInterface $strategy): void
    {
        $this->delegates[] = $strategy;
    }

    /**
     * Whether the class should be skipped.
     */
    public function shouldSkipClass(ClassMetadata $metadata, Context $context): bool
    {
        foreach ($this->delegates as $delegate) {
            \assert($delegate instanceof ExclusionStrategyInterface);
            if ($delegate->shouldSkipClass($metadata, $context)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Whether the property should be skipped.
     */
    public function shouldSkipProperty(PropertyMetadata $property, Context $context): bool
    {
        foreach ($this->delegates as $delegate) {
            \assert($delegate instanceof ExclusionStrategyInterface);
            if ($delegate->shouldSkipProperty($property, $context)) {
                return true;
            }
        }

        return false;
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer;

use function iterator_to_array;

final class Functions
{
    /**
     * Copy the iterable into an array. If the iterable is already an array, return it.
     *
     * @return mixed[]
     */
    public static function iterableToArray(iterable $iterable): array
    {
        return is_array($iterable) ? $iterable : iterator_to_array($iterable);
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer;

use JMS\Serializer\Exception\RuntimeException;

/**
 * Serializer Interface.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
interface SerializerInterface
{
    /**
     * Serializes the given data to the specified output format.
     *
     * @param mixed $data
     *
     * @throws RuntimeException
     */
    public function serialize($data, string $format, ?SerializationContext $context = null, ?string $type = null): string;

    /**
     * Deserializes the given data to the specified type.
     *
     * @return mixed
     *
     * @throws RuntimeException
     */
    public function deserialize(string $data, string $type, string $format, ?DeserializationContext $context = null);
}
<?php

declare(strict_types=1);

namespace JMS\Serializer;

/**
 * Interface for visitors.
 *
 * This contains the minimal set of values that must be supported for any
 * output format.
 *
 * @internal
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 * @author Asmir Mustafic <goetas@gmail.com>
 */
interface VisitorInterface
{
    /**
     * Allows visitors to convert the input data to a different representation
     * before the actual serialization/deserialization process starts.
     *
     * @param mixed $data
     *
     * @return mixed
     */
    public function prepare($data);

    /**
     * Called before serialization/deserialization starts.
     */
    public function setNavigator(GraphNavigatorInterface $navigator): void;

    /**
     * Get the result of the serialization/deserialization process.
     *
     * @param mixed $data
     *
     * @return mixed
     */
    public function getResult($data);
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Accessor;

use JMS\Serializer\DeserializationContext;
use JMS\Serializer\Exception\ExpressionLanguageRequiredException;
use JMS\Serializer\Exception\LogicException;
use JMS\Serializer\Exception\UninitializedPropertyException;
use JMS\Serializer\Expression\CompilableExpressionEvaluatorInterface;
use JMS\Serializer\Expression\Expression;
use JMS\Serializer\Expression\ExpressionEvaluatorInterface;
use JMS\Serializer\Metadata\ExpressionPropertyMetadata;
use JMS\Serializer\Metadata\PropertyMetadata;
use JMS\Serializer\Metadata\StaticPropertyMetadata;
use JMS\Serializer\SerializationContext;

/**
 * @author Asmir Mustafic <goetas@gmail.com>
 */
final class DefaultAccessorStrategy implements AccessorStrategyInterface
{
    /**
     * @var callable[]
     */
    private $readAccessors = [];

    /**
     * @var callable[]
     */
    private $writeAccessors = [];

    /**
     * @var \ReflectionProperty[]
     */
    private $propertyReflectionCache = [];

    /**
     * @var ExpressionEvaluatorInterface
     */
    private $evaluator;

    public function __construct(?ExpressionEvaluatorInterface $evaluator = null)
    {
        $this->evaluator = $evaluator;
    }

    /**
     * {@inheritdoc}
     */
    public function getValue(object $object, PropertyMetadata $metadata, SerializationContext $context)
    {
        if ($metadata instanceof StaticPropertyMetadata) {
            return $metadata->getValue();
        }

        if ($metadata instanceof ExpressionPropertyMetadata) {
            if (null === $this->evaluator) {
                throw new ExpressionLanguageRequiredException(sprintf('The property %s on %s requires the expression accessor strategy to be enabled.', $metadata->name, $metadata->class));
            }

            $variables = ['object' => $object, 'context' => $context, 'property_metadata' => $metadata];

            if (($metadata->expression instanceof Expression) && ($this->evaluator instanceof CompilableExpressionEvaluatorInterface)) {
                return $this->evaluator->evaluateParsed($metadata->expression, $variables);
            }

            return $this->evaluator->evaluate($metadata->expression, $variables);
        }

        if (null !== $metadata->getter) {
            return $object->{$metadata->getter}();
        }

        if ($metadata->forceReflectionAccess) {
            $ref = $this->propertyReflectionCache[$metadata->class][$metadata->name] ?? null;
            if (null === $ref) {
                $ref = new \ReflectionProperty($metadata->class, $metadata->name);
                $ref->setAccessible(true);
                $this->propertyReflectionCache[$metadata->class][$metadata->name] = $ref;
            }

            return $ref->getValue($object);
        }

        $accessor = $this->readAccessors[$metadata->class] ?? null;
        if (null === $accessor) {
            $accessor = \Closure::bind(static fn ($o, $name) => $o->$name, null, $metadata->class);
            $this->readAccessors[$metadata->class] = $accessor;
        }

        try {
            return $accessor($object, $metadata->name);
        } catch (\Error $e) {
            // handle uninitialized properties in PHP >= 7.4
            if (preg_match('/^Typed property ([\w\\\\@]+)::\$(\w+) must not be accessed before initialization$/', $e->getMessage(), $matches)) {
                throw new UninitializedPropertyException(sprintf('Uninitialized property "%s::$%s".', $metadata->class, $metadata->name), 0, $e);
            }

            throw $e;
        }
    }

    /**
     * {@inheritdoc}
     */
    public function setValue(object $object, $value, PropertyMetadata $metadata, DeserializationContext $context): void
    {
        if (true === $metadata->readOnly) {
            throw new LogicException(sprintf('%s on %s is read only.', $metadata->name, $metadata->class));
        }

        if (null !== $metadata->setter) {
            $object->{$metadata->setter}($value);

            return;
        }

        if ($metadata->forceReflectionAccess) {
            $ref = $this->propertyReflectionCache[$metadata->class][$metadata->name] ?? null;
            if (null === $ref) {
                $ref = new \ReflectionProperty($metadata->class, $metadata->name);
                $ref->setAccessible(true);
                $this->propertyReflectionCache[$metadata->class][$metadata->name] = $ref;
            }

            $ref->setValue($object, $value);

            return;
        }

        $accessor = $this->writeAccessors[$metadata->class] ?? null;
        if (null === $accessor) {
            $accessor = \Closure::bind(static function ($o, $name, $value): void {
                $o->$name = $value;
            }, null, $metadata->class);
            $this->writeAccessors[$metadata->class] = $accessor;
        }

        $accessor($object, $metadata->name, $value);
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Accessor;

use JMS\Serializer\DeserializationContext;
use JMS\Serializer\Metadata\PropertyMetadata;
use JMS\Serializer\SerializationContext;

/**
 * @author Asmir Mustafic <goetas@gmail.com>
 */
interface AccessorStrategyInterface
{
    /**
     * @return mixed
     */
    public function getValue(object $object, PropertyMetadata $metadata, SerializationContext $context);

    /**
     * @param mixed $value
     */
    public function setValue(object $object, $value, PropertyMetadata $metadata, DeserializationContext $context): void;
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Ordering;

final class IdenticalPropertyOrderingStrategy implements PropertyOrderingInterface
{
    /**
     * {@inheritdoc}
     */
    public function order(array $properties): array
    {
        return $properties;
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Ordering;

use JMS\Serializer\Metadata\PropertyMetadata;

final class AlphabeticalPropertyOrderingStrategy implements PropertyOrderingInterface
{
    /**
     * {@inheritdoc}
     */
    public function order(array $properties): array
    {
        uasort(
            $properties,
            static fn (PropertyMetadata $a, PropertyMetadata $b): int => strcmp($a->name, $b->name),
        );

        return $properties;
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Ordering;

use JMS\Serializer\Metadata\PropertyMetadata;

interface PropertyOrderingInterface
{
    /**
     * @param PropertyMetadata[] $properties name => property
     *
     * @return PropertyMetadata[] name => property
     */
    public function order(array $properties): array;
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Ordering;

final class CustomPropertyOrderingStrategy implements PropertyOrderingInterface
{
    /** @var int[] property => weight */
    private $ordering;

    /**
     * @param int[] $ordering property => weight
     */
    public function __construct(array $ordering)
    {
        $this->ordering = $ordering;
    }

    /**
     * {@inheritdoc}
     */
    public function order(array $properties): array
    {
        $currentSorting = $properties ? array_combine(array_keys($properties), range(1, \count($properties))) : [];

        uksort($properties, function ($a, $b) use ($currentSorting) {
            $existsA = isset($this->ordering[$a]);
            $existsB = isset($this->ordering[$b]);

            if (!$existsA && !$existsB) {
                return $currentSorting[$a] - $currentSorting[$b];
            }

            if (!$existsA) {
                return 1;
            }

            if (!$existsB) {
                return -1;
            }

            return $this->ordering[$a] < $this->ordering[$b] ? -1 : 1;
        });

        return $properties;
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer;

use JMS\Serializer\Exception\RuntimeException;
use Metadata\MetadataFactoryInterface;

class SerializationContext extends Context
{
    /** @var \SplObjectStorage */
    private $visitingSet;

    /** @var \SplStack */
    private $visitingStack;

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

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

    public static function create(): self
    {
        return new self();
    }

    public function initialize(string $format, VisitorInterface $visitor, GraphNavigatorInterface $navigator, MetadataFactoryInterface $factory): void
    {
        parent::initialize($format, $visitor, $navigator, $factory);

        $this->visitingSet = new \SplObjectStorage();
        $this->visitingStack = new \SplStack();
    }

    /**
     * Set if NULLs should be serialized (TRUE) ot not (FALSE)
     */
    public function setSerializeNull(bool $bool): self
    {
        $this->assertMutable();

        $this->serializeNull = $bool;

        return $this;
    }

    /**
     * Returns TRUE when NULLs should be serialized
     * Returns FALSE when NULLs should not be serialized
     */
    public function shouldSerializeNull(): bool
    {
        return $this->serializeNull;
    }

    /**
     * @param mixed $object
     */
    public function startVisiting($object): void
    {
        if (!\is_object($object)) {
            return;
        }

        $this->visitingSet->attach($object);
        $this->visitingStack->push($object);
    }

    /**
     * @param mixed $object
     */
    public function stopVisiting($object): void
    {
        if (!\is_object($object)) {
            return;
        }

        $this->visitingSet->detach($object);
        $poppedObject = $this->visitingStack->pop();

        if ($object !== $poppedObject) {
            throw new RuntimeException('Context visitingStack not working well');
        }
    }

    /**
     * @param mixed $object
     */
    public function isVisiting($object): bool
    {
        if (!\is_object($object)) {
            return false;
        }

        return $this->visitingSet->contains($object);
    }

    public function getPath(): ?string
    {
        $path = [];
        foreach ($this->visitingStack as $obj) {
            $path[] = \get_class($obj);
        }

        if (!$path) {
            return null;
        }

        return implode(' -> ', $path);
    }

    public function getDirection(): int
    {
        return GraphNavigatorInterface::DIRECTION_SERIALIZATION;
    }

    public function getDepth(): int
    {
        return $this->visitingStack->count();
    }

    public function getObject(): ?object
    {
        return !$this->visitingStack->isEmpty() ? $this->visitingStack->top() : null;
    }

    public function getVisitingStack(): \SplStack
    {
        return $this->visitingStack;
    }

    public function getVisitingSet(): \SplObjectStorage
    {
        return $this->visitingSet;
    }

    /**
     * @return $this
     */
    public function setInitialType(string $type): self
    {
        $this->assertMutable();

        $this->initialType = $type;
        $this->setAttribute('initial_type', $type);

        return $this;
    }

    public function getInitialType(): ?string
    {
        return $this->initialType ?: ($this->hasAttribute('initial_type') ? $this->getAttribute('initial_type') : null);
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer;

/**
 * Interface for array transformation.
 *
 * @author Daniel Bojdo <daniel@bojdo.eu>
 */
interface ArrayTransformerInterface
{
    /**
     * Converts objects to an array structure.
     *
     * This is useful when the data needs to be passed on to other methods which expect array data.
     *
     * @param mixed $data anything that converts to an array, typically an object or an array of objects
     *
     * @return array
     */
    public function toArray($data, ?SerializationContext $context = null, ?string $type = null): array;

    /**
     * Restores objects from an array structure.
     *
     * @param array $data
     *
     * @return mixed this returns whatever the passed type is, typically an object or an array of objects
     */
    public function fromArray(array $data, string $type, ?DeserializationContext $context = null);
}
<?php

declare(strict_types=1);

namespace JMS\Serializer;

use JMS\Serializer\ContextFactory\DefaultDeserializationContextFactory;
use JMS\Serializer\ContextFactory\DefaultSerializationContextFactory;
use JMS\Serializer\ContextFactory\DeserializationContextFactoryInterface;
use JMS\Serializer\ContextFactory\SerializationContextFactoryInterface;
use JMS\Serializer\Exception\InvalidArgumentException;
use JMS\Serializer\Exception\RuntimeException;
use JMS\Serializer\Exception\UnsupportedFormatException;
use JMS\Serializer\GraphNavigator\Factory\GraphNavigatorFactoryInterface;
use JMS\Serializer\Type\Parser;
use JMS\Serializer\Type\ParserInterface;
use JMS\Serializer\Visitor\Factory\DeserializationVisitorFactory;
use JMS\Serializer\Visitor\Factory\SerializationVisitorFactory;
use Metadata\MetadataFactoryInterface;

/**
 * Serializer Implementation.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
final class Serializer implements SerializerInterface, ArrayTransformerInterface
{
    /**
     * @var MetadataFactoryInterface
     */
    private $factory;

    /**
     * @var ParserInterface
     */
    private $typeParser;

    /**
     * @var SerializationVisitorFactory[]
     */
    private $serializationVisitors;

    /**
     * @var DeserializationVisitorFactory[]
     */
    private $deserializationVisitors;

    /**
     * @var SerializationContextFactoryInterface
     */
    private $serializationContextFactory;

    /**
     * @var DeserializationContextFactoryInterface
     */
    private $deserializationContextFactory;

    /**
     * @var GraphNavigatorFactoryInterface[]
     */
    private $graphNavigators;

    /**
     * @param GraphNavigatorFactoryInterface[] $graphNavigators
     * @param SerializationVisitorFactory[] $serializationVisitors
     * @param DeserializationVisitorFactory[] $deserializationVisitors
     */
    public function __construct(
        MetadataFactoryInterface $factory,
        array $graphNavigators,
        array $serializationVisitors,
        array $deserializationVisitors,
        ?SerializationContextFactoryInterface $serializationContextFactory = null,
        ?DeserializationContextFactoryInterface $deserializationContextFactory = null,
        ?ParserInterface $typeParser = null
    ) {
        $this->factory = $factory;
        $this->graphNavigators = $graphNavigators;
        $this->serializationVisitors = $serializationVisitors;
        $this->deserializationVisitors = $deserializationVisitors;

        $this->typeParser = $typeParser ?? new Parser();

        $this->serializationContextFactory = $serializationContextFactory ?: new DefaultSerializationContextFactory();
        $this->deserializationContextFactory = $deserializationContextFactory ?: new DefaultDeserializationContextFactory();
    }

    /**
     * Parses a direction string to one of the direction constants.
     */
    public static function parseDirection(string $dirStr): int
    {
        switch (strtolower($dirStr)) {
            case 'serialization':
                return GraphNavigatorInterface::DIRECTION_SERIALIZATION;

            case 'deserialization':
                return GraphNavigatorInterface::DIRECTION_DESERIALIZATION;

            default:
                throw new InvalidArgumentException(sprintf('The direction "%s" does not exist.', $dirStr));
        }
    }

    private function findInitialType(?string $type, SerializationContext $context): ?string
    {
        if (null !== $type) {
            return $type;
        } elseif ($context->hasAttribute('initial_type')) {
            return $context->getAttribute('initial_type');
        }

        return null;
    }

    private function getNavigator(int $direction): GraphNavigatorInterface
    {
        if (!isset($this->graphNavigators[$direction])) {
            throw new RuntimeException(
                sprintf(
                    'Can not find a graph navigator for the direction "%s".',
                    GraphNavigatorInterface::DIRECTION_SERIALIZATION === $direction ? 'serialization' : 'deserialization',
                ),
            );
        }

        return $this->graphNavigators[$direction]->getGraphNavigator();
    }

    private function getVisitor(int $direction, string $format): VisitorInterface
    {
        $factories = GraphNavigatorInterface::DIRECTION_SERIALIZATION === $direction
            ? $this->serializationVisitors
            : $this->deserializationVisitors;

        if (!isset($factories[$format])) {
            throw new UnsupportedFormatException(
                sprintf(
                    'The format "%s" is not supported for %s.',
                    $format,
                    GraphNavigatorInterface::DIRECTION_SERIALIZATION === $direction ? 'serialization' : 'deserialization',
                ),
            );
        }

        return $factories[$format]->getVisitor();
    }

    /**
     * {@InheritDoc}
     */
    public function serialize($data, string $format, ?SerializationContext $context = null, ?string $type = null): string
    {
        if (null === $context) {
            $context = $this->serializationContextFactory->createSerializationContext();
        }

        $visitor = $this->getVisitor(GraphNavigatorInterface::DIRECTION_SERIALIZATION, $format);
        $navigator = $this->getNavigator(GraphNavigatorInterface::DIRECTION_SERIALIZATION);

        $type = $this->findInitialType($type, $context);

        $result = $this->visit($navigator, $visitor, $context, $data, $format, $type);

        $context->close();

        return $visitor->getResult($result);
    }

    /**
     * {@InheritDoc}
     */
    public function deserialize(string $data, string $type, string $format, ?DeserializationContext $context = null)
    {
        if (null === $context) {
            $context = $this->deserializationContextFactory->createDeserializationContext();
        }

        $visitor = $this->getVisitor(GraphNavigatorInterface::DIRECTION_DESERIALIZATION, $format);
        $navigator = $this->getNavigator(GraphNavigatorInterface::DIRECTION_DESERIALIZATION);

        $result = $this->visit($navigator, $visitor, $context, $data, $format, $type);

        $context->close();

        return $visitor->getResult($result);
    }

    /**
     * {@InheritDoc}
     */
    public function toArray($data, ?SerializationContext $context = null, ?string $type = null): array
    {
        if (null === $context) {
            $context = $this->serializationContextFactory->createSerializationContext();
        }

        $visitor = $this->getVisitor(GraphNavigatorInterface::DIRECTION_SERIALIZATION, 'json');
        $navigator = $this->getNavigator(GraphNavigatorInterface::DIRECTION_SERIALIZATION);

        $type = $this->findInitialType($type, $context);
        $result = $this->visit($navigator, $visitor, $context, $data, 'json', $type);
        $result = $this->convertArrayObjects($result);

        if (!\is_array($result)) {
            throw new RuntimeException(sprintf(
                'The input data of type "%s" did not convert to an array, but got a result of type "%s".',
                \is_object($data) ? \get_class($data) : \gettype($data),
                \is_object($result) ? \get_class($result) : \gettype($result),
            ));
        }

        return $result;
    }

    /**
     * {@InheritDoc}
     */
    public function fromArray(array $data, string $type, ?DeserializationContext $context = null)
    {
        if (null === $context) {
            $context = $this->deserializationContextFactory->createDeserializationContext();
        }

        $visitor = $this->getVisitor(GraphNavigatorInterface::DIRECTION_DESERIALIZATION, 'json');
        $navigator = $this->getNavigator(GraphNavigatorInterface::DIRECTION_DESERIALIZATION);

        return $this->visit($navigator, $visitor, $context, $data, 'json', $type, false);
    }

    /**
     * @param mixed $data
     *
     * @return mixed
     */
    private function visit(GraphNavigatorInterface $navigator, VisitorInterface $visitor, Context $context, $data, string $format, ?string $type = null, bool $prepare = true)
    {
        $context->initialize(
            $format,
            $visitor,
            $navigator,
            $this->factory,
        );

        $visitor->setNavigator($navigator);
        $navigator->initialize($visitor, $context);

        if ($prepare) {
            $data = $visitor->prepare($data);
        }

        if (null !== $type) {
            $type = $this->typeParser->parse($type);
        }

        return $navigator->accept($data, $type);
    }

    /**
     * @param mixed $data
     *
     * @return mixed
     */
    private function convertArrayObjects($data)
    {
        if ($data instanceof \ArrayObject || $data instanceof \stdClass) {
            $data = (array) $data;
        }

        if (\is_array($data)) {
            foreach ($data as $k => $v) {
                $data[$k] = $this->convertArrayObjects($v);
            }
        }

        return $data;
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Visitor;

use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\Metadata\PropertyMetadata;
use JMS\Serializer\Type\Type;
use JMS\Serializer\VisitorInterface;

/**
 * Interface for visitors.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 * @author Asmir Mustafic <goetas@gmail.com>
 *
 * @phpstan-import-type TypeArray from Type
 */
interface SerializationVisitorInterface extends VisitorInterface
{
    /**
     * @param mixed $data
     * @param TypeArray $type
     *
     * @return mixed
     */
    public function visitNull($data, array $type);

    /**
     * @param TypeArray $type
     *
     * @return mixed
     */
    public function visitString(string $data, array $type);

    /**
     * @param TypeArray $type
     *
     * @return mixed
     */
    public function visitBoolean(bool $data, array $type);

    /**
     * @param TypeArray $type
     *
     * @return mixed
     */
    public function visitDouble(float $data, array $type);

    /**
     * @param TypeArray $type
     *
     * @return mixed
     */
    public function visitInteger(int $data, array $type);

    /**
     * @param TypeArray $type
     *
     * @return array|\ArrayObject|void
     */
    public function visitArray(array $data, array $type);

    /**
     * @param TypeArray $type
     * Called before the properties of the object are being visited.
     */
    public function startVisitingObject(ClassMetadata $metadata, object $data, array $type): void;

    /**
     * @param mixed $data
     */
    public function visitProperty(PropertyMetadata $metadata, $data): void;

    /**
     * @param TypeArray $type
     * Called after all properties of the object have been visited.
     *
     * @return array|\ArrayObject|void
     */
    public function endVisitingObject(ClassMetadata $metadata, object $data, array $type);
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Visitor\Factory;

use JMS\Serializer\JsonSerializationVisitor;
use JMS\Serializer\Visitor\SerializationVisitorInterface;

/**
 * @author Asmir Mustafic <goetas@gmail.com>
 */
final class JsonSerializationVisitorFactory implements SerializationVisitorFactory
{
    /**
     * @var int
     */
    private $options = JSON_PRESERVE_ZERO_FRACTION;

    public function getVisitor(): SerializationVisitorInterface
    {
        return new JsonSerializationVisitor($this->options);
    }

    public function setOptions(int $options): self
    {
        $this->options = $options;

        return $this;
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Visitor\Factory;

use JMS\Serializer\Visitor\SerializationVisitorInterface;

/**
 * @author Asmir Mustafic <goetas@gmail.com>
 */
interface SerializationVisitorFactory
{
    public function getVisitor(): SerializationVisitorInterface;
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Visitor\Factory;

use JMS\Serializer\Visitor\DeserializationVisitorInterface;

/**
 * @author Asmir Mustafic <goetas@gmail.com>
 */
interface DeserializationVisitorFactory
{
    public function getVisitor(): DeserializationVisitorInterface;
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Visitor\Factory;

use JMS\Serializer\Visitor\DeserializationVisitorInterface;
use JMS\Serializer\XmlDeserializationVisitor;

/**
 * @author Asmir Mustafic <goetas@gmail.com>
 */
final class XmlDeserializationVisitorFactory implements DeserializationVisitorFactory
{
    /**
     * @var bool
     */
    private $disableExternalEntities = true;

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

    /**
     * @var int
     */
    private $options = 0;

    /**
     * @return XmlDeserializationVisitor
     */
    public function getVisitor(): DeserializationVisitorInterface
    {
        return new XmlDeserializationVisitor($this->disableExternalEntities, $this->doctypeWhitelist, $this->options);
    }

    public function enableExternalEntities(bool $enable = true): self
    {
        $this->disableExternalEntities = !$enable;

        return $this;
    }

    /**
     * @param string[] $doctypeWhitelist
     */
    public function setDoctypeWhitelist(array $doctypeWhitelist): self
    {
        $this->doctypeWhitelist = $doctypeWhitelist;

        return $this;
    }

    public function setOptions(int $options): self
    {
        $this->options = $options;

        return $this;
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Visitor\Factory;

use JMS\Serializer\JsonDeserializationStrictVisitor;
use JMS\Serializer\JsonDeserializationVisitor;
use JMS\Serializer\Visitor\DeserializationVisitorInterface;

/**
 * @author Asmir Mustafic <goetas@gmail.com>
 */
final class JsonDeserializationVisitorFactory implements DeserializationVisitorFactory
{
    /**
     * @var int
     */
    private $options = 0;

    /**
     * @var int
     */
    private $depth = 512;

    /**
     * @var bool
     */
    private $strict;

    public function __construct(bool $strict = false)
    {
        $this->strict = $strict;
    }

    public function getVisitor(): DeserializationVisitorInterface
    {
        if ($this->strict) {
            return new JsonDeserializationStrictVisitor($this->options, $this->depth);
        }

        return new JsonDeserializationVisitor($this->options, $this->depth);
    }

    public function setOptions(int $options): self
    {
        $this->options = $options;

        return $this;
    }

    public function setDepth(int $depth): self
    {
        $this->depth = $depth;

        return $this;
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Visitor\Factory;

use JMS\Serializer\Visitor\SerializationVisitorInterface;
use JMS\Serializer\XmlSerializationVisitor;

/**
 * @author Asmir Mustafic <goetas@gmail.com>
 */
final class XmlSerializationVisitorFactory implements SerializationVisitorFactory
{
    /**
     * @var string
     */
    private $defaultRootName = 'result';

    /**
     * @var string
     */
    private $defaultVersion = '1.0';

    /**
     * @var string
     */
    private $defaultEncoding = 'UTF-8';

    /**
     * @var bool
     */
    private $formatOutput = true;

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

    public function getVisitor(): SerializationVisitorInterface
    {
        return new XmlSerializationVisitor(
            $this->formatOutput,
            $this->defaultEncoding,
            $this->defaultVersion,
            $this->defaultRootName,
            $this->defaultRootNamespace,
        );
    }

    public function setDefaultRootName(string $name, ?string $namespace = null): self
    {
        $this->defaultRootName = $name;
        $this->defaultRootNamespace = $namespace;

        return $this;
    }

    public function setDefaultVersion(string $version): self
    {
        $this->defaultVersion = $version;

        return $this;
    }

    public function setDefaultEncoding(string $encoding): self
    {
        $this->defaultEncoding = $encoding;

        return $this;
    }

    public function setFormatOutput(bool $formatOutput): self
    {
        $this->formatOutput = (bool) $formatOutput;

        return $this;
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Visitor;

use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\Metadata\PropertyMetadata;
use JMS\Serializer\Type\Type;
use JMS\Serializer\VisitorInterface;

/**
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 * @author Asmir Mustafic <goetas@gmail.com>
 *
 * @phpstan-import-type TypeArray from Type
 */
interface DeserializationVisitorInterface extends VisitorInterface
{
    /**
     * @param mixed $data
     * @param TypeArray $type
     *
     * @return null
     */
    public function visitNull($data, array $type);

    /**
     * @param mixed $data
     * @param TypeArray $type
     */
    public function visitString($data, array $type): ?string;

    /**
     * @param mixed $data
     * @param TypeArray $type
     */
    public function visitBoolean($data, array $type): ?bool;

    /**
     * @param mixed $data
     * @param TypeArray $type
     */
    public function visitDouble($data, array $type): ?float;

    /**
     * @param mixed $data
     * @param TypeArray $type
     */
    public function visitInteger($data, array $type): ?int;

    /**
     * Returns the class name based on the type of the discriminator map value
     *
     * @param mixed $data
     */
    public function visitDiscriminatorMapProperty($data, ClassMetadata $metadata): string;

    /**
     * @param mixed $data
     * @param TypeArray $type
     *
     * @return array<mixed>
     */
    public function visitArray($data, array $type): array;

    /**
     * Called before the properties of the object are being visited.
     *
     * @param TypeArray $type
     */
    public function startVisitingObject(ClassMetadata $metadata, object $data, array $type): void;

    /**
     * @param mixed $data
     *
     * @return mixed
     */
    public function visitProperty(PropertyMetadata $metadata, $data);

    /**
     * Called after all properties of the object have been visited.
     *
     * @param mixed $data
     * @param TypeArray $type
     */
    public function endVisitingObject(ClassMetadata $metadata, $data, array $type): object;

    /**
     * @param mixed $data
     *
     * @return mixed
     */
    public function getResult($data);
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\GraphNavigator;

use JMS\Serializer\Accessor\AccessorStrategyInterface;
use JMS\Serializer\Context;
use JMS\Serializer\EventDispatcher\EventDispatcher;
use JMS\Serializer\EventDispatcher\EventDispatcherInterface;
use JMS\Serializer\EventDispatcher\ObjectEvent;
use JMS\Serializer\EventDispatcher\PreSerializeEvent;
use JMS\Serializer\Exception\CircularReferenceDetectedException;
use JMS\Serializer\Exception\ExcludedClassException;
use JMS\Serializer\Exception\ExpressionLanguageRequiredException;
use JMS\Serializer\Exception\InvalidArgumentException;
use JMS\Serializer\Exception\NotAcceptableException;
use JMS\Serializer\Exception\RuntimeException;
use JMS\Serializer\Exception\SkipHandlerException;
use JMS\Serializer\Exception\UninitializedPropertyException;
use JMS\Serializer\Exclusion\ExpressionLanguageExclusionStrategy;
use JMS\Serializer\Expression\ExpressionEvaluatorInterface;
use JMS\Serializer\Functions;
use JMS\Serializer\GraphNavigator;
use JMS\Serializer\GraphNavigatorInterface;
use JMS\Serializer\Handler\HandlerRegistryInterface;
use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\NullAwareVisitorInterface;
use JMS\Serializer\SerializationContext;
use JMS\Serializer\Type\Type;
use JMS\Serializer\Visitor\SerializationVisitorInterface;
use JMS\Serializer\VisitorInterface;
use Metadata\MetadataFactoryInterface;

use function assert;

/**
 * Handles traversal along the object graph.
 *
 * This class handles traversal along the graph, and calls different methods
 * on visitors, or custom handlers to process its nodes.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 *
 * @phpstan-import-type TypeArray from Type
 */
final class SerializationGraphNavigator extends GraphNavigator
{
    /**
     * @var SerializationVisitorInterface
     */
    protected $visitor;

    /**
     * @var SerializationContext
     */
    protected $context;

    /**
     * @var ExpressionLanguageExclusionStrategy
     */
    private $expressionExclusionStrategy;

    /**
     * @var EventDispatcherInterface
     */
    private $dispatcher;

    /**
     * @var MetadataFactoryInterface
     */
    private $metadataFactory;

    /**
     * @var HandlerRegistryInterface
     */
    private $handlerRegistry;
    /**
     * @var AccessorStrategyInterface
     */
    private $accessor;

    /**
     * @var bool
     */
    private $shouldSerializeNull;

    public function __construct(
        MetadataFactoryInterface $metadataFactory,
        HandlerRegistryInterface $handlerRegistry,
        AccessorStrategyInterface $accessor,
        ?EventDispatcherInterface $dispatcher = null,
        ?ExpressionEvaluatorInterface $expressionEvaluator = null
    ) {
        $this->dispatcher = $dispatcher ?: new EventDispatcher();
        $this->metadataFactory = $metadataFactory;
        $this->handlerRegistry = $handlerRegistry;
        $this->accessor = $accessor;

        if ($expressionEvaluator) {
            $this->expressionExclusionStrategy = new ExpressionLanguageExclusionStrategy($expressionEvaluator);
        }
    }

    public function initialize(VisitorInterface $visitor, Context $context): void
    {
        assert($context instanceof SerializationContext);

        parent::initialize($visitor, $context);

        $this->shouldSerializeNull = $context->shouldSerializeNull();
    }

    /**
     * Called for each node of the graph that is being traversed.
     *
     * @param mixed $data the data depends on the direction, and type of visitor
     * @param TypeArray|null $type array has the format ["name" => string, "params" => array]
     *
     * @return mixed the return value depends on the direction, and type of visitor
     */
    public function accept($data, ?array $type = null)
    {
        // If the type was not given, we infer the most specific type from the
        // input data in serialization mode.
        if (null === $type) {
            $typeName = \gettype($data);
            if ('object' === $typeName) {
                $typeName = \get_class($data);
            }

            $type = ['name' => $typeName, 'params' => []];
        } elseif (null === $data) {
            // If the data is null, we have to force the type to null regardless of the input in order to
            // guarantee correct handling of null values, and not have any internal auto-casting behavior.
            $type = ['name' => 'NULL', 'params' => []];
        }

        // Sometimes data can convey null but is not of a null type.
        // Visitors can have the power to add this custom null evaluation
        if ($this->visitor instanceof NullAwareVisitorInterface && true === $this->visitor->isNull($data)) {
            $type = ['name' => 'NULL', 'params' => []];
        }

        switch ($type['name']) {
            case 'NULL':
                if (!$this->shouldSerializeNull && !$this->isRootNullAllowed()) {
                    throw new NotAcceptableException();
                }

                return $this->visitor->visitNull($data, $type);

            case 'string':
                return $this->visitor->visitString((string) $data, $type);

            case 'int':
            case 'integer':
                return $this->visitor->visitInteger((int) $data, $type);

            case 'bool':
            case 'boolean':
            case 'true':
            case 'false':
                return $this->visitor->visitBoolean((bool) $data, $type);

            case 'double':
            case 'float':
                return $this->visitor->visitDouble((float) $data, $type);

            case 'iterable':
                return $this->visitor->visitArray(Functions::iterableToArray($data), $type);

            case 'array':
            case 'list':
                return $this->visitor->visitArray((array) $data, $type);

            case 'resource':
                $msg = 'Resources are not supported in serialized data.';
                if (null !== $path = $this->context->getPath()) {
                    $msg .= ' Path: ' . $path;
                }

                throw new RuntimeException($msg);

            case 'union':
                if (null !== $handler = $this->handlerRegistry->getHandler(GraphNavigatorInterface::DIRECTION_SERIALIZATION, $type['name'], $this->format)) {
                    try {
                        return \call_user_func($handler, $this->visitor, $data, $type, $this->context);
                    } catch (SkipHandlerException $e) {
                        // Skip handler, fallback to default behavior
                    }
                }

                break;
            default:
                if (null !== $data) {
                    if ($this->context->isVisiting($data)) {
                        throw new CircularReferenceDetectedException();
                    }

                    $this->context->startVisiting($data);
                }

                // If we're serializing a polymorphic type, then we'll be interested in the
                // metadata for the actual type of the object, not the base class.
                if (class_exists($type['name'], false) || interface_exists($type['name'], false)) {
                    if (is_subclass_of($data, $type['name'], false) && null === $this->handlerRegistry->getHandler(GraphNavigatorInterface::DIRECTION_SERIALIZATION, $type['name'], $this->format)) {
                        $type = ['name' => \get_class($data), 'params' => $type['params'] ?? []];
                    }
                }

                // Trigger pre-serialization callbacks, and listeners if they exist.
                // Dispatch pre-serialization event before handling data to have ability change type in listener
                if ($this->dispatcher->hasListeners('serializer.pre_serialize', $type['name'], $this->format)) {
                    $this->dispatcher->dispatch('serializer.pre_serialize', $type['name'], $this->format, $event = new PreSerializeEvent($this->context, $data, $type));
                    $type = $event->getType();
                }

                // First, try whether a custom handler exists for the given type. This is done
                // before loading metadata because the type name might not be a class, but
                // could also simply be an artifical type.
                if (null !== $handler = $this->handlerRegistry->getHandler(GraphNavigatorInterface::DIRECTION_SERIALIZATION, $type['name'], $this->format)) {
                    try {
                        $rs = \call_user_func($handler, $this->visitor, $data, $type, $this->context);
                        $this->context->stopVisiting($data);

                        return $rs;
                    } catch (SkipHandlerException $e) {
                        // Skip handler, fallback to default behavior
                    } catch (NotAcceptableException $e) {
                        $this->context->stopVisiting($data);

                        throw $e;
                    }
                }

                $metadata = $this->metadataFactory->getMetadataForClass($type['name']);
                \assert($metadata instanceof ClassMetadata);

                if ($metadata->usingExpression && null === $this->expressionExclusionStrategy) {
                    throw new ExpressionLanguageRequiredException(sprintf('To use conditional exclude/expose in %s you must configure the expression language.', $metadata->name));
                }

                if (null !== $this->exclusionStrategy && $this->exclusionStrategy->shouldSkipClass($metadata, $this->context)) {
                    $this->context->stopVisiting($data);

                    throw new ExcludedClassException();
                }

                if (null !== $this->expressionExclusionStrategy && $this->expressionExclusionStrategy->shouldSkipClass($metadata, $this->context)) {
                    $this->context->stopVisiting($data);

                    throw new ExcludedClassException();
                }

                if (!is_object($data)) {
                    throw new InvalidArgumentException('Value at ' . $this->context->getPath() . ' is expected to be an object of class ' . $type['name'] . ' but is of type ' . gettype($data));
                }

                $this->context->pushClassMetadata($metadata);

                foreach ($metadata->preSerializeMethods as $method) {
                    $method->invoke($data);
                }

                $this->visitor->startVisitingObject($metadata, $data, $type);
                foreach ($metadata->propertyMetadata as $propertyMetadata) {
                    if (null !== $this->exclusionStrategy && $this->exclusionStrategy->shouldSkipProperty($propertyMetadata, $this->context)) {
                        continue;
                    }

                    if (null !== $this->expressionExclusionStrategy && $this->expressionExclusionStrategy->shouldSkipProperty($propertyMetadata, $this->context)) {
                        continue;
                    }

                    try {
                        $v = $this->accessor->getValue($data, $propertyMetadata, $this->context);
                    } catch (UninitializedPropertyException $e) {
                        continue;
                    }

                    if (null === $v && true !== $this->shouldSerializeNull) {
                        continue;
                    }

                    $this->context->pushPropertyMetadata($propertyMetadata);
                    $this->visitor->visitProperty($propertyMetadata, $v);
                    $this->context->popPropertyMetadata();
                }

                $this->afterVisitingObject($metadata, $data, $type);

                return $this->visitor->endVisitingObject($metadata, $data, $type);
        }
    }

    private function isRootNullAllowed(): bool
    {
        return $this->context->hasAttribute('allows_root_null') && $this->context->getAttribute('allows_root_null') && 0 === $this->context->getVisitingSet()->count();
    }

    /**
     * @param TypeArray $type
     */
    private function afterVisitingObject(ClassMetadata $metadata, object $object, array $type): void
    {
        $this->context->stopVisiting($object);
        $this->context->popClassMetadata();

        foreach ($metadata->postSerializeMethods as $method) {
            $method->invoke($object);
        }

        if ($this->dispatcher->hasListeners('serializer.post_serialize', $metadata->name, $this->format)) {
            $this->dispatcher->dispatch('serializer.post_serialize', $metadata->name, $this->format, new ObjectEvent($this->context, $object, $type));
        }
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\GraphNavigator;

use JMS\Serializer\Accessor\AccessorStrategyInterface;
use JMS\Serializer\Construction\ObjectConstructorInterface;
use JMS\Serializer\DeserializationContext;
use JMS\Serializer\EventDispatcher\EventDispatcher;
use JMS\Serializer\EventDispatcher\EventDispatcherInterface;
use JMS\Serializer\EventDispatcher\ObjectEvent;
use JMS\Serializer\EventDispatcher\PreDeserializeEvent;
use JMS\Serializer\Exception\ExpressionLanguageRequiredException;
use JMS\Serializer\Exception\LogicException;
use JMS\Serializer\Exception\NotAcceptableException;
use JMS\Serializer\Exception\RuntimeException;
use JMS\Serializer\Exception\SkipHandlerException;
use JMS\Serializer\Exclusion\ExpressionLanguageExclusionStrategy;
use JMS\Serializer\Expression\ExpressionEvaluatorInterface;
use JMS\Serializer\GraphNavigator;
use JMS\Serializer\GraphNavigatorInterface;
use JMS\Serializer\Handler\HandlerRegistryInterface;
use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\NullAwareVisitorInterface;
use JMS\Serializer\Type\Type;
use JMS\Serializer\Visitor\DeserializationVisitorInterface;
use Metadata\MetadataFactoryInterface;

/**
 * Handles traversal along the object graph.
 *
 * This class handles traversal along the graph, and calls different methods
 * on visitors, or custom handlers to process its nodes.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 *
 * @phpstan-import-type TypeArray from Type
 */
final class DeserializationGraphNavigator extends GraphNavigator implements GraphNavigatorInterface
{
    /**
     * @var DeserializationVisitorInterface
     */
    protected $visitor;

    /**
     * @var DeserializationContext
     */
    protected $context;

    /**
     * @var ExpressionLanguageExclusionStrategy
     */
    private $expressionExclusionStrategy;

    /**
     * @var EventDispatcherInterface
     */
    private $dispatcher;

    /**
     * @var MetadataFactoryInterface
     */
    private $metadataFactory;

    /**
     * @var HandlerRegistryInterface
     */
    private $handlerRegistry;

    /**
     * @var ObjectConstructorInterface
     */
    private $objectConstructor;
    /**
     * @var AccessorStrategyInterface
     */
    private $accessor;

    public function __construct(
        MetadataFactoryInterface $metadataFactory,
        HandlerRegistryInterface $handlerRegistry,
        ObjectConstructorInterface $objectConstructor,
        AccessorStrategyInterface $accessor,
        ?EventDispatcherInterface $dispatcher = null,
        ?ExpressionEvaluatorInterface $expressionEvaluator = null
    ) {
        $this->dispatcher = $dispatcher ?: new EventDispatcher();
        $this->metadataFactory = $metadataFactory;
        $this->handlerRegistry = $handlerRegistry;
        $this->objectConstructor = $objectConstructor;
        $this->accessor = $accessor;
        if ($expressionEvaluator) {
            $this->expressionExclusionStrategy = new ExpressionLanguageExclusionStrategy($expressionEvaluator);
        }
    }

    /**
     * Called for each node of the graph that is being traversed.
     *
     * @param mixed $data the data depends on the direction, and type of visitor
     * @param TypeArray|null $type array has the format ["name" => string, "params" => array]
     *
     * @return mixed the return value depends on the direction, and type of visitor
     */
    public function accept($data, ?array $type = null)
    {
        // If the type was not given, we infer the most specific type from the
        // input data in serialization mode.
        if (null === $type) {
            throw new RuntimeException('The type must be given for all properties when deserializing.');
        }

        // Sometimes data can convey null but is not of a null type.
        // Visitors can have the power to add this custom null evaluation
        if ($this->visitor instanceof NullAwareVisitorInterface && true === $this->visitor->isNull($data)) {
            $type = ['name' => 'NULL', 'params' => []];
        }

        switch ($type['name']) {
            case 'NULL':
                return $this->visitor->visitNull($data, $type);

            case 'string':
                return $this->visitor->visitString($data, $type);

            case 'int':
            case 'integer':
                return $this->visitor->visitInteger($data, $type);

            case 'bool':
            case 'boolean':
            case 'false':
            case 'true':
                return $this->visitor->visitBoolean($data, $type);

            case 'double':
            case 'float':
                return $this->visitor->visitDouble($data, $type);

            case 'array':
            case 'iterable':
            case 'list':
                return $this->visitor->visitArray($data, $type);

            case 'resource':
                throw new RuntimeException('Resources are not supported in serialized data.');

            default:
                $this->context->increaseDepth();

                // Trigger pre-serialization callbacks, and listeners if they exist.
                // Dispatch pre-serialization event before handling data to have ability change type in listener
                if ($this->dispatcher->hasListeners('serializer.pre_deserialize', $type['name'], $this->format)) {
                    $this->dispatcher->dispatch('serializer.pre_deserialize', $type['name'], $this->format, $event = new PreDeserializeEvent($this->context, $data, $type));
                    $type = $event->getType();
                    $data = $event->getData();
                }

                // First, try whether a custom handler exists for the given type. This is done
                // before loading metadata because the type name might not be a class, but
                // could also simply be an artifical type.
                if (null !== $handler = $this->handlerRegistry->getHandler(GraphNavigatorInterface::DIRECTION_DESERIALIZATION, $type['name'], $this->format)) {
                    try {
                        $rs = \call_user_func($handler, $this->visitor, $data, $type, $this->context);
                        $this->context->decreaseDepth();

                        return $rs;
                    } catch (SkipHandlerException $e) {
                        // Skip handler, fallback to default behavior
                    }
                }

                $metadata = $this->metadataFactory->getMetadataForClass($type['name']);
                \assert($metadata instanceof ClassMetadata);

                if ($metadata->usingExpression && !$this->expressionExclusionStrategy) {
                    throw new ExpressionLanguageRequiredException(sprintf('To use conditional exclude/expose in %s you must configure the expression language.', $metadata->name));
                }

                if (!empty($metadata->discriminatorMap) && $type['name'] === $metadata->discriminatorBaseClass) {
                    $metadata = $this->resolveMetadata($data, $metadata);
                }

                if (null !== $this->exclusionStrategy && $this->exclusionStrategy->shouldSkipClass($metadata, $this->context)) {
                    $this->context->decreaseDepth();

                    return null;
                }

                $this->context->pushClassMetadata($metadata);

                $object = $this->objectConstructor->construct($this->visitor, $metadata, $data, $type, $this->context);

                if (null === $object) {
                    $this->context->popClassMetadata();
                    $this->context->decreaseDepth();

                    return $this->visitor->visitNull($data, $type);
                }

                $this->visitor->startVisitingObject($metadata, $object, $type);
                foreach ($metadata->propertyMetadata as $propertyMetadata) {
                    if (null !== $this->exclusionStrategy && $this->exclusionStrategy->shouldSkipProperty($propertyMetadata, $this->context)) {
                        continue;
                    }

                    if (null !== $this->expressionExclusionStrategy && $this->expressionExclusionStrategy->shouldSkipProperty($propertyMetadata, $this->context)) {
                        continue;
                    }

                    if ($propertyMetadata->readOnly) {
                        continue;
                    }

                    $this->context->pushPropertyMetadata($propertyMetadata);
                    try {
                        $v = $this->visitor->visitProperty($propertyMetadata, $data);
                        $this->accessor->setValue($object, $v, $propertyMetadata, $this->context);
                    } catch (NotAcceptableException $e) {
                        if (true === $propertyMetadata->hasDefault) {
                            $cloned = clone $propertyMetadata;
                            $cloned->setter = null;
                            $this->accessor->setValue($object, $cloned->defaultValue, $cloned, $this->context);
                        }
                    }

                    $this->context->popPropertyMetadata();
                }

                $rs = $this->visitor->endVisitingObject($metadata, $data, $type);
                $this->afterVisitingObject($metadata, $rs, $type);

                return $rs;
        }
    }

    /**
     * @param mixed $data
     */
    private function resolveMetadata($data, ClassMetadata $metadata): ?ClassMetadata
    {
        $typeValue = $this->visitor->visitDiscriminatorMapProperty($data, $metadata);

        if (!isset($metadata->discriminatorMap[$typeValue])) {
            throw new LogicException(sprintf(
                'The type value "%s" does not exist in the discriminator map of class "%s". Available types: %s',
                $typeValue,
                $metadata->name,
                implode(', ', array_keys($metadata->discriminatorMap)),
            ));
        }

        return $this->metadataFactory->getMetadataForClass($metadata->discriminatorMap[$typeValue]);
    }

    /**
     * @param TypeArray $type
     */
    private function afterVisitingObject(ClassMetadata $metadata, object $object, array $type): void
    {
        $this->context->decreaseDepth();
        $this->context->popClassMetadata();

        foreach ($metadata->postDeserializeMethods as $method) {
            $method->invoke($object);
        }

        if ($this->dispatcher->hasListeners('serializer.post_deserialize', $metadata->name, $this->format)) {
            $this->dispatcher->dispatch('serializer.post_deserialize', $metadata->name, $this->format, new ObjectEvent($this->context, $object, $type));
        }
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\GraphNavigator\Factory;

use JMS\Serializer\Accessor\AccessorStrategyInterface;
use JMS\Serializer\Construction\ObjectConstructorInterface;
use JMS\Serializer\EventDispatcher\EventDispatcherInterface;
use JMS\Serializer\Expression\ExpressionEvaluatorInterface;
use JMS\Serializer\GraphNavigator\DeserializationGraphNavigator;
use JMS\Serializer\GraphNavigatorInterface;
use JMS\Serializer\Handler\HandlerRegistryInterface;
use Metadata\MetadataFactoryInterface;

final class DeserializationGraphNavigatorFactory implements GraphNavigatorFactoryInterface
{
    /**
     * @var MetadataFactoryInterface
     */
    private $metadataFactory;
    /**
     * @var HandlerRegistryInterface
     */
    private $handlerRegistry;
    /**
     * @var ObjectConstructorInterface
     */
    private $objectConstructor;
    /**
     * @var AccessorStrategyInterface
     */
    private $accessor;
    /**
     * @var EventDispatcherInterface
     */
    private $dispatcher;
    /**
     * @var ExpressionEvaluatorInterface
     */
    private $expressionEvaluator;

    public function __construct(
        MetadataFactoryInterface $metadataFactory,
        HandlerRegistryInterface $handlerRegistry,
        ObjectConstructorInterface $objectConstructor,
        AccessorStrategyInterface $accessor,
        ?EventDispatcherInterface $dispatcher = null,
        ?ExpressionEvaluatorInterface $expressionEvaluator = null
    ) {
        $this->metadataFactory = $metadataFactory;
        $this->handlerRegistry = $handlerRegistry;
        $this->objectConstructor = $objectConstructor;
        $this->accessor = $accessor;
        $this->dispatcher = $dispatcher;
        $this->expressionEvaluator = $expressionEvaluator;
    }

    public function getGraphNavigator(): GraphNavigatorInterface
    {
        return new DeserializationGraphNavigator($this->metadataFactory, $this->handlerRegistry, $this->objectConstructor, $this->accessor, $this->dispatcher, $this->expressionEvaluator);
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\GraphNavigator\Factory;

use JMS\Serializer\GraphNavigatorInterface;

interface GraphNavigatorFactoryInterface
{
    public function getGraphNavigator(): GraphNavigatorInterface;
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\GraphNavigator\Factory;

use JMS\Serializer\Accessor\AccessorStrategyInterface;
use JMS\Serializer\Accessor\DefaultAccessorStrategy;
use JMS\Serializer\EventDispatcher\EventDispatcher;
use JMS\Serializer\EventDispatcher\EventDispatcherInterface;
use JMS\Serializer\Expression\ExpressionEvaluatorInterface;
use JMS\Serializer\GraphNavigator\SerializationGraphNavigator;
use JMS\Serializer\GraphNavigatorInterface;
use JMS\Serializer\Handler\HandlerRegistryInterface;
use Metadata\MetadataFactoryInterface;

final class SerializationGraphNavigatorFactory implements GraphNavigatorFactoryInterface
{
    /**
     * @var MetadataFactoryInterface
     */
    private $metadataFactory;
    /**
     * @var HandlerRegistryInterface
     */
    private $handlerRegistry;
    /**
     * @var AccessorStrategyInterface
     */
    private $accessor;
    /**
     * @var EventDispatcherInterface
     */
    private $dispatcher;
    /**
     * @var ExpressionEvaluatorInterface
     */
    private $expressionEvaluator;

    public function __construct(
        MetadataFactoryInterface $metadataFactory,
        HandlerRegistryInterface $handlerRegistry,
        ?AccessorStrategyInterface $accessor = null,
        ?EventDispatcherInterface $dispatcher = null,
        ?ExpressionEvaluatorInterface $expressionEvaluator = null
    ) {
        $this->metadataFactory = $metadataFactory;
        $this->handlerRegistry = $handlerRegistry;
        $this->accessor = $accessor ?: new DefaultAccessorStrategy();
        $this->dispatcher = $dispatcher ?: new EventDispatcher();
        $this->expressionEvaluator = $expressionEvaluator;
    }

    public function getGraphNavigator(): GraphNavigatorInterface
    {
        return new SerializationGraphNavigator($this->metadataFactory, $this->handlerRegistry, $this->accessor, $this->dispatcher, $this->expressionEvaluator);
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Twig;

use Twig\TwigFilter;
use Twig\TwigFunction;

/**
 * @author Asmir Mustafic <goetas@gmail.com>
 */
final class SerializerRuntimeExtension extends SerializerBaseExtension
{
    /**
     * @return TwigFilter[]
     *
     * @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingReturnTypeHint
     */
    public function getFilters()
    {
        return [
            new TwigFilter($this->serializationFunctionsPrefix . 'serialize', [SerializerRuntimeHelper::class, 'serialize']),
        ];
    }

    /**
     * @return TwigFunction[]
     *
     * @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingReturnTypeHint
     */
    public function getFunctions()
    {
        return [
            new TwigFunction($this->serializationFunctionsPrefix . 'serialization_context', '\JMS\Serializer\SerializationContext::create'),
        ];
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Twig;

use Twig\Extension\AbstractExtension;

abstract class SerializerBaseExtension extends AbstractExtension
{
    /**
     * @var string
     */
    protected $serializationFunctionsPrefix;

    /**
     * @return string
     *
     * @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingReturnTypeHint
     */
    public function getName()
    {
        return 'jms_serializer';
    }

    public function __construct(string $serializationFunctionsPrefix = '')
    {
        $this->serializationFunctionsPrefix = $serializationFunctionsPrefix;
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Twig;

use JMS\Serializer\SerializationContext;
use JMS\Serializer\SerializerInterface;
use Twig\TwigFilter;
use Twig\TwigFunction;

/**
 * Serializer helper twig extension
 *
 * Basically provides access to JMSSerializer from Twig
 */
class SerializerExtension extends SerializerBaseExtension
{
    /**
     * @var SerializerInterface
     */
    protected $serializer;

    public function __construct(SerializerInterface $serializer, string $serializationFunctionsPrefix = '')
    {
        $this->serializer = $serializer;

        parent::__construct($serializationFunctionsPrefix);
    }

    /**
     * @return TwigFilter[]
     *
     * @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingReturnTypeHint
     */
    public function getFilters()
    {
        return [
            new TwigFilter($this->serializationFunctionsPrefix . 'serialize', [$this, 'serialize']),
        ];
    }

    /**
     * @return TwigFunction[]
     *
     * @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingReturnTypeHint
     */
    public function getFunctions()
    {
        return [
            new TwigFunction($this->serializationFunctionsPrefix . 'serialization_context', '\JMS\Serializer\SerializationContext::create'),
        ];
    }

    public function serialize(object $object, string $type = 'json', ?SerializationContext $context = null): string
    {
        return $this->serializer->serialize($object, $type, $context);
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Twig;

use JMS\Serializer\SerializationContext;
use JMS\Serializer\SerializerInterface;

/**
 * @author Asmir Mustafic <goetas@gmail.com>
 */
final class SerializerRuntimeHelper
{
    /**
     * @var SerializerInterface
     */
    protected $serializer;

    public function __construct(SerializerInterface $serializer)
    {
        $this->serializer = $serializer;
    }

    /**
     * @param mixed $object
     */
    public function serialize($object, string $type = 'json', ?SerializationContext $context = null): string
    {
        return $this->serializer->serialize($object, $type, $context);
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Naming;

use JMS\Serializer\Metadata\PropertyMetadata;

/**
 * Interface for property naming strategies.
 *
 * Implementations translate the property name to a serialized name that is
 * displayed.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
interface PropertyNamingStrategyInterface
{
    /**
     * Translates the name of the property to the serialized version.
     */
    public function translateName(PropertyMetadata $property): string;
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Naming;

use JMS\Serializer\Metadata\PropertyMetadata;

/**
 * Naming strategy which uses an annotation to translate the property name.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
final class SerializedNameAnnotationStrategy implements PropertyNamingStrategyInterface
{
    /**
     * @var PropertyNamingStrategyInterface
     */
    private $delegate;

    public function __construct(PropertyNamingStrategyInterface $namingStrategy)
    {
        $this->delegate = $namingStrategy;
    }

    public function translateName(PropertyMetadata $property): string
    {
        if (null !== $name = $property->serializedName) {
            return $name;
        }

        return $this->delegate->translateName($property);
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Naming;

use JMS\Serializer\Metadata\PropertyMetadata;

/**
 * Generic naming strategy which translates a camel-cased property name.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
final class CamelCaseNamingStrategy implements PropertyNamingStrategyInterface
{
    /**
     * @var string
     */
    private $separator;

    /**
     * @var bool
     */
    private $lowerCase;

    public function __construct(string $separator = '_', bool $lowerCase = true)
    {
        $this->separator = $separator;
        $this->lowerCase = $lowerCase;
    }

    public function translateName(PropertyMetadata $property): string
    {
        $name = preg_replace('/[A-Z]+/', $this->separator . '\\0', $property->name);

        if ($this->lowerCase) {
            return strtolower($name);
        }

        return ucfirst($name);
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Naming;

use JMS\Serializer\Metadata\PropertyMetadata;

final class IdenticalPropertyNamingStrategy implements PropertyNamingStrategyInterface
{
    public function translateName(PropertyMetadata $property): string
    {
        return $property->name;
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Annotation;

/**
 * @Annotation
 * @Target({"PROPERTY","METHOD", "ANNOTATION"})
 */
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)]
final class SerializedName implements SerializerAttribute
{
    use AnnotationUtilsTrait;

    /**
     * @var string|null
     */
    public $name = null;

    public function __construct($values = [], ?string $name = null)
    {
        $this->loadAnnotationParameters(get_defined_vars());
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Annotation;

/**
 * @Annotation
 * @Target({"PROPERTY", "METHOD", "ANNOTATION"})
 */
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)]
final class Since extends Version
{
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Annotation;

/**
 * @Annotation
 * @Target({"PROPERTY","METHOD","ANNOTATION"})
 */
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)]
final class Inline implements SerializerAttribute
{
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Annotation;

/**
 * @Annotation
 * @Target({"PROPERTY","METHOD","ANNOTATION"})
 */
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)]
final class XmlList extends XmlCollection
{
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Annotation;

/**
 * @Annotation
 * @Target({"PROPERTY","METHOD","ANNOTATION"})
 */
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)]
final class XmlValue implements SerializerAttribute
{
    use AnnotationUtilsTrait;

    /**
     * @var bool
     */
    public $cdata = true;

    public function __construct(array $values = [], bool $cdata = true)
    {
        $this->loadAnnotationParameters(get_defined_vars());
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Annotation;

abstract class Version implements SerializerAttribute
{
    use AnnotationUtilsTrait;

    /**
     * @Required
     * @var string|null
     */
    public $version = null;

    public function __construct($values = [], ?string $version = null)
    {
        $this->loadAnnotationParameters(get_defined_vars());
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Annotation;

use JMS\Serializer\Exception\InvalidArgumentException;

trait AnnotationUtilsTrait
{
    private function loadAnnotationParameters(array $vars): void
    {
        if (!array_key_exists('values', $vars)) {
            $values = [];
        } elseif (!is_array($vars['values'])) {
            $values = ['value' => $vars['values']];
        } else {
            $values = $vars['values'];
        }

        unset($vars['values']);

        if (array_key_exists('value', $values)) {
            $values[key($vars)] = $values['value'];
            unset($values['value']);
        }

        foreach ($values as $key => $value) {
            $vars[$key] = $value;
        }

        foreach ($vars as $key => $value) {
            if (!property_exists(static::class, $key)) {
                throw new InvalidArgumentException(sprintf('Unknown property "%s" on annotation "%s".', $key, static::class));
            }

            $this->{$key} = $value;
        }
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Annotation;

/**
 * @Annotation
 * @Target({"CLASS", "PROPERTY"})
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_PROPERTY)]
final class AccessType implements SerializerAttribute
{
    use AnnotationUtilsTrait;

    /**
     * @Required
     * @var string|null
     */
    public $type;

    public function __construct(array $values = [], ?string $type = null)
    {
        $this->loadAnnotationParameters(get_defined_vars());
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Annotation;

use JMS\Serializer\Exception\RuntimeException;

/**
 * @Annotation
 * @Target("CLASS")
 */
#[\Attribute(\Attribute::TARGET_CLASS)]
final class ExclusionPolicy implements SerializerAttribute
{
    use AnnotationUtilsTrait;

    public const NONE = 'NONE';
    public const ALL = 'ALL';

    /**
     * @var string|null
     */
    public $policy = 'NONE';

    public function __construct($values = [], ?string $policy = null)
    {
        $this->loadAnnotationParameters(get_defined_vars());

        $this->policy = strtoupper($this->policy);

        if (self::NONE !== $this->policy && self::ALL !== $this->policy) {
            throw new RuntimeException('Exclusion policy must either be "ALL", or "NONE".');
        }
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Annotation;

/**
 * @Annotation
 * @Target("CLASS")
 */
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
final class XmlNamespace implements SerializerAttribute
{
    use AnnotationUtilsTrait;

    /**
     * @Required
     * @var string|null
     */
    public $uri = null;

    /**
     * @var string
     */
    public $prefix = '';

    public function __construct(array $values = [], ?string $uri = null, string $prefix = '')
    {
        $this->loadAnnotationParameters(get_defined_vars());
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Annotation;

/**
 * @Annotation
 * @Target({"PROPERTY", "METHOD","ANNOTATION"})
 */
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)]
final class Type implements SerializerAttribute
{
    use AnnotationUtilsTrait;

    /**
     * @Required
     * @var string|\Stringable|null
     */
    public $name = null;

    public function __construct($values = [], $name = null)
    {
        if ((null !== $name) && !is_string($name) && !(is_object($name) && method_exists($name, '__toString'))) {
            throw new \RuntimeException(
                'Type must be either string, null or object implements __toString() method.',
            );
        }

        if (is_object($name)) {
            $name = (string) $name;
        }

        if (is_object($values)) {
            if (false === method_exists($values, '__toString')) {
                throw new \RuntimeException(
                    'Type must be either string or object implements __toString() method.',
                );
            }

            $values = (string) $values;
        }

        $this->loadAnnotationParameters(get_defined_vars());
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Annotation;

/**
 * @Annotation
 * @Target("PROPERTY")
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
#[\Attribute(\Attribute::TARGET_PROPERTY)]
final class Accessor implements SerializerAttribute
{
    use AnnotationUtilsTrait;

    /**
     * @var string|null
     */
    public $getter = null;

    /**
     * @var string|null
     */
    public $setter = null;

    public function __construct(array $values = [], ?string $getter = null, ?string $setter = null)
    {
        $this->loadAnnotationParameters(get_defined_vars());
    }
}
<?php

declare(strict_types=1);

use JMS\Serializer\Annotation\DeprecatedReadOnly;

class_alias(DeprecatedReadOnly::class, 'JMS\Serializer\Annotation\ReadOnly');
<?php

declare(strict_types=1);

namespace JMS\Serializer\Annotation;

/**
 * @Annotation
 * @Target("CLASS")
 */
#[\Attribute(\Attribute::TARGET_CLASS)]
class XmlDiscriminator implements SerializerAttribute
{
    use AnnotationUtilsTrait;

    /**
     * @var bool
     */
    public $attribute = false;

    /**
     * @var bool
     */
    public $cdata = true;

    /**
     * @var string|null
     */
    public $namespace = null;

    public function __construct(array $values = [], bool $attribute = false, bool $cdata = false, ?string $namespace = null)
    {
        $this->loadAnnotationParameters(get_defined_vars());
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Annotation;

/**
 * Marker interface for serializer attributes
 */
interface SerializerAttribute
{
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Annotation;

/**
 * This annotation can be defined on methods which are called after the
 * deserialization of the object is complete.
 *
 * These methods do not necessarily have to be public.
 *
 * @Annotation
 * @Target("METHOD")
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
#[\Attribute(\Attribute::TARGET_METHOD)]
final class PostDeserialize implements SerializerAttribute
{
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Annotation;

/**
 * @Annotation
 * @Target({"PROPERTY", "METHOD","ANNOTATION"})
 */
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)]
final class XmlAttribute implements SerializerAttribute
{
    use AnnotationUtilsTrait;

    /**
     * @var string|null
     */
    public $namespace = null;

    public function __construct($values = [], ?string $namespace = null)
    {
        $this->loadAnnotationParameters(get_defined_vars());
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Annotation;

/**
 * @Annotation
 * @Target({"PROPERTY", "METHOD","ANNOTATION"})
 */
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)]
final class XmlElement implements SerializerAttribute
{
    use AnnotationUtilsTrait;

    /**
     * @var bool
     */
    public $cdata = true;

    /**
     * @var string|null
     */
    public $namespace = null;

    public function __construct(array $values = [], bool $cdata = true, ?string $namespace = null)
    {
        $this->loadAnnotationParameters(get_defined_vars());
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Annotation;

/**
 * @Annotation
 * @Target({"PROPERTY", "METHOD", "ANNOTATION"})
 */
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)]
final class Expose implements SerializerAttribute
{
    use AnnotationUtilsTrait;

    /**
     * @var string|null
     */
    public $if = null;

    public function __construct(array $values = [], ?string $if = null)
    {
        $this->loadAnnotationParameters(get_defined_vars());
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Annotation;

/**
 * @Annotation
 * @Target({"PROPERTY"})
 */
#[\Attribute(\Attribute::TARGET_PROPERTY)]
final class UnionDiscriminator implements SerializerAttribute
{
    use AnnotationUtilsTrait;

    /** @var array<string> */
    public $map = [];

    /** @var string */
    public $field = 'type';

    public function __construct(array $values = [], string $field = 'type', array $map = [])
    {
        $this->loadAnnotationParameters(get_defined_vars());
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Annotation;

/**
 * @Annotation
 * @Target({"PROPERTY", "METHOD"})
 */
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)]
final class XmlAttributeMap implements SerializerAttribute
{
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Annotation;

/**
 * @Annotation
 * @Target({"PROPERTY","METHOD","ANNOTATION"})
 */
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)]
final class XmlMap extends XmlCollection
{
    use AnnotationUtilsTrait;

    /**
     * @var string
     */
    public $keyAttribute = '_key';

    public function __construct(array $values = [], string $keyAttribute = '_key', string $entry = 'entry', bool $inline = false, ?string $namespace = null, bool $skipWhenEmpty = true)
    {
        $this->loadAnnotationParameters(get_defined_vars());
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Annotation;

/**
 * @Annotation
 * @Target({"PROPERTY", "CLASS", "METHOD", "ANNOTATION"})
 */
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD)]
final class Exclude implements SerializerAttribute
{
    use AnnotationUtilsTrait;

    /**
     * @var string|null
     */
    public $if;

    public function __construct(array $values = [], ?string $if = null)
    {
        $this->loadAnnotationParameters(get_defined_vars());
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Annotation;

/**
 * @Annotation
 * @Target("CLASS")
 */
#[\Attribute(\Attribute::TARGET_CLASS)]
final class XmlRoot implements SerializerAttribute
{
    use AnnotationUtilsTrait;

    /**
     * @Required
     * @var string|null
     */
    public $name = null;

    /**
     * @var string|null
     */
    public $namespace = null;

    /**
     * @var string|null
     */
    public $prefix = null;

    public function __construct($values = [], ?string $name = null, ?string $namespace = null, ?string $prefix = null)
    {
        $this->loadAnnotationParameters(get_defined_vars());
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Annotation;

/**
 * @Annotation
 * @Target({"METHOD", "CLASS"})
 *
 * @author Alexander Klimenkov <alx.devel@gmail.com>
 */
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
final class VirtualProperty implements SerializerAttribute
{
    use AnnotationUtilsTrait;

    /**
     * @var string|null
     */
    public $exp = null;

    /**
     * @var string|null
     */
    public $name = null;

    /**
     * @var array
     */
    public $options = [];

    public function __construct($values = [], ?string $name = null, ?string $exp = null, array $options = [])
    {
        $vars = get_defined_vars();
        unset($vars['options']);
        $this->loadAnnotationParameters($vars);

        if (0 !== count($options)) {
            $this->options = $options;
        }

        foreach ($options as $option) {
            if (is_array($option) && class_exists($option[0])) {
                $this->options[] = new $option[0]([], ...$option[1]);

                continue;
            }

            $this->options[] = $option;
        }
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Annotation;

/**
 * This annotation can be declared on methods which should be called
 * before the Serialization process.
 *
 * These methods do not need to be public, and should do any clean-up, or
 * preparation of the object that is necessary.
 *
 * @Annotation
 * @Target("METHOD")
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
#[\Attribute(\Attribute::TARGET_METHOD)]
final class PreSerialize implements SerializerAttribute
{
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Annotation;

/**
 * @Annotation
 * @Target({"PROPERTY","METHOD","ANNOTATION"})
 */
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)]
final class SkipWhenEmpty implements SerializerAttribute
{
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Annotation;

/**
 * @Annotation
 * @Target({"CLASS","PROPERTY"})
 *
 * @final
 */
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_PROPERTY)]
/* final */ class ReadOnlyProperty implements SerializerAttribute
{
    use AnnotationUtilsTrait;

    /**
     * @var bool
     */
    public $readOnly = true;

    public function __construct(array $values = [], bool $readOnly = true)
    {
        $this->loadAnnotationParameters(get_defined_vars());
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Annotation;

/**
 * @Annotation
 * @Target("CLASS")
 */
#[\Attribute(\Attribute::TARGET_CLASS)]
class Discriminator implements SerializerAttribute
{
    use AnnotationUtilsTrait;

    /** @var array<string> */
    public $map = [];

    /** @var string */
    public $field = 'type';

    /** @var bool */
    public $disabled = false;

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

    public function __construct(array $values = [], string $field = 'type', array $groups = [], array $map = [], bool $disabled = false)
    {
        $this->loadAnnotationParameters(get_defined_vars());
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Annotation;

/**
 * @Annotation
 * @Target({"PROPERTY","METHOD","ANNOTATION"})
 */
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)]
final class Groups implements SerializerAttribute
{
    use AnnotationUtilsTrait;

    /** @var array<string> @Required */
    public $groups = [];

    public function __construct(array $values = [], array $groups = [])
    {
        $vars = get_defined_vars();
        /*
            if someone wants to set as Groups(['value' => '...']) this check will miserably fail (only one group with 'value' as only key).
            That is because doctrine annotations uses for @Groups("abc") the same values content (buy validation will fail since groups has to be an array).
            All the other cases should work as expected.
            The alternative here is to use the explicit syntax  Groups(groups=['value' => '...'])
        */
        if (count($values) > 0 && ((!isset($values['value']) && !isset($values['groups'])) || count($values) > 1) && 0 === count($groups)) {
            $vars['groups'] = $values;
            $vars['values'] = [];
        }

        $this->loadAnnotationParameters($vars);
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Annotation;

abstract class XmlCollection implements SerializerAttribute
{
    use AnnotationUtilsTrait;

    /**
     * @var string
     */
    public $entry = 'entry';

    /**
     * @var bool
     */
    public $inline = false;

    /**
     * @var string|null
     */
    public $namespace = null;

    /**
     * @var bool
     */
    public $skipWhenEmpty = true;

    public function __construct(array $values = [], string $entry = 'entry', bool $inline = false, ?string $namespace = null, bool $skipWhenEmpty = true)
    {
        $this->loadAnnotationParameters(get_defined_vars());
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Annotation;

/**
 * @Annotation
 * @Target({"CLASS","PROPERTY"})
 *
 * @deprecated use `@ReadOnlyProperty` instead
 */
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_PROPERTY)]
final class DeprecatedReadOnly extends ReadOnlyProperty
{
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Annotation;

/**
 * @Annotation
 * @Target({"PROPERTY", "METHOD", "ANNOTATION"})
 */
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)]
final class Until extends Version
{
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Annotation;

/**
 * @Annotation
 * @Target({"PROPERTY","METHOD","ANNOTATION"})
 */
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)]
final class XmlKeyValuePairs implements SerializerAttribute
{
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Annotation;

/**
 * Controls the order of properties in a class.
 *
 * @Annotation
 * @Target("CLASS")
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
#[\Attribute(\Attribute::TARGET_CLASS)]
final class AccessorOrder implements SerializerAttribute
{
    use AnnotationUtilsTrait;

    /**
     * @Required
     * @var string|null
     */
    public $order = null;

    /**
     * @var array<string>
     */
    public $custom = [];

    public function __construct(array $values = [], ?string $order = null, array $custom = [])
    {
        $this->loadAnnotationParameters(get_defined_vars());
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Annotation;

/**
 * @Annotation
 * @Target("METHOD")
 */
#[\Attribute(\Attribute::TARGET_METHOD)]
final class PostSerialize implements SerializerAttribute
{
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Annotation;

/**
 * @Annotation
 * @Target({"PROPERTY","METHOD","ANNOTATION"})
 */
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)]
final class MaxDepth implements SerializerAttribute
{
    use AnnotationUtilsTrait;

    /**
     * @Required
     * @var int
     */
    public $depth;

    public function __construct($values = [], int $depth = 0)
    {
        $this->loadAnnotationParameters(get_defined_vars());
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Builder;

use Doctrine\Common\Annotations\Reader;
use JMS\Serializer\Metadata\Driver\DocBlockDriver;
use JMS\Serializer\Type\ParserInterface;
use Metadata\Driver\DriverInterface;

class DocBlockDriverFactory implements DriverFactoryInterface
{
    /**
     * @var DriverFactoryInterface
     */
    private $driverFactoryToDecorate;
    /**
     * @var ParserInterface|null
     */
    private $typeParser;

    public function __construct(DriverFactoryInterface $driverFactoryToDecorate, ?ParserInterface $typeParser = null)
    {
        $this->driverFactoryToDecorate = $driverFactoryToDecorate;
        $this->typeParser = $typeParser;
    }

    public function createDriver(array $metadataDirs, ?Reader $annotationReader = null): DriverInterface
    {
        $driver = $this->driverFactoryToDecorate->createDriver($metadataDirs, $annotationReader);

        return new DocBlockDriver($driver, $this->typeParser);
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Builder;

use Doctrine\Common\Annotations\Reader;
use JMS\Serializer\Exception\LogicException;
use Metadata\Driver\DriverInterface;

final class CallbackDriverFactory implements DriverFactoryInterface
{
    /**
     * @var callable
     * @phpstan-var callable(array $metadataDirs, Reader|null $reader): DriverInterface
     */
    private $callback;

    /**
     * @phpstan-param callable(array $metadataDirs, Reader|null $reader): DriverInterface $callable
     */
    public function __construct(callable $callable)
    {
        $this->callback = $callable;
    }

    public function createDriver(array $metadataDirs, ?Reader $reader = null): DriverInterface
    {
        $driver = \call_user_func($this->callback, $metadataDirs, $reader);
        if (!$driver instanceof DriverInterface) {
            throw new LogicException('The callback must return an instance of DriverInterface.');
        }

        return $driver;
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Builder;

use Doctrine\Common\Annotations\Reader;
use Metadata\Driver\DriverInterface;

interface DriverFactoryInterface
{
    public function createDriver(array $metadataDirs, ?Reader $annotationReader = null): DriverInterface;
}
<?php

declare(strict_types=1);

namespace JMS\Serializer\Builder;

use Doctrine\Common\Annotations\Reader;
use JMS\Serializer\Exception\RuntimeException;
use JMS\Serializer\Expression\CompilableExpressionEvaluatorInterface;
use JMS\Serializer\Metadata\Driver\AnnotationOrAttributeDriver;
use JMS\Serializer\Metadata\Driver\DefaultValuePropertyDriver;
use JMS\Serializer\Metadata\Driver\EnumPropertiesDriver;
use JMS\Serializer\Metadata\Driver\NullDriver;
use JMS\Serializer\Metadata\Driver\TypedPropertiesDriver;
use JMS\Serializer\Metadata\Driver\XmlDriver;
use JMS\Serializer\Metadata\Driver\YamlDriver;
use JMS\Serializer\Naming\PropertyNamingStrategyInterface;
use JMS\Serializer\Type\Parser;
use JMS\Serializer\Type\ParserInterface;
use Metadata\Driver\DriverChain;
use Metadata\Driver\DriverInterface;
use Metadata\Driver\FileLocator;
use Symfony\Component\Yaml\Yaml;

final class DefaultDriverFactory implements DriverFactoryInterface
{
    /**
     * @var ParserInterface
     */
    private $typeParser;

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

    /**
     * @var PropertyNamingStrategyInterface
     */
    private $propertyNamingStrategy;

    /**
     * @var CompilableExpressionEvaluatorInterface
     */
    private $expressionEvaluator;

    public function __construct(PropertyNamingStrategyInterface $propertyNamingStrategy, ?ParserInterface $typeParser = null, ?CompilableExpressionEvaluatorInterface $expressionEvaluator = null)
    {
        $this->typeParser = $typeParser ?: new Parser();
        $this->propertyNamingStrategy = $propertyNamingStrategy;
        $this->expressionEvaluator = $expressionEvaluator;
    }

    public function enableEnumSupport(bool $enableEnumSupport = true): void
    {
        $this->enableEnumSupport = $enableEnumSupport;
    }

    public function createDriver(array $metadataDirs, ?Reader $annotationReader = null): DriverInterface
    {
        if (PHP_VERSION_ID < 80000 && empty($metadataDirs) && !interface_exists(Reader::class)) {
            throw new RuntimeException(sprintf('To use "%s", either a list of metadata directories must be provided, the "doctrine/annotations" package installed, or use PHP 8.0 or later.', self::class));
        }

        /*
         * Build the sorted list of metadata drivers based on the environment. The final order should be:
         *
         * - YAML Driver
         * - XML Driver
         * - Annotations/Attributes Driver
         * - Null (Fallback) Driver
         */
        $metadataDrivers = [];

        if (PHP_VERSION_ID >= 80000 || $annotationReader instanceof Reader) {
            $metadataDrivers[] = new AnnotationOrAttributeDriver($this->propertyNamingStrategy, $this->typeParser, $this->expressionEvaluator, $annotationReader);
        }

        if (!empty($metadataDirs)) {
            $fileLocator = new FileLocator($metadataDirs);

            array_unshift($metadataDrivers, new XmlDriver($fileLocator, $this->propertyNamingStrategy, $this->typeParser, $this->expressionEvaluator));

            if (class_exists(Yaml::class)) {
                array_unshift($metadataDrivers, new YamlDriver($fileLocator, $this->propertyNamingStrategy, $this->typeParser, $this->expressionEvaluator));
            }
        }

        $driver = new DriverChain($metadataDrivers);
        $driver->addDriver(new NullDriver($this->propertyNamingStrategy));

        if ($this->enableEnumSupport) {
            $driver = new EnumPropertiesDriver($driver);
        }

        $driver = new TypedPropertiesDriver($driver, $this->typeParser);

        if (PHP_VERSION_ID >= 80000) {
            $driver = new DefaultValuePropertyDriver($driver);
        }

        return $driver;
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer;

use JMS\Serializer\Exception\LogicException;

class DeserializationContext extends Context
{
    /**
     * @var int
     */
    private $depth = 0;

    public static function create(): self
    {
        return new self();
    }

    public function getDirection(): int
    {
        return GraphNavigatorInterface::DIRECTION_DESERIALIZATION;
    }

    public function getDepth(): int
    {
        return $this->depth;
    }

    public function increaseDepth(): void
    {
        $this->depth += 1;
    }

    public function decreaseDepth(): void
    {
        if ($this->depth <= 0) {
            throw new LogicException('Depth cannot be smaller than zero.');
        }

        $this->depth -= 1;
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer;

use JMS\Serializer\Exception\LogicException;
use JMS\Serializer\Exception\RuntimeException;
use JMS\Serializer\Exclusion\DepthExclusionStrategy;
use JMS\Serializer\Exclusion\DisjunctExclusionStrategy;
use JMS\Serializer\Exclusion\ExclusionStrategyInterface;
use JMS\Serializer\Exclusion\GroupsExclusionStrategy;
use JMS\Serializer\Exclusion\VersionExclusionStrategy;
use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\Metadata\PropertyMetadata;
use Metadata\MetadataFactoryInterface;

abstract class Context
{
    /**
     * @var array
     */
    private $attributes = [];

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

    /**
     * @var VisitorInterface
     */
    private $visitor;

    /**
     * @var GraphNavigatorInterface
     */
    private $navigator;

    /**
     * @var MetadataFactoryInterface
     */
    private $metadataFactory;

    /** @var ExclusionStrategyInterface */
    private $exclusionStrategy;

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

    /** @var \SplStack */
    private $metadataStack;

    public function __construct()
    {
        $this->metadataStack = new \SplStack();
    }

    public function initialize(string $format, VisitorInterface $visitor, GraphNavigatorInterface $navigator, MetadataFactoryInterface $factory): void
    {
        if ($this->initialized) {
            throw new LogicException('This context was already initialized, and cannot be re-used.');
        }

        $this->format = $format;
        $this->visitor = $visitor;
        $this->navigator = $navigator;
        $this->metadataFactory = $factory;
        $this->metadataStack = new \SplStack();

        if (isset($this->attributes['groups'])) {
            $this->addExclusionStrategy(new GroupsExclusionStrategy($this->attributes['groups']));
        }

        if (isset($this->attributes['version'])) {
            $this->addExclusionStrategy(new VersionExclusionStrategy($this->attributes['version']));
        }

        if (!empty($this->attributes['max_depth_checks'])) {
            $this->addExclusionStrategy(new DepthExclusionStrategy());
        }

        $this->initialized = true;
    }

    public function getMetadataFactory(): MetadataFactoryInterface
    {
        return $this->metadataFactory;
    }

    public function getVisitor(): VisitorInterface
    {
        return $this->visitor;
    }

    public function getNavigator(): GraphNavigatorInterface
    {
        return $this->navigator;
    }

    public function getExclusionStrategy(): ?ExclusionStrategyInterface
    {
        return $this->exclusionStrategy;
    }

    /**
     * @return mixed
     */
    public function getAttribute(string $key)
    {
        return $this->attributes[$key];
    }

    public function hasAttribute(string $key): bool
    {
        return isset($this->attributes[$key]);
    }

    /**
     * @param mixed $value
     *
     * @return $this
     */
    public function setAttribute(string $key, $value): self
    {
        $this->assertMutable();
        $this->attributes[$key] = $value;

        return $this;
    }

    final protected function assertMutable(): void
    {
        if (!$this->initialized) {
            return;
        }

        throw new LogicException('This context was already initialized and is immutable; you cannot modify it anymore.');
    }

    /**
     * @return $this
     */
    public function addExclusionStrategy(ExclusionStrategyInterface $strategy): self
    {
        $this->assertMutable();

        if (null === $this->exclusionStrategy) {
            $this->exclusionStrategy = $strategy;

            return $this;
        }

        if ($this->exclusionStrategy instanceof DisjunctExclusionStrategy) {
            $this->exclusionStrategy->addStrategy($strategy);

            return $this;
        }

        $this->exclusionStrategy = new DisjunctExclusionStrategy([
            $this->exclusionStrategy,
            $strategy,
        ]);

        return $this;
    }

    /**
     * @return $this
     */
    public function setVersion(string $version): self
    {
        $this->attributes['version'] = $version;

        return $this;
    }

    /**
     * @param array|string $groups
     *
     * @return $this
     */
    public function setGroups($groups): self
    {
        if (empty($groups)) {
            throw new LogicException('The groups must not be empty.');
        }

        $this->attributes['groups'] = (array) $groups;

        return $this;
    }

    /**
     * @return $this
     */
    public function enableMaxDepthChecks(): self
    {
        $this->attributes['max_depth_checks'] = true;

        return $this;
    }

    public function getFormat(): string
    {
        return $this->format;
    }

    public function pushClassMetadata(ClassMetadata $metadata): void
    {
        $this->metadataStack->push($metadata);
    }

    public function pushPropertyMetadata(PropertyMetadata $metadata): void
    {
        $this->metadataStack->push($metadata);
    }

    public function popPropertyMetadata(): void
    {
        $metadata = $this->metadataStack->pop();

        if (!$metadata instanceof PropertyMetadata) {
            throw new RuntimeException('Context metadataStack not working well');
        }
    }

    public function popClassMetadata(): void
    {
        $metadata = $this->metadataStack->pop();

        if (!$metadata instanceof ClassMetadata) {
            throw new RuntimeException('Context metadataStack not working well');
        }
    }

    public function getMetadataStack(): \SplStack
    {
        return $this->metadataStack;
    }

    public function getCurrentPath(): array
    {
        if (!$this->metadataStack) {
            return [];
        }

        $paths = [];
        foreach ($this->metadataStack as $metadata) {
            if ($metadata instanceof PropertyMetadata) {
                array_unshift($paths, $metadata->name);
            }
        }

        return $paths;
    }

    abstract public function getDepth(): int;

    abstract public function getDirection(): int;

    public function close(): void
    {
        unset($this->visitor, $this->navigator);
    }
}
<?php

declare(strict_types=1);

namespace JMS\Serializer;

use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\Common\Annotations\CachedReader;
use Doctrine\Common\Annotations\PsrCachedReader;
use Doctrine\Common\Annotations\Reader;
use Doctrine\Common\Cache\FilesystemCache;
use JMS\Serializer\Accessor\AccessorStrategyInterface;
use JMS\Serializer\Accessor\DefaultAccessorStrategy;
use JMS\Serializer\Builder\DefaultDriverFactory;
use JMS\Serializer\Builder\DocBlockDriverFactory;
use JMS\Serializer\Builder\DriverFactoryInterface;
use JMS\Serializer\Construction\ObjectConstructorInterface;
use JMS\Serializer\Construction\UnserializeObjectConstructor;
use JMS\Serializer\ContextFactory\CallableDeserializationContextFactory;
use JMS\Serializer\ContextFactory\CallableSerializationContextFactory;
use JMS\Serializer\ContextFactory\DeserializationContextFactoryInterface;
use JMS\Serializer\ContextFactory\SerializationContextFactoryInterface;
use JMS\Serializer\EventDispatcher\EventDispatcher;
use JMS\Serializer\EventDispatcher\EventDispatcherInterface;
use JMS\Serializer\EventDispatcher\Subscriber\DoctrineProxySubscriber;
use JMS\Serializer\EventDispatcher\Subscriber\EnumSubscriber;
use JMS\Serializer\Exception\InvalidArgumentException;
use JMS\Serializer\Exception\RuntimeException;
use JMS\Serializer\Expression\CompilableExpressionEvaluatorInterface;
use JMS\Serializer\Expression\ExpressionEvaluatorInterface;
use JMS\Serializer\GraphNavigator\Factory\DeserializationGraphNavigatorFactory;
use JMS\Serializer\GraphNavigator\Factory\GraphNavigatorFactoryInterface;
use JMS\Serializer\GraphNavigator\Factory\SerializationGraphNavigatorFactory;
use JMS\Serializer\Handler\ArrayCollectionHandler;
use JMS\Serializer\Handler\DateHandler;
use JMS\Serializer\Handler\EnumHandler;
use JMS\Serializer\Handler\HandlerRegistry;
use JMS\Serializer\Handler\HandlerRegistryInterface;
use JMS\Serializer\Handler\IteratorHandler;
use JMS\Serializer\Handler\StdClassHandler;
use JMS\Serializer\Handler\UnionHandler;
use JMS\Serializer\Naming\CamelCaseNamingStrategy;
use JMS\Serializer\Naming\PropertyNamingStrategyInterface;
use JMS\Serializer\Naming\SerializedNameAnnotationStrategy;
use JMS\Serializer\Type\Parser;
use JMS\Serializer\Type\ParserInterface;
use JMS\Serializer\Visitor\Factory\DeserializationVisitorFactory;
use JMS\Serializer\Visitor\Factory\JsonDeserializationVisitorFactory;
use JMS\Serializer\Visitor\Factory\JsonSerializationVisitorFactory;
use JMS\Serializer\Visitor\Factory\SerializationVisitorFactory;
use JMS\Serializer\Visitor\Factory\XmlDeserializationVisitorFactory;
use JMS\Serializer\Visitor\Factory\XmlSerializationVisitorFactory;
use Metadata\Cache\CacheInterface;
use Metadata\Cache\FileCache;
use Metadata\MetadataFactory;
use Metadata\MetadataFactoryInterface;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;

/**
 * Builder for serializer instances.
 *
 * This object makes serializer construction a breeze for projects that do not use
 * any special dependency injection container.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
final class SerializerBuilder
{
    /**
     * @var string[]
     */
    private $metadataDirs = [];

    /**
     * @var HandlerRegistryInterface
     */
    private $handlerRegistry;

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

    /**
     * @var EventDispatcherInterface
     */
    private $eventDispatcher;

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

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

    /**
     * @var ObjectConstructorInterface
     */
    private $objectConstructor;

    /**
     * @var SerializationVisitorFactory[]
     */
    private $serializationVisitors;

    /**
     * @var DeserializationVisitorFactory[]
     */
    private $deserializationVisitors;

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

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

    /**
     * @var PropertyNamingStrategyInterface
     */
    private $propertyNamingStrategy;

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

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

    /**
     * @var Reader
     */
    private $annotationReader;

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

    /**
     * @var DriverFactoryInterface
     */
    private $driverFactory;

    /**
     * @var SerializationContextFactoryInterface
     */
    private $serializationContextFactory;

    /**
     * @var DeserializationContextFactoryInterface
     */
    private $deserializationContextFactory;

    /**
     * @var ParserInterface
     */
    private $typeParser;

    /**
     * @var ExpressionEvaluatorInterface
     */
    private $expressionEvaluator;

    /**
     * @var AccessorStrategyInterface
     */
    private $accessorStrategy;

    /**
     * @var CacheInterface
     */
    private $metadataCache;

    /**
     * @var bool
     */
    private $docBlockTyperResolver;

    /**
     * @param mixed ...$args
     *
     * @return SerializerBuilder
     */
    public static function create(...$args): self
    {
        return new static(...$args);
    }

    public function __construct(?HandlerRegistryInterface $handlerRegistry = null, ?EventDispatcherInterface $eventDispatcher = null)
    {
        $this->typeParser = new Parser();
        $this->handlerRegistry = $handlerRegistry ?: new HandlerRegistry();
        $this->eventDispatcher = $eventDispatcher ?: new EventDispatcher();
        $this->serializationVisitors = [];
        $this->deserializationVisitors = [];

        if ($handlerRegistry) {
            $this->handlersConfigured = true;
        }

        if ($eventDispatcher) {
            $this->listenersConfigured = true;
        }
    }

    public function setAccessorStrategy(AccessorStrategyInterface $accessorStrategy): self
    {
        $this->accessorStrategy = $accessorStrategy;

        return $this;
    }

    private function getAccessorStrategy(): AccessorStrategyInterface
    {
        if (!$this->accessorStrategy) {
            $this->accessorStrategy = new DefaultAccessorStrategy($this->expressionEvaluator);
        }

        return $this->accessorStrategy;
    }

    public function setExpressionEvaluator(ExpressionEvaluatorInterface $expressionEvaluator): self
    {
        $this->expressionEvaluator = $expressionEvaluator;

        return $this;
    }

    public function setTypeParser(ParserInterface $parser): self
    {
        $this->typeParser = $parser;

        return $this;
    }

    public function setAnnotationReader(Reader $reader): self
    {
        $this->annotationReader = $reader;

        return $this;
    }

    public function setDebug(bool $bool): self
    {
        $this->debug = $bool;

        return $this;
    }

    public function setCacheDir(string $dir): self
    {
        if (!is_dir($dir)) {
            $this->createDir($dir);
        }

        if (!is_writable($dir)) {
            throw new InvalidArgumentException(sprintf('The cache directory "%s" is not writable.', $dir));
        }

        $this->cacheDir = $dir;

        return $this;
    }

    public function addDefaultHandlers(): self
    {
        $this->handlersConfigured = true;
        $this->handlerRegistry->registerSubscribingHandler(new DateHandler());
        $this->handlerRegistry->registerSubscribingHandler(new StdClassHandler());
        $this->handlerRegistry->registerSubscribingHandler(new ArrayCollectionHandler());
        $this->handlerRegistry->registerSubscribingHandler(new IteratorHandler());

        if ($this->enableEnumSupport) {
            $this->handlerRegistry->registerSubscribingHandler(new EnumHandler());
        }

        if (PHP_VERSION_ID >= 80000) {
            $this->handlerRegistry->registerSubscribingHandler(new UnionHandler());
        }

        return $this;
    }

    public function configureHandlers(\Closure $closure): self
    {
        $this->handlersConfigured = true;
        $closure($this->handlerRegistry);

        return $this;
    }

    public function addDefaultListeners(): self
    {
        $this->listenersConfigured = true;
        $this->eventDispatcher->addSubscriber(new DoctrineProxySubscriber());
        if ($this->enableEnumSupport) {
            $this->eventDispatcher->addSubscriber(new EnumSubscriber());
        }

        return $this;
    }

    public function configureListeners(\Closure $closure): self
    {
        $this->listenersConfigured = true;
        $closure($this->eventDispatcher);

        return $this;
    }

    public function setObjectConstructor(ObjectConstructorInterface $constructor): self
    {
        $this->objectConstructor = $constructor;

        return $this;
    }

    public function setPropertyNamingStrategy(PropertyNamingStrategyInterface $propertyNamingStrategy): self
    {
        $this->propertyNamingStrategy = $propertyNamingStrategy;

        return $this;
    }

    public function setSerializationVisitor(string $format, SerializationVisitorFactory $visitor): self
    {
        $this->serializationVisitorsAdded = true;
        $this->serializationVisitors[$format] = $visitor;

        return $this;
    }

    public function setDeserializationVisitor(string $format, DeserializationVisitorFactory $visitor): self
    {
        $this->deserializationVisitorsAdded = true;
        $this->deserializationVisitors[$format] = $visitor;

        return $this;
    }

    public function addDefaultSerializationVisitors(): self
    {
        $this->serializationVisitorsAdded = true;
        $this->serializationVisitors = [
            'xml' => new XmlSerializationVisitorFactory(),
            'json' => new JsonSerializationVisitorFactory(),
        ];

        return $this;
    }

    public function addDefaultDeserializationVisitors(): self
    {
        $this->deserializationVisitorsAdded = true;
        $this->deserializationVisitors = [
            'xml' => new XmlDeserializationVisitorFactory(),
            'json' => new JsonDeserializationVisitorFactory(),
        ];

        return $this;
    }

    /**
     * @param bool $include Whether to include the metadata from the interfaces
     *
     * @return SerializerBuilder
     */
    public function includeInterfaceMetadata(bool $include): self
    {
        $this->includeInterfaceMetadata = $include;

        return $this;
    }

    /**
     * Sets a map of namespace prefixes to directories.
     *
     * This method overrides any previously defined directories.
     *
     * @param array <string,string> $namespacePrefixToDirMap
     *
     * @return SerializerBuilder
     *
     * @throws InvalidArgumentException When a directory does not exist.
     */
    public function setMetadataDirs(array $namespacePrefixToDirMap): self
    {
        foreach ($namespacePrefixToDirMap as $dir) {
            if (!is_dir($dir)) {
                throw new InvalidArgumentException(sprintf('The directory "%s" does not exist.', $dir));
            }
        }

        $this->metadataDirs = $namespacePrefixToDirMap;

        return $this;
    }

    /**
     * Adds a directory where the serializer will look for class metadata.
     *
     * The namespace prefix will make the names of the actual metadata files a bit shorter. For example, let's assume
     * that you have a directory where you only store metadata files for the ``MyApplication\Entity`` namespace.
     *
     * If you use an empty prefix, your metadata files would need to look like:
     *
     * ``my-dir/MyApplication.Entity.SomeObject.yml``
     * ``my-dir/MyApplication.Entity.OtherObject.xml``
     *
     * If you use ``MyApplication\Entity`` as prefix, your metadata files would need to look like:
     *
     * ``my-dir/SomeObject.yml``
     * ``my-dir/OtherObject.yml``
     *
     * Please keep in mind that you currently may only have one directory per namespace prefix.
     *
     * @param string $dir             The directory where metadata files are located.
     * @param string $namespacePrefix An optional prefix if you only store metadata for specific namespaces in this directory.
     *
     * @return SerializerBuilder
     *
     * @throws InvalidArgumentException When a directory does not exist.
     * @throws InvalidArgumentException When a directory has already been registered.
     */
    public function addMetadataDir(string $dir, string $namespacePrefix = ''): self
    {
        if (!is_dir($dir)) {
            throw new InvalidArgumentException(sprintf('The directory "%s" does not exist.', $dir));
        }

        if (isset($this->metadataDirs[$namespacePrefix])) {
            throw new InvalidArgumentException(sprintf('There is already a directory configured for the namespace prefix "%s". Please use replaceMetadataDir() to override directories.', $namespacePrefix));
        }

        $this->metadataDirs[$namespacePrefix] = $dir;

        return $this;
    }

    /**
     * Adds a map of namespace prefixes to directories.
     *
     * @param array <string,string> $namespacePrefixToDirMap
     *
     * @return SerializerBuilder
     */
    public function addMetadataDirs(array $namespacePrefixToDirMap): self
    {
        foreach ($namespacePrefixToDirMap as $prefix => $dir) {
            $this->addMetadataDir($dir, $prefix);
        }

        return $this;
    }

    /**
     * Similar to addMetadataDir(), but overrides an existing entry.
     *
     * @return SerializerBuilder
     *
     * @throws InvalidArgumentException When a directory does not exist.
     * @throws InvalidArgumentException When no directory is configured for the ns prefix.
     */
    public function replaceMetadataDir(string $dir, string $namespacePrefix = ''): self
    {
        if (!is_dir($dir)) {
            throw new InvalidArgumentException(sprintf('The directory "%s" does not exist.', $dir));
        }

        if (!isset($this->metadataDirs[$namespacePrefix])) {
            throw new InvalidArgumentException(sprintf('There is no directory configured for namespace prefix "%s". Please use addMetadataDir() for adding new directories.', $namespacePrefix));
        }

        $this->metadataDirs[$namespacePrefix] = $dir;

        return $this;
    }

    public function setMetadataDriverFactory(DriverFactoryInterface $driverFactory): self
    {
        $this->driverFactory = $driverFactory;

        return $this;
    }

    /**
     * @param SerializationContextFactoryInterface|callable $serializationContextFactory
     */
    public function setSerializationContextFactory($serializationContextFactory): self
    {
        if ($serializationContextFactory instanceof SerializationContextFactoryInterface) {
            $this->serializationContextFactory = $serializationContextFactory;
        } elseif (is_callable($serializationContextFactory)) {
            $this->serializationContextFactory = new CallableSerializationContextFactory(
                $serializationContextFactory,
            );
        } else {
            throw new InvalidArgumentException('expected SerializationContextFactoryInterface or callable.');
        }

        return $this;
    }

    /**
     * @param DeserializationContextFactoryInterface|callable $deserializationContextFactory
     */
    public function setDeserializationContextFactory($deserializationContextFactory): self
    {
        if ($deserializationContextFactory instanceof DeserializationContextFactoryInterface) {
            $this->deserializationContextFactory = $deserializationContextFactory;
        } elseif (is_callable($deserializationContextFactory)) {
            $this->deserializationContextFactory = new CallableDeserializationContextFactory(
                $deserializationContextFactory,
            );
        } else {
            throw new InvalidArgumentException('expected DeserializationContextFactoryInterface or callable.');
        }

        return $this;
    }

    public function enableEnumSupport(bool $enableEnumSupport = true): self
    {
        if ($enableEnumSupport && PHP_VERSION_ID < 80100) {
            throw new InvalidArgumentException('Enum support can be enabled only on PHP 8.1 or higher.');
        }

        $this->enableEnumSupport = $enableEnumSupport;

        return $this;
    }

    public function setMetadataCache(CacheInterface $cache): self
    {
        $this->metadataCache = $cache;

        return $this;
    }

    public function setDocBlockTypeResolver(bool $docBlockTypeResolver): self
    {
        $this->docBlockTyperResolver = $docBlockTypeResolver;

        return $this;
    }

    public function build(): Serializer
    {
        $annotationReader = $this->annotationReader;
        if (null === $annotationReader && class_exists(AnnotationReader::class)) {
            $annotationReader = $this->decorateAnnotationReader(new AnnotationReader());
        }

        if (null === $this->driverFactory) {
            $this->initializePropertyNamingStrategy();
            $this->driverFactory = new DefaultDriverFactory(
                $this->propertyNamingStrategy,
                $this->typeParser,
                $this->expressionEvaluator instanceof CompilableExpressionEvaluatorInterface ? $this->expressionEvaluator : null,
            );
            $this->driverFactory->enableEnumSupport($this->enableEnumSupport);
        }

        if ($this->docBlockTyperResolver) {
            $this->driverFactory = new DocBlockDriverFactory($this->driverFactory, $this->typeParser);
        }

        $metadataDriver = $this->driverFactory->createDriver($this->metadataDirs, $annotationReader);
        $metadataFactory = new MetadataFactory($metadataDriver, null, $this->debug);

        $metadataFactory->setIncludeInterfaces($this->includeInterfaceMetadata);

        if (null !== $this->metadataCache) {
            $metadataFactory->setCache($this->metadataCache);
        } elseif (null !== $this->cacheDir) {
            $this->createDir($this->cacheDir . '/metadata');
            $metadataFactory->setCache(new FileCache($this->cacheDir . '/metadata'));
        }

        if (!$this->handlersConfigured) {
            $this->addDefaultHandlers();
        }

        if (!$this->listenersConfigured) {
            $this->addDefaultListeners();
        }

        if (!$this->serializationVisitorsAdded) {
            $this->addDefaultSerializationVisitors();
        }

        if (!$this->deserializationVisitorsAdded) {
            $this->addDefaultDeserializationVisitors();
        }

        $navigatorFactories = [
            GraphNavigatorInterface::DIRECTION_SERIALIZATION => $this->getSerializationNavigatorFactory($metadataFactory),
            GraphNavigatorInterface::DIRECTION_DESERIALIZATION => $this->getDeserializationNavigatorFactory($metadataFactory),
        ];

        return new Serializer(
            $metadataFactory,
            $navigatorFactories,
            $this->serializationVisitors,
            $this->deserializationVisitors,
            $this->serializationContextFactory,
            $this->deserializationContextFactory,
            $this->typeParser,
        );
    }

    private function getSerializationNavigatorFactory(MetadataFactoryInterface $metadataFactory): GraphNavigatorFactoryInterface
    {
        return new SerializationGraphNavigatorFactory(
            $metadataFactory,
            $this->handlerRegistry,
            $this->getAccessorStrategy(),
            $this->eventDispatcher,
            $this->expressionEvaluator,
        );
    }

    private function getDeserializationNavigatorFactory(MetadataFactoryInterface $metadataFactory): GraphNavigatorFactoryInterface
    {
        return new DeserializationGraphNavigatorFactory(
            $metadataFactory,
            $this->handlerRegistry,
            $this->objectConstructor ?: new UnserializeObjectConstructor(),
            $this->getAccessorStrategy(),
            $this->eventDispatcher,
            $this->expressionEvaluator,
        );
    }

    private function initializePropertyNamingStrategy(): void
    {
        if (null !== $this->propertyNamingStrategy) {
            return;
        }

        $this->propertyNamingStrategy = new SerializedNameAnnotationStrategy(new CamelCaseNamingStrategy());
    }

    private function createDir(string $dir): void
    {
        if (is_dir($dir)) {
            return;
        }

        if (false === @mkdir($dir, 0777, true) && false === is_dir($dir)) {
            throw new RuntimeException(sprintf('Could not create directory "%s".', $dir));
        }
    }

    private function decorateAnnotationReader(Reader $annotationReader): Reader
    {
        if (null !== $this->cacheDir) {
            $this->createDir($this->cacheDir . '/annotations');
            if (class_exists(FilesystemAdapter::class)) {
                $annotationsCache = new FilesystemAdapter('', 0, $this->cacheDir . '/annotations');
                $annotationReader = new PsrCachedReader($annotationReader, $annotationsCache, $this->debug);
            } elseif (class_exists(FilesystemCache::class) && class_exists(CachedReader::class)) {
                $annotationsCache = new FilesystemCache($this->cacheDir . '/annotations');
                $annotationReader = new CachedReader($annotationReader, $annotationsCache, $this->debug);
            }
        }

        return $annotationReader;
    }
}
<?php

declare(strict_types=1);

use Rector\Config\RectorConfig;

return RectorConfig::configure()->withPaths([
    __DIR__ . '/src',
])
        ->withPhp74Sets();
<?php

// autoload_files.php @generated by Composer

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

return array(
    '6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
    'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
    '0d59ee240a4cd96ddbb4ff164fccea4d' => $vendorDir . '/symfony/polyfill-php73/bootstrap.php',
    '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
    '320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/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',
);

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.

<?php return array(
    'root' => array(
        'name' => 'sensiolabs/insight',
        'pretty_version' => 'dev-main',
        'version' => 'dev-main',
        'reference' => 'a72e27ec1564d717517142e598d7ecd21c12585f',
        'type' => 'library',
        'install_path' => __DIR__ . '/../../',
        'aliases' => array(
            0 => '1.x-dev',
        ),
        'dev' => true,
    ),
    'versions' => array(
        'doctrine/annotations' => array(
            'pretty_version' => '2.0.2',
            'version' => '2.0.2.0',
            'reference' => '901c2ee5d26eb64ff43c47976e114bf00843acf7',
            'type' => 'library',
            'install_path' => __DIR__ . '/../doctrine/annotations',
            'aliases' => array(),
            'dev_requirement' => false,
        ),
        'doctrine/deprecations' => array(
            'pretty_version' => '1.1.5',
            'version' => '1.1.5.0',
            'reference' => '459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38',
            'type' => 'library',
            'install_path' => __DIR__ . '/../doctrine/deprecations',
            'aliases' => array(),
            'dev_requirement' => false,
        ),
        'doctrine/instantiator' => array(
            'pretty_version' => '1.5.0',
            'version' => '1.5.0.0',
            'reference' => '0a0fa9780f5d4e507415a065172d26a98d02047b',
            'type' => 'library',
            'install_path' => __DIR__ . '/../doctrine/instantiator',
            'aliases' => array(),
            'dev_requirement' => false,
        ),
        'doctrine/lexer' => array(
            'pretty_version' => '2.1.1',
            'version' => '2.1.1.0',
            'reference' => '861c870e8b75f7c8f69c146c7f89cc1c0f1b49b6',
            'type' => 'library',
            'install_path' => __DIR__ . '/../doctrine/lexer',
            'aliases' => array(),
            'dev_requirement' => false,
        ),
        'jms/metadata' => array(
            'pretty_version' => '2.8.0',
            'version' => '2.8.0.0',
            'reference' => '7ca240dcac0c655eb15933ee55736ccd2ea0d7a6',
            'type' => 'library',
            'install_path' => __DIR__ . '/../jms/metadata',
            'aliases' => array(),
            'dev_requirement' => false,
        ),
        'jms/serializer' => array(
            'pretty_version' => '3.32.5',
            'version' => '3.32.5.0',
            'reference' => '7c88b1b02ff868eecc870eeddbb3b1250e4bd89c',
            'type' => 'library',
            'install_path' => __DIR__ . '/../jms/serializer',
            'aliases' => array(),
            'dev_requirement' => false,
        ),
        'monolog/monolog' => array(
            'pretty_version' => '1.27.1',
            'version' => '1.27.1.0',
            'reference' => '904713c5929655dc9b97288b69cfeedad610c9a1',
            'type' => 'library',
            'install_path' => __DIR__ . '/../monolog/monolog',
            'aliases' => array(),
            'dev_requirement' => true,
        ),
        'php-http/async-client-implementation' => array(
            'dev_requirement' => false,
            'provided' => array(
                0 => '*',
            ),
        ),
        'php-http/client-implementation' => array(
            'dev_requirement' => false,
            'provided' => array(
                0 => '*',
            ),
        ),
        'phpstan/phpdoc-parser' => array(
            'pretty_version' => '2.3.0',
            'version' => '2.3.0.0',
            'reference' => '1e0cd5370df5dd2e556a36b9c62f62e555870495',
            'type' => 'library',
            'install_path' => __DIR__ . '/../phpstan/phpdoc-parser',
            'aliases' => array(),
            'dev_requirement' => false,
        ),
        'psr/cache' => array(
            'pretty_version' => '1.0.1',
            'version' => '1.0.1.0',
            'reference' => 'd11b50ad223250cf17b86e38383413f5a6764bf8',
            'type' => 'library',
            'install_path' => __DIR__ . '/../psr/cache',
            'aliases' => array(),
            'dev_requirement' => false,
        ),
        'psr/cache-implementation' => array(
            'dev_requirement' => false,
            'provided' => array(
                0 => '1.0|2.0',
            ),
        ),
        'psr/container' => array(
            'pretty_version' => '1.1.2',
            'version' => '1.1.2.0',
            'reference' => '513e0666f7216c7459170d56df27dfcefe1689ea',
            'type' => 'library',
            'install_path' => __DIR__ . '/../psr/container',
            'aliases' => array(),
            'dev_requirement' => false,
        ),
        'psr/http-client-implementation' => array(
            'dev_requirement' => false,
            'provided' => array(
                0 => '1.0',
            ),
        ),
        'psr/log' => array(
            'pretty_version' => '1.1.4',
            'version' => '1.1.4.0',
            'reference' => 'd49695b909c3b7628b6289db5479a1c204601f11',
            'type' => 'library',
            'install_path' => __DIR__ . '/../psr/log',
            'aliases' => array(),
            'dev_requirement' => false,
        ),
        'psr/log-implementation' => array(
            'dev_requirement' => false,
            'provided' => array(
                0 => '1.0.0',
                1 => '1.0|2.0',
            ),
        ),
        'psr/simple-cache-implementation' => array(
            'dev_requirement' => false,
            'provided' => array(
                0 => '1.0|2.0',
            ),
        ),
        'sensiolabs/insight' => array(
            'pretty_version' => 'dev-main',
            'version' => 'dev-main',
            'reference' => 'a72e27ec1564d717517142e598d7ecd21c12585f',
            'type' => 'library',
            'install_path' => __DIR__ . '/../../',
            'aliases' => array(
                0 => '1.x-dev',
            ),
            'dev_requirement' => false,
        ),
        'symfony/cache' => array(
            'pretty_version' => 'v5.4.46',
            'version' => '5.4.46.0',
            'reference' => '0fe08ee32cec2748fbfea10c52d3ee02049e0f6b',
            'type' => 'library',
            'install_path' => __DIR__ . '/../symfony/cache',
            'aliases' => array(),
            'dev_requirement' => false,
        ),
        'symfony/cache-contracts' => array(
            'pretty_version' => 'v2.5.4',
            'version' => '2.5.4.0',
            'reference' => '517c3a3619dadfa6952c4651767fcadffb4df65e',
            'type' => 'library',
            'install_path' => __DIR__ . '/../symfony/cache-contracts',
            'aliases' => array(),
            'dev_requirement' => false,
        ),
        'symfony/cache-implementation' => array(
            'dev_requirement' => false,
            'provided' => array(
                0 => '1.0|2.0',
            ),
        ),
        'symfony/console' => array(
            'pretty_version' => 'v5.4.47',
            'version' => '5.4.47.0',
            'reference' => 'c4ba980ca61a9eb18ee6bcc73f28e475852bb1ed',
            'type' => 'library',
            'install_path' => __DIR__ . '/../symfony/console',
            'aliases' => array(),
            'dev_requirement' => false,
        ),
        'symfony/deprecation-contracts' => array(
            'pretty_version' => 'v2.5.4',
            'version' => '2.5.4.0',
            'reference' => '605389f2a7e5625f273b53960dc46aeaf9c62918',
            'type' => 'library',
            'install_path' => __DIR__ . '/../symfony/deprecation-contracts',
            'aliases' => array(),
            'dev_requirement' => false,
        ),
        'symfony/expression-language' => array(
            'pretty_version' => 'v5.4.45',
            'version' => '5.4.45.0',
            'reference' => 'a784b66edc4c151eb05076d04707906ee2c209a9',
            'type' => 'library',
            'install_path' => __DIR__ . '/../symfony/expression-language',
            'aliases' => array(),
            'dev_requirement' => false,
        ),
        'symfony/http-client' => array(
            'pretty_version' => 'v5.4.49',
            'version' => '5.4.49.0',
            'reference' => 'd77d8e212cde7b5c4a64142bf431522f19487c28',
            'type' => 'library',
            'install_path' => __DIR__ . '/../symfony/http-client',
            'aliases' => array(),
            'dev_requirement' => false,
        ),
        'symfony/http-client-contracts' => array(
            'pretty_version' => 'v2.5.5',
            'version' => '2.5.5.0',
            'reference' => '48ef1d0a082885877b664332b9427662065a360c',
            'type' => 'library',
            'install_path' => __DIR__ . '/../symfony/http-client-contracts',
            'aliases' => array(),
            'dev_requirement' => false,
        ),
        'symfony/http-client-implementation' => array(
            'dev_requirement' => false,
            'provided' => array(
                0 => '2.4',
            ),
        ),
        'symfony/phpunit-bridge' => array(
            'pretty_version' => 'v7.3.3',
            'version' => '7.3.3.0',
            'reference' => '7954e563ed14f924593169f6c4645d58d9d9ac77',
            'type' => 'symfony-bridge',
            'install_path' => __DIR__ . '/../symfony/phpunit-bridge',
            'aliases' => array(),
            'dev_requirement' => true,
        ),
        'symfony/polyfill-ctype' => array(
            'pretty_version' => 'v1.33.0',
            'version' => '1.33.0.0',
            'reference' => 'a3cc8b044a6ea513310cbd48ef7333b384945638',
            'type' => 'library',
            'install_path' => __DIR__ . '/../symfony/polyfill-ctype',
            'aliases' => array(),
            'dev_requirement' => false,
        ),
        'symfony/polyfill-intl-grapheme' => array(
            'pretty_version' => 'v1.33.0',
            'version' => '1.33.0.0',
            'reference' => '380872130d3a5dd3ace2f4010d95125fde5d5c70',
            'type' => 'library',
            'install_path' => __DIR__ . '/../symfony/polyfill-intl-grapheme',
            'aliases' => array(),
            'dev_requirement' => false,
        ),
        'symfony/polyfill-intl-normalizer' => array(
            'pretty_version' => 'v1.33.0',
            'version' => '1.33.0.0',
            'reference' => '3833d7255cc303546435cb650316bff708a1c75c',
            'type' => 'library',
            'install_path' => __DIR__ . '/../symfony/polyfill-intl-normalizer',
            'aliases' => array(),
            'dev_requirement' => false,
        ),
        'symfony/polyfill-mbstring' => array(
            'pretty_version' => 'v1.33.0',
            'version' => '1.33.0.0',
            'reference' => '6d857f4d76bd4b343eac26d6b539585d2bc56493',
            'type' => 'library',
            'install_path' => __DIR__ . '/../symfony/polyfill-mbstring',
            'aliases' => array(),
            'dev_requirement' => false,
        ),
        'symfony/polyfill-php73' => array(
            'pretty_version' => 'v1.33.0',
            'version' => '1.33.0.0',
            'reference' => '0f68c03565dcaaf25a890667542e8bd75fe7e5bb',
            'type' => 'library',
            'install_path' => __DIR__ . '/../symfony/polyfill-php73',
            'aliases' => array(),
            'dev_requirement' => false,
        ),
        'symfony/polyfill-php80' => array(
            'pretty_version' => 'v1.33.0',
            'version' => '1.33.0.0',
            'reference' => '0cc9dd0f17f61d8131e7df6b84bd344899fe2608',
            'type' => 'library',
            'install_path' => __DIR__ . '/../symfony/polyfill-php80',
            'aliases' => array(),
            'dev_requirement' => false,
        ),
        'symfony/service-contracts' => array(
            'pretty_version' => 'v2.5.4',
            'version' => '2.5.4.0',
            'reference' => 'f37b419f7aea2e9abf10abd261832cace12e3300',
            'type' => 'library',
            'install_path' => __DIR__ . '/../symfony/service-contracts',
            'aliases' => array(),
            'dev_requirement' => false,
        ),
        'symfony/string' => array(
            'pretty_version' => 'v5.4.47',
            'version' => '5.4.47.0',
            'reference' => '136ca7d72f72b599f2631aca474a4f8e26719799',
            'type' => 'library',
            'install_path' => __DIR__ . '/../symfony/string',
            'aliases' => array(),
            'dev_requirement' => false,
        ),
        'symfony/var-dumper' => array(
            'pretty_version' => 'v5.4.48',
            'version' => '5.4.48.0',
            'reference' => '42f18f170aa86d612c3559cfb3bd11a375df32c8',
            'type' => 'library',
            'install_path' => __DIR__ . '/../symfony/var-dumper',
            'aliases' => array(),
            'dev_requirement' => true,
        ),
        'symfony/var-exporter' => array(
            'pretty_version' => 'v5.4.45',
            'version' => '5.4.45.0',
            'reference' => '862700068db0ddfd8c5b850671e029a90246ec75',
            'type' => 'library',
            'install_path' => __DIR__ . '/../symfony/var-exporter',
            'aliases' => array(),
            'dev_requirement' => false,
        ),
    ),
);
<?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 string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
     * @internal
     */
    private static $selfDir = null;

    /**
     * @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 string
     */
    private static function getSelfDir()
    {
        if (self::$selfDir === null) {
            self::$selfDir = strtr(__DIR__, '\\', '/');
        }

        return self::$selfDir;
    }

    /**
     * @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 = self::getSelfDir();
            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

/*
 * 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_psr4.php @generated by Composer

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

return array(
    'Symfony\\Polyfill\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'),
    'Symfony\\Polyfill\\Php73\\' => array($vendorDir . '/symfony/polyfill-php73'),
    '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\\HttpClient\\' => array($vendorDir . '/symfony/http-client-contracts'),
    'Symfony\\Contracts\\Cache\\' => array($vendorDir . '/symfony/cache-contracts'),
    'Symfony\\Component\\VarExporter\\' => array($vendorDir . '/symfony/var-exporter'),
    'Symfony\\Component\\String\\' => array($vendorDir . '/symfony/string'),
    'Symfony\\Component\\HttpClient\\' => array($vendorDir . '/symfony/http-client'),
    'Symfony\\Component\\ExpressionLanguage\\' => array($vendorDir . '/symfony/expression-language'),
    'Symfony\\Component\\Console\\' => array($vendorDir . '/symfony/console'),
    'Symfony\\Component\\Cache\\' => array($vendorDir . '/symfony/cache'),
    'SensioLabs\\Insight\\' => array($baseDir . '/'),
    'Psr\\Log\\' => array($vendorDir . '/psr/log/Psr/Log'),
    'Psr\\Container\\' => array($vendorDir . '/psr/container/src'),
    'Psr\\Cache\\' => array($vendorDir . '/psr/cache/src'),
    'PHPStan\\PhpDocParser\\' => array($vendorDir . '/phpstan/phpdoc-parser/src'),
    'Metadata\\' => array($vendorDir . '/jms/metadata/src'),
    'JMS\\Serializer\\' => array($vendorDir . '/jms/serializer/src'),
    'Doctrine\\Instantiator\\' => array($vendorDir . '/doctrine/instantiator/src/Doctrine/Instantiator'),
    'Doctrine\\Deprecations\\' => array($vendorDir . '/doctrine/deprecations/src'),
    'Doctrine\\Common\\Lexer\\' => array($vendorDir . '/doctrine/lexer/src'),
    'Doctrine\\Common\\Annotations\\' => array($vendorDir . '/doctrine/annotations/lib/Doctrine/Common/Annotations'),
);
<?php

// platform_check.php @generated by Composer

$issues = array();

if (!(PHP_VERSION_ID >= 70400)) {
    $issues[] = 'Your Composer dependencies require a PHP version ">= 7.4.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;
        }
    }
    throw new \RuntimeException(
        'Composer detected issues in your platform: ' . implode(' ', $issues)
    );
}
<?php

// autoload_classmap.php @generated by Composer

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

return array(
    'Attribute' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
    'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
    'Doctrine\\Common\\Annotations\\Annotation' => $vendorDir . '/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation.php',
    'Doctrine\\Common\\Annotations\\AnnotationException' => $vendorDir . '/doctrine/annotations/lib/Doctrine/Common/Annotations/AnnotationException.php',
    'Doctrine\\Common\\Annotations\\AnnotationReader' => $vendorDir . '/doctrine/annotations/lib/Doctrine/Common/Annotations/AnnotationReader.php',
    'Doctrine\\Common\\Annotations\\AnnotationRegistry' => $vendorDir . '/doctrine/annotations/lib/Doctrine/Common/Annotations/AnnotationRegistry.php',
    'Doctrine\\Common\\Annotations\\Annotation\\Attribute' => $vendorDir . '/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation/Attribute.php',
    'Doctrine\\Common\\Annotations\\Annotation\\Attributes' => $vendorDir . '/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation/Attributes.php',
    'Doctrine\\Common\\Annotations\\Annotation\\Enum' => $vendorDir . '/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation/Enum.php',
    'Doctrine\\Common\\Annotations\\Annotation\\IgnoreAnnotation' => $vendorDir . '/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation/IgnoreAnnotation.php',
    'Doctrine\\Common\\Annotations\\Annotation\\NamedArgumentConstructor' => $vendorDir . '/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation/NamedArgumentConstructor.php',
    'Doctrine\\Common\\Annotations\\Annotation\\Required' => $vendorDir . '/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation/Required.php',
    'Doctrine\\Common\\Annotations\\Annotation\\Target' => $vendorDir . '/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation/Target.php',
    'Doctrine\\Common\\Annotations\\DocLexer' => $vendorDir . '/doctrine/annotations/lib/Doctrine/Common/Annotations/DocLexer.php',
    'Doctrine\\Common\\Annotations\\DocParser' => $vendorDir . '/doctrine/annotations/lib/Doctrine/Common/Annotations/DocParser.php',
    'Doctrine\\Common\\Annotations\\ImplicitlyIgnoredAnnotationNames' => $vendorDir . '/doctrine/annotations/lib/Doctrine/Common/Annotations/ImplicitlyIgnoredAnnotationNames.php',
    'Doctrine\\Common\\Annotations\\IndexedReader' => $vendorDir . '/doctrine/annotations/lib/Doctrine/Common/Annotations/IndexedReader.php',
    'Doctrine\\Common\\Annotations\\PhpParser' => $vendorDir . '/doctrine/annotations/lib/Doctrine/Common/Annotations/PhpParser.php',
    'Doctrine\\Common\\Annotations\\PsrCachedReader' => $vendorDir . '/doctrine/annotations/lib/Doctrine/Common/Annotations/PsrCachedReader.php',
    'Doctrine\\Common\\Annotations\\Reader' => $vendorDir . '/doctrine/annotations/lib/Doctrine/Common/Annotations/Reader.php',
    'Doctrine\\Common\\Annotations\\TokenParser' => $vendorDir . '/doctrine/annotations/lib/Doctrine/Common/Annotations/TokenParser.php',
    'Doctrine\\Common\\Lexer\\AbstractLexer' => $vendorDir . '/doctrine/lexer/src/AbstractLexer.php',
    'Doctrine\\Common\\Lexer\\Token' => $vendorDir . '/doctrine/lexer/src/Token.php',
    'Doctrine\\Deprecations\\Deprecation' => $vendorDir . '/doctrine/deprecations/src/Deprecation.php',
    'Doctrine\\Deprecations\\PHPUnit\\VerifyDeprecations' => $vendorDir . '/doctrine/deprecations/src/PHPUnit/VerifyDeprecations.php',
    'Doctrine\\Instantiator\\Exception\\ExceptionInterface' => $vendorDir . '/doctrine/instantiator/src/Doctrine/Instantiator/Exception/ExceptionInterface.php',
    'Doctrine\\Instantiator\\Exception\\InvalidArgumentException' => $vendorDir . '/doctrine/instantiator/src/Doctrine/Instantiator/Exception/InvalidArgumentException.php',
    'Doctrine\\Instantiator\\Exception\\UnexpectedValueException' => $vendorDir . '/doctrine/instantiator/src/Doctrine/Instantiator/Exception/UnexpectedValueException.php',
    'Doctrine\\Instantiator\\Instantiator' => $vendorDir . '/doctrine/instantiator/src/Doctrine/Instantiator/Instantiator.php',
    'Doctrine\\Instantiator\\InstantiatorInterface' => $vendorDir . '/doctrine/instantiator/src/Doctrine/Instantiator/InstantiatorInterface.php',
    'JMS\\Serializer\\AbstractVisitor' => $vendorDir . '/jms/serializer/src/AbstractVisitor.php',
    'JMS\\Serializer\\Accessor\\AccessorStrategyInterface' => $vendorDir . '/jms/serializer/src/Accessor/AccessorStrategyInterface.php',
    'JMS\\Serializer\\Accessor\\DefaultAccessorStrategy' => $vendorDir . '/jms/serializer/src/Accessor/DefaultAccessorStrategy.php',
    'JMS\\Serializer\\Annotation\\AccessType' => $vendorDir . '/jms/serializer/src/Annotation/AccessType.php',
    'JMS\\Serializer\\Annotation\\Accessor' => $vendorDir . '/jms/serializer/src/Annotation/Accessor.php',
    'JMS\\Serializer\\Annotation\\AccessorOrder' => $vendorDir . '/jms/serializer/src/Annotation/AccessorOrder.php',
    'JMS\\Serializer\\Annotation\\AnnotationUtilsTrait' => $vendorDir . '/jms/serializer/src/Annotation/AnnotationUtilsTrait.php',
    'JMS\\Serializer\\Annotation\\DeprecatedReadOnly' => $vendorDir . '/jms/serializer/src/Annotation/DeprecatedReadOnly.php',
    'JMS\\Serializer\\Annotation\\Discriminator' => $vendorDir . '/jms/serializer/src/Annotation/Discriminator.php',
    'JMS\\Serializer\\Annotation\\Exclude' => $vendorDir . '/jms/serializer/src/Annotation/Exclude.php',
    'JMS\\Serializer\\Annotation\\ExclusionPolicy' => $vendorDir . '/jms/serializer/src/Annotation/ExclusionPolicy.php',
    'JMS\\Serializer\\Annotation\\Expose' => $vendorDir . '/jms/serializer/src/Annotation/Expose.php',
    'JMS\\Serializer\\Annotation\\Groups' => $vendorDir . '/jms/serializer/src/Annotation/Groups.php',
    'JMS\\Serializer\\Annotation\\Inline' => $vendorDir . '/jms/serializer/src/Annotation/Inline.php',
    'JMS\\Serializer\\Annotation\\MaxDepth' => $vendorDir . '/jms/serializer/src/Annotation/MaxDepth.php',
    'JMS\\Serializer\\Annotation\\PostDeserialize' => $vendorDir . '/jms/serializer/src/Annotation/PostDeserialize.php',
    'JMS\\Serializer\\Annotation\\PostSerialize' => $vendorDir . '/jms/serializer/src/Annotation/PostSerialize.php',
    'JMS\\Serializer\\Annotation\\PreSerialize' => $vendorDir . '/jms/serializer/src/Annotation/PreSerialize.php',
    'JMS\\Serializer\\Annotation\\ReadOnlyProperty' => $vendorDir . '/jms/serializer/src/Annotation/ReadOnlyProperty.php',
    'JMS\\Serializer\\Annotation\\SerializedName' => $vendorDir . '/jms/serializer/src/Annotation/SerializedName.php',
    'JMS\\Serializer\\Annotation\\SerializerAttribute' => $vendorDir . '/jms/serializer/src/Annotation/SerializerAttribute.php',
    'JMS\\Serializer\\Annotation\\Since' => $vendorDir . '/jms/serializer/src/Annotation/Since.php',
    'JMS\\Serializer\\Annotation\\SkipWhenEmpty' => $vendorDir . '/jms/serializer/src/Annotation/SkipWhenEmpty.php',
    'JMS\\Serializer\\Annotation\\Type' => $vendorDir . '/jms/serializer/src/Annotation/Type.php',
    'JMS\\Serializer\\Annotation\\UnionDiscriminator' => $vendorDir . '/jms/serializer/src/Annotation/UnionDiscriminator.php',
    'JMS\\Serializer\\Annotation\\Until' => $vendorDir . '/jms/serializer/src/Annotation/Until.php',
    'JMS\\Serializer\\Annotation\\Version' => $vendorDir . '/jms/serializer/src/Annotation/Version.php',
    'JMS\\Serializer\\Annotation\\VirtualProperty' => $vendorDir . '/jms/serializer/src/Annotation/VirtualProperty.php',
    'JMS\\Serializer\\Annotation\\XmlAttribute' => $vendorDir . '/jms/serializer/src/Annotation/XmlAttribute.php',
    'JMS\\Serializer\\Annotation\\XmlAttributeMap' => $vendorDir . '/jms/serializer/src/Annotation/XmlAttributeMap.php',
    'JMS\\Serializer\\Annotation\\XmlCollection' => $vendorDir . '/jms/serializer/src/Annotation/XmlCollection.php',
    'JMS\\Serializer\\Annotation\\XmlDiscriminator' => $vendorDir . '/jms/serializer/src/Annotation/XmlDiscriminator.php',
    'JMS\\Serializer\\Annotation\\XmlElement' => $vendorDir . '/jms/serializer/src/Annotation/XmlElement.php',
    'JMS\\Serializer\\Annotation\\XmlKeyValuePairs' => $vendorDir . '/jms/serializer/src/Annotation/XmlKeyValuePairs.php',
    'JMS\\Serializer\\Annotation\\XmlList' => $vendorDir . '/jms/serializer/src/Annotation/XmlList.php',
    'JMS\\Serializer\\Annotation\\XmlMap' => $vendorDir . '/jms/serializer/src/Annotation/XmlMap.php',
    'JMS\\Serializer\\Annotation\\XmlNamespace' => $vendorDir . '/jms/serializer/src/Annotation/XmlNamespace.php',
    'JMS\\Serializer\\Annotation\\XmlRoot' => $vendorDir . '/jms/serializer/src/Annotation/XmlRoot.php',
    'JMS\\Serializer\\Annotation\\XmlValue' => $vendorDir . '/jms/serializer/src/Annotation/XmlValue.php',
    'JMS\\Serializer\\ArrayTransformerInterface' => $vendorDir . '/jms/serializer/src/ArrayTransformerInterface.php',
    'JMS\\Serializer\\Builder\\CallbackDriverFactory' => $vendorDir . '/jms/serializer/src/Builder/CallbackDriverFactory.php',
    'JMS\\Serializer\\Builder\\DefaultDriverFactory' => $vendorDir . '/jms/serializer/src/Builder/DefaultDriverFactory.php',
    'JMS\\Serializer\\Builder\\DocBlockDriverFactory' => $vendorDir . '/jms/serializer/src/Builder/DocBlockDriverFactory.php',
    'JMS\\Serializer\\Builder\\DriverFactoryInterface' => $vendorDir . '/jms/serializer/src/Builder/DriverFactoryInterface.php',
    'JMS\\Serializer\\Construction\\DoctrineObjectConstructor' => $vendorDir . '/jms/serializer/src/Construction/DoctrineObjectConstructor.php',
    'JMS\\Serializer\\Construction\\ObjectConstructorInterface' => $vendorDir . '/jms/serializer/src/Construction/ObjectConstructorInterface.php',
    'JMS\\Serializer\\Construction\\UnserializeObjectConstructor' => $vendorDir . '/jms/serializer/src/Construction/UnserializeObjectConstructor.php',
    'JMS\\Serializer\\Context' => $vendorDir . '/jms/serializer/src/Context.php',
    'JMS\\Serializer\\ContextFactory\\CallableContextFactory' => $vendorDir . '/jms/serializer/src/ContextFactory/CallableContextFactory.php',
    'JMS\\Serializer\\ContextFactory\\CallableDeserializationContextFactory' => $vendorDir . '/jms/serializer/src/ContextFactory/CallableDeserializationContextFactory.php',
    'JMS\\Serializer\\ContextFactory\\CallableSerializationContextFactory' => $vendorDir . '/jms/serializer/src/ContextFactory/CallableSerializationContextFactory.php',
    'JMS\\Serializer\\ContextFactory\\DefaultDeserializationContextFactory' => $vendorDir . '/jms/serializer/src/ContextFactory/DefaultDeserializationContextFactory.php',
    'JMS\\Serializer\\ContextFactory\\DefaultSerializationContextFactory' => $vendorDir . '/jms/serializer/src/ContextFactory/DefaultSerializationContextFactory.php',
    'JMS\\Serializer\\ContextFactory\\DeserializationContextFactoryInterface' => $vendorDir . '/jms/serializer/src/ContextFactory/DeserializationContextFactoryInterface.php',
    'JMS\\Serializer\\ContextFactory\\SerializationContextFactoryInterface' => $vendorDir . '/jms/serializer/src/ContextFactory/SerializationContextFactoryInterface.php',
    'JMS\\Serializer\\DeserializationContext' => $vendorDir . '/jms/serializer/src/DeserializationContext.php',
    'JMS\\Serializer\\EventDispatcher\\Event' => $vendorDir . '/jms/serializer/src/EventDispatcher/Event.php',
    'JMS\\Serializer\\EventDispatcher\\EventDispatcher' => $vendorDir . '/jms/serializer/src/EventDispatcher/EventDispatcher.php',
    'JMS\\Serializer\\EventDispatcher\\EventDispatcherInterface' => $vendorDir . '/jms/serializer/src/EventDispatcher/EventDispatcherInterface.php',
    'JMS\\Serializer\\EventDispatcher\\EventSubscriberInterface' => $vendorDir . '/jms/serializer/src/EventDispatcher/EventSubscriberInterface.php',
    'JMS\\Serializer\\EventDispatcher\\Events' => $vendorDir . '/jms/serializer/src/EventDispatcher/Events.php',
    'JMS\\Serializer\\EventDispatcher\\LazyEventDispatcher' => $vendorDir . '/jms/serializer/src/EventDispatcher/LazyEventDispatcher.php',
    'JMS\\Serializer\\EventDispatcher\\ObjectEvent' => $vendorDir . '/jms/serializer/src/EventDispatcher/ObjectEvent.php',
    'JMS\\Serializer\\EventDispatcher\\PreDeserializeEvent' => $vendorDir . '/jms/serializer/src/EventDispatcher/PreDeserializeEvent.php',
    'JMS\\Serializer\\EventDispatcher\\PreSerializeEvent' => $vendorDir . '/jms/serializer/src/EventDispatcher/PreSerializeEvent.php',
    'JMS\\Serializer\\EventDispatcher\\Subscriber\\DoctrineProxySubscriber' => $vendorDir . '/jms/serializer/src/EventDispatcher/Subscriber/DoctrineProxySubscriber.php',
    'JMS\\Serializer\\EventDispatcher\\Subscriber\\EnumSubscriber' => $vendorDir . '/jms/serializer/src/EventDispatcher/Subscriber/EnumSubscriber.php',
    'JMS\\Serializer\\EventDispatcher\\Subscriber\\SymfonyValidatorValidatorSubscriber' => $vendorDir . '/jms/serializer/src/EventDispatcher/Subscriber/SymfonyValidatorValidatorSubscriber.php',
    'JMS\\Serializer\\Exception\\CircularReferenceDetectedException' => $vendorDir . '/jms/serializer/src/Exception/CircularReferenceDetectedException.php',
    'JMS\\Serializer\\Exception\\Exception' => $vendorDir . '/jms/serializer/src/Exception/Exception.php',
    'JMS\\Serializer\\Exception\\ExcludedClassException' => $vendorDir . '/jms/serializer/src/Exception/ExcludedClassException.php',
    'JMS\\Serializer\\Exception\\ExpressionLanguageRequiredException' => $vendorDir . '/jms/serializer/src/Exception/ExpressionLanguageRequiredException.php',
    'JMS\\Serializer\\Exception\\InvalidArgumentException' => $vendorDir . '/jms/serializer/src/Exception/InvalidArgumentException.php',
    'JMS\\Serializer\\Exception\\InvalidMetadataException' => $vendorDir . '/jms/serializer/src/Exception/InvalidMetadataException.php',
    'JMS\\Serializer\\Exception\\LogicException' => $vendorDir . '/jms/serializer/src/Exception/LogicException.php',
    'JMS\\Serializer\\Exception\\NonCastableTypeException' => $vendorDir . '/jms/serializer/src/Exception/NonCastableTypeException.php',
    'JMS\\Serializer\\Exception\\NonFloatCastableTypeException' => $vendorDir . '/jms/serializer/src/Exception/NonFloatCastableTypeException.php',
    'JMS\\Serializer\\Exception\\NonIntCastableTypeException' => $vendorDir . '/jms/serializer/src/Exception/NonIntCastableTypeException.php',
    'JMS\\Serializer\\Exception\\NonStringCastableTypeException' => $vendorDir . '/jms/serializer/src/Exception/NonStringCastableTypeException.php',
    'JMS\\Serializer\\Exception\\NonVisitableTypeException' => $vendorDir . '/jms/serializer/src/Exception/NonVisitableTypeException.php',
    'JMS\\Serializer\\Exception\\NotAcceptableException' => $vendorDir . '/jms/serializer/src/Exception/NotAcceptableException.php',
    'JMS\\Serializer\\Exception\\ObjectConstructionException' => $vendorDir . '/jms/serializer/src/Exception/ObjectConstructionException.php',
    'JMS\\Serializer\\Exception\\RuntimeException' => $vendorDir . '/jms/serializer/src/Exception/RuntimeException.php',
    'JMS\\Serializer\\Exception\\SkipHandlerException' => $vendorDir . '/jms/serializer/src/Exception/SkipHandlerException.php',
    'JMS\\Serializer\\Exception\\UninitializedPropertyException' => $vendorDir . '/jms/serializer/src/Exception/UninitializedPropertyException.php',
    'JMS\\Serializer\\Exception\\UnsupportedFormatException' => $vendorDir . '/jms/serializer/src/Exception/UnsupportedFormatException.php',
    'JMS\\Serializer\\Exception\\ValidationFailedException' => $vendorDir . '/jms/serializer/src/Exception/ValidationFailedException.php',
    'JMS\\Serializer\\Exception\\XmlErrorException' => $vendorDir . '/jms/serializer/src/Exception/XmlErrorException.php',
    'JMS\\Serializer\\Exclusion\\DepthExclusionStrategy' => $vendorDir . '/jms/serializer/src/Exclusion/DepthExclusionStrategy.php',
    'JMS\\Serializer\\Exclusion\\DisjunctExclusionStrategy' => $vendorDir . '/jms/serializer/src/Exclusion/DisjunctExclusionStrategy.php',
    'JMS\\Serializer\\Exclusion\\ExclusionStrategyInterface' => $vendorDir . '/jms/serializer/src/Exclusion/ExclusionStrategyInterface.php',
    'JMS\\Serializer\\Exclusion\\ExpressionLanguageExclusionStrategy' => $vendorDir . '/jms/serializer/src/Exclusion/ExpressionLanguageExclusionStrategy.php',
    'JMS\\Serializer\\Exclusion\\GroupsExclusionStrategy' => $vendorDir . '/jms/serializer/src/Exclusion/GroupsExclusionStrategy.php',
    'JMS\\Serializer\\Exclusion\\VersionExclusionStrategy' => $vendorDir . '/jms/serializer/src/Exclusion/VersionExclusionStrategy.php',
    'JMS\\Serializer\\Expression\\CompilableExpressionEvaluatorInterface' => $vendorDir . '/jms/serializer/src/Expression/CompilableExpressionEvaluatorInterface.php',
    'JMS\\Serializer\\Expression\\Expression' => $vendorDir . '/jms/serializer/src/Expression/Expression.php',
    'JMS\\Serializer\\Expression\\ExpressionEvaluator' => $vendorDir . '/jms/serializer/src/Expression/ExpressionEvaluator.php',
    'JMS\\Serializer\\Expression\\ExpressionEvaluatorInterface' => $vendorDir . '/jms/serializer/src/Expression/ExpressionEvaluatorInterface.php',
    'JMS\\Serializer\\Functions' => $vendorDir . '/jms/serializer/src/Functions.php',
    'JMS\\Serializer\\GraphNavigator' => $vendorDir . '/jms/serializer/src/GraphNavigator.php',
    'JMS\\Serializer\\GraphNavigatorInterface' => $vendorDir . '/jms/serializer/src/GraphNavigatorInterface.php',
    'JMS\\Serializer\\GraphNavigator\\DeserializationGraphNavigator' => $vendorDir . '/jms/serializer/src/GraphNavigator/DeserializationGraphNavigator.php',
    'JMS\\Serializer\\GraphNavigator\\Factory\\DeserializationGraphNavigatorFactory' => $vendorDir . '/jms/serializer/src/GraphNavigator/Factory/DeserializationGraphNavigatorFactory.php',
    'JMS\\Serializer\\GraphNavigator\\Factory\\GraphNavigatorFactoryInterface' => $vendorDir . '/jms/serializer/src/GraphNavigator/Factory/GraphNavigatorFactoryInterface.php',
    'JMS\\Serializer\\GraphNavigator\\Factory\\SerializationGraphNavigatorFactory' => $vendorDir . '/jms/serializer/src/GraphNavigator/Factory/SerializationGraphNavigatorFactory.php',
    'JMS\\Serializer\\GraphNavigator\\SerializationGraphNavigator' => $vendorDir . '/jms/serializer/src/GraphNavigator/SerializationGraphNavigator.php',
    'JMS\\Serializer\\Handler\\ArrayCollectionHandler' => $vendorDir . '/jms/serializer/src/Handler/ArrayCollectionHandler.php',
    'JMS\\Serializer\\Handler\\ConstraintViolationHandler' => $vendorDir . '/jms/serializer/src/Handler/ConstraintViolationHandler.php',
    'JMS\\Serializer\\Handler\\DateHandler' => $vendorDir . '/jms/serializer/src/Handler/DateHandler.php',
    'JMS\\Serializer\\Handler\\EnumHandler' => $vendorDir . '/jms/serializer/src/Handler/EnumHandler.php',
    'JMS\\Serializer\\Handler\\FormErrorHandler' => $vendorDir . '/jms/serializer/src/Handler/FormErrorHandler.php',
    'JMS\\Serializer\\Handler\\HandlerRegistry' => $vendorDir . '/jms/serializer/src/Handler/HandlerRegistry.php',
    'JMS\\Serializer\\Handler\\HandlerRegistryInterface' => $vendorDir . '/jms/serializer/src/Handler/HandlerRegistryInterface.php',
    'JMS\\Serializer\\Handler\\IteratorHandler' => $vendorDir . '/jms/serializer/src/Handler/IteratorHandler.php',
    'JMS\\Serializer\\Handler\\LazyHandlerRegistry' => $vendorDir . '/jms/serializer/src/Handler/LazyHandlerRegistry.php',
    'JMS\\Serializer\\Handler\\StdClassHandler' => $vendorDir . '/jms/serializer/src/Handler/StdClassHandler.php',
    'JMS\\Serializer\\Handler\\SubscribingHandlerInterface' => $vendorDir . '/jms/serializer/src/Handler/SubscribingHandlerInterface.php',
    'JMS\\Serializer\\Handler\\SymfonyUidHandler' => $vendorDir . '/jms/serializer/src/Handler/SymfonyUidHandler.php',
    'JMS\\Serializer\\Handler\\UnionHandler' => $vendorDir . '/jms/serializer/src/Handler/UnionHandler.php',
    'JMS\\Serializer\\JsonDeserializationStrictVisitor' => $vendorDir . '/jms/serializer/src/JsonDeserializationStrictVisitor.php',
    'JMS\\Serializer\\JsonDeserializationVisitor' => $vendorDir . '/jms/serializer/src/JsonDeserializationVisitor.php',
    'JMS\\Serializer\\JsonSerializationVisitor' => $vendorDir . '/jms/serializer/src/JsonSerializationVisitor.php',
    'JMS\\Serializer\\Metadata\\ClassMetadata' => $vendorDir . '/jms/serializer/src/Metadata/ClassMetadata.php',
    'JMS\\Serializer\\Metadata\\Driver\\AbstractDoctrineTypeDriver' => $vendorDir . '/jms/serializer/src/Metadata/Driver/AbstractDoctrineTypeDriver.php',
    'JMS\\Serializer\\Metadata\\Driver\\AnnotationDriver' => $vendorDir . '/jms/serializer/src/Metadata/Driver/AnnotationDriver.php',
    'JMS\\Serializer\\Metadata\\Driver\\AnnotationOrAttributeDriver' => $vendorDir . '/jms/serializer/src/Metadata/Driver/AnnotationOrAttributeDriver.php',
    'JMS\\Serializer\\Metadata\\Driver\\AttributeDriver' => $vendorDir . '/jms/serializer/src/Metadata/Driver/AttributeDriver.php',
    'JMS\\Serializer\\Metadata\\Driver\\AttributeDriver\\AttributeReader' => $vendorDir . '/jms/serializer/src/Metadata/Driver/AttributeDriver/AttributeReader.php',
    'JMS\\Serializer\\Metadata\\Driver\\DefaultValuePropertyDriver' => $vendorDir . '/jms/serializer/src/Metadata/Driver/DefaultValuePropertyDriver.php',
    'JMS\\Serializer\\Metadata\\Driver\\DocBlockDriver' => $vendorDir . '/jms/serializer/src/Metadata/Driver/DocBlockDriver.php',
    'JMS\\Serializer\\Metadata\\Driver\\DocBlockDriver\\DocBlockTypeResolver' => $vendorDir . '/jms/serializer/src/Metadata/Driver/DocBlockDriver/DocBlockTypeResolver.php',
    'JMS\\Serializer\\Metadata\\Driver\\DoctrinePHPCRTypeDriver' => $vendorDir . '/jms/serializer/src/Metadata/Driver/DoctrinePHPCRTypeDriver.php',
    'JMS\\Serializer\\Metadata\\Driver\\DoctrineTypeDriver' => $vendorDir . '/jms/serializer/src/Metadata/Driver/DoctrineTypeDriver.php',
    'JMS\\Serializer\\Metadata\\Driver\\EnumPropertiesDriver' => $vendorDir . '/jms/serializer/src/Metadata/Driver/EnumPropertiesDriver.php',
    'JMS\\Serializer\\Metadata\\Driver\\ExpressionMetadataTrait' => $vendorDir . '/jms/serializer/src/Metadata/Driver/ExpressionMetadataTrait.php',
    'JMS\\Serializer\\Metadata\\Driver\\NullDriver' => $vendorDir . '/jms/serializer/src/Metadata/Driver/NullDriver.php',
    'JMS\\Serializer\\Metadata\\Driver\\TypedPropertiesDriver' => $vendorDir . '/jms/serializer/src/Metadata/Driver/TypedPropertiesDriver.php',
    'JMS\\Serializer\\Metadata\\Driver\\XmlDriver' => $vendorDir . '/jms/serializer/src/Metadata/Driver/XmlDriver.php',
    'JMS\\Serializer\\Metadata\\Driver\\YamlDriver' => $vendorDir . '/jms/serializer/src/Metadata/Driver/YamlDriver.php',
    'JMS\\Serializer\\Metadata\\ExpressionPropertyMetadata' => $vendorDir . '/jms/serializer/src/Metadata/ExpressionPropertyMetadata.php',
    'JMS\\Serializer\\Metadata\\PropertyMetadata' => $vendorDir . '/jms/serializer/src/Metadata/PropertyMetadata.php',
    'JMS\\Serializer\\Metadata\\StaticPropertyMetadata' => $vendorDir . '/jms/serializer/src/Metadata/StaticPropertyMetadata.php',
    'JMS\\Serializer\\Metadata\\VirtualPropertyMetadata' => $vendorDir . '/jms/serializer/src/Metadata/VirtualPropertyMetadata.php',
    'JMS\\Serializer\\Naming\\CamelCaseNamingStrategy' => $vendorDir . '/jms/serializer/src/Naming/CamelCaseNamingStrategy.php',
    'JMS\\Serializer\\Naming\\IdenticalPropertyNamingStrategy' => $vendorDir . '/jms/serializer/src/Naming/IdenticalPropertyNamingStrategy.php',
    'JMS\\Serializer\\Naming\\PropertyNamingStrategyInterface' => $vendorDir . '/jms/serializer/src/Naming/PropertyNamingStrategyInterface.php',
    'JMS\\Serializer\\Naming\\SerializedNameAnnotationStrategy' => $vendorDir . '/jms/serializer/src/Naming/SerializedNameAnnotationStrategy.php',
    'JMS\\Serializer\\NullAwareVisitorInterface' => $vendorDir . '/jms/serializer/src/NullAwareVisitorInterface.php',
    'JMS\\Serializer\\Ordering\\AlphabeticalPropertyOrderingStrategy' => $vendorDir . '/jms/serializer/src/Ordering/AlphabeticalPropertyOrderingStrategy.php',
    'JMS\\Serializer\\Ordering\\CustomPropertyOrderingStrategy' => $vendorDir . '/jms/serializer/src/Ordering/CustomPropertyOrderingStrategy.php',
    'JMS\\Serializer\\Ordering\\IdenticalPropertyOrderingStrategy' => $vendorDir . '/jms/serializer/src/Ordering/IdenticalPropertyOrderingStrategy.php',
    'JMS\\Serializer\\Ordering\\PropertyOrderingInterface' => $vendorDir . '/jms/serializer/src/Ordering/PropertyOrderingInterface.php',
    'JMS\\Serializer\\SerializationContext' => $vendorDir . '/jms/serializer/src/SerializationContext.php',
    'JMS\\Serializer\\Serializer' => $vendorDir . '/jms/serializer/src/Serializer.php',
    'JMS\\Serializer\\SerializerBuilder' => $vendorDir . '/jms/serializer/src/SerializerBuilder.php',
    'JMS\\Serializer\\SerializerInterface' => $vendorDir . '/jms/serializer/src/SerializerInterface.php',
    'JMS\\Serializer\\Twig\\SerializerBaseExtension' => $vendorDir . '/jms/serializer/src/Twig/SerializerBaseExtension.php',
    'JMS\\Serializer\\Twig\\SerializerExtension' => $vendorDir . '/jms/serializer/src/Twig/SerializerExtension.php',
    'JMS\\Serializer\\Twig\\SerializerRuntimeExtension' => $vendorDir . '/jms/serializer/src/Twig/SerializerRuntimeExtension.php',
    'JMS\\Serializer\\Twig\\SerializerRuntimeHelper' => $vendorDir . '/jms/serializer/src/Twig/SerializerRuntimeHelper.php',
    'JMS\\Serializer\\Type\\Exception\\Exception' => $vendorDir . '/jms/serializer/src/Type/Exception/Exception.php',
    'JMS\\Serializer\\Type\\Exception\\InvalidNode' => $vendorDir . '/jms/serializer/src/Type/Exception/InvalidNode.php',
    'JMS\\Serializer\\Type\\Exception\\SyntaxError' => $vendorDir . '/jms/serializer/src/Type/Exception/SyntaxError.php',
    'JMS\\Serializer\\Type\\Lexer' => $vendorDir . '/jms/serializer/src/Type/Lexer.php',
    'JMS\\Serializer\\Type\\Parser' => $vendorDir . '/jms/serializer/src/Type/Parser.php',
    'JMS\\Serializer\\Type\\ParserInterface' => $vendorDir . '/jms/serializer/src/Type/ParserInterface.php',
    'JMS\\Serializer\\Type\\Type' => $vendorDir . '/jms/serializer/src/Type/Type.php',
    'JMS\\Serializer\\VisitorInterface' => $vendorDir . '/jms/serializer/src/VisitorInterface.php',
    'JMS\\Serializer\\Visitor\\DeserializationVisitorInterface' => $vendorDir . '/jms/serializer/src/Visitor/DeserializationVisitorInterface.php',
    'JMS\\Serializer\\Visitor\\Factory\\DeserializationVisitorFactory' => $vendorDir . '/jms/serializer/src/Visitor/Factory/DeserializationVisitorFactory.php',
    'JMS\\Serializer\\Visitor\\Factory\\JsonDeserializationVisitorFactory' => $vendorDir . '/jms/serializer/src/Visitor/Factory/JsonDeserializationVisitorFactory.php',
    'JMS\\Serializer\\Visitor\\Factory\\JsonSerializationVisitorFactory' => $vendorDir . '/jms/serializer/src/Visitor/Factory/JsonSerializationVisitorFactory.php',
    'JMS\\Serializer\\Visitor\\Factory\\SerializationVisitorFactory' => $vendorDir . '/jms/serializer/src/Visitor/Factory/SerializationVisitorFactory.php',
    'JMS\\Serializer\\Visitor\\Factory\\XmlDeserializationVisitorFactory' => $vendorDir . '/jms/serializer/src/Visitor/Factory/XmlDeserializationVisitorFactory.php',
    'JMS\\Serializer\\Visitor\\Factory\\XmlSerializationVisitorFactory' => $vendorDir . '/jms/serializer/src/Visitor/Factory/XmlSerializationVisitorFactory.php',
    'JMS\\Serializer\\Visitor\\SerializationVisitorInterface' => $vendorDir . '/jms/serializer/src/Visitor/SerializationVisitorInterface.php',
    'JMS\\Serializer\\XmlDeserializationVisitor' => $vendorDir . '/jms/serializer/src/XmlDeserializationVisitor.php',
    'JMS\\Serializer\\XmlSerializationVisitor' => $vendorDir . '/jms/serializer/src/XmlSerializationVisitor.php',
    'JsonException' => $vendorDir . '/symfony/polyfill-php73/Resources/stubs/JsonException.php',
    'Metadata\\AdvancedMetadataFactoryInterface' => $vendorDir . '/jms/metadata/src/AdvancedMetadataFactoryInterface.php',
    'Metadata\\Cache\\CacheInterface' => $vendorDir . '/jms/metadata/src/Cache/CacheInterface.php',
    'Metadata\\Cache\\ClearableCacheInterface' => $vendorDir . '/jms/metadata/src/Cache/ClearableCacheInterface.php',
    'Metadata\\Cache\\DoctrineCacheAdapter' => $vendorDir . '/jms/metadata/src/Cache/DoctrineCacheAdapter.php',
    'Metadata\\Cache\\FileCache' => $vendorDir . '/jms/metadata/src/Cache/FileCache.php',
    'Metadata\\Cache\\PsrCacheAdapter' => $vendorDir . '/jms/metadata/src/Cache/PsrCacheAdapter.php',
    'Metadata\\ClassHierarchyMetadata' => $vendorDir . '/jms/metadata/src/ClassHierarchyMetadata.php',
    'Metadata\\ClassMetadata' => $vendorDir . '/jms/metadata/src/ClassMetadata.php',
    'Metadata\\Driver\\AbstractFileDriver' => $vendorDir . '/jms/metadata/src/Driver/AbstractFileDriver.php',
    'Metadata\\Driver\\AdvancedDriverInterface' => $vendorDir . '/jms/metadata/src/Driver/AdvancedDriverInterface.php',
    'Metadata\\Driver\\AdvancedFileLocatorInterface' => $vendorDir . '/jms/metadata/src/Driver/AdvancedFileLocatorInterface.php',
    'Metadata\\Driver\\DriverChain' => $vendorDir . '/jms/metadata/src/Driver/DriverChain.php',
    'Metadata\\Driver\\DriverInterface' => $vendorDir . '/jms/metadata/src/Driver/DriverInterface.php',
    'Metadata\\Driver\\FileLocator' => $vendorDir . '/jms/metadata/src/Driver/FileLocator.php',
    'Metadata\\Driver\\FileLocatorInterface' => $vendorDir . '/jms/metadata/src/Driver/FileLocatorInterface.php',
    'Metadata\\Driver\\LazyLoadingDriver' => $vendorDir . '/jms/metadata/src/Driver/LazyLoadingDriver.php',
    'Metadata\\Driver\\TraceableFileLocatorInterface' => $vendorDir . '/jms/metadata/src/Driver/TraceableFileLocatorInterface.php',
    'Metadata\\MergeableClassMetadata' => $vendorDir . '/jms/metadata/src/MergeableClassMetadata.php',
    'Metadata\\MergeableInterface' => $vendorDir . '/jms/metadata/src/MergeableInterface.php',
    'Metadata\\MetadataFactory' => $vendorDir . '/jms/metadata/src/MetadataFactory.php',
    'Metadata\\MetadataFactoryInterface' => $vendorDir . '/jms/metadata/src/MetadataFactoryInterface.php',
    'Metadata\\MethodMetadata' => $vendorDir . '/jms/metadata/src/MethodMetadata.php',
    'Metadata\\NullMetadata' => $vendorDir . '/jms/metadata/src/NullMetadata.php',
    'Metadata\\PropertyMetadata' => $vendorDir . '/jms/metadata/src/PropertyMetadata.php',
    'Metadata\\SerializationHelper' => $vendorDir . '/jms/metadata/src/SerializationHelper.php',
    'Normalizer' => $vendorDir . '/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php',
    'PHPStan\\PhpDocParser\\Ast\\AbstractNodeVisitor' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/AbstractNodeVisitor.php',
    'PHPStan\\PhpDocParser\\Ast\\Attribute' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/Attribute.php',
    'PHPStan\\PhpDocParser\\Ast\\Comment' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/Comment.php',
    'PHPStan\\PhpDocParser\\Ast\\ConstExpr\\ConstExprArrayItemNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/ConstExpr/ConstExprArrayItemNode.php',
    'PHPStan\\PhpDocParser\\Ast\\ConstExpr\\ConstExprArrayNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/ConstExpr/ConstExprArrayNode.php',
    'PHPStan\\PhpDocParser\\Ast\\ConstExpr\\ConstExprFalseNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/ConstExpr/ConstExprFalseNode.php',
    'PHPStan\\PhpDocParser\\Ast\\ConstExpr\\ConstExprFloatNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/ConstExpr/ConstExprFloatNode.php',
    'PHPStan\\PhpDocParser\\Ast\\ConstExpr\\ConstExprIntegerNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/ConstExpr/ConstExprIntegerNode.php',
    'PHPStan\\PhpDocParser\\Ast\\ConstExpr\\ConstExprNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/ConstExpr/ConstExprNode.php',
    'PHPStan\\PhpDocParser\\Ast\\ConstExpr\\ConstExprNullNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/ConstExpr/ConstExprNullNode.php',
    'PHPStan\\PhpDocParser\\Ast\\ConstExpr\\ConstExprStringNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/ConstExpr/ConstExprStringNode.php',
    'PHPStan\\PhpDocParser\\Ast\\ConstExpr\\ConstExprTrueNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/ConstExpr/ConstExprTrueNode.php',
    'PHPStan\\PhpDocParser\\Ast\\ConstExpr\\ConstFetchNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/ConstExpr/ConstFetchNode.php',
    'PHPStan\\PhpDocParser\\Ast\\ConstExpr\\DoctrineConstExprStringNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/ConstExpr/DoctrineConstExprStringNode.php',
    'PHPStan\\PhpDocParser\\Ast\\Node' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/Node.php',
    'PHPStan\\PhpDocParser\\Ast\\NodeAttributes' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/NodeAttributes.php',
    'PHPStan\\PhpDocParser\\Ast\\NodeTraverser' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/NodeTraverser.php',
    'PHPStan\\PhpDocParser\\Ast\\NodeVisitor' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/NodeVisitor.php',
    'PHPStan\\PhpDocParser\\Ast\\NodeVisitor\\CloningVisitor' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/NodeVisitor/CloningVisitor.php',
    'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\AssertTagMethodValueNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/AssertTagMethodValueNode.php',
    'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\AssertTagPropertyValueNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/AssertTagPropertyValueNode.php',
    'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\AssertTagValueNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/AssertTagValueNode.php',
    'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\DeprecatedTagValueNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/DeprecatedTagValueNode.php',
    'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\Doctrine\\DoctrineAnnotation' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/Doctrine/DoctrineAnnotation.php',
    'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\Doctrine\\DoctrineArgument' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/Doctrine/DoctrineArgument.php',
    'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\Doctrine\\DoctrineArray' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/Doctrine/DoctrineArray.php',
    'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\Doctrine\\DoctrineArrayItem' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/Doctrine/DoctrineArrayItem.php',
    'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\Doctrine\\DoctrineTagValueNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/Doctrine/DoctrineTagValueNode.php',
    'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\ExtendsTagValueNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/ExtendsTagValueNode.php',
    'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\GenericTagValueNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/GenericTagValueNode.php',
    'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\ImplementsTagValueNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/ImplementsTagValueNode.php',
    'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\InvalidTagValueNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/InvalidTagValueNode.php',
    'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\MethodTagValueNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/MethodTagValueNode.php',
    'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\MethodTagValueParameterNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/MethodTagValueParameterNode.php',
    'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\MixinTagValueNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/MixinTagValueNode.php',
    'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\ParamClosureThisTagValueNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/ParamClosureThisTagValueNode.php',
    'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\ParamImmediatelyInvokedCallableTagValueNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/ParamImmediatelyInvokedCallableTagValueNode.php',
    'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\ParamLaterInvokedCallableTagValueNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/ParamLaterInvokedCallableTagValueNode.php',
    'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\ParamOutTagValueNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/ParamOutTagValueNode.php',
    'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\ParamTagValueNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/ParamTagValueNode.php',
    'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\PhpDocChildNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/PhpDocChildNode.php',
    'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\PhpDocNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/PhpDocNode.php',
    'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\PhpDocTagNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/PhpDocTagNode.php',
    'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\PhpDocTagValueNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/PhpDocTagValueNode.php',
    'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\PhpDocTextNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/PhpDocTextNode.php',
    'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\PropertyTagValueNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/PropertyTagValueNode.php',
    'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\PureUnlessCallableIsImpureTagValueNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/PureUnlessCallableIsImpureTagValueNode.php',
    'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\RequireExtendsTagValueNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/RequireExtendsTagValueNode.php',
    'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\RequireImplementsTagValueNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/RequireImplementsTagValueNode.php',
    'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\ReturnTagValueNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/ReturnTagValueNode.php',
    'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\SealedTagValueNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/SealedTagValueNode.php',
    'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\SelfOutTagValueNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/SelfOutTagValueNode.php',
    'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\TemplateTagValueNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/TemplateTagValueNode.php',
    'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\ThrowsTagValueNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/ThrowsTagValueNode.php',
    'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\TypeAliasImportTagValueNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/TypeAliasImportTagValueNode.php',
    'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\TypeAliasTagValueNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/TypeAliasTagValueNode.php',
    'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\TypelessParamTagValueNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/TypelessParamTagValueNode.php',
    'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\UsesTagValueNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/UsesTagValueNode.php',
    'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\VarTagValueNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/VarTagValueNode.php',
    'PHPStan\\PhpDocParser\\Ast\\Type\\ArrayShapeItemNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/Type/ArrayShapeItemNode.php',
    'PHPStan\\PhpDocParser\\Ast\\Type\\ArrayShapeNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/Type/ArrayShapeNode.php',
    'PHPStan\\PhpDocParser\\Ast\\Type\\ArrayShapeUnsealedTypeNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/Type/ArrayShapeUnsealedTypeNode.php',
    'PHPStan\\PhpDocParser\\Ast\\Type\\ArrayTypeNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/Type/ArrayTypeNode.php',
    'PHPStan\\PhpDocParser\\Ast\\Type\\CallableTypeNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/Type/CallableTypeNode.php',
    'PHPStan\\PhpDocParser\\Ast\\Type\\CallableTypeParameterNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/Type/CallableTypeParameterNode.php',
    'PHPStan\\PhpDocParser\\Ast\\Type\\ConditionalTypeForParameterNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/Type/ConditionalTypeForParameterNode.php',
    'PHPStan\\PhpDocParser\\Ast\\Type\\ConditionalTypeNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/Type/ConditionalTypeNode.php',
    'PHPStan\\PhpDocParser\\Ast\\Type\\ConstTypeNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/Type/ConstTypeNode.php',
    'PHPStan\\PhpDocParser\\Ast\\Type\\GenericTypeNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/Type/GenericTypeNode.php',
    'PHPStan\\PhpDocParser\\Ast\\Type\\IdentifierTypeNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/Type/IdentifierTypeNode.php',
    'PHPStan\\PhpDocParser\\Ast\\Type\\IntersectionTypeNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/Type/IntersectionTypeNode.php',
    'PHPStan\\PhpDocParser\\Ast\\Type\\InvalidTypeNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/Type/InvalidTypeNode.php',
    'PHPStan\\PhpDocParser\\Ast\\Type\\NullableTypeNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/Type/NullableTypeNode.php',
    'PHPStan\\PhpDocParser\\Ast\\Type\\ObjectShapeItemNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/Type/ObjectShapeItemNode.php',
    'PHPStan\\PhpDocParser\\Ast\\Type\\ObjectShapeNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/Type/ObjectShapeNode.php',
    'PHPStan\\PhpDocParser\\Ast\\Type\\OffsetAccessTypeNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/Type/OffsetAccessTypeNode.php',
    'PHPStan\\PhpDocParser\\Ast\\Type\\ThisTypeNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/Type/ThisTypeNode.php',
    'PHPStan\\PhpDocParser\\Ast\\Type\\TypeNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/Type/TypeNode.php',
    'PHPStan\\PhpDocParser\\Ast\\Type\\UnionTypeNode' => $vendorDir . '/phpstan/phpdoc-parser/src/Ast/Type/UnionTypeNode.php',
    'PHPStan\\PhpDocParser\\Lexer\\Lexer' => $vendorDir . '/phpstan/phpdoc-parser/src/Lexer/Lexer.php',
    'PHPStan\\PhpDocParser\\ParserConfig' => $vendorDir . '/phpstan/phpdoc-parser/src/ParserConfig.php',
    'PHPStan\\PhpDocParser\\Parser\\ConstExprParser' => $vendorDir . '/phpstan/phpdoc-parser/src/Parser/ConstExprParser.php',
    'PHPStan\\PhpDocParser\\Parser\\ParserException' => $vendorDir . '/phpstan/phpdoc-parser/src/Parser/ParserException.php',
    'PHPStan\\PhpDocParser\\Parser\\PhpDocParser' => $vendorDir . '/phpstan/phpdoc-parser/src/Parser/PhpDocParser.php',
    'PHPStan\\PhpDocParser\\Parser\\StringUnescaper' => $vendorDir . '/phpstan/phpdoc-parser/src/Parser/StringUnescaper.php',
    'PHPStan\\PhpDocParser\\Parser\\TokenIterator' => $vendorDir . '/phpstan/phpdoc-parser/src/Parser/TokenIterator.php',
    'PHPStan\\PhpDocParser\\Parser\\TypeParser' => $vendorDir . '/phpstan/phpdoc-parser/src/Parser/TypeParser.php',
    'PHPStan\\PhpDocParser\\Printer\\DiffElem' => $vendorDir . '/phpstan/phpdoc-parser/src/Printer/DiffElem.php',
    'PHPStan\\PhpDocParser\\Printer\\Differ' => $vendorDir . '/phpstan/phpdoc-parser/src/Printer/Differ.php',
    'PHPStan\\PhpDocParser\\Printer\\Printer' => $vendorDir . '/phpstan/phpdoc-parser/src/Printer/Printer.php',
    'PhpToken' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php',
    'Psr\\Cache\\CacheException' => $vendorDir . '/psr/cache/src/CacheException.php',
    'Psr\\Cache\\CacheItemInterface' => $vendorDir . '/psr/cache/src/CacheItemInterface.php',
    'Psr\\Cache\\CacheItemPoolInterface' => $vendorDir . '/psr/cache/src/CacheItemPoolInterface.php',
    'Psr\\Cache\\InvalidArgumentException' => $vendorDir . '/psr/cache/src/InvalidArgumentException.php',
    'Psr\\Container\\ContainerExceptionInterface' => $vendorDir . '/psr/container/src/ContainerExceptionInterface.php',
    'Psr\\Container\\ContainerInterface' => $vendorDir . '/psr/container/src/ContainerInterface.php',
    'Psr\\Container\\NotFoundExceptionInterface' => $vendorDir . '/psr/container/src/NotFoundExceptionInterface.php',
    'Psr\\Log\\AbstractLogger' => $vendorDir . '/psr/log/Psr/Log/AbstractLogger.php',
    'Psr\\Log\\InvalidArgumentException' => $vendorDir . '/psr/log/Psr/Log/InvalidArgumentException.php',
    'Psr\\Log\\LogLevel' => $vendorDir . '/psr/log/Psr/Log/LogLevel.php',
    'Psr\\Log\\LoggerAwareInterface' => $vendorDir . '/psr/log/Psr/Log/LoggerAwareInterface.php',
    'Psr\\Log\\LoggerAwareTrait' => $vendorDir . '/psr/log/Psr/Log/LoggerAwareTrait.php',
    'Psr\\Log\\LoggerInterface' => $vendorDir . '/psr/log/Psr/Log/LoggerInterface.php',
    'Psr\\Log\\LoggerTrait' => $vendorDir . '/psr/log/Psr/Log/LoggerTrait.php',
    'Psr\\Log\\NullLogger' => $vendorDir . '/psr/log/Psr/Log/NullLogger.php',
    'Psr\\Log\\Test\\DummyTest' => $vendorDir . '/psr/log/Psr/Log/Test/DummyTest.php',
    'Psr\\Log\\Test\\LoggerInterfaceTest' => $vendorDir . '/psr/log/Psr/Log/Test/LoggerInterfaceTest.php',
    'Psr\\Log\\Test\\TestLogger' => $vendorDir . '/psr/log/Psr/Log/Test/TestLogger.php',
    'SensioLabs\\Insight\\Cli\\Application' => $baseDir . '/Cli/Application.php',
    'SensioLabs\\Insight\\Cli\\Command\\AnalysisCommand' => $baseDir . '/Cli/Command/AnalysisCommand.php',
    'SensioLabs\\Insight\\Cli\\Command\\AnalyzeCommand' => $baseDir . '/Cli/Command/AnalyzeCommand.php',
    'SensioLabs\\Insight\\Cli\\Command\\ConfigureCommand' => $baseDir . '/Cli/Command/ConfigureCommand.php',
    'SensioLabs\\Insight\\Cli\\Command\\NeedConfigurationInterface' => $baseDir . '/Cli/Command/NeedConfigurationInterface.php',
    'SensioLabs\\Insight\\Cli\\Command\\ProjectsCommand' => $baseDir . '/Cli/Command/ProjectsCommand.php',
    'SensioLabs\\Insight\\Cli\\Command\\SelfUpdateCommand' => $baseDir . '/Cli/Command/SelfUpdateCommand.php',
    'SensioLabs\\Insight\\Cli\\Configuration' => $baseDir . '/Cli/Configuration.php',
    'SensioLabs\\Insight\\Cli\\Descriptor\\AbstractDescriptor' => $baseDir . '/Cli/Descriptor/AbstractDescriptor.php',
    'SensioLabs\\Insight\\Cli\\Descriptor\\JsonDescriptor' => $baseDir . '/Cli/Descriptor/JsonDescriptor.php',
    'SensioLabs\\Insight\\Cli\\Descriptor\\PmdDescriptor' => $baseDir . '/Cli/Descriptor/PmdDescriptor.php',
    'SensioLabs\\Insight\\Cli\\Descriptor\\TextDescriptor' => $baseDir . '/Cli/Descriptor/TextDescriptor.php',
    'SensioLabs\\Insight\\Cli\\Descriptor\\XmlDescriptor' => $baseDir . '/Cli/Descriptor/XmlDescriptor.php',
    'SensioLabs\\Insight\\Cli\\Helper\\ConfigurationHelper' => $baseDir . '/Cli/Helper/ConfigurationHelper.php',
    'SensioLabs\\Insight\\Cli\\Helper\\DescriptorHelper' => $baseDir . '/Cli/Helper/DescriptorHelper.php',
    'SensioLabs\\Insight\\Cli\\Helper\\FailConditionHelper' => $baseDir . '/Cli/Helper/FailConditionHelper.php',
    'SensioLabs\\Insight\\Sdk\\Api' => $baseDir . '/Sdk/Api.php',
    'SensioLabs\\Insight\\Sdk\\Exception\\ApiClientException' => $baseDir . '/Sdk/Exception/ApiClientException.php',
    'SensioLabs\\Insight\\Sdk\\Exception\\ApiParserException' => $baseDir . '/Sdk/Exception/ApiParserException.php',
    'SensioLabs\\Insight\\Sdk\\Exception\\ApiServerException' => $baseDir . '/Sdk/Exception/ApiServerException.php',
    'SensioLabs\\Insight\\Sdk\\Exception\\ExceptionInterface' => $baseDir . '/Sdk/Exception/ExceptionInterface.php',
    'SensioLabs\\Insight\\Sdk\\Model\\Analyses' => $baseDir . '/Sdk/Model/Analyses.php',
    'SensioLabs\\Insight\\Sdk\\Model\\Analysis' => $baseDir . '/Sdk/Model/Analysis.php',
    'SensioLabs\\Insight\\Sdk\\Model\\Error' => $baseDir . '/Sdk/Model/Error.php',
    'SensioLabs\\Insight\\Sdk\\Model\\Link' => $baseDir . '/Sdk/Model/Link.php',
    'SensioLabs\\Insight\\Sdk\\Model\\Project' => $baseDir . '/Sdk/Model/Project.php',
    'SensioLabs\\Insight\\Sdk\\Model\\Projects' => $baseDir . '/Sdk/Model/Projects.php',
    'SensioLabs\\Insight\\Sdk\\Model\\Violation' => $baseDir . '/Sdk/Model/Violation.php',
    'SensioLabs\\Insight\\Sdk\\Model\\Violations' => $baseDir . '/Sdk/Model/Violations.php',
    'SensioLabs\\Insight\\Sdk\\Parser' => $baseDir . '/Sdk/Parser.php',
    'Stringable' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Stringable.php',
    'Symfony\\Component\\Cache\\Adapter\\AbstractAdapter' => $vendorDir . '/symfony/cache/Adapter/AbstractAdapter.php',
    'Symfony\\Component\\Cache\\Adapter\\AbstractTagAwareAdapter' => $vendorDir . '/symfony/cache/Adapter/AbstractTagAwareAdapter.php',
    'Symfony\\Component\\Cache\\Adapter\\AdapterInterface' => $vendorDir . '/symfony/cache/Adapter/AdapterInterface.php',
    'Symfony\\Component\\Cache\\Adapter\\ApcuAdapter' => $vendorDir . '/symfony/cache/Adapter/ApcuAdapter.php',
    'Symfony\\Component\\Cache\\Adapter\\ArrayAdapter' => $vendorDir . '/symfony/cache/Adapter/ArrayAdapter.php',
    'Symfony\\Component\\Cache\\Adapter\\ChainAdapter' => $vendorDir . '/symfony/cache/Adapter/ChainAdapter.php',
    'Symfony\\Component\\Cache\\Adapter\\CouchbaseBucketAdapter' => $vendorDir . '/symfony/cache/Adapter/CouchbaseBucketAdapter.php',
    'Symfony\\Component\\Cache\\Adapter\\CouchbaseCollectionAdapter' => $vendorDir . '/symfony/cache/Adapter/CouchbaseCollectionAdapter.php',
    'Symfony\\Component\\Cache\\Adapter\\DoctrineAdapter' => $vendorDir . '/symfony/cache/Adapter/DoctrineAdapter.php',
    'Symfony\\Component\\Cache\\Adapter\\DoctrineDbalAdapter' => $vendorDir . '/symfony/cache/Adapter/DoctrineDbalAdapter.php',
    'Symfony\\Component\\Cache\\Adapter\\FilesystemAdapter' => $vendorDir . '/symfony/cache/Adapter/FilesystemAdapter.php',
    'Symfony\\Component\\Cache\\Adapter\\FilesystemTagAwareAdapter' => $vendorDir . '/symfony/cache/Adapter/FilesystemTagAwareAdapter.php',
    'Symfony\\Component\\Cache\\Adapter\\MemcachedAdapter' => $vendorDir . '/symfony/cache/Adapter/MemcachedAdapter.php',
    'Symfony\\Component\\Cache\\Adapter\\NullAdapter' => $vendorDir . '/symfony/cache/Adapter/NullAdapter.php',
    'Symfony\\Component\\Cache\\Adapter\\ParameterNormalizer' => $vendorDir . '/symfony/cache/Adapter/ParameterNormalizer.php',
    'Symfony\\Component\\Cache\\Adapter\\PdoAdapter' => $vendorDir . '/symfony/cache/Adapter/PdoAdapter.php',
    'Symfony\\Component\\Cache\\Adapter\\PhpArrayAdapter' => $vendorDir . '/symfony/cache/Adapter/PhpArrayAdapter.php',
    'Symfony\\Component\\Cache\\Adapter\\PhpFilesAdapter' => $vendorDir . '/symfony/cache/Adapter/PhpFilesAdapter.php',
    'Symfony\\Component\\Cache\\Adapter\\ProxyAdapter' => $vendorDir . '/symfony/cache/Adapter/ProxyAdapter.php',
    'Symfony\\Component\\Cache\\Adapter\\Psr16Adapter' => $vendorDir . '/symfony/cache/Adapter/Psr16Adapter.php',
    'Symfony\\Component\\Cache\\Adapter\\RedisAdapter' => $vendorDir . '/symfony/cache/Adapter/RedisAdapter.php',
    'Symfony\\Component\\Cache\\Adapter\\RedisTagAwareAdapter' => $vendorDir . '/symfony/cache/Adapter/RedisTagAwareAdapter.php',
    'Symfony\\Component\\Cache\\Adapter\\TagAwareAdapter' => $vendorDir . '/symfony/cache/Adapter/TagAwareAdapter.php',
    'Symfony\\Component\\Cache\\Adapter\\TagAwareAdapterInterface' => $vendorDir . '/symfony/cache/Adapter/TagAwareAdapterInterface.php',
    'Symfony\\Component\\Cache\\Adapter\\TraceableAdapter' => $vendorDir . '/symfony/cache/Adapter/TraceableAdapter.php',
    'Symfony\\Component\\Cache\\Adapter\\TraceableTagAwareAdapter' => $vendorDir . '/symfony/cache/Adapter/TraceableTagAwareAdapter.php',
    'Symfony\\Component\\Cache\\CacheItem' => $vendorDir . '/symfony/cache/CacheItem.php',
    'Symfony\\Component\\Cache\\DataCollector\\CacheDataCollector' => $vendorDir . '/symfony/cache/DataCollector/CacheDataCollector.php',
    'Symfony\\Component\\Cache\\DependencyInjection\\CacheCollectorPass' => $vendorDir . '/symfony/cache/DependencyInjection/CacheCollectorPass.php',
    'Symfony\\Component\\Cache\\DependencyInjection\\CachePoolClearerPass' => $vendorDir . '/symfony/cache/DependencyInjection/CachePoolClearerPass.php',
    'Symfony\\Component\\Cache\\DependencyInjection\\CachePoolPass' => $vendorDir . '/symfony/cache/DependencyInjection/CachePoolPass.php',
    'Symfony\\Component\\Cache\\DependencyInjection\\CachePoolPrunerPass' => $vendorDir . '/symfony/cache/DependencyInjection/CachePoolPrunerPass.php',
    'Symfony\\Component\\Cache\\DoctrineProvider' => $vendorDir . '/symfony/cache/DoctrineProvider.php',
    'Symfony\\Component\\Cache\\Exception\\CacheException' => $vendorDir . '/symfony/cache/Exception/CacheException.php',
    'Symfony\\Component\\Cache\\Exception\\InvalidArgumentException' => $vendorDir . '/symfony/cache/Exception/InvalidArgumentException.php',
    'Symfony\\Component\\Cache\\Exception\\LogicException' => $vendorDir . '/symfony/cache/Exception/LogicException.php',
    'Symfony\\Component\\Cache\\LockRegistry' => $vendorDir . '/symfony/cache/LockRegistry.php',
    'Symfony\\Component\\Cache\\Marshaller\\DefaultMarshaller' => $vendorDir . '/symfony/cache/Marshaller/DefaultMarshaller.php',
    'Symfony\\Component\\Cache\\Marshaller\\DeflateMarshaller' => $vendorDir . '/symfony/cache/Marshaller/DeflateMarshaller.php',
    'Symfony\\Component\\Cache\\Marshaller\\MarshallerInterface' => $vendorDir . '/symfony/cache/Marshaller/MarshallerInterface.php',
    'Symfony\\Component\\Cache\\Marshaller\\SodiumMarshaller' => $vendorDir . '/symfony/cache/Marshaller/SodiumMarshaller.php',
    'Symfony\\Component\\Cache\\Marshaller\\TagAwareMarshaller' => $vendorDir . '/symfony/cache/Marshaller/TagAwareMarshaller.php',
    'Symfony\\Component\\Cache\\Messenger\\EarlyExpirationDispatcher' => $vendorDir . '/symfony/cache/Messenger/EarlyExpirationDispatcher.php',
    'Symfony\\Component\\Cache\\Messenger\\EarlyExpirationHandler' => $vendorDir . '/symfony/cache/Messenger/EarlyExpirationHandler.php',
    'Symfony\\Component\\Cache\\Messenger\\EarlyExpirationMessage' => $vendorDir . '/symfony/cache/Messenger/EarlyExpirationMessage.php',
    'Symfony\\Component\\Cache\\PruneableInterface' => $vendorDir . '/symfony/cache/PruneableInterface.php',
    'Symfony\\Component\\Cache\\Psr16Cache' => $vendorDir . '/symfony/cache/Psr16Cache.php',
    'Symfony\\Component\\Cache\\ResettableInterface' => $vendorDir . '/symfony/cache/ResettableInterface.php',
    'Symfony\\Component\\Cache\\Traits\\AbstractAdapterTrait' => $vendorDir . '/symfony/cache/Traits/AbstractAdapterTrait.php',
    'Symfony\\Component\\Cache\\Traits\\ContractsTrait' => $vendorDir . '/symfony/cache/Traits/ContractsTrait.php',
    'Symfony\\Component\\Cache\\Traits\\FilesystemCommonTrait' => $vendorDir . '/symfony/cache/Traits/FilesystemCommonTrait.php',
    'Symfony\\Component\\Cache\\Traits\\FilesystemTrait' => $vendorDir . '/symfony/cache/Traits/FilesystemTrait.php',
    'Symfony\\Component\\Cache\\Traits\\ProxyTrait' => $vendorDir . '/symfony/cache/Traits/ProxyTrait.php',
    'Symfony\\Component\\Cache\\Traits\\RedisClusterNodeProxy' => $vendorDir . '/symfony/cache/Traits/RedisClusterNodeProxy.php',
    'Symfony\\Component\\Cache\\Traits\\RedisClusterProxy' => $vendorDir . '/symfony/cache/Traits/RedisClusterProxy.php',
    'Symfony\\Component\\Cache\\Traits\\RedisProxy' => $vendorDir . '/symfony/cache/Traits/RedisProxy.php',
    'Symfony\\Component\\Cache\\Traits\\RedisTrait' => $vendorDir . '/symfony/cache/Traits/RedisTrait.php',
    'Symfony\\Component\\Console\\Application' => $vendorDir . '/symfony/console/Application.php',
    'Symfony\\Component\\Console\\Attribute\\AsCommand' => $vendorDir . '/symfony/console/Attribute/AsCommand.php',
    'Symfony\\Component\\Console\\CI\\GithubActionReporter' => $vendorDir . '/symfony/console/CI/GithubActionReporter.php',
    'Symfony\\Component\\Console\\Color' => $vendorDir . '/symfony/console/Color.php',
    'Symfony\\Component\\Console\\CommandLoader\\CommandLoaderInterface' => $vendorDir . '/symfony/console/CommandLoader/CommandLoaderInterface.php',
    'Symfony\\Component\\Console\\CommandLoader\\ContainerCommandLoader' => $vendorDir . '/symfony/console/CommandLoader/ContainerCommandLoader.php',
    'Symfony\\Component\\Console\\CommandLoader\\FactoryCommandLoader' => $vendorDir . '/symfony/console/CommandLoader/FactoryCommandLoader.php',
    'Symfony\\Component\\Console\\Command\\Command' => $vendorDir . '/symfony/console/Command/Command.php',
    'Symfony\\Component\\Console\\Command\\CompleteCommand' => $vendorDir . '/symfony/console/Command/CompleteCommand.php',
    'Symfony\\Component\\Console\\Command\\DumpCompletionCommand' => $vendorDir . '/symfony/console/Command/DumpCompletionCommand.php',
    'Symfony\\Component\\Console\\Command\\HelpCommand' => $vendorDir . '/symfony/console/Command/HelpCommand.php',
    'Symfony\\Component\\Console\\Command\\LazyCommand' => $vendorDir . '/symfony/console/Command/LazyCommand.php',
    'Symfony\\Component\\Console\\Command\\ListCommand' => $vendorDir . '/symfony/console/Command/ListCommand.php',
    'Symfony\\Component\\Console\\Command\\LockableTrait' => $vendorDir . '/symfony/console/Command/LockableTrait.php',
    'Symfony\\Component\\Console\\Command\\SignalableCommandInterface' => $vendorDir . '/symfony/console/Command/SignalableCommandInterface.php',
    'Symfony\\Component\\Console\\Completion\\CompletionInput' => $vendorDir . '/symfony/console/Completion/CompletionInput.php',
    'Symfony\\Component\\Console\\Completion\\CompletionSuggestions' => $vendorDir . '/symfony/console/Completion/CompletionSuggestions.php',
    'Symfony\\Component\\Console\\Completion\\Output\\BashCompletionOutput' => $vendorDir . '/symfony/console/Completion/Output/BashCompletionOutput.php',
    'Symfony\\Component\\Console\\Completion\\Output\\CompletionOutputInterface' => $vendorDir . '/symfony/console/Completion/Output/CompletionOutputInterface.php',
    'Symfony\\Component\\Console\\Completion\\Suggestion' => $vendorDir . '/symfony/console/Completion/Suggestion.php',
    'Symfony\\Component\\Console\\ConsoleEvents' => $vendorDir . '/symfony/console/ConsoleEvents.php',
    'Symfony\\Component\\Console\\Cursor' => $vendorDir . '/symfony/console/Cursor.php',
    'Symfony\\Component\\Console\\DependencyInjection\\AddConsoleCommandPass' => $vendorDir . '/symfony/console/DependencyInjection/AddConsoleCommandPass.php',
    'Symfony\\Component\\Console\\Descriptor\\ApplicationDescription' => $vendorDir . '/symfony/console/Descriptor/ApplicationDescription.php',
    'Symfony\\Component\\Console\\Descriptor\\Descriptor' => $vendorDir . '/symfony/console/Descriptor/Descriptor.php',
    'Symfony\\Component\\Console\\Descriptor\\DescriptorInterface' => $vendorDir . '/symfony/console/Descriptor/DescriptorInterface.php',
    'Symfony\\Component\\Console\\Descriptor\\JsonDescriptor' => $vendorDir . '/symfony/console/Descriptor/JsonDescriptor.php',
    'Symfony\\Component\\Console\\Descriptor\\MarkdownDescriptor' => $vendorDir . '/symfony/console/Descriptor/MarkdownDescriptor.php',
    'Symfony\\Component\\Console\\Descriptor\\TextDescriptor' => $vendorDir . '/symfony/console/Descriptor/TextDescriptor.php',
    'Symfony\\Component\\Console\\Descriptor\\XmlDescriptor' => $vendorDir . '/symfony/console/Descriptor/XmlDescriptor.php',
    'Symfony\\Component\\Console\\EventListener\\ErrorListener' => $vendorDir . '/symfony/console/EventListener/ErrorListener.php',
    'Symfony\\Component\\Console\\Event\\ConsoleCommandEvent' => $vendorDir . '/symfony/console/Event/ConsoleCommandEvent.php',
    'Symfony\\Component\\Console\\Event\\ConsoleErrorEvent' => $vendorDir . '/symfony/console/Event/ConsoleErrorEvent.php',
    'Symfony\\Component\\Console\\Event\\ConsoleEvent' => $vendorDir . '/symfony/console/Event/ConsoleEvent.php',
    'Symfony\\Component\\Console\\Event\\ConsoleSignalEvent' => $vendorDir . '/symfony/console/Event/ConsoleSignalEvent.php',
    'Symfony\\Component\\Console\\Event\\ConsoleTerminateEvent' => $vendorDir . '/symfony/console/Event/ConsoleTerminateEvent.php',
    'Symfony\\Component\\Console\\Exception\\CommandNotFoundException' => $vendorDir . '/symfony/console/Exception/CommandNotFoundException.php',
    'Symfony\\Component\\Console\\Exception\\ExceptionInterface' => $vendorDir . '/symfony/console/Exception/ExceptionInterface.php',
    'Symfony\\Component\\Console\\Exception\\InvalidArgumentException' => $vendorDir . '/symfony/console/Exception/InvalidArgumentException.php',
    'Symfony\\Component\\Console\\Exception\\InvalidOptionException' => $vendorDir . '/symfony/console/Exception/InvalidOptionException.php',
    'Symfony\\Component\\Console\\Exception\\LogicException' => $vendorDir . '/symfony/console/Exception/LogicException.php',
    'Symfony\\Component\\Console\\Exception\\MissingInputException' => $vendorDir . '/symfony/console/Exception/MissingInputException.php',
    'Symfony\\Component\\Console\\Exception\\NamespaceNotFoundException' => $vendorDir . '/symfony/console/Exception/NamespaceNotFoundException.php',
    'Symfony\\Component\\Console\\Exception\\RuntimeException' => $vendorDir . '/symfony/console/Exception/RuntimeException.php',
    'Symfony\\Component\\Console\\Formatter\\NullOutputFormatter' => $vendorDir . '/symfony/console/Formatter/NullOutputFormatter.php',
    'Symfony\\Component\\Console\\Formatter\\NullOutputFormatterStyle' => $vendorDir . '/symfony/console/Formatter/NullOutputFormatterStyle.php',
    'Symfony\\Component\\Console\\Formatter\\OutputFormatter' => $vendorDir . '/symfony/console/Formatter/OutputFormatter.php',
    'Symfony\\Component\\Console\\Formatter\\OutputFormatterInterface' => $vendorDir . '/symfony/console/Formatter/OutputFormatterInterface.php',
    'Symfony\\Component\\Console\\Formatter\\OutputFormatterStyle' => $vendorDir . '/symfony/console/Formatter/OutputFormatterStyle.php',
    'Symfony\\Component\\Console\\Formatter\\OutputFormatterStyleInterface' => $vendorDir . '/symfony/console/Formatter/OutputFormatterStyleInterface.php',
    'Symfony\\Component\\Console\\Formatter\\OutputFormatterStyleStack' => $vendorDir . '/symfony/console/Formatter/OutputFormatterStyleStack.php',
    'Symfony\\Component\\Console\\Formatter\\WrappableOutputFormatterInterface' => $vendorDir . '/symfony/console/Formatter/WrappableOutputFormatterInterface.php',
    'Symfony\\Component\\Console\\Helper\\DebugFormatterHelper' => $vendorDir . '/symfony/console/Helper/DebugFormatterHelper.php',
    'Symfony\\Component\\Console\\Helper\\DescriptorHelper' => $vendorDir . '/symfony/console/Helper/DescriptorHelper.php',
    'Symfony\\Component\\Console\\Helper\\Dumper' => $vendorDir . '/symfony/console/Helper/Dumper.php',
    'Symfony\\Component\\Console\\Helper\\FormatterHelper' => $vendorDir . '/symfony/console/Helper/FormatterHelper.php',
    'Symfony\\Component\\Console\\Helper\\Helper' => $vendorDir . '/symfony/console/Helper/Helper.php',
    'Symfony\\Component\\Console\\Helper\\HelperInterface' => $vendorDir . '/symfony/console/Helper/HelperInterface.php',
    'Symfony\\Component\\Console\\Helper\\HelperSet' => $vendorDir . '/symfony/console/Helper/HelperSet.php',
    'Symfony\\Component\\Console\\Helper\\InputAwareHelper' => $vendorDir . '/symfony/console/Helper/InputAwareHelper.php',
    'Symfony\\Component\\Console\\Helper\\ProcessHelper' => $vendorDir . '/symfony/console/Helper/ProcessHelper.php',
    'Symfony\\Component\\Console\\Helper\\ProgressBar' => $vendorDir . '/symfony/console/Helper/ProgressBar.php',
    'Symfony\\Component\\Console\\Helper\\ProgressIndicator' => $vendorDir . '/symfony/console/Helper/ProgressIndicator.php',
    'Symfony\\Component\\Console\\Helper\\QuestionHelper' => $vendorDir . '/symfony/console/Helper/QuestionHelper.php',
    'Symfony\\Component\\Console\\Helper\\SymfonyQuestionHelper' => $vendorDir . '/symfony/console/Helper/SymfonyQuestionHelper.php',
    'Symfony\\Component\\Console\\Helper\\Table' => $vendorDir . '/symfony/console/Helper/Table.php',
    'Symfony\\Component\\Console\\Helper\\TableCell' => $vendorDir . '/symfony/console/Helper/TableCell.php',
    'Symfony\\Component\\Console\\Helper\\TableCellStyle' => $vendorDir . '/symfony/console/Helper/TableCellStyle.php',
    'Symfony\\Component\\Console\\Helper\\TableRows' => $vendorDir . '/symfony/console/Helper/TableRows.php',
    'Symfony\\Component\\Console\\Helper\\TableSeparator' => $vendorDir . '/symfony/console/Helper/TableSeparator.php',
    'Symfony\\Component\\Console\\Helper\\TableStyle' => $vendorDir . '/symfony/console/Helper/TableStyle.php',
    'Symfony\\Component\\Console\\Input\\ArgvInput' => $vendorDir . '/symfony/console/Input/ArgvInput.php',
    'Symfony\\Component\\Console\\Input\\ArrayInput' => $vendorDir . '/symfony/console/Input/ArrayInput.php',
    'Symfony\\Component\\Console\\Input\\Input' => $vendorDir . '/symfony/console/Input/Input.php',
    'Symfony\\Component\\Console\\Input\\InputArgument' => $vendorDir . '/symfony/console/Input/InputArgument.php',
    'Symfony\\Component\\Console\\Input\\InputAwareInterface' => $vendorDir . '/symfony/console/Input/InputAwareInterface.php',
    'Symfony\\Component\\Console\\Input\\InputDefinition' => $vendorDir . '/symfony/console/Input/InputDefinition.php',
    'Symfony\\Component\\Console\\Input\\InputInterface' => $vendorDir . '/symfony/console/Input/InputInterface.php',
    'Symfony\\Component\\Console\\Input\\InputOption' => $vendorDir . '/symfony/console/Input/InputOption.php',
    'Symfony\\Component\\Console\\Input\\StreamableInputInterface' => $vendorDir . '/symfony/console/Input/StreamableInputInterface.php',
    'Symfony\\Component\\Console\\Input\\StringInput' => $vendorDir . '/symfony/console/Input/StringInput.php',
    'Symfony\\Component\\Console\\Logger\\ConsoleLogger' => $vendorDir . '/symfony/console/Logger/ConsoleLogger.php',
    'Symfony\\Component\\Console\\Output\\BufferedOutput' => $vendorDir . '/symfony/console/Output/BufferedOutput.php',
    'Symfony\\Component\\Console\\Output\\ConsoleOutput' => $vendorDir . '/symfony/console/Output/ConsoleOutput.php',
    'Symfony\\Component\\Console\\Output\\ConsoleOutputInterface' => $vendorDir . '/symfony/console/Output/ConsoleOutputInterface.php',
    'Symfony\\Component\\Console\\Output\\ConsoleSectionOutput' => $vendorDir . '/symfony/console/Output/ConsoleSectionOutput.php',
    'Symfony\\Component\\Console\\Output\\NullOutput' => $vendorDir . '/symfony/console/Output/NullOutput.php',
    'Symfony\\Component\\Console\\Output\\Output' => $vendorDir . '/symfony/console/Output/Output.php',
    'Symfony\\Component\\Console\\Output\\OutputInterface' => $vendorDir . '/symfony/console/Output/OutputInterface.php',
    'Symfony\\Component\\Console\\Output\\StreamOutput' => $vendorDir . '/symfony/console/Output/StreamOutput.php',
    'Symfony\\Component\\Console\\Output\\TrimmedBufferOutput' => $vendorDir . '/symfony/console/Output/TrimmedBufferOutput.php',
    'Symfony\\Component\\Console\\Question\\ChoiceQuestion' => $vendorDir . '/symfony/console/Question/ChoiceQuestion.php',
    'Symfony\\Component\\Console\\Question\\ConfirmationQuestion' => $vendorDir . '/symfony/console/Question/ConfirmationQuestion.php',
    'Symfony\\Component\\Console\\Question\\Question' => $vendorDir . '/symfony/console/Question/Question.php',
    'Symfony\\Component\\Console\\SignalRegistry\\SignalRegistry' => $vendorDir . '/symfony/console/SignalRegistry/SignalRegistry.php',
    'Symfony\\Component\\Console\\SingleCommandApplication' => $vendorDir . '/symfony/console/SingleCommandApplication.php',
    'Symfony\\Component\\Console\\Style\\OutputStyle' => $vendorDir . '/symfony/console/Style/OutputStyle.php',
    'Symfony\\Component\\Console\\Style\\StyleInterface' => $vendorDir . '/symfony/console/Style/StyleInterface.php',
    'Symfony\\Component\\Console\\Style\\SymfonyStyle' => $vendorDir . '/symfony/console/Style/SymfonyStyle.php',
    'Symfony\\Component\\Console\\Terminal' => $vendorDir . '/symfony/console/Terminal.php',
    'Symfony\\Component\\Console\\Tester\\ApplicationTester' => $vendorDir . '/symfony/console/Tester/ApplicationTester.php',
    'Symfony\\Component\\Console\\Tester\\CommandCompletionTester' => $vendorDir . '/symfony/console/Tester/CommandCompletionTester.php',
    'Symfony\\Component\\Console\\Tester\\CommandTester' => $vendorDir . '/symfony/console/Tester/CommandTester.php',
    'Symfony\\Component\\Console\\Tester\\Constraint\\CommandIsSuccessful' => $vendorDir . '/symfony/console/Tester/Constraint/CommandIsSuccessful.php',
    'Symfony\\Component\\Console\\Tester\\TesterTrait' => $vendorDir . '/symfony/console/Tester/TesterTrait.php',
    'Symfony\\Component\\ExpressionLanguage\\Compiler' => $vendorDir . '/symfony/expression-language/Compiler.php',
    'Symfony\\Component\\ExpressionLanguage\\Expression' => $vendorDir . '/symfony/expression-language/Expression.php',
    'Symfony\\Component\\ExpressionLanguage\\ExpressionFunction' => $vendorDir . '/symfony/expression-language/ExpressionFunction.php',
    'Symfony\\Component\\ExpressionLanguage\\ExpressionFunctionProviderInterface' => $vendorDir . '/symfony/expression-language/ExpressionFunctionProviderInterface.php',
    'Symfony\\Component\\ExpressionLanguage\\ExpressionLanguage' => $vendorDir . '/symfony/expression-language/ExpressionLanguage.php',
    'Symfony\\Component\\ExpressionLanguage\\Lexer' => $vendorDir . '/symfony/expression-language/Lexer.php',
    'Symfony\\Component\\ExpressionLanguage\\Node\\ArgumentsNode' => $vendorDir . '/symfony/expression-language/Node/ArgumentsNode.php',
    'Symfony\\Component\\ExpressionLanguage\\Node\\ArrayNode' => $vendorDir . '/symfony/expression-language/Node/ArrayNode.php',
    'Symfony\\Component\\ExpressionLanguage\\Node\\BinaryNode' => $vendorDir . '/symfony/expression-language/Node/BinaryNode.php',
    'Symfony\\Component\\ExpressionLanguage\\Node\\ConditionalNode' => $vendorDir . '/symfony/expression-language/Node/ConditionalNode.php',
    'Symfony\\Component\\ExpressionLanguage\\Node\\ConstantNode' => $vendorDir . '/symfony/expression-language/Node/ConstantNode.php',
    'Symfony\\Component\\ExpressionLanguage\\Node\\FunctionNode' => $vendorDir . '/symfony/expression-language/Node/FunctionNode.php',
    'Symfony\\Component\\ExpressionLanguage\\Node\\GetAttrNode' => $vendorDir . '/symfony/expression-language/Node/GetAttrNode.php',
    'Symfony\\Component\\ExpressionLanguage\\Node\\NameNode' => $vendorDir . '/symfony/expression-language/Node/NameNode.php',
    'Symfony\\Component\\ExpressionLanguage\\Node\\Node' => $vendorDir . '/symfony/expression-language/Node/Node.php',
    'Symfony\\Component\\ExpressionLanguage\\Node\\UnaryNode' => $vendorDir . '/symfony/expression-language/Node/UnaryNode.php',
    'Symfony\\Component\\ExpressionLanguage\\ParsedExpression' => $vendorDir . '/symfony/expression-language/ParsedExpression.php',
    'Symfony\\Component\\ExpressionLanguage\\Parser' => $vendorDir . '/symfony/expression-language/Parser.php',
    'Symfony\\Component\\ExpressionLanguage\\SerializedParsedExpression' => $vendorDir . '/symfony/expression-language/SerializedParsedExpression.php',
    'Symfony\\Component\\ExpressionLanguage\\SyntaxError' => $vendorDir . '/symfony/expression-language/SyntaxError.php',
    'Symfony\\Component\\ExpressionLanguage\\Token' => $vendorDir . '/symfony/expression-language/Token.php',
    'Symfony\\Component\\ExpressionLanguage\\TokenStream' => $vendorDir . '/symfony/expression-language/TokenStream.php',
    'Symfony\\Component\\HttpClient\\AmpHttpClient' => $vendorDir . '/symfony/http-client/AmpHttpClient.php',
    'Symfony\\Component\\HttpClient\\AsyncDecoratorTrait' => $vendorDir . '/symfony/http-client/AsyncDecoratorTrait.php',
    'Symfony\\Component\\HttpClient\\CachingHttpClient' => $vendorDir . '/symfony/http-client/CachingHttpClient.php',
    'Symfony\\Component\\HttpClient\\Chunk\\DataChunk' => $vendorDir . '/symfony/http-client/Chunk/DataChunk.php',
    'Symfony\\Component\\HttpClient\\Chunk\\ErrorChunk' => $vendorDir . '/symfony/http-client/Chunk/ErrorChunk.php',
    'Symfony\\Component\\HttpClient\\Chunk\\FirstChunk' => $vendorDir . '/symfony/http-client/Chunk/FirstChunk.php',
    'Symfony\\Component\\HttpClient\\Chunk\\InformationalChunk' => $vendorDir . '/symfony/http-client/Chunk/InformationalChunk.php',
    'Symfony\\Component\\HttpClient\\Chunk\\LastChunk' => $vendorDir . '/symfony/http-client/Chunk/LastChunk.php',
    'Symfony\\Component\\HttpClient\\Chunk\\ServerSentEvent' => $vendorDir . '/symfony/http-client/Chunk/ServerSentEvent.php',
    'Symfony\\Component\\HttpClient\\CurlHttpClient' => $vendorDir . '/symfony/http-client/CurlHttpClient.php',
    'Symfony\\Component\\HttpClient\\DataCollector\\HttpClientDataCollector' => $vendorDir . '/symfony/http-client/DataCollector/HttpClientDataCollector.php',
    'Symfony\\Component\\HttpClient\\DecoratorTrait' => $vendorDir . '/symfony/http-client/DecoratorTrait.php',
    'Symfony\\Component\\HttpClient\\DependencyInjection\\HttpClientPass' => $vendorDir . '/symfony/http-client/DependencyInjection/HttpClientPass.php',
    'Symfony\\Component\\HttpClient\\EventSourceHttpClient' => $vendorDir . '/symfony/http-client/EventSourceHttpClient.php',
    'Symfony\\Component\\HttpClient\\Exception\\ClientException' => $vendorDir . '/symfony/http-client/Exception/ClientException.php',
    'Symfony\\Component\\HttpClient\\Exception\\EventSourceException' => $vendorDir . '/symfony/http-client/Exception/EventSourceException.php',
    'Symfony\\Component\\HttpClient\\Exception\\HttpExceptionTrait' => $vendorDir . '/symfony/http-client/Exception/HttpExceptionTrait.php',
    'Symfony\\Component\\HttpClient\\Exception\\InvalidArgumentException' => $vendorDir . '/symfony/http-client/Exception/InvalidArgumentException.php',
    'Symfony\\Component\\HttpClient\\Exception\\JsonException' => $vendorDir . '/symfony/http-client/Exception/JsonException.php',
    'Symfony\\Component\\HttpClient\\Exception\\RedirectionException' => $vendorDir . '/symfony/http-client/Exception/RedirectionException.php',
    'Symfony\\Component\\HttpClient\\Exception\\ServerException' => $vendorDir . '/symfony/http-client/Exception/ServerException.php',
    'Symfony\\Component\\HttpClient\\Exception\\TimeoutException' => $vendorDir . '/symfony/http-client/Exception/TimeoutException.php',
    'Symfony\\Component\\HttpClient\\Exception\\TransportException' => $vendorDir . '/symfony/http-client/Exception/TransportException.php',
    'Symfony\\Component\\HttpClient\\HttpClient' => $vendorDir . '/symfony/http-client/HttpClient.php',
    'Symfony\\Component\\HttpClient\\HttpClientTrait' => $vendorDir . '/symfony/http-client/HttpClientTrait.php',
    'Symfony\\Component\\HttpClient\\HttpOptions' => $vendorDir . '/symfony/http-client/HttpOptions.php',
    'Symfony\\Component\\HttpClient\\HttplugClient' => $vendorDir . '/symfony/http-client/HttplugClient.php',
    'Symfony\\Component\\HttpClient\\Internal\\AmpBody' => $vendorDir . '/symfony/http-client/Internal/AmpBody.php',
    'Symfony\\Component\\HttpClient\\Internal\\AmpClientState' => $vendorDir . '/symfony/http-client/Internal/AmpClientState.php',
    'Symfony\\Component\\HttpClient\\Internal\\AmpListener' => $vendorDir . '/symfony/http-client/Internal/AmpListener.php',
    'Symfony\\Component\\HttpClient\\Internal\\AmpResolver' => $vendorDir . '/symfony/http-client/Internal/AmpResolver.php',
    'Symfony\\Component\\HttpClient\\Internal\\Canary' => $vendorDir . '/symfony/http-client/Internal/Canary.php',
    'Symfony\\Component\\HttpClient\\Internal\\ClientState' => $vendorDir . '/symfony/http-client/Internal/ClientState.php',
    'Symfony\\Component\\HttpClient\\Internal\\CurlClientState' => $vendorDir . '/symfony/http-client/Internal/CurlClientState.php',
    'Symfony\\Component\\HttpClient\\Internal\\DnsCache' => $vendorDir . '/symfony/http-client/Internal/DnsCache.php',
    'Symfony\\Component\\HttpClient\\Internal\\HttplugWaitLoop' => $vendorDir . '/symfony/http-client/Internal/HttplugWaitLoop.php',
    'Symfony\\Component\\HttpClient\\Internal\\NativeClientState' => $vendorDir . '/symfony/http-client/Internal/NativeClientState.php',
    'Symfony\\Component\\HttpClient\\Internal\\PushedResponse' => $vendorDir . '/symfony/http-client/Internal/PushedResponse.php',
    'Symfony\\Component\\HttpClient\\MockHttpClient' => $vendorDir . '/symfony/http-client/MockHttpClient.php',
    'Symfony\\Component\\HttpClient\\NativeHttpClient' => $vendorDir . '/symfony/http-client/NativeHttpClient.php',
    'Symfony\\Component\\HttpClient\\NoPrivateNetworkHttpClient' => $vendorDir . '/symfony/http-client/NoPrivateNetworkHttpClient.php',
    'Symfony\\Component\\HttpClient\\Psr18Client' => $vendorDir . '/symfony/http-client/Psr18Client.php',
    'Symfony\\Component\\HttpClient\\Response\\AmpResponse' => $vendorDir . '/symfony/http-client/Response/AmpResponse.php',
    'Symfony\\Component\\HttpClient\\Response\\AsyncContext' => $vendorDir . '/symfony/http-client/Response/AsyncContext.php',
    'Symfony\\Component\\HttpClient\\Response\\AsyncResponse' => $vendorDir . '/symfony/http-client/Response/AsyncResponse.php',
    'Symfony\\Component\\HttpClient\\Response\\CommonResponseTrait' => $vendorDir . '/symfony/http-client/Response/CommonResponseTrait.php',
    'Symfony\\Component\\HttpClient\\Response\\CurlResponse' => $vendorDir . '/symfony/http-client/Response/CurlResponse.php',
    'Symfony\\Component\\HttpClient\\Response\\HttplugPromise' => $vendorDir . '/symfony/http-client/Response/HttplugPromise.php',
    'Symfony\\Component\\HttpClient\\Response\\MockResponse' => $vendorDir . '/symfony/http-client/Response/MockResponse.php',
    'Symfony\\Component\\HttpClient\\Response\\NativeResponse' => $vendorDir . '/symfony/http-client/Response/NativeResponse.php',
    'Symfony\\Component\\HttpClient\\Response\\ResponseStream' => $vendorDir . '/symfony/http-client/Response/ResponseStream.php',
    'Symfony\\Component\\HttpClient\\Response\\StreamWrapper' => $vendorDir . '/symfony/http-client/Response/StreamWrapper.php',
    'Symfony\\Component\\HttpClient\\Response\\StreamableInterface' => $vendorDir . '/symfony/http-client/Response/StreamableInterface.php',
    'Symfony\\Component\\HttpClient\\Response\\TraceableResponse' => $vendorDir . '/symfony/http-client/Response/TraceableResponse.php',
    'Symfony\\Component\\HttpClient\\Response\\TransportResponseTrait' => $vendorDir . '/symfony/http-client/Response/TransportResponseTrait.php',
    'Symfony\\Component\\HttpClient\\Retry\\GenericRetryStrategy' => $vendorDir . '/symfony/http-client/Retry/GenericRetryStrategy.php',
    'Symfony\\Component\\HttpClient\\Retry\\RetryStrategyInterface' => $vendorDir . '/symfony/http-client/Retry/RetryStrategyInterface.php',
    'Symfony\\Component\\HttpClient\\RetryableHttpClient' => $vendorDir . '/symfony/http-client/RetryableHttpClient.php',
    'Symfony\\Component\\HttpClient\\ScopingHttpClient' => $vendorDir . '/symfony/http-client/ScopingHttpClient.php',
    'Symfony\\Component\\HttpClient\\TraceableHttpClient' => $vendorDir . '/symfony/http-client/TraceableHttpClient.php',
    'Symfony\\Component\\String\\AbstractString' => $vendorDir . '/symfony/string/AbstractString.php',
    'Symfony\\Component\\String\\AbstractUnicodeString' => $vendorDir . '/symfony/string/AbstractUnicodeString.php',
    'Symfony\\Component\\String\\ByteString' => $vendorDir . '/symfony/string/ByteString.php',
    'Symfony\\Component\\String\\CodePointString' => $vendorDir . '/symfony/string/CodePointString.php',
    'Symfony\\Component\\String\\Exception\\ExceptionInterface' => $vendorDir . '/symfony/string/Exception/ExceptionInterface.php',
    'Symfony\\Component\\String\\Exception\\InvalidArgumentException' => $vendorDir . '/symfony/string/Exception/InvalidArgumentException.php',
    'Symfony\\Component\\String\\Exception\\RuntimeException' => $vendorDir . '/symfony/string/Exception/RuntimeException.php',
    'Symfony\\Component\\String\\Inflector\\EnglishInflector' => $vendorDir . '/symfony/string/Inflector/EnglishInflector.php',
    'Symfony\\Component\\String\\Inflector\\FrenchInflector' => $vendorDir . '/symfony/string/Inflector/FrenchInflector.php',
    'Symfony\\Component\\String\\Inflector\\InflectorInterface' => $vendorDir . '/symfony/string/Inflector/InflectorInterface.php',
    'Symfony\\Component\\String\\LazyString' => $vendorDir . '/symfony/string/LazyString.php',
    'Symfony\\Component\\String\\Slugger\\AsciiSlugger' => $vendorDir . '/symfony/string/Slugger/AsciiSlugger.php',
    'Symfony\\Component\\String\\Slugger\\SluggerInterface' => $vendorDir . '/symfony/string/Slugger/SluggerInterface.php',
    'Symfony\\Component\\String\\UnicodeString' => $vendorDir . '/symfony/string/UnicodeString.php',
    'Symfony\\Component\\VarExporter\\Exception\\ClassNotFoundException' => $vendorDir . '/symfony/var-exporter/Exception/ClassNotFoundException.php',
    'Symfony\\Component\\VarExporter\\Exception\\ExceptionInterface' => $vendorDir . '/symfony/var-exporter/Exception/ExceptionInterface.php',
    'Symfony\\Component\\VarExporter\\Exception\\NotInstantiableTypeException' => $vendorDir . '/symfony/var-exporter/Exception/NotInstantiableTypeException.php',
    'Symfony\\Component\\VarExporter\\Instantiator' => $vendorDir . '/symfony/var-exporter/Instantiator.php',
    'Symfony\\Component\\VarExporter\\Internal\\Exporter' => $vendorDir . '/symfony/var-exporter/Internal/Exporter.php',
    'Symfony\\Component\\VarExporter\\Internal\\Hydrator' => $vendorDir . '/symfony/var-exporter/Internal/Hydrator.php',
    'Symfony\\Component\\VarExporter\\Internal\\Reference' => $vendorDir . '/symfony/var-exporter/Internal/Reference.php',
    'Symfony\\Component\\VarExporter\\Internal\\Registry' => $vendorDir . '/symfony/var-exporter/Internal/Registry.php',
    'Symfony\\Component\\VarExporter\\Internal\\Values' => $vendorDir . '/symfony/var-exporter/Internal/Values.php',
    'Symfony\\Component\\VarExporter\\VarExporter' => $vendorDir . '/symfony/var-exporter/VarExporter.php',
    'Symfony\\Contracts\\Cache\\CacheInterface' => $vendorDir . '/symfony/cache-contracts/CacheInterface.php',
    'Symfony\\Contracts\\Cache\\CacheTrait' => $vendorDir . '/symfony/cache-contracts/CacheTrait.php',
    'Symfony\\Contracts\\Cache\\CallbackInterface' => $vendorDir . '/symfony/cache-contracts/CallbackInterface.php',
    'Symfony\\Contracts\\Cache\\ItemInterface' => $vendorDir . '/symfony/cache-contracts/ItemInterface.php',
    'Symfony\\Contracts\\Cache\\TagAwareCacheInterface' => $vendorDir . '/symfony/cache-contracts/TagAwareCacheInterface.php',
    'Symfony\\Contracts\\HttpClient\\ChunkInterface' => $vendorDir . '/symfony/http-client-contracts/ChunkInterface.php',
    'Symfony\\Contracts\\HttpClient\\Exception\\ClientExceptionInterface' => $vendorDir . '/symfony/http-client-contracts/Exception/ClientExceptionInterface.php',
    'Symfony\\Contracts\\HttpClient\\Exception\\DecodingExceptionInterface' => $vendorDir . '/symfony/http-client-contracts/Exception/DecodingExceptionInterface.php',
    'Symfony\\Contracts\\HttpClient\\Exception\\ExceptionInterface' => $vendorDir . '/symfony/http-client-contracts/Exception/ExceptionInterface.php',
    'Symfony\\Contracts\\HttpClient\\Exception\\HttpExceptionInterface' => $vendorDir . '/symfony/http-client-contracts/Exception/HttpExceptionInterface.php',
    'Symfony\\Contracts\\HttpClient\\Exception\\RedirectionExceptionInterface' => $vendorDir . '/symfony/http-client-contracts/Exception/RedirectionExceptionInterface.php',
    'Symfony\\Contracts\\HttpClient\\Exception\\ServerExceptionInterface' => $vendorDir . '/symfony/http-client-contracts/Exception/ServerExceptionInterface.php',
    'Symfony\\Contracts\\HttpClient\\Exception\\TimeoutExceptionInterface' => $vendorDir . '/symfony/http-client-contracts/Exception/TimeoutExceptionInterface.php',
    'Symfony\\Contracts\\HttpClient\\Exception\\TransportExceptionInterface' => $vendorDir . '/symfony/http-client-contracts/Exception/TransportExceptionInterface.php',
    'Symfony\\Contracts\\HttpClient\\HttpClientInterface' => $vendorDir . '/symfony/http-client-contracts/HttpClientInterface.php',
    'Symfony\\Contracts\\HttpClient\\ResponseInterface' => $vendorDir . '/symfony/http-client-contracts/ResponseInterface.php',
    'Symfony\\Contracts\\HttpClient\\ResponseStreamInterface' => $vendorDir . '/symfony/http-client-contracts/ResponseStreamInterface.php',
    'Symfony\\Contracts\\HttpClient\\Test\\HttpClientTestCase' => $vendorDir . '/symfony/http-client-contracts/Test/HttpClientTestCase.php',
    'Symfony\\Contracts\\HttpClient\\Test\\TestHttpServer' => $vendorDir . '/symfony/http-client-contracts/Test/TestHttpServer.php',
    'Symfony\\Contracts\\Service\\Attribute\\Required' => $vendorDir . '/symfony/service-contracts/Attribute/Required.php',
    'Symfony\\Contracts\\Service\\Attribute\\SubscribedService' => $vendorDir . '/symfony/service-contracts/Attribute/SubscribedService.php',
    'Symfony\\Contracts\\Service\\ResetInterface' => $vendorDir . '/symfony/service-contracts/ResetInterface.php',
    'Symfony\\Contracts\\Service\\ServiceLocatorTrait' => $vendorDir . '/symfony/service-contracts/ServiceLocatorTrait.php',
    'Symfony\\Contracts\\Service\\ServiceProviderInterface' => $vendorDir . '/symfony/service-contracts/ServiceProviderInterface.php',
    'Symfony\\Contracts\\Service\\ServiceSubscriberInterface' => $vendorDir . '/symfony/service-contracts/ServiceSubscriberInterface.php',
    'Symfony\\Contracts\\Service\\ServiceSubscriberTrait' => $vendorDir . '/symfony/service-contracts/ServiceSubscriberTrait.php',
    'Symfony\\Contracts\\Service\\Test\\ServiceLocatorTest' => $vendorDir . '/symfony/service-contracts/Test/ServiceLocatorTest.php',
    'Symfony\\Contracts\\Service\\Test\\ServiceLocatorTestCase' => $vendorDir . '/symfony/service-contracts/Test/ServiceLocatorTestCase.php',
    'Symfony\\Polyfill\\Ctype\\Ctype' => $vendorDir . '/symfony/polyfill-ctype/Ctype.php',
    'Symfony\\Polyfill\\Intl\\Grapheme\\Grapheme' => $vendorDir . '/symfony/polyfill-intl-grapheme/Grapheme.php',
    'Symfony\\Polyfill\\Intl\\Normalizer\\Normalizer' => $vendorDir . '/symfony/polyfill-intl-normalizer/Normalizer.php',
    'Symfony\\Polyfill\\Mbstring\\Mbstring' => $vendorDir . '/symfony/polyfill-mbstring/Mbstring.php',
    'Symfony\\Polyfill\\Php73\\Php73' => $vendorDir . '/symfony/polyfill-php73/Php73.php',
    'Symfony\\Polyfill\\Php80\\Php80' => $vendorDir . '/symfony/polyfill-php80/Php80.php',
    'Symfony\\Polyfill\\Php80\\PhpToken' => $vendorDir . '/symfony/polyfill-php80/PhpToken.php',
    'UnhandledMatchError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php',
    'ValueError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/ValueError.php',
);
<?php

// autoload_static.php @generated by Composer

namespace Composer\Autoload;

class ComposerStaticInitb258b286557a5c443ca34ff495d81cd6
{
    public static $files = array (
        '6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php',
        'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php',
        '0d59ee240a4cd96ddbb4ff164fccea4d' => __DIR__ . '/..' . '/symfony/polyfill-php73/bootstrap.php',
        '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
        '320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/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 (
        'S' => 
        array (
            'Symfony\\Polyfill\\Php80\\' => 23,
            'Symfony\\Polyfill\\Php73\\' => 23,
            'Symfony\\Polyfill\\Mbstring\\' => 26,
            'Symfony\\Polyfill\\Intl\\Normalizer\\' => 33,
            'Symfony\\Polyfill\\Intl\\Grapheme\\' => 31,
            'Symfony\\Polyfill\\Ctype\\' => 23,
            'Symfony\\Contracts\\Service\\' => 26,
            'Symfony\\Contracts\\HttpClient\\' => 29,
            'Symfony\\Contracts\\Cache\\' => 24,
            'Symfony\\Component\\VarExporter\\' => 30,
            'Symfony\\Component\\String\\' => 25,
            'Symfony\\Component\\HttpClient\\' => 29,
            'Symfony\\Component\\ExpressionLanguage\\' => 37,
            'Symfony\\Component\\Console\\' => 26,
            'Symfony\\Component\\Cache\\' => 24,
            'SensioLabs\\Insight\\' => 19,
        ),
        'P' => 
        array (
            'Psr\\Log\\' => 8,
            'Psr\\Container\\' => 14,
            'Psr\\Cache\\' => 10,
            'PHPStan\\PhpDocParser\\' => 21,
        ),
        'M' => 
        array (
            'Metadata\\' => 9,
        ),
        'J' => 
        array (
            'JMS\\Serializer\\' => 15,
        ),
        'D' => 
        array (
            'Doctrine\\Instantiator\\' => 22,
            'Doctrine\\Deprecations\\' => 22,
            'Doctrine\\Common\\Lexer\\' => 22,
            'Doctrine\\Common\\Annotations\\' => 28,
        ),
    );

    public static $prefixDirsPsr4 = array (
        'Symfony\\Polyfill\\Php80\\' => 
        array (
            0 => __DIR__ . '/..' . '/symfony/polyfill-php80',
        ),
        'Symfony\\Polyfill\\Php73\\' => 
        array (
            0 => __DIR__ . '/..' . '/symfony/polyfill-php73',
        ),
        '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\\HttpClient\\' => 
        array (
            0 => __DIR__ . '/..' . '/symfony/http-client-contracts',
        ),
        'Symfony\\Contracts\\Cache\\' => 
        array (
            0 => __DIR__ . '/..' . '/symfony/cache-contracts',
        ),
        'Symfony\\Component\\VarExporter\\' => 
        array (
            0 => __DIR__ . '/..' . '/symfony/var-exporter',
        ),
        'Symfony\\Component\\String\\' => 
        array (
            0 => __DIR__ . '/..' . '/symfony/string',
        ),
        'Symfony\\Component\\HttpClient\\' => 
        array (
            0 => __DIR__ . '/..' . '/symfony/http-client',
        ),
        'Symfony\\Component\\ExpressionLanguage\\' => 
        array (
            0 => __DIR__ . '/..' . '/symfony/expression-language',
        ),
        'Symfony\\Component\\Console\\' => 
        array (
            0 => __DIR__ . '/..' . '/symfony/console',
        ),
        'Symfony\\Component\\Cache\\' => 
        array (
            0 => __DIR__ . '/..' . '/symfony/cache',
        ),
        'SensioLabs\\Insight\\' => 
        array (
            0 => __DIR__ . '/../..' . '/',
        ),
        'Psr\\Log\\' => 
        array (
            0 => __DIR__ . '/..' . '/psr/log/Psr/Log',
        ),
        'Psr\\Container\\' => 
        array (
            0 => __DIR__ . '/..' . '/psr/container/src',
        ),
        'Psr\\Cache\\' => 
        array (
            0 => __DIR__ . '/..' . '/psr/cache/src',
        ),
        'PHPStan\\PhpDocParser\\' => 
        array (
            0 => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src',
        ),
        'Metadata\\' => 
        array (
            0 => __DIR__ . '/..' . '/jms/metadata/src',
        ),
        'JMS\\Serializer\\' => 
        array (
            0 => __DIR__ . '/..' . '/jms/serializer/src',
        ),
        'Doctrine\\Instantiator\\' => 
        array (
            0 => __DIR__ . '/..' . '/doctrine/instantiator/src/Doctrine/Instantiator',
        ),
        'Doctrine\\Deprecations\\' => 
        array (
            0 => __DIR__ . '/..' . '/doctrine/deprecations/src',
        ),
        'Doctrine\\Common\\Lexer\\' => 
        array (
            0 => __DIR__ . '/..' . '/doctrine/lexer/src',
        ),
        'Doctrine\\Common\\Annotations\\' => 
        array (
            0 => __DIR__ . '/..' . '/doctrine/annotations/lib/Doctrine/Common/Annotations',
        ),
    );

    public static $classMap = array (
        'Attribute' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
        'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
        'Doctrine\\Common\\Annotations\\Annotation' => __DIR__ . '/..' . '/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation.php',
        'Doctrine\\Common\\Annotations\\AnnotationException' => __DIR__ . '/..' . '/doctrine/annotations/lib/Doctrine/Common/Annotations/AnnotationException.php',
        'Doctrine\\Common\\Annotations\\AnnotationReader' => __DIR__ . '/..' . '/doctrine/annotations/lib/Doctrine/Common/Annotations/AnnotationReader.php',
        'Doctrine\\Common\\Annotations\\AnnotationRegistry' => __DIR__ . '/..' . '/doctrine/annotations/lib/Doctrine/Common/Annotations/AnnotationRegistry.php',
        'Doctrine\\Common\\Annotations\\Annotation\\Attribute' => __DIR__ . '/..' . '/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation/Attribute.php',
        'Doctrine\\Common\\Annotations\\Annotation\\Attributes' => __DIR__ . '/..' . '/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation/Attributes.php',
        'Doctrine\\Common\\Annotations\\Annotation\\Enum' => __DIR__ . '/..' . '/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation/Enum.php',
        'Doctrine\\Common\\Annotations\\Annotation\\IgnoreAnnotation' => __DIR__ . '/..' . '/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation/IgnoreAnnotation.php',
        'Doctrine\\Common\\Annotations\\Annotation\\NamedArgumentConstructor' => __DIR__ . '/..' . '/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation/NamedArgumentConstructor.php',
        'Doctrine\\Common\\Annotations\\Annotation\\Required' => __DIR__ . '/..' . '/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation/Required.php',
        'Doctrine\\Common\\Annotations\\Annotation\\Target' => __DIR__ . '/..' . '/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation/Target.php',
        'Doctrine\\Common\\Annotations\\DocLexer' => __DIR__ . '/..' . '/doctrine/annotations/lib/Doctrine/Common/Annotations/DocLexer.php',
        'Doctrine\\Common\\Annotations\\DocParser' => __DIR__ . '/..' . '/doctrine/annotations/lib/Doctrine/Common/Annotations/DocParser.php',
        'Doctrine\\Common\\Annotations\\ImplicitlyIgnoredAnnotationNames' => __DIR__ . '/..' . '/doctrine/annotations/lib/Doctrine/Common/Annotations/ImplicitlyIgnoredAnnotationNames.php',
        'Doctrine\\Common\\Annotations\\IndexedReader' => __DIR__ . '/..' . '/doctrine/annotations/lib/Doctrine/Common/Annotations/IndexedReader.php',
        'Doctrine\\Common\\Annotations\\PhpParser' => __DIR__ . '/..' . '/doctrine/annotations/lib/Doctrine/Common/Annotations/PhpParser.php',
        'Doctrine\\Common\\Annotations\\PsrCachedReader' => __DIR__ . '/..' . '/doctrine/annotations/lib/Doctrine/Common/Annotations/PsrCachedReader.php',
        'Doctrine\\Common\\Annotations\\Reader' => __DIR__ . '/..' . '/doctrine/annotations/lib/Doctrine/Common/Annotations/Reader.php',
        'Doctrine\\Common\\Annotations\\TokenParser' => __DIR__ . '/..' . '/doctrine/annotations/lib/Doctrine/Common/Annotations/TokenParser.php',
        'Doctrine\\Common\\Lexer\\AbstractLexer' => __DIR__ . '/..' . '/doctrine/lexer/src/AbstractLexer.php',
        'Doctrine\\Common\\Lexer\\Token' => __DIR__ . '/..' . '/doctrine/lexer/src/Token.php',
        'Doctrine\\Deprecations\\Deprecation' => __DIR__ . '/..' . '/doctrine/deprecations/src/Deprecation.php',
        'Doctrine\\Deprecations\\PHPUnit\\VerifyDeprecations' => __DIR__ . '/..' . '/doctrine/deprecations/src/PHPUnit/VerifyDeprecations.php',
        'Doctrine\\Instantiator\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/doctrine/instantiator/src/Doctrine/Instantiator/Exception/ExceptionInterface.php',
        'Doctrine\\Instantiator\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/doctrine/instantiator/src/Doctrine/Instantiator/Exception/InvalidArgumentException.php',
        'Doctrine\\Instantiator\\Exception\\UnexpectedValueException' => __DIR__ . '/..' . '/doctrine/instantiator/src/Doctrine/Instantiator/Exception/UnexpectedValueException.php',
        'Doctrine\\Instantiator\\Instantiator' => __DIR__ . '/..' . '/doctrine/instantiator/src/Doctrine/Instantiator/Instantiator.php',
        'Doctrine\\Instantiator\\InstantiatorInterface' => __DIR__ . '/..' . '/doctrine/instantiator/src/Doctrine/Instantiator/InstantiatorInterface.php',
        'JMS\\Serializer\\AbstractVisitor' => __DIR__ . '/..' . '/jms/serializer/src/AbstractVisitor.php',
        'JMS\\Serializer\\Accessor\\AccessorStrategyInterface' => __DIR__ . '/..' . '/jms/serializer/src/Accessor/AccessorStrategyInterface.php',
        'JMS\\Serializer\\Accessor\\DefaultAccessorStrategy' => __DIR__ . '/..' . '/jms/serializer/src/Accessor/DefaultAccessorStrategy.php',
        'JMS\\Serializer\\Annotation\\AccessType' => __DIR__ . '/..' . '/jms/serializer/src/Annotation/AccessType.php',
        'JMS\\Serializer\\Annotation\\Accessor' => __DIR__ . '/..' . '/jms/serializer/src/Annotation/Accessor.php',
        'JMS\\Serializer\\Annotation\\AccessorOrder' => __DIR__ . '/..' . '/jms/serializer/src/Annotation/AccessorOrder.php',
        'JMS\\Serializer\\Annotation\\AnnotationUtilsTrait' => __DIR__ . '/..' . '/jms/serializer/src/Annotation/AnnotationUtilsTrait.php',
        'JMS\\Serializer\\Annotation\\DeprecatedReadOnly' => __DIR__ . '/..' . '/jms/serializer/src/Annotation/DeprecatedReadOnly.php',
        'JMS\\Serializer\\Annotation\\Discriminator' => __DIR__ . '/..' . '/jms/serializer/src/Annotation/Discriminator.php',
        'JMS\\Serializer\\Annotation\\Exclude' => __DIR__ . '/..' . '/jms/serializer/src/Annotation/Exclude.php',
        'JMS\\Serializer\\Annotation\\ExclusionPolicy' => __DIR__ . '/..' . '/jms/serializer/src/Annotation/ExclusionPolicy.php',
        'JMS\\Serializer\\Annotation\\Expose' => __DIR__ . '/..' . '/jms/serializer/src/Annotation/Expose.php',
        'JMS\\Serializer\\Annotation\\Groups' => __DIR__ . '/..' . '/jms/serializer/src/Annotation/Groups.php',
        'JMS\\Serializer\\Annotation\\Inline' => __DIR__ . '/..' . '/jms/serializer/src/Annotation/Inline.php',
        'JMS\\Serializer\\Annotation\\MaxDepth' => __DIR__ . '/..' . '/jms/serializer/src/Annotation/MaxDepth.php',
        'JMS\\Serializer\\Annotation\\PostDeserialize' => __DIR__ . '/..' . '/jms/serializer/src/Annotation/PostDeserialize.php',
        'JMS\\Serializer\\Annotation\\PostSerialize' => __DIR__ . '/..' . '/jms/serializer/src/Annotation/PostSerialize.php',
        'JMS\\Serializer\\Annotation\\PreSerialize' => __DIR__ . '/..' . '/jms/serializer/src/Annotation/PreSerialize.php',
        'JMS\\Serializer\\Annotation\\ReadOnlyProperty' => __DIR__ . '/..' . '/jms/serializer/src/Annotation/ReadOnlyProperty.php',
        'JMS\\Serializer\\Annotation\\SerializedName' => __DIR__ . '/..' . '/jms/serializer/src/Annotation/SerializedName.php',
        'JMS\\Serializer\\Annotation\\SerializerAttribute' => __DIR__ . '/..' . '/jms/serializer/src/Annotation/SerializerAttribute.php',
        'JMS\\Serializer\\Annotation\\Since' => __DIR__ . '/..' . '/jms/serializer/src/Annotation/Since.php',
        'JMS\\Serializer\\Annotation\\SkipWhenEmpty' => __DIR__ . '/..' . '/jms/serializer/src/Annotation/SkipWhenEmpty.php',
        'JMS\\Serializer\\Annotation\\Type' => __DIR__ . '/..' . '/jms/serializer/src/Annotation/Type.php',
        'JMS\\Serializer\\Annotation\\UnionDiscriminator' => __DIR__ . '/..' . '/jms/serializer/src/Annotation/UnionDiscriminator.php',
        'JMS\\Serializer\\Annotation\\Until' => __DIR__ . '/..' . '/jms/serializer/src/Annotation/Until.php',
        'JMS\\Serializer\\Annotation\\Version' => __DIR__ . '/..' . '/jms/serializer/src/Annotation/Version.php',
        'JMS\\Serializer\\Annotation\\VirtualProperty' => __DIR__ . '/..' . '/jms/serializer/src/Annotation/VirtualProperty.php',
        'JMS\\Serializer\\Annotation\\XmlAttribute' => __DIR__ . '/..' . '/jms/serializer/src/Annotation/XmlAttribute.php',
        'JMS\\Serializer\\Annotation\\XmlAttributeMap' => __DIR__ . '/..' . '/jms/serializer/src/Annotation/XmlAttributeMap.php',
        'JMS\\Serializer\\Annotation\\XmlCollection' => __DIR__ . '/..' . '/jms/serializer/src/Annotation/XmlCollection.php',
        'JMS\\Serializer\\Annotation\\XmlDiscriminator' => __DIR__ . '/..' . '/jms/serializer/src/Annotation/XmlDiscriminator.php',
        'JMS\\Serializer\\Annotation\\XmlElement' => __DIR__ . '/..' . '/jms/serializer/src/Annotation/XmlElement.php',
        'JMS\\Serializer\\Annotation\\XmlKeyValuePairs' => __DIR__ . '/..' . '/jms/serializer/src/Annotation/XmlKeyValuePairs.php',
        'JMS\\Serializer\\Annotation\\XmlList' => __DIR__ . '/..' . '/jms/serializer/src/Annotation/XmlList.php',
        'JMS\\Serializer\\Annotation\\XmlMap' => __DIR__ . '/..' . '/jms/serializer/src/Annotation/XmlMap.php',
        'JMS\\Serializer\\Annotation\\XmlNamespace' => __DIR__ . '/..' . '/jms/serializer/src/Annotation/XmlNamespace.php',
        'JMS\\Serializer\\Annotation\\XmlRoot' => __DIR__ . '/..' . '/jms/serializer/src/Annotation/XmlRoot.php',
        'JMS\\Serializer\\Annotation\\XmlValue' => __DIR__ . '/..' . '/jms/serializer/src/Annotation/XmlValue.php',
        'JMS\\Serializer\\ArrayTransformerInterface' => __DIR__ . '/..' . '/jms/serializer/src/ArrayTransformerInterface.php',
        'JMS\\Serializer\\Builder\\CallbackDriverFactory' => __DIR__ . '/..' . '/jms/serializer/src/Builder/CallbackDriverFactory.php',
        'JMS\\Serializer\\Builder\\DefaultDriverFactory' => __DIR__ . '/..' . '/jms/serializer/src/Builder/DefaultDriverFactory.php',
        'JMS\\Serializer\\Builder\\DocBlockDriverFactory' => __DIR__ . '/..' . '/jms/serializer/src/Builder/DocBlockDriverFactory.php',
        'JMS\\Serializer\\Builder\\DriverFactoryInterface' => __DIR__ . '/..' . '/jms/serializer/src/Builder/DriverFactoryInterface.php',
        'JMS\\Serializer\\Construction\\DoctrineObjectConstructor' => __DIR__ . '/..' . '/jms/serializer/src/Construction/DoctrineObjectConstructor.php',
        'JMS\\Serializer\\Construction\\ObjectConstructorInterface' => __DIR__ . '/..' . '/jms/serializer/src/Construction/ObjectConstructorInterface.php',
        'JMS\\Serializer\\Construction\\UnserializeObjectConstructor' => __DIR__ . '/..' . '/jms/serializer/src/Construction/UnserializeObjectConstructor.php',
        'JMS\\Serializer\\Context' => __DIR__ . '/..' . '/jms/serializer/src/Context.php',
        'JMS\\Serializer\\ContextFactory\\CallableContextFactory' => __DIR__ . '/..' . '/jms/serializer/src/ContextFactory/CallableContextFactory.php',
        'JMS\\Serializer\\ContextFactory\\CallableDeserializationContextFactory' => __DIR__ . '/..' . '/jms/serializer/src/ContextFactory/CallableDeserializationContextFactory.php',
        'JMS\\Serializer\\ContextFactory\\CallableSerializationContextFactory' => __DIR__ . '/..' . '/jms/serializer/src/ContextFactory/CallableSerializationContextFactory.php',
        'JMS\\Serializer\\ContextFactory\\DefaultDeserializationContextFactory' => __DIR__ . '/..' . '/jms/serializer/src/ContextFactory/DefaultDeserializationContextFactory.php',
        'JMS\\Serializer\\ContextFactory\\DefaultSerializationContextFactory' => __DIR__ . '/..' . '/jms/serializer/src/ContextFactory/DefaultSerializationContextFactory.php',
        'JMS\\Serializer\\ContextFactory\\DeserializationContextFactoryInterface' => __DIR__ . '/..' . '/jms/serializer/src/ContextFactory/DeserializationContextFactoryInterface.php',
        'JMS\\Serializer\\ContextFactory\\SerializationContextFactoryInterface' => __DIR__ . '/..' . '/jms/serializer/src/ContextFactory/SerializationContextFactoryInterface.php',
        'JMS\\Serializer\\DeserializationContext' => __DIR__ . '/..' . '/jms/serializer/src/DeserializationContext.php',
        'JMS\\Serializer\\EventDispatcher\\Event' => __DIR__ . '/..' . '/jms/serializer/src/EventDispatcher/Event.php',
        'JMS\\Serializer\\EventDispatcher\\EventDispatcher' => __DIR__ . '/..' . '/jms/serializer/src/EventDispatcher/EventDispatcher.php',
        'JMS\\Serializer\\EventDispatcher\\EventDispatcherInterface' => __DIR__ . '/..' . '/jms/serializer/src/EventDispatcher/EventDispatcherInterface.php',
        'JMS\\Serializer\\EventDispatcher\\EventSubscriberInterface' => __DIR__ . '/..' . '/jms/serializer/src/EventDispatcher/EventSubscriberInterface.php',
        'JMS\\Serializer\\EventDispatcher\\Events' => __DIR__ . '/..' . '/jms/serializer/src/EventDispatcher/Events.php',
        'JMS\\Serializer\\EventDispatcher\\LazyEventDispatcher' => __DIR__ . '/..' . '/jms/serializer/src/EventDispatcher/LazyEventDispatcher.php',
        'JMS\\Serializer\\EventDispatcher\\ObjectEvent' => __DIR__ . '/..' . '/jms/serializer/src/EventDispatcher/ObjectEvent.php',
        'JMS\\Serializer\\EventDispatcher\\PreDeserializeEvent' => __DIR__ . '/..' . '/jms/serializer/src/EventDispatcher/PreDeserializeEvent.php',
        'JMS\\Serializer\\EventDispatcher\\PreSerializeEvent' => __DIR__ . '/..' . '/jms/serializer/src/EventDispatcher/PreSerializeEvent.php',
        'JMS\\Serializer\\EventDispatcher\\Subscriber\\DoctrineProxySubscriber' => __DIR__ . '/..' . '/jms/serializer/src/EventDispatcher/Subscriber/DoctrineProxySubscriber.php',
        'JMS\\Serializer\\EventDispatcher\\Subscriber\\EnumSubscriber' => __DIR__ . '/..' . '/jms/serializer/src/EventDispatcher/Subscriber/EnumSubscriber.php',
        'JMS\\Serializer\\EventDispatcher\\Subscriber\\SymfonyValidatorValidatorSubscriber' => __DIR__ . '/..' . '/jms/serializer/src/EventDispatcher/Subscriber/SymfonyValidatorValidatorSubscriber.php',
        'JMS\\Serializer\\Exception\\CircularReferenceDetectedException' => __DIR__ . '/..' . '/jms/serializer/src/Exception/CircularReferenceDetectedException.php',
        'JMS\\Serializer\\Exception\\Exception' => __DIR__ . '/..' . '/jms/serializer/src/Exception/Exception.php',
        'JMS\\Serializer\\Exception\\ExcludedClassException' => __DIR__ . '/..' . '/jms/serializer/src/Exception/ExcludedClassException.php',
        'JMS\\Serializer\\Exception\\ExpressionLanguageRequiredException' => __DIR__ . '/..' . '/jms/serializer/src/Exception/ExpressionLanguageRequiredException.php',
        'JMS\\Serializer\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/jms/serializer/src/Exception/InvalidArgumentException.php',
        'JMS\\Serializer\\Exception\\InvalidMetadataException' => __DIR__ . '/..' . '/jms/serializer/src/Exception/InvalidMetadataException.php',
        'JMS\\Serializer\\Exception\\LogicException' => __DIR__ . '/..' . '/jms/serializer/src/Exception/LogicException.php',
        'JMS\\Serializer\\Exception\\NonCastableTypeException' => __DIR__ . '/..' . '/jms/serializer/src/Exception/NonCastableTypeException.php',
        'JMS\\Serializer\\Exception\\NonFloatCastableTypeException' => __DIR__ . '/..' . '/jms/serializer/src/Exception/NonFloatCastableTypeException.php',
        'JMS\\Serializer\\Exception\\NonIntCastableTypeException' => __DIR__ . '/..' . '/jms/serializer/src/Exception/NonIntCastableTypeException.php',
        'JMS\\Serializer\\Exception\\NonStringCastableTypeException' => __DIR__ . '/..' . '/jms/serializer/src/Exception/NonStringCastableTypeException.php',
        'JMS\\Serializer\\Exception\\NonVisitableTypeException' => __DIR__ . '/..' . '/jms/serializer/src/Exception/NonVisitableTypeException.php',
        'JMS\\Serializer\\Exception\\NotAcceptableException' => __DIR__ . '/..' . '/jms/serializer/src/Exception/NotAcceptableException.php',
        'JMS\\Serializer\\Exception\\ObjectConstructionException' => __DIR__ . '/..' . '/jms/serializer/src/Exception/ObjectConstructionException.php',
        'JMS\\Serializer\\Exception\\RuntimeException' => __DIR__ . '/..' . '/jms/serializer/src/Exception/RuntimeException.php',
        'JMS\\Serializer\\Exception\\SkipHandlerException' => __DIR__ . '/..' . '/jms/serializer/src/Exception/SkipHandlerException.php',
        'JMS\\Serializer\\Exception\\UninitializedPropertyException' => __DIR__ . '/..' . '/jms/serializer/src/Exception/UninitializedPropertyException.php',
        'JMS\\Serializer\\Exception\\UnsupportedFormatException' => __DIR__ . '/..' . '/jms/serializer/src/Exception/UnsupportedFormatException.php',
        'JMS\\Serializer\\Exception\\ValidationFailedException' => __DIR__ . '/..' . '/jms/serializer/src/Exception/ValidationFailedException.php',
        'JMS\\Serializer\\Exception\\XmlErrorException' => __DIR__ . '/..' . '/jms/serializer/src/Exception/XmlErrorException.php',
        'JMS\\Serializer\\Exclusion\\DepthExclusionStrategy' => __DIR__ . '/..' . '/jms/serializer/src/Exclusion/DepthExclusionStrategy.php',
        'JMS\\Serializer\\Exclusion\\DisjunctExclusionStrategy' => __DIR__ . '/..' . '/jms/serializer/src/Exclusion/DisjunctExclusionStrategy.php',
        'JMS\\Serializer\\Exclusion\\ExclusionStrategyInterface' => __DIR__ . '/..' . '/jms/serializer/src/Exclusion/ExclusionStrategyInterface.php',
        'JMS\\Serializer\\Exclusion\\ExpressionLanguageExclusionStrategy' => __DIR__ . '/..' . '/jms/serializer/src/Exclusion/ExpressionLanguageExclusionStrategy.php',
        'JMS\\Serializer\\Exclusion\\GroupsExclusionStrategy' => __DIR__ . '/..' . '/jms/serializer/src/Exclusion/GroupsExclusionStrategy.php',
        'JMS\\Serializer\\Exclusion\\VersionExclusionStrategy' => __DIR__ . '/..' . '/jms/serializer/src/Exclusion/VersionExclusionStrategy.php',
        'JMS\\Serializer\\Expression\\CompilableExpressionEvaluatorInterface' => __DIR__ . '/..' . '/jms/serializer/src/Expression/CompilableExpressionEvaluatorInterface.php',
        'JMS\\Serializer\\Expression\\Expression' => __DIR__ . '/..' . '/jms/serializer/src/Expression/Expression.php',
        'JMS\\Serializer\\Expression\\ExpressionEvaluator' => __DIR__ . '/..' . '/jms/serializer/src/Expression/ExpressionEvaluator.php',
        'JMS\\Serializer\\Expression\\ExpressionEvaluatorInterface' => __DIR__ . '/..' . '/jms/serializer/src/Expression/ExpressionEvaluatorInterface.php',
        'JMS\\Serializer\\Functions' => __DIR__ . '/..' . '/jms/serializer/src/Functions.php',
        'JMS\\Serializer\\GraphNavigator' => __DIR__ . '/..' . '/jms/serializer/src/GraphNavigator.php',
        'JMS\\Serializer\\GraphNavigatorInterface' => __DIR__ . '/..' . '/jms/serializer/src/GraphNavigatorInterface.php',
        'JMS\\Serializer\\GraphNavigator\\DeserializationGraphNavigator' => __DIR__ . '/..' . '/jms/serializer/src/GraphNavigator/DeserializationGraphNavigator.php',
        'JMS\\Serializer\\GraphNavigator\\Factory\\DeserializationGraphNavigatorFactory' => __DIR__ . '/..' . '/jms/serializer/src/GraphNavigator/Factory/DeserializationGraphNavigatorFactory.php',
        'JMS\\Serializer\\GraphNavigator\\Factory\\GraphNavigatorFactoryInterface' => __DIR__ . '/..' . '/jms/serializer/src/GraphNavigator/Factory/GraphNavigatorFactoryInterface.php',
        'JMS\\Serializer\\GraphNavigator\\Factory\\SerializationGraphNavigatorFactory' => __DIR__ . '/..' . '/jms/serializer/src/GraphNavigator/Factory/SerializationGraphNavigatorFactory.php',
        'JMS\\Serializer\\GraphNavigator\\SerializationGraphNavigator' => __DIR__ . '/..' . '/jms/serializer/src/GraphNavigator/SerializationGraphNavigator.php',
        'JMS\\Serializer\\Handler\\ArrayCollectionHandler' => __DIR__ . '/..' . '/jms/serializer/src/Handler/ArrayCollectionHandler.php',
        'JMS\\Serializer\\Handler\\ConstraintViolationHandler' => __DIR__ . '/..' . '/jms/serializer/src/Handler/ConstraintViolationHandler.php',
        'JMS\\Serializer\\Handler\\DateHandler' => __DIR__ . '/..' . '/jms/serializer/src/Handler/DateHandler.php',
        'JMS\\Serializer\\Handler\\EnumHandler' => __DIR__ . '/..' . '/jms/serializer/src/Handler/EnumHandler.php',
        'JMS\\Serializer\\Handler\\FormErrorHandler' => __DIR__ . '/..' . '/jms/serializer/src/Handler/FormErrorHandler.php',
        'JMS\\Serializer\\Handler\\HandlerRegistry' => __DIR__ . '/..' . '/jms/serializer/src/Handler/HandlerRegistry.php',
        'JMS\\Serializer\\Handler\\HandlerRegistryInterface' => __DIR__ . '/..' . '/jms/serializer/src/Handler/HandlerRegistryInterface.php',
        'JMS\\Serializer\\Handler\\IteratorHandler' => __DIR__ . '/..' . '/jms/serializer/src/Handler/IteratorHandler.php',
        'JMS\\Serializer\\Handler\\LazyHandlerRegistry' => __DIR__ . '/..' . '/jms/serializer/src/Handler/LazyHandlerRegistry.php',
        'JMS\\Serializer\\Handler\\StdClassHandler' => __DIR__ . '/..' . '/jms/serializer/src/Handler/StdClassHandler.php',
        'JMS\\Serializer\\Handler\\SubscribingHandlerInterface' => __DIR__ . '/..' . '/jms/serializer/src/Handler/SubscribingHandlerInterface.php',
        'JMS\\Serializer\\Handler\\SymfonyUidHandler' => __DIR__ . '/..' . '/jms/serializer/src/Handler/SymfonyUidHandler.php',
        'JMS\\Serializer\\Handler\\UnionHandler' => __DIR__ . '/..' . '/jms/serializer/src/Handler/UnionHandler.php',
        'JMS\\Serializer\\JsonDeserializationStrictVisitor' => __DIR__ . '/..' . '/jms/serializer/src/JsonDeserializationStrictVisitor.php',
        'JMS\\Serializer\\JsonDeserializationVisitor' => __DIR__ . '/..' . '/jms/serializer/src/JsonDeserializationVisitor.php',
        'JMS\\Serializer\\JsonSerializationVisitor' => __DIR__ . '/..' . '/jms/serializer/src/JsonSerializationVisitor.php',
        'JMS\\Serializer\\Metadata\\ClassMetadata' => __DIR__ . '/..' . '/jms/serializer/src/Metadata/ClassMetadata.php',
        'JMS\\Serializer\\Metadata\\Driver\\AbstractDoctrineTypeDriver' => __DIR__ . '/..' . '/jms/serializer/src/Metadata/Driver/AbstractDoctrineTypeDriver.php',
        'JMS\\Serializer\\Metadata\\Driver\\AnnotationDriver' => __DIR__ . '/..' . '/jms/serializer/src/Metadata/Driver/AnnotationDriver.php',
        'JMS\\Serializer\\Metadata\\Driver\\AnnotationOrAttributeDriver' => __DIR__ . '/..' . '/jms/serializer/src/Metadata/Driver/AnnotationOrAttributeDriver.php',
        'JMS\\Serializer\\Metadata\\Driver\\AttributeDriver' => __DIR__ . '/..' . '/jms/serializer/src/Metadata/Driver/AttributeDriver.php',
        'JMS\\Serializer\\Metadata\\Driver\\AttributeDriver\\AttributeReader' => __DIR__ . '/..' . '/jms/serializer/src/Metadata/Driver/AttributeDriver/AttributeReader.php',
        'JMS\\Serializer\\Metadata\\Driver\\DefaultValuePropertyDriver' => __DIR__ . '/..' . '/jms/serializer/src/Metadata/Driver/DefaultValuePropertyDriver.php',
        'JMS\\Serializer\\Metadata\\Driver\\DocBlockDriver' => __DIR__ . '/..' . '/jms/serializer/src/Metadata/Driver/DocBlockDriver.php',
        'JMS\\Serializer\\Metadata\\Driver\\DocBlockDriver\\DocBlockTypeResolver' => __DIR__ . '/..' . '/jms/serializer/src/Metadata/Driver/DocBlockDriver/DocBlockTypeResolver.php',
        'JMS\\Serializer\\Metadata\\Driver\\DoctrinePHPCRTypeDriver' => __DIR__ . '/..' . '/jms/serializer/src/Metadata/Driver/DoctrinePHPCRTypeDriver.php',
        'JMS\\Serializer\\Metadata\\Driver\\DoctrineTypeDriver' => __DIR__ . '/..' . '/jms/serializer/src/Metadata/Driver/DoctrineTypeDriver.php',
        'JMS\\Serializer\\Metadata\\Driver\\EnumPropertiesDriver' => __DIR__ . '/..' . '/jms/serializer/src/Metadata/Driver/EnumPropertiesDriver.php',
        'JMS\\Serializer\\Metadata\\Driver\\ExpressionMetadataTrait' => __DIR__ . '/..' . '/jms/serializer/src/Metadata/Driver/ExpressionMetadataTrait.php',
        'JMS\\Serializer\\Metadata\\Driver\\NullDriver' => __DIR__ . '/..' . '/jms/serializer/src/Metadata/Driver/NullDriver.php',
        'JMS\\Serializer\\Metadata\\Driver\\TypedPropertiesDriver' => __DIR__ . '/..' . '/jms/serializer/src/Metadata/Driver/TypedPropertiesDriver.php',
        'JMS\\Serializer\\Metadata\\Driver\\XmlDriver' => __DIR__ . '/..' . '/jms/serializer/src/Metadata/Driver/XmlDriver.php',
        'JMS\\Serializer\\Metadata\\Driver\\YamlDriver' => __DIR__ . '/..' . '/jms/serializer/src/Metadata/Driver/YamlDriver.php',
        'JMS\\Serializer\\Metadata\\ExpressionPropertyMetadata' => __DIR__ . '/..' . '/jms/serializer/src/Metadata/ExpressionPropertyMetadata.php',
        'JMS\\Serializer\\Metadata\\PropertyMetadata' => __DIR__ . '/..' . '/jms/serializer/src/Metadata/PropertyMetadata.php',
        'JMS\\Serializer\\Metadata\\StaticPropertyMetadata' => __DIR__ . '/..' . '/jms/serializer/src/Metadata/StaticPropertyMetadata.php',
        'JMS\\Serializer\\Metadata\\VirtualPropertyMetadata' => __DIR__ . '/..' . '/jms/serializer/src/Metadata/VirtualPropertyMetadata.php',
        'JMS\\Serializer\\Naming\\CamelCaseNamingStrategy' => __DIR__ . '/..' . '/jms/serializer/src/Naming/CamelCaseNamingStrategy.php',
        'JMS\\Serializer\\Naming\\IdenticalPropertyNamingStrategy' => __DIR__ . '/..' . '/jms/serializer/src/Naming/IdenticalPropertyNamingStrategy.php',
        'JMS\\Serializer\\Naming\\PropertyNamingStrategyInterface' => __DIR__ . '/..' . '/jms/serializer/src/Naming/PropertyNamingStrategyInterface.php',
        'JMS\\Serializer\\Naming\\SerializedNameAnnotationStrategy' => __DIR__ . '/..' . '/jms/serializer/src/Naming/SerializedNameAnnotationStrategy.php',
        'JMS\\Serializer\\NullAwareVisitorInterface' => __DIR__ . '/..' . '/jms/serializer/src/NullAwareVisitorInterface.php',
        'JMS\\Serializer\\Ordering\\AlphabeticalPropertyOrderingStrategy' => __DIR__ . '/..' . '/jms/serializer/src/Ordering/AlphabeticalPropertyOrderingStrategy.php',
        'JMS\\Serializer\\Ordering\\CustomPropertyOrderingStrategy' => __DIR__ . '/..' . '/jms/serializer/src/Ordering/CustomPropertyOrderingStrategy.php',
        'JMS\\Serializer\\Ordering\\IdenticalPropertyOrderingStrategy' => __DIR__ . '/..' . '/jms/serializer/src/Ordering/IdenticalPropertyOrderingStrategy.php',
        'JMS\\Serializer\\Ordering\\PropertyOrderingInterface' => __DIR__ . '/..' . '/jms/serializer/src/Ordering/PropertyOrderingInterface.php',
        'JMS\\Serializer\\SerializationContext' => __DIR__ . '/..' . '/jms/serializer/src/SerializationContext.php',
        'JMS\\Serializer\\Serializer' => __DIR__ . '/..' . '/jms/serializer/src/Serializer.php',
        'JMS\\Serializer\\SerializerBuilder' => __DIR__ . '/..' . '/jms/serializer/src/SerializerBuilder.php',
        'JMS\\Serializer\\SerializerInterface' => __DIR__ . '/..' . '/jms/serializer/src/SerializerInterface.php',
        'JMS\\Serializer\\Twig\\SerializerBaseExtension' => __DIR__ . '/..' . '/jms/serializer/src/Twig/SerializerBaseExtension.php',
        'JMS\\Serializer\\Twig\\SerializerExtension' => __DIR__ . '/..' . '/jms/serializer/src/Twig/SerializerExtension.php',
        'JMS\\Serializer\\Twig\\SerializerRuntimeExtension' => __DIR__ . '/..' . '/jms/serializer/src/Twig/SerializerRuntimeExtension.php',
        'JMS\\Serializer\\Twig\\SerializerRuntimeHelper' => __DIR__ . '/..' . '/jms/serializer/src/Twig/SerializerRuntimeHelper.php',
        'JMS\\Serializer\\Type\\Exception\\Exception' => __DIR__ . '/..' . '/jms/serializer/src/Type/Exception/Exception.php',
        'JMS\\Serializer\\Type\\Exception\\InvalidNode' => __DIR__ . '/..' . '/jms/serializer/src/Type/Exception/InvalidNode.php',
        'JMS\\Serializer\\Type\\Exception\\SyntaxError' => __DIR__ . '/..' . '/jms/serializer/src/Type/Exception/SyntaxError.php',
        'JMS\\Serializer\\Type\\Lexer' => __DIR__ . '/..' . '/jms/serializer/src/Type/Lexer.php',
        'JMS\\Serializer\\Type\\Parser' => __DIR__ . '/..' . '/jms/serializer/src/Type/Parser.php',
        'JMS\\Serializer\\Type\\ParserInterface' => __DIR__ . '/..' . '/jms/serializer/src/Type/ParserInterface.php',
        'JMS\\Serializer\\Type\\Type' => __DIR__ . '/..' . '/jms/serializer/src/Type/Type.php',
        'JMS\\Serializer\\VisitorInterface' => __DIR__ . '/..' . '/jms/serializer/src/VisitorInterface.php',
        'JMS\\Serializer\\Visitor\\DeserializationVisitorInterface' => __DIR__ . '/..' . '/jms/serializer/src/Visitor/DeserializationVisitorInterface.php',
        'JMS\\Serializer\\Visitor\\Factory\\DeserializationVisitorFactory' => __DIR__ . '/..' . '/jms/serializer/src/Visitor/Factory/DeserializationVisitorFactory.php',
        'JMS\\Serializer\\Visitor\\Factory\\JsonDeserializationVisitorFactory' => __DIR__ . '/..' . '/jms/serializer/src/Visitor/Factory/JsonDeserializationVisitorFactory.php',
        'JMS\\Serializer\\Visitor\\Factory\\JsonSerializationVisitorFactory' => __DIR__ . '/..' . '/jms/serializer/src/Visitor/Factory/JsonSerializationVisitorFactory.php',
        'JMS\\Serializer\\Visitor\\Factory\\SerializationVisitorFactory' => __DIR__ . '/..' . '/jms/serializer/src/Visitor/Factory/SerializationVisitorFactory.php',
        'JMS\\Serializer\\Visitor\\Factory\\XmlDeserializationVisitorFactory' => __DIR__ . '/..' . '/jms/serializer/src/Visitor/Factory/XmlDeserializationVisitorFactory.php',
        'JMS\\Serializer\\Visitor\\Factory\\XmlSerializationVisitorFactory' => __DIR__ . '/..' . '/jms/serializer/src/Visitor/Factory/XmlSerializationVisitorFactory.php',
        'JMS\\Serializer\\Visitor\\SerializationVisitorInterface' => __DIR__ . '/..' . '/jms/serializer/src/Visitor/SerializationVisitorInterface.php',
        'JMS\\Serializer\\XmlDeserializationVisitor' => __DIR__ . '/..' . '/jms/serializer/src/XmlDeserializationVisitor.php',
        'JMS\\Serializer\\XmlSerializationVisitor' => __DIR__ . '/..' . '/jms/serializer/src/XmlSerializationVisitor.php',
        'JsonException' => __DIR__ . '/..' . '/symfony/polyfill-php73/Resources/stubs/JsonException.php',
        'Metadata\\AdvancedMetadataFactoryInterface' => __DIR__ . '/..' . '/jms/metadata/src/AdvancedMetadataFactoryInterface.php',
        'Metadata\\Cache\\CacheInterface' => __DIR__ . '/..' . '/jms/metadata/src/Cache/CacheInterface.php',
        'Metadata\\Cache\\ClearableCacheInterface' => __DIR__ . '/..' . '/jms/metadata/src/Cache/ClearableCacheInterface.php',
        'Metadata\\Cache\\DoctrineCacheAdapter' => __DIR__ . '/..' . '/jms/metadata/src/Cache/DoctrineCacheAdapter.php',
        'Metadata\\Cache\\FileCache' => __DIR__ . '/..' . '/jms/metadata/src/Cache/FileCache.php',
        'Metadata\\Cache\\PsrCacheAdapter' => __DIR__ . '/..' . '/jms/metadata/src/Cache/PsrCacheAdapter.php',
        'Metadata\\ClassHierarchyMetadata' => __DIR__ . '/..' . '/jms/metadata/src/ClassHierarchyMetadata.php',
        'Metadata\\ClassMetadata' => __DIR__ . '/..' . '/jms/metadata/src/ClassMetadata.php',
        'Metadata\\Driver\\AbstractFileDriver' => __DIR__ . '/..' . '/jms/metadata/src/Driver/AbstractFileDriver.php',
        'Metadata\\Driver\\AdvancedDriverInterface' => __DIR__ . '/..' . '/jms/metadata/src/Driver/AdvancedDriverInterface.php',
        'Metadata\\Driver\\AdvancedFileLocatorInterface' => __DIR__ . '/..' . '/jms/metadata/src/Driver/AdvancedFileLocatorInterface.php',
        'Metadata\\Driver\\DriverChain' => __DIR__ . '/..' . '/jms/metadata/src/Driver/DriverChain.php',
        'Metadata\\Driver\\DriverInterface' => __DIR__ . '/..' . '/jms/metadata/src/Driver/DriverInterface.php',
        'Metadata\\Driver\\FileLocator' => __DIR__ . '/..' . '/jms/metadata/src/Driver/FileLocator.php',
        'Metadata\\Driver\\FileLocatorInterface' => __DIR__ . '/..' . '/jms/metadata/src/Driver/FileLocatorInterface.php',
        'Metadata\\Driver\\LazyLoadingDriver' => __DIR__ . '/..' . '/jms/metadata/src/Driver/LazyLoadingDriver.php',
        'Metadata\\Driver\\TraceableFileLocatorInterface' => __DIR__ . '/..' . '/jms/metadata/src/Driver/TraceableFileLocatorInterface.php',
        'Metadata\\MergeableClassMetadata' => __DIR__ . '/..' . '/jms/metadata/src/MergeableClassMetadata.php',
        'Metadata\\MergeableInterface' => __DIR__ . '/..' . '/jms/metadata/src/MergeableInterface.php',
        'Metadata\\MetadataFactory' => __DIR__ . '/..' . '/jms/metadata/src/MetadataFactory.php',
        'Metadata\\MetadataFactoryInterface' => __DIR__ . '/..' . '/jms/metadata/src/MetadataFactoryInterface.php',
        'Metadata\\MethodMetadata' => __DIR__ . '/..' . '/jms/metadata/src/MethodMetadata.php',
        'Metadata\\NullMetadata' => __DIR__ . '/..' . '/jms/metadata/src/NullMetadata.php',
        'Metadata\\PropertyMetadata' => __DIR__ . '/..' . '/jms/metadata/src/PropertyMetadata.php',
        'Metadata\\SerializationHelper' => __DIR__ . '/..' . '/jms/metadata/src/SerializationHelper.php',
        'Normalizer' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php',
        'PHPStan\\PhpDocParser\\Ast\\AbstractNodeVisitor' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/AbstractNodeVisitor.php',
        'PHPStan\\PhpDocParser\\Ast\\Attribute' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/Attribute.php',
        'PHPStan\\PhpDocParser\\Ast\\Comment' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/Comment.php',
        'PHPStan\\PhpDocParser\\Ast\\ConstExpr\\ConstExprArrayItemNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/ConstExpr/ConstExprArrayItemNode.php',
        'PHPStan\\PhpDocParser\\Ast\\ConstExpr\\ConstExprArrayNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/ConstExpr/ConstExprArrayNode.php',
        'PHPStan\\PhpDocParser\\Ast\\ConstExpr\\ConstExprFalseNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/ConstExpr/ConstExprFalseNode.php',
        'PHPStan\\PhpDocParser\\Ast\\ConstExpr\\ConstExprFloatNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/ConstExpr/ConstExprFloatNode.php',
        'PHPStan\\PhpDocParser\\Ast\\ConstExpr\\ConstExprIntegerNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/ConstExpr/ConstExprIntegerNode.php',
        'PHPStan\\PhpDocParser\\Ast\\ConstExpr\\ConstExprNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/ConstExpr/ConstExprNode.php',
        'PHPStan\\PhpDocParser\\Ast\\ConstExpr\\ConstExprNullNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/ConstExpr/ConstExprNullNode.php',
        'PHPStan\\PhpDocParser\\Ast\\ConstExpr\\ConstExprStringNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/ConstExpr/ConstExprStringNode.php',
        'PHPStan\\PhpDocParser\\Ast\\ConstExpr\\ConstExprTrueNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/ConstExpr/ConstExprTrueNode.php',
        'PHPStan\\PhpDocParser\\Ast\\ConstExpr\\ConstFetchNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/ConstExpr/ConstFetchNode.php',
        'PHPStan\\PhpDocParser\\Ast\\ConstExpr\\DoctrineConstExprStringNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/ConstExpr/DoctrineConstExprStringNode.php',
        'PHPStan\\PhpDocParser\\Ast\\Node' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/Node.php',
        'PHPStan\\PhpDocParser\\Ast\\NodeAttributes' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/NodeAttributes.php',
        'PHPStan\\PhpDocParser\\Ast\\NodeTraverser' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/NodeTraverser.php',
        'PHPStan\\PhpDocParser\\Ast\\NodeVisitor' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/NodeVisitor.php',
        'PHPStan\\PhpDocParser\\Ast\\NodeVisitor\\CloningVisitor' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/NodeVisitor/CloningVisitor.php',
        'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\AssertTagMethodValueNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/AssertTagMethodValueNode.php',
        'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\AssertTagPropertyValueNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/AssertTagPropertyValueNode.php',
        'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\AssertTagValueNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/AssertTagValueNode.php',
        'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\DeprecatedTagValueNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/DeprecatedTagValueNode.php',
        'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\Doctrine\\DoctrineAnnotation' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/Doctrine/DoctrineAnnotation.php',
        'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\Doctrine\\DoctrineArgument' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/Doctrine/DoctrineArgument.php',
        'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\Doctrine\\DoctrineArray' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/Doctrine/DoctrineArray.php',
        'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\Doctrine\\DoctrineArrayItem' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/Doctrine/DoctrineArrayItem.php',
        'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\Doctrine\\DoctrineTagValueNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/Doctrine/DoctrineTagValueNode.php',
        'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\ExtendsTagValueNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/ExtendsTagValueNode.php',
        'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\GenericTagValueNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/GenericTagValueNode.php',
        'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\ImplementsTagValueNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/ImplementsTagValueNode.php',
        'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\InvalidTagValueNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/InvalidTagValueNode.php',
        'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\MethodTagValueNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/MethodTagValueNode.php',
        'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\MethodTagValueParameterNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/MethodTagValueParameterNode.php',
        'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\MixinTagValueNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/MixinTagValueNode.php',
        'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\ParamClosureThisTagValueNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/ParamClosureThisTagValueNode.php',
        'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\ParamImmediatelyInvokedCallableTagValueNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/ParamImmediatelyInvokedCallableTagValueNode.php',
        'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\ParamLaterInvokedCallableTagValueNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/ParamLaterInvokedCallableTagValueNode.php',
        'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\ParamOutTagValueNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/ParamOutTagValueNode.php',
        'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\ParamTagValueNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/ParamTagValueNode.php',
        'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\PhpDocChildNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/PhpDocChildNode.php',
        'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\PhpDocNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/PhpDocNode.php',
        'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\PhpDocTagNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/PhpDocTagNode.php',
        'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\PhpDocTagValueNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/PhpDocTagValueNode.php',
        'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\PhpDocTextNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/PhpDocTextNode.php',
        'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\PropertyTagValueNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/PropertyTagValueNode.php',
        'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\PureUnlessCallableIsImpureTagValueNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/PureUnlessCallableIsImpureTagValueNode.php',
        'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\RequireExtendsTagValueNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/RequireExtendsTagValueNode.php',
        'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\RequireImplementsTagValueNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/RequireImplementsTagValueNode.php',
        'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\ReturnTagValueNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/ReturnTagValueNode.php',
        'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\SealedTagValueNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/SealedTagValueNode.php',
        'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\SelfOutTagValueNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/SelfOutTagValueNode.php',
        'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\TemplateTagValueNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/TemplateTagValueNode.php',
        'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\ThrowsTagValueNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/ThrowsTagValueNode.php',
        'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\TypeAliasImportTagValueNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/TypeAliasImportTagValueNode.php',
        'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\TypeAliasTagValueNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/TypeAliasTagValueNode.php',
        'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\TypelessParamTagValueNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/TypelessParamTagValueNode.php',
        'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\UsesTagValueNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/UsesTagValueNode.php',
        'PHPStan\\PhpDocParser\\Ast\\PhpDoc\\VarTagValueNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/PhpDoc/VarTagValueNode.php',
        'PHPStan\\PhpDocParser\\Ast\\Type\\ArrayShapeItemNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/Type/ArrayShapeItemNode.php',
        'PHPStan\\PhpDocParser\\Ast\\Type\\ArrayShapeNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/Type/ArrayShapeNode.php',
        'PHPStan\\PhpDocParser\\Ast\\Type\\ArrayShapeUnsealedTypeNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/Type/ArrayShapeUnsealedTypeNode.php',
        'PHPStan\\PhpDocParser\\Ast\\Type\\ArrayTypeNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/Type/ArrayTypeNode.php',
        'PHPStan\\PhpDocParser\\Ast\\Type\\CallableTypeNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/Type/CallableTypeNode.php',
        'PHPStan\\PhpDocParser\\Ast\\Type\\CallableTypeParameterNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/Type/CallableTypeParameterNode.php',
        'PHPStan\\PhpDocParser\\Ast\\Type\\ConditionalTypeForParameterNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/Type/ConditionalTypeForParameterNode.php',
        'PHPStan\\PhpDocParser\\Ast\\Type\\ConditionalTypeNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/Type/ConditionalTypeNode.php',
        'PHPStan\\PhpDocParser\\Ast\\Type\\ConstTypeNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/Type/ConstTypeNode.php',
        'PHPStan\\PhpDocParser\\Ast\\Type\\GenericTypeNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/Type/GenericTypeNode.php',
        'PHPStan\\PhpDocParser\\Ast\\Type\\IdentifierTypeNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/Type/IdentifierTypeNode.php',
        'PHPStan\\PhpDocParser\\Ast\\Type\\IntersectionTypeNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/Type/IntersectionTypeNode.php',
        'PHPStan\\PhpDocParser\\Ast\\Type\\InvalidTypeNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/Type/InvalidTypeNode.php',
        'PHPStan\\PhpDocParser\\Ast\\Type\\NullableTypeNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/Type/NullableTypeNode.php',
        'PHPStan\\PhpDocParser\\Ast\\Type\\ObjectShapeItemNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/Type/ObjectShapeItemNode.php',
        'PHPStan\\PhpDocParser\\Ast\\Type\\ObjectShapeNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/Type/ObjectShapeNode.php',
        'PHPStan\\PhpDocParser\\Ast\\Type\\OffsetAccessTypeNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/Type/OffsetAccessTypeNode.php',
        'PHPStan\\PhpDocParser\\Ast\\Type\\ThisTypeNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/Type/ThisTypeNode.php',
        'PHPStan\\PhpDocParser\\Ast\\Type\\TypeNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/Type/TypeNode.php',
        'PHPStan\\PhpDocParser\\Ast\\Type\\UnionTypeNode' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Ast/Type/UnionTypeNode.php',
        'PHPStan\\PhpDocParser\\Lexer\\Lexer' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Lexer/Lexer.php',
        'PHPStan\\PhpDocParser\\ParserConfig' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/ParserConfig.php',
        'PHPStan\\PhpDocParser\\Parser\\ConstExprParser' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Parser/ConstExprParser.php',
        'PHPStan\\PhpDocParser\\Parser\\ParserException' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Parser/ParserException.php',
        'PHPStan\\PhpDocParser\\Parser\\PhpDocParser' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Parser/PhpDocParser.php',
        'PHPStan\\PhpDocParser\\Parser\\StringUnescaper' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Parser/StringUnescaper.php',
        'PHPStan\\PhpDocParser\\Parser\\TokenIterator' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Parser/TokenIterator.php',
        'PHPStan\\PhpDocParser\\Parser\\TypeParser' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Parser/TypeParser.php',
        'PHPStan\\PhpDocParser\\Printer\\DiffElem' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Printer/DiffElem.php',
        'PHPStan\\PhpDocParser\\Printer\\Differ' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Printer/Differ.php',
        'PHPStan\\PhpDocParser\\Printer\\Printer' => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src/Printer/Printer.php',
        'PhpToken' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php',
        'Psr\\Cache\\CacheException' => __DIR__ . '/..' . '/psr/cache/src/CacheException.php',
        'Psr\\Cache\\CacheItemInterface' => __DIR__ . '/..' . '/psr/cache/src/CacheItemInterface.php',
        'Psr\\Cache\\CacheItemPoolInterface' => __DIR__ . '/..' . '/psr/cache/src/CacheItemPoolInterface.php',
        'Psr\\Cache\\InvalidArgumentException' => __DIR__ . '/..' . '/psr/cache/src/InvalidArgumentException.php',
        'Psr\\Container\\ContainerExceptionInterface' => __DIR__ . '/..' . '/psr/container/src/ContainerExceptionInterface.php',
        'Psr\\Container\\ContainerInterface' => __DIR__ . '/..' . '/psr/container/src/ContainerInterface.php',
        'Psr\\Container\\NotFoundExceptionInterface' => __DIR__ . '/..' . '/psr/container/src/NotFoundExceptionInterface.php',
        'Psr\\Log\\AbstractLogger' => __DIR__ . '/..' . '/psr/log/Psr/Log/AbstractLogger.php',
        'Psr\\Log\\InvalidArgumentException' => __DIR__ . '/..' . '/psr/log/Psr/Log/InvalidArgumentException.php',
        'Psr\\Log\\LogLevel' => __DIR__ . '/..' . '/psr/log/Psr/Log/LogLevel.php',
        'Psr\\Log\\LoggerAwareInterface' => __DIR__ . '/..' . '/psr/log/Psr/Log/LoggerAwareInterface.php',
        'Psr\\Log\\LoggerAwareTrait' => __DIR__ . '/..' . '/psr/log/Psr/Log/LoggerAwareTrait.php',
        'Psr\\Log\\LoggerInterface' => __DIR__ . '/..' . '/psr/log/Psr/Log/LoggerInterface.php',
        'Psr\\Log\\LoggerTrait' => __DIR__ . '/..' . '/psr/log/Psr/Log/LoggerTrait.php',
        'Psr\\Log\\NullLogger' => __DIR__ . '/..' . '/psr/log/Psr/Log/NullLogger.php',
        'Psr\\Log\\Test\\DummyTest' => __DIR__ . '/..' . '/psr/log/Psr/Log/Test/DummyTest.php',
        'Psr\\Log\\Test\\LoggerInterfaceTest' => __DIR__ . '/..' . '/psr/log/Psr/Log/Test/LoggerInterfaceTest.php',
        'Psr\\Log\\Test\\TestLogger' => __DIR__ . '/..' . '/psr/log/Psr/Log/Test/TestLogger.php',
        'SensioLabs\\Insight\\Cli\\Application' => __DIR__ . '/../..' . '/Cli/Application.php',
        'SensioLabs\\Insight\\Cli\\Command\\AnalysisCommand' => __DIR__ . '/../..' . '/Cli/Command/AnalysisCommand.php',
        'SensioLabs\\Insight\\Cli\\Command\\AnalyzeCommand' => __DIR__ . '/../..' . '/Cli/Command/AnalyzeCommand.php',
        'SensioLabs\\Insight\\Cli\\Command\\ConfigureCommand' => __DIR__ . '/../..' . '/Cli/Command/ConfigureCommand.php',
        'SensioLabs\\Insight\\Cli\\Command\\NeedConfigurationInterface' => __DIR__ . '/../..' . '/Cli/Command/NeedConfigurationInterface.php',
        'SensioLabs\\Insight\\Cli\\Command\\ProjectsCommand' => __DIR__ . '/../..' . '/Cli/Command/ProjectsCommand.php',
        'SensioLabs\\Insight\\Cli\\Command\\SelfUpdateCommand' => __DIR__ . '/../..' . '/Cli/Command/SelfUpdateCommand.php',
        'SensioLabs\\Insight\\Cli\\Configuration' => __DIR__ . '/../..' . '/Cli/Configuration.php',
        'SensioLabs\\Insight\\Cli\\Descriptor\\AbstractDescriptor' => __DIR__ . '/../..' . '/Cli/Descriptor/AbstractDescriptor.php',
        'SensioLabs\\Insight\\Cli\\Descriptor\\JsonDescriptor' => __DIR__ . '/../..' . '/Cli/Descriptor/JsonDescriptor.php',
        'SensioLabs\\Insight\\Cli\\Descriptor\\PmdDescriptor' => __DIR__ . '/../..' . '/Cli/Descriptor/PmdDescriptor.php',
        'SensioLabs\\Insight\\Cli\\Descriptor\\TextDescriptor' => __DIR__ . '/../..' . '/Cli/Descriptor/TextDescriptor.php',
        'SensioLabs\\Insight\\Cli\\Descriptor\\XmlDescriptor' => __DIR__ . '/../..' . '/Cli/Descriptor/XmlDescriptor.php',
        'SensioLabs\\Insight\\Cli\\Helper\\ConfigurationHelper' => __DIR__ . '/../..' . '/Cli/Helper/ConfigurationHelper.php',
        'SensioLabs\\Insight\\Cli\\Helper\\DescriptorHelper' => __DIR__ . '/../..' . '/Cli/Helper/DescriptorHelper.php',
        'SensioLabs\\Insight\\Cli\\Helper\\FailConditionHelper' => __DIR__ . '/../..' . '/Cli/Helper/FailConditionHelper.php',
        'SensioLabs\\Insight\\Sdk\\Api' => __DIR__ . '/../..' . '/Sdk/Api.php',
        'SensioLabs\\Insight\\Sdk\\Exception\\ApiClientException' => __DIR__ . '/../..' . '/Sdk/Exception/ApiClientException.php',
        'SensioLabs\\Insight\\Sdk\\Exception\\ApiParserException' => __DIR__ . '/../..' . '/Sdk/Exception/ApiParserException.php',
        'SensioLabs\\Insight\\Sdk\\Exception\\ApiServerException' => __DIR__ . '/../..' . '/Sdk/Exception/ApiServerException.php',
        'SensioLabs\\Insight\\Sdk\\Exception\\ExceptionInterface' => __DIR__ . '/../..' . '/Sdk/Exception/ExceptionInterface.php',
        'SensioLabs\\Insight\\Sdk\\Model\\Analyses' => __DIR__ . '/../..' . '/Sdk/Model/Analyses.php',
        'SensioLabs\\Insight\\Sdk\\Model\\Analysis' => __DIR__ . '/../..' . '/Sdk/Model/Analysis.php',
        'SensioLabs\\Insight\\Sdk\\Model\\Error' => __DIR__ . '/../..' . '/Sdk/Model/Error.php',
        'SensioLabs\\Insight\\Sdk\\Model\\Link' => __DIR__ . '/../..' . '/Sdk/Model/Link.php',
        'SensioLabs\\Insight\\Sdk\\Model\\Project' => __DIR__ . '/../..' . '/Sdk/Model/Project.php',
        'SensioLabs\\Insight\\Sdk\\Model\\Projects' => __DIR__ . '/../..' . '/Sdk/Model/Projects.php',
        'SensioLabs\\Insight\\Sdk\\Model\\Violation' => __DIR__ . '/../..' . '/Sdk/Model/Violation.php',
        'SensioLabs\\Insight\\Sdk\\Model\\Violations' => __DIR__ . '/../..' . '/Sdk/Model/Violations.php',
        'SensioLabs\\Insight\\Sdk\\Parser' => __DIR__ . '/../..' . '/Sdk/Parser.php',
        'Stringable' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Stringable.php',
        'Symfony\\Component\\Cache\\Adapter\\AbstractAdapter' => __DIR__ . '/..' . '/symfony/cache/Adapter/AbstractAdapter.php',
        'Symfony\\Component\\Cache\\Adapter\\AbstractTagAwareAdapter' => __DIR__ . '/..' . '/symfony/cache/Adapter/AbstractTagAwareAdapter.php',
        'Symfony\\Component\\Cache\\Adapter\\AdapterInterface' => __DIR__ . '/..' . '/symfony/cache/Adapter/AdapterInterface.php',
        'Symfony\\Component\\Cache\\Adapter\\ApcuAdapter' => __DIR__ . '/..' . '/symfony/cache/Adapter/ApcuAdapter.php',
        'Symfony\\Component\\Cache\\Adapter\\ArrayAdapter' => __DIR__ . '/..' . '/symfony/cache/Adapter/ArrayAdapter.php',
        'Symfony\\Component\\Cache\\Adapter\\ChainAdapter' => __DIR__ . '/..' . '/symfony/cache/Adapter/ChainAdapter.php',
        'Symfony\\Component\\Cache\\Adapter\\CouchbaseBucketAdapter' => __DIR__ . '/..' . '/symfony/cache/Adapter/CouchbaseBucketAdapter.php',
        'Symfony\\Component\\Cache\\Adapter\\CouchbaseCollectionAdapter' => __DIR__ . '/..' . '/symfony/cache/Adapter/CouchbaseCollectionAdapter.php',
        'Symfony\\Component\\Cache\\Adapter\\DoctrineAdapter' => __DIR__ . '/..' . '/symfony/cache/Adapter/DoctrineAdapter.php',
        'Symfony\\Component\\Cache\\Adapter\\DoctrineDbalAdapter' => __DIR__ . '/..' . '/symfony/cache/Adapter/DoctrineDbalAdapter.php',
        'Symfony\\Component\\Cache\\Adapter\\FilesystemAdapter' => __DIR__ . '/..' . '/symfony/cache/Adapter/FilesystemAdapter.php',
        'Symfony\\Component\\Cache\\Adapter\\FilesystemTagAwareAdapter' => __DIR__ . '/..' . '/symfony/cache/Adapter/FilesystemTagAwareAdapter.php',
        'Symfony\\Component\\Cache\\Adapter\\MemcachedAdapter' => __DIR__ . '/..' . '/symfony/cache/Adapter/MemcachedAdapter.php',
        'Symfony\\Component\\Cache\\Adapter\\NullAdapter' => __DIR__ . '/..' . '/symfony/cache/Adapter/NullAdapter.php',
        'Symfony\\Component\\Cache\\Adapter\\ParameterNormalizer' => __DIR__ . '/..' . '/symfony/cache/Adapter/ParameterNormalizer.php',
        'Symfony\\Component\\Cache\\Adapter\\PdoAdapter' => __DIR__ . '/..' . '/symfony/cache/Adapter/PdoAdapter.php',
        'Symfony\\Component\\Cache\\Adapter\\PhpArrayAdapter' => __DIR__ . '/..' . '/symfony/cache/Adapter/PhpArrayAdapter.php',
        'Symfony\\Component\\Cache\\Adapter\\PhpFilesAdapter' => __DIR__ . '/..' . '/symfony/cache/Adapter/PhpFilesAdapter.php',
        'Symfony\\Component\\Cache\\Adapter\\ProxyAdapter' => __DIR__ . '/..' . '/symfony/cache/Adapter/ProxyAdapter.php',
        'Symfony\\Component\\Cache\\Adapter\\Psr16Adapter' => __DIR__ . '/..' . '/symfony/cache/Adapter/Psr16Adapter.php',
        'Symfony\\Component\\Cache\\Adapter\\RedisAdapter' => __DIR__ . '/..' . '/symfony/cache/Adapter/RedisAdapter.php',
        'Symfony\\Component\\Cache\\Adapter\\RedisTagAwareAdapter' => __DIR__ . '/..' . '/symfony/cache/Adapter/RedisTagAwareAdapter.php',
        'Symfony\\Component\\Cache\\Adapter\\TagAwareAdapter' => __DIR__ . '/..' . '/symfony/cache/Adapter/TagAwareAdapter.php',
        'Symfony\\Component\\Cache\\Adapter\\TagAwareAdapterInterface' => __DIR__ . '/..' . '/symfony/cache/Adapter/TagAwareAdapterInterface.php',
        'Symfony\\Component\\Cache\\Adapter\\TraceableAdapter' => __DIR__ . '/..' . '/symfony/cache/Adapter/TraceableAdapter.php',
        'Symfony\\Component\\Cache\\Adapter\\TraceableTagAwareAdapter' => __DIR__ . '/..' . '/symfony/cache/Adapter/TraceableTagAwareAdapter.php',
        'Symfony\\Component\\Cache\\CacheItem' => __DIR__ . '/..' . '/symfony/cache/CacheItem.php',
        'Symfony\\Component\\Cache\\DataCollector\\CacheDataCollector' => __DIR__ . '/..' . '/symfony/cache/DataCollector/CacheDataCollector.php',
        'Symfony\\Component\\Cache\\DependencyInjection\\CacheCollectorPass' => __DIR__ . '/..' . '/symfony/cache/DependencyInjection/CacheCollectorPass.php',
        'Symfony\\Component\\Cache\\DependencyInjection\\CachePoolClearerPass' => __DIR__ . '/..' . '/symfony/cache/DependencyInjection/CachePoolClearerPass.php',
        'Symfony\\Component\\Cache\\DependencyInjection\\CachePoolPass' => __DIR__ . '/..' . '/symfony/cache/DependencyInjection/CachePoolPass.php',
        'Symfony\\Component\\Cache\\DependencyInjection\\CachePoolPrunerPass' => __DIR__ . '/..' . '/symfony/cache/DependencyInjection/CachePoolPrunerPass.php',
        'Symfony\\Component\\Cache\\DoctrineProvider' => __DIR__ . '/..' . '/symfony/cache/DoctrineProvider.php',
        'Symfony\\Component\\Cache\\Exception\\CacheException' => __DIR__ . '/..' . '/symfony/cache/Exception/CacheException.php',
        'Symfony\\Component\\Cache\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/symfony/cache/Exception/InvalidArgumentException.php',
        'Symfony\\Component\\Cache\\Exception\\LogicException' => __DIR__ . '/..' . '/symfony/cache/Exception/LogicException.php',
        'Symfony\\Component\\Cache\\LockRegistry' => __DIR__ . '/..' . '/symfony/cache/LockRegistry.php',
        'Symfony\\Component\\Cache\\Marshaller\\DefaultMarshaller' => __DIR__ . '/..' . '/symfony/cache/Marshaller/DefaultMarshaller.php',
        'Symfony\\Component\\Cache\\Marshaller\\DeflateMarshaller' => __DIR__ . '/..' . '/symfony/cache/Marshaller/DeflateMarshaller.php',
        'Symfony\\Component\\Cache\\Marshaller\\MarshallerInterface' => __DIR__ . '/..' . '/symfony/cache/Marshaller/MarshallerInterface.php',
        'Symfony\\Component\\Cache\\Marshaller\\SodiumMarshaller' => __DIR__ . '/..' . '/symfony/cache/Marshaller/SodiumMarshaller.php',
        'Symfony\\Component\\Cache\\Marshaller\\TagAwareMarshaller' => __DIR__ . '/..' . '/symfony/cache/Marshaller/TagAwareMarshaller.php',
        'Symfony\\Component\\Cache\\Messenger\\EarlyExpirationDispatcher' => __DIR__ . '/..' . '/symfony/cache/Messenger/EarlyExpirationDispatcher.php',
        'Symfony\\Component\\Cache\\Messenger\\EarlyExpirationHandler' => __DIR__ . '/..' . '/symfony/cache/Messenger/EarlyExpirationHandler.php',
        'Symfony\\Component\\Cache\\Messenger\\EarlyExpirationMessage' => __DIR__ . '/..' . '/symfony/cache/Messenger/EarlyExpirationMessage.php',
        'Symfony\\Component\\Cache\\PruneableInterface' => __DIR__ . '/..' . '/symfony/cache/PruneableInterface.php',
        'Symfony\\Component\\Cache\\Psr16Cache' => __DIR__ . '/..' . '/symfony/cache/Psr16Cache.php',
        'Symfony\\Component\\Cache\\ResettableInterface' => __DIR__ . '/..' . '/symfony/cache/ResettableInterface.php',
        'Symfony\\Component\\Cache\\Traits\\AbstractAdapterTrait' => __DIR__ . '/..' . '/symfony/cache/Traits/AbstractAdapterTrait.php',
        'Symfony\\Component\\Cache\\Traits\\ContractsTrait' => __DIR__ . '/..' . '/symfony/cache/Traits/ContractsTrait.php',
        'Symfony\\Component\\Cache\\Traits\\FilesystemCommonTrait' => __DIR__ . '/..' . '/symfony/cache/Traits/FilesystemCommonTrait.php',
        'Symfony\\Component\\Cache\\Traits\\FilesystemTrait' => __DIR__ . '/..' . '/symfony/cache/Traits/FilesystemTrait.php',
        'Symfony\\Component\\Cache\\Traits\\ProxyTrait' => __DIR__ . '/..' . '/symfony/cache/Traits/ProxyTrait.php',
        'Symfony\\Component\\Cache\\Traits\\RedisClusterNodeProxy' => __DIR__ . '/..' . '/symfony/cache/Traits/RedisClusterNodeProxy.php',
        'Symfony\\Component\\Cache\\Traits\\RedisClusterProxy' => __DIR__ . '/..' . '/symfony/cache/Traits/RedisClusterProxy.php',
        'Symfony\\Component\\Cache\\Traits\\RedisProxy' => __DIR__ . '/..' . '/symfony/cache/Traits/RedisProxy.php',
        'Symfony\\Component\\Cache\\Traits\\RedisTrait' => __DIR__ . '/..' . '/symfony/cache/Traits/RedisTrait.php',
        'Symfony\\Component\\Console\\Application' => __DIR__ . '/..' . '/symfony/console/Application.php',
        'Symfony\\Component\\Console\\Attribute\\AsCommand' => __DIR__ . '/..' . '/symfony/console/Attribute/AsCommand.php',
        'Symfony\\Component\\Console\\CI\\GithubActionReporter' => __DIR__ . '/..' . '/symfony/console/CI/GithubActionReporter.php',
        'Symfony\\Component\\Console\\Color' => __DIR__ . '/..' . '/symfony/console/Color.php',
        'Symfony\\Component\\Console\\CommandLoader\\CommandLoaderInterface' => __DIR__ . '/..' . '/symfony/console/CommandLoader/CommandLoaderInterface.php',
        'Symfony\\Component\\Console\\CommandLoader\\ContainerCommandLoader' => __DIR__ . '/..' . '/symfony/console/CommandLoader/ContainerCommandLoader.php',
        'Symfony\\Component\\Console\\CommandLoader\\FactoryCommandLoader' => __DIR__ . '/..' . '/symfony/console/CommandLoader/FactoryCommandLoader.php',
        'Symfony\\Component\\Console\\Command\\Command' => __DIR__ . '/..' . '/symfony/console/Command/Command.php',
        'Symfony\\Component\\Console\\Command\\CompleteCommand' => __DIR__ . '/..' . '/symfony/console/Command/CompleteCommand.php',
        'Symfony\\Component\\Console\\Command\\DumpCompletionCommand' => __DIR__ . '/..' . '/symfony/console/Command/DumpCompletionCommand.php',
        'Symfony\\Component\\Console\\Command\\HelpCommand' => __DIR__ . '/..' . '/symfony/console/Command/HelpCommand.php',
        'Symfony\\Component\\Console\\Command\\LazyCommand' => __DIR__ . '/..' . '/symfony/console/Command/LazyCommand.php',
        'Symfony\\Component\\Console\\Command\\ListCommand' => __DIR__ . '/..' . '/symfony/console/Command/ListCommand.php',
        'Symfony\\Component\\Console\\Command\\LockableTrait' => __DIR__ . '/..' . '/symfony/console/Command/LockableTrait.php',
        'Symfony\\Component\\Console\\Command\\SignalableCommandInterface' => __DIR__ . '/..' . '/symfony/console/Command/SignalableCommandInterface.php',
        'Symfony\\Component\\Console\\Completion\\CompletionInput' => __DIR__ . '/..' . '/symfony/console/Completion/CompletionInput.php',
        'Symfony\\Component\\Console\\Completion\\CompletionSuggestions' => __DIR__ . '/..' . '/symfony/console/Completion/CompletionSuggestions.php',
        'Symfony\\Component\\Console\\Completion\\Output\\BashCompletionOutput' => __DIR__ . '/..' . '/symfony/console/Completion/Output/BashCompletionOutput.php',
        'Symfony\\Component\\Console\\Completion\\Output\\CompletionOutputInterface' => __DIR__ . '/..' . '/symfony/console/Completion/Output/CompletionOutputInterface.php',
        'Symfony\\Component\\Console\\Completion\\Suggestion' => __DIR__ . '/..' . '/symfony/console/Completion/Suggestion.php',
        'Symfony\\Component\\Console\\ConsoleEvents' => __DIR__ . '/..' . '/symfony/console/ConsoleEvents.php',
        'Symfony\\Component\\Console\\Cursor' => __DIR__ . '/..' . '/symfony/console/Cursor.php',
        'Symfony\\Component\\Console\\DependencyInjection\\AddConsoleCommandPass' => __DIR__ . '/..' . '/symfony/console/DependencyInjection/AddConsoleCommandPass.php',
        'Symfony\\Component\\Console\\Descriptor\\ApplicationDescription' => __DIR__ . '/..' . '/symfony/console/Descriptor/ApplicationDescription.php',
        'Symfony\\Component\\Console\\Descriptor\\Descriptor' => __DIR__ . '/..' . '/symfony/console/Descriptor/Descriptor.php',
        'Symfony\\Component\\Console\\Descriptor\\DescriptorInterface' => __DIR__ . '/..' . '/symfony/console/Descriptor/DescriptorInterface.php',
        'Symfony\\Component\\Console\\Descriptor\\JsonDescriptor' => __DIR__ . '/..' . '/symfony/console/Descriptor/JsonDescriptor.php',
        'Symfony\\Component\\Console\\Descriptor\\MarkdownDescriptor' => __DIR__ . '/..' . '/symfony/console/Descriptor/MarkdownDescriptor.php',
        'Symfony\\Component\\Console\\Descriptor\\TextDescriptor' => __DIR__ . '/..' . '/symfony/console/Descriptor/TextDescriptor.php',
        'Symfony\\Component\\Console\\Descriptor\\XmlDescriptor' => __DIR__ . '/..' . '/symfony/console/Descriptor/XmlDescriptor.php',
        'Symfony\\Component\\Console\\EventListener\\ErrorListener' => __DIR__ . '/..' . '/symfony/console/EventListener/ErrorListener.php',
        'Symfony\\Component\\Console\\Event\\ConsoleCommandEvent' => __DIR__ . '/..' . '/symfony/console/Event/ConsoleCommandEvent.php',
        'Symfony\\Component\\Console\\Event\\ConsoleErrorEvent' => __DIR__ . '/..' . '/symfony/console/Event/ConsoleErrorEvent.php',
        'Symfony\\Component\\Console\\Event\\ConsoleEvent' => __DIR__ . '/..' . '/symfony/console/Event/ConsoleEvent.php',
        'Symfony\\Component\\Console\\Event\\ConsoleSignalEvent' => __DIR__ . '/..' . '/symfony/console/Event/ConsoleSignalEvent.php',
        'Symfony\\Component\\Console\\Event\\ConsoleTerminateEvent' => __DIR__ . '/..' . '/symfony/console/Event/ConsoleTerminateEvent.php',
        'Symfony\\Component\\Console\\Exception\\CommandNotFoundException' => __DIR__ . '/..' . '/symfony/console/Exception/CommandNotFoundException.php',
        'Symfony\\Component\\Console\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/symfony/console/Exception/ExceptionInterface.php',
        'Symfony\\Component\\Console\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/symfony/console/Exception/InvalidArgumentException.php',
        'Symfony\\Component\\Console\\Exception\\InvalidOptionException' => __DIR__ . '/..' . '/symfony/console/Exception/InvalidOptionException.php',
        'Symfony\\Component\\Console\\Exception\\LogicException' => __DIR__ . '/..' . '/symfony/console/Exception/LogicException.php',
        'Symfony\\Component\\Console\\Exception\\MissingInputException' => __DIR__ . '/..' . '/symfony/console/Exception/MissingInputException.php',
        'Symfony\\Component\\Console\\Exception\\NamespaceNotFoundException' => __DIR__ . '/..' . '/symfony/console/Exception/NamespaceNotFoundException.php',
        'Symfony\\Component\\Console\\Exception\\RuntimeException' => __DIR__ . '/..' . '/symfony/console/Exception/RuntimeException.php',
        'Symfony\\Component\\Console\\Formatter\\NullOutputFormatter' => __DIR__ . '/..' . '/symfony/console/Formatter/NullOutputFormatter.php',
        'Symfony\\Component\\Console\\Formatter\\NullOutputFormatterStyle' => __DIR__ . '/..' . '/symfony/console/Formatter/NullOutputFormatterStyle.php',
        'Symfony\\Component\\Console\\Formatter\\OutputFormatter' => __DIR__ . '/..' . '/symfony/console/Formatter/OutputFormatter.php',
        'Symfony\\Component\\Console\\Formatter\\OutputFormatterInterface' => __DIR__ . '/..' . '/symfony/console/Formatter/OutputFormatterInterface.php',
        'Symfony\\Component\\Console\\Formatter\\OutputFormatterStyle' => __DIR__ . '/..' . '/symfony/console/Formatter/OutputFormatterStyle.php',
        'Symfony\\Component\\Console\\Formatter\\OutputFormatterStyleInterface' => __DIR__ . '/..' . '/symfony/console/Formatter/OutputFormatterStyleInterface.php',
        'Symfony\\Component\\Console\\Formatter\\OutputFormatterStyleStack' => __DIR__ . '/..' . '/symfony/console/Formatter/OutputFormatterStyleStack.php',
        'Symfony\\Component\\Console\\Formatter\\WrappableOutputFormatterInterface' => __DIR__ . '/..' . '/symfony/console/Formatter/WrappableOutputFormatterInterface.php',
        'Symfony\\Component\\Console\\Helper\\DebugFormatterHelper' => __DIR__ . '/..' . '/symfony/console/Helper/DebugFormatterHelper.php',
        'Symfony\\Component\\Console\\Helper\\DescriptorHelper' => __DIR__ . '/..' . '/symfony/console/Helper/DescriptorHelper.php',
        'Symfony\\Component\\Console\\Helper\\Dumper' => __DIR__ . '/..' . '/symfony/console/Helper/Dumper.php',
        'Symfony\\Component\\Console\\Helper\\FormatterHelper' => __DIR__ . '/..' . '/symfony/console/Helper/FormatterHelper.php',
        'Symfony\\Component\\Console\\Helper\\Helper' => __DIR__ . '/..' . '/symfony/console/Helper/Helper.php',
        'Symfony\\Component\\Console\\Helper\\HelperInterface' => __DIR__ . '/..' . '/symfony/console/Helper/HelperInterface.php',
        'Symfony\\Component\\Console\\Helper\\HelperSet' => __DIR__ . '/..' . '/symfony/console/Helper/HelperSet.php',
        'Symfony\\Component\\Console\\Helper\\InputAwareHelper' => __DIR__ . '/..' . '/symfony/console/Helper/InputAwareHelper.php',
        'Symfony\\Component\\Console\\Helper\\ProcessHelper' => __DIR__ . '/..' . '/symfony/console/Helper/ProcessHelper.php',
        'Symfony\\Component\\Console\\Helper\\ProgressBar' => __DIR__ . '/..' . '/symfony/console/Helper/ProgressBar.php',
        'Symfony\\Component\\Console\\Helper\\ProgressIndicator' => __DIR__ . '/..' . '/symfony/console/Helper/ProgressIndicator.php',
        'Symfony\\Component\\Console\\Helper\\QuestionHelper' => __DIR__ . '/..' . '/symfony/console/Helper/QuestionHelper.php',
        'Symfony\\Component\\Console\\Helper\\SymfonyQuestionHelper' => __DIR__ . '/..' . '/symfony/console/Helper/SymfonyQuestionHelper.php',
        'Symfony\\Component\\Console\\Helper\\Table' => __DIR__ . '/..' . '/symfony/console/Helper/Table.php',
        'Symfony\\Component\\Console\\Helper\\TableCell' => __DIR__ . '/..' . '/symfony/console/Helper/TableCell.php',
        'Symfony\\Component\\Console\\Helper\\TableCellStyle' => __DIR__ . '/..' . '/symfony/console/Helper/TableCellStyle.php',
        'Symfony\\Component\\Console\\Helper\\TableRows' => __DIR__ . '/..' . '/symfony/console/Helper/TableRows.php',
        'Symfony\\Component\\Console\\Helper\\TableSeparator' => __DIR__ . '/..' . '/symfony/console/Helper/TableSeparator.php',
        'Symfony\\Component\\Console\\Helper\\TableStyle' => __DIR__ . '/..' . '/symfony/console/Helper/TableStyle.php',
        'Symfony\\Component\\Console\\Input\\ArgvInput' => __DIR__ . '/..' . '/symfony/console/Input/ArgvInput.php',
        'Symfony\\Component\\Console\\Input\\ArrayInput' => __DIR__ . '/..' . '/symfony/console/Input/ArrayInput.php',
        'Symfony\\Component\\Console\\Input\\Input' => __DIR__ . '/..' . '/symfony/console/Input/Input.php',
        'Symfony\\Component\\Console\\Input\\InputArgument' => __DIR__ . '/..' . '/symfony/console/Input/InputArgument.php',
        'Symfony\\Component\\Console\\Input\\InputAwareInterface' => __DIR__ . '/..' . '/symfony/console/Input/InputAwareInterface.php',
        'Symfony\\Component\\Console\\Input\\InputDefinition' => __DIR__ . '/..' . '/symfony/console/Input/InputDefinition.php',
        'Symfony\\Component\\Console\\Input\\InputInterface' => __DIR__ . '/..' . '/symfony/console/Input/InputInterface.php',
        'Symfony\\Component\\Console\\Input\\InputOption' => __DIR__ . '/..' . '/symfony/console/Input/InputOption.php',
        'Symfony\\Component\\Console\\Input\\StreamableInputInterface' => __DIR__ . '/..' . '/symfony/console/Input/StreamableInputInterface.php',
        'Symfony\\Component\\Console\\Input\\StringInput' => __DIR__ . '/..' . '/symfony/console/Input/StringInput.php',
        'Symfony\\Component\\Console\\Logger\\ConsoleLogger' => __DIR__ . '/..' . '/symfony/console/Logger/ConsoleLogger.php',
        'Symfony\\Component\\Console\\Output\\BufferedOutput' => __DIR__ . '/..' . '/symfony/console/Output/BufferedOutput.php',
        'Symfony\\Component\\Console\\Output\\ConsoleOutput' => __DIR__ . '/..' . '/symfony/console/Output/ConsoleOutput.php',
        'Symfony\\Component\\Console\\Output\\ConsoleOutputInterface' => __DIR__ . '/..' . '/symfony/console/Output/ConsoleOutputInterface.php',
        'Symfony\\Component\\Console\\Output\\ConsoleSectionOutput' => __DIR__ . '/..' . '/symfony/console/Output/ConsoleSectionOutput.php',
        'Symfony\\Component\\Console\\Output\\NullOutput' => __DIR__ . '/..' . '/symfony/console/Output/NullOutput.php',
        'Symfony\\Component\\Console\\Output\\Output' => __DIR__ . '/..' . '/symfony/console/Output/Output.php',
        'Symfony\\Component\\Console\\Output\\OutputInterface' => __DIR__ . '/..' . '/symfony/console/Output/OutputInterface.php',
        'Symfony\\Component\\Console\\Output\\StreamOutput' => __DIR__ . '/..' . '/symfony/console/Output/StreamOutput.php',
        'Symfony\\Component\\Console\\Output\\TrimmedBufferOutput' => __DIR__ . '/..' . '/symfony/console/Output/TrimmedBufferOutput.php',
        'Symfony\\Component\\Console\\Question\\ChoiceQuestion' => __DIR__ . '/..' . '/symfony/console/Question/ChoiceQuestion.php',
        'Symfony\\Component\\Console\\Question\\ConfirmationQuestion' => __DIR__ . '/..' . '/symfony/console/Question/ConfirmationQuestion.php',
        'Symfony\\Component\\Console\\Question\\Question' => __DIR__ . '/..' . '/symfony/console/Question/Question.php',
        'Symfony\\Component\\Console\\SignalRegistry\\SignalRegistry' => __DIR__ . '/..' . '/symfony/console/SignalRegistry/SignalRegistry.php',
        'Symfony\\Component\\Console\\SingleCommandApplication' => __DIR__ . '/..' . '/symfony/console/SingleCommandApplication.php',
        'Symfony\\Component\\Console\\Style\\OutputStyle' => __DIR__ . '/..' . '/symfony/console/Style/OutputStyle.php',
        'Symfony\\Component\\Console\\Style\\StyleInterface' => __DIR__ . '/..' . '/symfony/console/Style/StyleInterface.php',
        'Symfony\\Component\\Console\\Style\\SymfonyStyle' => __DIR__ . '/..' . '/symfony/console/Style/SymfonyStyle.php',
        'Symfony\\Component\\Console\\Terminal' => __DIR__ . '/..' . '/symfony/console/Terminal.php',
        'Symfony\\Component\\Console\\Tester\\ApplicationTester' => __DIR__ . '/..' . '/symfony/console/Tester/ApplicationTester.php',
        'Symfony\\Component\\Console\\Tester\\CommandCompletionTester' => __DIR__ . '/..' . '/symfony/console/Tester/CommandCompletionTester.php',
        'Symfony\\Component\\Console\\Tester\\CommandTester' => __DIR__ . '/..' . '/symfony/console/Tester/CommandTester.php',
        'Symfony\\Component\\Console\\Tester\\Constraint\\CommandIsSuccessful' => __DIR__ . '/..' . '/symfony/console/Tester/Constraint/CommandIsSuccessful.php',
        'Symfony\\Component\\Console\\Tester\\TesterTrait' => __DIR__ . '/..' . '/symfony/console/Tester/TesterTrait.php',
        'Symfony\\Component\\ExpressionLanguage\\Compiler' => __DIR__ . '/..' . '/symfony/expression-language/Compiler.php',
        'Symfony\\Component\\ExpressionLanguage\\Expression' => __DIR__ . '/..' . '/symfony/expression-language/Expression.php',
        'Symfony\\Component\\ExpressionLanguage\\ExpressionFunction' => __DIR__ . '/..' . '/symfony/expression-language/ExpressionFunction.php',
        'Symfony\\Component\\ExpressionLanguage\\ExpressionFunctionProviderInterface' => __DIR__ . '/..' . '/symfony/expression-language/ExpressionFunctionProviderInterface.php',
        'Symfony\\Component\\ExpressionLanguage\\ExpressionLanguage' => __DIR__ . '/..' . '/symfony/expression-language/ExpressionLanguage.php',
        'Symfony\\Component\\ExpressionLanguage\\Lexer' => __DIR__ . '/..' . '/symfony/expression-language/Lexer.php',
        'Symfony\\Component\\ExpressionLanguage\\Node\\ArgumentsNode' => __DIR__ . '/..' . '/symfony/expression-language/Node/ArgumentsNode.php',
        'Symfony\\Component\\ExpressionLanguage\\Node\\ArrayNode' => __DIR__ . '/..' . '/symfony/expression-language/Node/ArrayNode.php',
        'Symfony\\Component\\ExpressionLanguage\\Node\\BinaryNode' => __DIR__ . '/..' . '/symfony/expression-language/Node/BinaryNode.php',
        'Symfony\\Component\\ExpressionLanguage\\Node\\ConditionalNode' => __DIR__ . '/..' . '/symfony/expression-language/Node/ConditionalNode.php',
        'Symfony\\Component\\ExpressionLanguage\\Node\\ConstantNode' => __DIR__ . '/..' . '/symfony/expression-language/Node/ConstantNode.php',
        'Symfony\\Component\\ExpressionLanguage\\Node\\FunctionNode' => __DIR__ . '/..' . '/symfony/expression-language/Node/FunctionNode.php',
        'Symfony\\Component\\ExpressionLanguage\\Node\\GetAttrNode' => __DIR__ . '/..' . '/symfony/expression-language/Node/GetAttrNode.php',
        'Symfony\\Component\\ExpressionLanguage\\Node\\NameNode' => __DIR__ . '/..' . '/symfony/expression-language/Node/NameNode.php',
        'Symfony\\Component\\ExpressionLanguage\\Node\\Node' => __DIR__ . '/..' . '/symfony/expression-language/Node/Node.php',
        'Symfony\\Component\\ExpressionLanguage\\Node\\UnaryNode' => __DIR__ . '/..' . '/symfony/expression-language/Node/UnaryNode.php',
        'Symfony\\Component\\ExpressionLanguage\\ParsedExpression' => __DIR__ . '/..' . '/symfony/expression-language/ParsedExpression.php',
        'Symfony\\Component\\ExpressionLanguage\\Parser' => __DIR__ . '/..' . '/symfony/expression-language/Parser.php',
        'Symfony\\Component\\ExpressionLanguage\\SerializedParsedExpression' => __DIR__ . '/..' . '/symfony/expression-language/SerializedParsedExpression.php',
        'Symfony\\Component\\ExpressionLanguage\\SyntaxError' => __DIR__ . '/..' . '/symfony/expression-language/SyntaxError.php',
        'Symfony\\Component\\ExpressionLanguage\\Token' => __DIR__ . '/..' . '/symfony/expression-language/Token.php',
        'Symfony\\Component\\ExpressionLanguage\\TokenStream' => __DIR__ . '/..' . '/symfony/expression-language/TokenStream.php',
        'Symfony\\Component\\HttpClient\\AmpHttpClient' => __DIR__ . '/..' . '/symfony/http-client/AmpHttpClient.php',
        'Symfony\\Component\\HttpClient\\AsyncDecoratorTrait' => __DIR__ . '/..' . '/symfony/http-client/AsyncDecoratorTrait.php',
        'Symfony\\Component\\HttpClient\\CachingHttpClient' => __DIR__ . '/..' . '/symfony/http-client/CachingHttpClient.php',
        'Symfony\\Component\\HttpClient\\Chunk\\DataChunk' => __DIR__ . '/..' . '/symfony/http-client/Chunk/DataChunk.php',
        'Symfony\\Component\\HttpClient\\Chunk\\ErrorChunk' => __DIR__ . '/..' . '/symfony/http-client/Chunk/ErrorChunk.php',
        'Symfony\\Component\\HttpClient\\Chunk\\FirstChunk' => __DIR__ . '/..' . '/symfony/http-client/Chunk/FirstChunk.php',
        'Symfony\\Component\\HttpClient\\Chunk\\InformationalChunk' => __DIR__ . '/..' . '/symfony/http-client/Chunk/InformationalChunk.php',
        'Symfony\\Component\\HttpClient\\Chunk\\LastChunk' => __DIR__ . '/..' . '/symfony/http-client/Chunk/LastChunk.php',
        'Symfony\\Component\\HttpClient\\Chunk\\ServerSentEvent' => __DIR__ . '/..' . '/symfony/http-client/Chunk/ServerSentEvent.php',
        'Symfony\\Component\\HttpClient\\CurlHttpClient' => __DIR__ . '/..' . '/symfony/http-client/CurlHttpClient.php',
        'Symfony\\Component\\HttpClient\\DataCollector\\HttpClientDataCollector' => __DIR__ . '/..' . '/symfony/http-client/DataCollector/HttpClientDataCollector.php',
        'Symfony\\Component\\HttpClient\\DecoratorTrait' => __DIR__ . '/..' . '/symfony/http-client/DecoratorTrait.php',
        'Symfony\\Component\\HttpClient\\DependencyInjection\\HttpClientPass' => __DIR__ . '/..' . '/symfony/http-client/DependencyInjection/HttpClientPass.php',
        'Symfony\\Component\\HttpClient\\EventSourceHttpClient' => __DIR__ . '/..' . '/symfony/http-client/EventSourceHttpClient.php',
        'Symfony\\Component\\HttpClient\\Exception\\ClientException' => __DIR__ . '/..' . '/symfony/http-client/Exception/ClientException.php',
        'Symfony\\Component\\HttpClient\\Exception\\EventSourceException' => __DIR__ . '/..' . '/symfony/http-client/Exception/EventSourceException.php',
        'Symfony\\Component\\HttpClient\\Exception\\HttpExceptionTrait' => __DIR__ . '/..' . '/symfony/http-client/Exception/HttpExceptionTrait.php',
        'Symfony\\Component\\HttpClient\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/symfony/http-client/Exception/InvalidArgumentException.php',
        'Symfony\\Component\\HttpClient\\Exception\\JsonException' => __DIR__ . '/..' . '/symfony/http-client/Exception/JsonException.php',
        'Symfony\\Component\\HttpClient\\Exception\\RedirectionException' => __DIR__ . '/..' . '/symfony/http-client/Exception/RedirectionException.php',
        'Symfony\\Component\\HttpClient\\Exception\\ServerException' => __DIR__ . '/..' . '/symfony/http-client/Exception/ServerException.php',
        'Symfony\\Component\\HttpClient\\Exception\\TimeoutException' => __DIR__ . '/..' . '/symfony/http-client/Exception/TimeoutException.php',
        'Symfony\\Component\\HttpClient\\Exception\\TransportException' => __DIR__ . '/..' . '/symfony/http-client/Exception/TransportException.php',
        'Symfony\\Component\\HttpClient\\HttpClient' => __DIR__ . '/..' . '/symfony/http-client/HttpClient.php',
        'Symfony\\Component\\HttpClient\\HttpClientTrait' => __DIR__ . '/..' . '/symfony/http-client/HttpClientTrait.php',
        'Symfony\\Component\\HttpClient\\HttpOptions' => __DIR__ . '/..' . '/symfony/http-client/HttpOptions.php',
        'Symfony\\Component\\HttpClient\\HttplugClient' => __DIR__ . '/..' . '/symfony/http-client/HttplugClient.php',
        'Symfony\\Component\\HttpClient\\Internal\\AmpBody' => __DIR__ . '/..' . '/symfony/http-client/Internal/AmpBody.php',
        'Symfony\\Component\\HttpClient\\Internal\\AmpClientState' => __DIR__ . '/..' . '/symfony/http-client/Internal/AmpClientState.php',
        'Symfony\\Component\\HttpClient\\Internal\\AmpListener' => __DIR__ . '/..' . '/symfony/http-client/Internal/AmpListener.php',
        'Symfony\\Component\\HttpClient\\Internal\\AmpResolver' => __DIR__ . '/..' . '/symfony/http-client/Internal/AmpResolver.php',
        'Symfony\\Component\\HttpClient\\Internal\\Canary' => __DIR__ . '/..' . '/symfony/http-client/Internal/Canary.php',
        'Symfony\\Component\\HttpClient\\Internal\\ClientState' => __DIR__ . '/..' . '/symfony/http-client/Internal/ClientState.php',
        'Symfony\\Component\\HttpClient\\Internal\\CurlClientState' => __DIR__ . '/..' . '/symfony/http-client/Internal/CurlClientState.php',
        'Symfony\\Component\\HttpClient\\Internal\\DnsCache' => __DIR__ . '/..' . '/symfony/http-client/Internal/DnsCache.php',
        'Symfony\\Component\\HttpClient\\Internal\\HttplugWaitLoop' => __DIR__ . '/..' . '/symfony/http-client/Internal/HttplugWaitLoop.php',
        'Symfony\\Component\\HttpClient\\Internal\\NativeClientState' => __DIR__ . '/..' . '/symfony/http-client/Internal/NativeClientState.php',
        'Symfony\\Component\\HttpClient\\Internal\\PushedResponse' => __DIR__ . '/..' . '/symfony/http-client/Internal/PushedResponse.php',
        'Symfony\\Component\\HttpClient\\MockHttpClient' => __DIR__ . '/..' . '/symfony/http-client/MockHttpClient.php',
        'Symfony\\Component\\HttpClient\\NativeHttpClient' => __DIR__ . '/..' . '/symfony/http-client/NativeHttpClient.php',
        'Symfony\\Component\\HttpClient\\NoPrivateNetworkHttpClient' => __DIR__ . '/..' . '/symfony/http-client/NoPrivateNetworkHttpClient.php',
        'Symfony\\Component\\HttpClient\\Psr18Client' => __DIR__ . '/..' . '/symfony/http-client/Psr18Client.php',
        'Symfony\\Component\\HttpClient\\Response\\AmpResponse' => __DIR__ . '/..' . '/symfony/http-client/Response/AmpResponse.php',
        'Symfony\\Component\\HttpClient\\Response\\AsyncContext' => __DIR__ . '/..' . '/symfony/http-client/Response/AsyncContext.php',
        'Symfony\\Component\\HttpClient\\Response\\AsyncResponse' => __DIR__ . '/..' . '/symfony/http-client/Response/AsyncResponse.php',
        'Symfony\\Component\\HttpClient\\Response\\CommonResponseTrait' => __DIR__ . '/..' . '/symfony/http-client/Response/CommonResponseTrait.php',
        'Symfony\\Component\\HttpClient\\Response\\CurlResponse' => __DIR__ . '/..' . '/symfony/http-client/Response/CurlResponse.php',
        'Symfony\\Component\\HttpClient\\Response\\HttplugPromise' => __DIR__ . '/..' . '/symfony/http-client/Response/HttplugPromise.php',
        'Symfony\\Component\\HttpClient\\Response\\MockResponse' => __DIR__ . '/..' . '/symfony/http-client/Response/MockResponse.php',
        'Symfony\\Component\\HttpClient\\Response\\NativeResponse' => __DIR__ . '/..' . '/symfony/http-client/Response/NativeResponse.php',
        'Symfony\\Component\\HttpClient\\Response\\ResponseStream' => __DIR__ . '/..' . '/symfony/http-client/Response/ResponseStream.php',
        'Symfony\\Component\\HttpClient\\Response\\StreamWrapper' => __DIR__ . '/..' . '/symfony/http-client/Response/StreamWrapper.php',
        'Symfony\\Component\\HttpClient\\Response\\StreamableInterface' => __DIR__ . '/..' . '/symfony/http-client/Response/StreamableInterface.php',
        'Symfony\\Component\\HttpClient\\Response\\TraceableResponse' => __DIR__ . '/..' . '/symfony/http-client/Response/TraceableResponse.php',
        'Symfony\\Component\\HttpClient\\Response\\TransportResponseTrait' => __DIR__ . '/..' . '/symfony/http-client/Response/TransportResponseTrait.php',
        'Symfony\\Component\\HttpClient\\Retry\\GenericRetryStrategy' => __DIR__ . '/..' . '/symfony/http-client/Retry/GenericRetryStrategy.php',
        'Symfony\\Component\\HttpClient\\Retry\\RetryStrategyInterface' => __DIR__ . '/..' . '/symfony/http-client/Retry/RetryStrategyInterface.php',
        'Symfony\\Component\\HttpClient\\RetryableHttpClient' => __DIR__ . '/..' . '/symfony/http-client/RetryableHttpClient.php',
        'Symfony\\Component\\HttpClient\\ScopingHttpClient' => __DIR__ . '/..' . '/symfony/http-client/ScopingHttpClient.php',
        'Symfony\\Component\\HttpClient\\TraceableHttpClient' => __DIR__ . '/..' . '/symfony/http-client/TraceableHttpClient.php',
        'Symfony\\Component\\String\\AbstractString' => __DIR__ . '/..' . '/symfony/string/AbstractString.php',
        'Symfony\\Component\\String\\AbstractUnicodeString' => __DIR__ . '/..' . '/symfony/string/AbstractUnicodeString.php',
        'Symfony\\Component\\String\\ByteString' => __DIR__ . '/..' . '/symfony/string/ByteString.php',
        'Symfony\\Component\\String\\CodePointString' => __DIR__ . '/..' . '/symfony/string/CodePointString.php',
        'Symfony\\Component\\String\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/symfony/string/Exception/ExceptionInterface.php',
        'Symfony\\Component\\String\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/symfony/string/Exception/InvalidArgumentException.php',
        'Symfony\\Component\\String\\Exception\\RuntimeException' => __DIR__ . '/..' . '/symfony/string/Exception/RuntimeException.php',
        'Symfony\\Component\\String\\Inflector\\EnglishInflector' => __DIR__ . '/..' . '/symfony/string/Inflector/EnglishInflector.php',
        'Symfony\\Component\\String\\Inflector\\FrenchInflector' => __DIR__ . '/..' . '/symfony/string/Inflector/FrenchInflector.php',
        'Symfony\\Component\\String\\Inflector\\InflectorInterface' => __DIR__ . '/..' . '/symfony/string/Inflector/InflectorInterface.php',
        'Symfony\\Component\\String\\LazyString' => __DIR__ . '/..' . '/symfony/string/LazyString.php',
        'Symfony\\Component\\String\\Slugger\\AsciiSlugger' => __DIR__ . '/..' . '/symfony/string/Slugger/AsciiSlugger.php',
        'Symfony\\Component\\String\\Slugger\\SluggerInterface' => __DIR__ . '/..' . '/symfony/string/Slugger/SluggerInterface.php',
        'Symfony\\Component\\String\\UnicodeString' => __DIR__ . '/..' . '/symfony/string/UnicodeString.php',
        'Symfony\\Component\\VarExporter\\Exception\\ClassNotFoundException' => __DIR__ . '/..' . '/symfony/var-exporter/Exception/ClassNotFoundException.php',
        'Symfony\\Component\\VarExporter\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/symfony/var-exporter/Exception/ExceptionInterface.php',
        'Symfony\\Component\\VarExporter\\Exception\\NotInstantiableTypeException' => __DIR__ . '/..' . '/symfony/var-exporter/Exception/NotInstantiableTypeException.php',
        'Symfony\\Component\\VarExporter\\Instantiator' => __DIR__ . '/..' . '/symfony/var-exporter/Instantiator.php',
        'Symfony\\Component\\VarExporter\\Internal\\Exporter' => __DIR__ . '/..' . '/symfony/var-exporter/Internal/Exporter.php',
        'Symfony\\Component\\VarExporter\\Internal\\Hydrator' => __DIR__ . '/..' . '/symfony/var-exporter/Internal/Hydrator.php',
        'Symfony\\Component\\VarExporter\\Internal\\Reference' => __DIR__ . '/..' . '/symfony/var-exporter/Internal/Reference.php',
        'Symfony\\Component\\VarExporter\\Internal\\Registry' => __DIR__ . '/..' . '/symfony/var-exporter/Internal/Registry.php',
        'Symfony\\Component\\VarExporter\\Internal\\Values' => __DIR__ . '/..' . '/symfony/var-exporter/Internal/Values.php',
        'Symfony\\Component\\VarExporter\\VarExporter' => __DIR__ . '/..' . '/symfony/var-exporter/VarExporter.php',
        'Symfony\\Contracts\\Cache\\CacheInterface' => __DIR__ . '/..' . '/symfony/cache-contracts/CacheInterface.php',
        'Symfony\\Contracts\\Cache\\CacheTrait' => __DIR__ . '/..' . '/symfony/cache-contracts/CacheTrait.php',
        'Symfony\\Contracts\\Cache\\CallbackInterface' => __DIR__ . '/..' . '/symfony/cache-contracts/CallbackInterface.php',
        'Symfony\\Contracts\\Cache\\ItemInterface' => __DIR__ . '/..' . '/symfony/cache-contracts/ItemInterface.php',
        'Symfony\\Contracts\\Cache\\TagAwareCacheInterface' => __DIR__ . '/..' . '/symfony/cache-contracts/TagAwareCacheInterface.php',
        'Symfony\\Contracts\\HttpClient\\ChunkInterface' => __DIR__ . '/..' . '/symfony/http-client-contracts/ChunkInterface.php',
        'Symfony\\Contracts\\HttpClient\\Exception\\ClientExceptionInterface' => __DIR__ . '/..' . '/symfony/http-client-contracts/Exception/ClientExceptionInterface.php',
        'Symfony\\Contracts\\HttpClient\\Exception\\DecodingExceptionInterface' => __DIR__ . '/..' . '/symfony/http-client-contracts/Exception/DecodingExceptionInterface.php',
        'Symfony\\Contracts\\HttpClient\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/symfony/http-client-contracts/Exception/ExceptionInterface.php',
        'Symfony\\Contracts\\HttpClient\\Exception\\HttpExceptionInterface' => __DIR__ . '/..' . '/symfony/http-client-contracts/Exception/HttpExceptionInterface.php',
        'Symfony\\Contracts\\HttpClient\\Exception\\RedirectionExceptionInterface' => __DIR__ . '/..' . '/symfony/http-client-contracts/Exception/RedirectionExceptionInterface.php',
        'Symfony\\Contracts\\HttpClient\\Exception\\ServerExceptionInterface' => __DIR__ . '/..' . '/symfony/http-client-contracts/Exception/ServerExceptionInterface.php',
        'Symfony\\Contracts\\HttpClient\\Exception\\TimeoutExceptionInterface' => __DIR__ . '/..' . '/symfony/http-client-contracts/Exception/TimeoutExceptionInterface.php',
        'Symfony\\Contracts\\HttpClient\\Exception\\TransportExceptionInterface' => __DIR__ . '/..' . '/symfony/http-client-contracts/Exception/TransportExceptionInterface.php',
        'Symfony\\Contracts\\HttpClient\\HttpClientInterface' => __DIR__ . '/..' . '/symfony/http-client-contracts/HttpClientInterface.php',
        'Symfony\\Contracts\\HttpClient\\ResponseInterface' => __DIR__ . '/..' . '/symfony/http-client-contracts/ResponseInterface.php',
        'Symfony\\Contracts\\HttpClient\\ResponseStreamInterface' => __DIR__ . '/..' . '/symfony/http-client-contracts/ResponseStreamInterface.php',
        'Symfony\\Contracts\\HttpClient\\Test\\HttpClientTestCase' => __DIR__ . '/..' . '/symfony/http-client-contracts/Test/HttpClientTestCase.php',
        'Symfony\\Contracts\\HttpClient\\Test\\TestHttpServer' => __DIR__ . '/..' . '/symfony/http-client-contracts/Test/TestHttpServer.php',
        'Symfony\\Contracts\\Service\\Attribute\\Required' => __DIR__ . '/..' . '/symfony/service-contracts/Attribute/Required.php',
        'Symfony\\Contracts\\Service\\Attribute\\SubscribedService' => __DIR__ . '/..' . '/symfony/service-contracts/Attribute/SubscribedService.php',
        'Symfony\\Contracts\\Service\\ResetInterface' => __DIR__ . '/..' . '/symfony/service-contracts/ResetInterface.php',
        'Symfony\\Contracts\\Service\\ServiceLocatorTrait' => __DIR__ . '/..' . '/symfony/service-contracts/ServiceLocatorTrait.php',
        'Symfony\\Contracts\\Service\\ServiceProviderInterface' => __DIR__ . '/..' . '/symfony/service-contracts/ServiceProviderInterface.php',
        'Symfony\\Contracts\\Service\\ServiceSubscriberInterface' => __DIR__ . '/..' . '/symfony/service-contracts/ServiceSubscriberInterface.php',
        'Symfony\\Contracts\\Service\\ServiceSubscriberTrait' => __DIR__ . '/..' . '/symfony/service-contracts/ServiceSubscriberTrait.php',
        'Symfony\\Contracts\\Service\\Test\\ServiceLocatorTest' => __DIR__ . '/..' . '/symfony/service-contracts/Test/ServiceLocatorTest.php',
        'Symfony\\Contracts\\Service\\Test\\ServiceLocatorTestCase' => __DIR__ . '/..' . '/symfony/service-contracts/Test/ServiceLocatorTestCase.php',
        'Symfony\\Polyfill\\Ctype\\Ctype' => __DIR__ . '/..' . '/symfony/polyfill-ctype/Ctype.php',
        'Symfony\\Polyfill\\Intl\\Grapheme\\Grapheme' => __DIR__ . '/..' . '/symfony/polyfill-intl-grapheme/Grapheme.php',
        'Symfony\\Polyfill\\Intl\\Normalizer\\Normalizer' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/Normalizer.php',
        'Symfony\\Polyfill\\Mbstring\\Mbstring' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/Mbstring.php',
        'Symfony\\Polyfill\\Php73\\Php73' => __DIR__ . '/..' . '/symfony/polyfill-php73/Php73.php',
        'Symfony\\Polyfill\\Php80\\Php80' => __DIR__ . '/..' . '/symfony/polyfill-php80/Php80.php',
        'Symfony\\Polyfill\\Php80\\PhpToken' => __DIR__ . '/..' . '/symfony/polyfill-php80/PhpToken.php',
        'UnhandledMatchError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php',
        'ValueError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/ValueError.php',
    );

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

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

// autoload_real.php @generated by Composer

class ComposerAutoloaderInitb258b286557a5c443ca34ff495d81cd6
{
    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('ComposerAutoloaderInitb258b286557a5c443ca34ff495d81cd6', 'loadClassLoader'), true, true);
        self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
        spl_autoload_unregister(array('ComposerAutoloaderInitb258b286557a5c443ca34ff495d81cd6', 'loadClassLoader'));

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

        $loader->setClassMapAuthoritative(true);
        $loader->register(true);

        $filesToLoad = \Composer\Autoload\ComposerStaticInitb258b286557a5c443ca34ff495d81cd6::$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_namespaces.php @generated by Composer

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

return array(
);
<?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(
                    "Expected deprecation with identifier '%s' was triggered by code executed in test, but expected not to.",
                    $identifier
                )
            );
        }
    }
}
<?php

declare(strict_types=1);

namespace Doctrine\Common\Lexer;

use ReflectionClass;
use UnitEnum;

use function get_class;
use function implode;
use function preg_split;
use function sprintf;
use function substr;

use const PREG_SPLIT_DELIM_CAPTURE;
use const PREG_SPLIT_NO_EMPTY;
use const PREG_SPLIT_OFFSET_CAPTURE;

/**
 * Base class for writing simple lexers, i.e. for creating small DSLs.
 *
 * @template T of UnitEnum|string|int
 * @template V of string|int
 */
abstract class AbstractLexer
{
    /**
     * Lexer original input string.
     *
     * @var string
     */
    private $input;

    /**
     * Array of scanned tokens.
     *
     * @var list<Token<T, V>>
     */
    private $tokens = [];

    /**
     * Current lexer position in input string.
     *
     * @var int
     */
    private $position = 0;

    /**
     * Current peek of current lexer position.
     *
     * @var int
     */
    private $peek = 0;

    /**
     * The next token in the input.
     *
     * @var Token<T, V>|null
     */
    public $lookahead;

    /**
     * The last matched/seen token.
     *
     * @var Token<T, V>|null
     */
    public $token;

    /**
     * Composed regex for input parsing.
     *
     * @var non-empty-string|null
     */
    private $regex;

    /**
     * Sets the input data to be tokenized.
     *
     * The Lexer is immediately reset and the new input tokenized.
     * Any unprocessed tokens from any previous input are lost.
     *
     * @param string $input The input to be tokenized.
     *
     * @return void
     */
    public function setInput($input)
    {
        $this->input  = $input;
        $this->tokens = [];

        $this->reset();
        $this->scan($input);
    }

    /**
     * Resets the lexer.
     *
     * @return void
     */
    public function reset()
    {
        $this->lookahead = null;
        $this->token     = null;
        $this->peek      = 0;
        $this->position  = 0;
    }

    /**
     * Resets the peek pointer to 0.
     *
     * @return void
     */
    public function resetPeek()
    {
        $this->peek = 0;
    }

    /**
     * Resets the lexer position on the input to the given position.
     *
     * @param int $position Position to place the lexical scanner.
     *
     * @return void
     */
    public function resetPosition($position = 0)
    {
        $this->position = $position;
    }

    /**
     * Retrieve the original lexer's input until a given position.
     *
     * @param int $position
     *
     * @return string
     */
    public function getInputUntilPosition($position)
    {
        return substr($this->input, 0, $position);
    }

    /**
     * Checks whether a given token matches the current lookahead.
     *
     * @param T $type
     *
     * @return bool
     *
     * @psalm-assert-if-true !=null $this->lookahead
     */
    public function isNextToken($type)
    {
        return $this->lookahead !== null && $this->lookahead->isA($type);
    }

    /**
     * Checks whether any of the given tokens matches the current lookahead.
     *
     * @param list<T> $types
     *
     * @return bool
     *
     * @psalm-assert-if-true !=null $this->lookahead
     */
    public function isNextTokenAny(array $types)
    {
        return $this->lookahead !== null && $this->lookahead->isA(...$types);
    }

    /**
     * Moves to the next token in the input string.
     *
     * @return bool
     *
     * @psalm-assert-if-true !null $this->lookahead
     */
    public function moveNext()
    {
        $this->peek      = 0;
        $this->token     = $this->lookahead;
        $this->lookahead = isset($this->tokens[$this->position])
            ? $this->tokens[$this->position++] : null;

        return $this->lookahead !== null;
    }

    /**
     * Tells the lexer to skip input tokens until it sees a token with the given value.
     *
     * @param T $type The token type to skip until.
     *
     * @return void
     */
    public function skipUntil($type)
    {
        while ($this->lookahead !== null && ! $this->lookahead->isA($type)) {
            $this->moveNext();
        }
    }

    /**
     * Checks if given value is identical to the given token.
     *
     * @param string     $value
     * @param int|string $token
     *
     * @return bool
     */
    public function isA($value, $token)
    {
        return $this->getType($value) === $token;
    }

    /**
     * Moves the lookahead token forward.
     *
     * @return Token<T, V>|null The next token or NULL if there are no more tokens ahead.
     */
    public function peek()
    {
        if (isset($this->tokens[$this->position + $this->peek])) {
            return $this->tokens[$this->position + $this->peek++];
        }

        return null;
    }

    /**
     * Peeks at the next token, returns it and immediately resets the peek.
     *
     * @return Token<T, V>|null The next token or NULL if there are no more tokens ahead.
     */
    public function glimpse()
    {
        $peek       = $this->peek();
        $this->peek = 0;

        return $peek;
    }

    /**
     * Scans the input string for tokens.
     *
     * @param string $input A query string.
     *
     * @return void
     */
    protected function scan($input)
    {
        if (! isset($this->regex)) {
            $this->regex = sprintf(
                '/(%s)|%s/%s',
                implode(')|(', $this->getCatchablePatterns()),
                implode('|', $this->getNonCatchablePatterns()),
                $this->getModifiers()
            );
        }

        $flags   = PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_OFFSET_CAPTURE;
        $matches = preg_split($this->regex, $input, -1, $flags);

        if ($matches === false) {
            // Work around https://bugs.php.net/78122
            $matches = [[$input, 0]];
        }

        foreach ($matches as $match) {
            // Must remain before 'value' assignment since it can change content
            $firstMatch = $match[0];
            $type       = $this->getType($firstMatch);

            $this->tokens[] = new Token(
                $firstMatch,
                $type,
                $match[1]
            );
        }
    }

    /**
     * Gets the literal for a given token.
     *
     * @param T $token
     *
     * @return int|string
     */
    public function getLiteral($token)
    {
        if ($token instanceof UnitEnum) {
            return get_class($token) . '::' . $token->name;
        }

        $className = static::class;

        $reflClass = new ReflectionClass($className);
        $constants = $reflClass->getConstants();

        foreach ($constants as $name => $value) {
            if ($value === $token) {
                return $className . '::' . $name;
            }
        }

        return $token;
    }

    /**
     * Regex modifiers
     *
     * @return string
     */
    protected function getModifiers()
    {
        return 'iu';
    }

    /**
     * Lexical catchable patterns.
     *
     * @return string[]
     */
    abstract protected function getCatchablePatterns();

    /**
     * Lexical non-catchable patterns.
     *
     * @return string[]
     */
    abstract protected function getNonCatchablePatterns();

    /**
     * Retrieve token type. Also processes the token value if necessary.
     *
     * @param string $value
     *
     * @return T|null
     *
     * @param-out V $value
     */
    abstract protected function getType(&$value);
}
<?php

declare(strict_types=1);

namespace Doctrine\Common\Lexer;

use ArrayAccess;
use Doctrine\Deprecations\Deprecation;
use ReturnTypeWillChange;
use UnitEnum;

use function in_array;

/**
 * @template T of UnitEnum|string|int
 * @template V of string|int
 * @implements ArrayAccess<string,mixed>
 */
final class Token implements ArrayAccess
{
    /**
     * The string value of the token in the input string
     *
     * @readonly
     * @var V
     */
    public $value;

    /**
     * The type of the token (identifier, numeric, string, input parameter, none)
     *
     * @readonly
     * @var T|null
     */
    public $type;

    /**
     * The position of the token in the input string
     *
     * @readonly
     * @var int
     */
    public $position;

    /**
     * @param V      $value
     * @param T|null $type
     */
    public function __construct($value, $type, int $position)
    {
        $this->value    = $value;
        $this->type     = $type;
        $this->position = $position;
    }

    /** @param T ...$types */
    public function isA(...$types): bool
    {
        return in_array($this->type, $types, true);
    }

    /**
     * @deprecated Use the value, type or position property instead
     * {@inheritDoc}
     */
    public function offsetExists($offset): bool
    {
        Deprecation::trigger(
            'doctrine/lexer',
            'https://github.com/doctrine/lexer/pull/79',
            'Accessing %s properties via ArrayAccess is deprecated, use the value, type or position property instead',
            self::class
        );

        return in_array($offset, ['value', 'type', 'position'], true);
    }

    /**
     * @deprecated Use the value, type or position property instead
     * {@inheritDoc}
     *
     * @param O $offset
     *
     * @return mixed
     * @psalm-return (
     *     O is 'value'
     *     ? V
     *     : (
     *         O is 'type'
     *         ? T|null
     *         : (
     *             O is 'position'
     *             ? int
     *             : mixed
     *         )
     *     )
     * )
     *
     * @template O of array-key
     */
    #[ReturnTypeWillChange]
    public function offsetGet($offset)
    {
        Deprecation::trigger(
            'doctrine/lexer',
            'https://github.com/doctrine/lexer/pull/79',
            'Accessing %s properties via ArrayAccess is deprecated, use the value, type or position property instead',
            self::class
        );

        return $this->$offset;
    }

    /**
     * @deprecated no replacement planned
     * {@inheritDoc}
     */
    public function offsetSet($offset, $value): void
    {
        Deprecation::trigger(
            'doctrine/lexer',
            'https://github.com/doctrine/lexer/pull/79',
            'Setting %s properties via ArrayAccess is deprecated',
            self::class
        );

        $this->$offset = $value;
    }

    /**
     * @deprecated no replacement planned
     * {@inheritDoc}
     */
    public function offsetUnset($offset): void
    {
        Deprecation::trigger(
            'doctrine/lexer',
            'https://github.com/doctrine/lexer/pull/79',
            'Setting %s properties via ArrayAccess is deprecated',
            self::class
        );

        $this->$offset = null;
    }
}
Introduction
============

This library provides a way of avoiding usage of constructors when instantiating PHP classes.

Installation
============

The suggested installation method is via `composer`_:

.. code-block:: console

   $ composer require doctrine/instantiator

Usage
=====

The instantiator is able to create new instances of any class without
using the constructor or any API of the class itself:

.. code-block:: php

    <?php

    use Doctrine\Instantiator\Instantiator;
    use App\Entities\User;

    $instantiator = new Instantiator();

    $user = $instantiator->instantiate(User::class);

Contributing
============

-  Follow the `Doctrine Coding Standard`_
-  The project will follow strict `object calisthenics`_
-  Any contribution must provide tests for additional introduced
   conditions
-  Any un-confirmed issue needs a failing test case before being
   accepted
-  Pull requests must be sent from a new hotfix/feature branch, not from
   ``master``.

Testing
=======

The PHPUnit version to be used is the one installed as a dev- dependency
via composer:

.. code-block:: console

   $ ./vendor/bin/phpunit

Accepted coverage for new contributions is 80%. Any contribution not
satisfying this requirement won’t be merged.

Credits
=======

This library was migrated from `ocramius/instantiator`_, which has been
donated to the doctrine organization, and which is now deprecated in
favour of this package.

.. _composer: https://getcomposer.org/
.. _CONTRIBUTING.md: CONTRIBUTING.md
.. _ocramius/instantiator: https://github.com/Ocramius/Instantiator
.. _Doctrine Coding Standard: https://github.com/doctrine/coding-standard
.. _object calisthenics: http://www.slideshare.net/guilhermeblanco/object-calisthenics-applied-to-php
.. toctree::
    :depth: 3

    index
<?xml version="1.0"?>
<psalm
    errorLevel="7"
    phpVersion="8.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>
<?php

namespace Doctrine\Instantiator;

use Doctrine\Instantiator\Exception\ExceptionInterface;

/**
 * Instantiator provides utility methods to build objects without invoking their constructors
 */
interface InstantiatorInterface
{
    /**
     * @param string $className
     * @phpstan-param class-string<T> $className
     *
     * @return object
     * @phpstan-return T
     *
     * @throws ExceptionInterface
     *
     * @template T of object
     */
    public function instantiate($className);
}
<?php

namespace Doctrine\Instantiator\Exception;

use InvalidArgumentException as BaseInvalidArgumentException;
use ReflectionClass;

use function interface_exists;
use function sprintf;
use function trait_exists;

/**
 * Exception for invalid arguments provided to the instantiator
 */
class InvalidArgumentException extends BaseInvalidArgumentException implements ExceptionInterface
{
    public static function fromNonExistingClass(string $className): self
    {
        if (interface_exists($className)) {
            return new self(sprintf('The provided type "%s" is an interface, and cannot be instantiated', $className));
        }

        if (trait_exists($className)) {
            return new self(sprintf('The provided type "%s" is a trait, and cannot be instantiated', $className));
        }

        return new self(sprintf('The provided class "%s" does not exist', $className));
    }

    /**
     * @phpstan-param ReflectionClass<T> $reflectionClass
     *
     * @template T of object
     */
    public static function fromAbstractClass(ReflectionClass $reflectionClass): self
    {
        return new self(sprintf(
            'The provided class "%s" is abstract, and cannot be instantiated',
            $reflectionClass->getName()
        ));
    }

    public static function fromEnum(string $className): self
    {
        return new self(sprintf(
            'The provided class "%s" is an enum, and cannot be instantiated',
            $className
        ));
    }
}
<?php

namespace Doctrine\Instantiator\Exception;

use Exception;
use ReflectionClass;
use UnexpectedValueException as BaseUnexpectedValueException;

use function sprintf;

/**
 * Exception for given parameters causing invalid/unexpected state on instantiation
 */
class UnexpectedValueException extends BaseUnexpectedValueException implements ExceptionInterface
{
    /**
     * @phpstan-param ReflectionClass<T> $reflectionClass
     *
     * @template T of object
     */
    public static function fromSerializationTriggeredException(
        ReflectionClass $reflectionClass,
        Exception $exception
    ): self {
        return new self(
            sprintf(
                'An exception was raised while trying to instantiate an instance of "%s" via un-serialization',
                $reflectionClass->getName()
            ),
            0,
            $exception
        );
    }

    /**
     * @phpstan-param ReflectionClass<T> $reflectionClass
     *
     * @template T of object
     */
    public static function fromUncleanUnSerialization(
        ReflectionClass $reflectionClass,
        string $errorString,
        int $errorCode,
        string $errorFile,
        int $errorLine
    ): self {
        return new self(
            sprintf(
                'Could not produce an instance of "%s" via un-serialization, since an error was triggered '
                . 'in file "%s" at line "%d"',
                $reflectionClass->getName(),
                $errorFile,
                $errorLine
            ),
            0,
            new Exception($errorString, $errorCode)
        );
    }
}
<?php

namespace Doctrine\Instantiator\Exception;

use Throwable;

/**
 * Base exception marker interface for the instantiator component
 */
interface ExceptionInterface extends Throwable
{
}
<?php

namespace Doctrine\Instantiator;

use ArrayIterator;
use Doctrine\Instantiator\Exception\ExceptionInterface;
use Doctrine\Instantiator\Exception\InvalidArgumentException;
use Doctrine\Instantiator\Exception\UnexpectedValueException;
use Exception;
use ReflectionClass;
use ReflectionException;
use Serializable;

use function class_exists;
use function enum_exists;
use function is_subclass_of;
use function restore_error_handler;
use function set_error_handler;
use function sprintf;
use function strlen;
use function unserialize;

use const PHP_VERSION_ID;

final class Instantiator implements InstantiatorInterface
{
    /**
     * Markers used internally by PHP to define whether {@see \unserialize} should invoke
     * the method {@see \Serializable::unserialize()} when dealing with classes implementing
     * the {@see \Serializable} interface.
     *
     * @deprecated This constant will be private in 2.0
     */
    public const SERIALIZATION_FORMAT_USE_UNSERIALIZER = 'C';

    /** @deprecated This constant will be private in 2.0 */
    public const SERIALIZATION_FORMAT_AVOID_UNSERIALIZER = 'O';

    /**
     * Used to instantiate specific classes, indexed by class name.
     *
     * @var callable[]
     */
    private static $cachedInstantiators = [];

    /**
     * Array of objects that can directly be cloned, indexed by class name.
     *
     * @var object[]
     */
    private static $cachedCloneables = [];

    /**
     * @param string $className
     * @phpstan-param class-string<T> $className
     *
     * @return object
     * @phpstan-return T
     *
     * @throws ExceptionInterface
     *
     * @template T of object
     */
    public function instantiate($className)
    {
        if (isset(self::$cachedCloneables[$className])) {
            /** @phpstan-var T */
            $cachedCloneable = self::$cachedCloneables[$className];

            return clone $cachedCloneable;
        }

        if (isset(self::$cachedInstantiators[$className])) {
            $factory = self::$cachedInstantiators[$className];

            return $factory();
        }

        return $this->buildAndCacheFromFactory($className);
    }

    /**
     * Builds the requested object and caches it in static properties for performance
     *
     * @phpstan-param class-string<T> $className
     *
     * @return object
     * @phpstan-return T
     *
     * @template T of object
     */
    private function buildAndCacheFromFactory(string $className)
    {
        $factory  = self::$cachedInstantiators[$className] = $this->buildFactory($className);
        $instance = $factory();

        if ($this->isSafeToClone(new ReflectionClass($instance))) {
            self::$cachedCloneables[$className] = clone $instance;
        }

        return $instance;
    }

    /**
     * Builds a callable capable of instantiating the given $className without
     * invoking its constructor.
     *
     * @phpstan-param class-string<T> $className
     *
     * @phpstan-return callable(): T
     *
     * @throws InvalidArgumentException
     * @throws UnexpectedValueException
     * @throws ReflectionException
     *
     * @template T of object
     */
    private function buildFactory(string $className): callable
    {
        $reflectionClass = $this->getReflectionClass($className);

        if ($this->isInstantiableViaReflection($reflectionClass)) {
            return [$reflectionClass, 'newInstanceWithoutConstructor'];
        }

        $serializedString = sprintf(
            '%s:%d:"%s":0:{}',
            is_subclass_of($className, Serializable::class) ? self::SERIALIZATION_FORMAT_USE_UNSERIALIZER : self::SERIALIZATION_FORMAT_AVOID_UNSERIALIZER,
            strlen($className),
            $className
        );

        $this->checkIfUnSerializationIsSupported($reflectionClass, $serializedString);

        return static function () use ($serializedString) {
            return unserialize($serializedString);
        };
    }

    /**
     * @phpstan-param class-string<T> $className
     *
     * @phpstan-return ReflectionClass<T>
     *
     * @throws InvalidArgumentException
     * @throws ReflectionException
     *
     * @template T of object
     */
    private function getReflectionClass(string $className): ReflectionClass
    {
        if (! class_exists($className)) {
            throw InvalidArgumentException::fromNonExistingClass($className);
        }

        if (PHP_VERSION_ID >= 80100 && enum_exists($className, false)) {
            throw InvalidArgumentException::fromEnum($className);
        }

        $reflection = new ReflectionClass($className);

        if ($reflection->isAbstract()) {
            throw InvalidArgumentException::fromAbstractClass($reflection);
        }

        return $reflection;
    }

    /**
     * @phpstan-param ReflectionClass<T> $reflectionClass
     *
     * @throws UnexpectedValueException
     *
     * @template T of object
     */
    private function checkIfUnSerializationIsSupported(ReflectionClass $reflectionClass, string $serializedString): void
    {
        set_error_handler(static function (int $code, string $message, string $file, int $line) use ($reflectionClass, &$error): bool {
            $error = UnexpectedValueException::fromUncleanUnSerialization(
                $reflectionClass,
                $message,
                $code,
                $file,
                $line
            );

            return true;
        });

        try {
            $this->attemptInstantiationViaUnSerialization($reflectionClass, $serializedString);
        } finally {
            restore_error_handler();
        }

        if ($error) {
            throw $error;
        }
    }

    /**
     * @phpstan-param ReflectionClass<T> $reflectionClass
     *
     * @throws UnexpectedValueException
     *
     * @template T of object
     */
    private function attemptInstantiationViaUnSerialization(ReflectionClass $reflectionClass, string $serializedString): void
    {
        try {
            unserialize($serializedString);
        } catch (Exception $exception) {
            throw UnexpectedValueException::fromSerializationTriggeredException($reflectionClass, $exception);
        }
    }

    /**
     * @phpstan-param ReflectionClass<T> $reflectionClass
     *
     * @template T of object
     */
    private function isInstantiableViaReflection(ReflectionClass $reflectionClass): bool
    {
        return ! ($this->hasInternalAncestors($reflectionClass) && $reflectionClass->isFinal());
    }

    /**
     * Verifies whether the given class is to be considered internal
     *
     * @phpstan-param ReflectionClass<T> $reflectionClass
     *
     * @template T of object
     */
    private function hasInternalAncestors(ReflectionClass $reflectionClass): bool
    {
        do {
            if ($reflectionClass->isInternal()) {
                return true;
            }

            $reflectionClass = $reflectionClass->getParentClass();
        } while ($reflectionClass);

        return false;
    }

    /**
     * Checks if a class is cloneable
     *
     * Classes implementing `__clone` cannot be safely cloned, as that may cause side-effects.
     *
     * @phpstan-param ReflectionClass<T> $reflectionClass
     *
     * @template T of object
     */
    private function isSafeToClone(ReflectionClass $reflectionClass): bool
    {
        return $reflectionClass->isCloneable()
            && ! $reflectionClass->hasMethod('__clone')
            && ! $reflectionClass->isSubclassOf(ArrayIterator::class);
    }
}
Handling Annotations
====================

There are several different approaches to handling annotations in PHP.
Doctrine Annotations maps docblock annotations to PHP classes. Because
not all docblock annotations are used for metadata purposes a filter is
applied to ignore or skip classes that are not Doctrine annotations.

Take a look at the following code snippet:

.. code-block:: php

    namespace MyProject\Entities;

    use Doctrine\ORM\Mapping AS ORM;
    use Symfony\Component\Validator\Constraints AS Assert;

    /**
     * @author Benjamin Eberlei
     * @ORM\Entity
     * @MyProject\Annotations\Foobarable
     */
    class User
    {
        /**
         * @ORM\Id @ORM\Column @ORM\GeneratedValue
         * @dummy
         * @var int
         */
        private $id;

        /**
         * @ORM\Column(type="string")
         * @Assert\NotEmpty
         * @Assert\Email
         * @var string
         */
        private $email;
    }

In this snippet you can see a variety of different docblock annotations:

- Documentation annotations such as ``@var`` and ``@author``. These
  annotations are ignored and never considered for throwing an
  exception due to wrongly used annotations.
- Annotations imported through use statements. The statement ``use
  Doctrine\ORM\Mapping AS ORM`` makes all classes under that namespace
  available as ``@ORM\ClassName``. Same goes for the import of
  ``@Assert``.
- The ``@dummy`` annotation. It is not a documentation annotation and
  not ignored. For Doctrine Annotations it is not entirely clear how
  to handle this annotation. Depending on the configuration an exception
  (unknown annotation) will be thrown when parsing this annotation.
- The fully qualified annotation ``@MyProject\Annotations\Foobarable``.
  This is transformed directly into the given class name.

How are these annotations loaded? From looking at the code you could
guess that the ORM Mapping, Assert Validation and the fully qualified
annotation can just be loaded using
the defined PHP autoloaders. This is not the case however: For error
handling reasons every check for class existence inside the
``AnnotationReader`` sets the second parameter $autoload
of ``class_exists($name, $autoload)`` to false. To work flawlessly the
``AnnotationReader`` requires silent autoloaders which many autoloaders are
not. Silent autoloading is NOT part of the `PSR-0 specification
<https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md>`_
for autoloading.

This is why Doctrine Annotations uses its own autoloading mechanism
through a global registry. If you are wondering about the annotation
registry being global, there is no other way to solve the architectural
problems of autoloading annotation classes in a straightforward fashion.
Additionally if you think about PHP autoloading then you recognize it is
a global as well.

To anticipate the configuration section, making the above PHP class work
with Doctrine Annotations requires this setup:

.. code-block:: php

    use Doctrine\Common\Annotations\AnnotationReader;

    $reader = new AnnotationReader();
    AnnotationReader::addGlobalIgnoredName('dummy');

We create the actual ``AnnotationReader`` instance.
Note that we also add ``dummy`` to the global list of ignored
annotations for which we do not throw exceptions. Setting this is
necessary in our example case, otherwise ``@dummy`` would trigger an
exception to be thrown during the parsing of the docblock of
``MyProject\Entities\User#id``.

Setup and Configuration
-----------------------

To use the annotations library is simple, you just need to create a new
``AnnotationReader`` instance:

.. code-block:: php

    $reader = new \Doctrine\Common\Annotations\AnnotationReader();

This creates a simple annotation reader with no caching other than in
memory (in php arrays). Since parsing docblocks can be expensive you
should cache this process by using a caching reader.

To cache annotations, you can create a ``Doctrine\Common\Annotations\PsrCachedReader``.
This reader decorates the original reader and stores all annotations in a PSR-6
cache:

.. code-block:: php

    use Doctrine\Common\Annotations\AnnotationReader;
    use Doctrine\Common\Annotations\PsrCachedReader;

    $cache = ... // instantiate a PSR-6 Cache pool

    $reader = new PsrCachedReader(
        new AnnotationReader(),
        $cache,
        $debug = true
    );

The ``debug`` flag is used here as well to invalidate the cache files
when the PHP class with annotations changed and should be used during
development.

.. warning ::

    The ``AnnotationReader`` works and caches under the
    assumption that all annotations of a doc-block are processed at
    once. That means that annotation classes that do not exist and
    aren't loaded and cannot be autoloaded (using the
    AnnotationRegistry) would never be visible and not accessible if a
    cache is used unless the cache is cleared and the annotations
    requested again, this time with all annotations defined.

By default the annotation reader returns a list of annotations with
numeric indexes. If you want your annotations to be indexed by their
class name you can wrap the reader in an ``IndexedReader``:

.. code-block:: php

    use Doctrine\Common\Annotations\AnnotationReader;
    use Doctrine\Common\Annotations\IndexedReader;

    $reader = new IndexedReader(new AnnotationReader());

.. warning::

    You should never wrap the indexed reader inside a cached reader,
    only the other way around. This way you can re-use the cache with
    indexed or numeric keys, otherwise your code may experience failures
    due to caching in a numerical or indexed format.

Ignoring missing exceptions
~~~~~~~~~~~~~~~~~~~~~~~~~~~

By default an exception is thrown from the ``AnnotationReader`` if an
annotation was found that:

- is not part of the list of ignored "documentation annotations";
- was not imported through a use statement;
- is not a fully qualified class that exists.

You can disable this behavior for specific names if your docblocks do
not follow strict requirements:

.. code-block:: php

    $reader = new \Doctrine\Common\Annotations\AnnotationReader();
    AnnotationReader::addGlobalIgnoredName('foo');

PHP Imports
~~~~~~~~~~~

By default the annotation reader parses the use-statement of a php file
to gain access to the import rules and register them for the annotation
processing. Only if you are using PHP Imports can you validate the
correct usage of annotations and throw exceptions if you misspelled an
annotation. This mechanism is enabled by default.

To ease the upgrade path, we still allow you to disable this mechanism.
Note however that we will remove this in future versions:

.. code-block:: php

    $reader = new \Doctrine\Common\Annotations\AnnotationReader();
    $reader->setEnabledPhpImports(false);
Custom Annotation Classes
=========================

If you want to define your own annotations, you just have to group them
in a namespace.
Annotation classes have to contain a class-level docblock with the text
``@Annotation``:

.. code-block:: php

    namespace MyCompany\Annotations;

    /** @Annotation */
    class Bar
    {
        // some code
    }

Inject annotation values
------------------------

The annotation parser checks if the annotation constructor has arguments,
if so then it will pass the value array, otherwise it will try to inject
values into public properties directly:


.. code-block:: php

    namespace MyCompany\Annotations;

    /**
     * @Annotation
     *
     * Some Annotation using a constructor
     */
    class Bar
    {
        private $foo;

        public function __construct(array $values)
        {
            $this->foo = $values['foo'];
        }
    }

    /**
     * @Annotation
     *
     * Some Annotation without a constructor
     */
    class Foo
    {
        public $bar;
    }

Optional: Constructors with Named Parameters
--------------------------------------------

Starting with Annotations v1.11 a new annotation instantiation strategy
is available that aims at compatibility of Annotation classes with the PHP 8
attribute feature. You need to declare a constructor with regular parameter
names that match the named arguments in the annotation syntax.

To enable this feature, you can tag your annotation class with
``@NamedArgumentConstructor`` (available from v1.12) or implement the
``Doctrine\Common\Annotations\NamedArgumentConstructorAnnotation`` interface
(available from v1.11 and deprecated as of v1.12).
When using the ``@NamedArgumentConstructor`` tag, the first argument of the
constructor is considered as the default one.


Usage with the ``@NamedArgumentConstructor`` tag

.. code-block:: php

    namespace MyCompany\Annotations;

    /**
     * @Annotation
     * @NamedArgumentConstructor
     */
    class Bar implements NamedArgumentConstructorAnnotation
    {
        private $foo;

        public function __construct(string $foo)
        {
            $this->foo = $foo;
        }
    }

    /** Usable with @Bar(foo="baz") */
    /** Usable with @Bar("baz") */

In combination with PHP 8's constructor property promotion feature
you can simplify this to:

.. code-block:: php

    namespace MyCompany\Annotations;

    /**
     * @Annotation
     * @NamedArgumentConstructor
     */
    class Bar implements NamedArgumentConstructorAnnotation
    {
        public function __construct(private string $foo) {}
    }


Usage with the
``Doctrine\Common\Annotations\NamedArgumentConstructorAnnotation``
interface (v1.11, deprecated as of v1.12):
.. code-block:: php

    namespace MyCompany\Annotations;

    use Doctrine\Common\Annotations\NamedArgumentConstructorAnnotation;

    /** @Annotation */
    class Bar implements NamedArgumentConstructorAnnotation
    {
        private $foo;

        public function __construct(private string $foo) {}
    }

    /** Usable with @Bar(foo="baz") */

Annotation Target
-----------------

``@Target`` indicates the kinds of class elements to which an annotation
type is applicable. Then you could define one or more targets:

-  ``CLASS`` Allowed in class docblocks
-  ``PROPERTY`` Allowed in property docblocks
-  ``METHOD`` Allowed in the method docblocks
-  ``FUNCTION`` Allowed in function dockblocks
-  ``ALL`` Allowed in class, property, method and function docblocks
-  ``ANNOTATION`` Allowed inside other annotations

If the annotations is not allowed in the current context, an
``AnnotationException`` is thrown.

.. code-block:: php

    namespace MyCompany\Annotations;

    /**
     * @Annotation
     * @Target({"METHOD","PROPERTY"})
     */
    class Bar
    {
        // some code
    }

    /**
     * @Annotation
     * @Target("CLASS")
     */
    class Foo
    {
        // some code
    }

Attribute types
---------------

The annotation parser checks the given parameters using the phpdoc
annotation ``@var``, The data type could be validated using the ``@var``
annotation on the annotation properties or using the ``@Attributes`` and
``@Attribute`` annotations.

If the data type does not match you get an ``AnnotationException``

.. code-block:: php

    namespace MyCompany\Annotations;

    /**
     * @Annotation
     * @Target({"METHOD","PROPERTY"})
     */
    class Bar
    {
        /** @var mixed */
        public $mixed;

        /** @var boolean */
        public $boolean;

        /** @var bool */
        public $bool;

        /** @var float */
        public $float;

        /** @var string */
        public $string;

        /** @var integer */
        public $integer;

        /** @var array */
        public $array;

        /** @var SomeAnnotationClass */
        public $annotation;

        /** @var array<integer> */
        public $arrayOfIntegers;

        /** @var array<SomeAnnotationClass> */
        public $arrayOfAnnotations;
    }

    /**
     * @Annotation
     * @Target({"METHOD","PROPERTY"})
     * @Attributes({
     *   @Attribute("stringProperty", type = "string"),
     *   @Attribute("annotProperty",  type = "SomeAnnotationClass"),
     * })
     */
    class Foo
    {
        public function __construct(array $values)
        {
            $this->stringProperty = $values['stringProperty'];
            $this->annotProperty = $values['annotProperty'];
        }

        // some code
    }

Annotation Required
-------------------

``@Required`` indicates that the field must be specified when the
annotation is used. If it is not used you get an ``AnnotationException``
stating that this value can not be null.

Declaring a required field:

.. code-block:: php

    /**
     * @Annotation
     * @Target("ALL")
     */
    class Foo
    {
        /** @Required */
        public $requiredField;
    }

Usage:

.. code-block:: php

    /** @Foo(requiredField="value") */
    public $direction;                  // Valid

     /** @Foo */
    public $direction;                  // Required field missing, throws an AnnotationException


Enumerated values
-----------------

- An annotation property marked with ``@Enum`` is a field that accepts a
  fixed set of scalar values.
- You should use ``@Enum`` fields any time you need to represent fixed
  values.
- The annotation parser checks the given value and throws an
  ``AnnotationException`` if the value does not match.


Declaring an enumerated property:

.. code-block:: php

    /**
     * @Annotation
     * @Target("ALL")
     */
    class Direction
    {
        /**
         * @Enum({"NORTH", "SOUTH", "EAST", "WEST"})
         */
        public $value;
    }

Annotation usage:

.. code-block:: php

    /** @Direction("NORTH") */
    public $direction;                  // Valid value

     /** @Direction("NORTHEAST") */
    public $direction;                  // Invalid value, throws an AnnotationException


Constants
---------

The use of constants and class constants is available on the annotations
parser.

The following usages are allowed:

.. code-block:: php

    namespace MyCompany\Entity;

    use MyCompany\Annotations\Foo;
    use MyCompany\Annotations\Bar;
    use MyCompany\Entity\SomeClass;

    /**
     * @Foo(PHP_EOL)
     * @Bar(Bar::FOO)
     * @Foo({SomeClass::FOO, SomeClass::BAR})
     * @Bar({SomeClass::FOO_KEY = SomeClass::BAR_VALUE})
     */
    class User
    {
    }


Be careful with constants and the cache !

.. note::

    The cached reader will not re-evaluate each time an annotation is
    loaded from cache. When a constant is changed the cache must be
    cleaned.


Usage
-----

Using the library API is simple. Using the annotations described in the
previous section, you can now annotate other classes with your
annotations:

.. code-block:: php

    namespace MyCompany\Entity;

    use MyCompany\Annotations\Foo;
    use MyCompany\Annotations\Bar;

    /**
     * @Foo(bar="foo")
     * @Bar(foo="bar")
     */
    class User
    {
    }

Now we can write a script to get the annotations above:

.. code-block:: php

    $reflClass = new ReflectionClass('MyCompany\Entity\User');
    $classAnnotations = $reader->getClassAnnotations($reflClass);

    foreach ($classAnnotations AS $annot) {
        if ($annot instanceof \MyCompany\Annotations\Foo) {
            echo $annot->bar; // prints "foo";
        } else if ($annot instanceof \MyCompany\Annotations\Bar) {
            echo $annot->foo; // prints "bar";
        }
    }

You have a complete API for retrieving annotation class instances from a
class, property or method docblock:


Reader API
~~~~~~~~~~

Access all annotations of a class
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. code-block:: php

    public function getClassAnnotations(\ReflectionClass $class);

Access one annotation of a class
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. code-block:: php

    public function getClassAnnotation(\ReflectionClass $class, $annotationName);

Access all annotations of a method
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. code-block:: php

    public function getMethodAnnotations(\ReflectionMethod $method);

Access one annotation of a method
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. code-block:: php

    public function getMethodAnnotation(\ReflectionMethod $method, $annotationName);

Access all annotations of a property
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. code-block:: php

    public function getPropertyAnnotations(\ReflectionProperty $property);

Access one annotation of a property
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. code-block:: php

    public function getPropertyAnnotation(\ReflectionProperty $property, $annotationName);

Access all annotations of a function
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. code-block:: php

    public function getFunctionAnnotations(\ReflectionFunction $property);

Access one annotation of a function
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. code-block:: php

    public function getFunctionAnnotation(\ReflectionFunction $property, $annotationName);
Deprecation notice
==================

PHP 8 introduced `attributes
<https://www.php.net/manual/en/language.attributes.overview.php>`_,
which are a native replacement for annotations. As such, this library is
considered feature complete, and should receive exclusively bugfixes and
security fixes.

We do not recommend using this library in new projects and encourage authors
of downstream libraries to offer support for attributes as an alternative to
Doctrine Annotations.

Have a look at [our blog](https://www.doctrine-project.org/2022/11/04/annotations-to-attributes.html)
to learn more.

Introduction
============

Doctrine Annotations allows to implement custom annotation
functionality for PHP classes and functions.

.. code-block:: php

    class Foo
    {
        /**
         * @MyAnnotation(myProperty="value")
         */
        private $bar;
    }

Annotations aren't implemented in PHP itself which is why this component
offers a way to use the PHP doc-blocks as a place for the well known
annotation syntax using the ``@`` char.

Annotations in Doctrine are used for the ORM configuration to build the
class mapping, but it can be used in other projects for other purposes
too.

Installation
============

You can install the Annotation component with composer:

.. code-block::

    $ composer require doctrine/annotations

Create an annotation class
==========================

An annotation class is a representation of the later used annotation
configuration in classes. The annotation class of the previous example
looks like this:

.. code-block:: php

    /**
     * @Annotation
     */
    final class MyAnnotation
    {
        public $myProperty;
    }

The annotation class is declared as an annotation by ``@Annotation``.

:ref:`Read more about custom annotations. <custom>`

Reading annotations
===================

The access to the annotations happens by reflection of the class or function
containing them. There are multiple reader-classes implementing the
``Doctrine\Common\Annotations\Reader`` interface, that can access the
annotations of a class. A common one is
``Doctrine\Common\Annotations\AnnotationReader``:

.. code-block:: php

    use Doctrine\Common\Annotations\AnnotationReader;

    $reflectionClass = new ReflectionClass(Foo::class);
    $property = $reflectionClass->getProperty('bar');

    $reader = new AnnotationReader();
    $myAnnotation = $reader->getPropertyAnnotation(
        $property,
        MyAnnotation::class
    );

    echo $myAnnotation->myProperty; // result: "value"

A reader has multiple methods to access the annotations of a class or
function.

:ref:`Read more about handling annotations. <annotations>`

IDE Support
-----------

Some IDEs already provide support for annotations:

- Eclipse via the `Symfony2 Plugin <https://github.com/pulse00/Symfony-2-Eclipse-Plugin>`_
- PhpStorm via the `PHP Annotations Plugin <https://plugins.jetbrains.com/plugin/7320-php-annotations>`_ or the `Symfony Plugin <https://plugins.jetbrains.com/plugin/7219-symfony-support>`_

.. _Read more about handling annotations.: annotations
.. _Read more about custom annotations.: custom
.. toctree::
    :depth: 3

    index
    annotations
    custom
<?php

namespace Doctrine\Common\Annotations;

use Psr\Cache\CacheItemPoolInterface;
use ReflectionClass;
use ReflectionMethod;
use ReflectionProperty;
use Reflector;

use function array_map;
use function array_merge;
use function assert;
use function filemtime;
use function is_file;
use function max;
use function rawurlencode;
use function time;

/**
 * A cache aware annotation reader.
 */
final class PsrCachedReader implements Reader
{
    /** @var Reader */
    private $delegate;

    /** @var CacheItemPoolInterface */
    private $cache;

    /** @var bool */
    private $debug;

    /** @var array<string, array<object>> */
    private $loadedAnnotations = [];

    /** @var int[] */
    private $loadedFilemtimes = [];

    public function __construct(Reader $reader, CacheItemPoolInterface $cache, bool $debug = false)
    {
        $this->delegate = $reader;
        $this->cache    = $cache;
        $this->debug    = (bool) $debug;
    }

    /**
     * {@inheritDoc}
     */
    public function getClassAnnotations(ReflectionClass $class)
    {
        $cacheKey = $class->getName();

        if (isset($this->loadedAnnotations[$cacheKey])) {
            return $this->loadedAnnotations[$cacheKey];
        }

        $annots = $this->fetchFromCache($cacheKey, $class, 'getClassAnnotations', $class);

        return $this->loadedAnnotations[$cacheKey] = $annots;
    }

    /**
     * {@inheritDoc}
     */
    public function getClassAnnotation(ReflectionClass $class, $annotationName)
    {
        foreach ($this->getClassAnnotations($class) as $annot) {
            if ($annot instanceof $annotationName) {
                return $annot;
            }
        }

        return null;
    }

    /**
     * {@inheritDoc}
     */
    public function getPropertyAnnotations(ReflectionProperty $property)
    {
        $class    = $property->getDeclaringClass();
        $cacheKey = $class->getName() . '$' . $property->getName();

        if (isset($this->loadedAnnotations[$cacheKey])) {
            return $this->loadedAnnotations[$cacheKey];
        }

        $annots = $this->fetchFromCache($cacheKey, $class, 'getPropertyAnnotations', $property);

        return $this->loadedAnnotations[$cacheKey] = $annots;
    }

    /**
     * {@inheritDoc}
     */
    public function getPropertyAnnotation(ReflectionProperty $property, $annotationName)
    {
        foreach ($this->getPropertyAnnotations($property) as $annot) {
            if ($annot instanceof $annotationName) {
                return $annot;
            }
        }

        return null;
    }

    /**
     * {@inheritDoc}
     */
    public function getMethodAnnotations(ReflectionMethod $method)
    {
        $class    = $method->getDeclaringClass();
        $cacheKey = $class->getName() . '#' . $method->getName();

        if (isset($this->loadedAnnotations[$cacheKey])) {
            return $this->loadedAnnotations[$cacheKey];
        }

        $annots = $this->fetchFromCache($cacheKey, $class, 'getMethodAnnotations', $method);

        return $this->loadedAnnotations[$cacheKey] = $annots;
    }

    /**
     * {@inheritDoc}
     */
    public function getMethodAnnotation(ReflectionMethod $method, $annotationName)
    {
        foreach ($this->getMethodAnnotations($method) as $annot) {
            if ($annot instanceof $annotationName) {
                return $annot;
            }
        }

        return null;
    }

    public function clearLoadedAnnotations(): void
    {
        $this->loadedAnnotations = [];
        $this->loadedFilemtimes  = [];
    }

    /** @return mixed[] */
    private function fetchFromCache(
        string $cacheKey,
        ReflectionClass $class,
        string $method,
        Reflector $reflector
    ): array {
        $cacheKey = rawurlencode($cacheKey);

        $item = $this->cache->getItem($cacheKey);
        if (($this->debug && ! $this->refresh($cacheKey, $class)) || ! $item->isHit()) {
            $this->cache->save($item->set($this->delegate->{$method}($reflector)));
        }

        return $item->get();
    }

    /**
     * Used in debug mode to check if the cache is fresh.
     *
     * @return bool Returns true if the cache was fresh, or false if the class
     * being read was modified since writing to the cache.
     */
    private function refresh(string $cacheKey, ReflectionClass $class): bool
    {
        $lastModification = $this->getLastModification($class);
        if ($lastModification === 0) {
            return true;
        }

        $item = $this->cache->getItem('[C]' . $cacheKey);
        if ($item->isHit() && $item->get() >= $lastModification) {
            return true;
        }

        $this->cache->save($item->set(time()));

        return false;
    }

    /**
     * Returns the time the class was last modified, testing traits and parents
     */
    private function getLastModification(ReflectionClass $class): int
    {
        $filename = $class->getFileName();

        if (isset($this->loadedFilemtimes[$filename])) {
            return $this->loadedFilemtimes[$filename];
        }

        $parent = $class->getParentClass();

        $lastModification =  max(array_merge(
            [$filename !== false && is_file($filename) ? filemtime($filename) : 0],
            array_map(function (ReflectionClass $reflectionTrait): int {
                return $this->getTraitLastModificationTime($reflectionTrait);
            }, $class->getTraits()),
            array_map(function (ReflectionClass $class): int {
                return $this->getLastModification($class);
            }, $class->getInterfaces()),
            $parent ? [$this->getLastModification($parent)] : []
        ));

        assert($lastModification !== false);

        return $this->loadedFilemtimes[$filename] = $lastModification;
    }

    private function getTraitLastModificationTime(ReflectionClass $reflectionTrait): int
    {
        $fileName = $reflectionTrait->getFileName();

        if (isset($this->loadedFilemtimes[$fileName])) {
            return $this->loadedFilemtimes[$fileName];
        }

        $lastModificationTime = max(array_merge(
            [$fileName !== false && is_file($fileName) ? filemtime($fileName) : 0],
            array_map(function (ReflectionClass $reflectionTrait): int {
                return $this->getTraitLastModificationTime($reflectionTrait);
            }, $reflectionTrait->getTraits())
        ));

        assert($lastModificationTime !== false);

        return $this->loadedFilemtimes[$fileName] = $lastModificationTime;
    }
}
<?php

namespace Doctrine\Common\Annotations;

use Exception;
use Throwable;

use function get_class;
use function gettype;
use function implode;
use function is_object;
use function sprintf;

/**
 * Description of AnnotationException
 */
class AnnotationException extends Exception
{
    /**
     * Creates a new AnnotationException describing a Syntax error.
     *
     * @return AnnotationException
     */
    public static function syntaxError(string $message)
    {
        return new self('[Syntax Error] ' . $message);
    }

    /**
     * Creates a new AnnotationException describing a Semantical error.
     *
     * @return AnnotationException
     */
    public static function semanticalError(string $message)
    {
        return new self('[Semantical Error] ' . $message);
    }

    /**
     * Creates a new AnnotationException describing an error which occurred during
     * the creation of the annotation.
     *
     * @return AnnotationException
     */
    public static function creationError(string $message, ?Throwable $previous = null)
    {
        return new self('[Creation Error] ' . $message, 0, $previous);
    }

    /**
     * Creates a new AnnotationException describing a type error.
     *
     * @return AnnotationException
     */
    public static function typeError(string $message)
    {
        return new self('[Type Error] ' . $message);
    }

    /**
     * Creates a new AnnotationException describing a constant semantical error.
     *
     * @return AnnotationException
     */
    public static function semanticalErrorConstants(string $identifier, ?string $context = null)
    {
        return self::semanticalError(sprintf(
            "Couldn't find constant %s%s.",
            $identifier,
            $context ? ', ' . $context : ''
        ));
    }

    /**
     * Creates a new AnnotationException describing an type error of an attribute.
     *
     * @param mixed $actual
     *
     * @return AnnotationException
     */
    public static function attributeTypeError(
        string $attributeName,
        string $annotationName,
        string $context,
        string $expected,
        $actual
    ) {
        return self::typeError(sprintf(
            'Attribute "%s" of @%s declared on %s expects %s, but got %s.',
            $attributeName,
            $annotationName,
            $context,
            $expected,
            is_object($actual) ? 'an instance of ' . get_class($actual) : gettype($actual)
        ));
    }

    /**
     * Creates a new AnnotationException describing an required error of an attribute.
     *
     * @return AnnotationException
     */
    public static function requiredError(
        string $attributeName,
        string $annotationName,
        string $context,
        string $expected
    ) {
        return self::typeError(sprintf(
            'Attribute "%s" of @%s declared on %s expects %s. This value should not be null.',
            $attributeName,
            $annotationName,
            $context,
            $expected
        ));
    }

    /**
     * Creates a new AnnotationException describing a invalid enummerator.
     *
     * @param mixed $given
     * @phpstan-param list<string> $available
     *
     * @return AnnotationException
     */
    public static function enumeratorError(
        string $attributeName,
        string $annotationName,
        string $context,
        array $available,
        $given
    ) {
        return new self(sprintf(
            '[Enum Error] Attribute "%s" of @%s declared on %s accepts only [%s], but got %s.',
            $attributeName,
            $annotationName,
            $context,
            implode(', ', $available),
            is_object($given) ? get_class($given) : $given
        ));
    }

    /** @return AnnotationException */
    public static function optimizerPlusSaveComments()
    {
        return new self(
            'You have to enable opcache.save_comments=1 or zend_optimizerplus.save_comments=1.'
        );
    }

    /** @return AnnotationException */
    public static function optimizerPlusLoadComments()
    {
        return new self(
            'You have to enable opcache.load_comments=1 or zend_optimizerplus.load_comments=1.'
        );
    }
}
<?php

namespace Doctrine\Common\Annotations;

use ReflectionClass;
use ReflectionMethod;
use ReflectionProperty;

use function call_user_func_array;
use function get_class;

/**
 * Allows the reader to be used in-place of Doctrine's reader.
 */
class IndexedReader implements Reader
{
    /** @var Reader */
    private $delegate;

    public function __construct(Reader $reader)
    {
        $this->delegate = $reader;
    }

    /**
     * {@inheritDoc}
     */
    public function getClassAnnotations(ReflectionClass $class)
    {
        $annotations = [];
        foreach ($this->delegate->getClassAnnotations($class) as $annot) {
            $annotations[get_class($annot)] = $annot;
        }

        return $annotations;
    }

    /**
     * {@inheritDoc}
     */
    public function getClassAnnotation(ReflectionClass $class, $annotationName)
    {
        return $this->delegate->getClassAnnotation($class, $annotationName);
    }

    /**
     * {@inheritDoc}
     */
    public function getMethodAnnotations(ReflectionMethod $method)
    {
        $annotations = [];
        foreach ($this->delegate->getMethodAnnotations($method) as $annot) {
            $annotations[get_class($annot)] = $annot;
        }

        return $annotations;
    }

    /**
     * {@inheritDoc}
     */
    public function getMethodAnnotation(ReflectionMethod $method, $annotationName)
    {
        return $this->delegate->getMethodAnnotation($method, $annotationName);
    }

    /**
     * {@inheritDoc}
     */
    public function getPropertyAnnotations(ReflectionProperty $property)
    {
        $annotations = [];
        foreach ($this->delegate->getPropertyAnnotations($property) as $annot) {
            $annotations[get_class($annot)] = $annot;
        }

        return $annotations;
    }

    /**
     * {@inheritDoc}
     */
    public function getPropertyAnnotation(ReflectionProperty $property, $annotationName)
    {
        return $this->delegate->getPropertyAnnotation($property, $annotationName);
    }

    /**
     * Proxies all methods to the delegate.
     *
     * @param mixed[] $args
     *
     * @return mixed
     */
    public function __call(string $method, array $args)
    {
        return call_user_func_array([$this->delegate, $method], $args);
    }
}
<?php

namespace Doctrine\Common\Annotations;

use Doctrine\Common\Annotations\Annotation\Attribute;
use Doctrine\Common\Annotations\Annotation\Attributes;
use Doctrine\Common\Annotations\Annotation\Enum;
use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor;
use Doctrine\Common\Annotations\Annotation\Target;
use ReflectionClass;
use ReflectionException;
use ReflectionProperty;
use RuntimeException;
use stdClass;
use Throwable;

use function array_keys;
use function array_map;
use function array_pop;
use function array_values;
use function class_exists;
use function constant;
use function count;
use function defined;
use function explode;
use function gettype;
use function implode;
use function in_array;
use function interface_exists;
use function is_array;
use function is_object;
use function json_encode;
use function ltrim;
use function preg_match;
use function reset;
use function rtrim;
use function sprintf;
use function stripos;
use function strlen;
use function strpos;
use function strrpos;
use function strtolower;
use function substr;
use function trim;

use const PHP_VERSION_ID;

/**
 * A parser for docblock annotations.
 *
 * It is strongly discouraged to change the default annotation parsing process.
 *
 * @psalm-type Arguments = array{positional_arguments?: array<int, mixed>, named_arguments?: array<string, mixed>}
 */
final class DocParser
{
    /**
     * An array of all valid tokens for a class name.
     *
     * @phpstan-var list<int>
     */
    private static $classIdentifiers = [
        DocLexer::T_IDENTIFIER,
        DocLexer::T_TRUE,
        DocLexer::T_FALSE,
        DocLexer::T_NULL,
    ];

    /**
     * The lexer.
     *
     * @var DocLexer
     */
    private $lexer;

    /**
     * Current target context.
     *
     * @var int
     */
    private $target;

    /**
     * Doc parser used to collect annotation target.
     *
     * @var DocParser
     */
    private static $metadataParser;

    /**
     * Flag to control if the current annotation is nested or not.
     *
     * @var bool
     */
    private $isNestedAnnotation = false;

    /**
     * Hashmap containing all use-statements that are to be used when parsing
     * the given doc block.
     *
     * @var array<string, class-string>
     */
    private $imports = [];

    /**
     * This hashmap is used internally to cache results of class_exists()
     * look-ups.
     *
     * @var array<class-string, bool>
     */
    private $classExists = [];

    /**
     * Whether annotations that have not been imported should be ignored.
     *
     * @var bool
     */
    private $ignoreNotImportedAnnotations = false;

    /**
     * An array of default namespaces if operating in simple mode.
     *
     * @var string[]
     */
    private $namespaces = [];

    /**
     * A list with annotations that are not causing exceptions when not resolved to an annotation class.
     *
     * The names must be the raw names as used in the class, not the fully qualified
     *
     * @var bool[] indexed by annotation name
     */
    private $ignoredAnnotationNames = [];

    /**
     * A list with annotations in namespaced format
     * that are not causing exceptions when not resolved to an annotation class.
     *
     * @var bool[] indexed by namespace name
     */
    private $ignoredAnnotationNamespaces = [];

    /** @var string */
    private $context = '';

    /**
     * Hash-map for caching annotation metadata.
     *
     * @var array<class-string, mixed[]>
     */
    private static $annotationMetadata = [
        Annotation\Target::class => [
            'is_annotation'                  => true,
            'has_constructor'                => true,
            'has_named_argument_constructor' => false,
            'properties'                     => [],
            'targets_literal'                => 'ANNOTATION_CLASS',
            'targets'                        => Target::TARGET_CLASS,
            'default_property'               => 'value',
            'attribute_types'                => [
                'value'  => [
                    'required'   => false,
                    'type'       => 'array',
                    'array_type' => 'string',
                    'value'      => 'array<string>',
                ],
            ],
        ],
        Annotation\Attribute::class => [
            'is_annotation'                  => true,
            'has_constructor'                => false,
            'has_named_argument_constructor' => false,
            'targets_literal'                => 'ANNOTATION_ANNOTATION',
            'targets'                        => Target::TARGET_ANNOTATION,
            'default_property'               => 'name',
            'properties'                     => [
                'name'      => 'name',
                'type'      => 'type',
                'required'  => 'required',
            ],
            'attribute_types'                => [
                'value'  => [
                    'required'  => true,
                    'type'      => 'string',
                    'value'     => 'string',
                ],
                'type'  => [
                    'required'  => true,
                    'type'      => 'string',
                    'value'     => 'string',
                ],
                'required'  => [
                    'required'  => false,
                    'type'      => 'boolean',
                    'value'     => 'boolean',
                ],
            ],
        ],
        Annotation\Attributes::class => [
            'is_annotation'                  => true,
            'has_constructor'                => false,
            'has_named_argument_constructor' => false,
            'targets_literal'                => 'ANNOTATION_CLASS',
            'targets'                        => Target::TARGET_CLASS,
            'default_property'               => 'value',
            'properties'                     => ['value' => 'value'],
            'attribute_types'                => [
                'value' => [
                    'type'      => 'array',
                    'required'  => true,
                    'array_type' => Annotation\Attribute::class,
                    'value'     => 'array<' . Annotation\Attribute::class . '>',
                ],
            ],
        ],
        Annotation\Enum::class => [
            'is_annotation'                  => true,
            'has_constructor'                => true,
            'has_named_argument_constructor' => false,
            'targets_literal'                => 'ANNOTATION_PROPERTY',
            'targets'                        => Target::TARGET_PROPERTY,
            'default_property'               => 'value',
            'properties'                     => ['value' => 'value'],
            'attribute_types'                => [
                'value' => [
                    'type'      => 'array',
                    'required'  => true,
                ],
                'literal' => [
                    'type'      => 'array',
                    'required'  => false,
                ],
            ],
        ],
        Annotation\NamedArgumentConstructor::class => [
            'is_annotation'                  => true,
            'has_constructor'                => false,
            'has_named_argument_constructor' => false,
            'targets_literal'                => 'ANNOTATION_CLASS',
            'targets'                        => Target::TARGET_CLASS,
            'default_property'               => null,
            'properties'                     => [],
            'attribute_types'                => [],
        ],
    ];

    /**
     * Hash-map for handle types declaration.
     *
     * @var array<string, string>
     */
    private static $typeMap = [
        'float'     => 'double',
        'bool'      => 'boolean',
        // allow uppercase Boolean in honor of George Boole
        'Boolean'   => 'boolean',
        'int'       => 'integer',
    ];

    /**
     * Constructs a new DocParser.
     */
    public function __construct()
    {
        $this->lexer = new DocLexer();
    }

    /**
     * Sets the annotation names that are ignored during the parsing process.
     *
     * The names are supposed to be the raw names as used in the class, not the
     * fully qualified class names.
     *
     * @param bool[] $names indexed by annotation name
     *
     * @return void
     */
    public function setIgnoredAnnotationNames(array $names)
    {
        $this->ignoredAnnotationNames = $names;
    }

    /**
     * Sets the annotation namespaces that are ignored during the parsing process.
     *
     * @param bool[] $ignoredAnnotationNamespaces indexed by annotation namespace name
     *
     * @return void
     */
    public function setIgnoredAnnotationNamespaces(array $ignoredAnnotationNamespaces)
    {
        $this->ignoredAnnotationNamespaces = $ignoredAnnotationNamespaces;
    }

    /**
     * Sets ignore on not-imported annotations.
     *
     * @return void
     */
    public function setIgnoreNotImportedAnnotations(bool $bool)
    {
        $this->ignoreNotImportedAnnotations = $bool;
    }

    /**
     * Sets the default namespaces.
     *
     * @return void
     *
     * @throws RuntimeException
     */
    public function addNamespace(string $namespace)
    {
        if ($this->imports) {
            throw new RuntimeException('You must either use addNamespace(), or setImports(), but not both.');
        }

        $this->namespaces[] = $namespace;
    }

    /**
     * Sets the imports.
     *
     * @param array<string, class-string> $imports
     *
     * @return void
     *
     * @throws RuntimeException
     */
    public function setImports(array $imports)
    {
        if ($this->namespaces) {
            throw new RuntimeException('You must either use addNamespace(), or setImports(), but not both.');
        }

        $this->imports = $imports;
    }

    /**
     * Sets current target context as bitmask.
     *
     * @return void
     */
    public function setTarget(int $target)
    {
        $this->target = $target;
    }

    /**
     * Parses the given docblock string for annotations.
     *
     * @phpstan-return list<object> Array of annotations. If no annotations are found, an empty array is returned.
     *
     * @throws AnnotationException
     * @throws ReflectionException
     */
    public function parse(string $input, string $context = '')
    {
        $pos = $this->findInitialTokenPosition($input);
        if ($pos === null) {
            return [];
        }

        $this->context = $context;

        $this->lexer->setInput(trim(substr($input, $pos), '* /'));
        $this->lexer->moveNext();

        return $this->Annotations();
    }

    /**
     * Finds the first valid annotation
     */
    private function findInitialTokenPosition(string $input): ?int
    {
        $pos = 0;

        // search for first valid annotation
        while (($pos = strpos($input, '@', $pos)) !== false) {
            $preceding = substr($input, $pos - 1, 1);

            // if the @ is preceded by a space, a tab or * it is valid
            if ($pos === 0 || $preceding === ' ' || $preceding === '*' || $preceding === "\t") {
                return $pos;
            }

            $pos++;
        }

        return null;
    }

    /**
     * Attempts to match the given token with the current lookahead token.
     * If they match, updates the lookahead token; otherwise raises a syntax error.
     *
     * @param int $token Type of token.
     *
     * @return bool True if tokens match; false otherwise.
     *
     * @throws AnnotationException
     */
    private function match(int $token): bool
    {
        if (! $this->lexer->isNextToken($token)) {
            throw $this->syntaxError($this->lexer->getLiteral($token));
        }

        return $this->lexer->moveNext();
    }

    /**
     * Attempts to match the current lookahead token with any of the given tokens.
     *
     * If any of them matches, this method updates the lookahead token; otherwise
     * a syntax error is raised.
     *
     * @phpstan-param list<mixed[]> $tokens
     *
     * @throws AnnotationException
     */
    private function matchAny(array $tokens): bool
    {
        if (! $this->lexer->isNextTokenAny($tokens)) {
            throw $this->syntaxError(implode(' or ', array_map([$this->lexer, 'getLiteral'], $tokens)));
        }

        return $this->lexer->moveNext();
    }

    /**
     * Generates a new syntax error.
     *
     * @param string       $expected Expected string.
     * @param mixed[]|null $token    Optional token.
     */
    private function syntaxError(string $expected, ?array $token = null): AnnotationException
    {
        if ($token === null) {
            $token = $this->lexer->lookahead;
        }

        $message  = sprintf('Expected %s, got ', $expected);
        $message .= $this->lexer->lookahead === null
            ? 'end of string'
            : sprintf("'%s' at position %s", $token->value, $token->position);

        if (strlen($this->context)) {
            $message .= ' in ' . $this->context;
        }

        $message .= '.';

        return AnnotationException::syntaxError($message);
    }

    /**
     * Attempts to check if a class exists or not. This never goes through the PHP autoloading mechanism
     * but uses the {@link AnnotationRegistry} to load classes.
     *
     * @param class-string $fqcn
     */
    private function classExists(string $fqcn): bool
    {
        if (isset($this->classExists[$fqcn])) {
            return $this->classExists[$fqcn];
        }

        // first check if the class already exists, maybe loaded through another AnnotationReader
        if (class_exists($fqcn, false)) {
            return $this->classExists[$fqcn] = true;
        }

        // final check, does this class exist?
        return $this->classExists[$fqcn] = AnnotationRegistry::loadAnnotationClass($fqcn);
    }

    /**
     * Collects parsing metadata for a given annotation class
     *
     * @param class-string $name The annotation name
     *
     * @throws AnnotationException
     * @throws ReflectionException
     */
    private function collectAnnotationMetadata(string $name): void
    {
        if (self::$metadataParser === null) {
            self::$metadataParser = new self();

            self::$metadataParser->setIgnoreNotImportedAnnotations(true);
            self::$metadataParser->setIgnoredAnnotationNames($this->ignoredAnnotationNames);
            self::$metadataParser->setImports([
                'enum'                     => Enum::class,
                'target'                   => Target::class,
                'attribute'                => Attribute::class,
                'attributes'               => Attributes::class,
                'namedargumentconstructor' => NamedArgumentConstructor::class,
            ]);

            // Make sure that annotations from metadata are loaded
            class_exists(Enum::class);
            class_exists(Target::class);
            class_exists(Attribute::class);
            class_exists(Attributes::class);
            class_exists(NamedArgumentConstructor::class);
        }

        $class      = new ReflectionClass($name);
        $docComment = $class->getDocComment();

        // Sets default values for annotation metadata
        $constructor = $class->getConstructor();
        $metadata    = [
            'default_property' => null,
            'has_constructor'  => $constructor !== null && $constructor->getNumberOfParameters() > 0,
            'constructor_args' => [],
            'properties'       => [],
            'property_types'   => [],
            'attribute_types'  => [],
            'targets_literal'  => null,
            'targets'          => Target::TARGET_ALL,
            'is_annotation'    => strpos($docComment, '@Annotation') !== false,
        ];

        $metadata['has_named_argument_constructor'] = false;

        // verify that the class is really meant to be an annotation
        if ($metadata['is_annotation']) {
            self::$metadataParser->setTarget(Target::TARGET_CLASS);

            foreach (self::$metadataParser->parse($docComment, 'class @' . $name) as $annotation) {
                if ($annotation instanceof Target) {
                    $metadata['targets']         = $annotation->targets;
                    $metadata['targets_literal'] = $annotation->literal;

                    continue;
                }

                if ($annotation instanceof NamedArgumentConstructor) {
                    $metadata['has_named_argument_constructor'] = $metadata['has_constructor'];
                    if ($metadata['has_named_argument_constructor']) {
                        // choose the first argument as the default property
                        $metadata['default_property'] = $constructor->getParameters()[0]->getName();
                    }
                }

                if (! ($annotation instanceof Attributes)) {
                    continue;
                }

                foreach ($annotation->value as $attribute) {
                    $this->collectAttributeTypeMetadata($metadata, $attribute);
                }
            }

            // if not has a constructor will inject values into public properties
            if ($metadata['has_constructor'] === false) {
                // collect all public properties
                foreach ($class->getProperties(ReflectionProperty::IS_PUBLIC) as $property) {
                    $metadata['properties'][$property->name] = $property->name;

                    $propertyComment = $property->getDocComment();
                    if ($propertyComment === false) {
                        continue;
                    }

                    $attribute = new Attribute();

                    $attribute->required = (strpos($propertyComment, '@Required') !== false);
                    $attribute->name     = $property->name;
                    $attribute->type     = (strpos($propertyComment, '@var') !== false &&
                        preg_match('/@var\s+([^\s]+)/', $propertyComment, $matches))
                        ? $matches[1]
                        : 'mixed';

                    $this->collectAttributeTypeMetadata($metadata, $attribute);

                    // checks if the property has @Enum
                    if (strpos($propertyComment, '@Enum') === false) {
                        continue;
                    }

                    $context = 'property ' . $class->name . '::$' . $property->name;

                    self::$metadataParser->setTarget(Target::TARGET_PROPERTY);

                    foreach (self::$metadataParser->parse($propertyComment, $context) as $annotation) {
                        if (! $annotation instanceof Enum) {
                            continue;
                        }

                        $metadata['enum'][$property->name]['value']   = $annotation->value;
                        $metadata['enum'][$property->name]['literal'] = (! empty($annotation->literal))
                            ? $annotation->literal
                            : $annotation->value;
                    }
                }

                // choose the first property as default property
                $metadata['default_property'] = reset($metadata['properties']);
            } elseif ($metadata['has_named_argument_constructor']) {
                foreach ($constructor->getParameters() as $parameter) {
                    if ($parameter->isVariadic()) {
                        break;
                    }

                    $metadata['constructor_args'][$parameter->getName()] = [
                        'position' => $parameter->getPosition(),
                        'default' => $parameter->isOptional() ? $parameter->getDefaultValue() : null,
                    ];
                }
            }
        }

        self::$annotationMetadata[$name] = $metadata;
    }

    /**
     * Collects parsing metadata for a given attribute.
     *
     * @param mixed[] $metadata
     */
    private function collectAttributeTypeMetadata(array &$metadata, Attribute $attribute): void
    {
        // handle internal type declaration
        $type = self::$typeMap[$attribute->type] ?? $attribute->type;

        // handle the case if the property type is mixed
        if ($type === 'mixed') {
            return;
        }

        // Evaluate type
        $pos = strpos($type, '<');
        if ($pos !== false) {
            // Checks if the property has array<type>
            $arrayType = substr($type, $pos + 1, -1);
            $type      = 'array';

            if (isset(self::$typeMap[$arrayType])) {
                $arrayType = self::$typeMap[$arrayType];
            }

            $metadata['attribute_types'][$attribute->name]['array_type'] = $arrayType;
        } else {
            // Checks if the property has type[]
            $pos = strrpos($type, '[');
            if ($pos !== false) {
                $arrayType = substr($type, 0, $pos);
                $type      = 'array';

                if (isset(self::$typeMap[$arrayType])) {
                    $arrayType = self::$typeMap[$arrayType];
                }

                $metadata['attribute_types'][$attribute->name]['array_type'] = $arrayType;
            }
        }

        $metadata['attribute_types'][$attribute->name]['type']     = $type;
        $metadata['attribute_types'][$attribute->name]['value']    = $attribute->type;
        $metadata['attribute_types'][$attribute->name]['required'] = $attribute->required;
    }

    /**
     * Annotations ::= Annotation {[ "*" ]* [Annotation]}*
     *
     * @phpstan-return list<object>
     *
     * @throws AnnotationException
     * @throws ReflectionException
     */
    private function Annotations(): array
    {
        $annotations = [];

        while ($this->lexer->lookahead !== null) {
            if ($this->lexer->lookahead->type !== DocLexer::T_AT) {
                $this->lexer->moveNext();
                continue;
            }

            // make sure the @ is preceded by non-catchable pattern
            if (
                $this->lexer->token !== null &&
                $this->lexer->lookahead->position === $this->lexer->token->position + strlen(
                    $this->lexer->token->value
                )
            ) {
                $this->lexer->moveNext();
                continue;
            }

            // make sure the @ is followed by either a namespace separator, or
            // an identifier token
            $peek = $this->lexer->glimpse();
            if (
                ($peek === null)
                || ($peek->type !== DocLexer::T_NAMESPACE_SEPARATOR && ! in_array(
                    $peek->type,
                    self::$classIdentifiers,
                    true
                ))
                || $peek->position !== $this->lexer->lookahead->position + 1
            ) {
                $this->lexer->moveNext();
                continue;
            }

            $this->isNestedAnnotation = false;
            $annot                    = $this->Annotation();
            if ($annot === false) {
                continue;
            }

            $annotations[] = $annot;
        }

        return $annotations;
    }

    /**
     * Annotation     ::= "@" AnnotationName MethodCall
     * AnnotationName ::= QualifiedName | SimpleName
     * QualifiedName  ::= NameSpacePart "\" {NameSpacePart "\"}* SimpleName
     * NameSpacePart  ::= identifier | null | false | true
     * SimpleName     ::= identifier | null | false | true
     *
     * @return object|false False if it is not a valid annotation.
     *
     * @throws AnnotationException
     * @throws ReflectionException
     */
    private function Annotation()
    {
        $this->match(DocLexer::T_AT);

        // check if we have an annotation
        $name = $this->Identifier();

        if (
            $this->lexer->isNextToken(DocLexer::T_MINUS)
            && $this->lexer->nextTokenIsAdjacent()
        ) {
            // Annotations with dashes, such as "@foo-" or "@foo-bar", are to be discarded
            return false;
        }

        // only process names which are not fully qualified, yet
        // fully qualified names must start with a \
        $originalName = $name;

        if ($name[0] !== '\\') {
            $pos          = strpos($name, '\\');
            $alias        = ($pos === false) ? $name : substr($name, 0, $pos);
            $found        = false;
            $loweredAlias = strtolower($alias);

            if ($this->namespaces) {
                foreach ($this->namespaces as $namespace) {
                    if ($this->classExists($namespace . '\\' . $name)) {
                        $name  = $namespace . '\\' . $name;
                        $found = true;
                        break;
                    }
                }
            } elseif (isset($this->imports[$loweredAlias])) {
                $namespace = ltrim($this->imports[$loweredAlias], '\\');
                $name      = ($pos !== false)
                    ? $namespace . substr($name, $pos)
                    : $namespace;
                $found     = $this->classExists($name);
            } elseif (
                ! isset($this->ignoredAnnotationNames[$name])
                && isset($this->imports['__NAMESPACE__'])
                && $this->classExists($this->imports['__NAMESPACE__'] . '\\' . $name)
            ) {
                $name  = $this->imports['__NAMESPACE__'] . '\\' . $name;
                $found = true;
            } elseif (! isset($this->ignoredAnnotationNames[$name]) && $this->classExists($name)) {
                $found = true;
            }

            if (! $found) {
                if ($this->isIgnoredAnnotation($name)) {
                    return false;
                }

                throw AnnotationException::semanticalError(sprintf(
                    <<<'EXCEPTION'
The annotation "@%s" in %s was never imported. Did you maybe forget to add a "use" statement for this annotation?
EXCEPTION
                    ,
                    $name,
                    $this->context
                ));
            }
        }

        $name = ltrim($name, '\\');

        if (! $this->classExists($name)) {
            throw AnnotationException::semanticalError(sprintf(
                'The annotation "@%s" in %s does not exist, or could not be auto-loaded.',
                $name,
                $this->context
            ));
        }

        // at this point, $name contains the fully qualified class name of the
        // annotation, and it is also guaranteed that this class exists, and
        // that it is loaded

        // collects the metadata annotation only if there is not yet
        if (! isset(self::$annotationMetadata[$name])) {
            $this->collectAnnotationMetadata($name);
        }

        // verify that the class is really meant to be an annotation and not just any ordinary class
        if (self::$annotationMetadata[$name]['is_annotation'] === false) {
            if ($this->isIgnoredAnnotation($originalName) || $this->isIgnoredAnnotation($name)) {
                return false;
            }

            throw AnnotationException::semanticalError(sprintf(
                <<<'EXCEPTION'
The class "%s" is not annotated with @Annotation.
Are you sure this class can be used as annotation?
If so, then you need to add @Annotation to the _class_ doc comment of "%s".
If it is indeed no annotation, then you need to add @IgnoreAnnotation("%s") to the _class_ doc comment of %s.
EXCEPTION
                ,
                $name,
                $name,
                $originalName,
                $this->context
            ));
        }

        //if target is nested annotation
        $target = $this->isNestedAnnotation ? Target::TARGET_ANNOTATION : $this->target;

        // Next will be nested
        $this->isNestedAnnotation = true;

        //if annotation does not support current target
        if ((self::$annotationMetadata[$name]['targets'] & $target) === 0 && $target) {
            throw AnnotationException::semanticalError(
                sprintf(
                    <<<'EXCEPTION'
Annotation @%s is not allowed to be declared on %s. You may only use this annotation on these code elements: %s.
EXCEPTION
                    ,
                    $originalName,
                    $this->context,
                    self::$annotationMetadata[$name]['targets_literal']
                )
            );
        }

        $arguments = $this->MethodCall();
        $values    = $this->resolvePositionalValues($arguments, $name);

        if (isset(self::$annotationMetadata[$name]['enum'])) {
            // checks all declared attributes
            foreach (self::$annotationMetadata[$name]['enum'] as $property => $enum) {
                // checks if the attribute is a valid enumerator
                if (isset($values[$property]) && ! in_array($values[$property], $enum['value'])) {
                    throw AnnotationException::enumeratorError(
                        $property,
                        $name,
                        $this->context,
                        $enum['literal'],
                        $values[$property]
                    );
                }
            }
        }

        // checks all declared attributes
        foreach (self::$annotationMetadata[$name]['attribute_types'] as $property => $type) {
            if (
                $property === self::$annotationMetadata[$name]['default_property']
                && ! isset($values[$property]) && isset($values['value'])
            ) {
                $property = 'value';
            }

            // handle a not given attribute or null value
            if (! isset($values[$property])) {
                if ($type['required']) {
                    throw AnnotationException::requiredError(
                        $property,
                        $originalName,
                        $this->context,
                        'a(n) ' . $type['value']
                    );
                }

                continue;
            }

            if ($type['type'] === 'array') {
                // handle the case of a single value
                if (! is_array($values[$property])) {
                    $values[$property] = [$values[$property]];
                }

                // checks if the attribute has array type declaration, such as "array<string>"
                if (isset($type['array_type'])) {
                    foreach ($values[$property] as $item) {
                        if (gettype($item) !== $type['array_type'] && ! $item instanceof $type['array_type']) {
                            throw AnnotationException::attributeTypeError(
                                $property,
                                $originalName,
                                $this->context,
                                'either a(n) ' . $type['array_type'] . ', or an array of ' . $type['array_type'] . 's',
                                $item
                            );
                        }
                    }
                }
            } elseif (gettype($values[$property]) !== $type['type'] && ! $values[$property] instanceof $type['type']) {
                throw AnnotationException::attributeTypeError(
                    $property,
                    $originalName,
                    $this->context,
                    'a(n) ' . $type['value'],
                    $values[$property]
                );
            }
        }

        if (self::$annotationMetadata[$name]['has_named_argument_constructor']) {
            if (PHP_VERSION_ID >= 80000) {
                foreach ($values as $property => $value) {
                    if (! isset(self::$annotationMetadata[$name]['constructor_args'][$property])) {
                        throw AnnotationException::creationError(sprintf(
                            <<<'EXCEPTION'
The annotation @%s declared on %s does not have a property named "%s"
that can be set through its named arguments constructor.
Available named arguments: %s
EXCEPTION
                            ,
                            $originalName,
                            $this->context,
                            $property,
                            implode(', ', array_keys(self::$annotationMetadata[$name]['constructor_args']))
                        ));
                    }
                }

                return $this->instantiateAnnotiation($originalName, $this->context, $name, $values);
            }

            $positionalValues = [];
            foreach (self::$annotationMetadata[$name]['constructor_args'] as $property => $parameter) {
                $positionalValues[$parameter['position']] = $parameter['default'];
            }

            foreach ($values as $property => $value) {
                if (! isset(self::$annotationMetadata[$name]['constructor_args'][$property])) {
                    throw AnnotationException::creationError(sprintf(
                        <<<'EXCEPTION'
The annotation @%s declared on %s does not have a property named "%s"
that can be set through its named arguments constructor.
Available named arguments: %s
EXCEPTION
                        ,
                        $originalName,
                        $this->context,
                        $property,
                        implode(', ', array_keys(self::$annotationMetadata[$name]['constructor_args']))
                    ));
                }

                $positionalValues[self::$annotationMetadata[$name]['constructor_args'][$property]['position']] = $value;
            }

            return $this->instantiateAnnotiation($originalName, $this->context, $name, $positionalValues);
        }

        // check if the annotation expects values via the constructor,
        // or directly injected into public properties
        if (self::$annotationMetadata[$name]['has_constructor'] === true) {
            return $this->instantiateAnnotiation($originalName, $this->context, $name, [$values]);
        }

        $instance = $this->instantiateAnnotiation($originalName, $this->context, $name, []);

        foreach ($values as $property => $value) {
            if (! isset(self::$annotationMetadata[$name]['properties'][$property])) {
                if ($property !== 'value') {
                    throw AnnotationException::creationError(sprintf(
                        <<<'EXCEPTION'
The annotation @%s declared on %s does not have a property named "%s".
Available properties: %s
EXCEPTION
                        ,
                        $originalName,
                        $this->context,
                        $property,
                        implode(', ', self::$annotationMetadata[$name]['properties'])
                    ));
                }

                // handle the case if the property has no annotations
                $property = self::$annotationMetadata[$name]['default_property'];
                if (! $property) {
                    throw AnnotationException::creationError(sprintf(
                        'The annotation @%s declared on %s does not accept any values, but got %s.',
                        $originalName,
                        $this->context,
                        json_encode($values)
                    ));
                }
            }

            $instance->{$property} = $value;
        }

        return $instance;
    }

    /**
     * MethodCall ::= ["(" [Values] ")"]
     *
     * @psalm-return Arguments
     *
     * @throws AnnotationException
     * @throws ReflectionException
     */
    private function MethodCall(): array
    {
        $values = [];

        if (! $this->lexer->isNextToken(DocLexer::T_OPEN_PARENTHESIS)) {
            return $values;
        }

        $this->match(DocLexer::T_OPEN_PARENTHESIS);

        if (! $this->lexer->isNextToken(DocLexer::T_CLOSE_PARENTHESIS)) {
            $values = $this->Values();
        }

        $this->match(DocLexer::T_CLOSE_PARENTHESIS);

        return $values;
    }

    /**
     * Values ::= Array | Value {"," Value}* [","]
     *
     * @psalm-return Arguments
     *
     * @throws AnnotationException
     * @throws ReflectionException
     */
    private function Values(): array
    {
        $values = [$this->Value()];

        while ($this->lexer->isNextToken(DocLexer::T_COMMA)) {
            $this->match(DocLexer::T_COMMA);

            if ($this->lexer->isNextToken(DocLexer::T_CLOSE_PARENTHESIS)) {
                break;
            }

            $token = $this->lexer->lookahead;
            $value = $this->Value();

            $values[] = $value;
        }

        $namedArguments      = [];
        $positionalArguments = [];
        foreach ($values as $k => $value) {
            if (is_object($value) && $value instanceof stdClass) {
                $namedArguments[$value->name] = $value->value;
            } else {
                $positionalArguments[$k] = $value;
            }
        }

        return ['named_arguments' => $namedArguments, 'positional_arguments' => $positionalArguments];
    }

    /**
     * Constant ::= integer | string | float | boolean
     *
     * @return mixed
     *
     * @throws AnnotationException
     */
    private function Constant()
    {
        $identifier = $this->Identifier();

        if (! defined($identifier) && strpos($identifier, '::') !== false && $identifier[0] !== '\\') {
            [$className, $const] = explode('::', $identifier);

            $pos          = strpos($className, '\\');
            $alias        = ($pos === false) ? $className : substr($className, 0, $pos);
            $found        = false;
            $loweredAlias = strtolower($alias);

            switch (true) {
                case ! empty($this->namespaces):
                    foreach ($this->namespaces as $ns) {
                        if (class_exists($ns . '\\' . $className) || interface_exists($ns . '\\' . $className)) {
                            $className = $ns . '\\' . $className;
                            $found     = true;
                            break;
                        }
                    }

                    break;

                case isset($this->imports[$loweredAlias]):
                    $found     = true;
                    $className = ($pos !== false)
                        ? $this->imports[$loweredAlias] . substr($className, $pos)
                        : $this->imports[$loweredAlias];
                    break;

                default:
                    if (isset($this->imports['__NAMESPACE__'])) {
                        $ns = $this->imports['__NAMESPACE__'];

                        if (class_exists($ns . '\\' . $className) || interface_exists($ns . '\\' . $className)) {
                            $className = $ns . '\\' . $className;
                            $found     = true;
                        }
                    }

                    break;
            }

            if ($found) {
                $identifier = $className . '::' . $const;
            }
        }

        /**
         * Checks if identifier ends with ::class and remove the leading backslash if it exists.
         */
        if (
            $this->identifierEndsWithClassConstant($identifier) &&
            ! $this->identifierStartsWithBackslash($identifier)
        ) {
            return substr($identifier, 0, $this->getClassConstantPositionInIdentifier($identifier));
        }

        if ($this->identifierEndsWithClassConstant($identifier) && $this->identifierStartsWithBackslash($identifier)) {
            return substr($identifier, 1, $this->getClassConstantPositionInIdentifier($identifier) - 1);
        }

        if (! defined($identifier)) {
            throw AnnotationException::semanticalErrorConstants($identifier, $this->context);
        }

        return constant($identifier);
    }

    private function identifierStartsWithBackslash(string $identifier): bool
    {
        return $identifier[0] === '\\';
    }

    private function identifierEndsWithClassConstant(string $identifier): bool
    {
        return $this->getClassConstantPositionInIdentifier($identifier) === strlen($identifier) - strlen('::class');
    }

    /** @return int|false */
    private function getClassConstantPositionInIdentifier(string $identifier)
    {
        return stripos($identifier, '::class');
    }

    /**
     * Identifier ::= string
     *
     * @throws AnnotationException
     */
    private function Identifier(): string
    {
        // check if we have an annotation
        if (! $this->lexer->isNextTokenAny(self::$classIdentifiers)) {
            throw $this->syntaxError('namespace separator or identifier');
        }

        $this->lexer->moveNext();

        $className = $this->lexer->token->value;

        while (
            $this->lexer->lookahead !== null &&
            $this->lexer->lookahead->position === ($this->lexer->token->position +
            strlen($this->lexer->token->value)) &&
            $this->lexer->isNextToken(DocLexer::T_NAMESPACE_SEPARATOR)
        ) {
            $this->match(DocLexer::T_NAMESPACE_SEPARATOR);
            $this->matchAny(self::$classIdentifiers);

            $className .= '\\' . $this->lexer->token->value;
        }

        return $className;
    }

    /**
     * Value ::= PlainValue | FieldAssignment
     *
     * @return mixed
     *
     * @throws AnnotationException
     * @throws ReflectionException
     */
    private function Value()
    {
        $peek = $this->lexer->glimpse();

        if ($peek->type === DocLexer::T_EQUALS) {
            return $this->FieldAssignment();
        }

        return $this->PlainValue();
    }

    /**
     * PlainValue ::= integer | string | float | boolean | Array | Annotation
     *
     * @return mixed
     *
     * @throws AnnotationException
     * @throws ReflectionException
     */
    private function PlainValue()
    {
        if ($this->lexer->isNextToken(DocLexer::T_OPEN_CURLY_BRACES)) {
            return $this->Arrayx();
        }

        if ($this->lexer->isNextToken(DocLexer::T_AT)) {
            return $this->Annotation();
        }

        if ($this->lexer->isNextToken(DocLexer::T_IDENTIFIER)) {
            return $this->Constant();
        }

        switch ($this->lexer->lookahead->type) {
            case DocLexer::T_STRING:
                $this->match(DocLexer::T_STRING);

                return $this->lexer->token->value;

            case DocLexer::T_INTEGER:
                $this->match(DocLexer::T_INTEGER);

                return (int) $this->lexer->token->value;

            case DocLexer::T_FLOAT:
                $this->match(DocLexer::T_FLOAT);

                return (float) $this->lexer->token->value;

            case DocLexer::T_TRUE:
                $this->match(DocLexer::T_TRUE);

                return true;

            case DocLexer::T_FALSE:
                $this->match(DocLexer::T_FALSE);

                return false;

            case DocLexer::T_NULL:
                $this->match(DocLexer::T_NULL);

                return null;

            default:
                throw $this->syntaxError('PlainValue');
        }
    }

    /**
     * FieldAssignment ::= FieldName "=" PlainValue
     * FieldName ::= identifier
     *
     * @throws AnnotationException
     * @throws ReflectionException
     */
    private function FieldAssignment(): stdClass
    {
        $this->match(DocLexer::T_IDENTIFIER);
        $fieldName = $this->lexer->token->value;

        $this->match(DocLexer::T_EQUALS);

        $item        = new stdClass();
        $item->name  = $fieldName;
        $item->value = $this->PlainValue();

        return $item;
    }

    /**
     * Array ::= "{" ArrayEntry {"," ArrayEntry}* [","] "}"
     *
     * @return mixed[]
     *
     * @throws AnnotationException
     * @throws ReflectionException
     */
    private function Arrayx(): array
    {
        $array = $values = [];

        $this->match(DocLexer::T_OPEN_CURLY_BRACES);

        // If the array is empty, stop parsing and return.
        if ($this->lexer->isNextToken(DocLexer::T_CLOSE_CURLY_BRACES)) {
            $this->match(DocLexer::T_CLOSE_CURLY_BRACES);

            return $array;
        }

        $values[] = $this->ArrayEntry();

        while ($this->lexer->isNextToken(DocLexer::T_COMMA)) {
            $this->match(DocLexer::T_COMMA);

            // optional trailing comma
            if ($this->lexer->isNextToken(DocLexer::T_CLOSE_CURLY_BRACES)) {
                break;
            }

            $values[] = $this->ArrayEntry();
        }

        $this->match(DocLexer::T_CLOSE_CURLY_BRACES);

        foreach ($values as $value) {
            [$key, $val] = $value;

            if ($key !== null) {
                $array[$key] = $val;
            } else {
                $array[] = $val;
            }
        }

        return $array;
    }

    /**
     * ArrayEntry ::= Value | KeyValuePair
     * KeyValuePair ::= Key ("=" | ":") PlainValue | Constant
     * Key ::= string | integer | Constant
     *
     * @phpstan-return array{mixed, mixed}
     *
     * @throws AnnotationException
     * @throws ReflectionException
     */
    private function ArrayEntry(): array
    {
        $peek = $this->lexer->glimpse();

        if (
            $peek->type === DocLexer::T_EQUALS
                || $peek->type === DocLexer::T_COLON
        ) {
            if ($this->lexer->isNextToken(DocLexer::T_IDENTIFIER)) {
                $key = $this->Constant();
            } else {
                $this->matchAny([DocLexer::T_INTEGER, DocLexer::T_STRING]);
                $key = $this->lexer->token->value;
            }

            $this->matchAny([DocLexer::T_EQUALS, DocLexer::T_COLON]);

            return [$key, $this->PlainValue()];
        }

        return [null, $this->Value()];
    }

    /**
     * Checks whether the given $name matches any ignored annotation name or namespace
     */
    private function isIgnoredAnnotation(string $name): bool
    {
        if ($this->ignoreNotImportedAnnotations || isset($this->ignoredAnnotationNames[$name])) {
            return true;
        }

        foreach (array_keys($this->ignoredAnnotationNamespaces) as $ignoredAnnotationNamespace) {
            $ignoredAnnotationNamespace = rtrim($ignoredAnnotationNamespace, '\\') . '\\';

            if (stripos(rtrim($name, '\\') . '\\', $ignoredAnnotationNamespace) === 0) {
                return true;
            }
        }

        return false;
    }

    /**
     * Resolve positional arguments (without name) to named ones
     *
     * @psalm-param Arguments $arguments
     *
     * @return array<string,mixed>
     */
    private function resolvePositionalValues(array $arguments, string $name): array
    {
        $positionalArguments = $arguments['positional_arguments'] ?? [];
        $values              = $arguments['named_arguments'] ?? [];

        if (
            self::$annotationMetadata[$name]['has_named_argument_constructor']
            && self::$annotationMetadata[$name]['default_property'] !== null
        ) {
            // We must ensure that we don't have positional arguments after named ones
            $positions    = array_keys($positionalArguments);
            $lastPosition = null;
            foreach ($positions as $position) {
                if (
                    ($lastPosition === null && $position !== 0) ||
                    ($lastPosition !== null && $position !== $lastPosition + 1)
                ) {
                    throw $this->syntaxError('Positional arguments after named arguments is not allowed');
                }

                $lastPosition = $position;
            }

            foreach (self::$annotationMetadata[$name]['constructor_args'] as $property => $parameter) {
                $position = $parameter['position'];
                if (isset($values[$property]) || ! isset($positionalArguments[$position])) {
                    continue;
                }

                $values[$property] = $positionalArguments[$position];
            }
        } else {
            if (count($positionalArguments) > 0 && ! isset($values['value'])) {
                if (count($positionalArguments) === 1) {
                    $value = array_pop($positionalArguments);
                } else {
                    $value = array_values($positionalArguments);
                }

                $values['value'] = $value;
            }
        }

        return $values;
    }

    /**
     * Try to instantiate the annotation and catch and process any exceptions related to failure
     *
     * @param class-string        $name
     * @param array<string,mixed> $arguments
     *
     * @return object
     *
     * @throws AnnotationException
     */
    private function instantiateAnnotiation(string $originalName, string $context, string $name, array $arguments)
    {
        try {
            return new $name(...$arguments);
        } catch (Throwable $exception) {
            throw AnnotationException::creationError(
                sprintf(
                    'An error occurred while instantiating the annotation @%s declared on %s: "%s".',
                    $originalName,
                    $context,
                    $exception->getMessage()
                ),
                $exception
            );
        }
    }
}
<?php

namespace Doctrine\Common\Annotations;

use Doctrine\Common\Annotations\Annotation\IgnoreAnnotation;
use Doctrine\Common\Annotations\Annotation\Target;
use ReflectionClass;
use ReflectionFunction;
use ReflectionMethod;
use ReflectionProperty;

use function array_merge;
use function class_exists;
use function extension_loaded;
use function filter_var;
use function ini_get;

use const FILTER_VALIDATE_BOOLEAN;

/**
 * A reader for docblock annotations.
 */
class AnnotationReader implements Reader
{
    /**
     * Global map for imports.
     *
     * @var array<string, class-string>
     */
    private static $globalImports = [
        'ignoreannotation' => Annotation\IgnoreAnnotation::class,
    ];

    /**
     * A list with annotations that are not causing exceptions when not resolved to an annotation class.
     *
     * The names are case sensitive.
     *
     * @var array<string, true>
     */
    private static $globalIgnoredNames = ImplicitlyIgnoredAnnotationNames::LIST;

    /**
     * A list with annotations that are not causing exceptions when not resolved to an annotation class.
     *
     * The names are case sensitive.
     *
     * @var array<string, true>
     */
    private static $globalIgnoredNamespaces = [];

    /**
     * Add a new annotation to the globally ignored annotation names with regard to exception handling.
     */
    public static function addGlobalIgnoredName(string $name)
    {
        self::$globalIgnoredNames[$name] = true;
    }

    /**
     * Add a new annotation to the globally ignored annotation namespaces with regard to exception handling.
     */
    public static function addGlobalIgnoredNamespace(string $namespace)
    {
        self::$globalIgnoredNamespaces[$namespace] = true;
    }

    /**
     * Annotations parser.
     *
     * @var DocParser
     */
    private $parser;

    /**
     * Annotations parser used to collect parsing metadata.
     *
     * @var DocParser
     */
    private $preParser;

    /**
     * PHP parser used to collect imports.
     *
     * @var PhpParser
     */
    private $phpParser;

    /**
     * In-memory cache mechanism to store imported annotations per class.
     *
     * @psalm-var array<'class'|'function', array<string, array<string, class-string>>>
     */
    private $imports = [];

    /**
     * In-memory cache mechanism to store ignored annotations per class.
     *
     * @psalm-var array<'class'|'function', array<string, array<string, true>>>
     */
    private $ignoredAnnotationNames = [];

    /**
     * Initializes a new AnnotationReader.
     *
     * @throws AnnotationException
     */
    public function __construct(?DocParser $parser = null)
    {
        if (
            extension_loaded('Zend Optimizer+') &&
            (filter_var(ini_get('zend_optimizerplus.save_comments'), FILTER_VALIDATE_BOOLEAN)  === false ||
            filter_var(ini_get('opcache.save_comments'), FILTER_VALIDATE_BOOLEAN) === false)
        ) {
            throw AnnotationException::optimizerPlusSaveComments();
        }

        if (
            extension_loaded('Zend OPcache') &&
            filter_var(ini_get('opcache.save_comments'), FILTER_VALIDATE_BOOLEAN) === false
        ) {
            throw AnnotationException::optimizerPlusSaveComments();
        }

        // Make sure that the IgnoreAnnotation annotation is loaded
        class_exists(IgnoreAnnotation::class);

        $this->parser = $parser ?: new DocParser();

        $this->preParser = new DocParser();

        $this->preParser->setImports(self::$globalImports);
        $this->preParser->setIgnoreNotImportedAnnotations(true);
        $this->preParser->setIgnoredAnnotationNames(self::$globalIgnoredNames);

        $this->phpParser = new PhpParser();
    }

    /**
     * {@inheritDoc}
     */
    public function getClassAnnotations(ReflectionClass $class)
    {
        $this->parser->setTarget(Target::TARGET_CLASS);
        $this->parser->setImports($this->getImports($class));
        $this->parser->setIgnoredAnnotationNames($this->getIgnoredAnnotationNames($class));
        $this->parser->setIgnoredAnnotationNamespaces(self::$globalIgnoredNamespaces);

        return $this->parser->parse($class->getDocComment(), 'class ' . $class->getName());
    }

    /**
     * {@inheritDoc}
     */
    public function getClassAnnotation(ReflectionClass $class, $annotationName)
    {
        $annotations = $this->getClassAnnotations($class);

        foreach ($annotations as $annotation) {
            if ($annotation instanceof $annotationName) {
                return $annotation;
            }
        }

        return null;
    }

    /**
     * {@inheritDoc}
     */
    public function getPropertyAnnotations(ReflectionProperty $property)
    {
        $class   = $property->getDeclaringClass();
        $context = 'property ' . $class->getName() . '::$' . $property->getName();

        $this->parser->setTarget(Target::TARGET_PROPERTY);
        $this->parser->setImports($this->getPropertyImports($property));
        $this->parser->setIgnoredAnnotationNames($this->getIgnoredAnnotationNames($class));
        $this->parser->setIgnoredAnnotationNamespaces(self::$globalIgnoredNamespaces);

        return $this->parser->parse($property->getDocComment(), $context);
    }

    /**
     * {@inheritDoc}
     */
    public function getPropertyAnnotation(ReflectionProperty $property, $annotationName)
    {
        $annotations = $this->getPropertyAnnotations($property);

        foreach ($annotations as $annotation) {
            if ($annotation instanceof $annotationName) {
                return $annotation;
            }
        }

        return null;
    }

    /**
     * {@inheritDoc}
     */
    public function getMethodAnnotations(ReflectionMethod $method)
    {
        $class   = $method->getDeclaringClass();
        $context = 'method ' . $class->getName() . '::' . $method->getName() . '()';

        $this->parser->setTarget(Target::TARGET_METHOD);
        $this->parser->setImports($this->getMethodImports($method));
        $this->parser->setIgnoredAnnotationNames($this->getIgnoredAnnotationNames($class));
        $this->parser->setIgnoredAnnotationNamespaces(self::$globalIgnoredNamespaces);

        return $this->parser->parse($method->getDocComment(), $context);
    }

    /**
     * {@inheritDoc}
     */
    public function getMethodAnnotation(ReflectionMethod $method, $annotationName)
    {
        $annotations = $this->getMethodAnnotations($method);

        foreach ($annotations as $annotation) {
            if ($annotation instanceof $annotationName) {
                return $annotation;
            }
        }

        return null;
    }

    /**
     * Gets the annotations applied to a function.
     *
     * @phpstan-return list<object> An array of Annotations.
     */
    public function getFunctionAnnotations(ReflectionFunction $function): array
    {
        $context = 'function ' . $function->getName();

        $this->parser->setTarget(Target::TARGET_FUNCTION);
        $this->parser->setImports($this->getImports($function));
        $this->parser->setIgnoredAnnotationNames($this->getIgnoredAnnotationNames($function));
        $this->parser->setIgnoredAnnotationNamespaces(self::$globalIgnoredNamespaces);

        return $this->parser->parse($function->getDocComment(), $context);
    }

    /**
     * Gets a function annotation.
     *
     * @return object|null The Annotation or NULL, if the requested annotation does not exist.
     */
    public function getFunctionAnnotation(ReflectionFunction $function, string $annotationName)
    {
        $annotations = $this->getFunctionAnnotations($function);

        foreach ($annotations as $annotation) {
            if ($annotation instanceof $annotationName) {
                return $annotation;
            }
        }

        return null;
    }

    /**
     * Returns the ignored annotations for the given class or function.
     *
     * @param ReflectionClass|ReflectionFunction $reflection
     *
     * @return array<string, true>
     */
    private function getIgnoredAnnotationNames($reflection): array
    {
        $type = $reflection instanceof ReflectionClass ? 'class' : 'function';
        $name = $reflection->getName();

        if (isset($this->ignoredAnnotationNames[$type][$name])) {
            return $this->ignoredAnnotationNames[$type][$name];
        }

        $this->collectParsingMetadata($reflection);

        return $this->ignoredAnnotationNames[$type][$name];
    }

    /**
     * Retrieves imports for a class or a function.
     *
     * @param ReflectionClass|ReflectionFunction $reflection
     *
     * @return array<string, class-string>
     */
    private function getImports($reflection): array
    {
        $type = $reflection instanceof ReflectionClass ? 'class' : 'function';
        $name = $reflection->getName();

        if (isset($this->imports[$type][$name])) {
            return $this->imports[$type][$name];
        }

        $this->collectParsingMetadata($reflection);

        return $this->imports[$type][$name];
    }

    /**
     * Retrieves imports for methods.
     *
     * @return array<string, class-string>
     */
    private function getMethodImports(ReflectionMethod $method)
    {
        $class        = $method->getDeclaringClass();
        $classImports = $this->getImports($class);

        $traitImports = [];

        foreach ($class->getTraits() as $trait) {
            if (
                ! $trait->hasMethod($method->getName())
                || $trait->getFileName() !== $method->getFileName()
            ) {
                continue;
            }

            $traitImports = array_merge($traitImports, $this->phpParser->parseUseStatements($trait));
        }

        return array_merge($classImports, $traitImports);
    }

    /**
     * Retrieves imports for properties.
     *
     * @return array<string, class-string>
     */
    private function getPropertyImports(ReflectionProperty $property)
    {
        $class        = $property->getDeclaringClass();
        $classImports = $this->getImports($class);

        $traitImports = [];

        foreach ($class->getTraits() as $trait) {
            if (! $trait->hasProperty($property->getName())) {
                continue;
            }

            $traitImports = array_merge($traitImports, $this->phpParser->parseUseStatements($trait));
        }

        return array_merge($classImports, $traitImports);
    }

    /**
     * Collects parsing metadata for a given class or function.
     *
     * @param ReflectionClass|ReflectionFunction $reflection
     */
    private function collectParsingMetadata($reflection): void
    {
        $type = $reflection instanceof ReflectionClass ? 'class' : 'function';
        $name = $reflection->getName();

        $ignoredAnnotationNames = self::$globalIgnoredNames;
        $annotations            = $this->preParser->parse($reflection->getDocComment(), $type . ' ' . $name);

        foreach ($annotations as $annotation) {
            if (! ($annotation instanceof IgnoreAnnotation)) {
                continue;
            }

            foreach ($annotation->names as $annot) {
                $ignoredAnnotationNames[$annot] = true;
            }
        }

        $this->imports[$type][$name] = array_merge(
            self::$globalImports,
            $this->phpParser->parseUseStatements($reflection),
            [
                '__NAMESPACE__' => $reflection->getNamespaceName(),
                'self' => $name,
            ]
        );

        $this->ignoredAnnotationNames[$type][$name] = $ignoredAnnotationNames;
    }
}
<?php

namespace Doctrine\Common\Annotations;

use function array_key_exists;
use function class_exists;

final class AnnotationRegistry
{
    /**
     * An array of classes which cannot be found
     *
     * @var null[] indexed by class name
     */
    private static $failedToAutoload = [];

    public static function reset(): void
    {
        self::$failedToAutoload = [];
    }

    /**
     * Autoloads an annotation class silently.
     */
    public static function loadAnnotationClass(string $class): bool
    {
        if (class_exists($class, false)) {
            return true;
        }

        if (array_key_exists($class, self::$failedToAutoload)) {
            return false;
        }

        if (class_exists($class)) {
            return true;
        }

        self::$failedToAutoload[$class] = null;

        return false;
    }
}
<?php

namespace Doctrine\Common\Annotations;

use ReflectionClass;
use ReflectionFunction;
use SplFileObject;

use function is_file;
use function method_exists;
use function preg_quote;
use function preg_replace;

/**
 * Parses a file for namespaces/use/class declarations.
 */
final class PhpParser
{
    /**
     * Parse a class or function for use statements.
     *
     * @param ReflectionClass|ReflectionFunction $reflection
     *
     * @psalm-return array<string, string> a list with use statements in the form (Alias => FQN).
     */
    public function parseUseStatements($reflection): array
    {
        if (method_exists($reflection, 'getUseStatements')) {
            return $reflection->getUseStatements();
        }

        $filename = $reflection->getFileName();

        if ($filename === false) {
            return [];
        }

        $content = $this->getFileContent($filename, $reflection->getStartLine());

        if ($content === null) {
            return [];
        }

        $namespace = preg_quote($reflection->getNamespaceName());
        $content   = preg_replace('/^.*?(\bnamespace\s+' . $namespace . '\s*[;{].*)$/s', '\\1', $content);
        $tokenizer = new TokenParser('<?php ' . $content);

        return $tokenizer->parseUseStatements($reflection->getNamespaceName());
    }

    /**
     * Gets the content of the file right up to the given line number.
     *
     * @param string $filename   The name of the file to load.
     * @param int    $lineNumber The number of lines to read from file.
     *
     * @return string|null The content of the file or null if the file does not exist.
     */
    private function getFileContent(string $filename, $lineNumber)
    {
        if (! is_file($filename)) {
            return null;
        }

        $content = '';
        $lineCnt = 0;
        $file    = new SplFileObject($filename);
        while (! $file->eof()) {
            if ($lineCnt++ === $lineNumber) {
                break;
            }

            $content .= $file->fgets();
        }

        return $content;
    }
}
<?php

namespace Doctrine\Common\Annotations;

use ReflectionClass;
use ReflectionMethod;
use ReflectionProperty;

/**
 * Interface for annotation readers.
 */
interface Reader
{
    /**
     * Gets the annotations applied to a class.
     *
     * @param ReflectionClass $class The ReflectionClass of the class from which
     * the class annotations should be read.
     *
     * @return array<object> An array of Annotations.
     */
    public function getClassAnnotations(ReflectionClass $class);

    /**
     * Gets a class annotation.
     *
     * @param ReflectionClass $class          The ReflectionClass of the class from which
     *          the class annotations should be read.
     * @param class-string<T> $annotationName The name of the annotation.
     *
     * @return T|null The Annotation or NULL, if the requested annotation does not exist.
     *
     * @template T
     */
    public function getClassAnnotation(ReflectionClass $class, $annotationName);

    /**
     * Gets the annotations applied to a method.
     *
     * @param ReflectionMethod $method The ReflectionMethod of the method from which
     * the annotations should be read.
     *
     * @return array<object> An array of Annotations.
     */
    public function getMethodAnnotations(ReflectionMethod $method);

    /**
     * Gets a method annotation.
     *
     * @param ReflectionMethod $method         The ReflectionMethod to read the annotations from.
     * @param class-string<T>  $annotationName The name of the annotation.
     *
     * @return T|null The Annotation or NULL, if the requested annotation does not exist.
     *
     * @template T
     */
    public function getMethodAnnotation(ReflectionMethod $method, $annotationName);

    /**
     * Gets the annotations applied to a property.
     *
     * @param ReflectionProperty $property The ReflectionProperty of the property
     * from which the annotations should be read.
     *
     * @return array<object> An array of Annotations.
     */
    public function getPropertyAnnotations(ReflectionProperty $property);

    /**
     * Gets a property annotation.
     *
     * @param ReflectionProperty $property       The ReflectionProperty to read the annotations from.
     * @param class-string<T>    $annotationName The name of the annotation.
     *
     * @return T|null The Annotation or NULL, if the requested annotation does not exist.
     *
     * @template T
     */
    public function getPropertyAnnotation(ReflectionProperty $property, $annotationName);
}
<?php

namespace Doctrine\Common\Annotations;

use function array_merge;
use function count;
use function explode;
use function strtolower;
use function token_get_all;

use const PHP_VERSION_ID;
use const T_AS;
use const T_COMMENT;
use const T_DOC_COMMENT;
use const T_NAME_FULLY_QUALIFIED;
use const T_NAME_QUALIFIED;
use const T_NAMESPACE;
use const T_NS_SEPARATOR;
use const T_STRING;
use const T_USE;
use const T_WHITESPACE;

/**
 * Parses a file for namespaces/use/class declarations.
 */
class TokenParser
{
    /**
     * The token list.
     *
     * @phpstan-var list<mixed[]>
     */
    private $tokens;

    /**
     * The number of tokens.
     *
     * @var int
     */
    private $numTokens;

    /**
     * The current array pointer.
     *
     * @var int
     */
    private $pointer = 0;

    public function __construct(string $contents)
    {
        $this->tokens = token_get_all($contents);

        // The PHP parser sets internal compiler globals for certain things. Annoyingly, the last docblock comment it
        // saw gets stored in doc_comment. When it comes to compile the next thing to be include()d this stored
        // doc_comment becomes owned by the first thing the compiler sees in the file that it considers might have a
        // docblock. If the first thing in the file is a class without a doc block this would cause calls to
        // getDocBlock() on said class to return our long lost doc_comment. Argh.
        // To workaround, cause the parser to parse an empty docblock. Sure getDocBlock() will return this, but at least
        // it's harmless to us.
        token_get_all("<?php\n/**\n *\n */");

        $this->numTokens = count($this->tokens);
    }

    /**
     * Gets the next non whitespace and non comment token.
     *
     * @param bool $docCommentIsComment If TRUE then a doc comment is considered a comment and skipped.
     * If FALSE then only whitespace and normal comments are skipped.
     *
     * @return mixed[]|string|null The token if exists, null otherwise.
     */
    public function next(bool $docCommentIsComment = true)
    {
        for ($i = $this->pointer; $i < $this->numTokens; $i++) {
            $this->pointer++;
            if (
                $this->tokens[$i][0] === T_WHITESPACE ||
                $this->tokens[$i][0] === T_COMMENT ||
                ($docCommentIsComment && $this->tokens[$i][0] === T_DOC_COMMENT)
            ) {
                continue;
            }

            return $this->tokens[$i];
        }

        return null;
    }

    /**
     * Parses a single use statement.
     *
     * @return array<string, string> A list with all found class names for a use statement.
     */
    public function parseUseStatement()
    {
        $groupRoot     = '';
        $class         = '';
        $alias         = '';
        $statements    = [];
        $explicitAlias = false;
        while (($token = $this->next())) {
            if (! $explicitAlias && $token[0] === T_STRING) {
                $class .= $token[1];
                $alias  = $token[1];
            } elseif ($explicitAlias && $token[0] === T_STRING) {
                $alias = $token[1];
            } elseif (
                PHP_VERSION_ID >= 80000 &&
                ($token[0] === T_NAME_QUALIFIED || $token[0] === T_NAME_FULLY_QUALIFIED)
            ) {
                $class .= $token[1];

                $classSplit = explode('\\', $token[1]);
                $alias      = $classSplit[count($classSplit) - 1];
            } elseif ($token[0] === T_NS_SEPARATOR) {
                $class .= '\\';
                $alias  = '';
            } elseif ($token[0] === T_AS) {
                $explicitAlias = true;
                $alias         = '';
            } elseif ($token === ',') {
                $statements[strtolower($alias)] = $groupRoot . $class;
                $class                          = '';
                $alias                          = '';
                $explicitAlias                  = false;
            } elseif ($token === ';') {
                $statements[strtolower($alias)] = $groupRoot . $class;
                break;
            } elseif ($token === '{') {
                $groupRoot = $class;
                $class     = '';
            } elseif ($token === '}') {
                continue;
            } else {
                break;
            }
        }

        return $statements;
    }

    /**
     * Gets all use statements.
     *
     * @param string $namespaceName The namespace name of the reflected class.
     *
     * @return array<string, string> A list with all found use statements.
     */
    public function parseUseStatements(string $namespaceName)
    {
        $statements = [];
        while (($token = $this->next())) {
            if ($token[0] === T_USE) {
                $statements = array_merge($statements, $this->parseUseStatement());
                continue;
            }

            if ($token[0] !== T_NAMESPACE || $this->parseNamespace() !== $namespaceName) {
                continue;
            }

            // Get fresh array for new namespace. This is to prevent the parser to collect the use statements
            // for a previous namespace with the same name. This is the case if a namespace is defined twice
            // or if a namespace with the same name is commented out.
            $statements = [];
        }

        return $statements;
    }

    /**
     * Gets the namespace.
     *
     * @return string The found namespace.
     */
    public function parseNamespace()
    {
        $name = '';
        while (
            ($token = $this->next()) && ($token[0] === T_STRING || $token[0] === T_NS_SEPARATOR || (
            PHP_VERSION_ID >= 80000 &&
            ($token[0] === T_NAME_QUALIFIED || $token[0] === T_NAME_FULLY_QUALIFIED)
            ))
        ) {
            $name .= $token[1];
        }

        return $name;
    }

    /**
     * Gets the class name.
     *
     * @return string The found class name.
     */
    public function parseClass()
    {
        // Namespaces and class names are tokenized the same: T_STRINGs
        // separated by T_NS_SEPARATOR so we can use one function to provide
        // both.
        return $this->parseNamespace();
    }
}
<?php

namespace Doctrine\Common\Annotations;

use BadMethodCallException;

use function sprintf;

/**
 * Annotations class.
 */
class Annotation
{
    /**
     * Value property. Common among all derived classes.
     *
     * @var mixed
     */
    public $value;

    /** @param array<string, mixed> $data Key-value for properties to be defined in this class. */
    final public function __construct(array $data)
    {
        foreach ($data as $key => $value) {
            $this->$key = $value;
        }
    }

    /**
     * Error handler for unknown property accessor in Annotation class.
     *
     * @throws BadMethodCallException
     */
    public function __get(string $name)
    {
        throw new BadMethodCallException(
            sprintf("Unknown property '%s' on annotation '%s'.", $name, static::class)
        );
    }

    /**
     * Error handler for unknown property mutator in Annotation class.
     *
     * @param mixed $value Property value.
     *
     * @throws BadMethodCallException
     */
    public function __set(string $name, $value)
    {
        throw new BadMethodCallException(
            sprintf("Unknown property '%s' on annotation '%s'.", $name, static::class)
        );
    }
}
<?php

declare(strict_types=1);

namespace Doctrine\Common\Annotations;

/**
 *  A list of annotations that are implicitly ignored during the parsing process.
 *
 *  All names are case sensitive.
 */
final class ImplicitlyIgnoredAnnotationNames
{
    private const Reserved = [
        'Annotation'               => true,
        'Attribute'                => true,
        'Attributes'               => true,
        /* Can we enable this? 'Enum' => true, */
        'Required'                 => true,
        'Target'                   => true,
        'NamedArgumentConstructor' => true,
    ];

    private const WidelyUsedNonStandard = [
        'fix'      => true,
        'fixme'    => true,
        'override' => true,
    ];

    private const PhpDocumentor1 = [
        'abstract'   => true,
        'access'     => true,
        'code'       => true,
        'deprec'     => true,
        'endcode'    => true,
        'exception'  => true,
        'final'      => true,
        'ingroup'    => true,
        'inheritdoc' => true,
        'inheritDoc' => true,
        'magic'      => true,
        'name'       => true,
        'private'    => true,
        'static'     => true,
        'staticvar'  => true,
        'staticVar'  => true,
        'toc'        => true,
        'tutorial'   => true,
        'throw'      => true,
    ];

    private const PhpDocumentor2 = [
        'api'            => true,
        'author'         => true,
        'category'       => true,
        'copyright'      => true,
        'deprecated'     => true,
        'example'        => true,
        'filesource'     => true,
        'global'         => true,
        'ignore'         => true,
        /* Can we enable this? 'index' => true, */
        'internal'       => true,
        'license'        => true,
        'link'           => true,
        'method'         => true,
        'package'        => true,
        'param'          => true,
        'property'       => true,
        'property-read'  => true,
        'property-write' => true,
        'return'         => true,
        'see'            => true,
        'since'          => true,
        'source'         => true,
        'subpackage'     => true,
        'throws'         => true,
        'todo'           => true,
        'TODO'           => true,
        'usedby'         => true,
        'uses'           => true,
        'var'            => true,
        'version'        => true,
    ];

    private const PHPUnit = [
        'author'                         => true,
        'after'                          => true,
        'afterClass'                     => true,
        'backupGlobals'                  => true,
        'backupStaticAttributes'         => true,
        'before'                         => true,
        'beforeClass'                    => true,
        'codeCoverageIgnore'             => true,
        'codeCoverageIgnoreStart'        => true,
        'codeCoverageIgnoreEnd'          => true,
        'covers'                         => true,
        'coversDefaultClass'             => true,
        'coversNothing'                  => true,
        'dataProvider'                   => true,
        'depends'                        => true,
        'doesNotPerformAssertions'       => true,
        'expectedException'              => true,
        'expectedExceptionCode'          => true,
        'expectedExceptionMessage'       => true,
        'expectedExceptionMessageRegExp' => true,
        'group'                          => true,
        'large'                          => true,
        'medium'                         => true,
        'preserveGlobalState'            => true,
        'requires'                       => true,
        'runTestsInSeparateProcesses'    => true,
        'runInSeparateProcess'           => true,
        'small'                          => true,
        'test'                           => true,
        'testdox'                        => true,
        'testWith'                       => true,
        'ticket'                         => true,
        'uses'                           => true,
    ];

    private const PhpCheckStyle = ['SuppressWarnings' => true];

    private const PhpStorm = ['noinspection' => true];

    private const PEAR = ['package_version' => true];

    private const PlainUML = [
        'startuml' => true,
        'enduml'   => true,
    ];

    private const Symfony = ['experimental' => true];

    private const PhpCodeSniffer = [
        'codingStandardsIgnoreStart' => true,
        'codingStandardsIgnoreEnd'   => true,
    ];

    private const SlevomatCodingStandard = ['phpcsSuppress' => true];

    private const Phan = ['suppress' => true];

    private const Rector = ['noRector' => true];

    private const StaticAnalysis = [
        // PHPStan, Psalm
        'extends' => true,
        'implements' => true,
        'readonly' => true,
        'template' => true,
        'use' => true,

        // Psalm
        'pure' => true,
        'immutable' => true,
    ];

    public const LIST = self::Reserved
        + self::WidelyUsedNonStandard
        + self::PhpDocumentor1
        + self::PhpDocumentor2
        + self::PHPUnit
        + self::PhpCheckStyle
        + self::PhpStorm
        + self::PEAR
        + self::PlainUML
        + self::Symfony
        + self::SlevomatCodingStandard
        + self::PhpCodeSniffer
        + self::Phan
        + self::Rector
        + self::StaticAnalysis;

    private function __construct()
    {
    }
}
<?php

namespace Doctrine\Common\Annotations\Annotation;

/**
 * Annotation that can be used to signal to the parser
 * to check the attribute type during the parsing process.
 *
 * @Annotation
 */
final class Attribute
{
    /** @var string */
    public $name;

    /** @var string */
    public $type;

    /** @var bool */
    public $required = false;
}
<?php

namespace Doctrine\Common\Annotations\Annotation;

/**
 * Annotation that indicates that the annotated class should be constructed with a named argument call.
 *
 * @Annotation
 * @Target("CLASS")
 */
final class NamedArgumentConstructor
{
}
<?php

namespace Doctrine\Common\Annotations\Annotation;

/**
 * Annotation that can be used to signal to the parser
 * to check the types of all declared attributes during the parsing process.
 *
 * @Annotation
 */
final class Attributes
{
    /** @var array<Attribute> */
    public $value;
}
<?php

namespace Doctrine\Common\Annotations\Annotation;

use InvalidArgumentException;

use function array_keys;
use function get_class;
use function gettype;
use function implode;
use function is_array;
use function is_object;
use function is_string;
use function sprintf;

/**
 * Annotation that can be used to signal to the parser
 * to check the annotation target during the parsing process.
 *
 * @Annotation
 */
final class Target
{
    public const TARGET_CLASS      = 1;
    public const TARGET_METHOD     = 2;
    public const TARGET_PROPERTY   = 4;
    public const TARGET_ANNOTATION = 8;
    public const TARGET_FUNCTION   = 16;
    public const TARGET_ALL        = 31;

    /** @var array<string, int> */
    private static $map = [
        'ALL'        => self::TARGET_ALL,
        'CLASS'      => self::TARGET_CLASS,
        'METHOD'     => self::TARGET_METHOD,
        'PROPERTY'   => self::TARGET_PROPERTY,
        'FUNCTION'   => self::TARGET_FUNCTION,
        'ANNOTATION' => self::TARGET_ANNOTATION,
    ];

    /** @phpstan-var list<string> */
    public $value;

    /**
     * Targets as bitmask.
     *
     * @var int
     */
    public $targets;

    /**
     * Literal target declaration.
     *
     * @var string
     */
    public $literal;

    /**
     * @phpstan-param array{value?: string|list<string>} $values
     *
     * @throws InvalidArgumentException
     */
    public function __construct(array $values)
    {
        if (! isset($values['value'])) {
            $values['value'] = null;
        }

        if (is_string($values['value'])) {
            $values['value'] = [$values['value']];
        }

        if (! is_array($values['value'])) {
            throw new InvalidArgumentException(
                sprintf(
                    '@Target expects either a string value, or an array of strings, "%s" given.',
                    is_object($values['value']) ? get_class($values['value']) : gettype($values['value'])
                )
            );
        }

        $bitmask = 0;
        foreach ($values['value'] as $literal) {
            if (! isset(self::$map[$literal])) {
                throw new InvalidArgumentException(
                    sprintf(
                        'Invalid Target "%s". Available targets: [%s]',
                        $literal,
                        implode(', ', array_keys(self::$map))
                    )
                );
            }

            $bitmask |= self::$map[$literal];
        }

        $this->targets = $bitmask;
        $this->value   = $values['value'];
        $this->literal = implode(', ', $this->value);
    }
}
<?php

namespace Doctrine\Common\Annotations\Annotation;

/**
 * Annotation that can be used to signal to the parser
 * to check if that attribute is required during the parsing process.
 *
 * @Annotation
 */
final class Required
{
}
<?php

namespace Doctrine\Common\Annotations\Annotation;

use InvalidArgumentException;

use function get_class;
use function gettype;
use function in_array;
use function is_object;
use function is_scalar;
use function sprintf;

/**
 * Annotation that can be used to signal to the parser
 * to check the available values during the parsing process.
 *
 * @Annotation
 * @Attributes({
 *    @Attribute("value",   required = true,  type = "array"),
 *    @Attribute("literal", required = false, type = "array")
 * })
 */
final class Enum
{
    /** @phpstan-var list<scalar> */
    public $value;

    /**
     * Literal target declaration.
     *
     * @var mixed[]
     */
    public $literal;

    /**
     * @phpstan-param array{literal?: mixed[], value: list<scalar>} $values
     *
     * @throws InvalidArgumentException
     */
    public function __construct(array $values)
    {
        if (! isset($values['literal'])) {
            $values['literal'] = [];
        }

        foreach ($values['value'] as $var) {
            if (! is_scalar($var)) {
                throw new InvalidArgumentException(sprintf(
                    '@Enum supports only scalar values "%s" given.',
                    is_object($var) ? get_class($var) : gettype($var)
                ));
            }
        }

        foreach ($values['literal'] as $key => $var) {
            if (! in_array($key, $values['value'])) {
                throw new InvalidArgumentException(sprintf(
                    'Undefined enumerator value "%s" for literal "%s".',
                    $key,
                    $var
                ));
            }
        }

        $this->value   = $values['value'];
        $this->literal = $values['literal'];
    }
}
<?php

namespace Doctrine\Common\Annotations\Annotation;

use RuntimeException;

use function is_array;
use function is_string;
use function json_encode;
use function sprintf;

/**
 * Annotation that can be used to signal to the parser to ignore specific
 * annotations during the parsing process.
 *
 * @Annotation
 */
final class IgnoreAnnotation
{
    /** @phpstan-var list<string> */
    public $names;

    /**
     * @phpstan-param array{value: string|list<string>} $values
     *
     * @throws RuntimeException
     */
    public function __construct(array $values)
    {
        if (is_string($values['value'])) {
            $values['value'] = [$values['value']];
        }

        if (! is_array($values['value'])) {
            throw new RuntimeException(sprintf(
                '@IgnoreAnnotation expects either a string name, or an array of strings, but got %s.',
                json_encode($values['value'])
            ));
        }

        $this->names = $values['value'];
    }
}
<?php

namespace Doctrine\Common\Annotations;

use Doctrine\Common\Lexer\AbstractLexer;

use function ctype_alpha;
use function is_numeric;
use function str_replace;
use function stripos;
use function strlen;
use function strpos;
use function strtolower;
use function substr;

/**
 * Simple lexer for docblock annotations.
 *
 * @template-extends AbstractLexer<DocLexer::T_*, string>
 */
final class DocLexer extends AbstractLexer
{
    public const T_NONE    = 1;
    public const T_INTEGER = 2;
    public const T_STRING  = 3;
    public const T_FLOAT   = 4;

    // All tokens that are also identifiers should be >= 100
    public const T_IDENTIFIER          = 100;
    public const T_AT                  = 101;
    public const T_CLOSE_CURLY_BRACES  = 102;
    public const T_CLOSE_PARENTHESIS   = 103;
    public const T_COMMA               = 104;
    public const T_EQUALS              = 105;
    public const T_FALSE               = 106;
    public const T_NAMESPACE_SEPARATOR = 107;
    public const T_OPEN_CURLY_BRACES   = 108;
    public const T_OPEN_PARENTHESIS    = 109;
    public const T_TRUE                = 110;
    public const T_NULL                = 111;
    public const T_COLON               = 112;
    public const T_MINUS               = 113;

    /** @var array<string, self::T*> */
    protected $noCase = [
        '@'  => self::T_AT,
        ','  => self::T_COMMA,
        '('  => self::T_OPEN_PARENTHESIS,
        ')'  => self::T_CLOSE_PARENTHESIS,
        '{'  => self::T_OPEN_CURLY_BRACES,
        '}'  => self::T_CLOSE_CURLY_BRACES,
        '='  => self::T_EQUALS,
        ':'  => self::T_COLON,
        '-'  => self::T_MINUS,
        '\\' => self::T_NAMESPACE_SEPARATOR,
    ];

    /** @var array<string, self::T*> */
    protected $withCase = [
        'true'  => self::T_TRUE,
        'false' => self::T_FALSE,
        'null'  => self::T_NULL,
    ];

    /**
     * Whether the next token starts immediately, or if there were
     * non-captured symbols before that
     */
    public function nextTokenIsAdjacent(): bool
    {
        return $this->token === null
            || ($this->lookahead !== null
                && ($this->lookahead->position - $this->token->position) === strlen($this->token->value));
    }

    /**
     * {@inheritDoc}
     */
    protected function getCatchablePatterns()
    {
        return [
            '[a-z_\\\][a-z0-9_\:\\\]*[a-z_][a-z0-9_]*',
            '(?:[+-]?[0-9]+(?:[\.][0-9]+)*)(?:[eE][+-]?[0-9]+)?',
            '"(?:""|[^"])*+"',
        ];
    }

    /**
     * {@inheritDoc}
     */
    protected function getNonCatchablePatterns()
    {
        return ['\s+', '\*+', '(.)'];
    }

    /**
     * {@inheritDoc}
     */
    protected function getType(&$value)
    {
        $type = self::T_NONE;

        if ($value[0] === '"') {
            $value = str_replace('""', '"', substr($value, 1, strlen($value) - 2));

            return self::T_STRING;
        }

        if (isset($this->noCase[$value])) {
            return $this->noCase[$value];
        }

        if ($value[0] === '_' || $value[0] === '\\' || ctype_alpha($value[0])) {
            return self::T_IDENTIFIER;
        }

        $lowerValue = strtolower($value);

        if (isset($this->withCase[$lowerValue])) {
            return $this->withCase[$lowerValue];
        }

        // Checking numeric value
        if (is_numeric($value)) {
            return strpos($value, '.') !== false || stripos($value, 'e') !== false
                ? self::T_FLOAT : self::T_INTEGER;
        }

        return $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\Contracts\HttpClient;

/**
 * Yields response chunks, returned by HttpClientInterface::stream().
 *
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @extends \Iterator<ResponseInterface, ChunkInterface>
 */
interface ResponseStreamInterface extends \Iterator
{
    public function key(): ResponseInterface;

    public function current(): ChunkInterface;
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <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\HttpClient\Exception;

/**
 * When a 3xx response is returned and the "max_redirects" option has been reached.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
interface RedirectionExceptionInterface extends HttpExceptionInterface
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <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\HttpClient\Exception;

/**
 * When a 5xx response is returned.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
interface ServerExceptionInterface extends HttpExceptionInterface
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <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\HttpClient\Exception;

/**
 * When a content-type cannot be decoded to the expected representation.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
interface DecodingExceptionInterface extends 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\Contracts\HttpClient\Exception;

/**
 * When a 4xx response is returned.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
interface ClientExceptionInterface extends HttpExceptionInterface
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <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\HttpClient\Exception;

/**
 * When any error happens at the transport level.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
interface TransportExceptionInterface extends 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\Contracts\HttpClient\Exception;

/**
 * When an idle timeout occurs.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
interface TimeoutExceptionInterface extends TransportExceptionInterface
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <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\HttpClient\Exception;

/**
 * The base interface for all exceptions in the contract.
 *
 * @author Nicolas Grekas <p@tchwork.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\Contracts\HttpClient\Exception;

use Symfony\Contracts\HttpClient\ResponseInterface;

/**
 * Base interface for HTTP-related exceptions.
 *
 * @author Anton Chernikov <anton_ch1989@mail.ru>
 */
interface HttpExceptionInterface extends ExceptionInterface
{
    public function getResponse(): ResponseInterface;
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <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\HttpClient;

use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;

/**
 * A (lazily retrieved) HTTP response.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
interface ResponseInterface
{
    /**
     * Gets the HTTP status code of the response.
     *
     * @throws TransportExceptionInterface when a network error occurs
     */
    public function getStatusCode(): int;

    /**
     * Gets the HTTP headers of the response.
     *
     * @param bool $throw Whether an exception should be thrown on 3/4/5xx status codes
     *
     * @return string[][] The headers of the response keyed by header names in lowercase
     *
     * @throws TransportExceptionInterface   When a network error occurs
     * @throws RedirectionExceptionInterface On a 3xx when $throw is true and the "max_redirects" option has been reached
     * @throws ClientExceptionInterface      On a 4xx when $throw is true
     * @throws ServerExceptionInterface      On a 5xx when $throw is true
     */
    public function getHeaders(bool $throw = true): array;

    /**
     * Gets the response body as a string.
     *
     * @param bool $throw Whether an exception should be thrown on 3/4/5xx status codes
     *
     * @throws TransportExceptionInterface   When a network error occurs
     * @throws RedirectionExceptionInterface On a 3xx when $throw is true and the "max_redirects" option has been reached
     * @throws ClientExceptionInterface      On a 4xx when $throw is true
     * @throws ServerExceptionInterface      On a 5xx when $throw is true
     */
    public function getContent(bool $throw = true): string;

    /**
     * Gets the response body decoded as array, typically from a JSON payload.
     *
     * @param bool $throw Whether an exception should be thrown on 3/4/5xx status codes
     *
     * @throws DecodingExceptionInterface    When the body cannot be decoded to an array
     * @throws TransportExceptionInterface   When a network error occurs
     * @throws RedirectionExceptionInterface On a 3xx when $throw is true and the "max_redirects" option has been reached
     * @throws ClientExceptionInterface      On a 4xx when $throw is true
     * @throws ServerExceptionInterface      On a 5xx when $throw is true
     */
    public function toArray(bool $throw = true): array;

    /**
     * Closes the response stream and all related buffers.
     *
     * No further chunk will be yielded after this method has been called.
     */
    public function cancel(): void;

    /**
     * Returns info coming from the transport layer.
     *
     * This method SHOULD NOT throw any ExceptionInterface and SHOULD be non-blocking.
     * The returned info is "live": it can be empty and can change from one call to
     * another, as the request/response progresses.
     *
     * The following info MUST be returned:
     *  - canceled (bool) - true if the response was canceled using ResponseInterface::cancel(), false otherwise
     *  - error (string|null) - the error message when the transfer was aborted, null otherwise
     *  - http_code (int) - the last response code or 0 when it is not known yet
     *  - http_method (string) - the HTTP verb of the last request
     *  - redirect_count (int) - the number of redirects followed while executing the request
     *  - redirect_url (string|null) - the resolved location of redirect responses, null otherwise
     *  - response_headers (array) - an array modelled after the special $http_response_header variable
     *  - start_time (float) - the time when the request was sent or 0.0 when it's pending
     *  - url (string) - the last effective URL of the request
     *  - user_data (mixed) - the value of the "user_data" request option, null if not set
     *
     * When the "capture_peer_cert_chain" option is true, the "peer_certificate_chain"
     * attribute SHOULD list the peer certificates as an array of OpenSSL X.509 resources.
     *
     * Other info SHOULD be named after curl_getinfo()'s associative return value.
     *
     * @return mixed An array of all available info, or one of them when $type is
     *               provided, or null when an unsupported type is requested
     */
    public function getInfo(?string $type = null);
}
<?php

if ('cli-server' !== \PHP_SAPI) {
    // safe guard against unwanted execution
    throw new \Exception("You cannot run this script directly, it's a fixture for TestHttpServer.");
}

$vars = [];

if (!$_POST) {
    $_POST = json_decode(file_get_contents('php://input'), true);
    $_POST['content-type'] = $_SERVER['HTTP_CONTENT_TYPE'] ?? '?';
}

$headers = [
    'SERVER_PROTOCOL',
    'SERVER_NAME',
    'REQUEST_URI',
    'REQUEST_METHOD',
    'PHP_AUTH_USER',
    'PHP_AUTH_PW',
    'REMOTE_ADDR',
    'REMOTE_PORT',
];

foreach ($headers as $k) {
    if (isset($_SERVER[$k])) {
        $vars[$k] = $_SERVER[$k];
    }
}

foreach ($_SERVER as $k => $v) {
    if (0 === strpos($k, 'HTTP_')) {
        $vars[$k] = $v;
    }
}

$json = json_encode($vars, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE);

switch (parse_url($vars['REQUEST_URI'], \PHP_URL_PATH)) {
    default:
        exit;

    case '/head':
        header('Content-Length: '.strlen($json), true);
        break;

    case '/':
    case '/?a=a&b=b':
    case 'http://127.0.0.1:8057/':
    case 'http://localhost:8057/':
        ob_start('ob_gzhandler');
        break;

    case '/103':
        header('HTTP/1.1 103 Early Hints');
        header('Link: </style.css>; rel=preload; as=style', false);
        header('Link: </script.js>; rel=preload; as=script', false);
        flush();
        usleep(1000);
        echo "HTTP/1.1 200 OK\r\n";
        echo "Date: Fri, 26 May 2017 10:02:11 GMT\r\n";
        echo "Content-Length: 13\r\n";
        echo "\r\n";
        echo 'Here the body';
        exit;

    case '/404':
        header('Content-Type: application/json', true, 404);
        break;

    case '/404-gzipped':
        header('Content-Type: text/plain', true, 404);
        ob_start('ob_gzhandler');
        @ob_flush();
        flush();
        usleep(300000);
        echo 'some text';
        exit;

    case '/301':
        if ('Basic Zm9vOmJhcg==' === $vars['HTTP_AUTHORIZATION']) {
            header('Location: http://127.0.0.1:8057/302', true, 301);
        }
        break;

    case '/301/bad-tld':
        header('Location: http://foo.example.', true, 301);
        break;

    case '/301/invalid':
        header('Location: //?foo=bar', true, 301);
        break;

    case '/301/proxy':
    case 'http://localhost:8057/301/proxy':
    case 'http://127.0.0.1:8057/301/proxy':
        header('Location: http://localhost:8057/', true, 301);
        break;

    case '/302':
        if (!isset($vars['HTTP_AUTHORIZATION'])) {
            $location = $_GET['location'] ?? 'http://localhost:8057/';
            header('Location: '.$location, true, 302);
        }
        break;

    case '/302/relative':
        header('Location: ..', true, 302);
        break;

    case '/304':
        header('Content-Length: 10', true, 304);
        echo '12345';

        return;

    case '/307':
        header('Location: http://localhost:8057/post', true, 307);
        break;

    case '/length-broken':
        header('Content-Length: 1000');
        break;

    case '/post':
        $output = json_encode($_POST + ['REQUEST_METHOD' => $vars['REQUEST_METHOD']], \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE);
        header('Content-Type: application/json', true);
        header('Content-Length: '.strlen($output));
        echo $output;
        exit;

    case '/timeout-header':
        usleep(300000);
        break;

    case '/timeout-body':
        echo '<1>';
        @ob_flush();
        flush();
        usleep(500000);
        echo '<2>';
        exit;

    case '/timeout-long':
        ignore_user_abort(false);
        sleep(1);
        while (true) {
            echo '<1>';
            @ob_flush();
            flush();
            usleep(500);
        }
        exit;

    case '/chunked':
        header('Transfer-Encoding: chunked');
        echo "8\r\nSymfony \r\n5\r\nis aw\r\n6\r\nesome!\r\n0\r\n\r\n";
        exit;

    case '/chunked-broken':
        header('Transfer-Encoding: chunked');
        echo "8\r\nSymfony \r\n5\r\nis aw\r\n6\r\ne";
        exit;

    case '/gzip-broken':
        header('Content-Encoding: gzip');
        echo str_repeat('-', 1000);
        exit;

    case '/max-duration':
        ignore_user_abort(false);
        while (true) {
            echo '<1>';
            @ob_flush();
            flush();
            usleep(500);
        }
        exit;

    case '/json':
        header('Content-Type: application/json');
        echo json_encode([
            'documents' => [
                ['id' => '/json/1'],
                ['id' => '/json/2'],
                ['id' => '/json/3'],
            ],
        ]);
        exit;

    case '/json/1':
    case '/json/2':
    case '/json/3':
        header('Content-Type: application/json');
        echo json_encode([
            'title' => $vars['REQUEST_URI'],
        ]);

        exit;
}

header('Content-Type: application/json', true);

echo $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\Contracts\HttpClient\Test;

use PHPUnit\Framework\TestCase;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TimeoutExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;

/**
 * A reference test suite for HttpClientInterface implementations.
 */
abstract class HttpClientTestCase extends TestCase
{
    public static function setUpBeforeClass(): void
    {
        if (!function_exists('ob_gzhandler')) {
            static::markTestSkipped('The "ob_gzhandler" function is not available.');
        }

        TestHttpServer::start();
    }

    public static function tearDownAfterClass(): void
    {
        TestHttpServer::stop(8067);
        TestHttpServer::stop(8077);
    }

    abstract protected function getHttpClient(string $testCase): HttpClientInterface;

    public function testGetRequest()
    {
        $client = $this->getHttpClient(__FUNCTION__);
        $response = $client->request('GET', 'http://localhost:8057', [
            'headers' => ['Foo' => 'baR'],
            'user_data' => $data = new \stdClass(),
        ]);

        $this->assertSame([], $response->getInfo('response_headers'));
        $this->assertSame($data, $response->getInfo()['user_data']);
        $this->assertSame(200, $response->getStatusCode());

        $info = $response->getInfo();
        $this->assertNull($info['error']);
        $this->assertSame(0, $info['redirect_count']);
        $this->assertSame('HTTP/1.1 200 OK', $info['response_headers'][0]);
        $this->assertSame('Host: localhost:8057', $info['response_headers'][1]);
        $this->assertSame('http://localhost:8057/', $info['url']);

        $headers = $response->getHeaders();

        $this->assertSame('localhost:8057', $headers['host'][0]);
        $this->assertSame(['application/json'], $headers['content-type']);

        $body = json_decode($response->getContent(), true);
        $this->assertSame($body, $response->toArray());

        $this->assertSame('HTTP/1.1', $body['SERVER_PROTOCOL']);
        $this->assertSame('/', $body['REQUEST_URI']);
        $this->assertSame('GET', $body['REQUEST_METHOD']);
        $this->assertSame('localhost:8057', $body['HTTP_HOST']);
        $this->assertSame('baR', $body['HTTP_FOO']);

        $response = $client->request('GET', 'http://localhost:8057/length-broken');

        $this->expectException(TransportExceptionInterface::class);
        $response->getContent();
    }

    public function testHeadRequest()
    {
        $client = $this->getHttpClient(__FUNCTION__);
        $response = $client->request('HEAD', 'http://localhost:8057/head', [
            'headers' => ['Foo' => 'baR'],
            'user_data' => $data = new \stdClass(),
            'buffer' => false,
        ]);

        $this->assertSame([], $response->getInfo('response_headers'));
        $this->assertSame(200, $response->getStatusCode());

        $info = $response->getInfo();
        $this->assertSame('HTTP/1.1 200 OK', $info['response_headers'][0]);
        $this->assertSame('Host: localhost:8057', $info['response_headers'][1]);

        $headers = $response->getHeaders();

        $this->assertSame('localhost:8057', $headers['host'][0]);
        $this->assertSame(['application/json'], $headers['content-type']);
        $this->assertTrue(0 < $headers['content-length'][0]);

        $this->assertSame('', $response->getContent());
    }

    public function testNonBufferedGetRequest()
    {
        $client = $this->getHttpClient(__FUNCTION__);
        $response = $client->request('GET', 'http://localhost:8057', [
            'buffer' => false,
            'headers' => ['Foo' => 'baR'],
        ]);

        $body = $response->toArray();
        $this->assertSame('baR', $body['HTTP_FOO']);

        $this->expectException(TransportExceptionInterface::class);
        $response->getContent();
    }

    public function testBufferSink()
    {
        $sink = fopen('php://temp', 'w+');
        $client = $this->getHttpClient(__FUNCTION__);
        $response = $client->request('GET', 'http://localhost:8057', [
            'buffer' => $sink,
            'headers' => ['Foo' => 'baR'],
        ]);

        $body = $response->toArray();
        $this->assertSame('baR', $body['HTTP_FOO']);

        rewind($sink);
        $sink = stream_get_contents($sink);
        $this->assertSame($sink, $response->getContent());
    }

    public function testConditionalBuffering()
    {
        $client = $this->getHttpClient(__FUNCTION__);
        $response = $client->request('GET', 'http://localhost:8057');
        $firstContent = $response->getContent();
        $secondContent = $response->getContent();

        $this->assertSame($firstContent, $secondContent);

        $response = $client->request('GET', 'http://localhost:8057', ['buffer' => function () { return false; }]);
        $response->getContent();

        $this->expectException(TransportExceptionInterface::class);
        $response->getContent();
    }

    public function testReentrantBufferCallback()
    {
        $client = $this->getHttpClient(__FUNCTION__);

        $response = $client->request('GET', 'http://localhost:8057', ['buffer' => function () use (&$response) {
            $response->cancel();

            return true;
        }]);

        $this->assertSame(200, $response->getStatusCode());

        $this->expectException(TransportExceptionInterface::class);
        $response->getContent();
    }

    public function testThrowingBufferCallback()
    {
        $client = $this->getHttpClient(__FUNCTION__);

        $response = $client->request('GET', 'http://localhost:8057', ['buffer' => function () {
            throw new \Exception('Boo.');
        }]);

        $this->assertSame(200, $response->getStatusCode());

        $this->expectException(TransportExceptionInterface::class);
        $this->expectExceptionMessage('Boo');
        $response->getContent();
    }

    public function testUnsupportedOption()
    {
        $client = $this->getHttpClient(__FUNCTION__);

        $this->expectException(\InvalidArgumentException::class);
        $client->request('GET', 'http://localhost:8057', [
            'capture_peer_cert' => 1.0,
        ]);
    }

    public function testHttpVersion()
    {
        $client = $this->getHttpClient(__FUNCTION__);
        $response = $client->request('GET', 'http://localhost:8057', [
            'http_version' => 1.0,
        ]);

        $this->assertSame(200, $response->getStatusCode());
        $this->assertSame('HTTP/1.0 200 OK', $response->getInfo('response_headers')[0]);

        $body = $response->toArray();

        $this->assertSame('HTTP/1.0', $body['SERVER_PROTOCOL']);
        $this->assertSame('GET', $body['REQUEST_METHOD']);
        $this->assertSame('/', $body['REQUEST_URI']);
    }

    public function testChunkedEncoding()
    {
        $client = $this->getHttpClient(__FUNCTION__);
        $response = $client->request('GET', 'http://localhost:8057/chunked');

        $this->assertSame(['chunked'], $response->getHeaders()['transfer-encoding']);
        $this->assertSame('Symfony is awesome!', $response->getContent());

        $response = $client->request('GET', 'http://localhost:8057/chunked-broken');

        $this->expectException(TransportExceptionInterface::class);
        $response->getContent();
    }

    public function testClientError()
    {
        $client = $this->getHttpClient(__FUNCTION__);
        $response = $client->request('GET', 'http://localhost:8057/404');

        $client->stream($response)->valid();

        $this->assertSame(404, $response->getInfo('http_code'));

        try {
            $response->getHeaders();
            $this->fail(ClientExceptionInterface::class.' expected');
        } catch (ClientExceptionInterface $e) {
        }

        try {
            $response->getContent();
            $this->fail(ClientExceptionInterface::class.' expected');
        } catch (ClientExceptionInterface $e) {
        }

        $this->assertSame(404, $response->getStatusCode());
        $this->assertSame(['application/json'], $response->getHeaders(false)['content-type']);
        $this->assertNotEmpty($response->getContent(false));

        $response = $client->request('GET', 'http://localhost:8057/404');

        try {
            foreach ($client->stream($response) as $chunk) {
                $this->assertTrue($chunk->isFirst());
            }
            $this->fail(ClientExceptionInterface::class.' expected');
        } catch (ClientExceptionInterface $e) {
        }
    }

    public function testIgnoreErrors()
    {
        $client = $this->getHttpClient(__FUNCTION__);
        $response = $client->request('GET', 'http://localhost:8057/404');

        $this->assertSame(404, $response->getStatusCode());
    }

    public function testDnsError()
    {
        $client = $this->getHttpClient(__FUNCTION__);
        $response = $client->request('GET', 'http://localhost:8057/301/bad-tld');

        try {
            $response->getStatusCode();
            $this->fail(TransportExceptionInterface::class.' expected');
        } catch (TransportExceptionInterface $e) {
            $this->addToAssertionCount(1);
        }

        try {
            $response->getStatusCode();
            $this->fail(TransportExceptionInterface::class.' still expected');
        } catch (TransportExceptionInterface $e) {
            $this->addToAssertionCount(1);
        }

        $response = $client->request('GET', 'http://localhost:8057/301/bad-tld');

        try {
            foreach ($client->stream($response) as $r => $chunk) {
            }
            $this->fail(TransportExceptionInterface::class.' expected');
        } catch (TransportExceptionInterface $e) {
            $this->addToAssertionCount(1);
        }

        $this->assertSame($response, $r);
        $this->assertNotNull($chunk->getError());

        $this->expectException(TransportExceptionInterface::class);
        foreach ($client->stream($response) as $chunk) {
        }
    }

    public function testInlineAuth()
    {
        $client = $this->getHttpClient(__FUNCTION__);
        $response = $client->request('GET', 'http://foo:bar%3Dbar@localhost:8057');

        $body = $response->toArray();

        $this->assertSame('foo', $body['PHP_AUTH_USER']);
        $this->assertSame('bar=bar', $body['PHP_AUTH_PW']);
    }

    public function testBadRequestBody()
    {
        $client = $this->getHttpClient(__FUNCTION__);

        $this->expectException(TransportExceptionInterface::class);

        $response = $client->request('POST', 'http://localhost:8057/', [
            'body' => function () { yield []; },
        ]);

        $response->getStatusCode();
    }

    public function test304()
    {
        $client = $this->getHttpClient(__FUNCTION__);
        $response = $client->request('GET', 'http://localhost:8057/304', [
            'headers' => ['If-Match' => '"abc"'],
            'buffer' => false,
        ]);

        $this->assertSame(304, $response->getStatusCode());
        $this->assertSame('', $response->getContent(false));
    }

    /**
     * @testWith [[]]
     *           [["Content-Length: 7"]]
     */
    public function testRedirects(array $headers = [])
    {
        $client = $this->getHttpClient(__FUNCTION__);
        $response = $client->request('POST', 'http://localhost:8057/301', [
            'auth_basic' => 'foo:bar',
            'headers' => $headers,
            'body' => function () {
                yield 'foo=bar';
            },
        ]);

        $body = $response->toArray();
        $this->assertSame('GET', $body['REQUEST_METHOD']);
        $this->assertSame('Basic Zm9vOmJhcg==', $body['HTTP_AUTHORIZATION']);
        $this->assertSame('http://localhost:8057/', $response->getInfo('url'));

        $this->assertSame(2, $response->getInfo('redirect_count'));
        $this->assertNull($response->getInfo('redirect_url'));

        $expected = [
            'HTTP/1.1 301 Moved Permanently',
            'Location: http://127.0.0.1:8057/302',
            'Content-Type: application/json',
            'HTTP/1.1 302 Found',
            'Location: http://localhost:8057/',
            'Content-Type: application/json',
            'HTTP/1.1 200 OK',
            'Content-Type: application/json',
        ];

        $filteredHeaders = array_values(array_filter($response->getInfo('response_headers'), function ($h) {
            return \in_array(substr($h, 0, 4), ['HTTP', 'Loca', 'Cont'], true) && 'Content-Encoding: gzip' !== $h;
        }));

        $this->assertSame($expected, $filteredHeaders);
    }

    public function testInvalidRedirect()
    {
        $client = $this->getHttpClient(__FUNCTION__);
        $response = $client->request('GET', 'http://localhost:8057/301/invalid');

        $this->assertSame(301, $response->getStatusCode());
        $this->assertSame(['//?foo=bar'], $response->getHeaders(false)['location']);
        $this->assertSame(0, $response->getInfo('redirect_count'));
        $this->assertNull($response->getInfo('redirect_url'));

        $this->expectException(RedirectionExceptionInterface::class);
        $response->getHeaders();
    }

    public function testRelativeRedirects()
    {
        $client = $this->getHttpClient(__FUNCTION__);
        $response = $client->request('GET', 'http://localhost:8057/302/relative');

        $body = $response->toArray();

        $this->assertSame('/', $body['REQUEST_URI']);
        $this->assertNull($response->getInfo('redirect_url'));

        $response = $client->request('GET', 'http://localhost:8057/302/relative', [
            'max_redirects' => 0,
        ]);

        $this->assertSame(302, $response->getStatusCode());
        $this->assertSame('http://localhost:8057/', $response->getInfo('redirect_url'));
    }

    public function testRedirect307()
    {
        $client = $this->getHttpClient(__FUNCTION__);

        $response = $client->request('POST', 'http://localhost:8057/307', [
            'body' => function () {
                yield 'foo=bar';
            },
            'max_redirects' => 0,
        ]);

        $this->assertSame(307, $response->getStatusCode());

        $response = $client->request('POST', 'http://localhost:8057/307', [
            'body' => 'foo=bar',
        ]);

        $body = $response->toArray();

        $this->assertSame(['foo' => 'bar', 'REQUEST_METHOD' => 'POST'], $body);
    }

    public function testMaxRedirects()
    {
        $client = $this->getHttpClient(__FUNCTION__);
        $response = $client->request('GET', 'http://localhost:8057/301', [
            'max_redirects' => 1,
            'auth_basic' => 'foo:bar',
        ]);

        try {
            $response->getHeaders();
            $this->fail(RedirectionExceptionInterface::class.' expected');
        } catch (RedirectionExceptionInterface $e) {
        }

        $this->assertSame(302, $response->getStatusCode());
        $this->assertSame(1, $response->getInfo('redirect_count'));
        $this->assertSame('http://localhost:8057/', $response->getInfo('redirect_url'));

        $expected = [
            'HTTP/1.1 301 Moved Permanently',
            'Location: http://127.0.0.1:8057/302',
            'Content-Type: application/json',
            'HTTP/1.1 302 Found',
            'Location: http://localhost:8057/',
            'Content-Type: application/json',
        ];

        $filteredHeaders = array_values(array_filter($response->getInfo('response_headers'), function ($h) {
            return \in_array(substr($h, 0, 4), ['HTTP', 'Loca', 'Cont'], true);
        }));

        $this->assertSame($expected, $filteredHeaders);
    }

    public function testStream()
    {
        $client = $this->getHttpClient(__FUNCTION__);

        $response = $client->request('GET', 'http://localhost:8057');
        $chunks = $client->stream($response);
        $result = [];

        foreach ($chunks as $r => $chunk) {
            if ($chunk->isTimeout()) {
                $result[] = 't';
            } elseif ($chunk->isLast()) {
                $result[] = 'l';
            } elseif ($chunk->isFirst()) {
                $result[] = 'f';
            }
        }

        $this->assertSame($response, $r);
        $this->assertSame(['f', 'l'], $result);

        $chunk = null;
        $i = 0;

        foreach ($client->stream($response) as $chunk) {
            ++$i;
        }

        $this->assertSame(1, $i);
        $this->assertTrue($chunk->isLast());
    }

    public function testAddToStream()
    {
        $client = $this->getHttpClient(__FUNCTION__);

        $r1 = $client->request('GET', 'http://localhost:8057');

        $completed = [];

        $pool = [$r1];

        while ($pool) {
            $chunks = $client->stream($pool);
            $pool = [];

            foreach ($chunks as $r => $chunk) {
                if (!$chunk->isLast()) {
                    continue;
                }

                if ($r1 === $r) {
                    $r2 = $client->request('GET', 'http://localhost:8057');
                    $pool[] = $r2;
                }

                $completed[] = $r;
            }
        }

        $this->assertSame([$r1, $r2], $completed);
    }

    public function testCompleteTypeError()
    {
        $client = $this->getHttpClient(__FUNCTION__);

        $this->expectException(\TypeError::class);
        $client->stream(123);
    }

    public function testOnProgress()
    {
        $client = $this->getHttpClient(__FUNCTION__);
        $response = $client->request('POST', 'http://localhost:8057/post', [
            'headers' => ['Content-Length' => 14],
            'body' => 'foo=0123456789',
            'on_progress' => function (...$state) use (&$steps) { $steps[] = $state; },
        ]);

        $body = $response->toArray();

        $this->assertSame(['foo' => '0123456789', 'REQUEST_METHOD' => 'POST'], $body);
        $this->assertSame([0, 0], \array_slice($steps[0], 0, 2));
        $lastStep = \array_slice($steps, -1)[0];
        $this->assertSame([57, 57], \array_slice($lastStep, 0, 2));
        $this->assertSame('http://localhost:8057/post', $steps[0][2]['url']);
    }

    public function testPostJson()
    {
        $client = $this->getHttpClient(__FUNCTION__);

        $response = $client->request('POST', 'http://localhost:8057/post', [
            'json' => ['foo' => 'bar'],
        ]);

        $body = $response->toArray();

        $this->assertStringContainsString('json', $body['content-type']);
        unset($body['content-type']);
        $this->assertSame(['foo' => 'bar', 'REQUEST_METHOD' => 'POST'], $body);
    }

    public function testPostArray()
    {
        $client = $this->getHttpClient(__FUNCTION__);

        $response = $client->request('POST', 'http://localhost:8057/post', [
            'body' => ['foo' => 'bar'],
        ]);

        $this->assertSame(['foo' => 'bar', 'REQUEST_METHOD' => 'POST'], $response->toArray());
    }

    public function testPostResource()
    {
        $client = $this->getHttpClient(__FUNCTION__);

        $h = fopen('php://temp', 'w+');
        fwrite($h, 'foo=0123456789');
        rewind($h);

        $response = $client->request('POST', 'http://localhost:8057/post', [
            'body' => $h,
        ]);

        $body = $response->toArray();

        $this->assertSame(['foo' => '0123456789', 'REQUEST_METHOD' => 'POST'], $body);
    }

    public function testPostCallback()
    {
        $client = $this->getHttpClient(__FUNCTION__);

        $response = $client->request('POST', 'http://localhost:8057/post', [
            'body' => function () {
                yield 'foo';
                yield '';
                yield '=';
                yield '0123456789';
            },
        ]);

        $this->assertSame(['foo' => '0123456789', 'REQUEST_METHOD' => 'POST'], $response->toArray());
    }

    public function testCancel()
    {
        $client = $this->getHttpClient(__FUNCTION__);
        $response = $client->request('GET', 'http://localhost:8057/timeout-header');

        $response->cancel();
        $this->expectException(TransportExceptionInterface::class);
        $response->getHeaders();
    }

    public function testInfoOnCanceledResponse()
    {
        $client = $this->getHttpClient(__FUNCTION__);

        $response = $client->request('GET', 'http://localhost:8057/timeout-header');

        $this->assertFalse($response->getInfo('canceled'));
        $response->cancel();
        $this->assertTrue($response->getInfo('canceled'));
    }

    public function testCancelInStream()
    {
        $client = $this->getHttpClient(__FUNCTION__);
        $response = $client->request('GET', 'http://localhost:8057/404');

        foreach ($client->stream($response) as $chunk) {
            $response->cancel();
        }

        $this->expectException(TransportExceptionInterface::class);

        foreach ($client->stream($response) as $chunk) {
        }
    }

    public function testOnProgressCancel()
    {
        $client = $this->getHttpClient(__FUNCTION__);
        $response = $client->request('GET', 'http://localhost:8057/timeout-body', [
            'on_progress' => function ($dlNow) {
                if (0 < $dlNow) {
                    throw new \Exception('Aborting the request.');
                }
            },
        ]);

        try {
            foreach ($client->stream([$response]) as $chunk) {
            }
            $this->fail(ClientExceptionInterface::class.' expected');
        } catch (TransportExceptionInterface $e) {
            $this->assertSame('Aborting the request.', $e->getPrevious()->getMessage());
        }

        $this->assertNotNull($response->getInfo('error'));
        $this->expectException(TransportExceptionInterface::class);
        $response->getContent();
    }

    public function testOnProgressError()
    {
        $client = $this->getHttpClient(__FUNCTION__);
        $response = $client->request('GET', 'http://localhost:8057/timeout-body', [
            'on_progress' => function ($dlNow) {
                if (0 < $dlNow) {
                    throw new \Error('BUG.');
                }
            },
        ]);

        try {
            foreach ($client->stream([$response]) as $chunk) {
            }
            $this->fail('Error expected');
        } catch (\Error $e) {
            $this->assertSame('BUG.', $e->getMessage());
        }

        $this->assertNotNull($response->getInfo('error'));
        $this->expectException(TransportExceptionInterface::class);
        $response->getContent();
    }

    public function testResolve()
    {
        $client = $this->getHttpClient(__FUNCTION__);
        $response = $client->request('GET', 'http://symfony.com:8057/', [
            'resolve' => ['symfony.com' => '127.0.0.1'],
        ]);

        $this->assertSame(200, $response->getStatusCode());
        $this->assertSame(200, $client->request('GET', 'http://symfony.com:8057/')->getStatusCode());

        $response = null;
        $this->expectException(TransportExceptionInterface::class);
        $client->request('GET', 'http://symfony.com:8057/', ['timeout' => 1]);
    }

    public function testIdnResolve()
    {
        $client = $this->getHttpClient(__FUNCTION__);

        $response = $client->request('GET', 'http://0-------------------------------------------------------------0.com:8057/', [
            'resolve' => ['0-------------------------------------------------------------0.com' => '127.0.0.1'],
        ]);

        $this->assertSame(200, $response->getStatusCode());

        $response = $client->request('GET', 'http://Bücher.example:8057/', [
            'resolve' => ['xn--bcher-kva.example' => '127.0.0.1'],
        ]);

        $this->assertSame(200, $response->getStatusCode());
    }

    public function testIPv6Resolve()
    {
        TestHttpServer::start(-8087);

        $client = $this->getHttpClient(__FUNCTION__);
        $response = $client->request('GET', 'http://symfony.com:8087/', [
            'resolve' => ['symfony.com' => '::1'],
        ]);

        $this->assertSame(200, $response->getStatusCode());
    }

    public function testNotATimeout()
    {
        $client = $this->getHttpClient(__FUNCTION__);
        $response = $client->request('GET', 'http://localhost:8057/timeout-header', [
            'timeout' => 0.9,
        ]);
        sleep(1);
        $this->assertSame(200, $response->getStatusCode());
    }

    public function testTimeoutOnAccess()
    {
        $client = $this->getHttpClient(__FUNCTION__);
        $response = $client->request('GET', 'http://localhost:8057/timeout-header', [
            'timeout' => 0.1,
        ]);

        $this->expectException(TransportExceptionInterface::class);
        $response->getHeaders();
    }

    public function testTimeoutIsNotAFatalError()
    {
        usleep(300000); // wait for the previous test to release the server
        $client = $this->getHttpClient(__FUNCTION__);
        $response = $client->request('GET', 'http://localhost:8057/timeout-body', [
            'timeout' => 0.25,
        ]);

        try {
            $response->getContent();
            $this->fail(TimeoutExceptionInterface::class.' expected');
        } catch (TimeoutExceptionInterface $e) {
        }

        for ($i = 0; $i < 10; ++$i) {
            try {
                $this->assertSame('<1><2>', $response->getContent());
                break;
            } catch (TimeoutExceptionInterface $e) {
            }
        }

        if (10 === $i) {
            throw $e;
        }
    }

    public function testTimeoutOnStream()
    {
        $client = $this->getHttpClient(__FUNCTION__);
        $response = $client->request('GET', 'http://localhost:8057/timeout-body');

        $this->assertSame(200, $response->getStatusCode());
        $chunks = $client->stream([$response], 0.2);

        $result = [];

        foreach ($chunks as $r => $chunk) {
            if ($chunk->isTimeout()) {
                $result[] = 't';
            } else {
                $result[] = $chunk->getContent();
            }
        }

        $this->assertSame(['<1>', 't'], $result);

        $chunks = $client->stream([$response]);

        foreach ($chunks as $r => $chunk) {
            $this->assertSame('<2>', $chunk->getContent());
            $this->assertSame('<1><2>', $r->getContent());

            return;
        }

        $this->fail('The response should have completed');
    }

    public function testUncheckedTimeoutThrows()
    {
        $client = $this->getHttpClient(__FUNCTION__);
        $response = $client->request('GET', 'http://localhost:8057/timeout-body');
        $chunks = $client->stream([$response], 0.1);

        $this->expectException(TransportExceptionInterface::class);

        foreach ($chunks as $r => $chunk) {
        }
    }

    public function testTimeoutWithActiveConcurrentStream()
    {
        $p1 = TestHttpServer::start(8067);
        $p2 = TestHttpServer::start(8077);

        $client = $this->getHttpClient(__FUNCTION__);
        $streamingResponse = $client->request('GET', 'http://localhost:8067/max-duration');
        $blockingResponse = $client->request('GET', 'http://localhost:8077/timeout-body', [
            'timeout' => 0.25,
        ]);

        $this->assertSame(200, $streamingResponse->getStatusCode());
        $this->assertSame(200, $blockingResponse->getStatusCode());

        $this->expectException(TransportExceptionInterface::class);

        try {
            $blockingResponse->getContent();
        } finally {
            $p1->stop();
            $p2->stop();
        }
    }

    public function testTimeoutOnInitialize()
    {
        $p1 = TestHttpServer::start(8067);
        $p2 = TestHttpServer::start(8077);

        $client = $this->getHttpClient(__FUNCTION__);
        $start = microtime(true);
        $responses = [];

        $responses[] = $client->request('GET', 'http://localhost:8067/timeout-header', ['timeout' => 0.25]);
        $responses[] = $client->request('GET', 'http://localhost:8077/timeout-header', ['timeout' => 0.25]);
        $responses[] = $client->request('GET', 'http://localhost:8067/timeout-header', ['timeout' => 0.25]);
        $responses[] = $client->request('GET', 'http://localhost:8077/timeout-header', ['timeout' => 0.25]);

        try {
            foreach ($responses as $response) {
                try {
                    $response->getContent();
                    $this->fail(TransportExceptionInterface::class.' expected');
                } catch (TransportExceptionInterface $e) {
                }
            }
            $responses = [];

            $duration = microtime(true) - $start;

            $this->assertLessThan(1.0, $duration);
        } finally {
            $p1->stop();
            $p2->stop();
        }
    }

    public function testTimeoutOnDestruct()
    {
        $p1 = TestHttpServer::start(8067);
        $p2 = TestHttpServer::start(8077);

        $client = $this->getHttpClient(__FUNCTION__);
        $start = microtime(true);
        $responses = [];

        $responses[] = $client->request('GET', 'http://localhost:8067/timeout-header', ['timeout' => 0.25]);
        $responses[] = $client->request('GET', 'http://localhost:8077/timeout-header', ['timeout' => 0.25]);
        $responses[] = $client->request('GET', 'http://localhost:8067/timeout-header', ['timeout' => 0.25]);
        $responses[] = $client->request('GET', 'http://localhost:8077/timeout-header', ['timeout' => 0.25]);

        try {
            while ($response = array_shift($responses)) {
                try {
                    unset($response);
                    $this->fail(TransportExceptionInterface::class.' expected');
                } catch (TransportExceptionInterface $e) {
                }
            }

            $duration = microtime(true) - $start;

            $this->assertLessThan(1.0, $duration);
        } finally {
            $p1->stop();
            $p2->stop();
        }
    }

    public function testDestruct()
    {
        $client = $this->getHttpClient(__FUNCTION__);

        $start = microtime(true);
        $client->request('GET', 'http://localhost:8057/timeout-long');
        $client = null;
        $duration = microtime(true) - $start;

        $this->assertGreaterThan(1, $duration);
        $this->assertLessThan(4, $duration);
    }

    public function testGetContentAfterDestruct()
    {
        $client = $this->getHttpClient(__FUNCTION__);

        try {
            $client->request('GET', 'http://localhost:8057/404');
            $this->fail(ClientExceptionInterface::class.' expected');
        } catch (ClientExceptionInterface $e) {
            $this->assertSame('GET', $e->getResponse()->toArray(false)['REQUEST_METHOD']);
        }
    }

    public function testGetEncodedContentAfterDestruct()
    {
        $client = $this->getHttpClient(__FUNCTION__);

        try {
            $client->request('GET', 'http://localhost:8057/404-gzipped');
            $this->fail(ClientExceptionInterface::class.' expected');
        } catch (ClientExceptionInterface $e) {
            $this->assertSame('some text', $e->getResponse()->getContent(false));
        }
    }

    public function testProxy()
    {
        $client = $this->getHttpClient(__FUNCTION__);
        $response = $client->request('GET', 'http://localhost:8057/', [
            'proxy' => 'http://localhost:8057',
        ]);

        $body = $response->toArray();
        $this->assertSame('localhost:8057', $body['HTTP_HOST']);
        $this->assertMatchesRegularExpression('#^http://(localhost|127\.0\.0\.1):8057/$#', $body['REQUEST_URI']);

        $response = $client->request('GET', 'http://localhost:8057/', [
            'proxy' => 'http://foo:b%3Dar@localhost:8057',
        ]);

        $body = $response->toArray();
        $this->assertSame('Basic Zm9vOmI9YXI=', $body['HTTP_PROXY_AUTHORIZATION']);

        $_SERVER['http_proxy'] = 'http://localhost:8057';
        try {
            $response = $client->request('GET', 'http://localhost:8057/');
            $body = $response->toArray();
            $this->assertSame('localhost:8057', $body['HTTP_HOST']);
            $this->assertMatchesRegularExpression('#^http://(localhost|127\.0\.0\.1):8057/$#', $body['REQUEST_URI']);
        } finally {
            unset($_SERVER['http_proxy']);
        }

        $response = $client->request('GET', 'http://localhost:8057/301/proxy', [
            'proxy' => 'http://localhost:8057',
        ]);

        $body = $response->toArray();
        $this->assertSame('localhost:8057', $body['HTTP_HOST']);
        $this->assertMatchesRegularExpression('#^http://(localhost|127\.0\.0\.1):8057/$#', $body['REQUEST_URI']);
    }

    public function testNoProxy()
    {
        putenv('no_proxy='.$_SERVER['no_proxy'] = 'example.com, localhost');

        try {
            $client = $this->getHttpClient(__FUNCTION__);
            $response = $client->request('GET', 'http://localhost:8057/', [
                'proxy' => 'http://localhost:8057',
            ]);

            $body = $response->toArray();

            $this->assertSame('HTTP/1.1', $body['SERVER_PROTOCOL']);
            $this->assertSame('/', $body['REQUEST_URI']);
            $this->assertSame('GET', $body['REQUEST_METHOD']);
        } finally {
            putenv('no_proxy');
            unset($_SERVER['no_proxy']);
        }
    }

    /**
     * @requires extension zlib
     */
    public function testAutoEncodingRequest()
    {
        $client = $this->getHttpClient(__FUNCTION__);
        $response = $client->request('GET', 'http://localhost:8057');

        $this->assertSame(200, $response->getStatusCode());

        $headers = $response->getHeaders();

        $this->assertSame(['Accept-Encoding'], $headers['vary']);
        $this->assertStringContainsString('gzip', $headers['content-encoding'][0]);

        $body = $response->toArray();

        $this->assertStringContainsString('gzip', $body['HTTP_ACCEPT_ENCODING']);
    }

    public function testBaseUri()
    {
        $client = $this->getHttpClient(__FUNCTION__);
        $response = $client->request('GET', '../404', [
            'base_uri' => 'http://localhost:8057/abc/',
        ]);

        $this->assertSame(404, $response->getStatusCode());
        $this->assertSame(['application/json'], $response->getHeaders(false)['content-type']);
    }

    public function testQuery()
    {
        $client = $this->getHttpClient(__FUNCTION__);
        $response = $client->request('GET', 'http://localhost:8057/?a=a', [
            'query' => ['b' => 'b'],
        ]);

        $body = $response->toArray();
        $this->assertSame('GET', $body['REQUEST_METHOD']);
        $this->assertSame('/?a=a&b=b', $body['REQUEST_URI']);
    }

    public function testInformationalResponse()
    {
        $client = $this->getHttpClient(__FUNCTION__);
        $response = $client->request('GET', 'http://localhost:8057/103');

        $this->assertSame('Here the body', $response->getContent());
        $this->assertSame(200, $response->getStatusCode());
    }

    public function testInformationalResponseStream()
    {
        $client = $this->getHttpClient(__FUNCTION__);
        $response = $client->request('GET', 'http://localhost:8057/103');

        $chunks = [];
        foreach ($client->stream($response) as $chunk) {
            $chunks[] = $chunk;
        }

        $this->assertSame(103, $chunks[0]->getInformationalStatus()[0]);
        $this->assertSame(['</style.css>; rel=preload; as=style', '</script.js>; rel=preload; as=script'], $chunks[0]->getInformationalStatus()[1]['link']);
        $this->assertTrue($chunks[1]->isFirst());
        $this->assertSame('Here the body', $chunks[2]->getContent());
        $this->assertTrue($chunks[3]->isLast());
        $this->assertNull($chunks[3]->getInformationalStatus());

        $this->assertSame(['date', 'content-length'], array_keys($response->getHeaders()));
        $this->assertContains('Link: </style.css>; rel=preload; as=style', $response->getInfo('response_headers'));
    }

    /**
     * @requires extension zlib
     */
    public function testUserlandEncodingRequest()
    {
        $client = $this->getHttpClient(__FUNCTION__);
        $response = $client->request('GET', 'http://localhost:8057', [
            'headers' => ['Accept-Encoding' => 'gzip'],
        ]);

        $headers = $response->getHeaders();

        $this->assertSame(['Accept-Encoding'], $headers['vary']);
        $this->assertStringContainsString('gzip', $headers['content-encoding'][0]);

        $body = $response->getContent();
        $this->assertSame("\x1F", $body[0]);

        $body = json_decode(gzdecode($body), true);
        $this->assertSame('gzip', $body['HTTP_ACCEPT_ENCODING']);
    }

    /**
     * @requires extension zlib
     */
    public function testGzipBroken()
    {
        $client = $this->getHttpClient(__FUNCTION__);
        $response = $client->request('GET', 'http://localhost:8057/gzip-broken');

        $this->expectException(TransportExceptionInterface::class);
        $response->getContent();
    }

    public function testMaxDuration()
    {
        $client = $this->getHttpClient(__FUNCTION__);
        $response = $client->request('GET', 'http://localhost:8057/max-duration', [
            'max_duration' => 0.1,
        ]);

        $start = microtime(true);

        try {
            $response->getContent();
        } catch (TransportExceptionInterface $e) {
            $this->addToAssertionCount(1);
        }

        $duration = microtime(true) - $start;

        $this->assertLessThan(10, $duration);
    }

    public function testWithOptions()
    {
        $client = $this->getHttpClient(__FUNCTION__);
        if (!method_exists($client, 'withOptions')) {
            $this->markTestSkipped(sprintf('Not implementing "%s::withOptions()" is deprecated.', get_debug_type($client)));
        }

        $client2 = $client->withOptions(['base_uri' => 'http://localhost:8057/']);

        $this->assertNotSame($client, $client2);
        $this->assertSame(\get_class($client), \get_class($client2));

        $response = $client2->request('GET', '/');
        $this->assertSame(200, $response->getStatusCode());
    }

    public function testBindToPort()
    {
        $client = $this->getHttpClient(__FUNCTION__);
        $response = $client->request('GET', 'http://localhost:8057', ['bindto' => '127.0.0.1:9876']);
        $response->getStatusCode();

        $vars = $response->toArray();

        self::assertSame('127.0.0.1', $vars['REMOTE_ADDR']);
        self::assertSame('9876', $vars['REMOTE_PORT']);
    }

    public function testBindToPortV6()
    {
        TestHttpServer::start(-8087);

        $client = $this->getHttpClient(__FUNCTION__);
        $response = $client->request('GET', 'http://[::1]:8087', ['bindto' => '[::1]:9876']);
        $response->getStatusCode();

        $vars = $response->toArray();

        self::assertSame('::1', $vars['REMOTE_ADDR']);

        if ('\\' !== \DIRECTORY_SEPARATOR) {
            self::assertSame('9876', $vars['REMOTE_PORT']);
        }
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <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\HttpClient\Test;

use Symfony\Component\Process\PhpExecutableFinder;
use Symfony\Component\Process\Process;

class TestHttpServer
{
    private static $process = [];

    /**
     * @return Process
     */
    public static function start(int $port = 8057)
    {
        if (0 > $port) {
            $port = -$port;
            $ip = '[::1]';
        } else {
            $ip = '127.0.0.1';
        }

        if (isset(self::$process[$port])) {
            self::$process[$port]->stop();
        } else {
            register_shutdown_function(static function () use ($port) {
                self::$process[$port]->stop();
            });
        }

        $finder = new PhpExecutableFinder();
        $process = new Process(array_merge([$finder->find(false)], $finder->findArguments(), ['-dopcache.enable=0', '-dvariables_order=EGPCS', '-S', $ip.':'.$port]));
        $process->setWorkingDirectory(__DIR__.'/Fixtures/web');
        $process->start();
        self::$process[$port] = $process;

        do {
            usleep(50000);
        } while (!@fopen('http://'.$ip.':'.$port, 'r'));

        return $process;
    }

    public static function stop(int $port = 8057)
    {
        if (isset(self::$process[$port])) {
            self::$process[$port]->stop();
        }
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <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\HttpClient;

use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;

/**
 * The interface of chunks returned by ResponseStreamInterface::current().
 *
 * When the chunk is first, last or timeout, the content MUST be empty.
 * When an unchecked timeout or a network error occurs, a TransportExceptionInterface
 * MUST be thrown by the destructor unless one was already thrown by another method.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
interface ChunkInterface
{
    /**
     * Tells when the idle timeout has been reached.
     *
     * @throws TransportExceptionInterface on a network error
     */
    public function isTimeout(): bool;

    /**
     * Tells when headers just arrived.
     *
     * @throws TransportExceptionInterface on a network error or when the idle timeout is reached
     */
    public function isFirst(): bool;

    /**
     * Tells when the body just completed.
     *
     * @throws TransportExceptionInterface on a network error or when the idle timeout is reached
     */
    public function isLast(): bool;

    /**
     * Returns a [status code, headers] tuple when a 1xx status code was just received.
     *
     * @throws TransportExceptionInterface on a network error or when the idle timeout is reached
     */
    public function getInformationalStatus(): ?array;

    /**
     * Returns the content of the response chunk.
     *
     * @throws TransportExceptionInterface on a network error or when the idle timeout is reached
     */
    public function getContent(): string;

    /**
     * Returns the offset of the chunk in the response body.
     */
    public function getOffset(): int;

    /**
     * In case of error, returns the message that describes it.
     */
    public function getError(): ?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\Contracts\HttpClient;

use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\Test\HttpClientTestCase;

/**
 * Provides flexible methods for requesting HTTP resources synchronously or asynchronously.
 *
 * @see HttpClientTestCase for a reference test suite
 *
 * @method static withOptions(array $options) Returns a new instance of the client with new default options
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
interface HttpClientInterface
{
    public const OPTIONS_DEFAULTS = [
        'auth_basic' => null,   // array|string - an array containing the username as first value, and optionally the
                                //   password as the second one; or string like username:password - enabling HTTP Basic
                                //   authentication (RFC 7617)
        'auth_bearer' => null,  // string - a token enabling HTTP Bearer authorization (RFC 6750)
        'query' => [],          // string[] - associative array of query string values to merge with the request's URL
        'headers' => [],        // iterable|string[]|string[][] - headers names provided as keys or as part of values
        'body' => '',           // array|string|resource|\Traversable|\Closure - the callback SHOULD yield a string
                                //   smaller than the amount requested as argument; the empty string signals EOF; if
                                //   an array is passed, it is meant as a form payload of field names and values
        'json' => null,         // mixed - if set, implementations MUST set the "body" option to the JSON-encoded
                                //   value and set the "content-type" header to a JSON-compatible value if it is not
                                //   explicitly defined in the headers option - typically "application/json"
        'user_data' => null,    // mixed - any extra data to attach to the request (scalar, callable, object...) that
                                //   MUST be available via $response->getInfo('user_data') - not used internally
        'max_redirects' => 20,  // int - the maximum number of redirects to follow; a value lower than or equal to 0
                                //   means redirects should not be followed; "Authorization" and "Cookie" headers MUST
                                //   NOT follow except for the initial host name
        'http_version' => null, // string - defaults to the best supported version, typically 1.1 or 2.0
        'base_uri' => null,     // string - the URI to resolve relative URLs, following rules in RFC 3986, section 2
        'buffer' => true,       // bool|resource|\Closure - whether the content of the response should be buffered or not,
                                //   or a stream resource where the response body should be written,
                                //   or a closure telling if/where the response should be buffered based on its headers
        'on_progress' => null,  // callable(int $dlNow, int $dlSize, array $info) - throwing any exceptions MUST abort the
                                //   request; it MUST be called on connection, on headers and on completion; it SHOULD be
                                //   called on upload/download of data and at least 1/s
        'resolve' => [],        // string[] - a map of host to IP address that SHOULD replace DNS resolution
        'proxy' => null,        // string - by default, the proxy-related env vars handled by curl SHOULD be honored
        'no_proxy' => null,     // string - a comma separated list of hosts that do not require a proxy to be reached
        'timeout' => null,      // float - the idle timeout (in seconds) - defaults to ini_get('default_socket_timeout')
        'max_duration' => 0,    // float - the maximum execution time (in seconds) for the request+response as a whole;
                                //   a value lower than or equal to 0 means it is unlimited
        'bindto' => '0',        // string - the interface or the local socket to bind to
        'verify_peer' => true,  // see https://php.net/context.ssl for the following options
        'verify_host' => true,
        'cafile' => null,
        'capath' => null,
        'local_cert' => null,
        'local_pk' => null,
        'passphrase' => null,
        'ciphers' => null,
        'peer_fingerprint' => null,
        'capture_peer_cert_chain' => false,
        'extra' => [],          // array - additional options that can be ignored if unsupported, unlike regular options
    ];

    /**
     * Requests an HTTP resource.
     *
     * Responses MUST be lazy, but their status code MUST be
     * checked even if none of their public methods are called.
     *
     * Implementations are not required to support all options described above; they can also
     * support more custom options; but in any case, they MUST throw a TransportExceptionInterface
     * when an unsupported option is passed.
     *
     * @throws TransportExceptionInterface When an unsupported option is passed
     */
    public function request(string $method, string $url, array $options = []): ResponseInterface;

    /**
     * Yields responses chunk by chunk as they complete.
     *
     * @param ResponseInterface|iterable<array-key, ResponseInterface> $responses One or more responses created by the current HTTP client
     * @param float|null                                               $timeout   The idle timeout before yielding timeout chunks
     */
    public function stream($responses, ?float $timeout = null): ResponseStreamInterface;
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <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>
 */
interface ServiceProviderInterface extends ContainerInterface
{
    /**
     * 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 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;

use Psr\Container\ContainerInterface;
use Symfony\Contracts\Service\Attribute\SubscribedService;

/**
 * Implementation of ServiceSubscriberInterface that determines subscribed services from
 * method return types. Service ids are available as "ClassName::methodName".
 *
 * @author Kevin Bond <kevinbond@gmail.com>
 */
trait ServiceSubscriberTrait
{
    /** @var ContainerInterface */
    protected $container;

    /**
     * {@inheritdoc}
     */
    public static function getSubscribedServices(): array
    {
        $services = method_exists(get_parent_class(self::class) ?: '', __FUNCTION__) ? parent::getSubscribedServices() : [];
        $attributeOptIn = false;

        if (\PHP_VERSION_ID >= 80000) {
            foreach ((new \ReflectionClass(self::class))->getMethods() as $method) {
                if (self::class !== $method->getDeclaringClass()->name) {
                    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));
                }

                $serviceId = $returnType instanceof \ReflectionNamedType ? $returnType->getName() : (string) $returnType;

                if ($returnType->allowsNull()) {
                    $serviceId = '?'.$serviceId;
                }

                $services[$attribute->newInstance()->key ?? self::class.'::'.$method->name] = $serviceId;
                $attributeOptIn = true;
            }
        }

        if (!$attributeOptIn) {
            foreach ((new \ReflectionClass(self::class))->getMethods() as $method) {
                if ($method->isStatic() || $method->isAbstract() || $method->isGenerator() || $method->isInternal() || $method->getNumberOfRequiredParameters()) {
                    continue;
                }

                if (self::class !== $method->getDeclaringClass()->name) {
                    continue;
                }

                if (!($returnType = $method->getReturnType()) instanceof \ReflectionNamedType) {
                    continue;
                }

                if ($returnType->isBuiltin()) {
                    continue;
                }

                if (\PHP_VERSION_ID >= 80000) {
                    trigger_deprecation('symfony/service-contracts', '2.5', 'Using "%s" in "%s" without using the "%s" attribute on any method is deprecated.', ServiceSubscriberTrait::class, self::class, SubscribedService::class);
                }

                $services[self::class.'::'.$method->name] = '?'.($returnType instanceof \ReflectionNamedType ? $returnType->getName() : $returnType);
            }
        }

        return $services;
    }

    /**
     * @required
     *
     * @return ContainerInterface|null
     */
    public function setContainer(ContainerInterface $container)
    {
        $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;

/**
 * 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 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']
     *
     * @return string[] The required service types, optionally keyed by service names
     */
    public static function getSubscribedServices();
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <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
{
    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\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 $factories;
    private $loading = [];
    private $providedTypes;

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

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function has(string $id)
    {
        return isset($this->factories[$id]);
    }

    /**
     * {@inheritdoc}
     *
     * @return mixed
     */
    public function get(string $id)
    {
        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]);
        }
    }

    /**
     * {@inheritdoc}
     */
    public function getProvidedServices(): array
    {
        if (null === $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\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\ServiceSubscriberTrait;

/**
 * Use with {@see ServiceSubscriberTrait} to mark a method's return type
 * as a subscribed service.
 *
 * @author Kevin Bond <kevinbond@gmail.com>
 */
#[\Attribute(\Attribute::TARGET_METHOD)]
final class SubscribedService
{
    /**
     * @param string|null $key The key to use for the service
     *                         If null, use "ClassName::methodName"
     */
    public function __construct(
        public ?string $key = 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\Contracts\Service\Test;

class_alias(ServiceLocatorTestCase::class, ServiceLocatorTest::class);

if (false) {
    /**
     * @deprecated since PHPUnit 9.6
     */
    class ServiceLocatorTest
    {
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <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\ContainerInterface;
use Symfony\Contracts\Service\ServiceLocatorTrait;

abstract class ServiceLocatorTestCase extends TestCase
{
    /**
     * @return ContainerInterface
     */
    protected function getServiceLocator(array $factories)
    {
        return new class($factories) implements ContainerInterface {
            use ServiceLocatorTrait;
        };
    }

    public function testHas()
    {
        $locator = $this->getServiceLocator([
            'foo' => function () { return 'bar'; },
            'bar' => function () { return 'baz'; },
            function () { return 'dummy'; },
        ]);

        $this->assertTrue($locator->has('foo'));
        $this->assertTrue($locator->has('bar'));
        $this->assertFalse($locator->has('dummy'));
    }

    public function testGet()
    {
        $locator = $this->getServiceLocator([
            'foo' => function () { return 'bar'; },
            'bar' => function () { return 'baz'; },
        ]);

        $this->assertSame('bar', $locator->get('foo'));
        $this->assertSame('baz', $locator->get('bar'));
    }

    public function testGetDoesNotMemoize()
    {
        $i = 0;
        $locator = $this->getServiceLocator([
            'foo' => 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()
    {
        if (!$this->getExpectedException()) {
            $this->expectException(\Psr\Container\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 = $this->getServiceLocator([
            'foo' => function () use (&$locator) { return $locator->get('bar'); },
        ]);

        $locator->get('foo');
    }

    public function testThrowsOnCircularReference()
    {
        $this->expectException(\Psr\Container\ContainerExceptionInterface::class);
        $this->expectExceptionMessage('Circular reference detected for service "bar", path: "bar -> baz -> bar".');
        $locator = $this->getServiceLocator([
            'foo' => function () use (&$locator) { return $locator->get('bar'); },
            'bar' => function () use (&$locator) { return $locator->get('baz'); },
            'baz' => function () use (&$locator) { return $locator->get('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\Component\ExpressionLanguage;

/**
 * Represents a token stream.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class TokenStream
{
    public $current;

    private $tokens;
    private $position = 0;
    private $expression;

    public function __construct(array $tokens, string $expression = '')
    {
        $this->tokens = $tokens;
        $this->current = $tokens[0];
        $this->expression = $expression;
    }

    /**
     * Returns a string representation of the token stream.
     *
     * @return string
     */
    public function __toString()
    {
        return implode("\n", $this->tokens);
    }

    /**
     * Sets the pointer to the next token and returns the old one.
     */
    public function next()
    {
        ++$this->position;

        if (!isset($this->tokens[$this->position])) {
            throw new SyntaxError('Unexpected end of expression.', $this->current->cursor, $this->expression);
        }

        $this->current = $this->tokens[$this->position];
    }

    /**
     * @param string|null $message The syntax error message
     */
    public function expect(string $type, ?string $value = null, ?string $message = null)
    {
        $token = $this->current;
        if (!$token->test($type, $value)) {
            throw new SyntaxError(sprintf('%sUnexpected token "%s" of value "%s" ("%s" expected%s).', $message ? $message.'. ' : '', $token->type, $token->value, $type, $value ? sprintf(' with value "%s"', $value) : ''), $token->cursor, $this->expression);
        }
        $this->next();
    }

    /**
     * Checks if end of stream was reached.
     *
     * @return bool
     */
    public function isEOF()
    {
        return Token::EOF_TYPE === $this->current->type;
    }

    /**
     * @internal
     */
    public function getExpression(): string
    {
        return $this->expression;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) 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.');
}

$operators = ['not', '!', 'or', '||', '&&', 'and', '|', '^', '&', '==', '===', '!=', '!==', '<', '>', '>=', '<=', 'not in', 'in', '..', '+', '-', '~', '*', '/', '%', 'matches', '**'];
$operators = array_combine($operators, array_map('strlen', $operators));
arsort($operators);

$regex = [];
foreach ($operators as $operator => $length) {
    // Collisions of character operators:
    // - an operator that begins with a character must have a space or a parenthesis before or starting at the beginning of a string
    // - an operator that ends with a character must be followed by a whitespace or a parenthesis
    $regex[] =
        (ctype_alpha($operator[0]) ? '(?<=^|[\s(])' : '')
        .preg_quote($operator, '/')
        .(ctype_alpha($operator[$length - 1]) ? '(?=[\s(])' : '');
}

echo '/'.implode('|', $regex).'/A';
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\ExpressionLanguage;

/**
 * @author Fabien Potencier <fabien@symfony.com>
 */
interface ExpressionFunctionProviderInterface
{
    /**
     * @return ExpressionFunction[]
     */
    public function getFunctions();
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\ExpressionLanguage;

/**
 * Represents an expression.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class Expression
{
    protected $expression;

    public function __construct(string $expression)
    {
        $this->expression = $expression;
    }

    /**
     * Gets the expression.
     *
     * @return string
     */
    public function __toString()
    {
        return $this->expression;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\ExpressionLanguage;

/**
 * Represents a function that can be used in an expression.
 *
 * A function is defined by two PHP callables. The callables are used
 * by the language to compile and/or evaluate the function.
 *
 * The "compiler" function is used at compilation time and must return a
 * PHP representation of the function call (it receives the function
 * arguments as arguments).
 *
 * The "evaluator" function is used for expression evaluation and must return
 * the value of the function call based on the values defined for the
 * expression (it receives the values as a first argument and the function
 * arguments as remaining arguments).
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class ExpressionFunction
{
    private $name;
    private $compiler;
    private $evaluator;

    /**
     * @param string   $name      The function name
     * @param callable $compiler  A callable able to compile the function
     * @param callable $evaluator A callable able to evaluate the function
     */
    public function __construct(string $name, callable $compiler, callable $evaluator)
    {
        $this->name = $name;
        $this->compiler = $compiler instanceof \Closure ? $compiler : \Closure::fromCallable($compiler);
        $this->evaluator = $evaluator instanceof \Closure ? $evaluator : \Closure::fromCallable($evaluator);
    }

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

    /**
     * @return \Closure
     */
    public function getCompiler()
    {
        return $this->compiler;
    }

    /**
     * @return \Closure
     */
    public function getEvaluator()
    {
        return $this->evaluator;
    }

    /**
     * Creates an ExpressionFunction from a PHP function name.
     *
     * @param string|null $expressionFunctionName The expression function name (default: same than the PHP function name)
     *
     * @return self
     *
     * @throws \InvalidArgumentException if given PHP function name does not exist
     * @throws \InvalidArgumentException if given PHP function name is in namespace
     *                                   and expression function name is not defined
     */
    public static function fromPhp(string $phpFunctionName, ?string $expressionFunctionName = null)
    {
        $phpFunctionName = ltrim($phpFunctionName, '\\');
        if (!\function_exists($phpFunctionName)) {
            throw new \InvalidArgumentException(sprintf('PHP function "%s" does not exist.', $phpFunctionName));
        }

        $parts = explode('\\', $phpFunctionName);
        if (!$expressionFunctionName && \count($parts) > 1) {
            throw new \InvalidArgumentException(sprintf('An expression function name must be defined when PHP function "%s" is namespaced.', $phpFunctionName));
        }

        $compiler = function (...$args) use ($phpFunctionName) {
            return sprintf('\%s(%s)', $phpFunctionName, implode(', ', $args));
        };

        $evaluator = function ($p, ...$args) use ($phpFunctionName) {
            return $phpFunctionName(...$args);
        };

        return new self($expressionFunctionName ?: end($parts), $compiler, $evaluator);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\ExpressionLanguage;

/**
 * Represents an already parsed expression.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class SerializedParsedExpression extends ParsedExpression
{
    private $nodes;

    /**
     * @param string $expression An expression
     * @param string $nodes      The serialized nodes for the expression
     */
    public function __construct(string $expression, string $nodes)
    {
        $this->expression = $expression;
        $this->nodes = $nodes;
    }

    public function getNodes()
    {
        return unserialize($this->nodes);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\ExpressionLanguage;

/**
 * Parsers a token stream.
 *
 * This parser implements a "Precedence climbing" algorithm.
 *
 * @see http://www.engr.mun.ca/~theo/Misc/exp_parsing.htm
 * @see http://en.wikipedia.org/wiki/Operator-precedence_parser
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class Parser
{
    public const OPERATOR_LEFT = 1;
    public const OPERATOR_RIGHT = 2;

    private $stream;
    private $unaryOperators;
    private $binaryOperators;
    private $functions;
    private $names;
    private $lint;

    public function __construct(array $functions)
    {
        $this->functions = $functions;

        $this->unaryOperators = [
            'not' => ['precedence' => 50],
            '!' => ['precedence' => 50],
            '-' => ['precedence' => 500],
            '+' => ['precedence' => 500],
        ];
        $this->binaryOperators = [
            'or' => ['precedence' => 10, 'associativity' => self::OPERATOR_LEFT],
            '||' => ['precedence' => 10, 'associativity' => self::OPERATOR_LEFT],
            'and' => ['precedence' => 15, 'associativity' => self::OPERATOR_LEFT],
            '&&' => ['precedence' => 15, 'associativity' => self::OPERATOR_LEFT],
            '|' => ['precedence' => 16, 'associativity' => self::OPERATOR_LEFT],
            '^' => ['precedence' => 17, 'associativity' => self::OPERATOR_LEFT],
            '&' => ['precedence' => 18, 'associativity' => self::OPERATOR_LEFT],
            '==' => ['precedence' => 20, 'associativity' => self::OPERATOR_LEFT],
            '===' => ['precedence' => 20, 'associativity' => self::OPERATOR_LEFT],
            '!=' => ['precedence' => 20, 'associativity' => self::OPERATOR_LEFT],
            '!==' => ['precedence' => 20, 'associativity' => self::OPERATOR_LEFT],
            '<' => ['precedence' => 20, 'associativity' => self::OPERATOR_LEFT],
            '>' => ['precedence' => 20, 'associativity' => self::OPERATOR_LEFT],
            '>=' => ['precedence' => 20, 'associativity' => self::OPERATOR_LEFT],
            '<=' => ['precedence' => 20, 'associativity' => self::OPERATOR_LEFT],
            'not in' => ['precedence' => 20, 'associativity' => self::OPERATOR_LEFT],
            'in' => ['precedence' => 20, 'associativity' => self::OPERATOR_LEFT],
            'matches' => ['precedence' => 20, 'associativity' => self::OPERATOR_LEFT],
            '..' => ['precedence' => 25, 'associativity' => self::OPERATOR_LEFT],
            '+' => ['precedence' => 30, 'associativity' => self::OPERATOR_LEFT],
            '-' => ['precedence' => 30, 'associativity' => self::OPERATOR_LEFT],
            '~' => ['precedence' => 40, 'associativity' => self::OPERATOR_LEFT],
            '*' => ['precedence' => 60, 'associativity' => self::OPERATOR_LEFT],
            '/' => ['precedence' => 60, 'associativity' => self::OPERATOR_LEFT],
            '%' => ['precedence' => 60, 'associativity' => self::OPERATOR_LEFT],
            '**' => ['precedence' => 200, 'associativity' => self::OPERATOR_RIGHT],
        ];
    }

    /**
     * Converts a token stream to a node tree.
     *
     * The valid names is an array where the values
     * are the names that the user can use in an expression.
     *
     * If the variable name in the compiled PHP code must be
     * different, define it as the key.
     *
     * For instance, ['this' => 'container'] means that the
     * variable 'container' can be used in the expression
     * but the compiled code will use 'this'.
     *
     * @return Node\Node
     *
     * @throws SyntaxError
     */
    public function parse(TokenStream $stream, array $names = [])
    {
        $this->lint = false;

        return $this->doParse($stream, $names);
    }

    /**
     * Validates the syntax of an expression.
     *
     * The syntax of the passed expression will be checked, but not parsed.
     * If you want to skip checking dynamic variable names, pass `null` instead of the array.
     *
     * @throws SyntaxError When the passed expression is invalid
     */
    public function lint(TokenStream $stream, ?array $names = []): void
    {
        $this->lint = true;
        $this->doParse($stream, $names);
    }

    /**
     * @throws SyntaxError
     */
    private function doParse(TokenStream $stream, ?array $names = []): Node\Node
    {
        $this->stream = $stream;
        $this->names = $names;

        $node = $this->parseExpression();
        if (!$stream->isEOF()) {
            throw new SyntaxError(sprintf('Unexpected token "%s" of value "%s".', $stream->current->type, $stream->current->value), $stream->current->cursor, $stream->getExpression());
        }

        $this->stream = null;
        $this->names = null;

        return $node;
    }

    public function parseExpression(int $precedence = 0)
    {
        $expr = $this->getPrimary();
        $token = $this->stream->current;
        while ($token->test(Token::OPERATOR_TYPE) && isset($this->binaryOperators[$token->value]) && $this->binaryOperators[$token->value]['precedence'] >= $precedence) {
            $op = $this->binaryOperators[$token->value];
            $this->stream->next();

            $expr1 = $this->parseExpression(self::OPERATOR_LEFT === $op['associativity'] ? $op['precedence'] + 1 : $op['precedence']);
            $expr = new Node\BinaryNode($token->value, $expr, $expr1);

            $token = $this->stream->current;
        }

        if (0 === $precedence) {
            return $this->parseConditionalExpression($expr);
        }

        return $expr;
    }

    protected function getPrimary()
    {
        $token = $this->stream->current;

        if ($token->test(Token::OPERATOR_TYPE) && isset($this->unaryOperators[$token->value])) {
            $operator = $this->unaryOperators[$token->value];
            $this->stream->next();
            $expr = $this->parseExpression($operator['precedence']);

            return $this->parsePostfixExpression(new Node\UnaryNode($token->value, $expr));
        }

        if ($token->test(Token::PUNCTUATION_TYPE, '(')) {
            $this->stream->next();
            $expr = $this->parseExpression();
            $this->stream->expect(Token::PUNCTUATION_TYPE, ')', 'An opened parenthesis is not properly closed');

            return $this->parsePostfixExpression($expr);
        }

        return $this->parsePrimaryExpression();
    }

    protected function parseConditionalExpression(Node\Node $expr)
    {
        while ($this->stream->current->test(Token::PUNCTUATION_TYPE, '?')) {
            $this->stream->next();
            if (!$this->stream->current->test(Token::PUNCTUATION_TYPE, ':')) {
                $expr2 = $this->parseExpression();
                if ($this->stream->current->test(Token::PUNCTUATION_TYPE, ':')) {
                    $this->stream->next();
                    $expr3 = $this->parseExpression();
                } else {
                    $expr3 = new Node\ConstantNode(null);
                }
            } else {
                $this->stream->next();
                $expr2 = $expr;
                $expr3 = $this->parseExpression();
            }

            $expr = new Node\ConditionalNode($expr, $expr2, $expr3);
        }

        return $expr;
    }

    public function parsePrimaryExpression()
    {
        $token = $this->stream->current;
        switch ($token->type) {
            case Token::NAME_TYPE:
                $this->stream->next();
                switch ($token->value) {
                    case 'true':
                    case 'TRUE':
                        return new Node\ConstantNode(true);

                    case 'false':
                    case 'FALSE':
                        return new Node\ConstantNode(false);

                    case 'null':
                    case 'NULL':
                        return new Node\ConstantNode(null);

                    default:
                        if ('(' === $this->stream->current->value) {
                            if (false === isset($this->functions[$token->value])) {
                                throw new SyntaxError(sprintf('The function "%s" does not exist.', $token->value), $token->cursor, $this->stream->getExpression(), $token->value, array_keys($this->functions));
                            }

                            $node = new Node\FunctionNode($token->value, $this->parseArguments());
                        } else {
                            if (!$this->lint || \is_array($this->names)) {
                                if (!\in_array($token->value, $this->names, true)) {
                                    throw new SyntaxError(sprintf('Variable "%s" is not valid.', $token->value), $token->cursor, $this->stream->getExpression(), $token->value, $this->names);
                                }

                                // is the name used in the compiled code different
                                // from the name used in the expression?
                                if (\is_int($name = array_search($token->value, $this->names))) {
                                    $name = $token->value;
                                }
                            } else {
                                $name = $token->value;
                            }

                            $node = new Node\NameNode($name);
                        }
                }
                break;

            case Token::NUMBER_TYPE:
            case Token::STRING_TYPE:
                $this->stream->next();

                return new Node\ConstantNode($token->value);

            default:
                if ($token->test(Token::PUNCTUATION_TYPE, '[')) {
                    $node = $this->parseArrayExpression();
                } elseif ($token->test(Token::PUNCTUATION_TYPE, '{')) {
                    $node = $this->parseHashExpression();
                } else {
                    throw new SyntaxError(sprintf('Unexpected token "%s" of value "%s".', $token->type, $token->value), $token->cursor, $this->stream->getExpression());
                }
        }

        return $this->parsePostfixExpression($node);
    }

    public function parseArrayExpression()
    {
        $this->stream->expect(Token::PUNCTUATION_TYPE, '[', 'An array element was expected');

        $node = new Node\ArrayNode();
        $first = true;
        while (!$this->stream->current->test(Token::PUNCTUATION_TYPE, ']')) {
            if (!$first) {
                $this->stream->expect(Token::PUNCTUATION_TYPE, ',', 'An array element must be followed by a comma');

                // trailing ,?
                if ($this->stream->current->test(Token::PUNCTUATION_TYPE, ']')) {
                    break;
                }
            }
            $first = false;

            $node->addElement($this->parseExpression());
        }
        $this->stream->expect(Token::PUNCTUATION_TYPE, ']', 'An opened array is not properly closed');

        return $node;
    }

    public function parseHashExpression()
    {
        $this->stream->expect(Token::PUNCTUATION_TYPE, '{', 'A hash element was expected');

        $node = new Node\ArrayNode();
        $first = true;
        while (!$this->stream->current->test(Token::PUNCTUATION_TYPE, '}')) {
            if (!$first) {
                $this->stream->expect(Token::PUNCTUATION_TYPE, ',', 'A hash value must be followed by a comma');

                // trailing ,?
                if ($this->stream->current->test(Token::PUNCTUATION_TYPE, '}')) {
                    break;
                }
            }
            $first = false;

            // a hash key can be:
            //
            //  * a number -- 12
            //  * a string -- 'a'
            //  * a name, which is equivalent to a string -- a
            //  * an expression, which must be enclosed in parentheses -- (1 + 2)
            if ($this->stream->current->test(Token::STRING_TYPE) || $this->stream->current->test(Token::NAME_TYPE) || $this->stream->current->test(Token::NUMBER_TYPE)) {
                $key = new Node\ConstantNode($this->stream->current->value);
                $this->stream->next();
            } elseif ($this->stream->current->test(Token::PUNCTUATION_TYPE, '(')) {
                $key = $this->parseExpression();
            } else {
                $current = $this->stream->current;

                throw new SyntaxError(sprintf('A hash key must be a quoted string, a number, a name, or an expression enclosed in parentheses (unexpected token "%s" of value "%s".', $current->type, $current->value), $current->cursor, $this->stream->getExpression());
            }

            $this->stream->expect(Token::PUNCTUATION_TYPE, ':', 'A hash key must be followed by a colon (:)');
            $value = $this->parseExpression();

            $node->addElement($value, $key);
        }
        $this->stream->expect(Token::PUNCTUATION_TYPE, '}', 'An opened hash is not properly closed');

        return $node;
    }

    public function parsePostfixExpression(Node\Node $node)
    {
        $token = $this->stream->current;
        while (Token::PUNCTUATION_TYPE == $token->type) {
            if ('.' === $token->value) {
                $this->stream->next();
                $token = $this->stream->current;
                $this->stream->next();

                if (
                    Token::NAME_TYPE !== $token->type
                    &&
                    // Operators like "not" and "matches" are valid method or property names,
                    //
                    // In other words, besides NAME_TYPE, OPERATOR_TYPE could also be parsed as a property or method.
                    // This is because operators are processed by the lexer prior to names. So "not" in "foo.not()" or "matches" in "foo.matches" will be recognized as an operator first.
                    // But in fact, "not" and "matches" in such expressions shall be parsed as method or property names.
                    //
                    // And this ONLY works if the operator consists of valid characters for a property or method name.
                    //
                    // Other types, such as STRING_TYPE and NUMBER_TYPE, can't be parsed as property nor method names.
                    //
                    // As a result, if $token is NOT an operator OR $token->value is NOT a valid property or method name, an exception shall be thrown.
                    (Token::OPERATOR_TYPE !== $token->type || !preg_match('/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/A', $token->value))
                ) {
                    throw new SyntaxError('Expected name.', $token->cursor, $this->stream->getExpression());
                }

                $arg = new Node\ConstantNode($token->value, true);

                $arguments = new Node\ArgumentsNode();
                if ($this->stream->current->test(Token::PUNCTUATION_TYPE, '(')) {
                    $type = Node\GetAttrNode::METHOD_CALL;
                    foreach ($this->parseArguments()->nodes as $n) {
                        $arguments->addElement($n);
                    }
                } else {
                    $type = Node\GetAttrNode::PROPERTY_CALL;
                }

                $node = new Node\GetAttrNode($node, $arg, $arguments, $type);
            } elseif ('[' === $token->value) {
                $this->stream->next();
                $arg = $this->parseExpression();
                $this->stream->expect(Token::PUNCTUATION_TYPE, ']');

                $node = new Node\GetAttrNode($node, $arg, new Node\ArgumentsNode(), Node\GetAttrNode::ARRAY_CALL);
            } else {
                break;
            }

            $token = $this->stream->current;
        }

        return $node;
    }

    /**
     * Parses arguments.
     */
    public function parseArguments()
    {
        $args = [];
        $this->stream->expect(Token::PUNCTUATION_TYPE, '(', 'A list of arguments must begin with an opening parenthesis');
        while (!$this->stream->current->test(Token::PUNCTUATION_TYPE, ')')) {
            if (!empty($args)) {
                $this->stream->expect(Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma');
            }

            $args[] = $this->parseExpression();
        }
        $this->stream->expect(Token::PUNCTUATION_TYPE, ')', 'A list of arguments must be closed by a parenthesis');

        return new Node\Node($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\ExpressionLanguage;

/**
 * Lexes an expression.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class Lexer
{
    /**
     * Tokenizes an expression.
     *
     * @return TokenStream
     *
     * @throws SyntaxError
     */
    public function tokenize(string $expression)
    {
        $expression = str_replace(["\r", "\n", "\t", "\v", "\f"], ' ', $expression);
        $cursor = 0;
        $tokens = [];
        $brackets = [];
        $end = \strlen($expression);

        while ($cursor < $end) {
            if (' ' == $expression[$cursor]) {
                ++$cursor;

                continue;
            }

            if (preg_match('/[0-9]+(?:\.[0-9]+)?([Ee][\+\-][0-9]+)?/A', $expression, $match, 0, $cursor)) {
                // numbers
                $number = (float) $match[0];  // floats
                if (preg_match('/^[0-9]+$/', $match[0]) && $number <= \PHP_INT_MAX) {
                    $number = (int) $match[0]; // integers lower than the maximum
                }
                $tokens[] = new Token(Token::NUMBER_TYPE, $number, $cursor + 1);
                $cursor += \strlen($match[0]);
            } elseif (false !== strpos('([{', $expression[$cursor])) {
                // opening bracket
                $brackets[] = [$expression[$cursor], $cursor];

                $tokens[] = new Token(Token::PUNCTUATION_TYPE, $expression[$cursor], $cursor + 1);
                ++$cursor;
            } elseif (false !== strpos(')]}', $expression[$cursor])) {
                // closing bracket
                if (empty($brackets)) {
                    throw new SyntaxError(sprintf('Unexpected "%s".', $expression[$cursor]), $cursor, $expression);
                }

                [$expect, $cur] = array_pop($brackets);
                if ($expression[$cursor] != strtr($expect, '([{', ')]}')) {
                    throw new SyntaxError(sprintf('Unclosed "%s".', $expect), $cur, $expression);
                }

                $tokens[] = new Token(Token::PUNCTUATION_TYPE, $expression[$cursor], $cursor + 1);
                ++$cursor;
            } elseif (preg_match('/"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\'/As', $expression, $match, 0, $cursor)) {
                // strings
                $tokens[] = new Token(Token::STRING_TYPE, stripcslashes(substr($match[0], 1, -1)), $cursor + 1);
                $cursor += \strlen($match[0]);
            } elseif (preg_match('/(?<=^|[\s(])not in(?=[\s(])|\!\=\=|(?<=^|[\s(])not(?=[\s(])|(?<=^|[\s(])and(?=[\s(])|\=\=\=|\>\=|(?<=^|[\s(])or(?=[\s(])|\<\=|\*\*|\.\.|(?<=^|[\s(])in(?=[\s(])|&&|\|\||(?<=^|[\s(])matches|\=\=|\!\=|\*|~|%|\/|\>|\||\!|\^|&|\+|\<|\-/A', $expression, $match, 0, $cursor)) {
                // operators
                $tokens[] = new Token(Token::OPERATOR_TYPE, $match[0], $cursor + 1);
                $cursor += \strlen($match[0]);
            } elseif (false !== strpos('.,?:', $expression[$cursor])) {
                // punctuation
                $tokens[] = new Token(Token::PUNCTUATION_TYPE, $expression[$cursor], $cursor + 1);
                ++$cursor;
            } elseif (preg_match('/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/A', $expression, $match, 0, $cursor)) {
                // names
                $tokens[] = new Token(Token::NAME_TYPE, $match[0], $cursor + 1);
                $cursor += \strlen($match[0]);
            } else {
                // unlexable
                throw new SyntaxError(sprintf('Unexpected character "%s".', $expression[$cursor]), $cursor, $expression);
            }
        }

        $tokens[] = new Token(Token::EOF_TYPE, null, $cursor + 1);

        if (!empty($brackets)) {
            [$expect, $cur] = array_pop($brackets);
            throw new SyntaxError(sprintf('Unclosed "%s".', $expect), $cur, $expression);
        }

        return new TokenStream($tokens, $expression);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\ExpressionLanguage;

use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Cache\Adapter\ArrayAdapter;

// Help opcache.preload discover always-needed symbols
class_exists(ParsedExpression::class);

/**
 * Allows to compile and evaluate expressions written in your own DSL.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class ExpressionLanguage
{
    private $cache;
    private $lexer;
    private $parser;
    private $compiler;

    protected $functions = [];

    /**
     * @param ExpressionFunctionProviderInterface[] $providers
     */
    public function __construct(?CacheItemPoolInterface $cache = null, array $providers = [])
    {
        $this->cache = $cache ?? new ArrayAdapter();
        $this->registerFunctions();
        foreach ($providers as $provider) {
            $this->registerProvider($provider);
        }
    }

    /**
     * Compiles an expression source code.
     *
     * @param Expression|string $expression The expression to compile
     *
     * @return string
     */
    public function compile($expression, array $names = [])
    {
        return $this->getCompiler()->compile($this->parse($expression, $names)->getNodes())->getSource();
    }

    /**
     * Evaluate an expression.
     *
     * @param Expression|string $expression The expression to compile
     *
     * @return mixed
     */
    public function evaluate($expression, array $values = [])
    {
        return $this->parse($expression, array_keys($values))->getNodes()->evaluate($this->functions, $values);
    }

    /**
     * Parses an expression.
     *
     * @param Expression|string $expression The expression to parse
     *
     * @return ParsedExpression
     */
    public function parse($expression, array $names)
    {
        if ($expression instanceof ParsedExpression) {
            return $expression;
        }

        asort($names);
        $cacheKeyItems = [];

        foreach ($names as $nameKey => $name) {
            $cacheKeyItems[] = \is_int($nameKey) ? $name : $nameKey.':'.$name;
        }

        $cacheItem = $this->cache->getItem(rawurlencode($expression.'//'.implode('|', $cacheKeyItems)));

        if (null === $parsedExpression = $cacheItem->get()) {
            $nodes = $this->getParser()->parse($this->getLexer()->tokenize((string) $expression), $names);
            $parsedExpression = new ParsedExpression((string) $expression, $nodes);

            $cacheItem->set($parsedExpression);
            $this->cache->save($cacheItem);
        }

        return $parsedExpression;
    }

    /**
     * Validates the syntax of an expression.
     *
     * @param Expression|string $expression The expression to validate
     * @param array|null        $names      The list of acceptable variable names in the expression, or null to accept any names
     *
     * @throws SyntaxError When the passed expression is invalid
     */
    public function lint($expression, ?array $names): void
    {
        if ($expression instanceof ParsedExpression) {
            return;
        }

        $this->getParser()->lint($this->getLexer()->tokenize((string) $expression), $names);
    }

    /**
     * Registers a function.
     *
     * @param callable $compiler  A callable able to compile the function
     * @param callable $evaluator A callable able to evaluate the function
     *
     * @throws \LogicException when registering a function after calling evaluate(), compile() or parse()
     *
     * @see ExpressionFunction
     */
    public function register(string $name, callable $compiler, callable $evaluator)
    {
        if (null !== $this->parser) {
            throw new \LogicException('Registering functions after calling evaluate(), compile() or parse() is not supported.');
        }

        $this->functions[$name] = ['compiler' => $compiler, 'evaluator' => $evaluator];
    }

    public function addFunction(ExpressionFunction $function)
    {
        $this->register($function->getName(), $function->getCompiler(), $function->getEvaluator());
    }

    public function registerProvider(ExpressionFunctionProviderInterface $provider)
    {
        foreach ($provider->getFunctions() as $function) {
            $this->addFunction($function);
        }
    }

    protected function registerFunctions()
    {
        $this->addFunction(ExpressionFunction::fromPhp('constant'));
    }

    private function getLexer(): Lexer
    {
        if (null === $this->lexer) {
            $this->lexer = new Lexer();
        }

        return $this->lexer;
    }

    private function getParser(): Parser
    {
        if (null === $this->parser) {
            $this->parser = new Parser($this->functions);
        }

        return $this->parser;
    }

    private function getCompiler(): Compiler
    {
        if (null === $this->compiler) {
            $this->compiler = new Compiler($this->functions);
        }

        return $this->compiler->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\Component\ExpressionLanguage;

class SyntaxError extends \LogicException
{
    public function __construct(string $message, int $cursor = 0, string $expression = '', ?string $subject = null, ?array $proposals = null)
    {
        $message = sprintf('%s around position %d', rtrim($message, '.'), $cursor);
        if ($expression) {
            $message = sprintf('%s for expression `%s`', $message, $expression);
        }
        $message .= '.';

        if (null !== $subject && null !== $proposals) {
            $minScore = \INF;
            foreach ($proposals as $proposal) {
                $distance = levenshtein($subject, $proposal);
                if ($distance < $minScore) {
                    $guess = $proposal;
                    $minScore = $distance;
                }
            }

            if (isset($guess) && $minScore < 3) {
                $message .= sprintf(' Did you mean "%s"?', $guess);
            }
        }

        parent::__construct($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\ExpressionLanguage;

use Symfony\Component\ExpressionLanguage\Node\Node;

/**
 * Represents an already parsed expression.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class ParsedExpression extends Expression
{
    private $nodes;

    public function __construct(string $expression, Node $nodes)
    {
        parent::__construct($expression);

        $this->nodes = $nodes;
    }

    public function getNodes()
    {
        return $this->nodes;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\ExpressionLanguage\Node;

use Symfony\Component\ExpressionLanguage\Compiler;

/**
 * @author Fabien Potencier <fabien@symfony.com>
 *
 * @internal
 */
class ArgumentsNode extends ArrayNode
{
    public function compile(Compiler $compiler)
    {
        $this->compileArguments($compiler, false);
    }

    public function toArray()
    {
        $array = [];

        foreach ($this->getKeyValuePairs() as $pair) {
            $array[] = $pair['value'];
            $array[] = ', ';
        }
        array_pop($array);

        return $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\ExpressionLanguage\Node;

use Symfony\Component\ExpressionLanguage\Compiler;

/**
 * @author Fabien Potencier <fabien@symfony.com>
 *
 * @internal
 */
class ConditionalNode extends Node
{
    public function __construct(Node $expr1, Node $expr2, Node $expr3)
    {
        parent::__construct(
            ['expr1' => $expr1, 'expr2' => $expr2, 'expr3' => $expr3]
        );
    }

    public function compile(Compiler $compiler)
    {
        $compiler
            ->raw('((')
            ->compile($this->nodes['expr1'])
            ->raw(') ? (')
            ->compile($this->nodes['expr2'])
            ->raw(') : (')
            ->compile($this->nodes['expr3'])
            ->raw('))')
        ;
    }

    public function evaluate(array $functions, array $values)
    {
        if ($this->nodes['expr1']->evaluate($functions, $values)) {
            return $this->nodes['expr2']->evaluate($functions, $values);
        }

        return $this->nodes['expr3']->evaluate($functions, $values);
    }

    public function toArray()
    {
        return ['(', $this->nodes['expr1'], ' ? ', $this->nodes['expr2'], ' : ', $this->nodes['expr3'], ')'];
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\ExpressionLanguage\Node;

use Symfony\Component\ExpressionLanguage\Compiler;

/**
 * Represents a node in the AST.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class Node
{
    public $nodes = [];
    public $attributes = [];

    /**
     * @param array $nodes      An array of nodes
     * @param array $attributes An array of attributes
     */
    public function __construct(array $nodes = [], array $attributes = [])
    {
        $this->nodes = $nodes;
        $this->attributes = $attributes;
    }

    /**
     * @return string
     */
    public function __toString()
    {
        $attributes = [];
        foreach ($this->attributes as $name => $value) {
            $attributes[] = sprintf('%s: %s', $name, str_replace("\n", '', var_export($value, true)));
        }

        $repr = [str_replace('Symfony\Component\ExpressionLanguage\Node\\', '', static::class).'('.implode(', ', $attributes)];

        if (\count($this->nodes)) {
            foreach ($this->nodes as $node) {
                foreach (explode("\n", (string) $node) as $line) {
                    $repr[] = '    '.$line;
                }
            }

            $repr[] = ')';
        } else {
            $repr[0] .= ')';
        }

        return implode("\n", $repr);
    }

    public function compile(Compiler $compiler)
    {
        foreach ($this->nodes as $node) {
            $node->compile($compiler);
        }
    }

    public function evaluate(array $functions, array $values)
    {
        $results = [];
        foreach ($this->nodes as $node) {
            $results[] = $node->evaluate($functions, $values);
        }

        return $results;
    }

    public function toArray()
    {
        throw new \BadMethodCallException(sprintf('Dumping a "%s" instance is not supported yet.', static::class));
    }

    public function dump()
    {
        $dump = '';

        foreach ($this->toArray() as $v) {
            $dump .= \is_scalar($v) ? $v : $v->dump();
        }

        return $dump;
    }

    protected function dumpString(string $value)
    {
        return sprintf('"%s"', addcslashes($value, "\0\t\"\\"));
    }

    protected function isHash(array $value)
    {
        $expectedKey = 0;

        foreach ($value as $key => $val) {
            if ($key !== $expectedKey++) {
                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\ExpressionLanguage\Node;

use Symfony\Component\ExpressionLanguage\Compiler;
use Symfony\Component\ExpressionLanguage\SyntaxError;

/**
 * @author Fabien Potencier <fabien@symfony.com>
 *
 * @internal
 */
class BinaryNode extends Node
{
    private const OPERATORS = [
        '~' => '.',
        'and' => '&&',
        'or' => '||',
    ];

    private const FUNCTIONS = [
        '**' => 'pow',
        '..' => 'range',
        'in' => 'in_array',
        'not in' => '!in_array',
    ];

    public function __construct(string $operator, Node $left, Node $right)
    {
        parent::__construct(
            ['left' => $left, 'right' => $right],
            ['operator' => $operator]
        );
    }

    public function compile(Compiler $compiler)
    {
        $operator = $this->attributes['operator'];

        if ('matches' == $operator) {
            if ($this->nodes['right'] instanceof ConstantNode) {
                $this->evaluateMatches($this->nodes['right']->evaluate([], []), '');
            }

            $compiler
                ->raw('(static function ($regexp, $str) { set_error_handler(function ($t, $m) use ($regexp, $str) { throw new \Symfony\Component\ExpressionLanguage\SyntaxError(sprintf(\'Regexp "%s" passed to "matches" is not valid\', $regexp).substr($m, 12)); }); try { return preg_match($regexp, (string) $str); } finally { restore_error_handler(); } })(')
                ->compile($this->nodes['right'])
                ->raw(', ')
                ->compile($this->nodes['left'])
                ->raw(')')
            ;

            return;
        }

        if (isset(self::FUNCTIONS[$operator])) {
            $compiler
                ->raw(sprintf('%s(', self::FUNCTIONS[$operator]))
                ->compile($this->nodes['left'])
                ->raw(', ')
                ->compile($this->nodes['right'])
                ->raw(')')
            ;

            return;
        }

        if (isset(self::OPERATORS[$operator])) {
            $operator = self::OPERATORS[$operator];
        }

        $compiler
            ->raw('(')
            ->compile($this->nodes['left'])
            ->raw(' ')
            ->raw($operator)
            ->raw(' ')
            ->compile($this->nodes['right'])
            ->raw(')')
        ;
    }

    public function evaluate(array $functions, array $values)
    {
        $operator = $this->attributes['operator'];
        $left = $this->nodes['left']->evaluate($functions, $values);

        if (isset(self::FUNCTIONS[$operator])) {
            $right = $this->nodes['right']->evaluate($functions, $values);

            if ('not in' === $operator) {
                return !\in_array($left, $right);
            }
            $f = self::FUNCTIONS[$operator];

            return $f($left, $right);
        }

        switch ($operator) {
            case 'or':
            case '||':
                return $left || $this->nodes['right']->evaluate($functions, $values);
            case 'and':
            case '&&':
                return $left && $this->nodes['right']->evaluate($functions, $values);
        }

        $right = $this->nodes['right']->evaluate($functions, $values);

        switch ($operator) {
            case '|':
                return $left | $right;
            case '^':
                return $left ^ $right;
            case '&':
                return $left & $right;
            case '==':
                return $left == $right;
            case '===':
                return $left === $right;
            case '!=':
                return $left != $right;
            case '!==':
                return $left !== $right;
            case '<':
                return $left < $right;
            case '>':
                return $left > $right;
            case '>=':
                return $left >= $right;
            case '<=':
                return $left <= $right;
            case 'not in':
                return !\in_array($left, $right);
            case 'in':
                return \in_array($left, $right);
            case '+':
                return $left + $right;
            case '-':
                return $left - $right;
            case '~':
                return $left.$right;
            case '*':
                return $left * $right;
            case '/':
                if (0 == $right) {
                    throw new \DivisionByZeroError('Division by zero.');
                }

                return $left / $right;
            case '%':
                if (0 == $right) {
                    throw new \DivisionByZeroError('Modulo by zero.');
                }

                return $left % $right;
            case 'matches':
                return $this->evaluateMatches($right, $left);
        }
    }

    public function toArray()
    {
        return ['(', $this->nodes['left'], ' '.$this->attributes['operator'].' ', $this->nodes['right'], ')'];
    }

    private function evaluateMatches(string $regexp, ?string $str): int
    {
        set_error_handler(function ($t, $m) use ($regexp) {
            throw new SyntaxError(sprintf('Regexp "%s" passed to "matches" is not valid', $regexp).substr($m, 12));
        });
        try {
            return preg_match($regexp, (string) $str);
        } finally {
            restore_error_handler();
        }
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\ExpressionLanguage\Node;

use Symfony\Component\ExpressionLanguage\Compiler;

/**
 * @author Fabien Potencier <fabien@symfony.com>
 *
 * @internal
 */
class ArrayNode extends Node
{
    protected $index;

    public function __construct()
    {
        $this->index = -1;
    }

    public function addElement(Node $value, ?Node $key = null)
    {
        if (null === $key) {
            $key = new ConstantNode(++$this->index);
        }

        array_push($this->nodes, $key, $value);
    }

    /**
     * Compiles the node to PHP.
     */
    public function compile(Compiler $compiler)
    {
        $compiler->raw('[');
        $this->compileArguments($compiler);
        $compiler->raw(']');
    }

    public function evaluate(array $functions, array $values)
    {
        $result = [];
        foreach ($this->getKeyValuePairs() as $pair) {
            $result[$pair['key']->evaluate($functions, $values)] = $pair['value']->evaluate($functions, $values);
        }

        return $result;
    }

    public function toArray()
    {
        $value = [];
        foreach ($this->getKeyValuePairs() as $pair) {
            $value[$pair['key']->attributes['value']] = $pair['value'];
        }

        $array = [];

        if ($this->isHash($value)) {
            foreach ($value as $k => $v) {
                $array[] = ', ';
                $array[] = new ConstantNode($k);
                $array[] = ': ';
                $array[] = $v;
            }
            $array[0] = '{';
            $array[] = '}';
        } else {
            foreach ($value as $v) {
                $array[] = ', ';
                $array[] = $v;
            }
            $array[0] = '[';
            $array[] = ']';
        }

        return $array;
    }

    protected function getKeyValuePairs()
    {
        $pairs = [];
        foreach (array_chunk($this->nodes, 2) as $pair) {
            $pairs[] = ['key' => $pair[0], 'value' => $pair[1]];
        }

        return $pairs;
    }

    protected function compileArguments(Compiler $compiler, bool $withKeys = true)
    {
        $first = true;
        foreach ($this->getKeyValuePairs() as $pair) {
            if (!$first) {
                $compiler->raw(', ');
            }
            $first = false;

            if ($withKeys) {
                $compiler
                    ->compile($pair['key'])
                    ->raw(' => ')
                ;
            }

            $compiler->compile($pair['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\ExpressionLanguage\Node;

use Symfony\Component\ExpressionLanguage\Compiler;

/**
 * @author Fabien Potencier <fabien@symfony.com>
 *
 * @internal
 */
class UnaryNode extends Node
{
    private const OPERATORS = [
        '!' => '!',
        'not' => '!',
        '+' => '+',
        '-' => '-',
    ];

    public function __construct(string $operator, Node $node)
    {
        parent::__construct(
            ['node' => $node],
            ['operator' => $operator]
        );
    }

    public function compile(Compiler $compiler)
    {
        $compiler
            ->raw('(')
            ->raw(self::OPERATORS[$this->attributes['operator']])
            ->compile($this->nodes['node'])
            ->raw(')')
        ;
    }

    public function evaluate(array $functions, array $values)
    {
        $value = $this->nodes['node']->evaluate($functions, $values);
        switch ($this->attributes['operator']) {
            case 'not':
            case '!':
                return !$value;
            case '-':
                return -$value;
        }

        return $value;
    }

    public function toArray(): array
    {
        return ['(', $this->attributes['operator'].' ', $this->nodes['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\ExpressionLanguage\Node;

use Symfony\Component\ExpressionLanguage\Compiler;

/**
 * @author Fabien Potencier <fabien@symfony.com>
 *
 * @internal
 */
class GetAttrNode extends Node
{
    public const PROPERTY_CALL = 1;
    public const METHOD_CALL = 2;
    public const ARRAY_CALL = 3;

    public function __construct(Node $node, Node $attribute, ArrayNode $arguments, int $type)
    {
        parent::__construct(
            ['node' => $node, 'attribute' => $attribute, 'arguments' => $arguments],
            ['type' => $type]
        );
    }

    public function compile(Compiler $compiler)
    {
        switch ($this->attributes['type']) {
            case self::PROPERTY_CALL:
                $compiler
                    ->compile($this->nodes['node'])
                    ->raw('->')
                    ->raw($this->nodes['attribute']->attributes['value'])
                ;
                break;

            case self::METHOD_CALL:
                $compiler
                    ->compile($this->nodes['node'])
                    ->raw('->')
                    ->raw($this->nodes['attribute']->attributes['value'])
                    ->raw('(')
                    ->compile($this->nodes['arguments'])
                    ->raw(')')
                ;
                break;

            case self::ARRAY_CALL:
                $compiler
                    ->compile($this->nodes['node'])
                    ->raw('[')
                    ->compile($this->nodes['attribute'])->raw(']')
                ;
                break;
        }
    }

    public function evaluate(array $functions, array $values)
    {
        switch ($this->attributes['type']) {
            case self::PROPERTY_CALL:
                $obj = $this->nodes['node']->evaluate($functions, $values);
                if (!\is_object($obj)) {
                    throw new \RuntimeException(sprintf('Unable to get property "%s" of non-object "%s".', $this->nodes['attribute']->dump(), $this->nodes['node']->dump()));
                }

                $property = $this->nodes['attribute']->attributes['value'];

                return $obj->$property;

            case self::METHOD_CALL:
                $obj = $this->nodes['node']->evaluate($functions, $values);
                if (!\is_object($obj)) {
                    throw new \RuntimeException(sprintf('Unable to call method "%s" of non-object "%s".', $this->nodes['attribute']->dump(), $this->nodes['node']->dump()));
                }
                if (!\is_callable($toCall = [$obj, $this->nodes['attribute']->attributes['value']])) {
                    throw new \RuntimeException(sprintf('Unable to call method "%s" of object "%s".', $this->nodes['attribute']->attributes['value'], get_debug_type($obj)));
                }

                return $toCall(...array_values($this->nodes['arguments']->evaluate($functions, $values)));

            case self::ARRAY_CALL:
                $array = $this->nodes['node']->evaluate($functions, $values);
                if (!\is_array($array) && !$array instanceof \ArrayAccess) {
                    throw new \RuntimeException(sprintf('Unable to get an item of non-array "%s".', $this->nodes['node']->dump()));
                }

                return $array[$this->nodes['attribute']->evaluate($functions, $values)];
        }
    }

    public function toArray()
    {
        switch ($this->attributes['type']) {
            case self::PROPERTY_CALL:
                return [$this->nodes['node'], '.', $this->nodes['attribute']];

            case self::METHOD_CALL:
                return [$this->nodes['node'], '.', $this->nodes['attribute'], '(', $this->nodes['arguments'], ')'];

            case self::ARRAY_CALL:
                return [$this->nodes['node'], '[', $this->nodes['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\ExpressionLanguage\Node;

use Symfony\Component\ExpressionLanguage\Compiler;

/**
 * @author Fabien Potencier <fabien@symfony.com>
 *
 * @internal
 */
class ConstantNode extends Node
{
    private $isIdentifier;

    public function __construct($value, bool $isIdentifier = false)
    {
        $this->isIdentifier = $isIdentifier;
        parent::__construct(
            [],
            ['value' => $value]
        );
    }

    public function compile(Compiler $compiler)
    {
        $compiler->repr($this->attributes['value']);
    }

    public function evaluate(array $functions, array $values)
    {
        return $this->attributes['value'];
    }

    public function toArray()
    {
        $array = [];
        $value = $this->attributes['value'];

        if ($this->isIdentifier) {
            $array[] = $value;
        } elseif (true === $value) {
            $array[] = 'true';
        } elseif (false === $value) {
            $array[] = 'false';
        } elseif (null === $value) {
            $array[] = 'null';
        } elseif (is_numeric($value)) {
            $array[] = $value;
        } elseif (!\is_array($value)) {
            $array[] = $this->dumpString($value);
        } elseif ($this->isHash($value)) {
            foreach ($value as $k => $v) {
                $array[] = ', ';
                $array[] = new self($k);
                $array[] = ': ';
                $array[] = new self($v);
            }
            $array[0] = '{';
            $array[] = '}';
        } else {
            foreach ($value as $v) {
                $array[] = ', ';
                $array[] = new self($v);
            }
            $array[0] = '[';
            $array[] = ']';
        }

        return $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\ExpressionLanguage\Node;

use Symfony\Component\ExpressionLanguage\Compiler;

/**
 * @author Fabien Potencier <fabien@symfony.com>
 *
 * @internal
 */
class NameNode extends Node
{
    public function __construct(string $name)
    {
        parent::__construct(
            [],
            ['name' => $name]
        );
    }

    public function compile(Compiler $compiler)
    {
        $compiler->raw('$'.$this->attributes['name']);
    }

    public function evaluate(array $functions, array $values)
    {
        return $values[$this->attributes['name']];
    }

    public function toArray()
    {
        return [$this->attributes['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\ExpressionLanguage\Node;

use Symfony\Component\ExpressionLanguage\Compiler;

/**
 * @author Fabien Potencier <fabien@symfony.com>
 *
 * @internal
 */
class FunctionNode extends Node
{
    public function __construct(string $name, Node $arguments)
    {
        parent::__construct(
            ['arguments' => $arguments],
            ['name' => $name]
        );
    }

    public function compile(Compiler $compiler)
    {
        $arguments = [];
        foreach ($this->nodes['arguments']->nodes as $node) {
            $arguments[] = $compiler->subcompile($node);
        }

        $function = $compiler->getFunction($this->attributes['name']);

        $compiler->raw($function['compiler'](...$arguments));
    }

    public function evaluate(array $functions, array $values)
    {
        $arguments = [$values];
        foreach ($this->nodes['arguments']->nodes as $node) {
            $arguments[] = $node->evaluate($functions, $values);
        }

        return $functions[$this->attributes['name']]['evaluator'](...$arguments);
    }

    public function toArray()
    {
        $array = [];
        $array[] = $this->attributes['name'];

        foreach ($this->nodes['arguments']->nodes as $node) {
            $array[] = ', ';
            $array[] = $node;
        }
        $array[1] = '(';
        $array[] = ')';

        return $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\ExpressionLanguage;

/**
 * Represents a Token.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class Token
{
    public $value;
    public $type;
    public $cursor;

    public const EOF_TYPE = 'end of expression';
    public const NAME_TYPE = 'name';
    public const NUMBER_TYPE = 'number';
    public const STRING_TYPE = 'string';
    public const OPERATOR_TYPE = 'operator';
    public const PUNCTUATION_TYPE = 'punctuation';

    /**
     * @param string                $type   The type of the token (self::*_TYPE)
     * @param string|int|float|null $value  The token value
     * @param int|null              $cursor The cursor position in the source
     */
    public function __construct(string $type, $value, ?int $cursor)
    {
        $this->type = $type;
        $this->value = $value;
        $this->cursor = $cursor;
    }

    /**
     * Returns a string representation of the token.
     *
     * @return string
     */
    public function __toString()
    {
        return sprintf('%3d %-11s %s', $this->cursor, strtoupper($this->type), $this->value);
    }

    /**
     * Tests the current token for a type and/or a value.
     *
     * @return bool
     */
    public function test(string $type, ?string $value = null)
    {
        return $this->type === $type && (null === $value || $this->value == $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\ExpressionLanguage;

use Symfony\Contracts\Service\ResetInterface;

/**
 * Compiles a node to PHP code.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class Compiler implements ResetInterface
{
    private $source;
    private $functions;

    public function __construct(array $functions)
    {
        $this->functions = $functions;
    }

    public function getFunction(string $name)
    {
        return $this->functions[$name];
    }

    /**
     * Gets the current PHP code after compilation.
     *
     * @return string
     */
    public function getSource()
    {
        return $this->source;
    }

    /**
     * @return $this
     */
    public function reset()
    {
        $this->source = '';

        return $this;
    }

    /**
     * Compiles a node.
     *
     * @return $this
     */
    public function compile(Node\Node $node)
    {
        $node->compile($this);

        return $this;
    }

    public function subcompile(Node\Node $node)
    {
        $current = $this->source;
        $this->source = '';

        $node->compile($this);

        $source = $this->source;
        $this->source = $current;

        return $source;
    }

    /**
     * Adds a raw string to the compiled code.
     *
     * @return $this
     */
    public function raw(string $string)
    {
        $this->source .= $string;

        return $this;
    }

    /**
     * Adds a quoted string to the compiled code.
     *
     * @return $this
     */
    public function string(string $value)
    {
        $this->source .= sprintf('"%s"', addcslashes($value, "\0\t\"\$\\"));

        return $this;
    }

    /**
     * Returns a PHP representation of a given value.
     *
     * @param mixed $value The value to convert
     *
     * @return $this
     */
    public function repr($value)
    {
        if (\is_int($value) || \is_float($value)) {
            if (false !== $locale = setlocale(\LC_NUMERIC, 0)) {
                setlocale(\LC_NUMERIC, 'C');
            }

            $this->raw($value);

            if (false !== $locale) {
                setlocale(\LC_NUMERIC, $locale);
            }
        } elseif (null === $value) {
            $this->raw('null');
        } elseif (\is_bool($value)) {
            $this->raw($value ? 'true' : 'false');
        } elseif (\is_array($value)) {
            $this->raw('[');
            $first = true;
            foreach ($value as $key => $value) {
                if (!$first) {
                    $this->raw(', ');
                }
                $first = false;
                $this->repr($key);
                $this->raw(' => ');
                $this->repr($value);
            }
            $this->raw(']');
        } else {
            $this->string($value);
        }

        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\HttpClient\DataCollector;

use Symfony\Component\HttpClient\TraceableHttpClient;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
use Symfony\Component\VarDumper\Caster\ImgStub;

/**
 * @author Jérémy Romey <jeremy@free-agent.fr>
 */
final class HttpClientDataCollector extends DataCollector implements LateDataCollectorInterface
{
    /**
     * @var TraceableHttpClient[]
     */
    private $clients = [];

    public function registerClient(string $name, TraceableHttpClient $client)
    {
        $this->clients[$name] = $client;
    }

    /**
     * {@inheritdoc}
     */
    public function collect(Request $request, Response $response, ?\Throwable $exception = null)
    {
        $this->lateCollect();
    }

    public function lateCollect()
    {
        $this->data['request_count'] = $this->data['request_count'] ?? 0;
        $this->data['error_count'] = $this->data['error_count'] ?? 0;
        $this->data += ['clients' => []];

        foreach ($this->clients as $name => $client) {
            [$errorCount, $traces] = $this->collectOnClient($client);

            $this->data['clients'] += [
                $name => [
                    'traces' => [],
                    'error_count' => 0,
                ],
            ];

            $this->data['clients'][$name]['traces'] = array_merge($this->data['clients'][$name]['traces'], $traces);
            $this->data['request_count'] += \count($traces);
            $this->data['error_count'] += $errorCount;
            $this->data['clients'][$name]['error_count'] += $errorCount;

            $client->reset();
        }
    }

    public function getClients(): array
    {
        return $this->data['clients'] ?? [];
    }

    public function getRequestCount(): int
    {
        return $this->data['request_count'] ?? 0;
    }

    public function getErrorCount(): int
    {
        return $this->data['error_count'] ?? 0;
    }

    /**
     * {@inheritdoc}
     */
    public function getName(): string
    {
        return 'http_client';
    }

    public function reset()
    {
        $this->data = [
            'clients' => [],
            'request_count' => 0,
            'error_count' => 0,
        ];
    }

    private function collectOnClient(TraceableHttpClient $client): array
    {
        $traces = $client->getTracedRequests();
        $errorCount = 0;
        $baseInfo = [
            'response_headers' => 1,
            'retry_count' => 1,
            'redirect_count' => 1,
            'redirect_url' => 1,
            'user_data' => 1,
            'error' => 1,
            'url' => 1,
        ];

        foreach ($traces as $i => $trace) {
            if (400 <= ($trace['info']['http_code'] ?? 0)) {
                ++$errorCount;
            }

            $info = $trace['info'];
            $traces[$i]['http_code'] = $info['http_code'] ?? 0;

            unset($info['filetime'], $info['http_code'], $info['ssl_verify_result'], $info['content_type']);

            if (($info['http_method'] ?? null) === $trace['method']) {
                unset($info['http_method']);
            }

            if (($info['url'] ?? null) === $trace['url']) {
                unset($info['url']);
            }

            foreach ($info as $k => $v) {
                if (!$v || (is_numeric($v) && 0 > $v)) {
                    unset($info[$k]);
                }
            }

            if (\is_string($content = $trace['content'])) {
                $contentType = 'application/octet-stream';

                foreach ($info['response_headers'] ?? [] as $h) {
                    if (0 === stripos($h, 'content-type: ')) {
                        $contentType = substr($h, \strlen('content-type: '));
                        break;
                    }
                }

                if (0 === strpos($contentType, 'image/') && class_exists(ImgStub::class)) {
                    $content = new ImgStub($content, $contentType, '');
                } else {
                    $content = [$content];
                }

                $content = ['response_content' => $content];
            } elseif (\is_array($content)) {
                $content = ['response_json' => $content];
            } else {
                $content = [];
            }

            if (isset($info['retry_count'])) {
                $content['retries'] = $info['previous_info'];
                unset($info['previous_info']);
            }

            $debugInfo = array_diff_key($info, $baseInfo);
            $info = ['info' => $debugInfo] + array_diff_key($info, $debugInfo) + $content;
            unset($traces[$i]['info']); // break PHP reference used by TraceableHttpClient
            $traces[$i]['info'] = $this->cloneVar($info);
            $traces[$i]['options'] = $this->cloneVar($trace['options']);
        }

        return [$errorCount, $traces];
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\HttpClient;

use Symfony\Component\HttpClient\Exception\TransportException;
use Symfony\Component\HttpClient\Response\MockResponse;
use Symfony\Component\HttpClient\Response\ResponseStream;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
use Symfony\Contracts\Service\ResetInterface;

/**
 * A test-friendly HttpClient that doesn't make actual HTTP requests.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
class MockHttpClient implements HttpClientInterface, ResetInterface
{
    use HttpClientTrait;

    private $responseFactory;
    private $requestsCount = 0;
    private $defaultOptions = [];

    /**
     * @param callable|callable[]|ResponseInterface|ResponseInterface[]|iterable|null $responseFactory
     */
    public function __construct($responseFactory = null, ?string $baseUri = 'https://example.com')
    {
        $this->setResponseFactory($responseFactory);
        $this->defaultOptions['base_uri'] = $baseUri;
    }

    /**
     * @param callable|callable[]|ResponseInterface|ResponseInterface[]|iterable|null $responseFactory
     */
    public function setResponseFactory($responseFactory): void
    {
        if ($responseFactory instanceof ResponseInterface) {
            $responseFactory = [$responseFactory];
        }

        if (!$responseFactory instanceof \Iterator && null !== $responseFactory && !\is_callable($responseFactory)) {
            $responseFactory = (static function () use ($responseFactory) {
                yield from $responseFactory;
            })();
        }

        $this->responseFactory = $responseFactory;
    }

    /**
     * {@inheritdoc}
     */
    public function request(string $method, string $url, array $options = []): ResponseInterface
    {
        [$url, $options] = $this->prepareRequest($method, $url, $options, $this->defaultOptions, true);
        $url = implode('', $url);

        if (null === $this->responseFactory) {
            $response = new MockResponse();
        } elseif (\is_callable($this->responseFactory)) {
            $response = ($this->responseFactory)($method, $url, $options);
        } elseif (!$this->responseFactory->valid()) {
            throw new TransportException('The response factory iterator passed to MockHttpClient is empty.');
        } else {
            $responseFactory = $this->responseFactory->current();
            $response = \is_callable($responseFactory) ? $responseFactory($method, $url, $options) : $responseFactory;
            $this->responseFactory->next();
        }
        ++$this->requestsCount;

        if (!$response instanceof ResponseInterface) {
            throw new TransportException(sprintf('The response factory passed to MockHttpClient must return/yield an instance of ResponseInterface, "%s" given.', \is_object($response) ? \get_class($response) : \gettype($response)));
        }

        return MockResponse::fromRequest($method, $url, $options, $response);
    }

    /**
     * {@inheritdoc}
     */
    public function stream($responses, ?float $timeout = null): ResponseStreamInterface
    {
        if ($responses instanceof ResponseInterface) {
            $responses = [$responses];
        } elseif (!is_iterable($responses)) {
            throw new \TypeError(sprintf('"%s()" expects parameter 1 to be an iterable of MockResponse objects, "%s" given.', __METHOD__, get_debug_type($responses)));
        }

        return new ResponseStream(MockResponse::stream($responses, $timeout));
    }

    public function getRequestsCount(): int
    {
        return $this->requestsCount;
    }

    /**
     * {@inheritdoc}
     */
    public function withOptions(array $options): self
    {
        $clone = clone $this;
        $clone->defaultOptions = self::mergeDefaultOptions($options, $this->defaultOptions, true);

        return $clone;
    }

    public function reset()
    {
        $this->requestsCount = 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\HttpClient;

use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpClient\Exception\InvalidArgumentException;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
use Symfony\Contracts\Service\ResetInterface;

/**
 * Auto-configure the default options based on the requested URL.
 *
 * @author Anthony Martin <anthony.martin@sensiolabs.com>
 */
class ScopingHttpClient implements HttpClientInterface, ResetInterface, LoggerAwareInterface
{
    use HttpClientTrait;

    private $client;
    private $defaultOptionsByRegexp;
    private $defaultRegexp;

    public function __construct(HttpClientInterface $client, array $defaultOptionsByRegexp, ?string $defaultRegexp = null)
    {
        $this->client = $client;
        $this->defaultOptionsByRegexp = $defaultOptionsByRegexp;
        $this->defaultRegexp = $defaultRegexp;

        if (null !== $defaultRegexp && !isset($defaultOptionsByRegexp[$defaultRegexp])) {
            throw new InvalidArgumentException(sprintf('No options are mapped to the provided "%s" default regexp.', $defaultRegexp));
        }
    }

    public static function forBaseUri(HttpClientInterface $client, string $baseUri, array $defaultOptions = [], ?string $regexp = null): self
    {
        if (null === $regexp) {
            $regexp = preg_quote(implode('', self::resolveUrl(self::parseUrl('.'), self::parseUrl($baseUri))));
        }

        $defaultOptions['base_uri'] = $baseUri;

        return new self($client, [$regexp => $defaultOptions], $regexp);
    }

    /**
     * {@inheritdoc}
     */
    public function request(string $method, string $url, array $options = []): ResponseInterface
    {
        $e = null;
        $url = self::parseUrl($url, $options['query'] ?? []);

        if (\is_string($options['base_uri'] ?? null)) {
            $options['base_uri'] = self::parseUrl($options['base_uri']);
        }

        try {
            $url = implode('', self::resolveUrl($url, $options['base_uri'] ?? null));
        } catch (InvalidArgumentException $e) {
            if (null === $this->defaultRegexp) {
                throw $e;
            }

            $defaultOptions = $this->defaultOptionsByRegexp[$this->defaultRegexp];
            $options = self::mergeDefaultOptions($options, $defaultOptions, true);
            if (\is_string($options['base_uri'] ?? null)) {
                $options['base_uri'] = self::parseUrl($options['base_uri']);
            }
            $url = implode('', self::resolveUrl($url, $options['base_uri'] ?? null, $defaultOptions['query'] ?? []));
        }

        foreach ($this->defaultOptionsByRegexp as $regexp => $defaultOptions) {
            if (preg_match("{{$regexp}}A", $url)) {
                if (null === $e || $regexp !== $this->defaultRegexp) {
                    $options = self::mergeDefaultOptions($options, $defaultOptions, true);
                }
                break;
            }
        }

        return $this->client->request($method, $url, $options);
    }

    /**
     * {@inheritdoc}
     */
    public function stream($responses, ?float $timeout = null): ResponseStreamInterface
    {
        return $this->client->stream($responses, $timeout);
    }

    public function reset()
    {
        if ($this->client instanceof ResetInterface) {
            $this->client->reset();
        }
    }

    /**
     * {@inheritdoc}
     */
    public function setLogger(LoggerInterface $logger): void
    {
        if ($this->client instanceof LoggerAwareInterface) {
            $this->client->setLogger($logger);
        }
    }

    /**
     * {@inheritdoc}
     */
    public function withOptions(array $options): self
    {
        $clone = clone $this;
        $clone->client = $this->client->withOptions($options);

        return $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\HttpClient\Response;

use Symfony\Component\HttpClient\Chunk\DataChunk;
use Symfony\Component\HttpClient\Chunk\ErrorChunk;
use Symfony\Component\HttpClient\Chunk\FirstChunk;
use Symfony\Component\HttpClient\Chunk\LastChunk;
use Symfony\Component\HttpClient\Exception\TransportException;
use Symfony\Component\HttpClient\Internal\ClientState;

/**
 * Implements common logic for transport-level response classes.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @internal
 */
trait TransportResponseTrait
{
    private $canary;
    private $headers = [];
    private $info = [
        'response_headers' => [],
        'http_code' => 0,
        'error' => null,
        'canceled' => false,
    ];

    /** @var object|resource */
    private $handle;
    private $id;
    private $timeout = 0;
    private $inflate;
    private $finalInfo;
    private $logger;

    /**
     * {@inheritdoc}
     */
    public function getStatusCode(): int
    {
        if ($this->initializer) {
            self::initialize($this);
        }

        return $this->info['http_code'];
    }

    /**
     * {@inheritdoc}
     */
    public function getHeaders(bool $throw = true): array
    {
        if ($this->initializer) {
            self::initialize($this);
        }

        if ($throw) {
            $this->checkStatusCode();
        }

        return $this->headers;
    }

    /**
     * {@inheritdoc}
     */
    public function cancel(): void
    {
        $this->info['canceled'] = true;
        $this->info['error'] = 'Response has been canceled.';
        $this->close();
    }

    /**
     * Closes the response and all its network handles.
     */
    protected function close(): void
    {
        $this->canary->cancel();
        $this->inflate = null;
    }

    /**
     * Adds pending responses to the activity list.
     */
    abstract protected static function schedule(self $response, array &$runningResponses): void;

    /**
     * Performs all pending non-blocking operations.
     */
    abstract protected static function perform(ClientState $multi, array &$responses): void;

    /**
     * Waits for network activity.
     */
    abstract protected static function select(ClientState $multi, float $timeout): int;

    private static function addResponseHeaders(array $responseHeaders, array &$info, array &$headers, string &$debug = ''): void
    {
        foreach ($responseHeaders as $h) {
            if (11 <= \strlen($h) && '/' === $h[4] && preg_match('#^HTTP/\d+(?:\.\d+)? (\d\d\d)(?: |$)#', $h, $m)) {
                if ($headers) {
                    $debug .= "< \r\n";
                    $headers = [];
                }
                $info['http_code'] = (int) $m[1];
            } elseif (2 === \count($m = explode(':', $h, 2))) {
                $headers[strtolower($m[0])][] = ltrim($m[1]);
            }

            $debug .= "< {$h}\r\n";
            $info['response_headers'][] = $h;
        }

        $debug .= "< \r\n";
    }

    /**
     * Ensures the request is always sent and that the response code was checked.
     */
    private function doDestruct()
    {
        $this->shouldBuffer = true;

        if ($this->initializer && null === $this->info['error']) {
            self::initialize($this);
            $this->checkStatusCode();
        }
    }

    /**
     * Implements an event loop based on a buffer activity queue.
     *
     * @param iterable<array-key, self> $responses
     *
     * @internal
     */
    public static function stream(iterable $responses, ?float $timeout = null): \Generator
    {
        $runningResponses = [];

        foreach ($responses as $response) {
            self::schedule($response, $runningResponses);
        }

        $lastActivity = microtime(true);
        $elapsedTimeout = 0;

        if ($fromLastTimeout = 0.0 === $timeout && '-0' === (string) $timeout) {
            $timeout = null;
        } elseif ($fromLastTimeout = 0 > $timeout) {
            $timeout = -$timeout;
        }

        while (true) {
            $hasActivity = false;
            $timeoutMax = 0;
            $timeoutMin = $timeout ?? \INF;

            /** @var ClientState $multi */
            foreach ($runningResponses as $i => [$multi]) {
                $responses = &$runningResponses[$i][1];
                self::perform($multi, $responses);

                foreach ($responses as $j => $response) {
                    $timeoutMax = $timeout ?? max($timeoutMax, $response->timeout);
                    $timeoutMin = min($timeoutMin, $response->timeout, 1);
                    $chunk = false;

                    if ($fromLastTimeout && null !== $multi->lastTimeout) {
                        $elapsedTimeout = microtime(true) - $multi->lastTimeout;
                    }

                    if (isset($multi->handlesActivity[$j])) {
                        $multi->lastTimeout = null;
                    } elseif (!isset($multi->openHandles[$j])) {
                        unset($responses[$j]);
                        continue;
                    } elseif ($elapsedTimeout >= $timeoutMax) {
                        $multi->handlesActivity[$j] = [new ErrorChunk($response->offset, sprintf('Idle timeout reached for "%s".', $response->getInfo('url')))];
                        $multi->lastTimeout ?? $multi->lastTimeout = $lastActivity;
                    } else {
                        continue;
                    }

                    while ($multi->handlesActivity[$j] ?? false) {
                        $hasActivity = true;
                        $elapsedTimeout = 0;

                        if (\is_string($chunk = array_shift($multi->handlesActivity[$j]))) {
                            if (null !== $response->inflate && false === $chunk = @inflate_add($response->inflate, $chunk)) {
                                $multi->handlesActivity[$j] = [null, new TransportException(sprintf('Error while processing content unencoding for "%s".', $response->getInfo('url')))];
                                continue;
                            }

                            if ('' !== $chunk && null !== $response->content && \strlen($chunk) !== fwrite($response->content, $chunk)) {
                                $multi->handlesActivity[$j] = [null, new TransportException(sprintf('Failed writing %d bytes to the response buffer.', \strlen($chunk)))];
                                continue;
                            }

                            $chunkLen = \strlen($chunk);
                            $chunk = new DataChunk($response->offset, $chunk);
                            $response->offset += $chunkLen;
                        } elseif (null === $chunk) {
                            $e = $multi->handlesActivity[$j][0];
                            unset($responses[$j], $multi->handlesActivity[$j]);
                            $response->close();

                            if (null !== $e) {
                                $response->info['error'] = $e->getMessage();

                                if ($e instanceof \Error) {
                                    throw $e;
                                }

                                $chunk = new ErrorChunk($response->offset, $e);
                            } else {
                                if (0 === $response->offset && null === $response->content) {
                                    $response->content = fopen('php://memory', 'w+');
                                }

                                $chunk = new LastChunk($response->offset);
                            }
                        } elseif ($chunk instanceof ErrorChunk) {
                            unset($responses[$j]);
                            $elapsedTimeout = $timeoutMax;
                        } elseif ($chunk instanceof FirstChunk) {
                            if ($response->logger) {
                                $info = $response->getInfo();
                                $response->logger->info(sprintf('Response: "%s %s"', $info['http_code'], $info['url']));
                            }

                            $response->inflate = \extension_loaded('zlib') && $response->inflate && 'gzip' === ($response->headers['content-encoding'][0] ?? null) ? inflate_init(\ZLIB_ENCODING_GZIP) : null;

                            if ($response->shouldBuffer instanceof \Closure) {
                                try {
                                    $response->shouldBuffer = ($response->shouldBuffer)($response->headers);

                                    if (null !== $response->info['error']) {
                                        throw new TransportException($response->info['error']);
                                    }
                                } catch (\Throwable $e) {
                                    $response->close();
                                    $multi->handlesActivity[$j] = [null, $e];
                                }
                            }

                            if (true === $response->shouldBuffer) {
                                $response->content = fopen('php://temp', 'w+');
                            } elseif (\is_resource($response->shouldBuffer)) {
                                $response->content = $response->shouldBuffer;
                            }
                            $response->shouldBuffer = null;

                            yield $response => $chunk;

                            if ($response->initializer && null === $response->info['error']) {
                                // Ensure the HTTP status code is always checked
                                $response->getHeaders(true);
                            }

                            continue;
                        }

                        yield $response => $chunk;
                    }

                    unset($multi->handlesActivity[$j]);

                    if ($chunk instanceof ErrorChunk && !$chunk->didThrow()) {
                        // Ensure transport exceptions are always thrown
                        $chunk->getContent();
                    }
                }

                if (!$responses) {
                    unset($runningResponses[$i]);
                }

                // Prevent memory leaks
                $multi->handlesActivity = $multi->handlesActivity ?: [];
                $multi->openHandles = $multi->openHandles ?: [];
            }

            if (!$runningResponses) {
                break;
            }

            if ($hasActivity) {
                $lastActivity = microtime(true);
                continue;
            }

            if (-1 === self::select($multi, min($timeoutMin, $timeoutMax - $elapsedTimeout))) {
                usleep((int) min(500, 1E6 * $timeoutMin));
            }

            $elapsedTimeout = microtime(true) - $lastActivity;
        }
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\HttpClient\Response;

use Psr\Log\LoggerInterface;
use Symfony\Component\HttpClient\Chunk\FirstChunk;
use Symfony\Component\HttpClient\Chunk\InformationalChunk;
use Symfony\Component\HttpClient\Exception\TransportException;
use Symfony\Component\HttpClient\Internal\Canary;
use Symfony\Component\HttpClient\Internal\ClientState;
use Symfony\Component\HttpClient\Internal\CurlClientState;
use Symfony\Contracts\HttpClient\ResponseInterface;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @internal
 */
final class CurlResponse implements ResponseInterface, StreamableInterface
{
    use CommonResponseTrait {
        getContent as private doGetContent;
    }
    use TransportResponseTrait;

    private $multi;
    private $debugBuffer;

    /**
     * @param \CurlHandle|resource|string $ch
     *
     * @internal
     */
    public function __construct(CurlClientState $multi, $ch, ?array $options = null, ?LoggerInterface $logger = null, string $method = 'GET', ?callable $resolveRedirect = null, ?int $curlVersion = null)
    {
        $this->multi = $multi;

        if (\is_resource($ch) || $ch instanceof \CurlHandle) {
            $this->handle = $ch;
            $this->debugBuffer = fopen('php://temp', 'w+');
            if (0x074000 === $curlVersion) {
                fwrite($this->debugBuffer, 'Due to a bug in curl 7.64.0, the debug log is disabled; use another version to work around the issue.');
            } else {
                curl_setopt($ch, \CURLOPT_VERBOSE, true);
                curl_setopt($ch, \CURLOPT_STDERR, $this->debugBuffer);
            }
        } else {
            $this->info['url'] = $ch;
            $ch = $this->handle;
        }

        $this->id = $id = (int) $ch;
        $this->logger = $logger;
        $this->shouldBuffer = $options['buffer'] ?? true;
        $this->timeout = $options['timeout'] ?? null;
        $this->info['http_method'] = $method;
        $this->info['user_data'] = $options['user_data'] ?? null;
        $this->info['max_duration'] = $options['max_duration'] ?? null;
        $this->info['start_time'] = $this->info['start_time'] ?? microtime(true);
        $info = &$this->info;
        $headers = &$this->headers;
        $debugBuffer = $this->debugBuffer;

        if (!$info['response_headers']) {
            // Used to keep track of what we're waiting for
            curl_setopt($ch, \CURLOPT_PRIVATE, \in_array($method, ['GET', 'HEAD', 'OPTIONS', 'TRACE'], true) && 1.0 < (float) ($options['http_version'] ?? 1.1) ? 'H2' : 'H0'); // H = headers + retry counter
        }

        curl_setopt($ch, \CURLOPT_HEADERFUNCTION, static function ($ch, string $data) use (&$info, &$headers, $options, $multi, $id, &$location, $resolveRedirect, $logger): int {
            return self::parseHeaderLine($ch, $data, $info, $headers, $options, $multi, $id, $location, $resolveRedirect, $logger);
        });

        if (null === $options) {
            // Pushed response: buffer until requested
            curl_setopt($ch, \CURLOPT_WRITEFUNCTION, static function ($ch, string $data) use ($multi, $id): int {
                $multi->handlesActivity[$id][] = $data;
                curl_pause($ch, \CURLPAUSE_RECV);

                return \strlen($data);
            });

            return;
        }

        $execCounter = $multi->execCounter;
        $this->info['pause_handler'] = static function (float $duration) use ($ch, $multi, $execCounter) {
            if (0 < $duration) {
                if ($execCounter === $multi->execCounter) {
                    curl_multi_remove_handle($multi->handle, $ch);
                }

                $lastExpiry = end($multi->pauseExpiries);
                $multi->pauseExpiries[(int) $ch] = $duration += microtime(true);
                if (false !== $lastExpiry && $lastExpiry > $duration) {
                    asort($multi->pauseExpiries);
                }
                curl_pause($ch, \CURLPAUSE_ALL);
            } else {
                unset($multi->pauseExpiries[(int) $ch]);
                curl_pause($ch, \CURLPAUSE_CONT);
                curl_multi_add_handle($multi->handle, $ch);
            }
        };

        $this->inflate = !isset($options['normalized_headers']['accept-encoding']);
        curl_pause($ch, \CURLPAUSE_CONT);

        if ($onProgress = $options['on_progress']) {
            $url = isset($info['url']) ? ['url' => $info['url']] : [];
            curl_setopt($ch, \CURLOPT_NOPROGRESS, false);
            curl_setopt($ch, \CURLOPT_PROGRESSFUNCTION, static function ($ch, $dlSize, $dlNow) use ($onProgress, &$info, $url, $multi, $debugBuffer) {
                try {
                    rewind($debugBuffer);
                    $debug = ['debug' => stream_get_contents($debugBuffer)];
                    $onProgress($dlNow, $dlSize, $url + curl_getinfo($ch) + $info + $debug);
                } catch (\Throwable $e) {
                    $multi->handlesActivity[(int) $ch][] = null;
                    $multi->handlesActivity[(int) $ch][] = $e;

                    return 1; // Abort the request
                }

                return null;
            });
        }

        curl_setopt($ch, \CURLOPT_WRITEFUNCTION, static function ($ch, string $data) use ($multi, $id): int {
            if ('H' === (curl_getinfo($ch, \CURLINFO_PRIVATE)[0] ?? null)) {
                $multi->handlesActivity[$id][] = null;
                $multi->handlesActivity[$id][] = new TransportException(sprintf('Unsupported protocol for "%s"', curl_getinfo($ch, \CURLINFO_EFFECTIVE_URL)));

                return 0;
            }

            curl_setopt($ch, \CURLOPT_WRITEFUNCTION, static function ($ch, string $data) use ($multi, $id): int {
                $multi->handlesActivity[$id][] = $data;

                return \strlen($data);
            });

            $multi->handlesActivity[$id][] = $data;

            return \strlen($data);
        });

        $this->initializer = static function (self $response) {
            $waitFor = curl_getinfo($ch = $response->handle, \CURLINFO_PRIVATE);

            return 'H' === $waitFor[0];
        };

        // Schedule the request in a non-blocking way
        $multi->lastTimeout = null;
        $multi->openHandles[$id] = [$ch, $options];
        curl_multi_add_handle($multi->handle, $ch);

        $this->canary = new Canary(static function () use ($ch, $multi, $id) {
            unset($multi->pauseExpiries[$id], $multi->openHandles[$id], $multi->handlesActivity[$id]);
            curl_setopt($ch, \CURLOPT_PRIVATE, '_0');

            if ($multi->performing) {
                return;
            }

            curl_multi_remove_handle($multi->handle, $ch);
            curl_setopt_array($ch, [
                \CURLOPT_NOPROGRESS => true,
                \CURLOPT_PROGRESSFUNCTION => null,
                \CURLOPT_HEADERFUNCTION => null,
                \CURLOPT_WRITEFUNCTION => null,
                \CURLOPT_READFUNCTION => null,
                \CURLOPT_INFILE => null,
            ]);

            if (!$multi->openHandles) {
                // Schedule DNS cache eviction for the next request
                $multi->dnsCache->evictions = $multi->dnsCache->evictions ?: $multi->dnsCache->removals;
                $multi->dnsCache->removals = $multi->dnsCache->hostnames = [];
            }
        });
    }

    /**
     * {@inheritdoc}
     */
    public function getInfo(?string $type = null)
    {
        if (!$info = $this->finalInfo) {
            $info = array_merge($this->info, curl_getinfo($this->handle));
            $info['url'] = $this->info['url'] ?? $info['url'];
            $info['redirect_url'] = $this->info['redirect_url'] ?? null;

            // workaround curl not subtracting the time offset for pushed responses
            if (isset($this->info['url']) && $info['start_time'] / 1000 < $info['total_time']) {
                $info['total_time'] -= $info['starttransfer_time'] ?: $info['total_time'];
                $info['starttransfer_time'] = 0.0;
            }

            rewind($this->debugBuffer);
            $info['debug'] = stream_get_contents($this->debugBuffer);
            $waitFor = curl_getinfo($this->handle, \CURLINFO_PRIVATE);

            if ('H' !== $waitFor[0] && 'C' !== $waitFor[0]) {
                curl_setopt($this->handle, \CURLOPT_VERBOSE, false);
                rewind($this->debugBuffer);
                ftruncate($this->debugBuffer, 0);
                $this->finalInfo = $info;
            }
        }

        return null !== $type ? $info[$type] ?? null : $info;
    }

    /**
     * {@inheritdoc}
     */
    public function getContent(bool $throw = true): string
    {
        $performing = $this->multi->performing;
        $this->multi->performing = $performing || '_0' === curl_getinfo($this->handle, \CURLINFO_PRIVATE);

        try {
            return $this->doGetContent($throw);
        } finally {
            $this->multi->performing = $performing;
        }
    }

    public function __destruct()
    {
        try {
            if (null === $this->timeout) {
                return; // Unused pushed response
            }

            $this->doDestruct();
        } finally {
            if (\is_resource($this->handle) || $this->handle instanceof \CurlHandle) {
                curl_setopt($this->handle, \CURLOPT_VERBOSE, false);
            }
        }
    }

    /**
     * {@inheritdoc}
     */
    private static function schedule(self $response, array &$runningResponses): void
    {
        if (isset($runningResponses[$i = (int) $response->multi->handle])) {
            $runningResponses[$i][1][$response->id] = $response;
        } else {
            $runningResponses[$i] = [$response->multi, [$response->id => $response]];
        }

        if ('_0' === curl_getinfo($ch = $response->handle, \CURLINFO_PRIVATE)) {
            // Response already completed
            $response->multi->handlesActivity[$response->id][] = null;
            $response->multi->handlesActivity[$response->id][] = null !== $response->info['error'] ? new TransportException($response->info['error']) : null;
        }
    }

    /**
     * {@inheritdoc}
     *
     * @param CurlClientState $multi
     */
    private static function perform(ClientState $multi, ?array &$responses = null): void
    {
        if ($multi->performing) {
            if ($responses) {
                $response = current($responses);
                $multi->handlesActivity[(int) $response->handle][] = null;
                $multi->handlesActivity[(int) $response->handle][] = new TransportException(sprintf('Userland callback cannot use the client nor the response while processing "%s".', curl_getinfo($response->handle, \CURLINFO_EFFECTIVE_URL)));
            }

            return;
        }

        try {
            $multi->performing = true;
            ++$multi->execCounter;
            $active = 0;
            while (\CURLM_CALL_MULTI_PERFORM === ($err = curl_multi_exec($multi->handle, $active))) {
            }

            if (\CURLM_OK !== $err) {
                throw new TransportException(curl_multi_strerror($err));
            }

            while ($info = curl_multi_info_read($multi->handle)) {
                if (\CURLMSG_DONE !== $info['msg']) {
                    continue;
                }
                $result = $info['result'];
                $id = (int) $ch = $info['handle'];
                $waitFor = @curl_getinfo($ch, \CURLINFO_PRIVATE) ?: '_0';

                if (\in_array($result, [\CURLE_SEND_ERROR, \CURLE_RECV_ERROR, /* CURLE_HTTP2 */ 16, /* CURLE_HTTP2_STREAM */ 92], true) && $waitFor[1] && 'C' !== $waitFor[0]) {
                    curl_multi_remove_handle($multi->handle, $ch);
                    $waitFor[1] = (string) ((int) $waitFor[1] - 1); // decrement the retry counter
                    curl_setopt($ch, \CURLOPT_PRIVATE, $waitFor);
                    curl_setopt($ch, \CURLOPT_FORBID_REUSE, true);

                    if (0 === curl_multi_add_handle($multi->handle, $ch)) {
                        continue;
                    }
                }

                if (\CURLE_RECV_ERROR === $result && 'H' === $waitFor[0] && 400 <= ($responses[(int) $ch]->info['http_code'] ?? 0)) {
                    $multi->handlesActivity[$id][] = new FirstChunk();
                }

                $multi->handlesActivity[$id][] = null;
                $multi->handlesActivity[$id][] = \in_array($result, [\CURLE_OK, \CURLE_TOO_MANY_REDIRECTS], true) || '_0' === $waitFor || curl_getinfo($ch, \CURLINFO_SIZE_DOWNLOAD) === curl_getinfo($ch, \CURLINFO_CONTENT_LENGTH_DOWNLOAD) || (curl_error($ch) === 'OpenSSL SSL_read: SSL_ERROR_SYSCALL, errno 0' && -1.0 === curl_getinfo($ch, \CURLINFO_CONTENT_LENGTH_DOWNLOAD) && \in_array('close', array_map('strtolower', $responses[$id]->headers['connection']), true)) ? null : new TransportException(ucfirst(curl_error($ch) ?: curl_strerror($result)).sprintf(' for "%s".', curl_getinfo($ch, \CURLINFO_EFFECTIVE_URL)));
            }
        } finally {
            $multi->performing = false;
        }
    }

    /**
     * {@inheritdoc}
     *
     * @param CurlClientState $multi
     */
    private static function select(ClientState $multi, float $timeout): int
    {
        if (\PHP_VERSION_ID < 70211) {
            // workaround https://bugs.php.net/76480
            $timeout = min($timeout, 0.01);
        }

        if ($multi->pauseExpiries) {
            $now = microtime(true);

            foreach ($multi->pauseExpiries as $id => $pauseExpiry) {
                if ($now < $pauseExpiry) {
                    $timeout = min($timeout, $pauseExpiry - $now);
                    break;
                }

                unset($multi->pauseExpiries[$id]);
                curl_pause($multi->openHandles[$id][0], \CURLPAUSE_CONT);
                curl_multi_add_handle($multi->handle, $multi->openHandles[$id][0]);
            }
        }

        if (0 !== $selected = curl_multi_select($multi->handle, $timeout)) {
            return $selected;
        }

        if ($multi->pauseExpiries && 0 < $timeout -= microtime(true) - $now) {
            usleep((int) (1E6 * $timeout));
        }

        return 0;
    }

    /**
     * Parses header lines as curl yields them to us.
     */
    private static function parseHeaderLine($ch, string $data, array &$info, array &$headers, ?array $options, CurlClientState $multi, int $id, ?string &$location, ?callable $resolveRedirect, ?LoggerInterface $logger): int
    {
        if (!str_ends_with($data, "\r\n")) {
            return 0;
        }

        $waitFor = @curl_getinfo($ch, \CURLINFO_PRIVATE) ?: '_0';

        if ('H' !== $waitFor[0]) {
            return \strlen($data); // Ignore HTTP trailers
        }

        $statusCode = curl_getinfo($ch, \CURLINFO_RESPONSE_CODE);

        if ($statusCode !== $info['http_code'] && !preg_match("#^HTTP/\d+(?:\.\d+)? {$statusCode}(?: |\r\n$)#", $data)) {
            return \strlen($data); // Ignore headers from responses to CONNECT requests
        }

        if ("\r\n" !== $data) {
            // Regular header line: add it to the list
            self::addResponseHeaders([substr($data, 0, -2)], $info, $headers);

            if (!str_starts_with($data, 'HTTP/')) {
                if (0 === stripos($data, 'Location:')) {
                    $location = trim(substr($data, 9, -2));
                }

                return \strlen($data);
            }

            if (\function_exists('openssl_x509_read') && $certinfo = curl_getinfo($ch, \CURLINFO_CERTINFO)) {
                $info['peer_certificate_chain'] = array_map('openssl_x509_read', array_column($certinfo, 'Cert'));
            }

            if (300 <= $info['http_code'] && $info['http_code'] < 400) {
                if (curl_getinfo($ch, \CURLINFO_REDIRECT_COUNT) === $options['max_redirects']) {
                    curl_setopt($ch, \CURLOPT_FOLLOWLOCATION, false);
                } elseif (303 === $info['http_code'] || ('POST' === $info['http_method'] && \in_array($info['http_code'], [301, 302], true))) {
                    curl_setopt($ch, \CURLOPT_POSTFIELDS, '');
                }
            }

            return \strlen($data);
        }

        // End of headers: handle informational responses, redirects, etc.

        if (200 > $statusCode) {
            $multi->handlesActivity[$id][] = new InformationalChunk($statusCode, $headers);
            $location = null;

            return \strlen($data);
        }

        $info['redirect_url'] = null;

        if (300 <= $statusCode && $statusCode < 400 && null !== $location) {
            if ($noContent = 303 === $statusCode || ('POST' === $info['http_method'] && \in_array($statusCode, [301, 302], true))) {
                $info['http_method'] = 'HEAD' === $info['http_method'] ? 'HEAD' : 'GET';
                curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, $info['http_method']);
            }

            if (null === $info['redirect_url'] = $resolveRedirect($ch, $location, $noContent)) {
                $options['max_redirects'] = curl_getinfo($ch, \CURLINFO_REDIRECT_COUNT);
                curl_setopt($ch, \CURLOPT_FOLLOWLOCATION, false);
                curl_setopt($ch, \CURLOPT_MAXREDIRS, $options['max_redirects']);
            }
        }

        if (401 === $statusCode && isset($options['auth_ntlm']) && 0 === strncasecmp($headers['www-authenticate'][0] ?? '', 'NTLM ', 5)) {
            // Continue with NTLM auth
        } elseif ($statusCode < 300 || 400 <= $statusCode || null === $location || curl_getinfo($ch, \CURLINFO_REDIRECT_COUNT) === $options['max_redirects']) {
            // Headers and redirects completed, time to get the response's content
            $multi->handlesActivity[$id][] = new FirstChunk();

            if ('HEAD' === $info['http_method'] || \in_array($statusCode, [204, 304], true)) {
                $waitFor = '_0'; // no content expected
                $multi->handlesActivity[$id][] = null;
                $multi->handlesActivity[$id][] = null;
            } else {
                $waitFor[0] = 'C'; // C = content
            }

            curl_setopt($ch, \CURLOPT_PRIVATE, $waitFor);
        } elseif (null !== $info['redirect_url'] && $logger) {
            $logger->info(sprintf('Redirecting: "%s %s"', $info['http_code'], $info['redirect_url']));
        }

        $location = null;

        return \strlen($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\HttpClient\Response;

use Symfony\Component\HttpClient\Chunk\ErrorChunk;
use Symfony\Component\HttpClient\Chunk\FirstChunk;
use Symfony\Component\HttpClient\Chunk\LastChunk;
use Symfony\Component\HttpClient\Exception\TransportException;
use Symfony\Contracts\HttpClient\ChunkInterface;
use Symfony\Contracts\HttpClient\Exception\ExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;

/**
 * Provides a single extension point to process a response's content stream.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
final class AsyncResponse implements ResponseInterface, StreamableInterface
{
    use CommonResponseTrait;

    private const FIRST_CHUNK_YIELDED = 1;
    private const LAST_CHUNK_YIELDED = 2;

    private $client;
    private $response;
    private $info = ['canceled' => false];
    private $passthru;
    private $stream;
    private $yieldedState;

    /**
     * @param ?callable(ChunkInterface, AsyncContext): ?\Iterator $passthru
     */
    public function __construct(HttpClientInterface $client, string $method, string $url, array $options, ?callable $passthru = null)
    {
        $this->client = $client;
        $this->shouldBuffer = $options['buffer'] ?? true;

        if (null !== $onProgress = $options['on_progress'] ?? null) {
            $thisInfo = &$this->info;
            $options['on_progress'] = static function (int $dlNow, int $dlSize, array $info) use (&$thisInfo, $onProgress) {
                $onProgress($dlNow, $dlSize, $thisInfo + $info);
            };
        }
        $this->response = $client->request($method, $url, ['buffer' => false] + $options);
        $this->passthru = $passthru;
        $this->initializer = static function (self $response, ?float $timeout = null) {
            if (null === $response->shouldBuffer) {
                return false;
            }

            while (true) {
                foreach (self::stream([$response], $timeout) as $chunk) {
                    if ($chunk->isTimeout() && $response->passthru) {
                        // Timeouts thrown during initialization are transport errors
                        foreach (self::passthru($response->client, $response, new ErrorChunk($response->offset, new TransportException($chunk->getError()))) as $chunk) {
                            if ($chunk->isFirst()) {
                                return false;
                            }
                        }

                        continue 2;
                    }

                    if ($chunk->isFirst()) {
                        return false;
                    }
                }

                return false;
            }
        };
        if (\array_key_exists('user_data', $options)) {
            $this->info['user_data'] = $options['user_data'];
        }
        if (\array_key_exists('max_duration', $options)) {
            $this->info['max_duration'] = $options['max_duration'];
        }
    }

    public function getStatusCode(): int
    {
        if ($this->initializer) {
            self::initialize($this);
        }

        return $this->response->getStatusCode();
    }

    public function getHeaders(bool $throw = true): array
    {
        if ($this->initializer) {
            self::initialize($this);
        }

        $headers = $this->response->getHeaders(false);

        if ($throw) {
            $this->checkStatusCode();
        }

        return $headers;
    }

    public function getInfo(?string $type = null)
    {
        if ('debug' === ($type ?? 'debug')) {
            $debug = implode('', array_column($this->info['previous_info'] ?? [], 'debug'));
            $debug .= $this->response->getInfo('debug');

            if ('debug' === $type) {
                return $debug;
            }
        }

        if (null !== $type) {
            return $this->info[$type] ?? $this->response->getInfo($type);
        }

        return array_merge($this->info + $this->response->getInfo(), ['debug' => $debug]);
    }

    public function toStream(bool $throw = true)
    {
        if ($throw) {
            // Ensure headers arrived
            $this->getHeaders(true);
        }

        $handle = function () {
            $stream = $this->response instanceof StreamableInterface ? $this->response->toStream(false) : StreamWrapper::createResource($this->response);

            return stream_get_meta_data($stream)['wrapper_data']->stream_cast(\STREAM_CAST_FOR_SELECT);
        };

        $stream = StreamWrapper::createResource($this);
        stream_get_meta_data($stream)['wrapper_data']
            ->bindHandles($handle, $this->content);

        return $stream;
    }

    public function cancel(): void
    {
        if ($this->info['canceled']) {
            return;
        }

        $this->info['canceled'] = true;
        $this->info['error'] = 'Response has been canceled.';
        $this->close();
        $client = $this->client;
        $this->client = null;

        if (!$this->passthru) {
            return;
        }

        try {
            foreach (self::passthru($client, $this, new LastChunk()) as $chunk) {
                // no-op
            }

            $this->passthru = null;
        } catch (ExceptionInterface $e) {
            // ignore any errors when canceling
        }
    }

    public function __destruct()
    {
        $httpException = null;

        if ($this->initializer && null === $this->getInfo('error')) {
            try {
                self::initialize($this, -0.0);
                $this->getHeaders(true);
            } catch (HttpExceptionInterface $httpException) {
                // no-op
            }
        }

        if ($this->passthru && null === $this->getInfo('error')) {
            $this->info['canceled'] = true;

            try {
                foreach (self::passthru($this->client, $this, new LastChunk()) as $chunk) {
                    // no-op
                }
            } catch (ExceptionInterface $e) {
                // ignore any errors when destructing
            }
        }

        if (null !== $httpException) {
            throw $httpException;
        }
    }

    /**
     * @internal
     */
    public static function stream(iterable $responses, ?float $timeout = null, ?string $class = null): \Generator
    {
        while ($responses) {
            $wrappedResponses = [];
            $asyncMap = new \SplObjectStorage();
            $client = null;

            foreach ($responses as $r) {
                if (!$r instanceof self) {
                    throw new \TypeError(sprintf('"%s::stream()" expects parameter 1 to be an iterable of AsyncResponse objects, "%s" given.', $class ?? static::class, get_debug_type($r)));
                }

                if (null !== $e = $r->info['error'] ?? null) {
                    yield $r => $chunk = new ErrorChunk($r->offset, new TransportException($e));
                    $chunk->didThrow() ?: $chunk->getContent();
                    continue;
                }

                if (null === $client) {
                    $client = $r->client;
                } elseif ($r->client !== $client) {
                    throw new TransportException('Cannot stream AsyncResponse objects with many clients.');
                }

                $asyncMap[$r->response] = $r;
                $wrappedResponses[] = $r->response;

                if ($r->stream) {
                    yield from self::passthruStream($response = $r->response, $r, new FirstChunk(), $asyncMap);

                    if (!isset($asyncMap[$response])) {
                        array_pop($wrappedResponses);
                    }

                    if ($r->response !== $response && !isset($asyncMap[$r->response])) {
                        $asyncMap[$r->response] = $r;
                        $wrappedResponses[] = $r->response;
                    }
                }
            }

            if (!$client || !$wrappedResponses) {
                return;
            }

            $chunk = null;
            foreach ($client->stream($wrappedResponses, $timeout) as $response => $chunk) {
                $r = $asyncMap[$response];

                if (null === $chunk->getError()) {
                    if ($chunk->isFirst()) {
                        // Ensure no exception is thrown on destruct for the wrapped response
                        $r->response->getStatusCode();
                    } elseif (0 === $r->offset && null === $r->content && $chunk->isLast()) {
                        $r->content = fopen('php://memory', 'w+');
                    }
                }

                if (!$r->passthru) {
                    if (null !== $chunk->getError() || $chunk->isLast()) {
                        unset($asyncMap[$response]);
                    } elseif (null !== $r->content && '' !== ($content = $chunk->getContent()) && \strlen($content) !== fwrite($r->content, $content)) {
                        $chunk = new ErrorChunk($r->offset, new TransportException(sprintf('Failed writing %d bytes to the response buffer.', \strlen($content))));
                        $r->info['error'] = $chunk->getError();
                        $r->response->cancel();
                    }

                    yield $r => $chunk;
                    continue;
                }

                if (null !== $chunk->getError()) {
                    // no-op
                } elseif ($chunk->isFirst()) {
                    $r->yieldedState = self::FIRST_CHUNK_YIELDED;
                } elseif (self::FIRST_CHUNK_YIELDED !== $r->yieldedState && null === $chunk->getInformationalStatus()) {
                    throw new \LogicException(sprintf('Instance of "%s" is already consumed and cannot be managed by "%s". A decorated client should not call any of the response\'s methods in its "request()" method.', get_debug_type($response), $class ?? static::class));
                }

                foreach (self::passthru($r->client, $r, $chunk, $asyncMap) as $chunk) {
                    yield $r => $chunk;
                }

                if ($r->response !== $response && isset($asyncMap[$response])) {
                    break;
                }
            }

            if (null === $chunk) {
                throw new \LogicException(\sprintf('"%s" is not compliant with HttpClientInterface: its "stream()" method didn\'t yield any chunks when it should have.', get_debug_type($client)));
            }
            if (null === $chunk->getError() && $chunk->isLast()) {
                $r->yieldedState = self::LAST_CHUNK_YIELDED;
            }
            if (null === $chunk->getError() && self::LAST_CHUNK_YIELDED !== $r->yieldedState && $r->response === $response && null !== $r->client) {
                throw new \LogicException('A chunk passthru must yield an "isLast()" chunk before ending a stream.');
            }

            $responses = [];
            foreach ($asyncMap as $response) {
                $r = $asyncMap[$response];

                if (null !== $r->client) {
                    $responses[] = $asyncMap[$response];
                }
            }
        }
    }

    /**
     * @param \SplObjectStorage<ResponseInterface, AsyncResponse>|null $asyncMap
     */
    private static function passthru(HttpClientInterface $client, self $r, ChunkInterface $chunk, ?\SplObjectStorage $asyncMap = null): \Generator
    {
        $r->stream = null;
        $response = $r->response;
        $context = new AsyncContext($r->passthru, $client, $r->response, $r->info, $r->content, $r->offset);
        if (null === $stream = ($r->passthru)($chunk, $context)) {
            if ($r->response === $response && (null !== $chunk->getError() || $chunk->isLast())) {
                throw new \LogicException('A chunk passthru cannot swallow the last chunk.');
            }

            return;
        }

        if (!$stream instanceof \Iterator) {
            throw new \LogicException(sprintf('A chunk passthru must return an "Iterator", "%s" returned.', get_debug_type($stream)));
        }
        $r->stream = $stream;

        yield from self::passthruStream($response, $r, null, $asyncMap);
    }

    /**
     * @param \SplObjectStorage<ResponseInterface, AsyncResponse>|null $asyncMap
     */
    private static function passthruStream(ResponseInterface $response, self $r, ?ChunkInterface $chunk, ?\SplObjectStorage $asyncMap): \Generator
    {
        while (true) {
            try {
                if (null !== $chunk && $r->stream) {
                    $r->stream->next();
                }

                if (!$r->stream || !$r->stream->valid() || !$r->stream) {
                    $r->stream = null;
                    break;
                }
            } catch (\Throwable $e) {
                unset($asyncMap[$response]);
                $r->stream = null;
                $r->info['error'] = $e->getMessage();
                $r->response->cancel();

                yield $r => $chunk = new ErrorChunk($r->offset, $e);
                $chunk->didThrow() ?: $chunk->getContent();
                break;
            }

            $chunk = $r->stream->current();

            if (!$chunk instanceof ChunkInterface) {
                throw new \LogicException(sprintf('A chunk passthru must yield instances of "%s", "%s" yielded.', ChunkInterface::class, get_debug_type($chunk)));
            }

            if (null !== $chunk->getError()) {
                // no-op
            } elseif ($chunk->isFirst()) {
                $e = $r->openBuffer();

                yield $r => $chunk;

                if ($r->initializer && null === $r->getInfo('error')) {
                    // Ensure the HTTP status code is always checked
                    $r->getHeaders(true);
                }

                if (null === $e) {
                    continue;
                }

                $r->response->cancel();
                $chunk = new ErrorChunk($r->offset, $e);
            } elseif ('' !== $content = $chunk->getContent()) {
                if (null !== $r->shouldBuffer) {
                    throw new \LogicException('A chunk passthru must yield an "isFirst()" chunk before any content chunk.');
                }

                if (null !== $r->content && \strlen($content) !== fwrite($r->content, $content)) {
                    $chunk = new ErrorChunk($r->offset, new TransportException(sprintf('Failed writing %d bytes to the response buffer.', \strlen($content))));
                    $r->info['error'] = $chunk->getError();
                    $r->response->cancel();
                }
            }

            if (null !== $chunk->getError() || $chunk->isLast()) {
                $stream = $r->stream;
                $r->stream = null;
                unset($asyncMap[$response]);
            }

            if (null === $chunk->getError()) {
                $r->offset += \strlen($content);

                yield $r => $chunk;

                if (!$chunk->isLast()) {
                    continue;
                }

                $stream->next();

                if ($stream->valid()) {
                    throw new \LogicException('A chunk passthru cannot yield after an "isLast()" chunk.');
                }

                $r->passthru = null;
            } else {
                if ($chunk instanceof ErrorChunk) {
                    $chunk->didThrow(false);
                } else {
                    try {
                        $chunk = new ErrorChunk($chunk->getOffset(), !$chunk->isTimeout() ?: $chunk->getError());
                    } catch (TransportExceptionInterface $e) {
                        $chunk = new ErrorChunk($chunk->getOffset(), $e);
                    }
                }

                yield $r => $chunk;
                $chunk->didThrow() ?: $chunk->getContent();
            }

            break;
        }
    }

    private function openBuffer(): ?\Throwable
    {
        if (null === $shouldBuffer = $this->shouldBuffer) {
            throw new \LogicException('A chunk passthru cannot yield more than one "isFirst()" chunk.');
        }

        $e = $this->shouldBuffer = null;

        if ($shouldBuffer instanceof \Closure) {
            try {
                $shouldBuffer = $shouldBuffer($this->getHeaders(false));

                if (null !== $e = $this->response->getInfo('error')) {
                    throw new TransportException($e);
                }
            } catch (\Throwable $e) {
                $this->info['error'] = $e->getMessage();
                $this->response->cancel();
            }
        }

        if (true === $shouldBuffer) {
            $this->content = fopen('php://temp', 'w+');
        } elseif (\is_resource($shouldBuffer)) {
            $this->content = $shouldBuffer;
        }

        return $e;
    }

    private function close(): void
    {
        $this->response->cancel();
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\HttpClient\Response;

use Amp\ByteStream\StreamException;
use Amp\CancellationTokenSource;
use Amp\Coroutine;
use Amp\Deferred;
use Amp\Http\Client\HttpException;
use Amp\Http\Client\Request;
use Amp\Http\Client\Response;
use Amp\Loop;
use Amp\Promise;
use Amp\Success;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpClient\Chunk\FirstChunk;
use Symfony\Component\HttpClient\Chunk\InformationalChunk;
use Symfony\Component\HttpClient\Exception\InvalidArgumentException;
use Symfony\Component\HttpClient\Exception\TransportException;
use Symfony\Component\HttpClient\HttpClientTrait;
use Symfony\Component\HttpClient\Internal\AmpBody;
use Symfony\Component\HttpClient\Internal\AmpClientState;
use Symfony\Component\HttpClient\Internal\Canary;
use Symfony\Component\HttpClient\Internal\ClientState;
use Symfony\Contracts\HttpClient\ResponseInterface;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @internal
 */
final class AmpResponse implements ResponseInterface, StreamableInterface
{
    use CommonResponseTrait;
    use TransportResponseTrait;

    private static $nextId = 'a';

    private $multi;
    private $options;
    private $onProgress;

    private static $delay;

    /**
     * @internal
     */
    public function __construct(AmpClientState $multi, Request $request, array $options, ?LoggerInterface $logger)
    {
        $this->multi = $multi;
        $this->options = &$options;
        $this->logger = $logger;
        $this->timeout = $options['timeout'];
        $this->shouldBuffer = $options['buffer'];

        if ($this->inflate = \extension_loaded('zlib') && !$request->hasHeader('accept-encoding')) {
            $request->setHeader('Accept-Encoding', 'gzip');
        }

        $this->initializer = static function (self $response) {
            return null !== $response->options;
        };

        $info = &$this->info;
        $headers = &$this->headers;
        $canceller = new CancellationTokenSource();
        $handle = &$this->handle;

        $info['url'] = (string) $request->getUri();
        $info['http_method'] = $request->getMethod();
        $info['start_time'] = null;
        $info['redirect_url'] = null;
        $info['redirect_time'] = 0.0;
        $info['redirect_count'] = 0;
        $info['size_upload'] = 0.0;
        $info['size_download'] = 0.0;
        $info['upload_content_length'] = -1.0;
        $info['download_content_length'] = -1.0;
        $info['user_data'] = $options['user_data'];
        $info['max_duration'] = $options['max_duration'];
        $info['debug'] = '';

        $onProgress = $options['on_progress'] ?? static function () {};
        $onProgress = $this->onProgress = static function () use (&$info, $onProgress) {
            $info['total_time'] = microtime(true) - $info['start_time'];
            $onProgress((int) $info['size_download'], ((int) (1 + $info['download_content_length']) ?: 1) - 1, (array) $info);
        };

        $pauseDeferred = new Deferred();
        $pause = new Success();

        $throttleWatcher = null;

        $this->id = $id = self::$nextId++;
        Loop::defer(static function () use ($request, $multi, &$id, &$info, &$headers, $canceller, &$options, $onProgress, &$handle, $logger, &$pause) {
            return new Coroutine(self::generateResponse($request, $multi, $id, $info, $headers, $canceller, $options, $onProgress, $handle, $logger, $pause));
        });

        $info['pause_handler'] = static function (float $duration) use (&$throttleWatcher, &$pauseDeferred, &$pause) {
            if (null !== $throttleWatcher) {
                Loop::cancel($throttleWatcher);
            }

            $pause = $pauseDeferred->promise();

            if ($duration <= 0) {
                $deferred = $pauseDeferred;
                $pauseDeferred = new Deferred();
                $deferred->resolve();
            } else {
                $throttleWatcher = Loop::delay(ceil(1000 * $duration), static function () use (&$pauseDeferred) {
                    $deferred = $pauseDeferred;
                    $pauseDeferred = new Deferred();
                    $deferred->resolve();
                });
            }
        };

        $multi->lastTimeout = null;
        $multi->openHandles[$id] = $id;
        ++$multi->responseCount;

        $this->canary = new Canary(static function () use ($canceller, $multi, $id) {
            $canceller->cancel();
            unset($multi->openHandles[$id], $multi->handlesActivity[$id]);
        });
    }

    /**
     * {@inheritdoc}
     */
    public function getInfo(?string $type = null)
    {
        return null !== $type ? $this->info[$type] ?? null : $this->info;
    }

    public function __sleep(): array
    {
        throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
    }

    public function __wakeup()
    {
        throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
    }

    public function __destruct()
    {
        try {
            $this->doDestruct();
        } finally {
            // Clear the DNS cache when all requests completed
            if (0 >= --$this->multi->responseCount) {
                $this->multi->responseCount = 0;
                $this->multi->dnsCache = [];
            }
        }
    }

    /**
     * {@inheritdoc}
     */
    private static function schedule(self $response, array &$runningResponses): void
    {
        if (isset($runningResponses[0])) {
            $runningResponses[0][1][$response->id] = $response;
        } else {
            $runningResponses[0] = [$response->multi, [$response->id => $response]];
        }

        if (!isset($response->multi->openHandles[$response->id])) {
            $response->multi->handlesActivity[$response->id][] = null;
            $response->multi->handlesActivity[$response->id][] = null !== $response->info['error'] ? new TransportException($response->info['error']) : null;
        }
    }

    /**
     * {@inheritdoc}
     *
     * @param AmpClientState $multi
     */
    private static function perform(ClientState $multi, ?array &$responses = null): void
    {
        if ($responses) {
            foreach ($responses as $response) {
                try {
                    if ($response->info['start_time']) {
                        $response->info['total_time'] = microtime(true) - $response->info['start_time'];
                        ($response->onProgress)();
                    }
                } catch (\Throwable $e) {
                    $multi->handlesActivity[$response->id][] = null;
                    $multi->handlesActivity[$response->id][] = $e;
                }
            }
        }
    }

    /**
     * {@inheritdoc}
     *
     * @param AmpClientState $multi
     */
    private static function select(ClientState $multi, float $timeout): int
    {
        $timeout += microtime(true);
        self::$delay = Loop::defer(static function () use ($timeout) {
            if (0 < $timeout -= microtime(true)) {
                self::$delay = Loop::delay(ceil(1000 * $timeout), [Loop::class, 'stop']);
            } else {
                Loop::stop();
            }
        });

        Loop::run();

        return null === self::$delay ? 1 : 0;
    }

    private static function generateResponse(Request $request, AmpClientState $multi, string $id, array &$info, array &$headers, CancellationTokenSource $canceller, array &$options, \Closure $onProgress, &$handle, ?LoggerInterface $logger, Promise &$pause)
    {
        $request->setInformationalResponseHandler(static function (Response $response) use ($multi, $id, &$info, &$headers) {
            self::addResponseHeaders($response, $info, $headers);
            $multi->handlesActivity[$id][] = new InformationalChunk($response->getStatus(), $response->getHeaders());
            self::stopLoop();
        });

        try {
            /* @var Response $response */
            if (null === $response = yield from self::getPushedResponse($request, $multi, $info, $headers, $options, $logger)) {
                $logger && $logger->info(sprintf('Request: "%s %s"', $info['http_method'], $info['url']));

                $response = yield from self::followRedirects($request, $multi, $info, $headers, $canceller, $options, $onProgress, $handle, $logger, $pause);
            }

            $options = null;

            $multi->handlesActivity[$id][] = new FirstChunk();

            if ('HEAD' === $response->getRequest()->getMethod() || \in_array($info['http_code'], [204, 304], true)) {
                $multi->handlesActivity[$id][] = null;
                $multi->handlesActivity[$id][] = null;
                self::stopLoop();

                return;
            }

            if ($response->hasHeader('content-length')) {
                $info['download_content_length'] = (float) $response->getHeader('content-length');
            }

            $body = $response->getBody();

            while (true) {
                self::stopLoop();

                yield $pause;

                if (null === $data = yield $body->read()) {
                    break;
                }

                $info['size_download'] += \strlen($data);
                $multi->handlesActivity[$id][] = $data;
            }

            $multi->handlesActivity[$id][] = null;
            $multi->handlesActivity[$id][] = null;
        } catch (\Throwable $e) {
            $multi->handlesActivity[$id][] = null;
            $multi->handlesActivity[$id][] = $e;
        } finally {
            $info['download_content_length'] = $info['size_download'];
        }

        self::stopLoop();
    }

    private static function followRedirects(Request $originRequest, AmpClientState $multi, array &$info, array &$headers, CancellationTokenSource $canceller, array $options, \Closure $onProgress, &$handle, ?LoggerInterface $logger, Promise &$pause)
    {
        yield $pause;

        $originRequest->setBody(new AmpBody($options['body'], $info, $onProgress));
        $response = yield $multi->request($options, $originRequest, $canceller->getToken(), $info, $onProgress, $handle);
        $previousUrl = null;

        while (true) {
            self::addResponseHeaders($response, $info, $headers);
            $status = $response->getStatus();

            if (!\in_array($status, [301, 302, 303, 307, 308], true) || null === $location = $response->getHeader('location')) {
                return $response;
            }

            $urlResolver = new class() {
                use HttpClientTrait {
                    parseUrl as public;
                    resolveUrl as public;
                }
            };

            try {
                $previousUrl = $previousUrl ?? $urlResolver::parseUrl($info['url']);
                $location = $urlResolver::parseUrl($location);
                $location = $urlResolver::resolveUrl($location, $previousUrl);
                $info['redirect_url'] = implode('', $location);
            } catch (InvalidArgumentException $e) {
                return $response;
            }

            if (0 >= $options['max_redirects'] || $info['redirect_count'] >= $options['max_redirects']) {
                return $response;
            }

            $logger && $logger->info(sprintf('Redirecting: "%s %s"', $status, $info['url']));

            try {
                // Discard body of redirects
                while (null !== yield $response->getBody()->read()) {
                }
            } catch (HttpException|StreamException $e) {
                // Ignore streaming errors on previous responses
            }

            ++$info['redirect_count'];
            $info['url'] = $info['redirect_url'];
            $info['redirect_url'] = null;
            $previousUrl = $location;

            $request = new Request($info['url'], $info['http_method']);
            $request->setProtocolVersions($originRequest->getProtocolVersions());
            $request->setTcpConnectTimeout($originRequest->getTcpConnectTimeout());
            $request->setTlsHandshakeTimeout($originRequest->getTlsHandshakeTimeout());
            $request->setTransferTimeout($originRequest->getTransferTimeout());

            if (\in_array($status, [301, 302, 303], true)) {
                $originRequest->removeHeader('transfer-encoding');
                $originRequest->removeHeader('content-length');
                $originRequest->removeHeader('content-type');

                // Do like curl and browsers: turn POST to GET on 301, 302 and 303
                if ('POST' === $response->getRequest()->getMethod() || 303 === $status) {
                    $info['http_method'] = 'HEAD' === $response->getRequest()->getMethod() ? 'HEAD' : 'GET';
                    $request->setMethod($info['http_method']);
                }
            } else {
                $request->setBody(AmpBody::rewind($response->getRequest()->getBody()));
            }

            foreach ($originRequest->getRawHeaders() as [$name, $value]) {
                $request->addHeader($name, $value);
            }

            if ($request->getUri()->getAuthority() !== $originRequest->getUri()->getAuthority()) {
                $request->removeHeader('authorization');
                $request->removeHeader('cookie');
                $request->removeHeader('host');
            }

            yield $pause;

            $response = yield $multi->request($options, $request, $canceller->getToken(), $info, $onProgress, $handle);
            $info['redirect_time'] = microtime(true) - $info['start_time'];
        }
    }

    private static function addResponseHeaders(Response $response, array &$info, array &$headers): void
    {
        $info['http_code'] = $response->getStatus();

        if ($headers) {
            $info['debug'] .= "< \r\n";
            $headers = [];
        }

        $h = sprintf('HTTP/%s %s %s', $response->getProtocolVersion(), $response->getStatus(), $response->getReason());
        $info['debug'] .= "< {$h}\r\n";
        $info['response_headers'][] = $h;

        foreach ($response->getRawHeaders() as [$name, $value]) {
            $headers[strtolower($name)][] = $value;
            $h = $name.': '.$value;
            $info['debug'] .= "< {$h}\r\n";
            $info['response_headers'][] = $h;
        }

        $info['debug'] .= "< \r\n";
    }

    /**
     * Accepts pushed responses only if their headers related to authentication match the request.
     */
    private static function getPushedResponse(Request $request, AmpClientState $multi, array &$info, array &$headers, array $options, ?LoggerInterface $logger)
    {
        if ('' !== $options['body']) {
            return null;
        }

        $authority = $request->getUri()->getAuthority();

        foreach ($multi->pushedResponses[$authority] ?? [] as $i => [$pushedUrl, $pushDeferred, $pushedRequest, $pushedResponse, $parentOptions]) {
            if ($info['url'] !== $pushedUrl || $info['http_method'] !== $pushedRequest->getMethod()) {
                continue;
            }

            foreach ($parentOptions as $k => $v) {
                if ($options[$k] !== $v) {
                    continue 2;
                }
            }

            foreach (['authorization', 'cookie', 'range', 'proxy-authorization'] as $k) {
                if ($pushedRequest->getHeaderArray($k) !== $request->getHeaderArray($k)) {
                    continue 2;
                }
            }

            $response = yield $pushedResponse;

            foreach ($response->getHeaderArray('vary') as $vary) {
                foreach (preg_split('/\s*+,\s*+/', $vary) as $v) {
                    if ('*' === $v || ($pushedRequest->getHeaderArray($v) !== $request->getHeaderArray($v) && 'accept-encoding' !== strtolower($v))) {
                        $logger && $logger->debug(sprintf('Skipping pushed response: "%s"', $info['url']));
                        continue 3;
                    }
                }
            }

            $pushDeferred->resolve();
            $logger && $logger->debug(sprintf('Accepting pushed response: "%s %s"', $info['http_method'], $info['url']));
            self::addResponseHeaders($response, $info, $headers);
            unset($multi->pushedResponses[$authority][$i]);

            if (!$multi->pushedResponses[$authority]) {
                unset($multi->pushedResponses[$authority]);
            }

            return $response;
        }
    }

    private static function stopLoop(): void
    {
        if (null !== self::$delay) {
            Loop::cancel(self::$delay);
            self::$delay = null;
        }

        Loop::defer([Loop::class, 'stop']);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\HttpClient\Response;

use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 */
interface StreamableInterface
{
    /**
     * Casts the response to a PHP stream resource.
     *
     * @return resource
     *
     * @throws TransportExceptionInterface   When a network error occurs
     * @throws RedirectionExceptionInterface On a 3xx when $throw is true and the "max_redirects" option has been reached
     * @throws ClientExceptionInterface      On a 4xx when $throw is true
     * @throws ServerExceptionInterface      On a 5xx when $throw is true
     */
    public function toStream(bool $throw = 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\HttpClient\Response;

use Symfony\Component\HttpClient\Chunk\DataChunk;
use Symfony\Component\HttpClient\Chunk\LastChunk;
use Symfony\Component\HttpClient\Exception\TransportException;
use Symfony\Contracts\HttpClient\ChunkInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;

/**
 * A DTO to work with AsyncResponse.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
final class AsyncContext
{
    private $passthru;
    private $client;
    private $response;
    private $info = [];
    private $content;
    private $offset;

    public function __construct(&$passthru, HttpClientInterface $client, ResponseInterface &$response, array &$info, $content, int $offset)
    {
        $this->passthru = &$passthru;
        $this->client = $client;
        $this->response = &$response;
        $this->info = &$info;
        $this->content = $content;
        $this->offset = $offset;
    }

    /**
     * Returns the HTTP status without consuming the response.
     */
    public function getStatusCode(): int
    {
        return $this->response->getInfo('http_code');
    }

    /**
     * Returns the headers without consuming the response.
     */
    public function getHeaders(): array
    {
        $headers = [];

        foreach ($this->response->getInfo('response_headers') as $h) {
            if (11 <= \strlen($h) && '/' === $h[4] && preg_match('#^HTTP/\d+(?:\.\d+)? ([123456789]\d\d)(?: |$)#', $h, $m)) {
                $headers = [];
            } elseif (2 === \count($m = explode(':', $h, 2))) {
                $headers[strtolower($m[0])][] = ltrim($m[1]);
            }
        }

        return $headers;
    }

    /**
     * @return resource|null The PHP stream resource where the content is buffered, if it is
     */
    public function getContent()
    {
        return $this->content;
    }

    /**
     * Creates a new chunk of content.
     */
    public function createChunk(string $data): ChunkInterface
    {
        return new DataChunk($this->offset, $data);
    }

    /**
     * Pauses the request for the given number of seconds.
     */
    public function pause(float $duration): void
    {
        if (\is_callable($pause = $this->response->getInfo('pause_handler'))) {
            $pause($duration);
        } elseif (0 < $duration) {
            usleep((int) (1E6 * $duration));
        }
    }

    /**
     * Cancels the request and returns the last chunk to yield.
     */
    public function cancel(): ChunkInterface
    {
        $this->info['canceled'] = true;
        $this->info['error'] = 'Response has been canceled.';
        $this->response->cancel();

        return new LastChunk();
    }

    /**
     * Returns the current info of the response.
     */
    public function getInfo(?string $type = null)
    {
        if (null !== $type) {
            return $this->info[$type] ?? $this->response->getInfo($type);
        }

        return $this->info + $this->response->getInfo();
    }

    /**
     * Attaches an info to the response.
     *
     * @return $this
     */
    public function setInfo(string $type, $value): self
    {
        if ('canceled' === $type && $value !== $this->info['canceled']) {
            throw new \LogicException('You cannot set the "canceled" info directly.');
        }

        if (null === $value) {
            unset($this->info[$type]);
        } else {
            $this->info[$type] = $value;
        }

        return $this;
    }

    /**
     * Returns the currently processed response.
     */
    public function getResponse(): ResponseInterface
    {
        return $this->response;
    }

    /**
     * Replaces the currently processed response by doing a new request.
     */
    public function replaceRequest(string $method, string $url, array $options = []): ResponseInterface
    {
        $this->info['previous_info'][] = $info = $this->response->getInfo();
        if (null !== $onProgress = $options['on_progress'] ?? null) {
            $thisInfo = &$this->info;
            $options['on_progress'] = static function (int $dlNow, int $dlSize, array $info) use (&$thisInfo, $onProgress) {
                $onProgress($dlNow, $dlSize, $thisInfo + $info);
            };
        }
        if (0 < ($info['max_duration'] ?? 0) && 0 < ($info['total_time'] ?? 0)) {
            if (0 >= $options['max_duration'] = $info['max_duration'] - $info['total_time']) {
                throw new TransportException(sprintf('Max duration was reached for "%s".', $info['url']));
            }
        }

        return $this->response = $this->client->request($method, $url, ['buffer' => false] + $options);
    }

    /**
     * Replaces the currently processed response by another one.
     */
    public function replaceResponse(ResponseInterface $response): ResponseInterface
    {
        $this->info['previous_info'][] = $this->response->getInfo();

        return $this->response = $response;
    }

    /**
     * Replaces or removes the chunk filter iterator.
     *
     * @param ?callable(ChunkInterface, self): ?\Iterator $passthru
     */
    public function passthru(?callable $passthru = null): void
    {
        $this->passthru = $passthru ?? static function ($chunk, $context) {
            $context->passthru = null;

            yield $chunk;
        };
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\HttpClient\Response;

use Symfony\Component\HttpClient\Exception\ClientException;
use Symfony\Component\HttpClient\Exception\JsonException;
use Symfony\Component\HttpClient\Exception\RedirectionException;
use Symfony\Component\HttpClient\Exception\ServerException;
use Symfony\Component\HttpClient\Exception\TransportException;

/**
 * Implements common logic for response classes.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @internal
 */
trait CommonResponseTrait
{
    /**
     * @var callable|null A callback that tells whether we're waiting for response headers
     */
    private $initializer;
    private $shouldBuffer;
    private $content;
    private $offset = 0;
    private $jsonData;

    /**
     * {@inheritdoc}
     */
    public function getContent(bool $throw = true): string
    {
        if ($this->initializer) {
            self::initialize($this);
        }

        if ($throw) {
            $this->checkStatusCode();
        }

        if (null === $this->content) {
            $content = null;

            foreach (self::stream([$this]) as $chunk) {
                if (!$chunk->isLast()) {
                    $content .= $chunk->getContent();
                }
            }

            if (null !== $content) {
                return $content;
            }

            if (null === $this->content) {
                throw new TransportException('Cannot get the content of the response twice: buffering is disabled.');
            }
        } else {
            foreach (self::stream([$this]) as $chunk) {
                // Chunks are buffered in $this->content already
            }
        }

        rewind($this->content);

        return stream_get_contents($this->content);
    }

    /**
     * {@inheritdoc}
     */
    public function toArray(bool $throw = true): array
    {
        if ('' === $content = $this->getContent($throw)) {
            throw new JsonException('Response body is empty.');
        }

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

        try {
            $content = json_decode($content, true, 512, \JSON_BIGINT_AS_STRING | (\PHP_VERSION_ID >= 70300 ? \JSON_THROW_ON_ERROR : 0));
        } catch (\JsonException $e) {
            throw new JsonException($e->getMessage().sprintf(' for "%s".', $this->getInfo('url')), $e->getCode());
        }

        if (\PHP_VERSION_ID < 70300 && \JSON_ERROR_NONE !== json_last_error()) {
            throw new JsonException(json_last_error_msg().sprintf(' for "%s".', $this->getInfo('url')), json_last_error());
        }

        if (!\is_array($content)) {
            throw new JsonException(sprintf('JSON content was expected to decode to an array, "%s" returned for "%s".', get_debug_type($content), $this->getInfo('url')));
        }

        if (null !== $this->content) {
            // Option "buffer" is true
            return $this->jsonData = $content;
        }

        return $content;
    }

    /**
     * {@inheritdoc}
     */
    public function toStream(bool $throw = true)
    {
        if ($throw) {
            // Ensure headers arrived
            $this->getHeaders($throw);
        }

        $stream = StreamWrapper::createResource($this);
        stream_get_meta_data($stream)['wrapper_data']
            ->bindHandles($this->handle, $this->content);

        return $stream;
    }

    public function __sleep(): array
    {
        throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
    }

    public function __wakeup()
    {
        throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
    }

    /**
     * Closes the response and all its network handles.
     */
    abstract protected function close(): void;

    private static function initialize(self $response): void
    {
        if (null !== $response->getInfo('error')) {
            throw new TransportException($response->getInfo('error'));
        }

        try {
            if (($response->initializer)($response, -0.0)) {
                foreach (self::stream([$response], -0.0) as $chunk) {
                    if ($chunk->isFirst()) {
                        break;
                    }
                }
            }
        } catch (\Throwable $e) {
            // Persist timeouts thrown during initialization
            $response->info['error'] = $e->getMessage();
            $response->close();
            throw $e;
        }

        $response->initializer = null;
    }

    private function checkStatusCode()
    {
        $code = $this->getInfo('http_code');

        if (500 <= $code) {
            throw new ServerException($this);
        }

        if (400 <= $code) {
            throw new ClientException($this);
        }

        if (300 <= $code) {
            throw new RedirectionException($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\HttpClient\Response;

use GuzzleHttp\Promise\Create;
use GuzzleHttp\Promise\PromiseInterface as GuzzlePromiseInterface;
use Http\Promise\Promise as HttplugPromiseInterface;
use Psr\Http\Message\ResponseInterface as Psr7ResponseInterface;

/**
 * @author Tobias Nyholm <tobias.nyholm@gmail.com>
 *
 * @internal
 */
final class HttplugPromise implements HttplugPromiseInterface
{
    private $promise;

    public function __construct(GuzzlePromiseInterface $promise)
    {
        $this->promise = $promise;
    }

    public function then(?callable $onFulfilled = null, ?callable $onRejected = null): self
    {
        return new self($this->promise->then(
            $this->wrapThenCallback($onFulfilled),
            $this->wrapThenCallback($onRejected)
        ));
    }

    public function cancel(): void
    {
        $this->promise->cancel();
    }

    /**
     * {@inheritdoc}
     */
    public function getState(): string
    {
        return $this->promise->getState();
    }

    /**
     * {@inheritdoc}
     *
     * @return Psr7ResponseInterface|mixed
     */
    public function wait($unwrap = true)
    {
        $result = $this->promise->wait($unwrap);

        while ($result instanceof HttplugPromiseInterface || $result instanceof GuzzlePromiseInterface) {
            $result = $result->wait($unwrap);
        }

        return $result;
    }

    private function wrapThenCallback(?callable $callback): ?callable
    {
        if (null === $callback) {
            return null;
        }

        return static function ($value) use ($callback) {
            return Create::promiseFor($callback($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\HttpClient\Response;

use Symfony\Contracts\HttpClient\ChunkInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\HttpClient\ResponseStreamInterface;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 */
final class ResponseStream implements ResponseStreamInterface
{
    private $generator;

    public function __construct(\Generator $generator)
    {
        $this->generator = $generator;
    }

    public function key(): ResponseInterface
    {
        return $this->generator->key();
    }

    public function current(): ChunkInterface
    {
        return $this->generator->current();
    }

    public function next(): void
    {
        $this->generator->next();
    }

    public function rewind(): void
    {
        $this->generator->rewind();
    }

    public function valid(): bool
    {
        return $this->generator->valid();
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\HttpClient\Response;

use Symfony\Component\HttpClient\Chunk\ErrorChunk;
use Symfony\Component\HttpClient\Chunk\FirstChunk;
use Symfony\Component\HttpClient\Exception\InvalidArgumentException;
use Symfony\Component\HttpClient\Exception\TransportException;
use Symfony\Component\HttpClient\Internal\ClientState;
use Symfony\Contracts\HttpClient\ResponseInterface;

/**
 * A test-friendly response.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
class MockResponse implements ResponseInterface, StreamableInterface
{
    use CommonResponseTrait;
    use TransportResponseTrait {
        doDestruct as public __destruct;
    }

    private $body;
    private $requestOptions = [];
    private $requestUrl;
    private $requestMethod;

    private static $mainMulti;
    private static $idSequence = 0;

    /**
     * @param string|string[]|iterable $body The response body as a string or an iterable of strings,
     *                                       yielding an empty string simulates an idle timeout,
     *                                       throwing an exception yields an ErrorChunk
     *
     * @see ResponseInterface::getInfo() for possible info, e.g. "response_headers"
     */
    public function __construct($body = '', array $info = [])
    {
        $this->body = is_iterable($body) ? $body : (string) $body;
        $this->info = $info + ['http_code' => 200] + $this->info;

        if (!isset($info['response_headers'])) {
            return;
        }

        $responseHeaders = [];

        foreach ($info['response_headers'] as $k => $v) {
            foreach ((array) $v as $v) {
                $responseHeaders[] = (\is_string($k) ? $k.': ' : '').$v;
            }
        }

        $this->info['response_headers'] = [];
        self::addResponseHeaders($responseHeaders, $this->info, $this->headers);
    }

    /**
     * Returns the options used when doing the request.
     */
    public function getRequestOptions(): array
    {
        return $this->requestOptions;
    }

    /**
     * Returns the URL used when doing the request.
     */
    public function getRequestUrl(): string
    {
        return $this->requestUrl;
    }

    /**
     * Returns the method used when doing the request.
     */
    public function getRequestMethod(): string
    {
        return $this->requestMethod;
    }

    /**
     * {@inheritdoc}
     */
    public function getInfo(?string $type = null)
    {
        return null !== $type ? $this->info[$type] ?? null : $this->info;
    }

    /**
     * {@inheritdoc}
     */
    public function cancel(): void
    {
        $this->info['canceled'] = true;
        $this->info['error'] = 'Response has been canceled.';
        try {
            $this->body = null;
        } catch (TransportException $e) {
            // ignore errors when canceling
        }

        $onProgress = $this->requestOptions['on_progress'] ?? static function () {};
        $dlSize = isset($this->headers['content-encoding']) || 'HEAD' === ($this->info['http_method'] ?? null) || \in_array($this->info['http_code'], [204, 304], true) ? 0 : (int) ($this->headers['content-length'][0] ?? 0);
        $onProgress($this->offset, $dlSize, $this->info);
    }

    /**
     * {@inheritdoc}
     */
    protected function close(): void
    {
        $this->inflate = null;
        $this->body = [];
    }

    /**
     * @internal
     */
    public static function fromRequest(string $method, string $url, array $options, ResponseInterface $mock): self
    {
        $response = new self([]);
        $response->requestOptions = $options;
        $response->id = ++self::$idSequence;
        $response->shouldBuffer = $options['buffer'] ?? true;
        $response->initializer = static function (self $response) {
            return \is_array($response->body[0] ?? null);
        };

        $response->info['redirect_count'] = 0;
        $response->info['redirect_url'] = null;
        $response->info['start_time'] = microtime(true);
        $response->info['http_method'] = $method;
        $response->info['http_code'] = 0;
        $response->info['user_data'] = $options['user_data'] ?? null;
        $response->info['max_duration'] = $options['max_duration'] ?? null;
        $response->info['url'] = $url;

        if ($mock instanceof self) {
            $mock->requestOptions = $response->requestOptions;
            $mock->requestMethod = $method;
            $mock->requestUrl = $url;
        }

        self::writeRequest($response, $options, $mock);
        $response->body[] = [$options, $mock];

        return $response;
    }

    /**
     * {@inheritdoc}
     */
    protected static function schedule(self $response, array &$runningResponses): void
    {
        if (!$response->id) {
            throw new InvalidArgumentException('MockResponse instances must be issued by MockHttpClient before processing.');
        }

        $multi = self::$mainMulti ?? self::$mainMulti = new ClientState();

        if (!isset($runningResponses[0])) {
            $runningResponses[0] = [$multi, []];
        }

        $runningResponses[0][1][$response->id] = $response;
    }

    /**
     * {@inheritdoc}
     */
    protected static function perform(ClientState $multi, array &$responses): void
    {
        foreach ($responses as $response) {
            $id = $response->id;

            if (null === $response->body) {
                // Canceled response
                $response->body = [];
            } elseif ([] === $response->body) {
                // Error chunk
                $multi->handlesActivity[$id][] = null;
                $multi->handlesActivity[$id][] = null !== $response->info['error'] ? new TransportException($response->info['error']) : null;
            } elseif (null === $chunk = array_shift($response->body)) {
                // Last chunk
                $multi->handlesActivity[$id][] = null;
                $multi->handlesActivity[$id][] = array_shift($response->body);
            } elseif (\is_array($chunk)) {
                // First chunk
                try {
                    $offset = 0;
                    $chunk[1]->getStatusCode();
                    $chunk[1]->getHeaders(false);
                    self::readResponse($response, $chunk[0], $chunk[1], $offset);
                    $multi->handlesActivity[$id][] = new FirstChunk();
                    $buffer = $response->requestOptions['buffer'] ?? null;

                    if ($buffer instanceof \Closure && $response->content = $buffer($response->headers) ?: null) {
                        $response->content = \is_resource($response->content) ? $response->content : fopen('php://temp', 'w+');
                    }
                } catch (\Throwable $e) {
                    $multi->handlesActivity[$id][] = null;
                    $multi->handlesActivity[$id][] = $e;
                }
            } elseif ($chunk instanceof \Throwable) {
                $multi->handlesActivity[$id][] = null;
                $multi->handlesActivity[$id][] = $chunk;
            } else {
                // Data or timeout chunk
                $multi->handlesActivity[$id][] = $chunk;
            }
        }
    }

    /**
     * {@inheritdoc}
     */
    protected static function select(ClientState $multi, float $timeout): int
    {
        return 42;
    }

    /**
     * Simulates sending the request.
     */
    private static function writeRequest(self $response, array $options, ResponseInterface $mock)
    {
        $onProgress = $options['on_progress'] ?? static function () {};
        $response->info += $mock->getInfo() ?: [];

        // simulate "size_upload" if it is set
        if (isset($response->info['size_upload'])) {
            $response->info['size_upload'] = 0.0;
        }

        // simulate "total_time" if it is not set
        if (!isset($response->info['total_time'])) {
            $response->info['total_time'] = microtime(true) - $response->info['start_time'];
        }

        // "notify" DNS resolution
        $onProgress(0, 0, $response->info);

        // consume the request body
        if (\is_resource($body = $options['body'] ?? '')) {
            $data = stream_get_contents($body);
            if (isset($response->info['size_upload'])) {
                $response->info['size_upload'] += \strlen($data);
            }
        } elseif ($body instanceof \Closure) {
            while ('' !== $data = $body(16372)) {
                if (!\is_string($data)) {
                    throw new TransportException(sprintf('Return value of the "body" option callback must be string, "%s" returned.', get_debug_type($data)));
                }

                // "notify" upload progress
                if (isset($response->info['size_upload'])) {
                    $response->info['size_upload'] += \strlen($data);
                }

                $onProgress(0, 0, $response->info);
            }
        }
    }

    /**
     * Simulates reading the response.
     */
    private static function readResponse(self $response, array $options, ResponseInterface $mock, int &$offset)
    {
        $onProgress = $options['on_progress'] ?? static function () {};

        // populate info related to headers
        $info = $mock->getInfo() ?: [];
        $response->info['http_code'] = ($info['http_code'] ?? 0) ?: $mock->getStatusCode() ?: 200;
        $response->addResponseHeaders($info['response_headers'] ?? [], $response->info, $response->headers);
        $dlSize = isset($response->headers['content-encoding']) || 'HEAD' === $response->info['http_method'] || \in_array($response->info['http_code'], [204, 304], true) ? 0 : (int) ($response->headers['content-length'][0] ?? 0);

        $response->info = [
            'start_time' => $response->info['start_time'],
            'user_data' => $response->info['user_data'],
            'max_duration' => $response->info['max_duration'],
            'http_code' => $response->info['http_code'],
        ] + $info + $response->info;

        if (null !== $response->info['error']) {
            throw new TransportException($response->info['error']);
        }

        if (!isset($response->info['total_time'])) {
            $response->info['total_time'] = microtime(true) - $response->info['start_time'];
        }

        // "notify" headers arrival
        $onProgress(0, $dlSize, $response->info);

        // cast response body to activity list
        $body = $mock instanceof self ? $mock->body : $mock->getContent(false);

        if (!\is_string($body)) {
            try {
                foreach ($body as $chunk) {
                    if ('' === $chunk = (string) $chunk) {
                        // simulate an idle timeout
                        $response->body[] = new ErrorChunk($offset, sprintf('Idle timeout reached for "%s".', $response->info['url']));
                    } else {
                        $response->body[] = $chunk;
                        $offset += \strlen($chunk);
                        // "notify" download progress
                        $onProgress($offset, $dlSize, $response->info);
                    }
                }
            } catch (\Throwable $e) {
                $response->body[] = $e;
            }
        } elseif ('' !== $body) {
            $response->body[] = $body;
            $offset = \strlen($body);
        }

        if (!isset($response->info['total_time'])) {
            $response->info['total_time'] = microtime(true) - $response->info['start_time'];
        }

        // "notify" completion
        $onProgress($offset, $dlSize, $response->info);

        if ($dlSize && $offset !== $dlSize) {
            throw new TransportException(sprintf('Transfer closed with %d bytes remaining to read.', $dlSize - $offset));
        }
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\HttpClient\Response;

use Symfony\Contracts\HttpClient\Exception\ExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;

/**
 * Allows turning ResponseInterface instances to PHP streams.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
class StreamWrapper
{
    /** @var resource|null */
    public $context;

    /** @var HttpClientInterface */
    private $client;

    /** @var ResponseInterface */
    private $response;

    /** @var resource|string|null */
    private $content;

    /** @var resource|null */
    private $handle;

    private $blocking = true;
    private $timeout;
    private $eof = false;
    private $offset = 0;

    /**
     * Creates a PHP stream resource from a ResponseInterface.
     *
     * @return resource
     */
    public static function createResource(ResponseInterface $response, ?HttpClientInterface $client = null)
    {
        if ($response instanceof StreamableInterface) {
            $stack = debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT | \DEBUG_BACKTRACE_IGNORE_ARGS, 2);

            if ($response !== ($stack[1]['object'] ?? null)) {
                return $response->toStream(false);
            }
        }

        if (null === $client && !method_exists($response, 'stream')) {
            throw new \InvalidArgumentException(sprintf('Providing a client to "%s()" is required when the response doesn\'t have any "stream()" method.', __CLASS__));
        }

        static $registered = false;

        if (!$registered = $registered || stream_wrapper_register(strtr(__CLASS__, '\\', '-'), __CLASS__)) {
            throw new \RuntimeException(error_get_last()['message'] ?? 'Registering the "symfony" stream wrapper failed.');
        }

        $context = [
            'client' => $client ?? $response,
            'response' => $response,
        ];

        return fopen(strtr(__CLASS__, '\\', '-').'://'.$response->getInfo('url'), 'r', false, stream_context_create(['symfony' => $context]));
    }

    public function getResponse(): ResponseInterface
    {
        return $this->response;
    }

    /**
     * @param resource|callable|null $handle  The resource handle that should be monitored when
     *                                        stream_select() is used on the created stream
     * @param resource|null          $content The seekable resource where the response body is buffered
     */
    public function bindHandles(&$handle, &$content): void
    {
        $this->handle = &$handle;
        $this->content = &$content;
        $this->offset = null;
    }

    public function stream_open(string $path, string $mode, int $options): bool
    {
        if ('r' !== $mode) {
            if ($options & \STREAM_REPORT_ERRORS) {
                trigger_error(sprintf('Invalid mode "%s": only "r" is supported.', $mode), \E_USER_WARNING);
            }

            return false;
        }

        $context = stream_context_get_options($this->context)['symfony'] ?? null;
        $this->client = $context['client'] ?? null;
        $this->response = $context['response'] ?? null;
        $this->context = null;

        if (null !== $this->client && null !== $this->response) {
            return true;
        }

        if ($options & \STREAM_REPORT_ERRORS) {
            trigger_error('Missing options "client" or "response" in "symfony" stream context.', \E_USER_WARNING);
        }

        return false;
    }

    public function stream_read(int $count)
    {
        if (\is_resource($this->content)) {
            // Empty the internal activity list
            foreach ($this->client->stream([$this->response], 0) as $chunk) {
                try {
                    if (!$chunk->isTimeout() && $chunk->isFirst()) {
                        $this->response->getStatusCode(); // ignore 3/4/5xx
                    }
                } catch (ExceptionInterface $e) {
                    trigger_error($e->getMessage(), \E_USER_WARNING);

                    return false;
                }
            }

            if (0 !== fseek($this->content, $this->offset ?? 0)) {
                return false;
            }

            if ('' !== $data = fread($this->content, $count)) {
                fseek($this->content, 0, \SEEK_END);
                $this->offset += \strlen($data);

                return $data;
            }
        }

        if (\is_string($this->content)) {
            if (\strlen($this->content) <= $count) {
                $data = $this->content;
                $this->content = null;
            } else {
                $data = substr($this->content, 0, $count);
                $this->content = substr($this->content, $count);
            }
            $this->offset += \strlen($data);

            return $data;
        }

        foreach ($this->client->stream([$this->response], $this->blocking ? $this->timeout : 0) as $chunk) {
            try {
                $this->eof = true;
                $this->eof = !$chunk->isTimeout();

                if (!$this->eof && !$this->blocking) {
                    return '';
                }

                $this->eof = $chunk->isLast();

                if ($chunk->isFirst()) {
                    $this->response->getStatusCode(); // ignore 3/4/5xx
                }

                if ('' !== $data = $chunk->getContent()) {
                    if (\strlen($data) > $count) {
                        if (null === $this->content) {
                            $this->content = substr($data, $count);
                        }
                        $data = substr($data, 0, $count);
                    }
                    $this->offset += \strlen($data);

                    return $data;
                }
            } catch (ExceptionInterface $e) {
                trigger_error($e->getMessage(), \E_USER_WARNING);

                return false;
            }
        }

        return '';
    }

    public function stream_set_option(int $option, int $arg1, ?int $arg2): bool
    {
        if (\STREAM_OPTION_BLOCKING === $option) {
            $this->blocking = (bool) $arg1;
        } elseif (\STREAM_OPTION_READ_TIMEOUT === $option) {
            $this->timeout = $arg1 + $arg2 / 1e6;
        } else {
            return false;
        }

        return true;
    }

    public function stream_tell(): int
    {
        return $this->offset ?? 0;
    }

    public function stream_eof(): bool
    {
        return $this->eof && !\is_string($this->content);
    }

    public function stream_seek(int $offset, int $whence = \SEEK_SET): bool
    {
        if (null === $this->content && null === $this->offset) {
            $this->response->getStatusCode();
            $this->offset = 0;
        }

        if (!\is_resource($this->content) || 0 !== fseek($this->content, 0, \SEEK_END)) {
            return false;
        }

        $size = ftell($this->content);

        if (\SEEK_CUR === $whence) {
            $offset += $this->offset ?? 0;
        }

        if (\SEEK_END === $whence || $size < $offset) {
            foreach ($this->client->stream([$this->response]) as $chunk) {
                try {
                    if ($chunk->isFirst()) {
                        $this->response->getStatusCode(); // ignore 3/4/5xx
                    }

                    // Chunks are buffered in $this->content already
                    $size += \strlen($chunk->getContent());

                    if (\SEEK_END !== $whence && $offset <= $size) {
                        break;
                    }
                } catch (ExceptionInterface $e) {
                    trigger_error($e->getMessage(), \E_USER_WARNING);

                    return false;
                }
            }

            if (\SEEK_END === $whence) {
                $offset += $size;
            }
        }

        if (0 <= $offset && $offset <= $size) {
            $this->eof = false;
            $this->offset = $offset;

            return true;
        }

        return false;
    }

    public function stream_cast(int $castAs)
    {
        if (\STREAM_CAST_FOR_SELECT === $castAs) {
            $this->response->getHeaders(false);

            return (\is_callable($this->handle) ? ($this->handle)() : $this->handle) ?? false;
        }

        return false;
    }

    public function stream_stat(): array
    {
        try {
            $headers = $this->response->getHeaders(false);
        } catch (ExceptionInterface $e) {
            trigger_error($e->getMessage(), \E_USER_WARNING);
            $headers = [];
        }

        return [
            'dev' => 0,
            'ino' => 0,
            'mode' => 33060,
            'nlink' => 0,
            'uid' => 0,
            'gid' => 0,
            'rdev' => 0,
            'size' => (int) ($headers['content-length'][0] ?? -1),
            'atime' => 0,
            'mtime' => strtotime($headers['last-modified'][0] ?? '') ?: 0,
            'ctime' => 0,
            'blksize' => 0,
            'blocks' => 0,
        ];
    }

    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.
 */

namespace Symfony\Component\HttpClient\Response;

use Symfony\Component\HttpClient\Chunk\ErrorChunk;
use Symfony\Component\HttpClient\Exception\ClientException;
use Symfony\Component\HttpClient\Exception\RedirectionException;
use Symfony\Component\HttpClient\Exception\ServerException;
use Symfony\Component\HttpClient\TraceableHttpClient;
use Symfony\Component\Stopwatch\StopwatchEvent;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @internal
 */
class TraceableResponse implements ResponseInterface, StreamableInterface
{
    private $client;
    private $response;
    private $content;
    private $event;

    public function __construct(HttpClientInterface $client, ResponseInterface $response, &$content, ?StopwatchEvent $event = null)
    {
        $this->client = $client;
        $this->response = $response;
        $this->content = &$content;
        $this->event = $event;
    }

    public function __sleep(): array
    {
        throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
    }

    public function __wakeup()
    {
        throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
    }

    public function __destruct()
    {
        try {
            if (method_exists($this->response, '__destruct')) {
                $this->response->__destruct();
            }
        } finally {
            if ($this->event && $this->event->isStarted()) {
                $this->event->stop();
            }
        }
    }

    public function getStatusCode(): int
    {
        try {
            return $this->response->getStatusCode();
        } finally {
            if ($this->event && $this->event->isStarted()) {
                $this->event->lap();
            }
        }
    }

    public function getHeaders(bool $throw = true): array
    {
        try {
            return $this->response->getHeaders($throw);
        } finally {
            if ($this->event && $this->event->isStarted()) {
                $this->event->lap();
            }
        }
    }

    public function getContent(bool $throw = true): string
    {
        try {
            if (false === $this->content) {
                return $this->response->getContent($throw);
            }

            return $this->content = $this->response->getContent(false);
        } finally {
            if ($this->event && $this->event->isStarted()) {
                $this->event->stop();
            }
            if ($throw) {
                $this->checkStatusCode($this->response->getStatusCode());
            }
        }
    }

    public function toArray(bool $throw = true): array
    {
        try {
            if (false === $this->content) {
                return $this->response->toArray($throw);
            }

            return $this->content = $this->response->toArray(false);
        } finally {
            if ($this->event && $this->event->isStarted()) {
                $this->event->stop();
            }
            if ($throw) {
                $this->checkStatusCode($this->response->getStatusCode());
            }
        }
    }

    public function cancel(): void
    {
        $this->response->cancel();

        if ($this->event && $this->event->isStarted()) {
            $this->event->stop();
        }
    }

    public function getInfo(?string $type = null)
    {
        return $this->response->getInfo($type);
    }

    /**
     * Casts the response to a PHP stream resource.
     *
     * @return resource
     *
     * @throws TransportExceptionInterface   When a network error occurs
     * @throws RedirectionExceptionInterface On a 3xx when $throw is true and the "max_redirects" option has been reached
     * @throws ClientExceptionInterface      On a 4xx when $throw is true
     * @throws ServerExceptionInterface      On a 5xx when $throw is true
     */
    public function toStream(bool $throw = true)
    {
        if ($throw) {
            // Ensure headers arrived
            $this->response->getHeaders(true);
        }

        if ($this->response instanceof StreamableInterface) {
            return $this->response->toStream(false);
        }

        return StreamWrapper::createResource($this->response, $this->client);
    }

    /**
     * @internal
     */
    public static function stream(HttpClientInterface $client, iterable $responses, ?float $timeout): \Generator
    {
        $wrappedResponses = [];
        $traceableMap = new \SplObjectStorage();

        foreach ($responses as $r) {
            if (!$r instanceof self) {
                throw new \TypeError(sprintf('"%s::stream()" expects parameter 1 to be an iterable of TraceableResponse objects, "%s" given.', TraceableHttpClient::class, get_debug_type($r)));
            }

            $traceableMap[$r->response] = $r;
            $wrappedResponses[] = $r->response;
            if ($r->event && !$r->event->isStarted()) {
                $r->event->start();
            }
        }

        foreach ($client->stream($wrappedResponses, $timeout) as $r => $chunk) {
            if ($traceableMap[$r]->event && $traceableMap[$r]->event->isStarted()) {
                try {
                    if ($chunk->isTimeout() || !$chunk->isLast()) {
                        $traceableMap[$r]->event->lap();
                    } else {
                        $traceableMap[$r]->event->stop();
                    }
                } catch (TransportExceptionInterface $e) {
                    $traceableMap[$r]->event->stop();
                    if ($chunk instanceof ErrorChunk) {
                        $chunk->didThrow(false);
                    } else {
                        $chunk = new ErrorChunk($chunk->getOffset(), $e);
                    }
                }
            }
            yield $traceableMap[$r] => $chunk;
        }
    }

    private function checkStatusCode(int $code)
    {
        if (500 <= $code) {
            throw new ServerException($this);
        }

        if (400 <= $code) {
            throw new ClientException($this);
        }

        if (300 <= $code) {
            throw new RedirectionException($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\HttpClient\Response;

use Psr\Log\LoggerInterface;
use Symfony\Component\HttpClient\Chunk\FirstChunk;
use Symfony\Component\HttpClient\Exception\TransportException;
use Symfony\Component\HttpClient\Internal\Canary;
use Symfony\Component\HttpClient\Internal\ClientState;
use Symfony\Component\HttpClient\Internal\NativeClientState;
use Symfony\Contracts\HttpClient\ResponseInterface;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @internal
 */
final class NativeResponse implements ResponseInterface, StreamableInterface
{
    use CommonResponseTrait;
    use TransportResponseTrait;

    private $context;
    private $url;
    private $resolver;
    private $onProgress;
    private $remaining;
    private $buffer;
    private $multi;
    private $pauseExpiry = 0;

    /**
     * @internal
     */
    public function __construct(NativeClientState $multi, $context, string $url, array $options, array &$info, callable $resolver, ?callable $onProgress, ?LoggerInterface $logger)
    {
        $this->multi = $multi;
        $this->id = $id = (int) $context;
        $this->context = $context;
        $this->url = $url;
        $this->logger = $logger;
        $this->timeout = $options['timeout'];
        $this->info = &$info;
        $this->resolver = $resolver;
        $this->onProgress = $onProgress;
        $this->inflate = !isset($options['normalized_headers']['accept-encoding']);
        $this->shouldBuffer = $options['buffer'] ?? true;

        // Temporary resource to dechunk the response stream
        $this->buffer = fopen('php://temp', 'w+');

        $info['user_data'] = $options['user_data'];
        $info['max_duration'] = $options['max_duration'];
        ++$multi->responseCount;

        $this->initializer = static function (self $response) {
            return null === $response->remaining;
        };

        $pauseExpiry = &$this->pauseExpiry;
        $info['pause_handler'] = static function (float $duration) use (&$pauseExpiry) {
            $pauseExpiry = 0 < $duration ? microtime(true) + $duration : 0;
        };

        $this->canary = new Canary(static function () use ($multi, $id) {
            if (null !== ($host = $multi->openHandles[$id][6] ?? null) && 0 >= --$multi->hosts[$host]) {
                unset($multi->hosts[$host]);
            }
            unset($multi->openHandles[$id], $multi->handlesActivity[$id]);
        });
    }

    /**
     * {@inheritdoc}
     */
    public function getInfo(?string $type = null)
    {
        if (!$info = $this->finalInfo) {
            $info = $this->info;
            $info['url'] = implode('', $info['url']);
            unset($info['size_body'], $info['request_header']);

            if (null === $this->buffer) {
                $this->finalInfo = $info;
            }
        }

        return null !== $type ? $info[$type] ?? null : $info;
    }

    public function __destruct()
    {
        try {
            $this->doDestruct();
        } finally {
            // Clear the DNS cache when all requests completed
            if (0 >= --$this->multi->responseCount) {
                $this->multi->responseCount = 0;
                $this->multi->dnsCache = [];
            }
        }
    }

    private function open(): void
    {
        $url = $this->url;

        set_error_handler(function ($type, $msg) use (&$url) {
            if (\E_NOTICE !== $type || 'fopen(): Content-type not specified assuming application/x-www-form-urlencoded' !== $msg) {
                throw new TransportException($msg);
            }

            $this->logger && $this->logger->info(sprintf('%s for "%s".', $msg, $url ?? $this->url));
        });

        try {
            $this->info['start_time'] = microtime(true);

            [$resolver, $url] = ($this->resolver)($this->multi);

            while (true) {
                $context = stream_context_get_options($this->context);

                if ($proxy = $context['http']['proxy'] ?? null) {
                    $this->info['debug'] .= "* Establish HTTP proxy tunnel to {$proxy}\n";
                    $this->info['request_header'] = $url;
                } else {
                    $this->info['debug'] .= "*   Trying {$this->info['primary_ip']}...\n";
                    $this->info['request_header'] = $this->info['url']['path'].$this->info['url']['query'];
                }

                $this->info['request_header'] = sprintf("> %s %s HTTP/%s \r\n", $context['http']['method'], $this->info['request_header'], $context['http']['protocol_version']);
                $this->info['request_header'] .= implode("\r\n", $context['http']['header'])."\r\n\r\n";

                if (\array_key_exists('peer_name', $context['ssl']) && null === $context['ssl']['peer_name']) {
                    unset($context['ssl']['peer_name']);
                    $this->context = stream_context_create([], ['options' => $context] + stream_context_get_params($this->context));
                }

                // Send request and follow redirects when needed
                $this->handle = $h = fopen($url, 'r', false, $this->context);
                self::addResponseHeaders(stream_get_meta_data($h)['wrapper_data'], $this->info, $this->headers, $this->info['debug']);
                $url = $resolver($this->multi, $this->headers['location'][0] ?? null, $this->context);

                if (null === $url) {
                    break;
                }

                $this->logger && $this->logger->info(sprintf('Redirecting: "%s %s"', $this->info['http_code'], $url ?? $this->url));
            }
        } catch (\Throwable $e) {
            $this->close();
            $this->multi->handlesActivity[$this->id][] = null;
            $this->multi->handlesActivity[$this->id][] = $e;

            return;
        } finally {
            $this->info['pretransfer_time'] = $this->info['total_time'] = microtime(true) - $this->info['start_time'];
            restore_error_handler();
        }

        if (isset($context['ssl']['capture_peer_cert_chain']) && isset(($context = stream_context_get_options($this->context))['ssl']['peer_certificate_chain'])) {
            $this->info['peer_certificate_chain'] = $context['ssl']['peer_certificate_chain'];
        }

        stream_set_blocking($h, false);
        $this->context = $this->resolver = null;

        // Create dechunk buffers
        if (isset($this->headers['content-length'])) {
            $this->remaining = (int) $this->headers['content-length'][0];
        } elseif ('chunked' === ($this->headers['transfer-encoding'][0] ?? null)) {
            stream_filter_append($this->buffer, 'dechunk', \STREAM_FILTER_WRITE);
            $this->remaining = -1;
        } else {
            $this->remaining = -2;
        }

        $this->multi->handlesActivity[$this->id] = [new FirstChunk()];

        if ('HEAD' === $context['http']['method'] || \in_array($this->info['http_code'], [204, 304], true)) {
            $this->multi->handlesActivity[$this->id][] = null;
            $this->multi->handlesActivity[$this->id][] = null;

            return;
        }

        $host = parse_url($this->info['redirect_url'] ?? $this->url, \PHP_URL_HOST);
        $this->multi->lastTimeout = null;
        $this->multi->openHandles[$this->id] = [&$this->pauseExpiry, $h, $this->buffer, $this->onProgress, &$this->remaining, &$this->info, $host];
        $this->multi->hosts[$host] = 1 + ($this->multi->hosts[$host] ?? 0);
    }

    /**
     * {@inheritdoc}
     */
    private function close(): void
    {
        $this->canary->cancel();
        $this->handle = $this->buffer = $this->inflate = $this->onProgress = null;
    }

    /**
     * {@inheritdoc}
     */
    private static function schedule(self $response, array &$runningResponses): void
    {
        if (!isset($runningResponses[$i = $response->multi->id])) {
            $runningResponses[$i] = [$response->multi, []];
        }

        $runningResponses[$i][1][$response->id] = $response;

        if (null === $response->buffer) {
            // Response already completed
            $response->multi->handlesActivity[$response->id][] = null;
            $response->multi->handlesActivity[$response->id][] = null !== $response->info['error'] ? new TransportException($response->info['error']) : null;
        }
    }

    /**
     * {@inheritdoc}
     *
     * @param NativeClientState $multi
     */
    private static function perform(ClientState $multi, ?array &$responses = null): void
    {
        foreach ($multi->openHandles as $i => [$pauseExpiry, $h, $buffer, $onProgress]) {
            if ($pauseExpiry) {
                if (microtime(true) < $pauseExpiry) {
                    continue;
                }

                $multi->openHandles[$i][0] = 0;
            }

            $hasActivity = false;
            $remaining = &$multi->openHandles[$i][4];
            $info = &$multi->openHandles[$i][5];
            $e = null;

            // Read incoming buffer and write it to the dechunk one
            try {
                if ($remaining && '' !== $data = (string) fread($h, 0 > $remaining ? 16372 : $remaining)) {
                    fwrite($buffer, $data);
                    $hasActivity = true;
                    $multi->sleep = false;

                    if (-1 !== $remaining) {
                        $remaining -= \strlen($data);
                    }
                }
            } catch (\Throwable $e) {
                $hasActivity = $onProgress = false;
            }

            if (!$hasActivity) {
                if ($onProgress) {
                    try {
                        // Notify the progress callback so that it can e.g. cancel
                        // the request if the stream is inactive for too long
                        $info['total_time'] = microtime(true) - $info['start_time'];
                        $onProgress();
                    } catch (\Throwable $e) {
                        // no-op
                    }
                }
            } elseif ('' !== $data = stream_get_contents($buffer, -1, 0)) {
                rewind($buffer);
                ftruncate($buffer, 0);

                if (null === $e) {
                    $multi->handlesActivity[$i][] = $data;
                }
            }

            if (null !== $e || !$remaining || feof($h)) {
                // Stream completed
                $info['total_time'] = microtime(true) - $info['start_time'];
                $info['starttransfer_time'] = $info['starttransfer_time'] ?: $info['total_time'];

                if ($onProgress) {
                    try {
                        $onProgress(-1);
                    } catch (\Throwable $e) {
                        // no-op
                    }
                }

                if (null === $e) {
                    if (0 < $remaining) {
                        $e = new TransportException(sprintf('Transfer closed with %s bytes remaining to read.', $remaining));
                    } elseif (-1 === $remaining && fwrite($buffer, '-') && '' !== stream_get_contents($buffer, -1, 0)) {
                        $e = new TransportException('Transfer closed with outstanding data remaining from chunked response.');
                    }
                }

                $multi->handlesActivity[$i][] = null;
                $multi->handlesActivity[$i][] = $e;
                if (null !== ($host = $multi->openHandles[$i][6] ?? null) && 0 >= --$multi->hosts[$host]) {
                    unset($multi->hosts[$host]);
                }
                unset($multi->openHandles[$i]);
                $multi->sleep = false;
            }
        }

        if (null === $responses) {
            return;
        }

        $maxHosts = $multi->maxHostConnections;

        foreach ($responses as $i => $response) {
            if (null !== $response->remaining || null === $response->buffer) {
                continue;
            }

            if ($response->pauseExpiry && microtime(true) < $response->pauseExpiry) {
                // Create empty open handles to tell we still have pending requests
                $multi->openHandles[$i] = [\INF, null, null, null];
            } elseif ($maxHosts && $maxHosts > ($multi->hosts[parse_url($response->url, \PHP_URL_HOST)] ?? 0)) {
                // Open the next pending request - this is a blocking operation so we do only one of them
                $response->open();
                $multi->sleep = false;
                self::perform($multi);
                $maxHosts = 0;
            }
        }
    }

    /**
     * {@inheritdoc}
     *
     * @param NativeClientState $multi
     */
    private static function select(ClientState $multi, float $timeout): int
    {
        if (!$multi->sleep = !$multi->sleep) {
            return -1;
        }

        $_ = $handles = [];
        $now = null;

        foreach ($multi->openHandles as [$pauseExpiry, $h]) {
            if (null === $h) {
                continue;
            }

            if ($pauseExpiry && ($now ?? $now = microtime(true)) < $pauseExpiry) {
                $timeout = min($timeout, $pauseExpiry - $now);
                continue;
            }

            $handles[] = $h;
        }

        if (!$handles) {
            usleep((int) (1E6 * $timeout));

            return 0;
        }

        return stream_select($handles, $_, $_, (int) $timeout, (int) (1E6 * ($timeout - (int) $timeout)));
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\HttpClient;

use Amp\Http\Client\Connection\ConnectionLimitingPool;
use Amp\Promise;
use Symfony\Contracts\HttpClient\HttpClientInterface;

/**
 * A factory to instantiate the best possible HTTP client for the runtime.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
final class HttpClient
{
    /**
     * @param array $defaultOptions     Default request's options
     * @param int   $maxHostConnections The maximum number of connections to a single host
     * @param int   $maxPendingPushes   The maximum number of pushed responses to accept in the queue
     *
     * @see HttpClientInterface::OPTIONS_DEFAULTS for available options
     */
    public static function create(array $defaultOptions = [], int $maxHostConnections = 6, int $maxPendingPushes = 50): HttpClientInterface
    {
        if ($amp = class_exists(ConnectionLimitingPool::class) && interface_exists(Promise::class)) {
            if (!\extension_loaded('curl')) {
                return new AmpHttpClient($defaultOptions, null, $maxHostConnections, $maxPendingPushes);
            }

            // Skip curl when HTTP/2 push is unsupported or buggy, see https://bugs.php.net/77535
            if (\PHP_VERSION_ID < 70217 || (\PHP_VERSION_ID >= 70300 && \PHP_VERSION_ID < 70304) || !\defined('CURLMOPT_PUSHFUNCTION')) {
                return new AmpHttpClient($defaultOptions, null, $maxHostConnections, $maxPendingPushes);
            }

            static $curlVersion = null;
            $curlVersion = $curlVersion ?? curl_version();

            // HTTP/2 push crashes before curl 7.61
            if (0x073D00 > $curlVersion['version_number'] || !(\CURL_VERSION_HTTP2 & $curlVersion['features'])) {
                return new AmpHttpClient($defaultOptions, null, $maxHostConnections, $maxPendingPushes);
            }
        }

        if (\extension_loaded('curl')) {
            if ('\\' !== \DIRECTORY_SEPARATOR || isset($defaultOptions['cafile']) || isset($defaultOptions['capath']) || \ini_get('curl.cainfo') || \ini_get('openssl.cafile') || \ini_get('openssl.capath')) {
                return new CurlHttpClient($defaultOptions, $maxHostConnections, $maxPendingPushes);
            }

            @trigger_error('Configure the "curl.cainfo", "openssl.cafile" or "openssl.capath" php.ini setting to enable the CurlHttpClient', \E_USER_WARNING);
        }

        if ($amp) {
            return new AmpHttpClient($defaultOptions, null, $maxHostConnections, $maxPendingPushes);
        }

        @trigger_error((\extension_loaded('curl') ? 'Upgrade' : 'Install').' the curl extension or run "composer require amphp/http-client:^4.2.1" to perform async HTTP operations, including full HTTP/2 support', \E_USER_NOTICE);

        return new NativeHttpClient($defaultOptions, $maxHostConnections);
    }

    /**
     * Creates a client that adds options (e.g. authentication headers) only when the request URL matches the provided base URI.
     */
    public static function createForBaseUri(string $baseUri, array $defaultOptions = [], int $maxHostConnections = 6, int $maxPendingPushes = 50): HttpClientInterface
    {
        $client = self::create([], $maxHostConnections, $maxPendingPushes);

        return ScopingHttpClient::forBaseUri($client, $baseUri, $defaultOptions);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\HttpClient;

use Amp\CancelledException;
use Amp\Http\Client\DelegateHttpClient;
use Amp\Http\Client\InterceptedHttpClient;
use Amp\Http\Client\PooledHttpClient;
use Amp\Http\Client\Request;
use Amp\Http\Tunnel\Http1TunnelConnector;
use Amp\Promise;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Symfony\Component\HttpClient\Exception\TransportException;
use Symfony\Component\HttpClient\Internal\AmpClientState;
use Symfony\Component\HttpClient\Response\AmpResponse;
use Symfony\Component\HttpClient\Response\ResponseStream;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
use Symfony\Contracts\Service\ResetInterface;

if (!interface_exists(DelegateHttpClient::class)) {
    throw new \LogicException('You cannot use "Symfony\Component\HttpClient\AmpHttpClient" as the "amphp/http-client" package is not installed. Try running "composer require amphp/http-client:^4.2.1".');
}

if (!interface_exists(Promise::class)) {
    throw new \LogicException('You cannot use "Symfony\Component\HttpClient\AmpHttpClient" as the installed "amphp/http-client" is not compatible with this version of "symfony/http-client". Try downgrading "amphp/http-client" to "^4.2.1".');
}

/**
 * A portable implementation of the HttpClientInterface contracts based on Amp's HTTP client.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
final class AmpHttpClient implements HttpClientInterface, LoggerAwareInterface, ResetInterface
{
    use HttpClientTrait;
    use LoggerAwareTrait;

    private $defaultOptions = self::OPTIONS_DEFAULTS;
    private static $emptyDefaults = self::OPTIONS_DEFAULTS;

    /** @var AmpClientState */
    private $multi;

    /**
     * @param array         $defaultOptions     Default requests' options
     * @param callable|null $clientConfigurator A callable that builds a {@see DelegateHttpClient} from a {@see PooledHttpClient};
     *                                          passing null builds an {@see InterceptedHttpClient} with 2 retries on failures
     * @param int           $maxHostConnections The maximum number of connections to a single host
     * @param int           $maxPendingPushes   The maximum number of pushed responses to accept in the queue
     *
     * @see HttpClientInterface::OPTIONS_DEFAULTS for available options
     */
    public function __construct(array $defaultOptions = [], ?callable $clientConfigurator = null, int $maxHostConnections = 6, int $maxPendingPushes = 50)
    {
        $this->defaultOptions['buffer'] = $this->defaultOptions['buffer'] ?? \Closure::fromCallable([__CLASS__, 'shouldBuffer']);

        if ($defaultOptions) {
            [, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, $this->defaultOptions);
        }

        $this->multi = new AmpClientState($clientConfigurator, $maxHostConnections, $maxPendingPushes, $this->logger);
    }

    /**
     * @see HttpClientInterface::OPTIONS_DEFAULTS for available options
     *
     * {@inheritdoc}
     */
    public function request(string $method, string $url, array $options = []): ResponseInterface
    {
        [$url, $options] = self::prepareRequest($method, $url, $options, $this->defaultOptions);

        $options['proxy'] = self::getProxy($options['proxy'], $url, $options['no_proxy']);

        if (null !== $options['proxy'] && !class_exists(Http1TunnelConnector::class)) {
            throw new \LogicException('You cannot use the "proxy" option as the "amphp/http-tunnel" package is not installed. Try running "composer require amphp/http-tunnel".');
        }

        if ($options['bindto']) {
            if (0 === strpos($options['bindto'], 'if!')) {
                throw new TransportException(__CLASS__.' cannot bind to network interfaces, use e.g. CurlHttpClient instead.');
            }
            if (0 === strpos($options['bindto'], 'host!')) {
                $options['bindto'] = substr($options['bindto'], 5);
            }
        }

        if (('' !== $options['body'] || 'POST' === $method || isset($options['normalized_headers']['content-length'])) && !isset($options['normalized_headers']['content-type'])) {
            $options['headers'][] = 'Content-Type: application/x-www-form-urlencoded';
        }

        if (!isset($options['normalized_headers']['user-agent'])) {
            $options['headers'][] = 'User-Agent: Symfony HttpClient/Amp';
        }

        if (0 < $options['max_duration']) {
            $options['timeout'] = min($options['max_duration'], $options['timeout']);
        }

        if ($options['resolve']) {
            $this->multi->dnsCache = $options['resolve'] + $this->multi->dnsCache;
        }

        if ($options['peer_fingerprint'] && !isset($options['peer_fingerprint']['pin-sha256'])) {
            throw new TransportException(__CLASS__.' supports only "pin-sha256" fingerprints.');
        }

        $request = new Request(implode('', $url), $method);
        $request->setBodySizeLimit(0);

        if ($options['http_version']) {
            switch ((float) $options['http_version']) {
                case 1.0: $request->setProtocolVersions(['1.0']); break;
                case 1.1: $request->setProtocolVersions(['1.1', '1.0']); break;
                default: $request->setProtocolVersions(['2', '1.1', '1.0']); break;
            }
        }

        foreach ($options['headers'] as $v) {
            $h = explode(': ', $v, 2);
            $request->addHeader($h[0], $h[1]);
        }

        $request->setTcpConnectTimeout(1000 * $options['timeout']);
        $request->setTlsHandshakeTimeout(1000 * $options['timeout']);
        $request->setTransferTimeout(1000 * $options['max_duration']);
        if (method_exists($request, 'setInactivityTimeout')) {
            $request->setInactivityTimeout(0);
        }

        if ('' !== $request->getUri()->getUserInfo() && !$request->hasHeader('authorization')) {
            $auth = explode(':', $request->getUri()->getUserInfo(), 2);
            $auth = array_map('rawurldecode', $auth) + [1 => ''];
            $request->setHeader('Authorization', 'Basic '.base64_encode(implode(':', $auth)));
        }

        return new AmpResponse($this->multi, $request, $options, $this->logger);
    }

    /**
     * {@inheritdoc}
     */
    public function stream($responses, ?float $timeout = null): ResponseStreamInterface
    {
        if ($responses instanceof AmpResponse) {
            $responses = [$responses];
        } elseif (!is_iterable($responses)) {
            throw new \TypeError(sprintf('"%s()" expects parameter 1 to be an iterable of AmpResponse objects, "%s" given.', __METHOD__, get_debug_type($responses)));
        }

        return new ResponseStream(AmpResponse::stream($responses, $timeout));
    }

    public function reset()
    {
        $this->multi->dnsCache = [];

        foreach ($this->multi->pushedResponses as $authority => $pushedResponses) {
            foreach ($pushedResponses as [$pushedUrl, $pushDeferred]) {
                $pushDeferred->fail(new CancelledException());

                if ($this->logger) {
                    $this->logger->debug(sprintf('Unused pushed response: "%s"', $pushedUrl));
                }
            }
        }

        $this->multi->pushedResponses = [];
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\HttpClient;

use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Symfony\Component\HttpClient\Exception\InvalidArgumentException;
use Symfony\Component\HttpClient\Exception\TransportException;
use Symfony\Component\HttpClient\Internal\NativeClientState;
use Symfony\Component\HttpClient\Response\NativeResponse;
use Symfony\Component\HttpClient\Response\ResponseStream;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
use Symfony\Contracts\Service\ResetInterface;

/**
 * A portable implementation of the HttpClientInterface contracts based on PHP stream wrappers.
 *
 * PHP stream wrappers are able to fetch response bodies concurrently,
 * but each request is opened synchronously.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
final class NativeHttpClient implements HttpClientInterface, LoggerAwareInterface, ResetInterface
{
    use HttpClientTrait;
    use LoggerAwareTrait;

    private $defaultOptions = self::OPTIONS_DEFAULTS;
    private static $emptyDefaults = self::OPTIONS_DEFAULTS;

    /** @var NativeClientState */
    private $multi;

    /**
     * @param array $defaultOptions     Default request's options
     * @param int   $maxHostConnections The maximum number of connections to open
     *
     * @see HttpClientInterface::OPTIONS_DEFAULTS for available options
     */
    public function __construct(array $defaultOptions = [], int $maxHostConnections = 6)
    {
        $this->defaultOptions['buffer'] = $this->defaultOptions['buffer'] ?? \Closure::fromCallable([__CLASS__, 'shouldBuffer']);

        if ($defaultOptions) {
            [, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, $this->defaultOptions);
        }

        $this->multi = new NativeClientState();
        $this->multi->maxHostConnections = 0 < $maxHostConnections ? $maxHostConnections : \PHP_INT_MAX;
    }

    /**
     * @see HttpClientInterface::OPTIONS_DEFAULTS for available options
     *
     * {@inheritdoc}
     */
    public function request(string $method, string $url, array $options = []): ResponseInterface
    {
        [$url, $options] = self::prepareRequest($method, $url, $options, $this->defaultOptions);

        if ($options['bindto']) {
            if (file_exists($options['bindto'])) {
                throw new TransportException(__CLASS__.' cannot bind to local Unix sockets, use e.g. CurlHttpClient instead.');
            }
            if (str_starts_with($options['bindto'], 'if!')) {
                throw new TransportException(__CLASS__.' cannot bind to network interfaces, use e.g. CurlHttpClient instead.');
            }
            if (str_starts_with($options['bindto'], 'host!')) {
                $options['bindto'] = substr($options['bindto'], 5);
            }
            if ((\PHP_VERSION_ID < 80223 || 80300 <= \PHP_VERSION_ID && 80311 < \PHP_VERSION_ID) && '\\' === \DIRECTORY_SEPARATOR && '[' === $options['bindto'][0]) {
                $options['bindto'] = preg_replace('{^\[[^\]]++\]}', '[$0]', $options['bindto']);
            }
        }

        $hasContentLength = isset($options['normalized_headers']['content-length']);
        $hasBody = '' !== $options['body'] || 'POST' === $method || $hasContentLength;

        $options['body'] = self::getBodyAsString($options['body']);

        if ('chunked' === substr($options['normalized_headers']['transfer-encoding'][0] ?? '', \strlen('Transfer-Encoding: '))) {
            unset($options['normalized_headers']['transfer-encoding']);
            $options['headers'] = array_merge(...array_values($options['normalized_headers']));
            $options['body'] = self::dechunk($options['body']);
        }
        if ('' === $options['body'] && $hasBody && !$hasContentLength) {
            $options['headers'][] = 'Content-Length: 0';
        }
        if ($hasBody && !isset($options['normalized_headers']['content-type'])) {
            $options['headers'][] = 'Content-Type: application/x-www-form-urlencoded';
        }

        if (\extension_loaded('zlib') && !isset($options['normalized_headers']['accept-encoding'])) {
            // gzip is the most widely available algo, no need to deal with deflate
            $options['headers'][] = 'Accept-Encoding: gzip';
        }

        if ($options['peer_fingerprint']) {
            if (isset($options['peer_fingerprint']['pin-sha256']) && 1 === \count($options['peer_fingerprint'])) {
                throw new TransportException(__CLASS__.' cannot verify "pin-sha256" fingerprints, please provide a "sha256" one.');
            }

            unset($options['peer_fingerprint']['pin-sha256']);
        }

        $info = [
            'response_headers' => [],
            'url' => $url,
            'error' => null,
            'canceled' => false,
            'http_method' => $method,
            'http_code' => 0,
            'redirect_count' => 0,
            'start_time' => 0.0,
            'connect_time' => 0.0,
            'redirect_time' => 0.0,
            'pretransfer_time' => 0.0,
            'starttransfer_time' => 0.0,
            'total_time' => 0.0,
            'namelookup_time' => 0.0,
            'size_upload' => 0,
            'size_download' => 0,
            'size_body' => \strlen($options['body']),
            'primary_ip' => '',
            'primary_port' => 'http:' === $url['scheme'] ? 80 : 443,
            'debug' => \extension_loaded('curl') ? '' : "* Enable the curl extension for better performance\n",
        ];

        if ($onProgress = $options['on_progress']) {
            // Memoize the last progress to ease calling the callback periodically when no network transfer happens
            $lastProgress = [0, 0];
            $maxDuration = 0 < $options['max_duration'] ? $options['max_duration'] : \INF;
            $onProgress = static function (...$progress) use ($onProgress, &$lastProgress, &$info, $maxDuration) {
                if ($info['total_time'] >= $maxDuration) {
                    throw new TransportException(sprintf('Max duration was reached for "%s".', implode('', $info['url'])));
                }

                $progressInfo = $info;
                $progressInfo['url'] = implode('', $info['url']);
                unset($progressInfo['size_body']);

                if ($progress && -1 === $progress[0]) {
                    // Response completed
                    $lastProgress[0] = max($lastProgress);
                } else {
                    $lastProgress = $progress ?: $lastProgress;
                }

                $onProgress($lastProgress[0], $lastProgress[1], $progressInfo);
            };
        } elseif (0 < $options['max_duration']) {
            $maxDuration = $options['max_duration'];
            $onProgress = static function () use (&$info, $maxDuration): void {
                if ($info['total_time'] >= $maxDuration) {
                    throw new TransportException(sprintf('Max duration was reached for "%s".', implode('', $info['url'])));
                }
            };
        }

        // Always register a notification callback to compute live stats about the response
        $notification = static function (int $code, int $severity, ?string $msg, int $msgCode, int $dlNow, int $dlSize) use ($onProgress, &$info) {
            $info['total_time'] = microtime(true) - $info['start_time'];

            if (\STREAM_NOTIFY_PROGRESS === $code) {
                $info['starttransfer_time'] = $info['starttransfer_time'] ?: $info['total_time'];
                $info['size_upload'] += $dlNow ? 0 : $info['size_body'];
                $info['size_download'] = $dlNow;
            } elseif (\STREAM_NOTIFY_CONNECT === $code) {
                $info['connect_time'] = $info['total_time'];
                $info['debug'] .= $info['request_header'];
                unset($info['request_header']);
            } else {
                return;
            }

            if ($onProgress) {
                $onProgress($dlNow, $dlSize);
            }
        };

        if ($options['resolve']) {
            $this->multi->dnsCache = $options['resolve'] + $this->multi->dnsCache;
        }

        $this->logger && $this->logger->info(sprintf('Request: "%s %s"', $method, implode('', $url)));

        if (!isset($options['normalized_headers']['user-agent'])) {
            $options['headers'][] = 'User-Agent: Symfony HttpClient/Native';
        }

        if (0 < $options['max_duration']) {
            $options['timeout'] = min($options['max_duration'], $options['timeout']);
        }

        $bindto = $options['bindto'];
        if (!$bindto && (70322 === \PHP_VERSION_ID || 70410 === \PHP_VERSION_ID)) {
            $bindto = '0:0';
        }

        $context = [
            'http' => [
                'protocol_version' => min($options['http_version'] ?: '1.1', '1.1'),
                'method' => $method,
                'content' => $options['body'],
                'ignore_errors' => true,
                'curl_verify_ssl_peer' => $options['verify_peer'],
                'curl_verify_ssl_host' => $options['verify_host'],
                'auto_decode' => false, // Disable dechunk filter, it's incompatible with stream_select()
                'timeout' => $options['timeout'],
                'follow_location' => false, // We follow redirects ourselves - the native logic is too limited
            ],
            'ssl' => array_filter([
                'verify_peer' => $options['verify_peer'],
                'verify_peer_name' => $options['verify_host'],
                'cafile' => $options['cafile'],
                'capath' => $options['capath'],
                'local_cert' => $options['local_cert'],
                'local_pk' => $options['local_pk'],
                'passphrase' => $options['passphrase'],
                'ciphers' => $options['ciphers'],
                'peer_fingerprint' => $options['peer_fingerprint'],
                'capture_peer_cert_chain' => $options['capture_peer_cert_chain'],
                'allow_self_signed' => (bool) $options['peer_fingerprint'],
                'SNI_enabled' => true,
                'disable_compression' => true,
            ], static function ($v) { return null !== $v; }),
            'socket' => [
                'bindto' => $bindto,
                'tcp_nodelay' => true,
            ],
        ];

        $context = stream_context_create($context, ['notification' => $notification]);

        $resolver = static function ($multi) use ($context, $options, $url, &$info, $onProgress) {
            [$host, $port] = self::parseHostPort($url, $info);

            if (!isset($options['normalized_headers']['host'])) {
                $options['headers'][] = 'Host: '.$host.$port;
            }

            $proxy = self::getProxy($options['proxy'], $url, $options['no_proxy']);

            if (!self::configureHeadersAndProxy($context, $host, $options['headers'], $proxy, 'https:' === $url['scheme'])) {
                $ip = self::dnsResolve($host, $multi, $info, $onProgress);
                $url['authority'] = substr_replace($url['authority'], $ip, -\strlen($host) - \strlen($port), \strlen($host));
            }

            return [self::createRedirectResolver($options, $host, $proxy, $info, $onProgress), implode('', $url)];
        };

        return new NativeResponse($this->multi, $context, implode('', $url), $options, $info, $resolver, $onProgress, $this->logger);
    }

    /**
     * {@inheritdoc}
     */
    public function stream($responses, ?float $timeout = null): ResponseStreamInterface
    {
        if ($responses instanceof NativeResponse) {
            $responses = [$responses];
        } elseif (!is_iterable($responses)) {
            throw new \TypeError(sprintf('"%s()" expects parameter 1 to be an iterable of NativeResponse objects, "%s" given.', __METHOD__, get_debug_type($responses)));
        }

        return new ResponseStream(NativeResponse::stream($responses, $timeout));
    }

    public function reset()
    {
        $this->multi->reset();
    }

    private static function getBodyAsString($body): string
    {
        if (\is_resource($body)) {
            return stream_get_contents($body);
        }

        if (!$body instanceof \Closure) {
            return $body;
        }

        $result = '';

        while ('' !== $data = $body(self::$CHUNK_SIZE)) {
            if (!\is_string($data)) {
                throw new TransportException(sprintf('Return value of the "body" option callback must be string, "%s" returned.', get_debug_type($data)));
            }

            $result .= $data;
        }

        return $result;
    }

    /**
     * Extracts the host and the port from the URL.
     */
    private static function parseHostPort(array $url, array &$info): array
    {
        if ($port = parse_url($url['authority'], \PHP_URL_PORT) ?: '') {
            $info['primary_port'] = $port;
            $port = ':'.$port;
        } else {
            $info['primary_port'] = 'http:' === $url['scheme'] ? 80 : 443;
        }

        return [parse_url($url['authority'], \PHP_URL_HOST), $port];
    }

    /**
     * Resolves the IP of the host using the local DNS cache if possible.
     */
    private static function dnsResolve($host, NativeClientState $multi, array &$info, ?\Closure $onProgress): string
    {
        $flag = '' !== $host && '[' === $host[0] && ']' === $host[-1] && str_contains($host, ':') ? \FILTER_FLAG_IPV6 : \FILTER_FLAG_IPV4;
        $ip = \FILTER_FLAG_IPV6 === $flag ? substr($host, 1, -1) : $host;

        if (filter_var($ip, \FILTER_VALIDATE_IP, $flag)) {
            // The host is already an IP address
        } elseif (null === $ip = $multi->dnsCache[$host] ?? null) {
            $info['debug'] .= "* Hostname was NOT found in DNS cache\n";
            $now = microtime(true);

            if (!$ip = gethostbynamel($host)) {
                throw new TransportException(sprintf('Could not resolve host "%s".', $host));
            }

            $multi->dnsCache[$host] = $ip = $ip[0];
            $info['debug'] .= "* Added {$host}:0:{$ip} to DNS cache\n";
            $host = $ip;
        } else {
            $info['debug'] .= "* Hostname was found in DNS cache\n";
            $host = str_contains($ip, ':') ? "[$ip]" : $ip;
        }

        $info['namelookup_time'] = microtime(true) - ($info['start_time'] ?: $now);
        $info['primary_ip'] = $ip;

        if ($onProgress) {
            // Notify DNS resolution
            $onProgress();
        }

        return $host;
    }

    /**
     * Handles redirects - the native logic is too buggy to be used.
     */
    private static function createRedirectResolver(array $options, string $host, ?array $proxy, array &$info, ?\Closure $onProgress): \Closure
    {
        $redirectHeaders = [];
        if (0 < $maxRedirects = $options['max_redirects']) {
            $redirectHeaders = ['host' => $host];
            $redirectHeaders['with_auth'] = $redirectHeaders['no_auth'] = array_filter($options['headers'], static function ($h) {
                return 0 !== stripos($h, 'Host:');
            });

            if (isset($options['normalized_headers']['authorization']) || isset($options['normalized_headers']['cookie'])) {
                $redirectHeaders['no_auth'] = array_filter($redirectHeaders['no_auth'], static function ($h) {
                    return 0 !== stripos($h, 'Authorization:') && 0 !== stripos($h, 'Cookie:');
                });
            }
        }

        return static function (NativeClientState $multi, ?string $location, $context) use (&$redirectHeaders, $proxy, &$info, $maxRedirects, $onProgress): ?string {
            if (null === $location || $info['http_code'] < 300 || 400 <= $info['http_code']) {
                $info['redirect_url'] = null;

                return null;
            }

            try {
                $url = self::parseUrl($location);
                $locationHasHost = isset($url['authority']);
                $url = self::resolveUrl($url, $info['url']);
            } catch (InvalidArgumentException $e) {
                $info['redirect_url'] = null;

                return null;
            }

            $info['redirect_url'] = implode('', $url);

            if ($info['redirect_count'] >= $maxRedirects) {
                return null;
            }

            $info['url'] = $url;
            ++$info['redirect_count'];
            $info['redirect_time'] = microtime(true) - $info['start_time'];

            // Do like curl and browsers: turn POST to GET on 301, 302 and 303
            if (\in_array($info['http_code'], [301, 302, 303], true)) {
                $options = stream_context_get_options($context)['http'];

                if ('POST' === $options['method'] || 303 === $info['http_code']) {
                    $info['http_method'] = $options['method'] = 'HEAD' === $options['method'] ? 'HEAD' : 'GET';
                    $options['content'] = '';
                    $filterContentHeaders = static function ($h) {
                        return 0 !== stripos($h, 'Content-Length:') && 0 !== stripos($h, 'Content-Type:') && 0 !== stripos($h, 'Transfer-Encoding:');
                    };
                    $options['header'] = array_filter($options['header'], $filterContentHeaders);
                    $redirectHeaders['no_auth'] = array_filter($redirectHeaders['no_auth'], $filterContentHeaders);
                    $redirectHeaders['with_auth'] = array_filter($redirectHeaders['with_auth'], $filterContentHeaders);

                    if (\PHP_VERSION_ID >= 80300) {
                        stream_context_set_options($context, ['http' => $options]);
                    } else {
                        stream_context_set_option($context, ['http' => $options]);
                    }
                }
            }

            [$host, $port] = self::parseHostPort($url, $info);

            if ($locationHasHost) {
                // Authorization and Cookie headers MUST NOT follow except for the initial host name
                $requestHeaders = $redirectHeaders['host'] === $host ? $redirectHeaders['with_auth'] : $redirectHeaders['no_auth'];
                $requestHeaders[] = 'Host: '.$host.$port;
                $dnsResolve = !self::configureHeadersAndProxy($context, $host, $requestHeaders, $proxy, 'https:' === $url['scheme']);
            } else {
                $dnsResolve = isset(stream_context_get_options($context)['ssl']['peer_name']);
            }

            if ($dnsResolve) {
                $ip = self::dnsResolve($host, $multi, $info, $onProgress);
                $url['authority'] = substr_replace($url['authority'], $ip, -\strlen($host) - \strlen($port), \strlen($host));
            }

            return implode('', $url);
        };
    }

    private static function configureHeadersAndProxy($context, string $host, array $requestHeaders, ?array $proxy, bool $isSsl): bool
    {
        if (null === $proxy) {
            stream_context_set_option($context, 'http', 'header', $requestHeaders);
            stream_context_set_option($context, 'ssl', 'peer_name', $host);

            return false;
        }

        // Matching "no_proxy" should follow the behavior of curl

        foreach ($proxy['no_proxy'] as $rule) {
            $dotRule = '.'.ltrim($rule, '.');

            if ('*' === $rule || $host === $rule || str_ends_with($host, $dotRule)) {
                stream_context_set_option($context, 'http', 'proxy', null);
                stream_context_set_option($context, 'http', 'request_fulluri', false);
                stream_context_set_option($context, 'http', 'header', $requestHeaders);
                stream_context_set_option($context, 'ssl', 'peer_name', $host);

                return false;
            }
        }

        if (null !== $proxy['auth']) {
            $requestHeaders[] = 'Proxy-Authorization: '.$proxy['auth'];
        }

        stream_context_set_option($context, 'http', 'proxy', $proxy['url']);
        stream_context_set_option($context, 'http', 'request_fulluri', !$isSsl);
        stream_context_set_option($context, 'http', 'header', $requestHeaders);
        stream_context_set_option($context, 'ssl', 'peer_name', null);

        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\HttpClient\Exception;

use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 */
final class InvalidArgumentException extends \InvalidArgumentException implements TransportExceptionInterface
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\HttpClient\Exception;

use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;

/**
 * Thrown by responses' toArray() method when their content cannot be JSON-decoded.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
final class JsonException extends \JsonException implements DecodingExceptionInterface
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\HttpClient\Exception;

use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 */
final class EventSourceException extends \RuntimeException implements DecodingExceptionInterface
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\HttpClient\Exception;

use Symfony\Contracts\HttpClient\Exception\TimeoutExceptionInterface;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 */
final class TimeoutException extends TransportException implements TimeoutExceptionInterface
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\HttpClient\Exception;

use Symfony\Contracts\HttpClient\ResponseInterface;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @internal
 */
trait HttpExceptionTrait
{
    private $response;

    public function __construct(ResponseInterface $response)
    {
        $this->response = $response;
        $code = $response->getInfo('http_code');
        $url = $response->getInfo('url');
        $message = sprintf('HTTP %d returned for "%s".', $code, $url);

        $httpCodeFound = false;
        $isJson = false;
        foreach (array_reverse($response->getInfo('response_headers')) as $h) {
            if (str_starts_with($h, 'HTTP/')) {
                if ($httpCodeFound) {
                    break;
                }

                $message = sprintf('%s returned for "%s".', $h, $url);
                $httpCodeFound = true;
            }

            if (0 === stripos($h, 'content-type:')) {
                if (preg_match('/\bjson\b/i', $h)) {
                    $isJson = true;
                }

                if ($httpCodeFound) {
                    break;
                }
            }
        }

        // Try to guess a better error message using common API error formats
        // The MIME type isn't explicitly checked because some formats inherit from others
        // Ex: JSON:API follows RFC 7807 semantics, Hydra can be used in any JSON-LD-compatible format
        if ($isJson && $body = json_decode($response->getContent(false), true)) {
            if (isset($body['hydra:title']) || isset($body['hydra:description'])) {
                // see http://www.hydra-cg.com/spec/latest/core/#description-of-http-status-codes-and-errors
                $separator = isset($body['hydra:title'], $body['hydra:description']) ? "\n\n" : '';
                $message = ($body['hydra:title'] ?? '').$separator.($body['hydra:description'] ?? '');
            } elseif ((isset($body['title']) || isset($body['detail']))
                && (\is_scalar($body['title'] ?? '') && \is_scalar($body['detail'] ?? ''))) {
                // see RFC 7807 and https://jsonapi.org/format/#error-objects
                $separator = isset($body['title'], $body['detail']) ? "\n\n" : '';
                $message = ($body['title'] ?? '').$separator.($body['detail'] ?? '');
            }
        }

        parent::__construct($message, $code);
    }

    public function getResponse(): ResponseInterface
    {
        return $this->response;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\HttpClient\Exception;

use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;

/**
 * Represents a 3xx response.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
final class RedirectionException extends \RuntimeException implements RedirectionExceptionInterface
{
    use HttpExceptionTrait;
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\HttpClient\Exception;

use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;

/**
 * Represents a 5xx response.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
final class ServerException extends \RuntimeException implements ServerExceptionInterface
{
    use HttpExceptionTrait;
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\HttpClient\Exception;

use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 */
class TransportException extends \RuntimeException implements TransportExceptionInterface
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\HttpClient\Exception;

use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;

/**
 * Represents a 4xx response.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
final class ClientException extends \RuntimeException implements ClientExceptionInterface
{
    use HttpExceptionTrait;
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\HttpClient;

use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
use Symfony\Contracts\Service\ResetInterface;

/**
 * Eases with writing decorators.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
trait DecoratorTrait
{
    private $client;

    public function __construct(?HttpClientInterface $client = null)
    {
        $this->client = $client ?? HttpClient::create();
    }

    /**
     * {@inheritdoc}
     */
    public function request(string $method, string $url, array $options = []): ResponseInterface
    {
        return $this->client->request($method, $url, $options);
    }

    /**
     * {@inheritdoc}
     */
    public function stream($responses, ?float $timeout = null): ResponseStreamInterface
    {
        return $this->client->stream($responses, $timeout);
    }

    /**
     * {@inheritdoc}
     */
    public function withOptions(array $options): self
    {
        $clone = clone $this;
        $clone->client = $this->client->withOptions($options);

        return $clone;
    }

    public function reset()
    {
        if ($this->client instanceof ResetInterface) {
            $this->client->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\Component\HttpClient;

use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Symfony\Component\HttpClient\Response\AsyncContext;
use Symfony\Component\HttpClient\Response\AsyncResponse;
use Symfony\Component\HttpClient\Retry\GenericRetryStrategy;
use Symfony\Component\HttpClient\Retry\RetryStrategyInterface;
use Symfony\Contracts\HttpClient\ChunkInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\Service\ResetInterface;

/**
 * Automatically retries failing HTTP requests.
 *
 * @author Jérémy Derussé <jeremy@derusse.com>
 */
class RetryableHttpClient implements HttpClientInterface, ResetInterface
{
    use AsyncDecoratorTrait;

    private $strategy;
    private $maxRetries;
    private $logger;

    /**
     * @param int $maxRetries The maximum number of times to retry
     */
    public function __construct(HttpClientInterface $client, ?RetryStrategyInterface $strategy = null, int $maxRetries = 3, ?LoggerInterface $logger = null)
    {
        $this->client = $client;
        $this->strategy = $strategy ?? new GenericRetryStrategy();
        $this->maxRetries = $maxRetries;
        $this->logger = $logger ?? new NullLogger();
    }

    public function request(string $method, string $url, array $options = []): ResponseInterface
    {
        if ($this->maxRetries <= 0) {
            return new AsyncResponse($this->client, $method, $url, $options);
        }

        $retryCount = 0;
        $content = '';
        $firstChunk = null;

        return new AsyncResponse($this->client, $method, $url, $options, function (ChunkInterface $chunk, AsyncContext $context) use ($method, $url, $options, &$retryCount, &$content, &$firstChunk) {
            $exception = null;
            try {
                if ($context->getInfo('canceled') || $chunk->isTimeout() || null !== $chunk->getInformationalStatus()) {
                    yield $chunk;

                    return;
                }
            } catch (TransportExceptionInterface $exception) {
                // catch TransportExceptionInterface to send it to the strategy
            }
            if (null !== $exception) {
                // always retry request that fail to resolve DNS
                if ('' !== $context->getInfo('primary_ip')) {
                    $shouldRetry = $this->strategy->shouldRetry($context, null, $exception);
                    if (null === $shouldRetry) {
                        throw new \LogicException(sprintf('The "%s::shouldRetry()" method must not return null when called with an exception.', \get_class($this->strategy)));
                    }

                    if (false === $shouldRetry) {
                        yield from $this->passthru($context, $firstChunk, $content, $chunk);

                        return;
                    }
                }
            } elseif ($chunk->isFirst()) {
                if (false === $shouldRetry = $this->strategy->shouldRetry($context, null, null)) {
                    yield from $this->passthru($context, $firstChunk, $content, $chunk);

                    return;
                }

                // Body is needed to decide
                if (null === $shouldRetry) {
                    $firstChunk = $chunk;
                    $content = '';

                    return;
                }
            } else {
                if (!$chunk->isLast()) {
                    $content .= $chunk->getContent();

                    return;
                }

                if (null === $shouldRetry = $this->strategy->shouldRetry($context, $content, null)) {
                    throw new \LogicException(sprintf('The "%s::shouldRetry()" method must not return null when called with a body.', \get_class($this->strategy)));
                }

                if (false === $shouldRetry) {
                    yield from $this->passthru($context, $firstChunk, $content, $chunk);

                    return;
                }
            }

            $context->getResponse()->cancel();

            $delay = $this->getDelayFromHeader($context->getHeaders()) ?? $this->strategy->getDelay($context, !$exception && $chunk->isLast() ? $content : null, $exception);
            ++$retryCount;
            $content = '';
            $firstChunk = null;

            $this->logger->info('Try #{count} after {delay}ms'.($exception ? ': '.$exception->getMessage() : ', status code: '.$context->getStatusCode()), [
                'count' => $retryCount,
                'delay' => $delay,
            ]);

            $context->setInfo('retry_count', $retryCount);
            $context->replaceRequest($method, $url, $options);
            $context->pause($delay / 1000);

            if ($retryCount >= $this->maxRetries) {
                $context->passthru();
            }
        });
    }

    private function getDelayFromHeader(array $headers): ?int
    {
        if (null !== $after = $headers['retry-after'][0] ?? null) {
            if (is_numeric($after)) {
                return (int) ($after * 1000);
            }

            if (false !== $time = strtotime($after)) {
                return max(0, $time - time()) * 1000;
            }
        }

        return null;
    }

    private function passthru(AsyncContext $context, ?ChunkInterface $firstChunk, string &$content, ChunkInterface $lastChunk): \Generator
    {
        $context->passthru();

        if (null !== $firstChunk) {
            yield $firstChunk;
        }

        if ('' !== $content) {
            $chunk = $context->createChunk($content);
            $content = '';

            yield $chunk;
        }

        yield $lastChunk;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\HttpClient;

use Symfony\Component\HttpClient\Chunk\DataChunk;
use Symfony\Component\HttpClient\Chunk\ServerSentEvent;
use Symfony\Component\HttpClient\Exception\EventSourceException;
use Symfony\Component\HttpClient\Response\AsyncContext;
use Symfony\Component\HttpClient\Response\AsyncResponse;
use Symfony\Contracts\HttpClient\ChunkInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\Service\ResetInterface;

/**
 * @author Antoine Bluchet <soyuka@gmail.com>
 * @author Nicolas Grekas <p@tchwork.com>
 */
final class EventSourceHttpClient implements HttpClientInterface, ResetInterface
{
    use AsyncDecoratorTrait, HttpClientTrait {
        AsyncDecoratorTrait::withOptions insteadof HttpClientTrait;
    }

    private $reconnectionTime;

    public function __construct(?HttpClientInterface $client = null, float $reconnectionTime = 10.0)
    {
        $this->client = $client ?? HttpClient::create();
        $this->reconnectionTime = $reconnectionTime;
    }

    public function connect(string $url, array $options = []): ResponseInterface
    {
        return $this->request('GET', $url, self::mergeDefaultOptions($options, [
            'buffer' => false,
            'headers' => [
                'Accept' => 'text/event-stream',
                'Cache-Control' => 'no-cache',
            ],
        ], true));
    }

    public function request(string $method, string $url, array $options = []): ResponseInterface
    {
        $state = new class() {
            public $buffer = null;
            public $lastEventId = null;
            public $reconnectionTime;
            public $lastError = null;
        };
        $state->reconnectionTime = $this->reconnectionTime;

        if ($accept = self::normalizeHeaders($options['headers'] ?? [])['accept'] ?? []) {
            $state->buffer = \in_array($accept, [['Accept: text/event-stream'], ['accept: text/event-stream']], true) ? '' : null;

            if (null !== $state->buffer) {
                $options['extra']['trace_content'] = false;
            }
        }

        return new AsyncResponse($this->client, $method, $url, $options, static function (ChunkInterface $chunk, AsyncContext $context) use ($state, $method, $url, $options) {
            if (null !== $state->buffer) {
                $context->setInfo('reconnection_time', $state->reconnectionTime);
                $isTimeout = false;
            }
            $lastError = $state->lastError;
            $state->lastError = null;

            try {
                $isTimeout = $chunk->isTimeout();

                if (null !== $chunk->getInformationalStatus() || $context->getInfo('canceled')) {
                    yield $chunk;

                    return;
                }
            } catch (TransportExceptionInterface $e) {
                $state->lastError = $lastError ?? microtime(true);

                if (null === $state->buffer || ($isTimeout && microtime(true) - $state->lastError < $state->reconnectionTime)) {
                    yield $chunk;
                } else {
                    $options['headers']['Last-Event-ID'] = $state->lastEventId;
                    $state->buffer = '';
                    $state->lastError = microtime(true);
                    $context->getResponse()->cancel();
                    $context->replaceRequest($method, $url, $options);
                    if ($isTimeout) {
                        yield $chunk;
                    } else {
                        $context->pause($state->reconnectionTime);
                    }
                }

                return;
            }

            if ($chunk->isFirst()) {
                if (preg_match('/^text\/event-stream(;|$)/i', $context->getHeaders()['content-type'][0] ?? '')) {
                    $state->buffer = '';
                } elseif (null !== $lastError || (null !== $state->buffer && 200 === $context->getStatusCode())) {
                    throw new EventSourceException(sprintf('Response content-type is "%s" while "text/event-stream" was expected for "%s".', $context->getHeaders()['content-type'][0] ?? '', $context->getInfo('url')));
                } else {
                    $context->passthru();
                }

                if (null === $lastError) {
                    yield $chunk;
                }

                return;
            }

            if ($chunk->isLast()) {
                if ('' !== $content = $state->buffer) {
                    $state->buffer = '';
                    yield new DataChunk(-1, $content);
                }

                yield $chunk;

                return;
            }

            $content = $state->buffer.$chunk->getContent();
            $events = preg_split('/((?:\r\n){2,}|\r{2,}|\n{2,})/', $content, -1, \PREG_SPLIT_DELIM_CAPTURE);
            $state->buffer = array_pop($events);

            for ($i = 0; isset($events[$i]); $i += 2) {
                $content = $events[$i].$events[1 + $i];
                if (!preg_match('/(?:^|\r\n|[\r\n])[^:\r\n]/', $content)) {
                    yield new DataChunk(-1, $content);

                    continue;
                }

                $event = new ServerSentEvent($content);

                if ('' !== $event->getId()) {
                    $context->setInfo('last_event_id', $state->lastEventId = $event->getId());
                }

                if ($event->getRetry()) {
                    $context->setInfo('reconnection_time', $state->reconnectionTime = $event->getRetry());
                }

                yield $event;
            }
        });
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\HttpClient;

use Symfony\Component\HttpClient\Exception\InvalidArgumentException;
use Symfony\Component\HttpClient\Exception\TransportException;

/**
 * Provides the common logic from writing HttpClientInterface implementations.
 *
 * All private methods are static to prevent implementers from creating memory leaks via circular references.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
trait HttpClientTrait
{
    private static $CHUNK_SIZE = 16372;

    /**
     * {@inheritdoc}
     */
    public function withOptions(array $options): self
    {
        $clone = clone $this;
        $clone->defaultOptions = self::mergeDefaultOptions($options, $this->defaultOptions);

        return $clone;
    }

    /**
     * Validates and normalizes method, URL and options, and merges them with defaults.
     *
     * @throws InvalidArgumentException When a not-supported option is found
     */
    private static function prepareRequest(?string $method, ?string $url, array $options, array $defaultOptions = [], bool $allowExtraOptions = false): array
    {
        if (null !== $method) {
            if (\strlen($method) !== strspn($method, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')) {
                throw new InvalidArgumentException(sprintf('Invalid HTTP method "%s", only uppercase letters are accepted.', $method));
            }
            if (!$method) {
                throw new InvalidArgumentException('The HTTP method cannot be empty.');
            }
        }

        $options = self::mergeDefaultOptions($options, $defaultOptions, $allowExtraOptions);

        $buffer = $options['buffer'] ?? true;

        if ($buffer instanceof \Closure) {
            $options['buffer'] = static function (array $headers) use ($buffer) {
                if (!\is_bool($buffer = $buffer($headers))) {
                    if (!\is_array($bufferInfo = @stream_get_meta_data($buffer))) {
                        throw new \LogicException(sprintf('The closure passed as option "buffer" must return bool or stream resource, got "%s".', get_debug_type($buffer)));
                    }

                    if (false === strpbrk($bufferInfo['mode'], 'acew+')) {
                        throw new \LogicException(sprintf('The stream returned by the closure passed as option "buffer" must be writeable, got mode "%s".', $bufferInfo['mode']));
                    }
                }

                return $buffer;
            };
        } elseif (!\is_bool($buffer)) {
            if (!\is_array($bufferInfo = @stream_get_meta_data($buffer))) {
                throw new InvalidArgumentException(sprintf('Option "buffer" must be bool, stream resource or Closure, "%s" given.', get_debug_type($buffer)));
            }

            if (false === strpbrk($bufferInfo['mode'], 'acew+')) {
                throw new InvalidArgumentException(sprintf('The stream in option "buffer" must be writeable, mode "%s" given.', $bufferInfo['mode']));
            }
        }

        if (isset($options['json'])) {
            if (isset($options['body']) && '' !== $options['body']) {
                throw new InvalidArgumentException('Define either the "json" or the "body" option, setting both is not supported.');
            }
            $options['body'] = self::jsonEncode($options['json']);
            unset($options['json']);

            if (!isset($options['normalized_headers']['content-type'])) {
                $options['normalized_headers']['content-type'] = ['Content-Type: application/json'];
            }
        }

        if (!isset($options['normalized_headers']['accept'])) {
            $options['normalized_headers']['accept'] = ['Accept: */*'];
        }

        if (isset($options['body'])) {
            $options['body'] = self::normalizeBody($options['body']);

            if (\is_string($options['body'])
                && (string) \strlen($options['body']) !== substr($h = $options['normalized_headers']['content-length'][0] ?? '', 16)
                && ('' !== $h || '' !== $options['body'])
            ) {
                if ('chunked' === substr($options['normalized_headers']['transfer-encoding'][0] ?? '', \strlen('Transfer-Encoding: '))) {
                    unset($options['normalized_headers']['transfer-encoding']);
                    $options['body'] = self::dechunk($options['body']);
                }

                $options['normalized_headers']['content-length'] = [substr_replace($h ?: 'Content-Length: ', \strlen($options['body']), 16)];
            }
        }

        if (isset($options['peer_fingerprint'])) {
            $options['peer_fingerprint'] = self::normalizePeerFingerprint($options['peer_fingerprint']);
        }

        // Validate on_progress
        if (isset($options['on_progress']) && !\is_callable($onProgress = $options['on_progress'])) {
            throw new InvalidArgumentException(sprintf('Option "on_progress" must be callable, "%s" given.', get_debug_type($onProgress)));
        }

        if (\is_array($options['auth_basic'] ?? null)) {
            $count = \count($options['auth_basic']);
            if ($count <= 0 || $count > 2) {
                throw new InvalidArgumentException(sprintf('Option "auth_basic" must contain 1 or 2 elements, "%s" given.', $count));
            }

            $options['auth_basic'] = implode(':', $options['auth_basic']);
        }

        if (!\is_string($options['auth_basic'] ?? '')) {
            throw new InvalidArgumentException(sprintf('Option "auth_basic" must be string or an array, "%s" given.', get_debug_type($options['auth_basic'])));
        }

        if (isset($options['auth_bearer'])) {
            if (!\is_string($options['auth_bearer'])) {
                throw new InvalidArgumentException(sprintf('Option "auth_bearer" must be a string, "%s" given.', get_debug_type($options['auth_bearer'])));
            }
            if (preg_match('{[^\x21-\x7E]}', $options['auth_bearer'])) {
                throw new InvalidArgumentException('Invalid character found in option "auth_bearer": '.json_encode($options['auth_bearer']).'.');
            }
        }

        if (isset($options['auth_basic'], $options['auth_bearer'])) {
            throw new InvalidArgumentException('Define either the "auth_basic" or the "auth_bearer" option, setting both is not supported.');
        }

        if (null !== $url) {
            // Merge auth with headers
            if (($options['auth_basic'] ?? false) && !($options['normalized_headers']['authorization'] ?? false)) {
                $options['normalized_headers']['authorization'] = ['Authorization: Basic '.base64_encode($options['auth_basic'])];
            }
            // Merge bearer with headers
            if (($options['auth_bearer'] ?? false) && !($options['normalized_headers']['authorization'] ?? false)) {
                $options['normalized_headers']['authorization'] = ['Authorization: Bearer '.$options['auth_bearer']];
            }

            unset($options['auth_basic'], $options['auth_bearer']);

            // Parse base URI
            if (\is_string($options['base_uri'])) {
                $options['base_uri'] = self::parseUrl($options['base_uri']);
            }

            // Validate and resolve URL
            $url = self::parseUrl($url, $options['query']);
            $url = self::resolveUrl($url, $options['base_uri'], $defaultOptions['query'] ?? []);
        }

        // Finalize normalization of options
        $options['http_version'] = (string) ($options['http_version'] ?? '') ?: null;
        if (0 > $options['timeout'] = (float) ($options['timeout'] ?? \ini_get('default_socket_timeout'))) {
            $options['timeout'] = 172800.0; // 2 days
        }

        $options['max_duration'] = isset($options['max_duration']) ? (float) $options['max_duration'] : 0;
        $options['headers'] = array_merge(...array_values($options['normalized_headers']));

        return [$url, $options];
    }

    /**
     * @throws InvalidArgumentException When an invalid option is found
     */
    private static function mergeDefaultOptions(array $options, array $defaultOptions, bool $allowExtraOptions = false): array
    {
        $options['normalized_headers'] = self::normalizeHeaders($options['headers'] ?? []);

        if ($defaultOptions['headers'] ?? false) {
            $options['normalized_headers'] += self::normalizeHeaders($defaultOptions['headers']);
        }

        $options['headers'] = array_merge(...array_values($options['normalized_headers']) ?: [[]]);

        if ($resolve = $options['resolve'] ?? false) {
            $options['resolve'] = [];
            foreach ($resolve as $k => $v) {
                if ('' === $v = (string) $v) {
                    $v = null;
                } elseif ('[' === $v[0] && ']' === substr($v, -1) && str_contains($v, ':')) {
                    $v = substr($v, 1, -1);
                }

                $options['resolve'][substr(self::parseUrl('http://'.$k)['authority'], 2)] = $v;
            }
        }

        // Option "query" is never inherited from defaults
        $options['query'] = $options['query'] ?? [];

        $options += $defaultOptions;

        if (isset(self::$emptyDefaults)) {
            foreach (self::$emptyDefaults as $k => $v) {
                if (!isset($options[$k])) {
                    $options[$k] = $v;
                }
            }
        }

        if (isset($defaultOptions['extra'])) {
            $options['extra'] += $defaultOptions['extra'];
        }

        if ($resolve = $defaultOptions['resolve'] ?? false) {
            foreach ($resolve as $k => $v) {
                if ('' === $v = (string) $v) {
                    $v = null;
                } elseif ('[' === $v[0] && ']' === substr($v, -1) && str_contains($v, ':')) {
                    $v = substr($v, 1, -1);
                }

                $options['resolve'] += [substr(self::parseUrl('http://'.$k)['authority'], 2) => $v];
            }
        }

        if ($allowExtraOptions || !$defaultOptions) {
            return $options;
        }

        // Look for unsupported options
        foreach ($options as $name => $v) {
            if (\array_key_exists($name, $defaultOptions) || 'normalized_headers' === $name) {
                continue;
            }

            if ('auth_ntlm' === $name) {
                if (!\extension_loaded('curl')) {
                    $msg = 'try installing the "curl" extension to use "%s" instead.';
                } else {
                    $msg = 'try using "%s" instead.';
                }

                throw new InvalidArgumentException(sprintf('Option "auth_ntlm" is not supported by "%s", '.$msg, __CLASS__, CurlHttpClient::class));
            }

            $alternatives = [];

            foreach ($defaultOptions as $k => $v) {
                if (levenshtein($name, $k) <= \strlen($name) / 3 || str_contains($k, $name)) {
                    $alternatives[] = $k;
                }
            }

            throw new InvalidArgumentException(sprintf('Unsupported option "%s" passed to "%s", did you mean "%s"?', $name, __CLASS__, implode('", "', $alternatives ?: array_keys($defaultOptions))));
        }

        return $options;
    }

    /**
     * @return string[][]
     *
     * @throws InvalidArgumentException When an invalid header is found
     */
    private static function normalizeHeaders(array $headers): array
    {
        $normalizedHeaders = [];

        foreach ($headers as $name => $values) {
            if (\is_object($values) && method_exists($values, '__toString')) {
                $values = (string) $values;
            }

            if (\is_int($name)) {
                if (!\is_string($values)) {
                    throw new InvalidArgumentException(sprintf('Invalid value for header "%s": expected string, "%s" given.', $name, get_debug_type($values)));
                }
                [$name, $values] = explode(':', $values, 2);
                $values = [ltrim($values)];
            } elseif (!is_iterable($values)) {
                if (\is_object($values)) {
                    throw new InvalidArgumentException(sprintf('Invalid value for header "%s": expected string, "%s" given.', $name, get_debug_type($values)));
                }

                $values = (array) $values;
            }

            $lcName = strtolower($name);
            $normalizedHeaders[$lcName] = [];

            foreach ($values as $value) {
                $normalizedHeaders[$lcName][] = $value = $name.': '.$value;

                if (\strlen($value) !== strcspn($value, "\r\n\0")) {
                    throw new InvalidArgumentException(sprintf('Invalid header: CR/LF/NUL found in "%s".', $value));
                }
            }
        }

        return $normalizedHeaders;
    }

    /**
     * @param array|string|resource|\Traversable|\Closure $body
     *
     * @return string|resource|\Closure
     *
     * @throws InvalidArgumentException When an invalid body is passed
     */
    private static function normalizeBody($body)
    {
        if (\is_array($body)) {
            array_walk_recursive($body, $caster = static function (&$v) use (&$caster) {
                if (\is_object($v)) {
                    if ($vars = get_object_vars($v)) {
                        array_walk_recursive($vars, $caster);
                        $v = $vars;
                    } elseif (method_exists($v, '__toString')) {
                        $v = (string) $v;
                    }
                }
            });

            return http_build_query($body, '', '&');
        }

        if (\is_string($body)) {
            return $body;
        }

        $generatorToCallable = static function (\Generator $body): \Closure {
            return static function () use ($body) {
                while ($body->valid()) {
                    $chunk = $body->current();
                    $body->next();

                    if ('' !== $chunk) {
                        return $chunk;
                    }
                }

                return '';
            };
        };

        if ($body instanceof \Generator) {
            return $generatorToCallable($body);
        }

        if ($body instanceof \Traversable) {
            return $generatorToCallable((static function ($body) { yield from $body; })($body));
        }

        if ($body instanceof \Closure) {
            $r = new \ReflectionFunction($body);
            $body = $r->getClosure();

            if ($r->isGenerator()) {
                $body = $body(self::$CHUNK_SIZE);

                return $generatorToCallable($body);
            }

            return $body;
        }

        if (!\is_array(@stream_get_meta_data($body))) {
            throw new InvalidArgumentException(sprintf('Option "body" must be string, stream resource, iterable or callable, "%s" given.', get_debug_type($body)));
        }

        return $body;
    }

    private static function dechunk(string $body): string
    {
        $h = fopen('php://temp', 'w+');
        stream_filter_append($h, 'dechunk', \STREAM_FILTER_WRITE);
        fwrite($h, $body);
        $body = stream_get_contents($h, -1, 0);
        rewind($h);
        ftruncate($h, 0);

        if (fwrite($h, '-') && '' !== stream_get_contents($h, -1, 0)) {
            throw new TransportException('Request body has broken chunked encoding.');
        }

        return $body;
    }

    /**
     * @param string|string[] $fingerprint
     *
     * @throws InvalidArgumentException When an invalid fingerprint is passed
     */
    private static function normalizePeerFingerprint($fingerprint): array
    {
        if (\is_string($fingerprint)) {
            switch (\strlen($fingerprint = str_replace(':', '', $fingerprint))) {
                case 32: $fingerprint = ['md5' => $fingerprint]; break;
                case 40: $fingerprint = ['sha1' => $fingerprint]; break;
                case 44: $fingerprint = ['pin-sha256' => [$fingerprint]]; break;
                case 64: $fingerprint = ['sha256' => $fingerprint]; break;
                default: throw new InvalidArgumentException(sprintf('Cannot auto-detect fingerprint algorithm for "%s".', $fingerprint));
            }
        } elseif (\is_array($fingerprint)) {
            foreach ($fingerprint as $algo => $hash) {
                $fingerprint[$algo] = 'pin-sha256' === $algo ? (array) $hash : str_replace(':', '', $hash);
            }
        } else {
            throw new InvalidArgumentException(sprintf('Option "peer_fingerprint" must be string or array, "%s" given.', get_debug_type($fingerprint)));
        }

        return $fingerprint;
    }

    /**
     * @param mixed $value
     *
     * @throws InvalidArgumentException When the value cannot be json-encoded
     */
    private static function jsonEncode($value, ?int $flags = null, int $maxDepth = 512): string
    {
        $flags = $flags ?? (\JSON_HEX_TAG | \JSON_HEX_APOS | \JSON_HEX_AMP | \JSON_HEX_QUOT | \JSON_PRESERVE_ZERO_FRACTION);

        try {
            $value = json_encode($value, $flags | (\PHP_VERSION_ID >= 70300 ? \JSON_THROW_ON_ERROR : 0), $maxDepth);
        } catch (\JsonException $e) {
            throw new InvalidArgumentException('Invalid value for "json" option: '.$e->getMessage());
        }

        if (\PHP_VERSION_ID < 70300 && \JSON_ERROR_NONE !== json_last_error() && (false === $value || !($flags & \JSON_PARTIAL_OUTPUT_ON_ERROR))) {
            throw new InvalidArgumentException('Invalid value for "json" option: '.json_last_error_msg());
        }

        return $value;
    }

    /**
     * Resolves a URL against a base URI.
     *
     * @see https://tools.ietf.org/html/rfc3986#section-5.2.2
     *
     * @throws InvalidArgumentException When an invalid URL is passed
     */
    private static function resolveUrl(array $url, ?array $base, array $queryDefaults = []): array
    {
        $givenUrl = $url;

        if (null !== $base && '' === ($base['scheme'] ?? '').($base['authority'] ?? '')) {
            throw new InvalidArgumentException(sprintf('Invalid "base_uri" option: host or scheme is missing in "%s".', implode('', $base)));
        }

        if (null === $url['scheme'] && (null === $base || null === $base['scheme'])) {
            throw new InvalidArgumentException(sprintf('Invalid URL: scheme is missing in "%s". Did you forget to add "http(s)://"?', implode('', $base ?? $url)));
        }

        if (null === $base && '' === $url['scheme'].$url['authority']) {
            throw new InvalidArgumentException(sprintf('Invalid URL: no "base_uri" option was provided and host or scheme is missing in "%s".', implode('', $url)));
        }

        if (null !== $url['scheme']) {
            $url['path'] = self::removeDotSegments($url['path'] ?? '');
        } else {
            if (null !== $url['authority']) {
                $url['path'] = self::removeDotSegments($url['path'] ?? '');
            } else {
                if (null === $url['path']) {
                    $url['path'] = $base['path'];
                    $url['query'] = $url['query'] ?? $base['query'];
                } else {
                    if ('/' !== $url['path'][0]) {
                        if (null === $base['path']) {
                            $url['path'] = '/'.$url['path'];
                        } else {
                            $segments = explode('/', $base['path']);
                            array_splice($segments, -1, 1, [$url['path']]);
                            $url['path'] = implode('/', $segments);
                        }
                    }

                    $url['path'] = self::removeDotSegments($url['path']);
                }

                $url['authority'] = $base['authority'];

                if ($queryDefaults) {
                    $url['query'] = '?'.self::mergeQueryString(substr($url['query'] ?? '', 1), $queryDefaults, false);
                }
            }

            $url['scheme'] = $base['scheme'];
        }

        if ('' === ($url['path'] ?? '')) {
            $url['path'] = '/';
        }

        if ('?' === ($url['query'] ?? '')) {
            $url['query'] = null;
        }

        if (null !== $url['scheme'] && null === $url['authority']) {
            throw new InvalidArgumentException(\sprintf('Invalid URL: host is missing in "%s".', implode('', $givenUrl)));
        }

        return $url;
    }

    /**
     * Parses a URL and fixes its encoding if needed.
     *
     * @throws InvalidArgumentException When an invalid URL is passed
     */
    private static function parseUrl(string $url, array $query = [], array $allowedSchemes = ['http' => 80, 'https' => 443]): array
    {
        $tail = '';

        if (false === $parts = parse_url(\strlen($url) !== strcspn($url, '?#') ? $url : $url.$tail = '#')) {
            throw new InvalidArgumentException(sprintf('Malformed URL "%s".', $url));
        }

        if ($query) {
            $parts['query'] = self::mergeQueryString($parts['query'] ?? null, $query, true);
        }

        $scheme = $parts['scheme'] ?? null;
        $host = $parts['host'] ?? null;

        if (!$scheme && $host && !str_starts_with($url, '//')) {
            $parts = parse_url(':/'.$url.$tail);
            $parts['path'] = substr($parts['path'], 2);
            $scheme = $host = null;
        }

        $port = $parts['port'] ?? 0;

        if (null !== $scheme) {
            if (!isset($allowedSchemes[$scheme = strtolower($scheme)])) {
                throw new InvalidArgumentException(sprintf('Unsupported scheme in "%s": "%s" expected.', $url, implode('" or "', array_keys($allowedSchemes))));
            }

            $port = $allowedSchemes[$scheme] === $port ? 0 : $port;
            $scheme .= ':';
        }

        if (null !== $host) {
            if (!\defined('INTL_IDNA_VARIANT_UTS46') && preg_match('/[\x80-\xFF]/', $host)) {
                throw new InvalidArgumentException(sprintf('Unsupported IDN "%s", try enabling the "intl" PHP extension or running "composer require symfony/polyfill-intl-idn".', $host));
            }

            $host = \defined('INTL_IDNA_VARIANT_UTS46') ? idn_to_ascii($host, \IDNA_DEFAULT | \IDNA_USE_STD3_RULES | \IDNA_CHECK_BIDI | \IDNA_CHECK_CONTEXTJ | \IDNA_NONTRANSITIONAL_TO_ASCII, \INTL_IDNA_VARIANT_UTS46) ?: strtolower($host) : strtolower($host);
            $host .= $port ? ':'.$port : '';
        }

        foreach (['user', 'pass', 'path', 'query', 'fragment'] as $part) {
            if (!isset($parts[$part])) {
                continue;
            }

            if (str_contains($parts[$part], '%')) {
                // https://tools.ietf.org/html/rfc3986#section-2.3
                $parts[$part] = preg_replace_callback('/%(?:2[DE]|3[0-9]|[46][1-9A-F]|5F|[57][0-9A]|7E)++/i', function ($m) { return rawurldecode($m[0]); }, $parts[$part]);
            }

            // https://tools.ietf.org/html/rfc3986#section-3.3
            $parts[$part] = preg_replace_callback("#[^-A-Za-z0-9._~!$&/'()[\]*+,;=:@{}%]++#", function ($m) { return rawurlencode($m[0]); }, $parts[$part]);
        }

        return [
            'scheme' => $scheme,
            'authority' => null !== $host ? '//'.(isset($parts['user']) ? $parts['user'].(isset($parts['pass']) ? ':'.$parts['pass'] : '').'@' : '').$host : null,
            'path' => isset($parts['path'][0]) ? $parts['path'] : null,
            'query' => isset($parts['query']) ? '?'.$parts['query'] : null,
            'fragment' => isset($parts['fragment']) && !$tail ? '#'.$parts['fragment'] : null,
        ];
    }

    /**
     * Removes dot-segments from a path.
     *
     * @see https://tools.ietf.org/html/rfc3986#section-5.2.4
     */
    private static function removeDotSegments(string $path)
    {
        $result = '';

        while (!\in_array($path, ['', '.', '..'], true)) {
            if ('.' === $path[0] && (str_starts_with($path, $p = '../') || str_starts_with($path, $p = './'))) {
                $path = substr($path, \strlen($p));
            } elseif ('/.' === $path || str_starts_with($path, '/./')) {
                $path = substr_replace($path, '/', 0, 3);
            } elseif ('/..' === $path || str_starts_with($path, '/../')) {
                $i = strrpos($result, '/');
                $result = $i ? substr($result, 0, $i) : '';
                $path = substr_replace($path, '/', 0, 4);
            } else {
                $i = strpos($path, '/', 1) ?: \strlen($path);
                $result .= substr($path, 0, $i);
                $path = substr($path, $i);
            }
        }

        return $result;
    }

    /**
     * Merges and encodes a query array with a query string.
     *
     * @throws InvalidArgumentException When an invalid query-string value is passed
     */
    private static function mergeQueryString(?string $queryString, array $queryArray, bool $replace): ?string
    {
        if (!$queryArray) {
            return $queryString;
        }

        $query = [];

        if (null !== $queryString) {
            foreach (explode('&', $queryString) as $v) {
                if ('' !== $v) {
                    $k = urldecode(explode('=', $v, 2)[0]);
                    $query[$k] = (isset($query[$k]) ? $query[$k].'&' : '').$v;
                }
            }
        }

        if ($replace) {
            foreach ($queryArray as $k => $v) {
                if (null === $v) {
                    unset($query[$k]);
                }
            }
        }

        $queryString = http_build_query($queryArray, '', '&', \PHP_QUERY_RFC3986);
        $queryArray = [];

        if ($queryString) {
            if (str_contains($queryString, '%')) {
                // https://tools.ietf.org/html/rfc3986#section-2.3 + some chars not encoded by browsers
                $queryString = strtr($queryString, [
                    '%21' => '!',
                    '%24' => '$',
                    '%28' => '(',
                    '%29' => ')',
                    '%2A' => '*',
                    '%2F' => '/',
                    '%3A' => ':',
                    '%3B' => ';',
                    '%40' => '@',
                    '%5B' => '[',
                    '%5D' => ']',
                ]);
            }

            foreach (explode('&', $queryString) as $v) {
                $queryArray[rawurldecode(explode('=', $v, 2)[0])] = $v;
            }
        }

        return implode('&', $replace ? array_replace($query, $queryArray) : ($query + $queryArray));
    }

    /**
     * Loads proxy configuration from the same environment variables as curl when no proxy is explicitly set.
     */
    private static function getProxy(?string $proxy, array $url, ?string $noProxy): ?array
    {
        if (null === $proxy = self::getProxyUrl($proxy, $url)) {
            return null;
        }

        $proxy = (parse_url($proxy) ?: []) + ['scheme' => 'http'];

        if (!isset($proxy['host'])) {
            throw new TransportException('Invalid HTTP proxy: host is missing.');
        }

        if ('http' === $proxy['scheme']) {
            $proxyUrl = 'tcp://'.$proxy['host'].':'.($proxy['port'] ?? '80');
        } elseif ('https' === $proxy['scheme']) {
            $proxyUrl = 'ssl://'.$proxy['host'].':'.($proxy['port'] ?? '443');
        } else {
            throw new TransportException(sprintf('Unsupported proxy scheme "%s": "http" or "https" expected.', $proxy['scheme']));
        }

        $noProxy = $noProxy ?? $_SERVER['no_proxy'] ?? $_SERVER['NO_PROXY'] ?? '';
        $noProxy = $noProxy ? preg_split('/[\s,]+/', $noProxy) : [];

        return [
            'url' => $proxyUrl,
            'auth' => isset($proxy['user']) ? 'Basic '.base64_encode(rawurldecode($proxy['user']).':'.rawurldecode($proxy['pass'] ?? '')) : null,
            'no_proxy' => $noProxy,
        ];
    }

    private static function getProxyUrl(?string $proxy, array $url): ?string
    {
        if (null !== $proxy) {
            return $proxy;
        }

        // Ignore HTTP_PROXY except on the CLI to work around httpoxy set of vulnerabilities
        $proxy = $_SERVER['http_proxy'] ?? (\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) ? $_SERVER['HTTP_PROXY'] ?? null : null) ?? $_SERVER['all_proxy'] ?? $_SERVER['ALL_PROXY'] ?? null;

        if ('https:' === $url['scheme']) {
            $proxy = $_SERVER['https_proxy'] ?? $_SERVER['HTTPS_PROXY'] ?? $proxy;
        }

        return $proxy;
    }

    private static function shouldBuffer(array $headers): bool
    {
        if (null === $contentType = $headers['content-type'][0] ?? null) {
            return false;
        }

        if (false !== $i = strpos($contentType, ';')) {
            $contentType = substr($contentType, 0, $i);
        }

        return $contentType && preg_match('#^(?:text/|application/(?:.+\+)?(?:json|xml)$)#i', $contentType);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\HttpClient\DependencyInjection;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpClient\TraceableHttpClient;

final class HttpClientPass implements CompilerPassInterface
{
    private $clientTag;

    public function __construct(string $clientTag = 'http_client.client')
    {
        if (0 < \func_num_args()) {
            trigger_deprecation('symfony/http-client', '5.3', 'Configuring "%s" is deprecated.', __CLASS__);
        }

        $this->clientTag = $clientTag;
    }

    /**
     * {@inheritdoc}
     */
    public function process(ContainerBuilder $container)
    {
        if (!$container->hasDefinition('data_collector.http_client')) {
            return;
        }

        foreach ($container->findTaggedServiceIds($this->clientTag) as $id => $tags) {
            $container->register('.debug.'.$id, TraceableHttpClient::class)
                ->setArguments([new Reference('.debug.'.$id.'.inner'), new Reference('debug.stopwatch', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)])
                ->addTag('kernel.reset', ['method' => 'reset'])
                ->setDecoratedService($id, null, 5);
            $container->getDefinition('data_collector.http_client')
                ->addMethodCall('registerClient', [$id, new Reference('.debug.'.$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\HttpClient;

use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpClient\Response\ResponseStream;
use Symfony\Component\HttpClient\Response\TraceableResponse;
use Symfony\Component\Stopwatch\Stopwatch;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
use Symfony\Contracts\Service\ResetInterface;

/**
 * @author Jérémy Romey <jeremy@free-agent.fr>
 */
final class TraceableHttpClient implements HttpClientInterface, ResetInterface, LoggerAwareInterface
{
    private $client;
    private $stopwatch;
    private $tracedRequests;

    public function __construct(HttpClientInterface $client, ?Stopwatch $stopwatch = null)
    {
        $this->client = $client;
        $this->stopwatch = $stopwatch;
        $this->tracedRequests = new \ArrayObject();
    }

    /**
     * {@inheritdoc}
     */
    public function request(string $method, string $url, array $options = []): ResponseInterface
    {
        $content = null;
        $traceInfo = [];
        $this->tracedRequests[] = [
            'method' => $method,
            'url' => $url,
            'options' => $options,
            'info' => &$traceInfo,
            'content' => &$content,
        ];
        $onProgress = $options['on_progress'] ?? null;

        if (false === ($options['extra']['trace_content'] ?? true)) {
            unset($content);
            $content = false;
        }

        $options['on_progress'] = function (int $dlNow, int $dlSize, array $info) use (&$traceInfo, $onProgress) {
            $traceInfo = $info;

            if (null !== $onProgress) {
                $onProgress($dlNow, $dlSize, $info);
            }
        };

        return new TraceableResponse($this->client, $this->client->request($method, $url, $options), $content, null === $this->stopwatch ? null : $this->stopwatch->start("$method $url", 'http_client'));
    }

    /**
     * {@inheritdoc}
     */
    public function stream($responses, ?float $timeout = null): ResponseStreamInterface
    {
        if ($responses instanceof TraceableResponse) {
            $responses = [$responses];
        } elseif (!is_iterable($responses)) {
            throw new \TypeError(sprintf('"%s()" expects parameter 1 to be an iterable of TraceableResponse objects, "%s" given.', __METHOD__, get_debug_type($responses)));
        }

        return new ResponseStream(TraceableResponse::stream($this->client, $responses, $timeout));
    }

    public function getTracedRequests(): array
    {
        return $this->tracedRequests->getArrayCopy();
    }

    public function reset()
    {
        if ($this->client instanceof ResetInterface) {
            $this->client->reset();
        }

        $this->tracedRequests->exchangeArray([]);
    }

    /**
     * {@inheritdoc}
     */
    public function setLogger(LoggerInterface $logger): void
    {
        if ($this->client instanceof LoggerAwareInterface) {
            $this->client->setLogger($logger);
        }
    }

    /**
     * {@inheritdoc}
     */
    public function withOptions(array $options): self
    {
        $clone = clone $this;
        $clone->client = $this->client->withOptions($options);

        return $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\HttpClient\Retry;

use Symfony\Component\HttpClient\Response\AsyncContext;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;

/**
 * @author Jérémy Derussé <jeremy@derusse.com>
 * @author Nicolas Grekas <p@tchwork.com>
 */
interface RetryStrategyInterface
{
    /**
     * Returns whether the request should be retried.
     *
     * @param ?string $responseContent Null is passed when the body did not arrive yet
     *
     * @return bool|null Returns null to signal that the body is required to take a decision
     */
    public function shouldRetry(AsyncContext $context, ?string $responseContent, ?TransportExceptionInterface $exception): ?bool;

    /**
     * Returns the time to wait in milliseconds.
     */
    public function getDelay(AsyncContext $context, ?string $responseContent, ?TransportExceptionInterface $exception): 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\HttpClient\Retry;

use Symfony\Component\HttpClient\Exception\InvalidArgumentException;
use Symfony\Component\HttpClient\Response\AsyncContext;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;

/**
 * Decides to retry the request when HTTP status codes belong to the given list of codes.
 *
 * @author Jérémy Derussé <jeremy@derusse.com>
 */
class GenericRetryStrategy implements RetryStrategyInterface
{
    public const IDEMPOTENT_METHODS = ['GET', 'HEAD', 'PUT', 'DELETE', 'OPTIONS', 'TRACE'];
    public const DEFAULT_RETRY_STATUS_CODES = [
        0 => self::IDEMPOTENT_METHODS, // for transport exceptions
        423,
        425,
        429,
        500 => self::IDEMPOTENT_METHODS,
        502,
        503,
        504 => self::IDEMPOTENT_METHODS,
        507 => self::IDEMPOTENT_METHODS,
        510 => self::IDEMPOTENT_METHODS,
    ];

    private $statusCodes;
    private $delayMs;
    private $multiplier;
    private $maxDelayMs;
    private $jitter;

    /**
     * @param array $statusCodes List of HTTP status codes that trigger a retry
     * @param int   $delayMs     Amount of time to delay (or the initial value when multiplier is used)
     * @param float $multiplier  Multiplier to apply to the delay each time a retry occurs
     * @param int   $maxDelayMs  Maximum delay to allow (0 means no maximum)
     * @param float $jitter      Probability of randomness int delay (0 = none, 1 = 100% random)
     */
    public function __construct(array $statusCodes = self::DEFAULT_RETRY_STATUS_CODES, int $delayMs = 1000, float $multiplier = 2.0, int $maxDelayMs = 0, float $jitter = 0.1)
    {
        $this->statusCodes = $statusCodes;

        if ($delayMs < 0) {
            throw new InvalidArgumentException(sprintf('Delay must be greater than or equal to zero: "%s" given.', $delayMs));
        }
        $this->delayMs = $delayMs;

        if ($multiplier < 1) {
            throw new InvalidArgumentException(sprintf('Multiplier must be greater than or equal to one: "%s" given.', $multiplier));
        }
        $this->multiplier = $multiplier;

        if ($maxDelayMs < 0) {
            throw new InvalidArgumentException(sprintf('Max delay must be greater than or equal to zero: "%s" given.', $maxDelayMs));
        }
        $this->maxDelayMs = $maxDelayMs;

        if ($jitter < 0 || $jitter > 1) {
            throw new InvalidArgumentException(sprintf('Jitter must be between 0 and 1: "%s" given.', $jitter));
        }
        $this->jitter = $jitter;
    }

    public function shouldRetry(AsyncContext $context, ?string $responseContent, ?TransportExceptionInterface $exception): ?bool
    {
        $statusCode = $context->getStatusCode();
        if (\in_array($statusCode, $this->statusCodes, true)) {
            return true;
        }
        if (isset($this->statusCodes[$statusCode]) && \is_array($this->statusCodes[$statusCode])) {
            return \in_array($context->getInfo('http_method'), $this->statusCodes[$statusCode], true);
        }
        if (null === $exception) {
            return false;
        }

        if (\in_array(0, $this->statusCodes, true)) {
            return true;
        }
        if (isset($this->statusCodes[0]) && \is_array($this->statusCodes[0])) {
            return \in_array($context->getInfo('http_method'), $this->statusCodes[0], true);
        }

        return false;
    }

    public function getDelay(AsyncContext $context, ?string $responseContent, ?TransportExceptionInterface $exception): int
    {
        $delay = $this->delayMs * $this->multiplier ** $context->getInfo('retry_count');

        if ($this->jitter > 0) {
            $randomness = (int) ($delay * $this->jitter);
            $delay = $delay + random_int(-$randomness, +$randomness);
        }

        if ($delay > $this->maxDelayMs && 0 !== $this->maxDelayMs) {
            return $this->maxDelayMs;
        }

        return (int) $delay;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\HttpClient;

use Http\Discovery\Exception\NotFoundException;
use Http\Discovery\Psr17FactoryDiscovery;
use Nyholm\Psr7\Factory\Psr17Factory;
use Nyholm\Psr7\Request;
use Nyholm\Psr7\Uri;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Client\NetworkExceptionInterface;
use Psr\Http\Client\RequestExceptionInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UriFactoryInterface;
use Psr\Http\Message\UriInterface;
use Symfony\Component\HttpClient\Internal\HttplugWaitLoop;
use Symfony\Component\HttpClient\Response\StreamableInterface;
use Symfony\Component\HttpClient\Response\StreamWrapper;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface as HttpClientResponseInterface;
use Symfony\Contracts\Service\ResetInterface;

if (!interface_exists(RequestFactoryInterface::class)) {
    throw new \LogicException('You cannot use the "Symfony\Component\HttpClient\Psr18Client" as the "psr/http-factory" package is not installed. Try running "composer require nyholm/psr7".');
}

if (!interface_exists(ClientInterface::class)) {
    throw new \LogicException('You cannot use the "Symfony\Component\HttpClient\Psr18Client" as the "psr/http-client" package is not installed. Try running "composer require psr/http-client".');
}

/**
 * An adapter to turn a Symfony HttpClientInterface into a PSR-18 ClientInterface.
 *
 * Run "composer require psr/http-client" to install the base ClientInterface. Run
 * "composer require nyholm/psr7" to install an efficient implementation of response
 * and stream factories with flex-provided autowiring aliases.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
final class Psr18Client implements ClientInterface, RequestFactoryInterface, StreamFactoryInterface, UriFactoryInterface, ResetInterface
{
    private $client;
    private $responseFactory;
    private $streamFactory;

    public function __construct(?HttpClientInterface $client = null, ?ResponseFactoryInterface $responseFactory = null, ?StreamFactoryInterface $streamFactory = null)
    {
        $this->client = $client ?? HttpClient::create();
        $this->responseFactory = $responseFactory;
        $this->streamFactory = $streamFactory ?? ($responseFactory instanceof StreamFactoryInterface ? $responseFactory : null);

        if (null !== $this->responseFactory && null !== $this->streamFactory) {
            return;
        }

        if (!class_exists(Psr17Factory::class) && !class_exists(Psr17FactoryDiscovery::class)) {
            throw new \LogicException('You cannot use the "Symfony\Component\HttpClient\Psr18Client" as no PSR-17 factories have been provided. Try running "composer require nyholm/psr7".');
        }

        try {
            $psr17Factory = class_exists(Psr17Factory::class, false) ? new Psr17Factory() : null;
            $this->responseFactory = $this->responseFactory ?? $psr17Factory ?? Psr17FactoryDiscovery::findResponseFactory();
            $this->streamFactory = $this->streamFactory ?? $psr17Factory ?? Psr17FactoryDiscovery::findStreamFactory();
        } catch (NotFoundException $e) {
            throw new \LogicException('You cannot use the "Symfony\Component\HttpClient\HttplugClient" as no PSR-17 factories have been found. Try running "composer require nyholm/psr7".', 0, $e);
        }
    }

    /**
     * {@inheritdoc}
     */
    public function sendRequest(RequestInterface $request): ResponseInterface
    {
        try {
            $body = $request->getBody();

            if ($body->isSeekable()) {
                $body->seek(0);
            }

            $options = [
                'headers' => $request->getHeaders(),
                'body' => $body->getContents(),
            ];

            if ('1.0' === $request->getProtocolVersion()) {
                $options['http_version'] = '1.0';
            }

            $response = $this->client->request($request->getMethod(), (string) $request->getUri(), $options);

            return HttplugWaitLoop::createPsr7Response($this->responseFactory, $this->streamFactory, $this->client, $response, false);
        } catch (TransportExceptionInterface $e) {
            if ($e instanceof \InvalidArgumentException) {
                throw new Psr18RequestException($e, $request);
            }

            throw new Psr18NetworkException($e, $request);
        }
    }

    /**
     * {@inheritdoc}
     */
    public function createRequest(string $method, $uri): RequestInterface
    {
        if ($this->responseFactory instanceof RequestFactoryInterface) {
            return $this->responseFactory->createRequest($method, $uri);
        }

        if (class_exists(Request::class)) {
            return new Request($method, $uri);
        }

        if (class_exists(Psr17FactoryDiscovery::class)) {
            return Psr17FactoryDiscovery::findRequestFactory()->createRequest($method, $uri);
        }

        throw new \LogicException(sprintf('You cannot use "%s()" as the "nyholm/psr7" package is not installed. Try running "composer require nyholm/psr7".', __METHOD__));
    }

    /**
     * {@inheritdoc}
     */
    public function createStream(string $content = ''): StreamInterface
    {
        $stream = $this->streamFactory->createStream($content);

        if ($stream->isSeekable()) {
            $stream->seek(0);
        }

        return $stream;
    }

    /**
     * {@inheritdoc}
     */
    public function createStreamFromFile(string $filename, string $mode = 'r'): StreamInterface
    {
        return $this->streamFactory->createStreamFromFile($filename, $mode);
    }

    /**
     * {@inheritdoc}
     */
    public function createStreamFromResource($resource): StreamInterface
    {
        return $this->streamFactory->createStreamFromResource($resource);
    }

    /**
     * {@inheritdoc}
     */
    public function createUri(string $uri = ''): UriInterface
    {
        if ($this->responseFactory instanceof UriFactoryInterface) {
            return $this->responseFactory->createUri($uri);
        }

        if (class_exists(Uri::class)) {
            return new Uri($uri);
        }

        if (class_exists(Psr17FactoryDiscovery::class)) {
            return Psr17FactoryDiscovery::findUrlFactory()->createUri($uri);
        }

        throw new \LogicException(sprintf('You cannot use "%s()" as the "nyholm/psr7" package is not installed. Try running "composer require nyholm/psr7".', __METHOD__));
    }

    public function reset()
    {
        if ($this->client instanceof ResetInterface) {
            $this->client->reset();
        }
    }
}

/**
 * @internal
 */
class Psr18NetworkException extends \RuntimeException implements NetworkExceptionInterface
{
    private $request;

    public function __construct(TransportExceptionInterface $e, RequestInterface $request)
    {
        parent::__construct($e->getMessage(), 0, $e);
        $this->request = $request;
    }

    public function getRequest(): RequestInterface
    {
        return $this->request;
    }
}

/**
 * @internal
 */
class Psr18RequestException extends \InvalidArgumentException implements RequestExceptionInterface
{
    private $request;

    public function __construct(TransportExceptionInterface $e, RequestInterface $request)
    {
        parent::__construct($e->getMessage(), 0, $e);
        $this->request = $request;
    }

    public function getRequest(): RequestInterface
    {
        return $this->request;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\HttpClient;

use Symfony\Contracts\HttpClient\HttpClientInterface;

/**
 * A helper providing autocompletion for available options.
 *
 * @see HttpClientInterface for a description of each options.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
class HttpOptions
{
    private $options = [];

    public function toArray(): array
    {
        return $this->options;
    }

    /**
     * @return $this
     */
    public function setAuthBasic(string $user, string $password = '')
    {
        $this->options['auth_basic'] = $user;

        if ('' !== $password) {
            $this->options['auth_basic'] .= ':'.$password;
        }

        return $this;
    }

    /**
     * @return $this
     */
    public function setAuthBearer(string $token)
    {
        $this->options['auth_bearer'] = $token;

        return $this;
    }

    /**
     * @return $this
     */
    public function setQuery(array $query)
    {
        $this->options['query'] = $query;

        return $this;
    }

    /**
     * @return $this
     */
    public function setHeaders(iterable $headers)
    {
        $this->options['headers'] = $headers;

        return $this;
    }

    /**
     * @param array|string|resource|\Traversable|\Closure $body
     *
     * @return $this
     */
    public function setBody($body)
    {
        $this->options['body'] = $body;

        return $this;
    }

    /**
     * @param mixed $json
     *
     * @return $this
     */
    public function setJson($json)
    {
        $this->options['json'] = $json;

        return $this;
    }

    /**
     * @return $this
     */
    public function setUserData($data)
    {
        $this->options['user_data'] = $data;

        return $this;
    }

    /**
     * @return $this
     */
    public function setMaxRedirects(int $max)
    {
        $this->options['max_redirects'] = $max;

        return $this;
    }

    /**
     * @return $this
     */
    public function setHttpVersion(string $version)
    {
        $this->options['http_version'] = $version;

        return $this;
    }

    /**
     * @return $this
     */
    public function setBaseUri(string $uri)
    {
        $this->options['base_uri'] = $uri;

        return $this;
    }

    /**
     * @return $this
     */
    public function buffer(bool $buffer)
    {
        $this->options['buffer'] = $buffer;

        return $this;
    }

    /**
     * @param callable(int, int, array, \Closure|null=):void $callback
     *
     * @return $this
     */
    public function setOnProgress(callable $callback)
    {
        $this->options['on_progress'] = $callback;

        return $this;
    }

    /**
     * @return $this
     */
    public function resolve(array $hostIps)
    {
        $this->options['resolve'] = $hostIps;

        return $this;
    }

    /**
     * @return $this
     */
    public function setProxy(string $proxy)
    {
        $this->options['proxy'] = $proxy;

        return $this;
    }

    /**
     * @return $this
     */
    public function setNoProxy(string $noProxy)
    {
        $this->options['no_proxy'] = $noProxy;

        return $this;
    }

    /**
     * @return $this
     */
    public function setTimeout(float $timeout)
    {
        $this->options['timeout'] = $timeout;

        return $this;
    }

    /**
     * @return $this
     */
    public function setMaxDuration(float $maxDuration)
    {
        $this->options['max_duration'] = $maxDuration;

        return $this;
    }

    /**
     * @return $this
     */
    public function bindTo(string $bindto)
    {
        $this->options['bindto'] = $bindto;

        return $this;
    }

    /**
     * @return $this
     */
    public function verifyPeer(bool $verify)
    {
        $this->options['verify_peer'] = $verify;

        return $this;
    }

    /**
     * @return $this
     */
    public function verifyHost(bool $verify)
    {
        $this->options['verify_host'] = $verify;

        return $this;
    }

    /**
     * @return $this
     */
    public function setCaFile(string $cafile)
    {
        $this->options['cafile'] = $cafile;

        return $this;
    }

    /**
     * @return $this
     */
    public function setCaPath(string $capath)
    {
        $this->options['capath'] = $capath;

        return $this;
    }

    /**
     * @return $this
     */
    public function setLocalCert(string $cert)
    {
        $this->options['local_cert'] = $cert;

        return $this;
    }

    /**
     * @return $this
     */
    public function setLocalPk(string $pk)
    {
        $this->options['local_pk'] = $pk;

        return $this;
    }

    /**
     * @return $this
     */
    public function setPassphrase(string $passphrase)
    {
        $this->options['passphrase'] = $passphrase;

        return $this;
    }

    /**
     * @return $this
     */
    public function setCiphers(string $ciphers)
    {
        $this->options['ciphers'] = $ciphers;

        return $this;
    }

    /**
     * @param string|array $fingerprint
     *
     * @return $this
     */
    public function setPeerFingerprint($fingerprint)
    {
        $this->options['peer_fingerprint'] = $fingerprint;

        return $this;
    }

    /**
     * @return $this
     */
    public function capturePeerCertChain(bool $capture)
    {
        $this->options['capture_peer_cert_chain'] = $capture;

        return $this;
    }

    /**
     * @return $this
     */
    public function setExtra(string $name, $value)
    {
        $this->options['extra'][$name] = $value;

        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\HttpClient;

use Symfony\Component\HttpClient\Response\MockResponse;
use Symfony\Component\HttpClient\Response\ResponseStream;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\HttpCache\HttpCache;
use Symfony\Component\HttpKernel\HttpCache\StoreInterface;
use Symfony\Component\HttpKernel\HttpClientKernel;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
use Symfony\Contracts\Service\ResetInterface;

/**
 * Adds caching on top of an HTTP client.
 *
 * The implementation buffers responses in memory and doesn't stream directly from the network.
 * You can disable/enable this layer by setting option "no_cache" under "extra" to true/false.
 * By default, caching is enabled unless the "buffer" option is set to false.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
class CachingHttpClient implements HttpClientInterface, ResetInterface
{
    use HttpClientTrait;

    private $client;
    private $cache;
    private $defaultOptions = self::OPTIONS_DEFAULTS;

    public function __construct(HttpClientInterface $client, StoreInterface $store, array $defaultOptions = [])
    {
        if (!class_exists(HttpClientKernel::class)) {
            throw new \LogicException(sprintf('Using "%s" requires that the HttpKernel component version 4.3 or higher is installed, try running "composer require symfony/http-kernel:^5.4".', __CLASS__));
        }

        $this->client = $client;
        $kernel = new HttpClientKernel($client);
        $this->cache = new HttpCache($kernel, $store, null, $defaultOptions);

        unset($defaultOptions['debug']);
        unset($defaultOptions['default_ttl']);
        unset($defaultOptions['private_headers']);
        unset($defaultOptions['allow_reload']);
        unset($defaultOptions['allow_revalidate']);
        unset($defaultOptions['stale_while_revalidate']);
        unset($defaultOptions['stale_if_error']);
        unset($defaultOptions['trace_level']);
        unset($defaultOptions['trace_header']);

        if ($defaultOptions) {
            [, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, $this->defaultOptions);
        }
    }

    /**
     * {@inheritdoc}
     */
    public function request(string $method, string $url, array $options = []): ResponseInterface
    {
        [$url, $options] = $this->prepareRequest($method, $url, $options, $this->defaultOptions, true);
        $url = implode('', $url);

        if (!empty($options['body']) || !empty($options['extra']['no_cache']) || !\in_array($method, ['GET', 'HEAD', 'OPTIONS'])) {
            return $this->client->request($method, $url, $options);
        }

        $request = Request::create($url, $method);
        $request->attributes->set('http_client_options', $options);

        foreach ($options['normalized_headers'] as $name => $values) {
            if ('cookie' !== $name) {
                foreach ($values as $value) {
                    $request->headers->set($name, substr($value, 2 + \strlen($name)), false);
                }

                continue;
            }

            foreach ($values as $cookies) {
                foreach (explode('; ', substr($cookies, \strlen('Cookie: '))) as $cookie) {
                    if ('' !== $cookie) {
                        $cookie = explode('=', $cookie, 2);
                        $request->cookies->set($cookie[0], $cookie[1] ?? '');
                    }
                }
            }
        }

        $response = $this->cache->handle($request);
        $response = new MockResponse($response->getContent(), [
            'http_code' => $response->getStatusCode(),
            'response_headers' => $response->headers->allPreserveCase(),
        ]);

        return MockResponse::fromRequest($method, $url, $options, $response);
    }

    /**
     * {@inheritdoc}
     */
    public function stream($responses, ?float $timeout = null): ResponseStreamInterface
    {
        if ($responses instanceof ResponseInterface) {
            $responses = [$responses];
        } elseif (!is_iterable($responses)) {
            throw new \TypeError(sprintf('"%s()" expects parameter 1 to be an iterable of ResponseInterface objects, "%s" given.', __METHOD__, get_debug_type($responses)));
        }

        $mockResponses = [];
        $clientResponses = [];

        foreach ($responses as $response) {
            if ($response instanceof MockResponse) {
                $mockResponses[] = $response;
            } else {
                $clientResponses[] = $response;
            }
        }

        if (!$mockResponses) {
            return $this->client->stream($clientResponses, $timeout);
        }

        if (!$clientResponses) {
            return new ResponseStream(MockResponse::stream($mockResponses, $timeout));
        }

        return new ResponseStream((function () use ($mockResponses, $clientResponses, $timeout) {
            yield from MockResponse::stream($mockResponses, $timeout);
            yield $this->client->stream($clientResponses, $timeout);
        })());
    }

    public function reset()
    {
        if ($this->client instanceof ResetInterface) {
            $this->client->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\Component\HttpClient\Chunk;

use Symfony\Contracts\HttpClient\ChunkInterface;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @internal
 */
class DataChunk implements ChunkInterface
{
    private $offset = 0;
    private $content = '';

    public function __construct(int $offset = 0, string $content = '')
    {
        $this->offset = $offset;
        $this->content = $content;
    }

    /**
     * {@inheritdoc}
     */
    public function isTimeout(): bool
    {
        return false;
    }

    /**
     * {@inheritdoc}
     */
    public function isFirst(): bool
    {
        return false;
    }

    /**
     * {@inheritdoc}
     */
    public function isLast(): bool
    {
        return false;
    }

    /**
     * {@inheritdoc}
     */
    public function getInformationalStatus(): ?array
    {
        return null;
    }

    /**
     * {@inheritdoc}
     */
    public function getContent(): string
    {
        return $this->content;
    }

    /**
     * {@inheritdoc}
     */
    public function getOffset(): int
    {
        return $this->offset;
    }

    /**
     * {@inheritdoc}
     */
    public function getError(): ?string
    {
        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\HttpClient\Chunk;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @internal
 */
class InformationalChunk extends DataChunk
{
    private $status;

    public function __construct(int $statusCode, array $headers)
    {
        $this->status = [$statusCode, $headers];
    }

    /**
     * {@inheritdoc}
     */
    public function getInformationalStatus(): ?array
    {
        return $this->status;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\HttpClient\Chunk;

use Symfony\Contracts\HttpClient\ChunkInterface;

/**
 * @author Antoine Bluchet <soyuka@gmail.com>
 * @author Nicolas Grekas <p@tchwork.com>
 */
final class ServerSentEvent extends DataChunk implements ChunkInterface
{
    private $data = '';
    private $id = '';
    private $type = 'message';
    private $retry = 0;

    public function __construct(string $content)
    {
        parent::__construct(-1, $content);

        // remove BOM
        if (0 === strpos($content, "\xEF\xBB\xBF")) {
            $content = substr($content, 3);
        }

        foreach (preg_split("/(?:\r\n|[\r\n])/", $content) as $line) {
            if (0 === $i = strpos($line, ':')) {
                continue;
            }

            $i = false === $i ? \strlen($line) : $i;
            $field = substr($line, 0, $i);
            $i += 1 + (' ' === ($line[1 + $i] ?? ''));

            switch ($field) {
                case 'id': $this->id = substr($line, $i); break;
                case 'event': $this->type = substr($line, $i); break;
                case 'data': $this->data .= ('' === $this->data ? '' : "\n").substr($line, $i); break;
                case 'retry':
                    $retry = substr($line, $i);

                    if ('' !== $retry && \strlen($retry) === strspn($retry, '0123456789')) {
                        $this->retry = $retry / 1000.0;
                    }
                    break;
            }
        }
    }

    public function getId(): string
    {
        return $this->id;
    }

    public function getType(): string
    {
        return $this->type;
    }

    public function getData(): string
    {
        return $this->data;
    }

    public function getRetry(): float
    {
        return $this->retry;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\HttpClient\Chunk;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @internal
 */
class LastChunk extends DataChunk
{
    /**
     * {@inheritdoc}
     */
    public function isLast(): bool
    {
        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\HttpClient\Chunk;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @internal
 */
class FirstChunk extends DataChunk
{
    /**
     * {@inheritdoc}
     */
    public function isFirst(): bool
    {
        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\HttpClient\Chunk;

use Symfony\Component\HttpClient\Exception\TimeoutException;
use Symfony\Component\HttpClient\Exception\TransportException;
use Symfony\Contracts\HttpClient\ChunkInterface;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @internal
 */
class ErrorChunk implements ChunkInterface
{
    private $didThrow = false;
    private $offset;
    private $errorMessage;
    private $error;

    /**
     * @param \Throwable|string $error
     */
    public function __construct(int $offset, $error)
    {
        $this->offset = $offset;

        if (\is_string($error)) {
            $this->errorMessage = $error;
        } else {
            $this->error = $error;
            $this->errorMessage = $error->getMessage();
        }
    }

    /**
     * {@inheritdoc}
     */
    public function isTimeout(): bool
    {
        $this->didThrow = true;

        if (null !== $this->error) {
            throw new TransportException($this->errorMessage, 0, $this->error);
        }

        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function isFirst(): bool
    {
        $this->didThrow = true;
        throw null !== $this->error ? new TransportException($this->errorMessage, 0, $this->error) : new TimeoutException($this->errorMessage);
    }

    /**
     * {@inheritdoc}
     */
    public function isLast(): bool
    {
        $this->didThrow = true;
        throw null !== $this->error ? new TransportException($this->errorMessage, 0, $this->error) : new TimeoutException($this->errorMessage);
    }

    /**
     * {@inheritdoc}
     */
    public function getInformationalStatus(): ?array
    {
        $this->didThrow = true;
        throw null !== $this->error ? new TransportException($this->errorMessage, 0, $this->error) : new TimeoutException($this->errorMessage);
    }

    /**
     * {@inheritdoc}
     */
    public function getContent(): string
    {
        $this->didThrow = true;
        throw null !== $this->error ? new TransportException($this->errorMessage, 0, $this->error) : new TimeoutException($this->errorMessage);
    }

    /**
     * {@inheritdoc}
     */
    public function getOffset(): int
    {
        return $this->offset;
    }

    /**
     * {@inheritdoc}
     */
    public function getError(): ?string
    {
        return $this->errorMessage;
    }

    /**
     * @return bool Whether the wrapped error has been thrown or not
     */
    public function didThrow(?bool $didThrow = null): bool
    {
        if (null !== $didThrow && $this->didThrow !== $didThrow) {
            return !$this->didThrow = $didThrow;
        }

        return $this->didThrow;
    }

    public function __sleep(): array
    {
        throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
    }

    public function __wakeup()
    {
        throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
    }

    public function __destruct()
    {
        if (!$this->didThrow) {
            $this->didThrow = true;
            throw null !== $this->error ? new TransportException($this->errorMessage, 0, $this->error) : new TimeoutException($this->errorMessage);
        }
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\HttpClient;

use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpClient\Exception\InvalidArgumentException;
use Symfony\Component\HttpClient\Exception\TransportException;
use Symfony\Component\HttpClient\Internal\CurlClientState;
use Symfony\Component\HttpClient\Internal\PushedResponse;
use Symfony\Component\HttpClient\Response\CurlResponse;
use Symfony\Component\HttpClient\Response\ResponseStream;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
use Symfony\Contracts\Service\ResetInterface;

/**
 * A performant implementation of the HttpClientInterface contracts based on the curl extension.
 *
 * This provides fully concurrent HTTP requests, with transparent
 * HTTP/2 push when a curl version that supports it is installed.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface, ResetInterface
{
    use HttpClientTrait;

    private $defaultOptions = self::OPTIONS_DEFAULTS + [
        'auth_ntlm' => null, // array|string - an array containing the username as first value, and optionally the
                             //   password as the second one; or string like username:password - enabling NTLM auth
        'extra' => [
            'curl' => [],    // A list of extra curl options indexed by their corresponding CURLOPT_*
        ],
    ];
    private static $emptyDefaults = self::OPTIONS_DEFAULTS + ['auth_ntlm' => null];

    /**
     * @var LoggerInterface|null
     */
    private $logger;

    private $maxHostConnections;
    private $maxPendingPushes;

    /**
     * An internal object to share state between the client and its responses.
     *
     * @var CurlClientState
     */
    private $multi;

    /**
     * @param array $defaultOptions     Default request's options
     * @param int   $maxHostConnections The maximum number of connections to a single host
     * @param int   $maxPendingPushes   The maximum number of pushed responses to accept in the queue
     *
     * @see HttpClientInterface::OPTIONS_DEFAULTS for available options
     */
    public function __construct(array $defaultOptions = [], int $maxHostConnections = 6, int $maxPendingPushes = 0)
    {
        if (!\extension_loaded('curl')) {
            throw new \LogicException('You cannot use the "Symfony\Component\HttpClient\CurlHttpClient" as the "curl" extension is not installed.');
        }

        $this->maxHostConnections = $maxHostConnections;
        $this->maxPendingPushes = $maxPendingPushes;

        $this->defaultOptions['buffer'] = $this->defaultOptions['buffer'] ?? \Closure::fromCallable([__CLASS__, 'shouldBuffer']);

        if ($defaultOptions) {
            [, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, $this->defaultOptions);
        }
    }

    public function setLogger(LoggerInterface $logger): void
    {
        $this->logger = $logger;
        if (isset($this->multi)) {
            $this->multi->logger = $logger;
        }
    }

    /**
     * @see HttpClientInterface::OPTIONS_DEFAULTS for available options
     *
     * {@inheritdoc}
     */
    public function request(string $method, string $url, array $options = []): ResponseInterface
    {
        $multi = $this->ensureState();

        [$url, $options] = self::prepareRequest($method, $url, $options, $this->defaultOptions);
        $scheme = $url['scheme'];
        $authority = $url['authority'];
        $host = parse_url($authority, \PHP_URL_HOST);
        $proxy = self::getProxyUrl($options['proxy'], $url);
        $url = implode('', $url);

        if (!isset($options['normalized_headers']['user-agent'])) {
            $options['headers'][] = 'User-Agent: Symfony HttpClient/Curl';
        }

        $curlopts = [
            \CURLOPT_URL => $url,
            \CURLOPT_TCP_NODELAY => true,
            \CURLOPT_PROTOCOLS => \CURLPROTO_HTTP | \CURLPROTO_HTTPS,
            \CURLOPT_REDIR_PROTOCOLS => \CURLPROTO_HTTP | \CURLPROTO_HTTPS,
            \CURLOPT_FOLLOWLOCATION => true,
            \CURLOPT_MAXREDIRS => 0 < $options['max_redirects'] ? $options['max_redirects'] : 0,
            \CURLOPT_COOKIEFILE => '', // Keep track of cookies during redirects
            \CURLOPT_TIMEOUT => 0,
            \CURLOPT_PROXY => $proxy,
            \CURLOPT_NOPROXY => $options['no_proxy'] ?? $_SERVER['no_proxy'] ?? $_SERVER['NO_PROXY'] ?? '',
            \CURLOPT_SSL_VERIFYPEER => $options['verify_peer'],
            \CURLOPT_SSL_VERIFYHOST => $options['verify_host'] ? 2 : 0,
            \CURLOPT_CAINFO => $options['cafile'],
            \CURLOPT_CAPATH => $options['capath'],
            \CURLOPT_SSL_CIPHER_LIST => $options['ciphers'],
            \CURLOPT_SSLCERT => $options['local_cert'],
            \CURLOPT_SSLKEY => $options['local_pk'],
            \CURLOPT_KEYPASSWD => $options['passphrase'],
            \CURLOPT_CERTINFO => $options['capture_peer_cert_chain'],
        ];

        if (1.0 === (float) $options['http_version']) {
            $curlopts[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_0;
        } elseif (1.1 === (float) $options['http_version']) {
            $curlopts[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_1;
        } elseif (\defined('CURL_VERSION_HTTP2') && (\CURL_VERSION_HTTP2 & CurlClientState::$curlVersion['features']) && ('https:' === $scheme || 2.0 === (float) $options['http_version'])) {
            $curlopts[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_2_0;
        }

        if (isset($options['auth_ntlm'])) {
            $curlopts[\CURLOPT_HTTPAUTH] = \CURLAUTH_NTLM;
            $curlopts[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_1;

            if (\is_array($options['auth_ntlm'])) {
                $count = \count($options['auth_ntlm']);
                if ($count <= 0 || $count > 2) {
                    throw new InvalidArgumentException(sprintf('Option "auth_ntlm" must contain 1 or 2 elements, %d given.', $count));
                }

                $options['auth_ntlm'] = implode(':', $options['auth_ntlm']);
            }

            if (!\is_string($options['auth_ntlm'])) {
                throw new InvalidArgumentException(sprintf('Option "auth_ntlm" must be a string or an array, "%s" given.', get_debug_type($options['auth_ntlm'])));
            }

            $curlopts[\CURLOPT_USERPWD] = $options['auth_ntlm'];
        }

        if (!\ZEND_THREAD_SAFE) {
            $curlopts[\CURLOPT_DNS_USE_GLOBAL_CACHE] = false;
        }

        if (\defined('CURLOPT_HEADEROPT') && \defined('CURLHEADER_SEPARATE')) {
            $curlopts[\CURLOPT_HEADEROPT] = \CURLHEADER_SEPARATE;
        }

        // curl's resolve feature varies by host:port but ours varies by host only, let's handle this with our own DNS map
        if (isset($multi->dnsCache->hostnames[$host])) {
            $options['resolve'] += [$host => $multi->dnsCache->hostnames[$host]];
        }

        if ($options['resolve'] || $multi->dnsCache->evictions) {
            // First reset any old DNS cache entries then add the new ones
            $resolve = $multi->dnsCache->evictions;
            $multi->dnsCache->evictions = [];
            $port = parse_url($authority, \PHP_URL_PORT) ?: ('http:' === $scheme ? 80 : 443);

            if ($resolve && 0x072A00 > CurlClientState::$curlVersion['version_number']) {
                // DNS cache removals require curl 7.42 or higher
                $multi->reset();
            }

            foreach ($options['resolve'] as $resolveHost => $ip) {
                $resolve[] = null === $ip ? "-$resolveHost:$port" : "$resolveHost:$port:$ip";
                $multi->dnsCache->hostnames[$resolveHost] = $ip;
                $multi->dnsCache->removals["-$resolveHost:$port"] = "-$resolveHost:$port";
            }

            $curlopts[\CURLOPT_RESOLVE] = $resolve;
        }

        if ('POST' === $method) {
            // Use CURLOPT_POST to have browser-like POST-to-GET redirects for 301, 302 and 303
            $curlopts[\CURLOPT_POST] = true;
        } elseif ('HEAD' === $method) {
            $curlopts[\CURLOPT_NOBODY] = true;
        } else {
            $curlopts[\CURLOPT_CUSTOMREQUEST] = $method;
        }

        if ('\\' !== \DIRECTORY_SEPARATOR && $options['timeout'] < 1) {
            $curlopts[\CURLOPT_NOSIGNAL] = true;
        }

        if (\extension_loaded('zlib') && !isset($options['normalized_headers']['accept-encoding'])) {
            $options['headers'][] = 'Accept-Encoding: gzip'; // Expose only one encoding, some servers mess up when more are provided
        }
        $body = $options['body'];

        foreach ($options['headers'] as $i => $header) {
            if (\is_string($body) && '' !== $body && 0 === stripos($header, 'Content-Length: ')) {
                // Let curl handle Content-Length headers
                unset($options['headers'][$i]);
                continue;
            }
            if (':' === $header[-2] && \strlen($header) - 2 === strpos($header, ': ')) {
                // curl requires a special syntax to send empty headers
                $curlopts[\CURLOPT_HTTPHEADER][] = substr_replace($header, ';', -2);
            } else {
                $curlopts[\CURLOPT_HTTPHEADER][] = $header;
            }
        }

        // Prevent curl from sending its default Accept and Expect headers
        foreach (['accept', 'expect'] as $header) {
            if (!isset($options['normalized_headers'][$header][0])) {
                $curlopts[\CURLOPT_HTTPHEADER][] = $header.':';
            }
        }

        if (!\is_string($body)) {
            if (\is_resource($body)) {
                $curlopts[\CURLOPT_INFILE] = $body;
            } else {
                $eof = false;
                $buffer = '';
                $curlopts[\CURLOPT_READFUNCTION] = static function ($ch, $fd, $length) use ($body, &$buffer, &$eof) {
                    return self::readRequestBody($length, $body, $buffer, $eof);
                };
            }

            if (isset($options['normalized_headers']['content-length'][0])) {
                $curlopts[\CURLOPT_INFILESIZE] = (int) substr($options['normalized_headers']['content-length'][0], \strlen('Content-Length: '));
            }
            if (!isset($options['normalized_headers']['transfer-encoding'])) {
                $curlopts[\CURLOPT_HTTPHEADER][] = 'Transfer-Encoding:'.(isset($curlopts[\CURLOPT_INFILESIZE]) ? '' : ' chunked');
            }

            if ('POST' !== $method) {
                $curlopts[\CURLOPT_UPLOAD] = true;

                if (!isset($options['normalized_headers']['content-type']) && 0 !== ($curlopts[\CURLOPT_INFILESIZE] ?? null)) {
                    $curlopts[\CURLOPT_HTTPHEADER][] = 'Content-Type: application/x-www-form-urlencoded';
                }
            }
        } elseif ('' !== $body || 'POST' === $method) {
            $curlopts[\CURLOPT_POSTFIELDS] = $body;
        }

        if ($options['peer_fingerprint']) {
            if (!isset($options['peer_fingerprint']['pin-sha256'])) {
                throw new TransportException(__CLASS__.' supports only "pin-sha256" fingerprints.');
            }

            $curlopts[\CURLOPT_PINNEDPUBLICKEY] = 'sha256//'.implode(';sha256//', $options['peer_fingerprint']['pin-sha256']);
        }

        if ($options['bindto']) {
            if (file_exists($options['bindto'])) {
                $curlopts[\CURLOPT_UNIX_SOCKET_PATH] = $options['bindto'];
            } elseif (!str_starts_with($options['bindto'], 'if!') && preg_match('/^(.*):(\d+)$/', $options['bindto'], $matches)) {
                $curlopts[\CURLOPT_INTERFACE] = trim($matches[1], '[]');
                $curlopts[\CURLOPT_LOCALPORT] = $matches[2];
            } else {
                $curlopts[\CURLOPT_INTERFACE] = $options['bindto'];
            }
        }

        if (0 < $options['max_duration']) {
            $curlopts[\CURLOPT_TIMEOUT_MS] = 1000 * $options['max_duration'];
        }

        if (!empty($options['extra']['curl']) && \is_array($options['extra']['curl'])) {
            $this->validateExtraCurlOptions($options['extra']['curl']);
            $curlopts += $options['extra']['curl'];
        }

        if ($pushedResponse = $multi->pushedResponses[$url] ?? null) {
            unset($multi->pushedResponses[$url]);

            if (self::acceptPushForRequest($method, $options, $pushedResponse)) {
                $this->logger && $this->logger->debug(sprintf('Accepting pushed response: "%s %s"', $method, $url));

                // Reinitialize the pushed response with request's options
                $ch = $pushedResponse->handle;
                $pushedResponse = $pushedResponse->response;
                $pushedResponse->__construct($multi, $url, $options, $this->logger);
            } else {
                $this->logger && $this->logger->debug(sprintf('Rejecting pushed response: "%s"', $url));
                $pushedResponse = null;
            }
        }

        if (!$pushedResponse) {
            $ch = curl_init();
            $this->logger && $this->logger->info(sprintf('Request: "%s %s"', $method, $url));
            $curlopts += [\CURLOPT_SHARE => $multi->share];
        }

        foreach ($curlopts as $opt => $value) {
            if (null !== $value && !curl_setopt($ch, $opt, $value) && \CURLOPT_CERTINFO !== $opt && (!\defined('CURLOPT_HEADEROPT') || \CURLOPT_HEADEROPT !== $opt)) {
                $constantName = $this->findConstantName($opt);
                throw new TransportException(sprintf('Curl option "%s" is not supported.', $constantName ?? $opt));
            }
        }

        return $pushedResponse ?? new CurlResponse($multi, $ch, $options, $this->logger, $method, self::createRedirectResolver($options, $host), CurlClientState::$curlVersion['version_number']);
    }

    /**
     * {@inheritdoc}
     */
    public function stream($responses, ?float $timeout = null): ResponseStreamInterface
    {
        if ($responses instanceof CurlResponse) {
            $responses = [$responses];
        } elseif (!is_iterable($responses)) {
            throw new \TypeError(sprintf('"%s()" expects parameter 1 to be an iterable of CurlResponse objects, "%s" given.', __METHOD__, get_debug_type($responses)));
        }

        $multi = $this->ensureState();

        if (\is_resource($multi->handle) || $multi->handle instanceof \CurlMultiHandle) {
            $active = 0;
            while (\CURLM_CALL_MULTI_PERFORM === curl_multi_exec($multi->handle, $active)) {
            }
        }

        return new ResponseStream(CurlResponse::stream($responses, $timeout));
    }

    public function reset()
    {
        if (isset($this->multi)) {
            $this->multi->reset();
        }
    }

    /**
     * Accepts pushed responses only if their headers related to authentication match the request.
     */
    private static function acceptPushForRequest(string $method, array $options, PushedResponse $pushedResponse): bool
    {
        if ('' !== $options['body'] || $method !== $pushedResponse->requestHeaders[':method'][0]) {
            return false;
        }

        foreach (['proxy', 'no_proxy', 'bindto', 'local_cert', 'local_pk'] as $k) {
            if ($options[$k] !== $pushedResponse->parentOptions[$k]) {
                return false;
            }
        }

        foreach (['authorization', 'cookie', 'range', 'proxy-authorization'] as $k) {
            $normalizedHeaders = $options['normalized_headers'][$k] ?? [];
            foreach ($normalizedHeaders as $i => $v) {
                $normalizedHeaders[$i] = substr($v, \strlen($k) + 2);
            }

            if (($pushedResponse->requestHeaders[$k] ?? []) !== $normalizedHeaders) {
                return false;
            }
        }

        return true;
    }

    /**
     * Wraps the request's body callback to allow it to return strings longer than curl requested.
     */
    private static function readRequestBody(int $length, \Closure $body, string &$buffer, bool &$eof): string
    {
        if (!$eof && \strlen($buffer) < $length) {
            if (!\is_string($data = $body($length))) {
                throw new TransportException(sprintf('The return value of the "body" option callback must be a string, "%s" returned.', get_debug_type($data)));
            }

            $buffer .= $data;
            $eof = '' === $data;
        }

        $data = substr($buffer, 0, $length);
        $buffer = substr($buffer, $length);

        return $data;
    }

    /**
     * Resolves relative URLs on redirects and deals with authentication headers.
     *
     * Work around CVE-2018-1000007: Authorization and Cookie headers should not follow redirects - fixed in Curl 7.64
     */
    private static function createRedirectResolver(array $options, string $host): \Closure
    {
        $redirectHeaders = [];
        if (0 < $options['max_redirects']) {
            $redirectHeaders['host'] = $host;
            $redirectHeaders['with_auth'] = $redirectHeaders['no_auth'] = array_filter($options['headers'], static function ($h) {
                return 0 !== stripos($h, 'Host:');
            });

            if (isset($options['normalized_headers']['authorization'][0]) || isset($options['normalized_headers']['cookie'][0])) {
                $redirectHeaders['no_auth'] = array_filter($options['headers'], static function ($h) {
                    return 0 !== stripos($h, 'Authorization:') && 0 !== stripos($h, 'Cookie:');
                });
            }
        }

        return static function ($ch, string $location, bool $noContent) use (&$redirectHeaders, $options) {
            try {
                $location = self::parseUrl($location);
                $url = self::parseUrl(curl_getinfo($ch, \CURLINFO_EFFECTIVE_URL));
                $url = self::resolveUrl($location, $url);
            } catch (InvalidArgumentException $e) {
                return null;
            }

            if ($noContent && $redirectHeaders) {
                $filterContentHeaders = static function ($h) {
                    return 0 !== stripos($h, 'Content-Length:') && 0 !== stripos($h, 'Content-Type:') && 0 !== stripos($h, 'Transfer-Encoding:');
                };
                $redirectHeaders['no_auth'] = array_filter($redirectHeaders['no_auth'], $filterContentHeaders);
                $redirectHeaders['with_auth'] = array_filter($redirectHeaders['with_auth'], $filterContentHeaders);
            }

            if ($redirectHeaders && isset($location['authority'])) {
                $requestHeaders = parse_url($location['authority'], \PHP_URL_HOST) === $redirectHeaders['host'] ? $redirectHeaders['with_auth'] : $redirectHeaders['no_auth'];
                curl_setopt($ch, \CURLOPT_HTTPHEADER, $requestHeaders);
            } elseif ($noContent && $redirectHeaders) {
                curl_setopt($ch, \CURLOPT_HTTPHEADER, $redirectHeaders['with_auth']);
            }

            curl_setopt($ch, \CURLOPT_PROXY, self::getProxyUrl($options['proxy'], $url));

            return implode('', $url);
        };
    }

    private function ensureState(): CurlClientState
    {
        if (!isset($this->multi)) {
            $this->multi = new CurlClientState($this->maxHostConnections, $this->maxPendingPushes);
            $this->multi->logger = $this->logger;
        }

        return $this->multi;
    }

    private function findConstantName(int $opt): ?string
    {
        $constants = array_filter(get_defined_constants(), static function ($v, $k) use ($opt) {
            return $v === $opt && 'C' === $k[0] && (str_starts_with($k, 'CURLOPT_') || str_starts_with($k, 'CURLINFO_'));
        }, \ARRAY_FILTER_USE_BOTH);

        return key($constants);
    }

    /**
     * Prevents overriding options that are set internally throughout the request.
     */
    private function validateExtraCurlOptions(array $options): void
    {
        $curloptsToConfig = [
            // options used in CurlHttpClient
            \CURLOPT_HTTPAUTH => 'auth_ntlm',
            \CURLOPT_USERPWD => 'auth_ntlm',
            \CURLOPT_RESOLVE => 'resolve',
            \CURLOPT_NOSIGNAL => 'timeout',
            \CURLOPT_HTTPHEADER => 'headers',
            \CURLOPT_INFILE => 'body',
            \CURLOPT_READFUNCTION => 'body',
            \CURLOPT_INFILESIZE => 'body',
            \CURLOPT_POSTFIELDS => 'body',
            \CURLOPT_UPLOAD => 'body',
            \CURLOPT_INTERFACE => 'bindto',
            \CURLOPT_TIMEOUT_MS => 'max_duration',
            \CURLOPT_TIMEOUT => 'max_duration',
            \CURLOPT_MAXREDIRS => 'max_redirects',
            \CURLOPT_POSTREDIR => 'max_redirects',
            \CURLOPT_PROXY => 'proxy',
            \CURLOPT_NOPROXY => 'no_proxy',
            \CURLOPT_SSL_VERIFYPEER => 'verify_peer',
            \CURLOPT_SSL_VERIFYHOST => 'verify_host',
            \CURLOPT_CAINFO => 'cafile',
            \CURLOPT_CAPATH => 'capath',
            \CURLOPT_SSL_CIPHER_LIST => 'ciphers',
            \CURLOPT_SSLCERT => 'local_cert',
            \CURLOPT_SSLKEY => 'local_pk',
            \CURLOPT_KEYPASSWD => 'passphrase',
            \CURLOPT_CERTINFO => 'capture_peer_cert_chain',
            \CURLOPT_USERAGENT => 'normalized_headers',
            \CURLOPT_REFERER => 'headers',
            // options used in CurlResponse
            \CURLOPT_NOPROGRESS => 'on_progress',
            \CURLOPT_PROGRESSFUNCTION => 'on_progress',
        ];

        if (\defined('CURLOPT_UNIX_SOCKET_PATH')) {
            $curloptsToConfig[\CURLOPT_UNIX_SOCKET_PATH] = 'bindto';
        }

        if (\defined('CURLOPT_PINNEDPUBLICKEY')) {
            $curloptsToConfig[\CURLOPT_PINNEDPUBLICKEY] = 'peer_fingerprint';
        }

        $curloptsToCheck = [
            \CURLOPT_PRIVATE,
            \CURLOPT_HEADERFUNCTION,
            \CURLOPT_WRITEFUNCTION,
            \CURLOPT_VERBOSE,
            \CURLOPT_STDERR,
            \CURLOPT_RETURNTRANSFER,
            \CURLOPT_URL,
            \CURLOPT_FOLLOWLOCATION,
            \CURLOPT_HEADER,
            \CURLOPT_CONNECTTIMEOUT,
            \CURLOPT_CONNECTTIMEOUT_MS,
            \CURLOPT_HTTP_VERSION,
            \CURLOPT_PORT,
            \CURLOPT_DNS_USE_GLOBAL_CACHE,
            \CURLOPT_PROTOCOLS,
            \CURLOPT_REDIR_PROTOCOLS,
            \CURLOPT_COOKIEFILE,
            \CURLINFO_REDIRECT_COUNT,
        ];

        if (\defined('CURLOPT_HTTP09_ALLOWED')) {
            $curloptsToCheck[] = \CURLOPT_HTTP09_ALLOWED;
        }

        if (\defined('CURLOPT_HEADEROPT')) {
            $curloptsToCheck[] = \CURLOPT_HEADEROPT;
        }

        $methodOpts = [
            \CURLOPT_POST,
            \CURLOPT_PUT,
            \CURLOPT_CUSTOMREQUEST,
            \CURLOPT_HTTPGET,
            \CURLOPT_NOBODY,
        ];

        foreach ($options as $opt => $optValue) {
            if (isset($curloptsToConfig[$opt])) {
                $constName = $this->findConstantName($opt) ?? $opt;
                throw new InvalidArgumentException(sprintf('Cannot set "%s" with "extra.curl", use option "%s" instead.', $constName, $curloptsToConfig[$opt]));
            }

            if (\in_array($opt, $methodOpts)) {
                throw new InvalidArgumentException('The HTTP method cannot be overridden using "extra.curl".');
            }

            if (\in_array($opt, $curloptsToCheck)) {
                $constName = $this->findConstantName($opt) ?? $opt;
                throw new InvalidArgumentException(sprintf('Cannot set "%s" with "extra.curl".', $constName));
            }
        }
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\HttpClient;

use GuzzleHttp\Promise\Promise as GuzzlePromise;
use GuzzleHttp\Promise\RejectedPromise;
use GuzzleHttp\Promise\Utils;
use Http\Client\Exception\NetworkException;
use Http\Client\Exception\RequestException;
use Http\Client\HttpAsyncClient;
use Http\Client\HttpClient as HttplugInterface;
use Http\Discovery\Exception\NotFoundException;
use Http\Discovery\Psr17FactoryDiscovery;
use Http\Message\RequestFactory;
use Http\Message\StreamFactory;
use Http\Message\UriFactory;
use Http\Promise\Promise;
use Nyholm\Psr7\Factory\Psr17Factory;
use Nyholm\Psr7\Request;
use Nyholm\Psr7\Uri;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface as Psr7ResponseInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UriFactoryInterface;
use Psr\Http\Message\UriInterface;
use Symfony\Component\HttpClient\Internal\HttplugWaitLoop;
use Symfony\Component\HttpClient\Response\HttplugPromise;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\Service\ResetInterface;

if (!interface_exists(HttplugInterface::class)) {
    throw new \LogicException('You cannot use "Symfony\Component\HttpClient\HttplugClient" as the "php-http/httplug" package is not installed. Try running "composer require php-http/httplug".');
}

if (!interface_exists(RequestFactory::class)) {
    throw new \LogicException('You cannot use "Symfony\Component\HttpClient\HttplugClient" as the "php-http/message-factory" package is not installed. Try running "composer require php-http/message-factory".');
}

/**
 * An adapter to turn a Symfony HttpClientInterface into an Httplug client.
 *
 * Run "composer require nyholm/psr7" to install an efficient implementation of response
 * and stream factories with flex-provided autowiring aliases.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
final class HttplugClient implements HttplugInterface, HttpAsyncClient, RequestFactory, StreamFactory, UriFactory, ResetInterface
{
    private $client;
    private $responseFactory;
    private $streamFactory;

    /**
     * @var \SplObjectStorage<ResponseInterface, array{RequestInterface, Promise}>|null
     */
    private $promisePool;

    private $waitLoop;

    public function __construct(?HttpClientInterface $client = null, ?ResponseFactoryInterface $responseFactory = null, ?StreamFactoryInterface $streamFactory = null)
    {
        $this->client = $client ?? HttpClient::create();
        $this->responseFactory = $responseFactory;
        $this->streamFactory = $streamFactory ?? ($responseFactory instanceof StreamFactoryInterface ? $responseFactory : null);
        $this->promisePool = class_exists(Utils::class) ? new \SplObjectStorage() : null;

        if (null === $this->responseFactory || null === $this->streamFactory) {
            if (!class_exists(Psr17Factory::class) && !class_exists(Psr17FactoryDiscovery::class)) {
                throw new \LogicException('You cannot use the "Symfony\Component\HttpClient\HttplugClient" as no PSR-17 factories have been provided. Try running "composer require nyholm/psr7".');
            }

            try {
                $psr17Factory = class_exists(Psr17Factory::class, false) ? new Psr17Factory() : null;
                $this->responseFactory = $this->responseFactory ?? $psr17Factory ?? Psr17FactoryDiscovery::findResponseFactory();
                $this->streamFactory = $this->streamFactory ?? $psr17Factory ?? Psr17FactoryDiscovery::findStreamFactory();
            } catch (NotFoundException $e) {
                throw new \LogicException('You cannot use the "Symfony\Component\HttpClient\HttplugClient" as no PSR-17 factories have been found. Try running "composer require nyholm/psr7".', 0, $e);
            }
        }

        $this->waitLoop = new HttplugWaitLoop($this->client, $this->promisePool, $this->responseFactory, $this->streamFactory);
    }

    /**
     * {@inheritdoc}
     */
    public function sendRequest(RequestInterface $request): Psr7ResponseInterface
    {
        try {
            return HttplugWaitLoop::createPsr7Response($this->responseFactory, $this->streamFactory, $this->client, $this->sendPsr7Request($request), true);
        } catch (TransportExceptionInterface $e) {
            throw new NetworkException($e->getMessage(), $request, $e);
        }
    }

    /**
     * {@inheritdoc}
     *
     * @return HttplugPromise
     */
    public function sendAsyncRequest(RequestInterface $request): Promise
    {
        if (!$promisePool = $this->promisePool) {
            throw new \LogicException(sprintf('You cannot use "%s()" as the "guzzlehttp/promises" package is not installed. Try running "composer require guzzlehttp/promises".', __METHOD__));
        }

        try {
            $response = $this->sendPsr7Request($request, true);
        } catch (NetworkException $e) {
            return new HttplugPromise(new RejectedPromise($e));
        }

        $waitLoop = $this->waitLoop;

        $promise = new GuzzlePromise(static function () use ($response, $waitLoop) {
            $waitLoop->wait($response);
        }, static function () use ($response, $promisePool) {
            $response->cancel();
            unset($promisePool[$response]);
        });

        $promisePool[$response] = [$request, $promise];

        return new HttplugPromise($promise);
    }

    /**
     * Resolves pending promises that complete before the timeouts are reached.
     *
     * When $maxDuration is null and $idleTimeout is reached, promises are rejected.
     *
     * @return int The number of remaining pending promises
     */
    public function wait(?float $maxDuration = null, ?float $idleTimeout = null): int
    {
        return $this->waitLoop->wait(null, $maxDuration, $idleTimeout);
    }

    /**
     * {@inheritdoc}
     */
    public function createRequest($method, $uri, array $headers = [], $body = null, $protocolVersion = '1.1'): RequestInterface
    {
        if ($this->responseFactory instanceof RequestFactoryInterface) {
            $request = $this->responseFactory->createRequest($method, $uri);
        } elseif (class_exists(Request::class)) {
            $request = new Request($method, $uri);
        } elseif (class_exists(Psr17FactoryDiscovery::class)) {
            $request = Psr17FactoryDiscovery::findRequestFactory()->createRequest($method, $uri);
        } else {
            throw new \LogicException(sprintf('You cannot use "%s()" as the "nyholm/psr7" package is not installed. Try running "composer require nyholm/psr7".', __METHOD__));
        }

        $request = $request
            ->withProtocolVersion($protocolVersion)
            ->withBody($this->createStream($body))
        ;

        foreach ($headers as $name => $value) {
            $request = $request->withAddedHeader($name, $value);
        }

        return $request;
    }

    /**
     * {@inheritdoc}
     */
    public function createStream($body = null): StreamInterface
    {
        if ($body instanceof StreamInterface) {
            return $body;
        }

        if (\is_string($body ?? '')) {
            $stream = $this->streamFactory->createStream($body ?? '');
        } elseif (\is_resource($body)) {
            $stream = $this->streamFactory->createStreamFromResource($body);
        } else {
            throw new \InvalidArgumentException(sprintf('"%s()" expects string, resource or StreamInterface, "%s" given.', __METHOD__, get_debug_type($body)));
        }

        if ($stream->isSeekable()) {
            $stream->seek(0);
        }

        return $stream;
    }

    /**
     * {@inheritdoc}
     */
    public function createUri($uri): UriInterface
    {
        if ($uri instanceof UriInterface) {
            return $uri;
        }

        if ($this->responseFactory instanceof UriFactoryInterface) {
            return $this->responseFactory->createUri($uri);
        }

        if (class_exists(Uri::class)) {
            return new Uri($uri);
        }

        if (class_exists(Psr17FactoryDiscovery::class)) {
            return Psr17FactoryDiscovery::findUrlFactory()->createUri($uri);
        }

        throw new \LogicException(sprintf('You cannot use "%s()" as the "nyholm/psr7" package is not installed. Try running "composer require nyholm/psr7".', __METHOD__));
    }

    public function __sleep(): array
    {
        throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
    }

    public function __wakeup()
    {
        throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
    }

    public function __destruct()
    {
        $this->wait();
    }

    public function reset()
    {
        if ($this->client instanceof ResetInterface) {
            $this->client->reset();
        }
    }

    private function sendPsr7Request(RequestInterface $request, ?bool $buffer = null): ResponseInterface
    {
        try {
            $body = $request->getBody();

            if ($body->isSeekable()) {
                $body->seek(0);
            }

            $options = [
                'headers' => $request->getHeaders(),
                'body' => $body->getContents(),
                'buffer' => $buffer,
            ];

            if ('1.0' === $request->getProtocolVersion()) {
                $options['http_version'] = '1.0';
            }

            return $this->client->request($request->getMethod(), (string) $request->getUri(), $options);
        } catch (\InvalidArgumentException $e) {
            throw new RequestException($e->getMessage(), $request, $e);
        } catch (TransportExceptionInterface $e) {
            throw new NetworkException($e->getMessage(), $request, $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\HttpClient\Internal;

/**
 * Cache for resolved DNS queries.
 *
 * @author Alexander M. Turek <me@derrabus.de>
 *
 * @internal
 */
final class DnsCache
{
    /**
     * Resolved hostnames (hostname => IP address).
     *
     * @var string[]
     */
    public $hostnames = [];

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

    /**
     * @var string[]
     */
    public $evictions = [];
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\HttpClient\Internal;

use Amp\CancellationToken;
use Amp\Deferred;
use Amp\Http\Client\Connection\ConnectionLimitingPool;
use Amp\Http\Client\Connection\DefaultConnectionFactory;
use Amp\Http\Client\InterceptedHttpClient;
use Amp\Http\Client\Interceptor\RetryRequests;
use Amp\Http\Client\PooledHttpClient;
use Amp\Http\Client\Request;
use Amp\Http\Client\Response;
use Amp\Http\Tunnel\Http1TunnelConnector;
use Amp\Http\Tunnel\Https1TunnelConnector;
use Amp\Promise;
use Amp\Socket\Certificate;
use Amp\Socket\ClientTlsContext;
use Amp\Socket\ConnectContext;
use Amp\Socket\Connector;
use Amp\Socket\DnsConnector;
use Amp\Socket\SocketAddress;
use Amp\Success;
use Psr\Log\LoggerInterface;

/**
 * Internal representation of the Amp client's state.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @internal
 */
final class AmpClientState extends ClientState
{
    public $dnsCache = [];
    public $responseCount = 0;
    public $pushedResponses = [];

    private $clients = [];
    private $clientConfigurator;
    private $maxHostConnections;
    private $maxPendingPushes;
    private $logger;

    public function __construct(?callable $clientConfigurator, int $maxHostConnections, int $maxPendingPushes, ?LoggerInterface &$logger)
    {
        $this->clientConfigurator = $clientConfigurator ?? static function (PooledHttpClient $client) {
            return new InterceptedHttpClient($client, new RetryRequests(2));
        };
        $this->maxHostConnections = $maxHostConnections;
        $this->maxPendingPushes = $maxPendingPushes;
        $this->logger = &$logger;
    }

    /**
     * @return Promise<Response>
     */
    public function request(array $options, Request $request, CancellationToken $cancellation, array &$info, \Closure $onProgress, &$handle): Promise
    {
        if ($options['proxy']) {
            if ($request->hasHeader('proxy-authorization')) {
                $options['proxy']['auth'] = $request->getHeader('proxy-authorization');
            }

            // Matching "no_proxy" should follow the behavior of curl
            $host = $request->getUri()->getHost();
            foreach ($options['proxy']['no_proxy'] as $rule) {
                $dotRule = '.'.ltrim($rule, '.');

                if ('*' === $rule || $host === $rule || substr($host, -\strlen($dotRule)) === $dotRule) {
                    $options['proxy'] = null;
                    break;
                }
            }
        }

        $request = clone $request;

        if ($request->hasHeader('proxy-authorization')) {
            $request->removeHeader('proxy-authorization');
        }

        if ($options['capture_peer_cert_chain']) {
            $info['peer_certificate_chain'] = [];
        }

        $request->addEventListener(new AmpListener($info, $options['peer_fingerprint']['pin-sha256'] ?? [], $onProgress, $handle));
        $request->setPushHandler(function ($request, $response) use ($options): Promise {
            return $this->handlePush($request, $response, $options);
        });

        ($request->hasHeader('content-length') ? new Success((int) $request->getHeader('content-length')) : $request->getBody()->getBodyLength())
            ->onResolve(static function ($e, $bodySize) use (&$info) {
                if (null !== $bodySize && 0 <= $bodySize) {
                    $info['upload_content_length'] = ((1 + $info['upload_content_length']) ?? 1) - 1 + $bodySize;
                }
            });

        [$client, $connector] = $this->getClient($options);
        $response = $client->request($request, $cancellation);
        $response->onResolve(static function ($e) use ($connector, &$handle) {
            if (null === $e) {
                $handle = $connector->handle;
            }
        });

        return $response;
    }

    private function getClient(array $options): array
    {
        $options = [
            'bindto' => $options['bindto'] ?: '0',
            'verify_peer' => $options['verify_peer'],
            'capath' => $options['capath'],
            'cafile' => $options['cafile'],
            'local_cert' => $options['local_cert'],
            'local_pk' => $options['local_pk'],
            'ciphers' => $options['ciphers'],
            'capture_peer_cert_chain' => $options['capture_peer_cert_chain'] || $options['peer_fingerprint'],
            'proxy' => $options['proxy'],
        ];

        $key = md5(serialize($options));

        if (isset($this->clients[$key])) {
            return $this->clients[$key];
        }

        $context = new ClientTlsContext('');
        $options['verify_peer'] || $context = $context->withoutPeerVerification();
        $options['cafile'] && $context = $context->withCaFile($options['cafile']);
        $options['capath'] && $context = $context->withCaPath($options['capath']);
        $options['local_cert'] && $context = $context->withCertificate(new Certificate($options['local_cert'], $options['local_pk']));
        $options['ciphers'] && $context = $context->withCiphers($options['ciphers']);
        $options['capture_peer_cert_chain'] && $context = $context->withPeerCapturing();

        $connector = $handleConnector = new class() implements Connector {
            public $connector;
            public $uri;
            public $handle;

            public function connect(string $uri, ?ConnectContext $context = null, ?CancellationToken $token = null): Promise
            {
                $result = $this->connector->connect($this->uri ?? $uri, $context, $token);
                $result->onResolve(function ($e, $socket) {
                    $this->handle = null !== $socket ? $socket->getResource() : false;
                });

                return $result;
            }
        };
        $connector->connector = new DnsConnector(new AmpResolver($this->dnsCache));

        $context = (new ConnectContext())
            ->withTcpNoDelay()
            ->withTlsContext($context);

        if ($options['bindto']) {
            if (file_exists($options['bindto'])) {
                $connector->uri = 'unix://'.$options['bindto'];
            } else {
                $context = $context->withBindTo($options['bindto']);
            }
        }

        if ($options['proxy']) {
            $proxyUrl = parse_url($options['proxy']['url']);
            $proxySocket = new SocketAddress($proxyUrl['host'], $proxyUrl['port']);
            $proxyHeaders = $options['proxy']['auth'] ? ['Proxy-Authorization' => $options['proxy']['auth']] : [];

            if ('ssl' === $proxyUrl['scheme']) {
                $connector = new Https1TunnelConnector($proxySocket, $context->getTlsContext(), $proxyHeaders, $connector);
            } else {
                $connector = new Http1TunnelConnector($proxySocket, $proxyHeaders, $connector);
            }
        }

        $maxHostConnections = 0 < $this->maxHostConnections ? $this->maxHostConnections : \PHP_INT_MAX;
        $pool = new DefaultConnectionFactory($connector, $context);
        $pool = ConnectionLimitingPool::byAuthority($maxHostConnections, $pool);

        return $this->clients[$key] = [($this->clientConfigurator)(new PooledHttpClient($pool)), $handleConnector];
    }

    private function handlePush(Request $request, Promise $response, array $options): Promise
    {
        $deferred = new Deferred();
        $authority = $request->getUri()->getAuthority();

        if ($this->maxPendingPushes <= \count($this->pushedResponses[$authority] ?? [])) {
            $fifoUrl = key($this->pushedResponses[$authority]);
            unset($this->pushedResponses[$authority][$fifoUrl]);
            $this->logger && $this->logger->debug(sprintf('Evicting oldest pushed response: "%s"', $fifoUrl));
        }

        $url = (string) $request->getUri();
        $this->logger && $this->logger->debug(sprintf('Queueing pushed response: "%s"', $url));
        $this->pushedResponses[$authority][] = [$url, $deferred, $request, $response, [
            'proxy' => $options['proxy'],
            'bindto' => $options['bindto'],
            'local_cert' => $options['local_cert'],
            'local_pk' => $options['local_pk'],
        ]];

        return $deferred->promise();
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\HttpClient\Internal;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @internal
 */
final class Canary
{
    private $canceller;

    public function __construct(\Closure $canceller)
    {
        $this->canceller = $canceller;
    }

    public function cancel()
    {
        if (($canceller = $this->canceller) instanceof \Closure) {
            $this->canceller = null;
            $canceller();
        }
    }

    public function __destruct()
    {
        $this->cancel();
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\HttpClient\Internal;

use Amp\Dns;
use Amp\Dns\Record;
use Amp\Promise;
use Amp\Success;

/**
 * Handles local overrides for the DNS resolver.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @internal
 */
class AmpResolver implements Dns\Resolver
{
    private $dnsMap;

    public function __construct(array &$dnsMap)
    {
        $this->dnsMap = &$dnsMap;
    }

    public function resolve(string $name, ?int $typeRestriction = null): Promise
    {
        $recordType = Record::A;
        $ip = $this->dnsMap[$name] ?? null;

        if (null !== $ip && str_contains($ip, ':')) {
            $recordType = Record::AAAA;
        }
        if (null === $ip || $recordType !== ($typeRestriction ?? $recordType)) {
            return Dns\resolver()->resolve($name, $typeRestriction);
        }

        return new Success([new Record($ip, $recordType, null)]);
    }

    public function query(string $name, int $type): Promise
    {
        $recordType = Record::A;
        $ip = $this->dnsMap[$name] ?? null;

        if (null !== $ip && str_contains($ip, ':')) {
            $recordType = Record::AAAA;
        }
        if (null === $ip || $recordType !== $type) {
            return Dns\resolver()->query($name, $type);
        }

        return new Success([new Record($ip, $recordType, 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\HttpClient\Internal;

use Amp\ByteStream\InputStream;
use Amp\ByteStream\ResourceInputStream;
use Amp\Http\Client\RequestBody;
use Amp\Promise;
use Amp\Success;
use Symfony\Component\HttpClient\Exception\TransportException;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @internal
 */
class AmpBody implements RequestBody, InputStream
{
    private $body;
    private $info;
    private $onProgress;
    private $offset = 0;
    private $length = -1;
    private $uploaded;

    public function __construct($body, &$info, \Closure $onProgress)
    {
        $this->body = $body;
        $this->info = &$info;
        $this->onProgress = $onProgress;

        if (\is_resource($body)) {
            $this->offset = ftell($body);
            $this->length = fstat($body)['size'];
            $this->body = new ResourceInputStream($body);
        } elseif (\is_string($body)) {
            $this->length = \strlen($body);
        }
    }

    public function createBodyStream(): InputStream
    {
        if (null !== $this->uploaded) {
            $this->uploaded = null;

            if (\is_string($this->body)) {
                $this->offset = 0;
            } elseif ($this->body instanceof ResourceInputStream) {
                fseek($this->body->getResource(), $this->offset);
            }
        }

        return $this;
    }

    public function getHeaders(): Promise
    {
        return new Success([]);
    }

    public function getBodyLength(): Promise
    {
        return new Success($this->length - $this->offset);
    }

    public function read(): Promise
    {
        $this->info['size_upload'] += $this->uploaded;
        $this->uploaded = 0;
        ($this->onProgress)();

        $chunk = $this->doRead();
        $chunk->onResolve(function ($e, $data) {
            if (null !== $data) {
                $this->uploaded = \strlen($data);
            } else {
                $this->info['upload_content_length'] = $this->info['size_upload'];
            }
        });

        return $chunk;
    }

    public static function rewind(RequestBody $body): RequestBody
    {
        if (!$body instanceof self) {
            return $body;
        }

        $body->uploaded = null;

        if ($body->body instanceof ResourceInputStream) {
            fseek($body->body->getResource(), $body->offset);

            return new $body($body->body, $body->info, $body->onProgress);
        }

        if (\is_string($body->body)) {
            $body->offset = 0;
        }

        return $body;
    }

    private function doRead(): Promise
    {
        if ($this->body instanceof ResourceInputStream) {
            return $this->body->read();
        }

        if (null === $this->offset || !$this->length) {
            return new Success();
        }

        if (\is_string($this->body)) {
            $this->offset = null;

            return new Success($this->body);
        }

        if ('' === $data = ($this->body)(16372)) {
            $this->offset = null;

            return new Success();
        }

        if (!\is_string($data)) {
            throw new TransportException(sprintf('Return value of the "body" option callback must be string, "%s" returned.', get_debug_type($data)));
        }

        return new Success($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\HttpClient\Internal;

use Amp\Http\Client\Connection\Stream;
use Amp\Http\Client\EventListener;
use Amp\Http\Client\Request;
use Amp\Promise;
use Amp\Success;
use Symfony\Component\HttpClient\Exception\TransportException;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @internal
 */
class AmpListener implements EventListener
{
    private $info;
    private $pinSha256;
    private $onProgress;
    private $handle;

    public function __construct(array &$info, array $pinSha256, \Closure $onProgress, &$handle)
    {
        $info += [
            'connect_time' => 0.0,
            'pretransfer_time' => 0.0,
            'starttransfer_time' => 0.0,
            'total_time' => 0.0,
            'namelookup_time' => 0.0,
            'primary_ip' => '',
            'primary_port' => 0,
        ];

        $this->info = &$info;
        $this->pinSha256 = $pinSha256;
        $this->onProgress = $onProgress;
        $this->handle = &$handle;
    }

    public function startRequest(Request $request): Promise
    {
        $this->info['start_time'] = $this->info['start_time'] ?? microtime(true);
        ($this->onProgress)();

        return new Success();
    }

    public function startDnsResolution(Request $request): Promise
    {
        ($this->onProgress)();

        return new Success();
    }

    public function startConnectionCreation(Request $request): Promise
    {
        ($this->onProgress)();

        return new Success();
    }

    public function startTlsNegotiation(Request $request): Promise
    {
        ($this->onProgress)();

        return new Success();
    }

    public function startSendingRequest(Request $request, Stream $stream): Promise
    {
        $host = $stream->getRemoteAddress()->getHost();
        $this->info['primary_ip'] = $host;

        if (false !== strpos($host, ':')) {
            $host = '['.$host.']';
        }

        $this->info['primary_port'] = $stream->getRemoteAddress()->getPort();
        $this->info['pretransfer_time'] = microtime(true) - $this->info['start_time'];
        $this->info['debug'] .= sprintf("* Connected to %s (%s) port %d\n", $request->getUri()->getHost(), $host, $this->info['primary_port']);

        if ((isset($this->info['peer_certificate_chain']) || $this->pinSha256) && null !== $tlsInfo = $stream->getTlsInfo()) {
            foreach ($tlsInfo->getPeerCertificates() as $cert) {
                $this->info['peer_certificate_chain'][] = openssl_x509_read($cert->toPem());
            }

            if ($this->pinSha256) {
                $pin = openssl_pkey_get_public($this->info['peer_certificate_chain'][0]);
                $pin = openssl_pkey_get_details($pin)['key'];
                $pin = \array_slice(explode("\n", $pin), 1, -2);
                $pin = base64_decode(implode('', $pin));
                $pin = base64_encode(hash('sha256', $pin, true));

                if (!\in_array($pin, $this->pinSha256, true)) {
                    throw new TransportException(sprintf('SSL public key does not match pinned public key for "%s".', $this->info['url']));
                }
            }
        }
        ($this->onProgress)();

        $uri = $request->getUri();
        $requestUri = $uri->getPath() ?: '/';

        if ('' !== $query = $uri->getQuery()) {
            $requestUri .= '?'.$query;
        }

        if ('CONNECT' === $method = $request->getMethod()) {
            $requestUri = $uri->getHost().': '.($uri->getPort() ?? ('https' === $uri->getScheme() ? 443 : 80));
        }

        $this->info['debug'] .= sprintf("> %s %s HTTP/%s \r\n", $method, $requestUri, $request->getProtocolVersions()[0]);

        foreach ($request->getRawHeaders() as [$name, $value]) {
            $this->info['debug'] .= $name.': '.$value."\r\n";
        }
        $this->info['debug'] .= "\r\n";

        return new Success();
    }

    public function completeSendingRequest(Request $request, Stream $stream): Promise
    {
        ($this->onProgress)();

        return new Success();
    }

    public function startReceivingResponse(Request $request, Stream $stream): Promise
    {
        $this->info['starttransfer_time'] = microtime(true) - $this->info['start_time'];
        ($this->onProgress)();

        return new Success();
    }

    public function completeReceivingResponse(Request $request, Stream $stream): Promise
    {
        $this->handle = null;
        ($this->onProgress)();

        return new Success();
    }

    public function completeDnsResolution(Request $request): Promise
    {
        $this->info['namelookup_time'] = microtime(true) - $this->info['start_time'];
        ($this->onProgress)();

        return new Success();
    }

    public function completeConnectionCreation(Request $request): Promise
    {
        $this->info['connect_time'] = microtime(true) - $this->info['start_time'];
        ($this->onProgress)();

        return new Success();
    }

    public function completeTlsNegotiation(Request $request): Promise
    {
        ($this->onProgress)();

        return new Success();
    }

    public function abort(Request $request, \Throwable $cause): Promise
    {
        return new 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 Symfony\Component\HttpClient\Internal;

/**
 * Internal representation of the native client's state.
 *
 * @author Alexander M. Turek <me@derrabus.de>
 *
 * @internal
 */
final class NativeClientState extends ClientState
{
    /** @var int */
    public $id;
    /** @var int */
    public $maxHostConnections = \PHP_INT_MAX;
    /** @var int */
    public $responseCount = 0;
    /** @var string[] */
    public $dnsCache = [];
    /** @var bool */
    public $sleep = false;
    /** @var int[] */
    public $hosts = [];

    public function __construct()
    {
        $this->id = random_int(\PHP_INT_MIN, \PHP_INT_MAX);
    }

    public function reset()
    {
        $this->responseCount = 0;
        $this->dnsCache = [];
        $this->hosts = [];
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\HttpClient\Internal;

use Symfony\Component\HttpClient\Response\CurlResponse;

/**
 * A pushed response with its request headers.
 *
 * @author Alexander M. Turek <me@derrabus.de>
 *
 * @internal
 */
final class PushedResponse
{
    public $response;

    /** @var string[] */
    public $requestHeaders;

    public $parentOptions = [];

    public $handle;

    public function __construct(CurlResponse $response, array $requestHeaders, array $parentOptions, $handle)
    {
        $this->response = $response;
        $this->requestHeaders = $requestHeaders;
        $this->parentOptions = $parentOptions;
        $this->handle = $handle;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\HttpClient\Internal;

use Psr\Log\LoggerInterface;
use Symfony\Component\HttpClient\Response\CurlResponse;

/**
 * Internal representation of the cURL client's state.
 *
 * @author Alexander M. Turek <me@derrabus.de>
 *
 * @internal
 */
final class CurlClientState extends ClientState
{
    /** @var \CurlMultiHandle|resource|null */
    public $handle;
    /** @var \CurlShareHandle|resource|null */
    public $share;
    /** @var PushedResponse[] */
    public $pushedResponses = [];
    /** @var DnsCache */
    public $dnsCache;
    /** @var float[] */
    public $pauseExpiries = [];
    public $execCounter = \PHP_INT_MIN;
    /** @var LoggerInterface|null */
    public $logger;
    public $performing = false;

    public static $curlVersion;

    public function __construct(int $maxHostConnections, int $maxPendingPushes)
    {
        self::$curlVersion = self::$curlVersion ?? curl_version();

        $this->handle = curl_multi_init();
        $this->dnsCache = new DnsCache();
        $this->reset();

        // Don't enable HTTP/1.1 pipelining: it forces responses to be sent in order
        if (\defined('CURLPIPE_MULTIPLEX')) {
            curl_multi_setopt($this->handle, \CURLMOPT_PIPELINING, \CURLPIPE_MULTIPLEX);
        }
        if (\defined('CURLMOPT_MAX_HOST_CONNECTIONS') && 0 < $maxHostConnections) {
            $maxHostConnections = curl_multi_setopt($this->handle, \CURLMOPT_MAX_HOST_CONNECTIONS, $maxHostConnections) ? 4294967295 : $maxHostConnections;
        }
        if (\defined('CURLMOPT_MAXCONNECTS') && 0 < $maxHostConnections) {
            curl_multi_setopt($this->handle, \CURLMOPT_MAXCONNECTS, $maxHostConnections);
        }

        // Skip configuring HTTP/2 push when it's unsupported or buggy, see https://bugs.php.net/77535
        if (0 >= $maxPendingPushes || \PHP_VERSION_ID < 70217 || (\PHP_VERSION_ID >= 70300 && \PHP_VERSION_ID < 70304)) {
            return;
        }

        // HTTP/2 push crashes before curl 7.61
        if (!\defined('CURLMOPT_PUSHFUNCTION') || 0x073D00 > self::$curlVersion['version_number'] || !(\CURL_VERSION_HTTP2 & self::$curlVersion['features'])) {
            return;
        }

        // Clone to prevent a circular reference
        $multi = clone $this;
        $multi->handle = null;
        $multi->share = null;
        $multi->pushedResponses = &$this->pushedResponses;
        $multi->logger = &$this->logger;
        $multi->handlesActivity = &$this->handlesActivity;
        $multi->openHandles = &$this->openHandles;

        curl_multi_setopt($this->handle, \CURLMOPT_PUSHFUNCTION, static function ($parent, $pushed, array $requestHeaders) use ($multi, $maxPendingPushes) {
            return $multi->handlePush($parent, $pushed, $requestHeaders, $maxPendingPushes);
        });
    }

    public function reset()
    {
        foreach ($this->pushedResponses as $url => $response) {
            $this->logger && $this->logger->debug(sprintf('Unused pushed response: "%s"', $url));
            curl_multi_remove_handle($this->handle, $response->handle);
            curl_close($response->handle);
        }

        $this->pushedResponses = [];
        $this->dnsCache->evictions = $this->dnsCache->evictions ?: $this->dnsCache->removals;
        $this->dnsCache->removals = $this->dnsCache->hostnames = [];

        $this->share = curl_share_init();

        curl_share_setopt($this->share, \CURLSHOPT_SHARE, \CURL_LOCK_DATA_DNS);
        curl_share_setopt($this->share, \CURLSHOPT_SHARE, \CURL_LOCK_DATA_SSL_SESSION);

        if (\defined('CURL_LOCK_DATA_CONNECT') && \PHP_VERSION_ID >= 80000) {
            curl_share_setopt($this->share, \CURLSHOPT_SHARE, \CURL_LOCK_DATA_CONNECT);
        }
    }

    private function handlePush($parent, $pushed, array $requestHeaders, int $maxPendingPushes): int
    {
        $headers = [];
        $origin = curl_getinfo($parent, \CURLINFO_EFFECTIVE_URL);

        foreach ($requestHeaders as $h) {
            if (false !== $i = strpos($h, ':', 1)) {
                $headers[substr($h, 0, $i)][] = substr($h, 1 + $i);
            }
        }

        if (!isset($headers[':method']) || !isset($headers[':scheme']) || !isset($headers[':authority']) || !isset($headers[':path'])) {
            $this->logger && $this->logger->debug(sprintf('Rejecting pushed response from "%s": pushed headers are invalid', $origin));

            return \CURL_PUSH_DENY;
        }

        $url = $headers[':scheme'][0].'://'.$headers[':authority'][0];

        // curl before 7.65 doesn't validate the pushed ":authority" header,
        // but this is a MUST in the HTTP/2 RFC; let's restrict pushes to the original host,
        // ignoring domains mentioned as alt-name in the certificate for now (same as curl).
        if (!str_starts_with($origin, $url.'/')) {
            $this->logger && $this->logger->debug(sprintf('Rejecting pushed response from "%s": server is not authoritative for "%s"', $origin, $url));

            return \CURL_PUSH_DENY;
        }

        if ($maxPendingPushes <= \count($this->pushedResponses)) {
            $fifoUrl = key($this->pushedResponses);
            unset($this->pushedResponses[$fifoUrl]);
            $this->logger && $this->logger->debug(sprintf('Evicting oldest pushed response: "%s"', $fifoUrl));
        }

        $url .= $headers[':path'][0];
        $this->logger && $this->logger->debug(sprintf('Queueing pushed response: "%s"', $url));

        $this->pushedResponses[$url] = new PushedResponse(new CurlResponse($this, $pushed), $headers, $this->openHandles[(int) $parent][1] ?? [], $pushed);

        return \CURL_PUSH_OK;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\HttpClient\Internal;

/**
 * Internal representation of the client state.
 *
 * @author Alexander M. Turek <me@derrabus.de>
 *
 * @internal
 */
class ClientState
{
    public $handlesActivity = [];
    public $openHandles = [];
    public $lastTimeout;
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\HttpClient\Internal;

use Http\Client\Exception\NetworkException;
use Http\Promise\Promise;
use Psr\Http\Message\RequestInterface as Psr7RequestInterface;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface as Psr7ResponseInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Symfony\Component\HttpClient\Response\StreamableInterface;
use Symfony\Component\HttpClient\Response\StreamWrapper;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @internal
 */
final class HttplugWaitLoop
{
    private $client;
    private $promisePool;
    private $responseFactory;
    private $streamFactory;

    /**
     * @param \SplObjectStorage<ResponseInterface, array{Psr7RequestInterface, Promise}>|null $promisePool
     */
    public function __construct(HttpClientInterface $client, ?\SplObjectStorage $promisePool, ResponseFactoryInterface $responseFactory, StreamFactoryInterface $streamFactory)
    {
        $this->client = $client;
        $this->promisePool = $promisePool;
        $this->responseFactory = $responseFactory;
        $this->streamFactory = $streamFactory;
    }

    public function wait(?ResponseInterface $pendingResponse, ?float $maxDuration = null, ?float $idleTimeout = null): int
    {
        if (!$this->promisePool) {
            return 0;
        }

        $guzzleQueue = \GuzzleHttp\Promise\Utils::queue();

        if (0.0 === $remainingDuration = $maxDuration) {
            $idleTimeout = 0.0;
        } elseif (null !== $maxDuration) {
            $startTime = microtime(true);
            $idleTimeout = max(0.0, min($maxDuration / 5, $idleTimeout ?? $maxDuration));
        }

        do {
            foreach ($this->client->stream($this->promisePool, $idleTimeout) as $response => $chunk) {
                try {
                    if (null !== $maxDuration && $chunk->isTimeout()) {
                        goto check_duration;
                    }

                    if ($chunk->isFirst()) {
                        // Deactivate throwing on 3/4/5xx
                        $response->getStatusCode();
                    }

                    if (!$chunk->isLast()) {
                        goto check_duration;
                    }

                    if ([, $promise] = $this->promisePool[$response] ?? null) {
                        unset($this->promisePool[$response]);
                        $promise->resolve(self::createPsr7Response($this->responseFactory, $this->streamFactory, $this->client, $response, true));
                    }
                } catch (\Exception $e) {
                    if ([$request, $promise] = $this->promisePool[$response] ?? null) {
                        unset($this->promisePool[$response]);

                        if ($e instanceof TransportExceptionInterface) {
                            $e = new NetworkException($e->getMessage(), $request, $e);
                        }

                        $promise->reject($e);
                    }
                }

                $guzzleQueue->run();

                if ($pendingResponse === $response) {
                    return $this->promisePool->count();
                }

                check_duration:
                if (null !== $maxDuration && $idleTimeout && $idleTimeout > $remainingDuration = max(0.0, $maxDuration - microtime(true) + $startTime)) {
                    $idleTimeout = $remainingDuration / 5;
                    break;
                }
            }

            if (!$count = $this->promisePool->count()) {
                return 0;
            }
        } while (null === $maxDuration || 0 < $remainingDuration);

        return $count;
    }

    public static function createPsr7Response(ResponseFactoryInterface $responseFactory, StreamFactoryInterface $streamFactory, HttpClientInterface $client, ResponseInterface $response, bool $buffer): Psr7ResponseInterface
    {
        $responseParameters = [$response->getStatusCode()];

        foreach ($response->getInfo('response_headers') as $h) {
            if (11 <= \strlen($h) && '/' === $h[4] && preg_match('#^HTTP/\d+(?:\.\d+)? (?:\d\d\d) (.+)#', $h, $m)) {
                $responseParameters[1] = $m[1];
            }
        }

        $psrResponse = $responseFactory->createResponse(...$responseParameters);

        foreach ($response->getHeaders(false) as $name => $values) {
            foreach ($values as $value) {
                try {
                    $psrResponse = $psrResponse->withAddedHeader($name, $value);
                } catch (\InvalidArgumentException $e) {
                    // ignore invalid header
                }
            }
        }

        if ($response instanceof StreamableInterface) {
            $body = $streamFactory->createStreamFromResource($response->toStream(false));
        } elseif (!$buffer) {
            $body = $streamFactory->createStreamFromResource(StreamWrapper::createResource($response, $client));
        } else {
            $body = $streamFactory->createStream($response->getContent(false));
        }

        if ($body->isSeekable()) {
            $body->seek(0);
        }

        return $psrResponse->withBody($body);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\HttpClient;

use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpClient\Exception\TransportException;
use Symfony\Component\HttpClient\Response\AsyncContext;
use Symfony\Component\HttpClient\Response\AsyncResponse;
use Symfony\Component\HttpFoundation\IpUtils;
use Symfony\Contracts\HttpClient\ChunkInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\Service\ResetInterface;

/**
 * Decorator that blocks requests to private networks by default.
 *
 * @author Hallison Boaventura <hallisonboaventura@gmail.com>
 * @author Nicolas Grekas <p@tchwork.com>
 */
final class NoPrivateNetworkHttpClient implements HttpClientInterface, LoggerAwareInterface, ResetInterface
{
    use HttpClientTrait;
    use AsyncDecoratorTrait;

    private const PRIVATE_SUBNETS = [
        '127.0.0.0/8',
        '10.0.0.0/8',
        '192.168.0.0/16',
        '172.16.0.0/12',
        '169.254.0.0/16',
        '0.0.0.0/8',
        '240.0.0.0/4',
        '::1/128',
        'fc00::/7',
        'fe80::/10',
        '::ffff:0:0/96',
        '::/128',
    ];

    private $defaultOptions = self::OPTIONS_DEFAULTS;
    private $client;
    private $subnets;
    private $ipFlags;
    private $dnsCache;

    /**
     * @param string|array|null $subnets String or array of subnets using CIDR notation that should be considered private.
     *                                   If null is passed, the standard private subnets will be used.
     */
    public function __construct(HttpClientInterface $client, $subnets = null)
    {
        if (!(\is_array($subnets) || \is_string($subnets) || null === $subnets)) {
            throw new \TypeError(sprintf('Argument 2 passed to "%s()" must be of the type array, string or null. "%s" given.', __METHOD__, get_debug_type($subnets)));
        }

        if (!class_exists(IpUtils::class)) {
            throw new \LogicException(sprintf('You cannot use "%s" if the HttpFoundation component is not installed. Try running "composer require symfony/http-foundation".', __CLASS__));
        }

        if (null === $subnets) {
            $ipFlags = \FILTER_FLAG_IPV4 | \FILTER_FLAG_IPV6;
        } else {
            $ipFlags = 0;
            foreach ((array) $subnets as $subnet) {
                $ipFlags |= str_contains($subnet, ':') ? \FILTER_FLAG_IPV6 : \FILTER_FLAG_IPV4;
            }
        }

        if (!\defined('STREAM_PF_INET6')) {
            $ipFlags &= ~\FILTER_FLAG_IPV6;
        }

        $this->client = $client;
        $this->subnets = null !== $subnets ? (array) $subnets : null;
        $this->ipFlags = $ipFlags;
        $this->dnsCache = new \ArrayObject();
    }

    /**
     * {@inheritdoc}
     */
    public function request(string $method, string $url, array $options = []): ResponseInterface
    {
        [$url, $options] = self::prepareRequest($method, $url, $options, $this->defaultOptions, true);

        $redirectHeaders = parse_url($url['authority']);
        $host = $redirectHeaders['host'];
        $url = implode('', $url);
        $dnsCache = $this->dnsCache;

        $ip = self::dnsResolve($dnsCache, $host, $this->ipFlags, $options);
        self::ipCheck($ip, $this->subnets, $this->ipFlags, $host, $url);

        $onProgress = $options['on_progress'] ?? null;
        $subnets = $this->subnets;
        $ipFlags = $this->ipFlags;
        $lastPrimaryIp = '';

        $options['on_progress'] = static function (int $dlNow, int $dlSize, array $info) use ($onProgress, $subnets, $ipFlags, &$lastPrimaryIp): void {
            if (!\in_array($info['primary_ip'] ?? '', ['', $lastPrimaryIp], true)) {
                self::ipCheck($info['primary_ip'], $subnets, $ipFlags, null, $info['url']);
                $lastPrimaryIp = $info['primary_ip'];
            }

            null !== $onProgress && $onProgress($dlNow, $dlSize, $info);
        };

        if (0 >= $maxRedirects = $options['max_redirects']) {
            return new AsyncResponse($this->client, $method, $url, $options);
        }

        $options['max_redirects'] = 0;
        $redirectHeaders['with_auth'] = $redirectHeaders['no_auth'] = $options['headers'];

        if (isset($options['normalized_headers']['host']) || isset($options['normalized_headers']['authorization']) || isset($options['normalized_headers']['cookie'])) {
            $redirectHeaders['no_auth'] = array_filter($redirectHeaders['no_auth'], static function ($h) {
                return 0 !== stripos($h, 'Host:') && 0 !== stripos($h, 'Authorization:') && 0 !== stripos($h, 'Cookie:');
            });
        }

        return new AsyncResponse($this->client, $method, $url, $options, static function (ChunkInterface $chunk, AsyncContext $context) use (&$method, &$options, $maxRedirects, &$redirectHeaders, $subnets, $ipFlags, $dnsCache): \Generator {
            if (null !== $chunk->getError() || $chunk->isTimeout() || !$chunk->isFirst()) {
                yield $chunk;

                return;
            }

            $statusCode = $context->getStatusCode();

            if ($statusCode < 300 || 400 <= $statusCode || null === $url = $context->getInfo('redirect_url')) {
                $context->passthru();

                yield $chunk;

                return;
            }

            $host = parse_url($url, \PHP_URL_HOST);
            $ip = self::dnsResolve($dnsCache, $host, $ipFlags, $options);
            self::ipCheck($ip, $subnets, $ipFlags, $host, $url);

            // Do like curl and browsers: turn POST to GET on 301, 302 and 303
            if (303 === $statusCode || 'POST' === $method && \in_array($statusCode, [301, 302], true)) {
                $method = 'HEAD' === $method ? 'HEAD' : 'GET';
                unset($options['body'], $options['json']);

                if (isset($options['normalized_headers']['content-length']) || isset($options['normalized_headers']['content-type']) || isset($options['normalized_headers']['transfer-encoding'])) {
                    $filterContentHeaders = static function ($h) {
                        return 0 !== stripos($h, 'Content-Length:') && 0 !== stripos($h, 'Content-Type:') && 0 !== stripos($h, 'Transfer-Encoding:');
                    };
                    $options['header'] = array_filter($options['header'], $filterContentHeaders);
                    $redirectHeaders['no_auth'] = array_filter($redirectHeaders['no_auth'], $filterContentHeaders);
                    $redirectHeaders['with_auth'] = array_filter($redirectHeaders['with_auth'], $filterContentHeaders);
                }
            }

            // Authorization and Cookie headers MUST NOT follow except for the initial host name
            $options['headers'] = $redirectHeaders['host'] === $host ? $redirectHeaders['with_auth'] : $redirectHeaders['no_auth'];

            static $redirectCount = 0;
            $context->setInfo('redirect_count', ++$redirectCount);

            $context->replaceRequest($method, $url, $options);

            if ($redirectCount >= $maxRedirects) {
                $context->passthru();
            }
        });
    }

    /**
     * {@inheritdoc}
     */
    public function setLogger(LoggerInterface $logger): void
    {
        if ($this->client instanceof LoggerAwareInterface) {
            $this->client->setLogger($logger);
        }
    }

    /**
     * {@inheritdoc}
     */
    public function withOptions(array $options): self
    {
        $clone = clone $this;
        $clone->client = $this->client->withOptions($options);
        $clone->defaultOptions = self::mergeDefaultOptions($options, $this->defaultOptions);

        return $clone;
    }

    public function reset()
    {
        $this->dnsCache->exchangeArray([]);

        if ($this->client instanceof ResetInterface) {
            $this->client->reset();
        }
    }

    private static function dnsResolve(\ArrayObject $dnsCache, string $host, int $ipFlags, array &$options): string
    {
        if ($ip = filter_var(trim($host, '[]'), \FILTER_VALIDATE_IP) ?: $options['resolve'][$host] ?? false) {
            return $ip;
        }

        if ($dnsCache->offsetExists($host)) {
            return $dnsCache[$host];
        }

        if ((\FILTER_FLAG_IPV4 & $ipFlags) && $ip = gethostbynamel($host)) {
            return $options['resolve'][$host] = $dnsCache[$host] = $ip[0];
        }

        if (!(\FILTER_FLAG_IPV6 & $ipFlags)) {
            return $host;
        }

        if ($ip = dns_get_record($host, \DNS_AAAA)) {
            $ip = $ip[0]['ipv6'];
        } elseif (extension_loaded('sockets')) {
            if (!$info = socket_addrinfo_lookup($host, 0, ['ai_socktype' => \SOCK_STREAM, 'ai_family' => \AF_INET6])) {
                return $host;
            }

            $ip = socket_addrinfo_explain($info[0])['ai_addr']['sin6_addr'];
        } elseif ('localhost' === $host || 'localhost.' === $host) {
            $ip = '::1';
        } else {
            return $host;
        }

        return $options['resolve'][$host] = $dnsCache[$host] = $ip;
    }

    private static function ipCheck(string $ip, ?array $subnets, int $ipFlags, ?string $host, string $url): void
    {
        if (null === $subnets) {
            // Quick check, but not reliable enough, see https://github.com/php/php-src/issues/16944
            $ipFlags |= \FILTER_FLAG_NO_PRIV_RANGE | \FILTER_FLAG_NO_RES_RANGE;
        }

        if (false !== filter_var($ip, \FILTER_VALIDATE_IP, $ipFlags) && !IpUtils::checkIp($ip, $subnets ?? self::PRIVATE_SUBNETS)) {
            return;
        }

        if (null !== $host) {
            $type = 'Host';
        } else {
            $host = $ip;
            $type = 'IP';
        }

        throw new TransportException($type.\sprintf(' "%s" is blocked for "%s".', $host, $url));
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\HttpClient;

use Symfony\Component\HttpClient\Response\AsyncResponse;
use Symfony\Component\HttpClient\Response\ResponseStream;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\HttpClient\ResponseStreamInterface;

/**
 * Eases with processing responses while streaming them.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
trait AsyncDecoratorTrait
{
    use DecoratorTrait;

    /**
     * {@inheritdoc}
     *
     * @return AsyncResponse
     */
    abstract public function request(string $method, string $url, array $options = []): ResponseInterface;

    /**
     * {@inheritdoc}
     */
    public function stream($responses, ?float $timeout = null): ResponseStreamInterface
    {
        if ($responses instanceof AsyncResponse) {
            $responses = [$responses];
        } elseif (!is_iterable($responses)) {
            throw new \TypeError(sprintf('"%s()" expects parameter 1 to be an iterable of AsyncResponse objects, "%s" given.', __METHOD__, get_debug_type($responses)));
        }

        return new ResponseStream(AsyncResponse::stream($responses, $timeout, static::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.
 */

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 (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 (!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.
 */

namespace Symfony\Polyfill\Intl\Grapheme;

\define('SYMFONY_GRAPHEME_CLUSTER_RX', ((float) \PCRE_VERSION < 10 ? (float) \PCRE_VERSION >= 8.32 : (float) \PCRE_VERSION >= 10.39) ? '\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
 *
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @internal
 */
final class Grapheme
{
    // (CRLF|([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{200C}\x{200D}]|[ᆨ-ᇹ]+|[ᄀ-ᅟ]*(?:[가개갸걔거게겨계고과괘괴교구궈궤귀규그긔기까깨꺄꺠꺼께껴꼐꼬꽈꽤꾀꾜꾸꿔꿰뀌뀨끄끠끼나내냐냬너네녀녜노놔놰뇌뇨누눠눼뉘뉴느늬니다대댜댸더데뎌뎨도돠돼되됴두둬뒈뒤듀드듸디따때땨떄떠떼뗘뗴또똬뙈뙤뚀뚜뚸뛔뛰뜌뜨띄띠라래랴럐러레려례로롸뢔뢰료루뤄뤠뤼류르릐리마매먀먜머메며몌모뫄뫠뫼묘무뭐뭬뮈뮤므믜미바배뱌뱨버베벼볘보봐봬뵈뵤부붜붸뷔뷰브븨비빠빼뺘뺴뻐뻬뼈뼤뽀뽜뽸뾔뾰뿌뿨쀄쀠쀼쁘쁴삐사새샤섀서세셔셰소솨쇄쇠쇼수숴쉐쉬슈스싀시싸쌔쌰썌써쎄쎠쎼쏘쏴쐐쐬쑈쑤쒀쒜쒸쓔쓰씌씨아애야얘어에여예오와왜외요우워웨위유으의이자재쟈쟤저제져졔조좌좨죄죠주줘줴쥐쥬즈즤지짜째쨔쨰쩌쩨쪄쪠쪼쫘쫴쬐쬬쭈쭤쮀쮜쮸쯔쯰찌차채챠챼처체쳐쳬초촤쵀최쵸추춰췌취츄츠츼치카캐캬컈커케켜켸코콰쾌쾨쿄쿠쿼퀘퀴큐크킈키타태탸턔터테텨톄토톼퇘퇴툐투퉈퉤튀튜트틔티파패퍄퍠퍼페펴폐포퐈퐤푀표푸풔풰퓌퓨프픠피하해햐햬허헤혀혜호화홰회효후훠훼휘휴흐희히]?[ᅠ-ᆢ]+|[가-힣])[ᆨ-ᇹ]*|[ᄀ-ᅟ]+|[^\p{Cc}\p{Cf}\p{Zl}\p{Zp}])[\p{Mn}\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{200D}\x{1D165}\x{1D16E}-\x{1D172}]*|[\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", 'ι'],
    ];

    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(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) {
            if (80000 > \PHP_VERSION_ID) {
                return false;
            }

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

    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;
        if (!preg_match('/./us', $s)) {
            return false;
        }
        if ($offset > 0) {
            $s = self::grapheme_substr($s, $offset);
        } elseif ($offset < 0) {
            if (2 > $mode) {
                $offset += self::grapheme_strlen($s);
                $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;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) 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 (!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, $length = 1) { return p\Grapheme::grapheme_str_split($string, $length); }
}
<?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

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

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <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';

    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 = iconv($fromEncoding, 'UTF-8//IGNORE', $s);
            }

            return preg_replace_callback('/[\x80-\xFF]+/', [__CLASS__, 'html_encoding_callback'], $s);
        }

        if ('HTML-ENTITIES' === $fromEncoding) {
            $s = html_entity_decode($s, \ENT_COMPAT, 'UTF-8');
            $fromEncoding = 'UTF-8';
        }

        return iconv($fromEncoding, $toEncoding.'//IGNORE', $s);
    }

    public static function mb_convert_variables($toEncoding, $fromEncoding, &...$vars)
    {
        $ok = true;
        array_walk_recursive($vars, 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 = @iconv('UTF-8', 'UTF-8//IGNORE', $s);
            }
        } else {
            $s = iconv($encoding, 'UTF-8//IGNORE', $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]+))(?!&);?/', 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 iconv('UTF-8', $encoding.'//IGNORE', $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 = @iconv('UTF-8', 'UTF-8//IGNORE', $s);
            }
        } else {
            $s = iconv($encoding, 'UTF-8//IGNORE', $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 iconv('UTF-8', $encoding.'//IGNORE', $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 = @iconv('UTF-8', 'UTF-8//IGNORE', $s);
            }
        } else {
            $s = iconv($encoding, 'UTF-8//IGNORE', $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 iconv('UTF-8', $encoding.'//IGNORE', $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);
        }

        return @iconv_strlen($s, $encoding);
    }

    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 = iconv($encoding, 'UTF-8//IGNORE', $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;
    }

    public static function mb_str_pad(string $string, int $length, string $pad_string = ' ', int $pad_type = \STR_PAD_RIGHT, ?string $encoding = null): string
    {
        if (!\in_array($pad_type, [\STR_PAD_RIGHT, \STR_PAD_LEFT, \STR_PAD_BOTH], true)) {
            throw new \ValueError('mb_str_pad(): Argument #4 ($pad_type) must be STR_PAD_LEFT, STR_PAD_RIGHT, or STR_PAD_BOTH');
        }

        if (null === $encoding) {
            $encoding = self::mb_internal_encoding();
        } else {
            self::assertEncoding($encoding, 'mb_str_pad(): Argument #5 ($encoding) must be a valid encoding, "%s" given');
        }

        if (self::mb_strlen($pad_string, $encoding) <= 0) {
            throw new \ValueError('mb_str_pad(): Argument #3 ($pad_string) must be a non-empty string');
        }

        $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);
        }
    }

    public static function mb_ucfirst(string $string, ?string $encoding = null): string
    {
        if (null === $encoding) {
            $encoding = self::mb_internal_encoding();
        } else {
            self::assertEncoding($encoding, 'mb_ucfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given');
        }

        $firstChar = mb_substr($string, 0, 1, $encoding);
        $firstChar = mb_convert_case($firstChar, \MB_CASE_TITLE, $encoding);

        return $firstChar.mb_substr($string, 1, null, $encoding);
    }

    public static function mb_lcfirst(string $string, ?string $encoding = null): string
    {
        if (null === $encoding) {
            $encoding = self::mb_internal_encoding();
        } else {
            self::assertEncoding($encoding, 'mb_lcfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given');
        }

        $firstChar = mb_substr($string, 0, 1, $encoding);
        $firstChar = mb_convert_case($firstChar, \MB_CASE_LOWER, $encoding);

        return $firstChar.mb_substr($string, 1, null, $encoding);
    }

    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';
        }

        return $encoding;
    }

    public static function mb_trim(string $string, ?string $characters = null, ?string $encoding = null): string
    {
        return self::mb_internal_trim('{^[%s]+|[%1$s]+$}Du', $string, $characters, $encoding, __FUNCTION__);
    }

    public static function mb_ltrim(string $string, ?string $characters = null, ?string $encoding = null): string
    {
        return self::mb_internal_trim('{^[%s]+}Du', $string, $characters, $encoding, __FUNCTION__);
    }

    public static function mb_rtrim(string $string, ?string $characters = null, ?string $encoding = null): string
    {
        return self::mb_internal_trim('{[%s]+$}Du', $string, $characters, $encoding, __FUNCTION__);
    }

    private static function mb_internal_trim(string $regex, string $string, ?string $characters, ?string $encoding, string $function): string
    {
        if (null === $encoding) {
            $encoding = self::mb_internal_encoding();
        } else {
            self::assertEncoding($encoding, $function.'(): Argument #3 ($encoding) must be a valid encoding, "%s" given');
        }

        if ('' === $characters) {
            return null === $encoding ? $string : self::mb_convert_encoding($string, $encoding);
        }

        if ('UTF-8' === $encoding) {
            $encoding = null;
            if (!preg_match('//u', $string)) {
                $string = @iconv('UTF-8', 'UTF-8//IGNORE', $string);
            }
            if (null !== $characters && !preg_match('//u', $characters)) {
                $characters = @iconv('UTF-8', 'UTF-8//IGNORE', $characters);
            }
        } else {
            $string = iconv($encoding, 'UTF-8//IGNORE', $string);

            if (null !== $characters) {
                $characters = iconv($encoding, 'UTF-8//IGNORE', $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 iconv('UTF-8', $encoding.'//IGNORE', $string);
    }

    private static function assertEncoding(string $encoding, string $errorFormat): void
    {
        try {
            $validEncoding = @self::mb_check_encoding('', $encoding);
        } catch (\ValueError $e) {
            throw new \ValueError(sprintf($errorFormat, $encoding));
        }

        // BC for PHP 7.3 and lower
        if (!$validEncoding) {
            throw new \ValueError(sprintf($errorFormat, $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.
 */

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 { $encoding ??= mb_internal_encoding(); return mb_convert_encoding((string) $string, $encoding, $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, $length, $pad_string, $pad_type, $encoding); }
}

if (!function_exists('mb_ucfirst')) {
    function mb_ucfirst(string $string, ?string $encoding = null): string { return p\Mbstring::mb_ucfirst($string, $encoding); }
}

if (!function_exists('mb_lcfirst')) {
    function mb_lcfirst(string $string, ?string $encoding = null): string { return p\Mbstring::mb_lcfirst($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, $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, $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, $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.
 */

use Symfony\Polyfill\Mbstring as p;

if (\PHP_VERSION_ID >= 80000) {
    return require __DIR__.'/bootstrap80.php';
}

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) { $encoding = null === $encoding ? mb_internal_encoding() : $encoding; return mb_convert_encoding($string, $encoding, $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')) {
    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, $length, $pad_string, $pad_type, $encoding); }
}

if (!function_exists('mb_ucfirst')) {
    function mb_ucfirst(string $string, ?string $encoding = null): string { return p\Mbstring::mb_ucfirst($string, $encoding); }
}

if (!function_exists('mb_lcfirst')) {
    function mb_lcfirst(string $string, ?string $encoding = null): string { return p\Mbstring::mb_lcfirst($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, $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, $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, $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\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\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 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;

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 mixed $value          The value to export
     * @param bool  &$isStaticValue Set to true after execution if the provided value is static, false otherwise
     * @param array &$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($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\VarExporter\Exception\ExceptionInterface;
use Symfony\Component\VarExporter\Exception\NotInstantiableTypeException;
use Symfony\Component\VarExporter\Internal\Hydrator;
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.
     *
     * For example:
     *
     *     // creates an empty instance of Foo
     *     Instantiator::instantiate(Foo::class);
     *
     *     // creates a Foo instance and sets one of its properties
     *     Instantiator::instantiate(Foo::class, ['propertyName' => $propertyValue]);
     *
     *     // creates a Foo instance and sets a private property defined on its parent Bar class
     *     Instantiator::instantiate(Foo::class, [], [
     *         Bar::class => ['privateBarProperty' => $propertyValue],
     *     ]);
     *
     * Instances of ArrayObject, ArrayIterator and SplObjectStorage can be created
     * by using the special "\0" property name to define their internal value:
     *
     *     // creates an SplObjectStorage where $info1 is attached to $obj1, etc.
     *     Instantiator::instantiate(SplObjectStorage::class, ["\0" => [$obj1, $info1, $obj2, $info2...]]);
     *
     *     // creates an ArrayObject populated with $inputArray
     *     Instantiator::instantiate(ArrayObject::class, ["\0" => [$inputArray]]);
     *
     * @param string $class             The class of the instance to create
     * @param array  $properties        The properties to set on the instance
     * @param array  $privateProperties The private properties to set on the instance,
     *                                  keyed by their declaring class
     *
     * @throws ExceptionInterface When the instance cannot be created
     */
    public static function instantiate(string $class, array $properties = [], array $privateProperties = []): object
    {
        $reflector = Registry::$reflectors[$class] ?? Registry::getClassReflector($class);

        if (Registry::$cloneable[$class]) {
            $wrappedInstance = [clone Registry::$prototypes[$class]];
        } elseif (Registry::$instantiableWithoutConstructor[$class]) {
            $wrappedInstance = [$reflector->newInstanceWithoutConstructor()];
        } elseif (null === Registry::$prototypes[$class]) {
            throw new NotInstantiableTypeException($class);
        } elseif ($reflector->implementsInterface('Serializable') && (\PHP_VERSION_ID < 70400 || !method_exists($class, '__unserialize'))) {
            $wrappedInstance = [unserialize('C:'.\strlen($class).':"'.$class.'":0:{}')];
        } else {
            $wrappedInstance = [unserialize('O:'.\strlen($class).':"'.$class.'":0:{}')];
        }

        if ($properties) {
            $privateProperties[$class] = isset($privateProperties[$class]) ? $properties + $privateProperties[$class] : $properties;
        }

        foreach ($privateProperties as $class => $properties) {
            if (!$properties) {
                continue;
            }
            foreach ($properties as $name => $value) {
                // because they're also used for "unserialization", hydrators
                // deal with array of instances, so we need to wrap values
                $properties[$name] = [$value];
            }
            (Hydrator::$hydrators[$class] ?? Hydrator::getHydrator($class))($properties, $wrappedInstance);
        }

        return $wrappedInstance[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\VarExporter\Internal;

use Symfony\Component\VarExporter\Exception\ClassNotFoundException;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @internal
 */
class Hydrator
{
    public static $hydrators = [];

    public $registry;
    public $values;
    public $properties;
    public $value;
    public $wakeups;

    public function __construct(?Registry $registry, ?Values $values, array $properties, $value, array $wakeups)
    {
        $this->registry = $registry;
        $this->values = $values;
        $this->properties = $properties;
        $this->value = $value;
        $this->wakeups = $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)
    {
        switch ($class) {
            case 'stdClass':
                return self::$hydrators[$class] = static function ($properties, $objects) {
                    foreach ($properties as $name => $values) {
                        foreach ($values as $i => $v) {
                            $objects[$i]->$name = $v;
                        }
                    }
                };

            case 'ErrorException':
                return self::$hydrators[$class] = (self::$hydrators['stdClass'] ?? self::getHydrator('stdClass'))->bindTo(null, new class() extends \ErrorException {
                });

            case 'TypeError':
                return self::$hydrators[$class] = (self::$hydrators['stdClass'] ?? self::getHydrator('stdClass'))->bindTo(null, new class() extends \Error {
                });

            case 'SplObjectStorage':
                return self::$hydrators[$class] = 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]->attach($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 = \Closure::fromCallable([$classReflector->getConstructor(), 'invokeArgs']);

                return self::$hydrators[$class] = 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 self::$hydrators[$class] = (self::$hydrators['stdClass'] ?? self::getHydrator('stdClass'))->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()) {
                $propertyReflector->setAccessible(true);
                $propertySetters[$propertyReflector->name] = \Closure::fromCallable([$propertyReflector, 'setValue']);
            }
        }

        if (!$propertySetters) {
            return self::$hydrators[$class] = self::$hydrators['stdClass'] ?? self::getHydrator('stdClass');
        }

        return self::$hydrators[$class] = 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;
                }
            }
        };
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@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
     *
     * @throws NotInstantiableTypeException When a value cannot be serialized
     */
    public static function prepare($values, $objectsPool, &$refsPool, &$objectsCount, &$valuesAreStatic): array
    {
        $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;
            }

            $class = \get_class($value);
            $reflector = Registry::$reflectors[$class] ?? Registry::getClassReflector($class);
            $properties = [];

            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($serializeProperties = $value->__serialize())) {
                    throw new \TypeError($class.'::__serialize() must return an array');
                }

                if ($reflector->hasMethod('__unserialize')) {
                    $properties = $serializeProperties;
                } else {
                    foreach ($serializeProperties as $n => $v) {
                        $c = \PHP_VERSION_ID >= 80100 && $reflector->hasProperty($n) && ($p = $reflector->getProperty($n))->isReadOnly() ? $p->class : 'stdClass';
                        $properties[$c][$n] = $v;
                    }
                }

                goto prepare_value;
            }

            $sleep = null;
            $proto = Registry::$prototypes[$class];

            if (($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
                || \PHP_VERSION_ID < 80200 && $value instanceof \DatePeriod
            ) {
                ++$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]) {
                    $c = \PHP_VERSION_ID >= 80100 && $reflector->hasProperty($n) && ($p = $reflector->getProperty($n))->isReadOnly() ? $p->class : 'stdClass';
                } elseif ('*' === $n[1]) {
                    $n = substr($n, 3);
                    $c = $reflector->getProperty($n)->class;
                    if ('Error' === $c) {
                        $c = 'TypeError';
                    } elseif ('Exception' === $c) {
                        $c = 'ErrorException';
                    }
                } 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 (!\array_key_exists($name, $proto) || $proto[$name] !== $v || "\x00Error\x00trace" === $name || "\x00Exception\x00trace" === $name) {
                    $properties[$c][$n] = $v;
                }
            }
            if ($sleep) {
                foreach ($sleep as $n => $v) {
                    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, string $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 (\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})++)(.)/", 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 ('n".\'' === substr($m[1], -4)) {
                    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 '\\'.\get_class($value)."::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;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @internal
 */
class Reference
{
    public $id;
    public $value;
    public $count = 0;

    public function __construct(int $id, $value = null)
    {
        $this->id = $id;
        $this->value = $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\Internal;

use Symfony\Component\VarExporter\Exception\ClassNotFoundException;
use Symfony\Component\VarExporter\Exception\NotInstantiableTypeException;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @internal
 */
class Registry
{
    public static $reflectors = [];
    public static $prototypes = [];
    public static $factories = [];
    public static $cloneable = [];
    public static $instantiableWithoutConstructor = [];

    public $classes = [];

    public function __construct(array $classes)
    {
        $this->classes = $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);
            }
        } 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] = \Closure::fromCallable([$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 self::$reflectors[$class] = $reflector;
        } else {
            try {
                $proto = $reflector->newInstanceWithoutConstructor();
                $instantiableWithoutConstructor = true;
            } catch (\ReflectionException $e) {
                $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:{}');
                    } catch (\Exception $e) {
                        if (__FILE__ !== $e->getFile()) {
                            throw $e;
                        }
                        throw new NotInstantiableTypeException($class, $e);
                    }
                    if (false === $proto) {
                        throw new NotInstantiableTypeException($class);
                    }
                }
            }
            if (null !== $proto && !$proto instanceof \Throwable && !$proto instanceof \Serializable && !method_exists($class, '__sleep') && (\PHP_VERSION_ID < 70400 || !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') && (\PHP_VERSION_ID < 70400 || !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]->setAccessible(true);
                $setTrace[1]->setAccessible(true);
                $setTrace[0] = \Closure::fromCallable([$setTrace[0], 'setValue']);
                $setTrace[1] = \Closure::fromCallable([$setTrace[1], 'setValue']);
            }

            $setTrace[$proto instanceof \Exception]($proto, []);
        }

        return self::$reflectors[$class] = $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\Internal;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @internal
 */
class Values
{
    public $values;

    public function __construct(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.
 */

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, ...$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.
 */

if (\PHP_VERSION_ID < 70300) {
    class JsonException 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\Polyfill\Php73;

/**
 * @author Gabriel Caruso <carusogabriel34@gmail.com>
 * @author Ion Bazan <ion.bazan@gmail.com>
 *
 * @internal
 */
final class Php73
{
    public static $startAt = 1533462603;

    /**
     * @param bool $asNum
     *
     * @return array|float|int
     */
    public static function hrtime($asNum = false)
    {
        $ns = microtime(false);
        $s = substr($ns, 11) - self::$startAt;
        $ns = 1E9 * (float) $ns;

        if ($asNum) {
            $ns += $s * 1E9;

            return \PHP_INT_SIZE === 4 ? $ns : (int) $ns;
        }

        return [$s, (int) $ns];
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) 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\Php73 as p;

if (\PHP_VERSION_ID >= 70300) {
    return;
}

if (!function_exists('is_countable')) {
    function is_countable($value) { return is_array($value) || $value instanceof Countable || $value instanceof ResourceBundle || $value instanceof SimpleXmlElement; }
}
if (!function_exists('hrtime')) {
    require_once __DIR__.'/Php73.php';
    p\Php73::$startAt = (int) microtime(true);
    function hrtime($as_number = false) { return p\Php73::hrtime($as_number); }
}
if (!function_exists('array_key_first')) {
    function array_key_first(array $array) { foreach ($array as $key => $value) { return $key; } }
}
if (!function_exists('array_key_last')) {
    function array_key_last(array $array) { return key(array_slice($array, -1, 1, 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\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 (!\is_int($int)) {
            return $int;
        }

        if ($int < -128 || $int > 255) {
            return (string) $int;
        }

        if (\PHP_VERSION_ID >= 80100) {
            @trigger_error($function.'(): Argument of type int will be interpreted as string in the future', \E_USER_DEPRECATED);
        }

        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

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <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 $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 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̀' => 'À',
  '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

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

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.
 */

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

/*
 * This file is part of the Symfony package.
 *
 * (c) 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); }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) 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) {
    interface Stringable
    {
        /**
         * @return string
         */
        public function __toString();
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

#[Attribute(Attribute::TARGET_CLASS)]
final class Attribute
{
    public const TARGET_CLASS = 1;
    public const TARGET_FUNCTION = 2;
    public const TARGET_METHOD = 4;
    public const TARGET_PROPERTY = 8;
    public const TARGET_CLASS_CONSTANT = 16;
    public const TARGET_PARAMETER = 32;
    public const TARGET_ALL = 63;
    public const IS_REPEATABLE = 64;

    /** @var int */
    public $flags;

    public function __construct(int $flags = self::TARGET_ALL)
    {
        $this->flags = $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.
 */

if (\PHP_VERSION_ID < 80000) {
    class ValueError extends Error
    {
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) 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 && extension_loaded('tokenizer')) {
    class PhpToken extends Symfony\Polyfill\Php80\PhpToken
    {
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) 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) {
    class UnhandledMatchError extends Error
    {
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <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\Php80;

/**
 * @author Fedonyuk Anton <info@ensostudio.ru>
 *
 * @internal
 */
class PhpToken implements \Stringable
{
    /**
     * @var int
     */
    public $id;

    /**
     * @var string
     */
    public $text;

    /**
     * @var -1|positive-int
     */
    public $line;

    /**
     * @var int
     */
    public $pos;

    /**
     * @param -1|positive-int $line
     */
    public function __construct(int $id, string $text, int $line = -1, int $position = -1)
    {
        $this->id = $id;
        $this->text = $text;
        $this->line = $line;
        $this->pos = $position;
    }

    public function getTokenName(): ?string
    {
        if ('UNKNOWN' === $name = token_name($this->id)) {
            $name = \strlen($this->text) > 1 || \ord($this->text) < 32 ? null : $this->text;
        }

        return $name;
    }

    /**
     * @param int|string|array $kind
     */
    public function is($kind): bool
    {
        foreach ((array) $kind as $value) {
            if (\in_array($value, [$this->id, $this->text], true)) {
                return true;
            }
        }

        return false;
    }

    public function isIgnorable(): bool
    {
        return \in_array($this->id, [\T_WHITESPACE, \T_COMMENT, \T_DOC_COMMENT, \T_OPEN_TAG], true);
    }

    public function __toString(): string
    {
        return (string) $this->text;
    }

    /**
     * @return list<static>
     */
    public static function tokenize(string $code, int $flags = 0): array
    {
        $line = 1;
        $position = 0;
        $tokens = token_get_all($code, $flags);
        foreach ($tokens as $index => $token) {
            if (\is_string($token)) {
                $id = \ord($token);
                $text = $token;
            } else {
                [$id, $text, $line] = $token;
            }
            $tokens[$index] = new static($id, $text, $line, $position);
            $position += \strlen($text);
        }

        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\Polyfill\Php80;

/**
 * @author Ion Bazan <ion.bazan@gmail.com>
 * @author Nico Oelgart <nicoswd@gmail.com>
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @internal
 */
final class Php80
{
    public static function fdiv(float $dividend, float $divisor): float
    {
        return @($dividend / $divisor);
    }

    public static function get_debug_type($value): string
    {
        switch (true) {
            case null === $value: return 'null';
            case \is_bool($value): return 'bool';
            case \is_string($value): return 'string';
            case \is_array($value): return 'array';
            case \is_int($value): return 'int';
            case \is_float($value): return 'float';
            case \is_object($value): break;
            case $value instanceof \__PHP_Incomplete_Class: return '__PHP_Incomplete_Class';
            default:
                if (null === $type = @get_resource_type($value)) {
                    return 'unknown';
                }

                if ('Unknown' === $type) {
                    $type = 'closed';
                }

                return "resource ($type)";
        }

        $class = \get_class($value);

        if (false === strpos($class, '@')) {
            return $class;
        }

        return (get_parent_class($class) ?: key(class_implements($class)) ?: 'class').'@anonymous';
    }

    public static function get_resource_id($res): int
    {
        if (!\is_resource($res) && null === @get_resource_type($res)) {
            throw new \TypeError(sprintf('Argument 1 passed to get_resource_id() must be of the type resource, %s given', get_debug_type($res)));
        }

        return (int) $res;
    }

    public static function preg_last_error_msg(): string
    {
        switch (preg_last_error()) {
            case \PREG_INTERNAL_ERROR:
                return 'Internal error';
            case \PREG_BAD_UTF8_ERROR:
                return 'Malformed UTF-8 characters, possibly incorrectly encoded';
            case \PREG_BAD_UTF8_OFFSET_ERROR:
                return 'The offset did not correspond to the beginning of a valid UTF-8 code point';
            case \PREG_BACKTRACK_LIMIT_ERROR:
                return 'Backtrack limit exhausted';
            case \PREG_RECURSION_LIMIT_ERROR:
                return 'Recursion limit exhausted';
            case \PREG_JIT_STACKLIMIT_ERROR:
                return 'JIT stack limit exhausted';
            case \PREG_NO_ERROR:
                return 'No error';
            default:
                return 'Unknown error';
        }
    }

    public static function str_contains(string $haystack, string $needle): bool
    {
        return '' === $needle || false !== strpos($haystack, $needle);
    }

    public static function str_starts_with(string $haystack, string $needle): bool
    {
        return 0 === strncmp($haystack, $needle, \strlen($needle));
    }

    public static function str_ends_with(string $haystack, string $needle): bool
    {
        if ('' === $needle || $needle === $haystack) {
            return true;
        }

        if ('' === $haystack) {
            return false;
        }

        $needleLength = \strlen($needle);

        return $needleLength <= \strlen($haystack) && 0 === substr_compare($haystack, $needle, -$needleLength);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) 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\Php80 as p;

if (\PHP_VERSION_ID >= 80000) {
    return;
}

if (!defined('FILTER_VALIDATE_BOOL') && defined('FILTER_VALIDATE_BOOLEAN')) {
    define('FILTER_VALIDATE_BOOL', \FILTER_VALIDATE_BOOLEAN);
}

if (!function_exists('fdiv')) {
    function fdiv(float $num1, float $num2): float { return p\Php80::fdiv($num1, $num2); }
}
if (!function_exists('preg_last_error_msg')) {
    function preg_last_error_msg(): string { return p\Php80::preg_last_error_msg(); }
}
if (!function_exists('str_contains')) {
    function str_contains(?string $haystack, ?string $needle): bool { return p\Php80::str_contains($haystack ?? '', $needle ?? ''); }
}
if (!function_exists('str_starts_with')) {
    function str_starts_with(?string $haystack, ?string $needle): bool { return p\Php80::str_starts_with($haystack ?? '', $needle ?? ''); }
}
if (!function_exists('str_ends_with')) {
    function str_ends_with(?string $haystack, ?string $needle): bool { return p\Php80::str_ends_with($haystack ?? '', $needle ?? ''); }
}
if (!function_exists('get_debug_type')) {
    function get_debug_type($value): string { return p\Php80::get_debug_type($value); }
}
if (!function_exists('get_resource_id')) {
    function get_resource_id($resource): int { return p\Php80::get_resource_id($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\Contracts\Cache;

use Psr\Cache\CacheItemPoolInterface;
use Psr\Cache\InvalidArgumentException;
use Psr\Log\LoggerInterface;

// Help opcache.preload discover always-needed symbols
class_exists(InvalidArgumentException::class);

/**
 * An implementation of CacheInterface for PSR-6 CacheItemPoolInterface classes.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
trait CacheTrait
{
    /**
     * {@inheritdoc}
     *
     * @return mixed
     */
    public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null)
    {
        return $this->doGet($this, $key, $callback, $beta, $metadata);
    }

    /**
     * {@inheritdoc}
     */
    public function delete(string $key): bool
    {
        return $this->deleteItem($key);
    }

    private function doGet(CacheItemPoolInterface $pool, string $key, callable $callback, ?float $beta, ?array &$metadata = null, ?LoggerInterface $logger = null)
    {
        if (0 > $beta = $beta ?? 1.0) {
            throw new class(sprintf('Argument "$beta" provided to "%s::get()" must be a positive number, %f given.', static::class, $beta)) extends \InvalidArgumentException implements InvalidArgumentException { };
        }

        $item = $pool->getItem($key);
        $recompute = !$item->isHit() || \INF === $beta;
        $metadata = $item instanceof ItemInterface ? $item->getMetadata() : [];

        if (!$recompute && $metadata) {
            $expiry = $metadata[ItemInterface::METADATA_EXPIRY] ?? false;
            $ctime = $metadata[ItemInterface::METADATA_CTIME] ?? false;

            if ($recompute = $ctime && $expiry && $expiry <= ($now = microtime(true)) - $ctime / 1000 * $beta * log(random_int(1, \PHP_INT_MAX) / \PHP_INT_MAX)) {
                // force applying defaultLifetime to expiry
                $item->expiresAt(null);
                $logger && $logger->info('Item "{key}" elected for early recomputation {delta}s before its expiration', [
                    'key' => $key,
                    'delta' => sprintf('%.1f', $expiry - $now),
                ]);
            }
        }

        if ($recompute) {
            $save = true;
            $item->set($callback($item, $save));
            if ($save) {
                $pool->save($item);
            }
        }

        return $item->get();
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <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\Cache;

use Psr\Cache\InvalidArgumentException;

/**
 * Allows invalidating cached items using tags.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
interface TagAwareCacheInterface extends CacheInterface
{
    /**
     * Invalidates cached items using tags.
     *
     * When implemented on a PSR-6 pool, invalidation should not apply
     * to deferred items. Instead, they should be committed as usual.
     * This allows replacing old tagged values by new ones without
     * race conditions.
     *
     * @param string[] $tags An array of tags to invalidate
     *
     * @return bool True on success
     *
     * @throws InvalidArgumentException When $tags is not valid
     */
    public function invalidateTags(array $tags);
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <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\Cache;

use Psr\Cache\CacheItemInterface;
use Psr\Cache\InvalidArgumentException;

/**
 * Covers most simple to advanced caching needs.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
interface CacheInterface
{
    /**
     * Fetches a value from the pool or computes it if not found.
     *
     * On cache misses, a callback is called that should return the missing value.
     * This callback is given a PSR-6 CacheItemInterface instance corresponding to the
     * requested key, that could be used e.g. for expiration control. It could also
     * be an ItemInterface instance when its additional features are needed.
     *
     * @param string                     $key       The key of the item to retrieve from the cache
     * @param callable|CallbackInterface $callback  Should return the computed value for the given key/item
     * @param float|null                 $beta      A float that, as it grows, controls the likeliness of triggering
     *                                              early expiration. 0 disables it, INF forces immediate expiration.
     *                                              The default (or providing null) is implementation dependent but should
     *                                              typically be 1.0, which should provide optimal stampede protection.
     *                                              See https://en.wikipedia.org/wiki/Cache_stampede#Probabilistic_early_expiration
     * @param array                      &$metadata The metadata of the cached item {@see ItemInterface::getMetadata()}
     *
     * @return mixed
     *
     * @throws InvalidArgumentException When $key is not valid or when $beta is negative
     */
    public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null);

    /**
     * Removes an item from the pool.
     *
     * @param string $key The key to delete
     *
     * @return bool True if the item was successfully removed, false if there was any error
     *
     * @throws InvalidArgumentException When $key is not valid
     */
    public function delete(string $key): 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\Contracts\Cache;

use Psr\Cache\CacheException;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\InvalidArgumentException;

/**
 * Augments PSR-6's CacheItemInterface with support for tags and metadata.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
interface ItemInterface extends CacheItemInterface
{
    /**
     * References the Unix timestamp stating when the item will expire.
     */
    public const METADATA_EXPIRY = 'expiry';

    /**
     * References the time the item took to be created, in milliseconds.
     */
    public const METADATA_CTIME = 'ctime';

    /**
     * References the list of tags that were assigned to the item, as string[].
     */
    public const METADATA_TAGS = 'tags';

    /**
     * Reserved characters that cannot be used in a key or tag.
     */
    public const RESERVED_CHARACTERS = '{}()/\@:';

    /**
     * Adds a tag to a cache item.
     *
     * Tags are strings that follow the same validation rules as keys.
     *
     * @param string|string[] $tags A tag or array of tags
     *
     * @return $this
     *
     * @throws InvalidArgumentException When $tag is not valid
     * @throws CacheException           When the item comes from a pool that is not tag-aware
     */
    public function tag($tags): self;

    /**
     * Returns a list of metadata info that were saved alongside with the cached value.
     *
     * See ItemInterface::METADATA_* consts for keys potentially found in the returned array.
     */
    public function getMetadata(): 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\Cache;

use Psr\Cache\CacheItemInterface;

/**
 * Computes and returns the cached value of an item.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
interface CallbackInterface
{
    /**
     * @param CacheItemInterface|ItemInterface $item  The item to compute the value for
     * @param bool                             &$save Should be set to false when the value should not be saved in the pool
     *
     * @return mixed The computed value for the passed item
     */
    public function __invoke(CacheItemInterface $item, bool &$save);
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Cache\Traits;

use Symfony\Component\Cache\PruneableInterface;
use Symfony\Contracts\Service\ResetInterface;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @internal
 */
trait ProxyTrait
{
    private $pool;

    /**
     * {@inheritdoc}
     */
    public function prune()
    {
        return $this->pool instanceof PruneableInterface && $this->pool->prune();
    }

    /**
     * {@inheritdoc}
     */
    public function reset()
    {
        if ($this->pool instanceof ResetInterface) {
            $this->pool->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\Component\Cache\Traits;

use Predis\Command\Redis\UNLINK;
use Predis\Connection\Aggregate\ClusterInterface;
use Predis\Connection\Aggregate\RedisCluster;
use Predis\Connection\Aggregate\ReplicationInterface;
use Predis\Connection\Cluster\ClusterInterface as Predis2ClusterInterface;
use Predis\Connection\Cluster\RedisCluster as Predis2RedisCluster;
use Predis\Response\ErrorInterface;
use Predis\Response\Status;
use Symfony\Component\Cache\Exception\CacheException;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;

/**
 * @author Aurimas Niekis <aurimas@niekis.lt>
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @internal
 */
trait RedisTrait
{
    private static $defaultConnectionOptions = [
        'class' => null,
        'persistent' => 0,
        'persistent_id' => null,
        'timeout' => 30,
        'read_timeout' => 0,
        'retry_interval' => 0,
        'tcp_keepalive' => 0,
        'lazy' => null,
        'redis_cluster' => false,
        'redis_sentinel' => null,
        'dbindex' => 0,
        'failover' => 'none',
        'ssl' => null, // see https://php.net/context.ssl
    ];
    private $redis;
    private $marshaller;

    /**
     * @param \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|RedisProxy|RedisClusterProxy $redis
     */
    private function init($redis, string $namespace, int $defaultLifetime, ?MarshallerInterface $marshaller)
    {
        parent::__construct($namespace, $defaultLifetime);

        if (preg_match('#[^-+_.A-Za-z0-9]#', $namespace, $match)) {
            throw new InvalidArgumentException(sprintf('RedisAdapter namespace contains "%s" but only characters in [-+_.A-Za-z0-9] are allowed.', $match[0]));
        }

        if (!$redis instanceof \Redis && !$redis instanceof \RedisArray && !$redis instanceof \RedisCluster && !$redis instanceof \Predis\ClientInterface && !$redis instanceof RedisProxy && !$redis instanceof RedisClusterProxy) {
            throw new InvalidArgumentException(sprintf('"%s()" expects parameter 1 to be Redis, RedisArray, RedisCluster or Predis\ClientInterface, "%s" given.', __METHOD__, get_debug_type($redis)));
        }

        if ($redis instanceof \Predis\ClientInterface && $redis->getOptions()->exceptions) {
            $options = clone $redis->getOptions();
            \Closure::bind(function () { $this->options['exceptions'] = false; }, $options, $options)();
            $redis = new $redis($redis->getConnection(), $options);
        }

        $this->redis = $redis;
        $this->marshaller = $marshaller ?? new DefaultMarshaller();
    }

    /**
     * Creates a Redis connection using a DSN configuration.
     *
     * Example DSN:
     *   - redis://localhost
     *   - redis://example.com:1234
     *   - redis://secret@example.com/13
     *   - redis:///var/run/redis.sock
     *   - redis://secret@/var/run/redis.sock/13
     *
     * @param array $options See self::$defaultConnectionOptions
     *
     * @return \Redis|\RedisArray|\RedisCluster|RedisClusterProxy|RedisProxy|\Predis\ClientInterface According to the "class" option
     *
     * @throws InvalidArgumentException when the DSN is invalid
     */
    public static function createConnection(string $dsn, array $options = [])
    {
        if (str_starts_with($dsn, 'redis:')) {
            $scheme = 'redis';
        } elseif (str_starts_with($dsn, 'rediss:')) {
            $scheme = 'rediss';
        } else {
            throw new InvalidArgumentException('Invalid Redis DSN: it does not start with "redis[s]:".');
        }

        if (!\extension_loaded('redis') && !class_exists(\Predis\Client::class)) {
            throw new CacheException('Cannot find the "redis" extension nor the "predis/predis" package.');
        }

        $params = preg_replace_callback('#^'.$scheme.':(//)?(?:(?:[^:@]*+:)?([^@]*+)@)?#', function ($m) use (&$auth) {
            if (isset($m[2])) {
                $auth = rawurldecode($m[2]);

                if ('' === $auth) {
                    $auth = null;
                }
            }

            return 'file:'.($m[1] ?? '');
        }, $dsn);

        if (false === $params = parse_url($params)) {
            throw new InvalidArgumentException('Invalid Redis DSN.');
        }

        $query = $hosts = [];

        $tls = 'rediss' === $scheme;
        $tcpScheme = $tls ? 'tls' : 'tcp';

        if (isset($params['query'])) {
            parse_str($params['query'], $query);

            if (isset($query['host'])) {
                if (!\is_array($hosts = $query['host'])) {
                    throw new InvalidArgumentException('Invalid Redis DSN: query parameter "host" must be an array.');
                }
                foreach ($hosts as $host => $parameters) {
                    if (\is_string($parameters)) {
                        parse_str($parameters, $parameters);
                    }
                    if (false === $i = strrpos($host, ':')) {
                        $hosts[$host] = ['scheme' => $tcpScheme, 'host' => $host, 'port' => 6379] + $parameters;
                    } elseif ($port = (int) substr($host, 1 + $i)) {
                        $hosts[$host] = ['scheme' => $tcpScheme, 'host' => substr($host, 0, $i), 'port' => $port] + $parameters;
                    } else {
                        $hosts[$host] = ['scheme' => 'unix', 'path' => substr($host, 0, $i)] + $parameters;
                    }
                }
                $hosts = array_values($hosts);
            }
        }

        if (isset($params['host']) || isset($params['path'])) {
            if (!isset($params['dbindex']) && isset($params['path'])) {
                if (preg_match('#/(\d+)?$#', $params['path'], $m)) {
                    $params['dbindex'] = $m[1] ?? $query['dbindex'] ?? '0';
                    $params['path'] = substr($params['path'], 0, -\strlen($m[0]));
                } elseif (isset($params['host'])) {
                    throw new InvalidArgumentException('Invalid Redis DSN: parameter "dbindex" must be a number.');
                }
            }

            if (isset($params['host'])) {
                array_unshift($hosts, ['scheme' => $tcpScheme, 'host' => $params['host'], 'port' => $params['port'] ?? 6379]);
            } else {
                array_unshift($hosts, ['scheme' => 'unix', 'path' => $params['path']]);
            }
        }

        if (!$hosts) {
            throw new InvalidArgumentException('Invalid Redis DSN: missing host.');
        }

        if (isset($params['dbindex'], $query['dbindex']) && $params['dbindex'] !== $query['dbindex']) {
            throw new InvalidArgumentException('Invalid Redis DSN: path and query "dbindex" parameters mismatch.');
        }

        $params += $query + $options + self::$defaultConnectionOptions;

        if (isset($params['redis_sentinel']) && !class_exists(\Predis\Client::class) && !class_exists(\RedisSentinel::class)) {
            throw new CacheException('Redis Sentinel support requires the "predis/predis" package or the "redis" extension v5.2 or higher.');
        }

        if (isset($params['lazy'])) {
            $params['lazy'] = filter_var($params['lazy'], \FILTER_VALIDATE_BOOLEAN);
        }
        $params['redis_cluster'] = filter_var($params['redis_cluster'], \FILTER_VALIDATE_BOOLEAN);

        if ($params['redis_cluster'] && isset($params['redis_sentinel'])) {
            throw new InvalidArgumentException('Cannot use both "redis_cluster" and "redis_sentinel" at the same time.');
        }

        if (null === $params['class'] && \extension_loaded('redis')) {
            $class = $params['redis_cluster'] ? \RedisCluster::class : (1 < \count($hosts) && !isset($params['redis_sentinel']) ? \RedisArray::class : \Redis::class);
        } else {
            $class = $params['class'] ?? \Predis\Client::class;

            if (isset($params['redis_sentinel']) && !is_a($class, \Predis\Client::class, true) && !class_exists(\RedisSentinel::class)) {
                throw new CacheException(sprintf('Cannot use Redis Sentinel: class "%s" does not extend "Predis\Client" and ext-redis >= 5.2 not found.', $class));
            }
        }

        if (is_a($class, \Redis::class, true)) {
            $connect = $params['persistent'] || $params['persistent_id'] ? 'pconnect' : 'connect';
            $redis = new $class();

            $initializer = static function ($redis) use ($connect, $params, $auth, $hosts, $tls) {
                $hostIndex = 0;
                do {
                    $host = $hosts[$hostIndex]['host'] ?? $hosts[$hostIndex]['path'];
                    $port = $hosts[$hostIndex]['port'] ?? 0;
                    $passAuth = \defined('Redis::OPT_NULL_MULTIBULK_AS_NULL') && isset($params['auth']);
                    $address = false;

                    if (isset($hosts[$hostIndex]['host']) && $tls) {
                        $host = 'tls://'.$host;
                    }

                    if (!isset($params['redis_sentinel'])) {
                        break;
                    }

                    if (version_compare(phpversion('redis'), '6.0.0', '>=')) {
                        $options = [
                            'host' => $host,
                            'port' => $port,
                            'connectTimeout' => (float) $params['timeout'],
                            'persistent' => $params['persistent_id'],
                            'retryInterval' => (int) $params['retry_interval'],
                            'readTimeout' => (float) $params['read_timeout'],
                        ];

                        if ($passAuth) {
                            $options['auth'] = $params['auth'];
                        }

                        $sentinel = new \RedisSentinel($options);
                    } else {
                        $extra = $passAuth ? [$params['auth']] : [];

                        $sentinel = new \RedisSentinel($host, $port, $params['timeout'], (string) $params['persistent_id'], $params['retry_interval'], $params['read_timeout'], ...$extra);
                    }

                    try {
                        if ($address = $sentinel->getMasterAddrByName($params['redis_sentinel'])) {
                            [$host, $port] = $address;
                        }
                    } catch (\RedisException $e) {
                    }
                } while (++$hostIndex < \count($hosts) && !$address);

                if (isset($params['redis_sentinel']) && !$address) {
                    throw new InvalidArgumentException(sprintf('Failed to retrieve master information from sentinel "%s".', $params['redis_sentinel']));
                }

                try {
                    $extra = [
                        'stream' => $params['ssl'] ?? null,
                    ];
                    $booleanStreamOptions = [
                        'allow_self_signed',
                        'capture_peer_cert',
                        'capture_peer_cert_chain',
                        'disable_compression',
                        'SNI_enabled',
                        'verify_peer',
                        'verify_peer_name',
                    ];

                    foreach ($extra['stream'] ?? [] as $streamOption => $value) {
                        if (\in_array($streamOption, $booleanStreamOptions, true) && \is_string($value)) {
                            $extra['stream'][$streamOption] = filter_var($value, \FILTER_VALIDATE_BOOL);
                        }
                    }

                    if (isset($params['auth'])) {
                        $extra['auth'] = $params['auth'];
                    }
                    @$redis->{$connect}($host, $port, $params['timeout'], (string) $params['persistent_id'], $params['retry_interval'], $params['read_timeout'], ...\defined('Redis::SCAN_PREFIX') ? [$extra] : []);

                    set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; });
                    try {
                        $isConnected = $redis->isConnected();
                    } finally {
                        restore_error_handler();
                    }
                    if (!$isConnected) {
                        $error = preg_match('/^Redis::p?connect\(\): (.*)/', $error ?? $redis->getLastError() ?? '', $error) ? sprintf(' (%s)', $error[1]) : '';
                        throw new InvalidArgumentException('Redis connection failed: '.$error.'.');
                    }

                    if ((null !== $auth && !$redis->auth($auth))
                        // Due to a bug in phpredis we must always select the dbindex if persistent pooling is enabled
                        // @see https://github.com/phpredis/phpredis/issues/1920
                        // @see https://github.com/symfony/symfony/issues/51578
                        || (($params['dbindex'] || ('pconnect' === $connect && '0' !== \ini_get('redis.pconnect.pooling_enabled'))) && !$redis->select($params['dbindex']))
                    ) {
                        $e = preg_replace('/^ERR /', '', $redis->getLastError());
                        throw new InvalidArgumentException('Redis connection failed: '.$e.'.');
                    }

                    if (0 < $params['tcp_keepalive'] && \defined('Redis::OPT_TCP_KEEPALIVE')) {
                        $redis->setOption(\Redis::OPT_TCP_KEEPALIVE, $params['tcp_keepalive']);
                    }
                } catch (\RedisException $e) {
                    throw new InvalidArgumentException('Redis connection failed: '.$e->getMessage());
                }

                return true;
            };

            if ($params['lazy']) {
                $redis = new RedisProxy($redis, $initializer);
            } else {
                $initializer($redis);
            }
        } elseif (is_a($class, \RedisArray::class, true)) {
            foreach ($hosts as $i => $host) {
                switch ($host['scheme']) {
                    case 'tcp': $hosts[$i] = $host['host'].':'.$host['port']; break;
                    case 'tls': $hosts[$i] = 'tls://'.$host['host'].':'.$host['port']; break;
                    default: $hosts[$i] = $host['path'];
                }
            }
            $params['lazy_connect'] = $params['lazy'] ?? true;
            $params['connect_timeout'] = $params['timeout'];

            try {
                $redis = new $class($hosts, $params);
            } catch (\RedisClusterException $e) {
                throw new InvalidArgumentException('Redis connection failed: '.$e->getMessage());
            }

            if (0 < $params['tcp_keepalive'] && \defined('Redis::OPT_TCP_KEEPALIVE')) {
                $redis->setOption(\Redis::OPT_TCP_KEEPALIVE, $params['tcp_keepalive']);
            }
        } elseif (is_a($class, \RedisCluster::class, true)) {
            $initializer = static function () use ($class, $params, $hosts) {
                foreach ($hosts as $i => $host) {
                    switch ($host['scheme']) {
                        case 'tcp': $hosts[$i] = $host['host'].':'.$host['port']; break;
                        case 'tls': $hosts[$i] = 'tls://'.$host['host'].':'.$host['port']; break;
                        default: $hosts[$i] = $host['path'];
                    }
                }

                try {
                    $redis = new $class(null, $hosts, $params['timeout'], $params['read_timeout'], (bool) $params['persistent'], $params['auth'] ?? '', ...\defined('Redis::SCAN_PREFIX') ? [$params['ssl'] ?? null] : []);
                } catch (\RedisClusterException $e) {
                    throw new InvalidArgumentException('Redis connection failed: '.$e->getMessage());
                }

                if (0 < $params['tcp_keepalive'] && \defined('Redis::OPT_TCP_KEEPALIVE')) {
                    $redis->setOption(\Redis::OPT_TCP_KEEPALIVE, $params['tcp_keepalive']);
                }
                switch ($params['failover']) {
                    case 'error': $redis->setOption(\RedisCluster::OPT_SLAVE_FAILOVER, \RedisCluster::FAILOVER_ERROR); break;
                    case 'distribute': $redis->setOption(\RedisCluster::OPT_SLAVE_FAILOVER, \RedisCluster::FAILOVER_DISTRIBUTE); break;
                    case 'slaves': $redis->setOption(\RedisCluster::OPT_SLAVE_FAILOVER, \RedisCluster::FAILOVER_DISTRIBUTE_SLAVES); break;
                }

                return $redis;
            };

            $redis = $params['lazy'] ? new RedisClusterProxy($initializer) : $initializer();
        } elseif (is_a($class, \Predis\ClientInterface::class, true)) {
            if ($params['redis_cluster']) {
                $params['cluster'] = 'redis';
            } elseif (isset($params['redis_sentinel'])) {
                $params['replication'] = 'sentinel';
                $params['service'] = $params['redis_sentinel'];
            }
            $params += ['parameters' => []];
            $params['parameters'] += [
                'persistent' => $params['persistent'],
                'timeout' => $params['timeout'],
                'read_write_timeout' => $params['read_timeout'],
                'tcp_nodelay' => true,
            ];
            if ($params['dbindex']) {
                $params['parameters']['database'] = $params['dbindex'];
            }
            if (null !== $auth) {
                $params['parameters']['password'] = $auth;
            }

            if (isset($params['ssl'])) {
                foreach ($hosts as $i => $host) {
                    if (!isset($host['ssl'])) {
                        $hosts[$i]['ssl'] = $params['ssl'];
                    }
                }
            }

            if (1 === \count($hosts) && !($params['redis_cluster'] || $params['redis_sentinel'])) {
                $hosts = $hosts[0];
            } elseif (\in_array($params['failover'], ['slaves', 'distribute'], true) && !isset($params['replication'])) {
                $params['replication'] = true;
                $hosts[0] += ['alias' => 'master'];
            }
            $params['exceptions'] = false;

            $redis = new $class($hosts, array_diff_key($params, self::$defaultConnectionOptions));
            if (isset($params['redis_sentinel'])) {
                $redis->getConnection()->setSentinelTimeout($params['timeout']);
            }
        } elseif (class_exists($class, false)) {
            throw new InvalidArgumentException(sprintf('"%s" is not a subclass of "Redis", "RedisArray", "RedisCluster" nor "Predis\ClientInterface".', $class));
        } else {
            throw new InvalidArgumentException(sprintf('Class "%s" does not exist.', $class));
        }

        return $redis;
    }

    protected function doFetch(array $ids)
    {
        if (!$ids) {
            return [];
        }

        $result = [];

        if ($this->redis instanceof \Predis\ClientInterface && ($this->redis->getConnection() instanceof ClusterInterface || $this->redis->getConnection() instanceof Predis2ClusterInterface)) {
            $values = $this->pipeline(function () use ($ids) {
                foreach ($ids as $id) {
                    yield 'get' => [$id];
                }
            });
        } else {
            $values = $this->redis->mget($ids);

            if (!\is_array($values) || \count($values) !== \count($ids)) {
                return [];
            }

            $values = array_combine($ids, $values);
        }

        foreach ($values as $id => $v) {
            if ($v) {
                $result[$id] = $this->marshaller->unmarshall($v);
            }
        }

        return $result;
    }

    protected function doHave(string $id)
    {
        return (bool) $this->redis->exists($id);
    }

    protected function doClear(string $namespace)
    {
        if ($this->redis instanceof \Predis\ClientInterface) {
            $prefix = $this->redis->getOptions()->prefix ? $this->redis->getOptions()->prefix->getPrefix() : '';
            $prefixLen = \strlen($prefix ?? '');
        }

        $cleared = true;
        $hosts = $this->getHosts();
        $host = reset($hosts);
        if ($host instanceof \Predis\Client && $host->getConnection() instanceof ReplicationInterface) {
            // Predis supports info command only on the master in replication environments
            $hosts = [$host->getClientFor('master')];
        }

        foreach ($hosts as $host) {
            if (!isset($namespace[0])) {
                $cleared = $host->flushDb() && $cleared;
                continue;
            }

            $info = $host->info('Server');
            $info = !$info instanceof ErrorInterface ? $info['Server'] ?? $info : ['redis_version' => '2.0'];

            if (!$host instanceof \Predis\ClientInterface) {
                $prefix = \defined('Redis::SCAN_PREFIX') && (\Redis::SCAN_PREFIX & $host->getOption(\Redis::OPT_SCAN)) ? '' : $host->getOption(\Redis::OPT_PREFIX);
                $prefixLen = \strlen($host->getOption(\Redis::OPT_PREFIX) ?? '');
            }
            $pattern = $prefix.$namespace.'*';

            if (!version_compare($info['redis_version'], '2.8', '>=')) {
                // As documented in Redis documentation (http://redis.io/commands/keys) using KEYS
                // can hang your server when it is executed against large databases (millions of items).
                // Whenever you hit this scale, you should really consider upgrading to Redis 2.8 or above.
                $unlink = version_compare($info['redis_version'], '4.0', '>=') ? 'UNLINK' : 'DEL';
                $args = $this->redis instanceof \Predis\ClientInterface ? [0, $pattern] : [[$pattern], 0];
                $cleared = $host->eval("local keys=redis.call('KEYS',ARGV[1]) for i=1,#keys,5000 do redis.call('$unlink',unpack(keys,i,math.min(i+4999,#keys))) end return 1", $args[0], $args[1]) && $cleared;
                continue;
            }

            $cursor = null;
            do {
                $keys = $host instanceof \Predis\ClientInterface ? $host->scan($cursor ?? 0, 'MATCH', $pattern, 'COUNT', 1000) : $host->scan($cursor, $pattern, 1000);
                if (isset($keys[1]) && \is_array($keys[1])) {
                    $cursor = $keys[0];
                    $keys = $keys[1];
                }
                if ($keys) {
                    if ($prefixLen) {
                        foreach ($keys as $i => $key) {
                            $keys[$i] = substr($key, $prefixLen);
                        }
                    }
                    $this->doDelete($keys);
                }
            } while ($cursor);
        }

        return $cleared;
    }

    protected function doDelete(array $ids)
    {
        if (!$ids) {
            return true;
        }

        if ($this->redis instanceof \Predis\ClientInterface && ($this->redis->getConnection() instanceof ClusterInterface || $this->redis->getConnection() instanceof Predis2ClusterInterface)) {
            static $del;
            $del = $del ?? (class_exists(UNLINK::class) ? 'unlink' : 'del');

            $this->pipeline(function () use ($ids, $del) {
                foreach ($ids as $id) {
                    yield $del => [$id];
                }
            })->rewind();
        } else {
            static $unlink = true;

            if ($unlink) {
                try {
                    $unlink = false !== $this->redis->unlink($ids);
                } catch (\Throwable $e) {
                    $unlink = false;
                }
            }

            if (!$unlink) {
                $this->redis->del($ids);
            }
        }

        return true;
    }

    protected function doSave(array $values, int $lifetime)
    {
        if (!$values = $this->marshaller->marshall($values, $failed)) {
            return $failed;
        }

        $results = $this->pipeline(function () use ($values, $lifetime) {
            foreach ($values as $id => $value) {
                if (0 >= $lifetime) {
                    yield 'set' => [$id, $value];
                } else {
                    yield 'setEx' => [$id, $lifetime, $value];
                }
            }
        });

        foreach ($results as $id => $result) {
            if (true !== $result && (!$result instanceof Status || Status::get('OK') !== $result)) {
                $failed[] = $id;
            }
        }

        return $failed;
    }

    private function pipeline(\Closure $generator, ?object $redis = null): \Generator
    {
        $ids = [];
        $redis = $redis ?? $this->redis;

        if ($redis instanceof RedisClusterProxy || $redis instanceof \RedisCluster || ($redis instanceof \Predis\ClientInterface && ($redis->getConnection() instanceof RedisCluster || $redis->getConnection() instanceof Predis2RedisCluster))) {
            // phpredis & predis don't support pipelining with RedisCluster
            // see https://github.com/phpredis/phpredis/blob/develop/cluster.markdown#pipelining
            // see https://github.com/nrk/predis/issues/267#issuecomment-123781423
            $results = [];
            foreach ($generator() as $command => $args) {
                $results[] = $redis->{$command}(...$args);
                $ids[] = 'eval' === $command ? ($redis instanceof \Predis\ClientInterface ? $args[2] : $args[1][0]) : $args[0];
            }
        } elseif ($redis instanceof \Predis\ClientInterface) {
            $results = $redis->pipeline(static function ($redis) use ($generator, &$ids) {
                foreach ($generator() as $command => $args) {
                    $redis->{$command}(...$args);
                    $ids[] = 'eval' === $command ? $args[2] : $args[0];
                }
            });
        } elseif ($redis instanceof \RedisArray) {
            $connections = $results = $ids = [];
            foreach ($generator() as $command => $args) {
                $id = 'eval' === $command ? $args[1][0] : $args[0];
                if (!isset($connections[$h = $redis->_target($id)])) {
                    $connections[$h] = [$redis->_instance($h), -1];
                    $connections[$h][0]->multi(\Redis::PIPELINE);
                }
                $connections[$h][0]->{$command}(...$args);
                $results[] = [$h, ++$connections[$h][1]];
                $ids[] = $id;
            }
            foreach ($connections as $h => $c) {
                $connections[$h] = $c[0]->exec();
            }
            foreach ($results as $k => [$h, $c]) {
                $results[$k] = $connections[$h][$c];
            }
        } else {
            $redis->multi(\Redis::PIPELINE);
            foreach ($generator() as $command => $args) {
                $redis->{$command}(...$args);
                $ids[] = 'eval' === $command ? $args[1][0] : $args[0];
            }
            $results = $redis->exec();
        }

        if (!$redis instanceof \Predis\ClientInterface && 'eval' === $command && $redis->getLastError()) {
            $e = new \RedisException($redis->getLastError());
            $results = array_map(function ($v) use ($e) { return false === $v ? $e : $v; }, (array) $results);
        }

        if (\is_bool($results)) {
            return;
        }

        foreach ($ids as $k => $id) {
            yield $id => $results[$k];
        }
    }

    private function getHosts(): array
    {
        $hosts = [$this->redis];
        if ($this->redis instanceof \Predis\ClientInterface) {
            $connection = $this->redis->getConnection();
            if (($connection instanceof ClusterInterface || $connection instanceof Predis2ClusterInterface) && $connection instanceof \Traversable) {
                $hosts = [];
                foreach ($connection as $c) {
                    $hosts[] = new \Predis\Client($c);
                }
            }
        } elseif ($this->redis instanceof \RedisArray) {
            $hosts = [];
            foreach ($this->redis->_hosts() as $host) {
                $hosts[] = $this->redis->_instance($host);
            }
        } elseif ($this->redis instanceof RedisClusterProxy || $this->redis instanceof \RedisCluster) {
            $hosts = [];
            foreach ($this->redis->_masters() as $host) {
                $hosts[] = new RedisClusterNodeProxy($host, $this->redis);
            }
        }

        return $hosts;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Cache\Traits;

use Symfony\Component\Cache\Exception\CacheException;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 * @author Rob Frawley 2nd <rmf@src.run>
 *
 * @internal
 */
trait FilesystemTrait
{
    use FilesystemCommonTrait;

    private $marshaller;

    /**
     * @return bool
     */
    public function prune()
    {
        $time = time();
        $pruned = true;

        foreach ($this->scanHashDir($this->directory) as $file) {
            if (!$h = @fopen($file, 'r')) {
                continue;
            }

            if (($expiresAt = (int) fgets($h)) && $time >= $expiresAt) {
                fclose($h);
                $pruned = (@unlink($file) || !file_exists($file)) && $pruned;
            } else {
                fclose($h);
            }
        }

        return $pruned;
    }

    /**
     * {@inheritdoc}
     */
    protected function doFetch(array $ids)
    {
        $values = [];
        $now = time();

        foreach ($ids as $id) {
            $file = $this->getFile($id);
            if (!is_file($file) || !$h = @fopen($file, 'r')) {
                continue;
            }
            if (($expiresAt = (int) fgets($h)) && $now >= $expiresAt) {
                fclose($h);
                @unlink($file);
            } else {
                $i = rawurldecode(rtrim(fgets($h)));
                $value = stream_get_contents($h);
                fclose($h);
                if ($i === $id) {
                    $values[$id] = $this->marshaller->unmarshall($value);
                }
            }
        }

        return $values;
    }

    /**
     * {@inheritdoc}
     */
    protected function doHave(string $id)
    {
        $file = $this->getFile($id);

        return is_file($file) && (@filemtime($file) > time() || $this->doFetch([$id]));
    }

    /**
     * {@inheritdoc}
     */
    protected function doSave(array $values, int $lifetime)
    {
        $expiresAt = $lifetime ? (time() + $lifetime) : 0;
        $values = $this->marshaller->marshall($values, $failed);

        foreach ($values as $id => $value) {
            if (!$this->write($this->getFile($id, true), $expiresAt."\n".rawurlencode($id)."\n".$value, $expiresAt)) {
                $failed[] = $id;
            }
        }

        if ($failed && !is_writable($this->directory)) {
            throw new CacheException(sprintf('Cache directory is not writable (%s).', $this->directory));
        }

        return $failed;
    }

    private function getFileKey(string $file): string
    {
        if (!$h = @fopen($file, 'r')) {
            return '';
        }

        fgets($h); // expiry
        $encodedKey = fgets($h);
        fclose($h);

        return rawurldecode(rtrim($encodedKey));
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Cache\Traits;

/**
 * @author Alessandro Chitolina <alekitto@gmail.com>
 *
 * @internal
 */
class RedisClusterProxy
{
    private $redis;
    private $initializer;

    public function __construct(\Closure $initializer)
    {
        $this->initializer = $initializer;
    }

    public function __call(string $method, array $args)
    {
        $this->redis ?: $this->redis = $this->initializer->__invoke();

        return $this->redis->{$method}(...$args);
    }

    public function hscan($strKey, &$iIterator, $strPattern = null, $iCount = null)
    {
        $this->redis ?: $this->redis = $this->initializer->__invoke();

        return $this->redis->hscan($strKey, $iIterator, $strPattern, $iCount);
    }

    public function scan(&$iIterator, $strPattern = null, $iCount = null)
    {
        $this->redis ?: $this->redis = $this->initializer->__invoke();

        return $this->redis->scan($iIterator, $strPattern, $iCount);
    }

    public function sscan($strKey, &$iIterator, $strPattern = null, $iCount = null)
    {
        $this->redis ?: $this->redis = $this->initializer->__invoke();

        return $this->redis->sscan($strKey, $iIterator, $strPattern, $iCount);
    }

    public function zscan($strKey, &$iIterator, $strPattern = null, $iCount = null)
    {
        $this->redis ?: $this->redis = $this->initializer->__invoke();

        return $this->redis->zscan($strKey, $iIterator, $strPattern, $iCount);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Cache\Traits;

/**
 * This file acts as a wrapper to the \RedisCluster implementation so it can accept the same type of calls as
 *  individual \Redis objects.
 *
 * Calls are made to individual nodes via: RedisCluster->{method}($host, ...args)'
 *  according to https://github.com/phpredis/phpredis/blob/develop/cluster.markdown#directed-node-commands
 *
 * @author Jack Thomas <jack.thomas@solidalpha.com>
 *
 * @internal
 */
class RedisClusterNodeProxy
{
    private $host;
    private $redis;

    /**
     * @param \RedisCluster|RedisClusterProxy $redis
     */
    public function __construct(array $host, $redis)
    {
        $this->host = $host;
        $this->redis = $redis;
    }

    public function __call(string $method, array $args)
    {
        return $this->redis->{$method}($this->host, ...$args);
    }

    public function scan(&$iIterator, $strPattern = null, $iCount = null)
    {
        return $this->redis->scan($iIterator, $this->host, $strPattern, $iCount);
    }

    public function getOption($name)
    {
        return $this->redis->getOption($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\Cache\Traits;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @internal
 */
class RedisProxy
{
    private $redis;
    private $initializer;
    private $ready = false;

    public function __construct(\Redis $redis, \Closure $initializer)
    {
        $this->redis = $redis;
        $this->initializer = $initializer;
    }

    public function __call(string $method, array $args)
    {
        $this->ready ?: $this->ready = $this->initializer->__invoke($this->redis);

        return $this->redis->{$method}(...$args);
    }

    public function hscan($strKey, &$iIterator, $strPattern = null, $iCount = null)
    {
        $this->ready ?: $this->ready = $this->initializer->__invoke($this->redis);

        return $this->redis->hscan($strKey, $iIterator, $strPattern, $iCount);
    }

    public function scan(&$iIterator, $strPattern = null, $iCount = null)
    {
        $this->ready ?: $this->ready = $this->initializer->__invoke($this->redis);

        return $this->redis->scan($iIterator, $strPattern, $iCount);
    }

    public function sscan($strKey, &$iIterator, $strPattern = null, $iCount = null)
    {
        $this->ready ?: $this->ready = $this->initializer->__invoke($this->redis);

        return $this->redis->sscan($strKey, $iIterator, $strPattern, $iCount);
    }

    public function zscan($strKey, &$iIterator, $strPattern = null, $iCount = null)
    {
        $this->ready ?: $this->ready = $this->initializer->__invoke($this->redis);

        return $this->redis->zscan($strKey, $iIterator, $strPattern, $iCount);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Cache\Traits;

use Psr\Cache\CacheItemInterface;
use Psr\Log\LoggerAwareTrait;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Cache\Exception\InvalidArgumentException;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @internal
 */
trait AbstractAdapterTrait
{
    use LoggerAwareTrait;

    /**
     * @var \Closure needs to be set by class, signature is function(string <key>, mixed <value>, bool <isHit>)
     */
    private static $createCacheItem;

    /**
     * @var \Closure needs to be set by class, signature is function(array <deferred>, string <namespace>, array <&expiredIds>)
     */
    private static $mergeByLifetime;

    private $namespace = '';
    private $defaultLifetime;
    private $namespaceVersion = '';
    private $versioningIsEnabled = false;
    private $deferred = [];
    private $ids = [];

    /**
     * @var int|null The maximum length to enforce for identifiers or null when no limit applies
     */
    protected $maxIdLength;

    /**
     * Fetches several cache items.
     *
     * @param array $ids The cache identifiers to fetch
     *
     * @return array|\Traversable
     */
    abstract protected function doFetch(array $ids);

    /**
     * Confirms if the cache contains specified cache item.
     *
     * @param string $id The identifier for which to check existence
     *
     * @return bool
     */
    abstract protected function doHave(string $id);

    /**
     * Deletes all items in the pool.
     *
     * @param string $namespace The prefix used for all identifiers managed by this pool
     *
     * @return bool
     */
    abstract protected function doClear(string $namespace);

    /**
     * Removes multiple items from the pool.
     *
     * @param array $ids An array of identifiers that should be removed from the pool
     *
     * @return bool
     */
    abstract protected function doDelete(array $ids);

    /**
     * Persists several cache items immediately.
     *
     * @param array $values   The values to cache, indexed by their cache identifier
     * @param int   $lifetime The lifetime of the cached values, 0 for persisting until manual cleaning
     *
     * @return array|bool The identifiers that failed to be cached or a boolean stating if caching succeeded or not
     */
    abstract protected function doSave(array $values, int $lifetime);

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function hasItem($key)
    {
        $id = $this->getId($key);

        if (isset($this->deferred[$key])) {
            $this->commit();
        }

        try {
            return $this->doHave($id);
        } catch (\Exception $e) {
            CacheItem::log($this->logger, 'Failed to check if key "{key}" is cached: '.$e->getMessage(), ['key' => $key, 'exception' => $e, 'cache-adapter' => get_debug_type($this)]);

            return false;
        }
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function clear(string $prefix = '')
    {
        $this->deferred = [];
        if ($cleared = $this->versioningIsEnabled) {
            if ('' === $namespaceVersionToClear = $this->namespaceVersion) {
                foreach ($this->doFetch([static::NS_SEPARATOR.$this->namespace]) as $v) {
                    $namespaceVersionToClear = $v;
                }
            }
            $namespaceToClear = $this->namespace.$namespaceVersionToClear;
            $namespaceVersion = self::formatNamespaceVersion(mt_rand());
            try {
                $e = $this->doSave([static::NS_SEPARATOR.$this->namespace => $namespaceVersion], 0);
            } catch (\Exception $e) {
            }
            if (true !== $e && [] !== $e) {
                $cleared = false;
                $message = 'Failed to save the new namespace'.($e instanceof \Exception ? ': '.$e->getMessage() : '.');
                CacheItem::log($this->logger, $message, ['exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
            } else {
                $this->namespaceVersion = $namespaceVersion;
                $this->ids = [];
            }
        } else {
            $namespaceToClear = $this->namespace.$prefix;
        }

        try {
            return $this->doClear($namespaceToClear) || $cleared;
        } catch (\Exception $e) {
            CacheItem::log($this->logger, 'Failed to clear the cache: '.$e->getMessage(), ['exception' => $e, 'cache-adapter' => get_debug_type($this)]);

            return false;
        }
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function deleteItem($key)
    {
        return $this->deleteItems([$key]);
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function deleteItems(array $keys)
    {
        $ids = [];

        foreach ($keys as $key) {
            $ids[$key] = $this->getId($key);
            unset($this->deferred[$key]);
        }

        try {
            if ($this->doDelete($ids)) {
                return true;
            }
        } catch (\Exception $e) {
        }

        $ok = true;

        // When bulk-delete failed, retry each item individually
        foreach ($ids as $key => $id) {
            try {
                $e = null;
                if ($this->doDelete([$id])) {
                    continue;
                }
            } catch (\Exception $e) {
            }
            $message = 'Failed to delete key "{key}"'.($e instanceof \Exception ? ': '.$e->getMessage() : '.');
            CacheItem::log($this->logger, $message, ['key' => $key, 'exception' => $e, 'cache-adapter' => get_debug_type($this)]);
            $ok = false;
        }

        return $ok;
    }

    /**
     * {@inheritdoc}
     */
    public function getItem($key)
    {
        $id = $this->getId($key);

        if (isset($this->deferred[$key])) {
            $this->commit();
        }

        $isHit = false;
        $value = null;

        try {
            foreach ($this->doFetch([$id]) as $value) {
                $isHit = true;
            }

            return (self::$createCacheItem)($key, $value, $isHit);
        } catch (\Exception $e) {
            CacheItem::log($this->logger, 'Failed to fetch key "{key}": '.$e->getMessage(), ['key' => $key, 'exception' => $e, 'cache-adapter' => get_debug_type($this)]);
        }

        return (self::$createCacheItem)($key, null, false);
    }

    /**
     * {@inheritdoc}
     */
    public function getItems(array $keys = [])
    {
        $ids = [];
        $commit = false;

        foreach ($keys as $key) {
            $ids[] = $this->getId($key);
            $commit = $commit || isset($this->deferred[$key]);
        }

        if ($commit) {
            $this->commit();
        }

        try {
            $items = $this->doFetch($ids);
        } catch (\Exception $e) {
            CacheItem::log($this->logger, 'Failed to fetch items: '.$e->getMessage(), ['keys' => $keys, 'exception' => $e, 'cache-adapter' => get_debug_type($this)]);
            $items = [];
        }
        $ids = array_combine($ids, $keys);

        return $this->generateItems($items, $ids);
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function save(CacheItemInterface $item)
    {
        if (!$item instanceof CacheItem) {
            return false;
        }
        $this->deferred[$item->getKey()] = $item;

        return $this->commit();
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function saveDeferred(CacheItemInterface $item)
    {
        if (!$item instanceof CacheItem) {
            return false;
        }
        $this->deferred[$item->getKey()] = $item;

        return true;
    }

    /**
     * Enables/disables versioning of items.
     *
     * When versioning is enabled, clearing the cache is atomic and doesn't require listing existing keys to proceed,
     * but old keys may need garbage collection and extra round-trips to the back-end are required.
     *
     * Calling this method also clears the memoized namespace version and thus forces a resynchronization of it.
     *
     * @return bool the previous state of versioning
     */
    public function enableVersioning(bool $enable = true)
    {
        $wasEnabled = $this->versioningIsEnabled;
        $this->versioningIsEnabled = $enable;
        $this->namespaceVersion = '';
        $this->ids = [];

        return $wasEnabled;
    }

    /**
     * {@inheritdoc}
     */
    public function reset()
    {
        if ($this->deferred) {
            $this->commit();
        }
        $this->namespaceVersion = '';
        $this->ids = [];
    }

    /**
     * @return array
     */
    public function __sleep()
    {
        throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
    }

    public function __wakeup()
    {
        throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
    }

    public function __destruct()
    {
        if ($this->deferred) {
            $this->commit();
        }
    }

    private function generateItems(iterable $items, array &$keys): \Generator
    {
        $f = self::$createCacheItem;

        try {
            foreach ($items as $id => $value) {
                if (!isset($keys[$id])) {
                    throw new InvalidArgumentException(sprintf('Could not match value id "%s" to keys "%s".', $id, implode('", "', $keys)));
                }
                $key = $keys[$id];
                unset($keys[$id]);
                yield $key => $f($key, $value, true);
            }
        } catch (\Exception $e) {
            CacheItem::log($this->logger, 'Failed to fetch items: '.$e->getMessage(), ['keys' => array_values($keys), 'exception' => $e, 'cache-adapter' => get_debug_type($this)]);
        }

        foreach ($keys as $key) {
            yield $key => $f($key, null, false);
        }
    }

    /**
     * @internal
     */
    protected function getId($key)
    {
        if ($this->versioningIsEnabled && '' === $this->namespaceVersion) {
            $this->ids = [];
            $this->namespaceVersion = '1'.static::NS_SEPARATOR;
            try {
                foreach ($this->doFetch([static::NS_SEPARATOR.$this->namespace]) as $v) {
                    $this->namespaceVersion = $v;
                }
                $e = true;
                if ('1'.static::NS_SEPARATOR === $this->namespaceVersion) {
                    $this->namespaceVersion = self::formatNamespaceVersion(time());
                    $e = $this->doSave([static::NS_SEPARATOR.$this->namespace => $this->namespaceVersion], 0);
                }
            } catch (\Exception $e) {
            }
            if (true !== $e && [] !== $e) {
                $message = 'Failed to save the new namespace'.($e instanceof \Exception ? ': '.$e->getMessage() : '.');
                CacheItem::log($this->logger, $message, ['exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
            }
        }

        if (\is_string($key) && isset($this->ids[$key])) {
            return $this->namespace.$this->namespaceVersion.$this->ids[$key];
        }
        \assert('' !== CacheItem::validateKey($key));
        $this->ids[$key] = $key;

        if (\count($this->ids) > 1000) {
            $this->ids = \array_slice($this->ids, 500, null, true); // stop memory leak if there are many keys
        }

        if (null === $this->maxIdLength) {
            return $this->namespace.$this->namespaceVersion.$key;
        }
        if (\strlen($id = $this->namespace.$this->namespaceVersion.$key) > $this->maxIdLength) {
            // Use MD5 to favor speed over security, which is not an issue here
            $this->ids[$key] = $id = substr_replace(base64_encode(hash('md5', $key, true)), static::NS_SEPARATOR, -(\strlen($this->namespaceVersion) + 2));
            $id = $this->namespace.$this->namespaceVersion.$id;
        }

        return $id;
    }

    /**
     * @internal
     */
    public static function handleUnserializeCallback(string $class)
    {
        throw new \DomainException('Class not found: '.$class);
    }

    private static function formatNamespaceVersion(int $value): string
    {
        return strtr(substr_replace(base64_encode(pack('V', $value)), static::NS_SEPARATOR, 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\Cache\Traits;

use Psr\Log\LoggerInterface;
use Symfony\Component\Cache\Adapter\AdapterInterface;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\LockRegistry;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\CacheTrait;
use Symfony\Contracts\Cache\ItemInterface;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @internal
 */
trait ContractsTrait
{
    use CacheTrait {
        doGet as private contractsGet;
    }

    private $callbackWrapper;
    private $computing = [];

    /**
     * Wraps the callback passed to ->get() in a callable.
     *
     * @return callable the previous callback wrapper
     */
    public function setCallbackWrapper(?callable $callbackWrapper): callable
    {
        if (!isset($this->callbackWrapper)) {
            $this->callbackWrapper = \Closure::fromCallable([LockRegistry::class, 'compute']);

            if (\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true)) {
                $this->setCallbackWrapper(null);
            }
        }

        $previousWrapper = $this->callbackWrapper;
        $this->callbackWrapper = $callbackWrapper ?? static function (callable $callback, ItemInterface $item, bool &$save, CacheInterface $pool, \Closure $setMetadata, ?LoggerInterface $logger) {
            return $callback($item, $save);
        };

        return $previousWrapper;
    }

    private function doGet(AdapterInterface $pool, string $key, callable $callback, ?float $beta, ?array &$metadata = null)
    {
        if (0 > $beta = $beta ?? 1.0) {
            throw new InvalidArgumentException(sprintf('Argument "$beta" provided to "%s::get()" must be a positive number, %f given.', static::class, $beta));
        }

        static $setMetadata;

        $setMetadata ?? $setMetadata = \Closure::bind(
            static function (CacheItem $item, float $startTime, ?array &$metadata) {
                if ($item->expiry > $endTime = microtime(true)) {
                    $item->newMetadata[CacheItem::METADATA_EXPIRY] = $metadata[CacheItem::METADATA_EXPIRY] = $item->expiry;
                    $item->newMetadata[CacheItem::METADATA_CTIME] = $metadata[CacheItem::METADATA_CTIME] = (int) ceil(1000 * ($endTime - $startTime));
                } else {
                    unset($metadata[CacheItem::METADATA_EXPIRY], $metadata[CacheItem::METADATA_CTIME]);
                }
            },
            null,
            CacheItem::class
        );

        return $this->contractsGet($pool, $key, function (CacheItem $item, bool &$save) use ($pool, $callback, $setMetadata, &$metadata, $key) {
            // don't wrap nor save recursive calls
            if (isset($this->computing[$key])) {
                $value = $callback($item, $save);
                $save = false;

                return $value;
            }

            $this->computing[$key] = $key;
            $startTime = microtime(true);

            if (!isset($this->callbackWrapper)) {
                $this->setCallbackWrapper($this->setCallbackWrapper(null));
            }

            try {
                $value = ($this->callbackWrapper)($callback, $item, $save, $pool, function (CacheItem $item) use ($setMetadata, $startTime, &$metadata) {
                    $setMetadata($item, $startTime, $metadata);
                }, $this->logger ?? null);
                $setMetadata($item, $startTime, $metadata);

                return $value;
            } finally {
                unset($this->computing[$key]);
            }
        }, $beta, $metadata, $this->logger ?? 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\Cache\Traits;

use Symfony\Component\Cache\Exception\InvalidArgumentException;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @internal
 */
trait FilesystemCommonTrait
{
    private $directory;
    private $tmp;

    private function init(string $namespace, ?string $directory)
    {
        if (!isset($directory[0])) {
            $directory = sys_get_temp_dir().\DIRECTORY_SEPARATOR.'symfony-cache';
        } else {
            $directory = realpath($directory) ?: $directory;
        }
        if (isset($namespace[0])) {
            if (preg_match('#[^-+_.A-Za-z0-9]#', $namespace, $match)) {
                throw new InvalidArgumentException(sprintf('Namespace contains "%s" but only characters in [-+_.A-Za-z0-9] are allowed.', $match[0]));
            }
            $directory .= \DIRECTORY_SEPARATOR.$namespace;
        } else {
            $directory .= \DIRECTORY_SEPARATOR.'@';
        }
        if (!is_dir($directory)) {
            @mkdir($directory, 0777, true);
        }
        $directory .= \DIRECTORY_SEPARATOR;
        // On Windows the whole path is limited to 258 chars
        if ('\\' === \DIRECTORY_SEPARATOR && \strlen($directory) > 234) {
            throw new InvalidArgumentException(sprintf('Cache directory too long (%s).', $directory));
        }

        $this->directory = $directory;
    }

    /**
     * {@inheritdoc}
     */
    protected function doClear(string $namespace)
    {
        $ok = true;

        foreach ($this->scanHashDir($this->directory) as $file) {
            if ('' !== $namespace && !str_starts_with($this->getFileKey($file), $namespace)) {
                continue;
            }

            $ok = ($this->doUnlink($file) || !file_exists($file)) && $ok;
        }

        return $ok;
    }

    /**
     * {@inheritdoc}
     */
    protected function doDelete(array $ids)
    {
        $ok = true;

        foreach ($ids as $id) {
            $file = $this->getFile($id);
            $ok = (!is_file($file) || $this->doUnlink($file) || !file_exists($file)) && $ok;
        }

        return $ok;
    }

    protected function doUnlink(string $file)
    {
        return @unlink($file);
    }

    private function write(string $file, string $data, ?int $expiresAt = null)
    {
        $unlink = false;
        set_error_handler(__CLASS__.'::throwError');
        try {
            if (null === $this->tmp) {
                $this->tmp = $this->directory.bin2hex(random_bytes(6));
            }
            try {
                $h = fopen($this->tmp, 'x');
            } catch (\ErrorException $e) {
                if (!str_contains($e->getMessage(), 'File exists')) {
                    throw $e;
                }

                $this->tmp = $this->directory.bin2hex(random_bytes(6));
                $h = fopen($this->tmp, 'x');
            }
            fwrite($h, $data);
            fclose($h);
            $unlink = true;

            if (null !== $expiresAt) {
                touch($this->tmp, $expiresAt ?: time() + 31556952); // 1 year in seconds
            }

            if ('\\' === \DIRECTORY_SEPARATOR) {
                $success = copy($this->tmp, $file);
                $unlink = true;
            } else {
                $success = rename($this->tmp, $file);
                $unlink = !$success;
            }

            return $success;
        } finally {
            restore_error_handler();

            if ($unlink) {
                @unlink($this->tmp);
            }
        }
    }

    private function getFile(string $id, bool $mkdir = false, ?string $directory = null)
    {
        // Use MD5 to favor speed over security, which is not an issue here
        $hash = str_replace('/', '-', base64_encode(hash('md5', static::class.$id, true)));
        $dir = ($directory ?? $this->directory).strtoupper($hash[0].\DIRECTORY_SEPARATOR.$hash[1].\DIRECTORY_SEPARATOR);

        if ($mkdir && !is_dir($dir)) {
            @mkdir($dir, 0777, true);
        }

        return $dir.substr($hash, 2, 20);
    }

    private function getFileKey(string $file): string
    {
        return '';
    }

    private function scanHashDir(string $directory): \Generator
    {
        if (!is_dir($directory)) {
            return;
        }

        $chars = '+-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';

        for ($i = 0; $i < 38; ++$i) {
            if (!is_dir($directory.$chars[$i])) {
                continue;
            }

            for ($j = 0; $j < 38; ++$j) {
                if (!is_dir($dir = $directory.$chars[$i].\DIRECTORY_SEPARATOR.$chars[$j])) {
                    continue;
                }

                foreach (@scandir($dir, \SCANDIR_SORT_NONE) ?: [] as $file) {
                    if ('.' !== $file && '..' !== $file) {
                        yield $dir.\DIRECTORY_SEPARATOR.$file;
                    }
                }
            }
        }
    }

    /**
     * @internal
     */
    public static function throwError(int $type, string $message, string $file, int $line)
    {
        throw new \ErrorException($message, 0, $type, $file, $line);
    }

    /**
     * @return array
     */
    public function __sleep()
    {
        throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
    }

    public function __wakeup()
    {
        throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
    }

    public function __destruct()
    {
        if (method_exists(parent::class, '__destruct')) {
            parent::__destruct();
        }
        if (null !== $this->tmp && is_file($this->tmp)) {
            unlink($this->tmp);
        }
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Cache;

use Psr\Log\LoggerInterface;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\Exception\LogicException;
use Symfony\Contracts\Cache\ItemInterface;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 */
final class CacheItem implements ItemInterface
{
    private const METADATA_EXPIRY_OFFSET = 1527506807;

    protected $key;
    protected $value;
    protected $isHit = false;
    protected $expiry;
    protected $metadata = [];
    protected $newMetadata = [];
    protected $innerItem;
    protected $poolHash;
    protected $isTaggable = false;

    /**
     * {@inheritdoc}
     */
    public function getKey(): string
    {
        return $this->key;
    }

    /**
     * {@inheritdoc}
     *
     * @return mixed
     */
    public function get()
    {
        return $this->value;
    }

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

    /**
     * {@inheritdoc}
     *
     * @return $this
     */
    public function set($value): self
    {
        $this->value = $value;

        return $this;
    }

    /**
     * {@inheritdoc}
     *
     * @return $this
     */
    public function expiresAt($expiration): self
    {
        if (null === $expiration) {
            $this->expiry = null;
        } elseif ($expiration instanceof \DateTimeInterface) {
            $this->expiry = (float) $expiration->format('U.u');
        } else {
            throw new InvalidArgumentException(sprintf('Expiration date must implement DateTimeInterface or be null, "%s" given.', get_debug_type($expiration)));
        }

        return $this;
    }

    /**
     * {@inheritdoc}
     *
     * @return $this
     */
    public function expiresAfter($time): self
    {
        if (null === $time) {
            $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 InvalidArgumentException(sprintf('Expiration date must be an integer, a DateInterval or null, "%s" given.', get_debug_type($time)));
        }

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function tag($tags): ItemInterface
    {
        if (!$this->isTaggable) {
            throw new LogicException(sprintf('Cache item "%s" comes from a non tag-aware pool: you cannot tag it.', $this->key));
        }
        if (!is_iterable($tags)) {
            $tags = [$tags];
        }
        foreach ($tags as $tag) {
            if (!\is_string($tag) && !(\is_object($tag) && method_exists($tag, '__toString'))) {
                throw new InvalidArgumentException(sprintf('Cache tag must be string or object that implements __toString(), "%s" given.', \is_object($tag) ? \get_class($tag) : \gettype($tag)));
            }
            $tag = (string) $tag;
            if (isset($this->newMetadata[self::METADATA_TAGS][$tag])) {
                continue;
            }
            if ('' === $tag) {
                throw new InvalidArgumentException('Cache tag length must be greater than zero.');
            }
            if (false !== strpbrk($tag, self::RESERVED_CHARACTERS)) {
                throw new InvalidArgumentException(sprintf('Cache tag "%s" contains reserved characters "%s".', $tag, self::RESERVED_CHARACTERS));
            }
            $this->newMetadata[self::METADATA_TAGS][$tag] = $tag;
        }

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function getMetadata(): array
    {
        return $this->metadata;
    }

    /**
     * Validates a cache key according to PSR-6.
     *
     * @param mixed $key The key to validate
     *
     * @throws InvalidArgumentException When $key is not valid
     */
    public static function validateKey($key): string
    {
        if (!\is_string($key)) {
            throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', get_debug_type($key)));
        }
        if ('' === $key) {
            throw new InvalidArgumentException('Cache key length must be greater than zero.');
        }
        if (false !== strpbrk($key, self::RESERVED_CHARACTERS)) {
            throw new InvalidArgumentException(sprintf('Cache key "%s" contains reserved characters "%s".', $key, self::RESERVED_CHARACTERS));
        }

        return $key;
    }

    /**
     * Internal logging helper.
     *
     * @internal
     */
    public static function log(?LoggerInterface $logger, string $message, array $context = [])
    {
        if ($logger) {
            $logger->warning($message, $context);
        } else {
            $replace = [];
            foreach ($context as $k => $v) {
                if (\is_scalar($v)) {
                    $replace['{'.$k.'}'] = $v;
                }
            }
            @trigger_error(strtr($message, $replace), \E_USER_WARNING);
        }
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Cache\DataCollector;

use Symfony\Component\Cache\Adapter\TraceableAdapter;
use Symfony\Component\Cache\Adapter\TraceableAdapterEvent;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;

/**
 * @author Aaron Scherer <aequasi@gmail.com>
 * @author Tobias Nyholm <tobias.nyholm@gmail.com>
 *
 * @final
 */
class CacheDataCollector extends DataCollector implements LateDataCollectorInterface
{
    /**
     * @var TraceableAdapter[]
     */
    private $instances = [];

    public function addInstance(string $name, TraceableAdapter $instance)
    {
        $this->instances[$name] = $instance;
    }

    /**
     * {@inheritdoc}
     */
    public function collect(Request $request, Response $response, ?\Throwable $exception = null)
    {
        $empty = ['calls' => [], 'config' => [], 'options' => [], 'statistics' => []];
        $this->data = ['instances' => $empty, 'total' => $empty];
        foreach ($this->instances as $name => $instance) {
            $this->data['instances']['calls'][$name] = $instance->getCalls();
        }

        $this->data['instances']['statistics'] = $this->calculateStatistics();
        $this->data['total']['statistics'] = $this->calculateTotalStatistics();
    }

    public function reset()
    {
        $this->data = [];
        foreach ($this->instances as $instance) {
            $instance->clearCalls();
        }
    }

    public function lateCollect()
    {
        $this->data['instances']['calls'] = $this->cloneVar($this->data['instances']['calls']);
    }

    /**
     * {@inheritdoc}
     */
    public function getName(): string
    {
        return 'cache';
    }

    /**
     * Method returns amount of logged Cache reads: "get" calls.
     */
    public function getStatistics(): array
    {
        return $this->data['instances']['statistics'];
    }

    /**
     * Method returns the statistic totals.
     */
    public function getTotals(): array
    {
        return $this->data['total']['statistics'];
    }

    /**
     * Method returns all logged Cache call objects.
     *
     * @return mixed
     */
    public function getCalls()
    {
        return $this->data['instances']['calls'];
    }

    private function calculateStatistics(): array
    {
        $statistics = [];
        foreach ($this->data['instances']['calls'] as $name => $calls) {
            $statistics[$name] = [
                'calls' => 0,
                'time' => 0,
                'reads' => 0,
                'writes' => 0,
                'deletes' => 0,
                'hits' => 0,
                'misses' => 0,
            ];
            /** @var TraceableAdapterEvent $call */
            foreach ($calls as $call) {
                ++$statistics[$name]['calls'];
                $statistics[$name]['time'] += ($call->end ?? microtime(true)) - $call->start;
                if ('get' === $call->name) {
                    ++$statistics[$name]['reads'];
                    if ($call->hits) {
                        ++$statistics[$name]['hits'];
                    } else {
                        ++$statistics[$name]['misses'];
                        ++$statistics[$name]['writes'];
                    }
                } elseif ('getItem' === $call->name) {
                    ++$statistics[$name]['reads'];
                    if ($call->hits) {
                        ++$statistics[$name]['hits'];
                    } else {
                        ++$statistics[$name]['misses'];
                    }
                } elseif ('getItems' === $call->name) {
                    $statistics[$name]['reads'] += $call->hits + $call->misses;
                    $statistics[$name]['hits'] += $call->hits;
                    $statistics[$name]['misses'] += $call->misses;
                } elseif ('hasItem' === $call->name) {
                    ++$statistics[$name]['reads'];
                    foreach ($call->result ?? [] as $result) {
                        ++$statistics[$name][$result ? 'hits' : 'misses'];
                    }
                } elseif ('save' === $call->name) {
                    ++$statistics[$name]['writes'];
                } elseif ('deleteItem' === $call->name) {
                    ++$statistics[$name]['deletes'];
                }
            }
            if ($statistics[$name]['reads']) {
                $statistics[$name]['hit_read_ratio'] = round(100 * $statistics[$name]['hits'] / $statistics[$name]['reads'], 2);
            } else {
                $statistics[$name]['hit_read_ratio'] = null;
            }
        }

        return $statistics;
    }

    private function calculateTotalStatistics(): array
    {
        $statistics = $this->getStatistics();
        $totals = [
            'calls' => 0,
            'time' => 0,
            'reads' => 0,
            'writes' => 0,
            'deletes' => 0,
            'hits' => 0,
            'misses' => 0,
        ];
        foreach ($statistics as $name => $values) {
            foreach ($totals as $key => $value) {
                $totals[$key] += $statistics[$name][$key];
            }
        }
        if ($totals['reads']) {
            $totals['hit_read_ratio'] = round(100 * $totals['hits'] / $totals['reads'], 2);
        } else {
            $totals['hit_read_ratio'] = null;
        }

        return $totals;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Cache;

use Psr\Log\LoggerInterface;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;

/**
 * LockRegistry is used internally by existing adapters to protect against cache stampede.
 *
 * It does so by wrapping the computation of items in a pool of locks.
 * Foreach each apps, there can be at most 20 concurrent processes that
 * compute items at the same time and only one per cache-key.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
final class LockRegistry
{
    private static $openedFiles = [];
    private static $lockedFiles;
    private static $signalingException;
    private static $signalingCallback;

    /**
     * The number of items in this list controls the max number of concurrent processes.
     */
    private static $files = [
        __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'AbstractAdapter.php',
        __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'AbstractTagAwareAdapter.php',
        __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'AdapterInterface.php',
        __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'ApcuAdapter.php',
        __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'ArrayAdapter.php',
        __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'ChainAdapter.php',
        __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'CouchbaseBucketAdapter.php',
        __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'CouchbaseCollectionAdapter.php',
        __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'DoctrineAdapter.php',
        __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'DoctrineDbalAdapter.php',
        __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'FilesystemAdapter.php',
        __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'FilesystemTagAwareAdapter.php',
        __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'MemcachedAdapter.php',
        __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'NullAdapter.php',
        __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'ParameterNormalizer.php',
        __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'PdoAdapter.php',
        __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'PhpArrayAdapter.php',
        __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'PhpFilesAdapter.php',
        __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'ProxyAdapter.php',
        __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'Psr16Adapter.php',
        __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'RedisAdapter.php',
        __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'RedisTagAwareAdapter.php',
        __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'TagAwareAdapter.php',
        __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'TagAwareAdapterInterface.php',
        __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'TraceableAdapter.php',
        __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'TraceableTagAwareAdapter.php',
    ];

    /**
     * Defines a set of existing files that will be used as keys to acquire locks.
     *
     * @return array The previously defined set of files
     */
    public static function setFiles(array $files): array
    {
        $previousFiles = self::$files;
        self::$files = $files;

        foreach (self::$openedFiles as $file) {
            if ($file) {
                flock($file, \LOCK_UN);
                fclose($file);
            }
        }
        self::$openedFiles = self::$lockedFiles = [];

        return $previousFiles;
    }

    public static function compute(callable $callback, ItemInterface $item, bool &$save, CacheInterface $pool, ?\Closure $setMetadata = null, ?LoggerInterface $logger = null)
    {
        if ('\\' === \DIRECTORY_SEPARATOR && null === self::$lockedFiles) {
            // disable locking on Windows by default
            self::$files = self::$lockedFiles = [];
        }

        $key = self::$files ? abs(crc32($item->getKey())) % \count(self::$files) : -1;

        if ($key < 0 || self::$lockedFiles || !$lock = self::open($key)) {
            return $callback($item, $save);
        }

        self::$signalingException ?? self::$signalingException = unserialize("O:9:\"Exception\":1:{s:16:\"\0Exception\0trace\";a:0:{}}");
        self::$signalingCallback ?? self::$signalingCallback = function () { throw self::$signalingException; };

        while (true) {
            try {
                $locked = false;
                // race to get the lock in non-blocking mode
                $locked = flock($lock, \LOCK_EX | \LOCK_NB, $wouldBlock);

                if ($locked || !$wouldBlock) {
                    $logger && $logger->info(sprintf('Lock %s, now computing item "{key}"', $locked ? 'acquired' : 'not supported'), ['key' => $item->getKey()]);
                    self::$lockedFiles[$key] = true;

                    $value = $callback($item, $save);

                    if ($save) {
                        if ($setMetadata) {
                            $setMetadata($item);
                        }

                        $pool->save($item->set($value));
                        $save = false;
                    }

                    return $value;
                }
                // if we failed the race, retry locking in blocking mode to wait for the winner
                $logger && $logger->info('Item "{key}" is locked, waiting for it to be released', ['key' => $item->getKey()]);
                flock($lock, \LOCK_SH);
            } finally {
                flock($lock, \LOCK_UN);
                unset(self::$lockedFiles[$key]);
            }

            try {
                $value = $pool->get($item->getKey(), self::$signalingCallback, 0);
                $logger && $logger->info('Item "{key}" retrieved after lock was released', ['key' => $item->getKey()]);
                $save = false;

                return $value;
            } catch (\Exception $e) {
                if (self::$signalingException !== $e) {
                    throw $e;
                }
                $logger && $logger->info('Item "{key}" not found while lock was released, now retrying', ['key' => $item->getKey()]);
            }
        }

        return null;
    }

    private static function open(int $key)
    {
        if (null !== $h = self::$openedFiles[$key] ?? null) {
            return $h;
        }
        set_error_handler(function () {});
        try {
            $h = fopen(self::$files[$key], 'r+');
        } finally {
            restore_error_handler();
        }

        return self::$openedFiles[$key] = $h ?: @fopen(self::$files[$key], 'r');
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Cache\Adapter;

use Symfony\Component\Cache\Marshaller\MarshallerInterface;
use Symfony\Component\Cache\Marshaller\TagAwareMarshaller;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\Cache\Traits\FilesystemTrait;

/**
 * Stores tag id <> cache id relationship as a symlink, and lookup on invalidation calls.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 * @author André Rømcke <andre.romcke+symfony@gmail.com>
 */
class FilesystemTagAwareAdapter extends AbstractTagAwareAdapter implements PruneableInterface
{
    use FilesystemTrait {
        doClear as private doClearCache;
        doSave as private doSaveCache;
    }

    /**
     * Folder used for tag symlinks.
     */
    private const TAG_FOLDER = 'tags';

    public function __construct(string $namespace = '', int $defaultLifetime = 0, ?string $directory = null, ?MarshallerInterface $marshaller = null)
    {
        $this->marshaller = new TagAwareMarshaller($marshaller);
        parent::__construct('', $defaultLifetime);
        $this->init($namespace, $directory);
    }

    /**
     * {@inheritdoc}
     */
    protected function doClear(string $namespace)
    {
        $ok = $this->doClearCache($namespace);

        if ('' !== $namespace) {
            return $ok;
        }

        set_error_handler(static function () {});
        $chars = '+-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';

        try {
            foreach ($this->scanHashDir($this->directory.self::TAG_FOLDER.\DIRECTORY_SEPARATOR) as $dir) {
                if (rename($dir, $renamed = substr_replace($dir, bin2hex(random_bytes(4)), -8))) {
                    $dir = $renamed.\DIRECTORY_SEPARATOR;
                } else {
                    $dir .= \DIRECTORY_SEPARATOR;
                    $renamed = null;
                }

                for ($i = 0; $i < 38; ++$i) {
                    if (!is_dir($dir.$chars[$i])) {
                        continue;
                    }
                    for ($j = 0; $j < 38; ++$j) {
                        if (!is_dir($d = $dir.$chars[$i].\DIRECTORY_SEPARATOR.$chars[$j])) {
                            continue;
                        }
                        foreach (scandir($d, \SCANDIR_SORT_NONE) ?: [] as $link) {
                            if ('.' !== $link && '..' !== $link && (null !== $renamed || !realpath($d.\DIRECTORY_SEPARATOR.$link))) {
                                unlink($d.\DIRECTORY_SEPARATOR.$link);
                            }
                        }
                        null === $renamed ?: rmdir($d);
                    }
                    null === $renamed ?: rmdir($dir.$chars[$i]);
                }
                null === $renamed ?: rmdir($renamed);
            }
        } finally {
            restore_error_handler();
        }

        return $ok;
    }

    /**
     * {@inheritdoc}
     */
    protected function doSave(array $values, int $lifetime, array $addTagData = [], array $removeTagData = []): array
    {
        $failed = $this->doSaveCache($values, $lifetime);

        // Add Tags as symlinks
        foreach ($addTagData as $tagId => $ids) {
            $tagFolder = $this->getTagFolder($tagId);
            foreach ($ids as $id) {
                if ($failed && \in_array($id, $failed, true)) {
                    continue;
                }

                $file = $this->getFile($id);

                if (!@symlink($file, $tagLink = $this->getFile($id, true, $tagFolder)) && !is_link($tagLink)) {
                    @unlink($file);
                    $failed[] = $id;
                }
            }
        }

        // Unlink removed Tags
        foreach ($removeTagData as $tagId => $ids) {
            $tagFolder = $this->getTagFolder($tagId);
            foreach ($ids as $id) {
                if ($failed && \in_array($id, $failed, true)) {
                    continue;
                }

                @unlink($this->getFile($id, false, $tagFolder));
            }
        }

        return $failed;
    }

    /**
     * {@inheritdoc}
     */
    protected function doDeleteYieldTags(array $ids): iterable
    {
        foreach ($ids as $id) {
            $file = $this->getFile($id);
            if (!is_file($file) || !$h = @fopen($file, 'r')) {
                continue;
            }

            if ((\PHP_VERSION_ID >= 70300 || '\\' !== \DIRECTORY_SEPARATOR) && !@unlink($file)) {
                fclose($h);
                continue;
            }

            $meta = explode("\n", fread($h, 4096), 3)[2] ?? '';

            // detect the compact format used in marshall() using magic numbers in the form 9D-..-..-..-..-00-..-..-..-5F
            if (13 < \strlen($meta) && "\x9D" === $meta[0] && "\0" === $meta[5] && "\x5F" === $meta[9]) {
                $meta[9] = "\0";
                $tagLen = unpack('Nlen', $meta, 9)['len'];
                $meta = substr($meta, 13, $tagLen);

                if (0 < $tagLen -= \strlen($meta)) {
                    $meta .= fread($h, $tagLen);
                }

                try {
                    yield $id => '' === $meta ? [] : $this->marshaller->unmarshall($meta);
                } catch (\Exception $e) {
                    yield $id => [];
                }
            }

            fclose($h);

            if (\PHP_VERSION_ID < 70300 && '\\' === \DIRECTORY_SEPARATOR) {
                @unlink($file);
            }
        }
    }

    /**
     * {@inheritdoc}
     */
    protected function doDeleteTagRelations(array $tagData): bool
    {
        foreach ($tagData as $tagId => $idList) {
            $tagFolder = $this->getTagFolder($tagId);
            foreach ($idList as $id) {
                @unlink($this->getFile($id, false, $tagFolder));
            }
        }

        return true;
    }

    /**
     * {@inheritdoc}
     */
    protected function doInvalidate(array $tagIds): bool
    {
        foreach ($tagIds as $tagId) {
            if (!is_dir($tagFolder = $this->getTagFolder($tagId))) {
                continue;
            }

            set_error_handler(static function () {});

            try {
                if (rename($tagFolder, $renamed = substr_replace($tagFolder, bin2hex(random_bytes(4)), -9))) {
                    $tagFolder = $renamed.\DIRECTORY_SEPARATOR;
                } else {
                    $renamed = null;
                }

                foreach ($this->scanHashDir($tagFolder) as $itemLink) {
                    unlink(realpath($itemLink) ?: $itemLink);
                    unlink($itemLink);
                }

                if (null === $renamed) {
                    continue;
                }

                $chars = '+-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';

                for ($i = 0; $i < 38; ++$i) {
                    for ($j = 0; $j < 38; ++$j) {
                        rmdir($tagFolder.$chars[$i].\DIRECTORY_SEPARATOR.$chars[$j]);
                    }
                    rmdir($tagFolder.$chars[$i]);
                }
                rmdir($renamed);
            } finally {
                restore_error_handler();
            }
        }

        return true;
    }

    private function getTagFolder(string $tagId): string
    {
        return $this->getFile($tagId, false, $this->directory.self::TAG_FOLDER.\DIRECTORY_SEPARATOR).\DIRECTORY_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\Cache\Adapter;

use Symfony\Component\Cache\Exception\CacheException;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;

/**
 * @author Antonio Jose Cerezo Aranda <aj.cerezo@gmail.com>
 */
class CouchbaseBucketAdapter extends AbstractAdapter
{
    private const THIRTY_DAYS_IN_SECONDS = 2592000;
    private const MAX_KEY_LENGTH = 250;
    private const KEY_NOT_FOUND = 13;
    private const VALID_DSN_OPTIONS = [
        'operationTimeout',
        'configTimeout',
        'configNodeTimeout',
        'n1qlTimeout',
        'httpTimeout',
        'configDelay',
        'htconfigIdleTimeout',
        'durabilityInterval',
        'durabilityTimeout',
    ];

    private $bucket;
    private $marshaller;

    public function __construct(\CouchbaseBucket $bucket, string $namespace = '', int $defaultLifetime = 0, ?MarshallerInterface $marshaller = null)
    {
        if (!static::isSupported()) {
            throw new CacheException('Couchbase >= 2.6.0 < 3.0.0 is required.');
        }

        $this->maxIdLength = static::MAX_KEY_LENGTH;

        $this->bucket = $bucket;

        parent::__construct($namespace, $defaultLifetime);
        $this->enableVersioning();
        $this->marshaller = $marshaller ?? new DefaultMarshaller();
    }

    /**
     * @param array|string $servers
     */
    public static function createConnection($servers, array $options = []): \CouchbaseBucket
    {
        if (\is_string($servers)) {
            $servers = [$servers];
        } elseif (!\is_array($servers)) {
            throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be array or string, "%s" given.', __METHOD__, get_debug_type($servers)));
        }

        if (!static::isSupported()) {
            throw new CacheException('Couchbase >= 2.6.0 < 3.0.0 is required.');
        }

        set_error_handler(function ($type, $msg, $file, $line) { throw new \ErrorException($msg, 0, $type, $file, $line); });

        $dsnPattern = '/^(?<protocol>couchbase(?:s)?)\:\/\/(?:(?<username>[^\:]+)\:(?<password>[^\@]{6,})@)?'
            .'(?<host>[^\:]+(?:\:\d+)?)(?:\/(?<bucketName>[^\?]+))(?:\?(?<options>.*))?$/i';

        $newServers = [];
        $protocol = 'couchbase';
        try {
            $options = self::initOptions($options);
            $username = $options['username'];
            $password = $options['password'];

            foreach ($servers as $dsn) {
                if (0 !== strpos($dsn, 'couchbase:')) {
                    throw new InvalidArgumentException('Invalid Couchbase DSN: it does not start with "couchbase:".');
                }

                preg_match($dsnPattern, $dsn, $matches);

                $username = $matches['username'] ?: $username;
                $password = $matches['password'] ?: $password;
                $protocol = $matches['protocol'] ?: $protocol;

                if (isset($matches['options'])) {
                    $optionsInDsn = self::getOptions($matches['options']);

                    foreach ($optionsInDsn as $parameter => $value) {
                        $options[$parameter] = $value;
                    }
                }

                $newServers[] = $matches['host'];
            }

            $connectionString = $protocol.'://'.implode(',', $newServers);

            $client = new \CouchbaseCluster($connectionString);
            $client->authenticateAs($username, $password);

            $bucket = $client->openBucket($matches['bucketName']);

            unset($options['username'], $options['password']);
            foreach ($options as $option => $value) {
                if (!empty($value)) {
                    $bucket->$option = $value;
                }
            }

            return $bucket;
        } finally {
            restore_error_handler();
        }
    }

    public static function isSupported(): bool
    {
        return \extension_loaded('couchbase') && version_compare(phpversion('couchbase'), '2.6.0', '>=') && version_compare(phpversion('couchbase'), '3.0', '<');
    }

    private static function getOptions(string $options): array
    {
        $results = [];
        $optionsInArray = explode('&', $options);

        foreach ($optionsInArray as $option) {
            [$key, $value] = explode('=', $option);

            if (\in_array($key, static::VALID_DSN_OPTIONS, true)) {
                $results[$key] = $value;
            }
        }

        return $results;
    }

    private static function initOptions(array $options): array
    {
        $options['username'] = $options['username'] ?? '';
        $options['password'] = $options['password'] ?? '';
        $options['operationTimeout'] = $options['operationTimeout'] ?? 0;
        $options['configTimeout'] = $options['configTimeout'] ?? 0;
        $options['configNodeTimeout'] = $options['configNodeTimeout'] ?? 0;
        $options['n1qlTimeout'] = $options['n1qlTimeout'] ?? 0;
        $options['httpTimeout'] = $options['httpTimeout'] ?? 0;
        $options['configDelay'] = $options['configDelay'] ?? 0;
        $options['htconfigIdleTimeout'] = $options['htconfigIdleTimeout'] ?? 0;
        $options['durabilityInterval'] = $options['durabilityInterval'] ?? 0;
        $options['durabilityTimeout'] = $options['durabilityTimeout'] ?? 0;

        return $options;
    }

    /**
     * {@inheritdoc}
     */
    protected function doFetch(array $ids)
    {
        $resultsCouchbase = $this->bucket->get($ids);

        $results = [];
        foreach ($resultsCouchbase as $key => $value) {
            if (null !== $value->error) {
                continue;
            }
            $results[$key] = $this->marshaller->unmarshall($value->value);
        }

        return $results;
    }

    /**
     * {@inheritdoc}
     */
    protected function doHave(string $id): bool
    {
        return false !== $this->bucket->get($id);
    }

    /**
     * {@inheritdoc}
     */
    protected function doClear(string $namespace): bool
    {
        if ('' === $namespace) {
            $this->bucket->manager()->flush();

            return true;
        }

        return false;
    }

    /**
     * {@inheritdoc}
     */
    protected function doDelete(array $ids): bool
    {
        $results = $this->bucket->remove(array_values($ids));

        foreach ($results as $key => $result) {
            if (null !== $result->error && static::KEY_NOT_FOUND !== $result->error->getCode()) {
                continue;
            }
            unset($results[$key]);
        }

        return 0 === \count($results);
    }

    /**
     * {@inheritdoc}
     */
    protected function doSave(array $values, int $lifetime)
    {
        if (!$values = $this->marshaller->marshall($values, $failed)) {
            return $failed;
        }

        $lifetime = $this->normalizeExpiry($lifetime);

        $ko = [];
        foreach ($values as $key => $value) {
            $result = $this->bucket->upsert($key, $value, ['expiry' => $lifetime]);

            if (null !== $result->error) {
                $ko[$key] = $result;
            }
        }

        return [] === $ko ? true : $ko;
    }

    private function normalizeExpiry(int $expiry): int
    {
        if ($expiry && $expiry > static::THIRTY_DAYS_IN_SECONDS) {
            $expiry += time();
        }

        return $expiry;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Cache\Adapter;

use Doctrine\Common\Cache\CacheProvider;
use Doctrine\Common\Cache\Psr6\CacheAdapter;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @deprecated Since Symfony 5.4, use Doctrine\Common\Cache\Psr6\CacheAdapter instead
 */
class DoctrineAdapter extends AbstractAdapter
{
    private $provider;

    public function __construct(CacheProvider $provider, string $namespace = '', int $defaultLifetime = 0)
    {
        trigger_deprecation('symfony/cache', '5.4', '"%s" is deprecated, use "%s" instead.', __CLASS__, CacheAdapter::class);

        parent::__construct('', $defaultLifetime);
        $this->provider = $provider;
        $provider->setNamespace($namespace);
    }

    /**
     * {@inheritdoc}
     */
    public function reset()
    {
        parent::reset();
        $this->provider->setNamespace($this->provider->getNamespace());
    }

    /**
     * {@inheritdoc}
     */
    protected function doFetch(array $ids)
    {
        $unserializeCallbackHandler = ini_set('unserialize_callback_func', parent::class.'::handleUnserializeCallback');
        try {
            return $this->provider->fetchMultiple($ids);
        } catch (\Error $e) {
            $trace = $e->getTrace();

            if (isset($trace[0]['function']) && !isset($trace[0]['class'])) {
                switch ($trace[0]['function']) {
                    case 'unserialize':
                    case 'apcu_fetch':
                    case 'apc_fetch':
                        throw new \ErrorException($e->getMessage(), $e->getCode(), \E_ERROR, $e->getFile(), $e->getLine());
                }
            }

            throw $e;
        } finally {
            ini_set('unserialize_callback_func', $unserializeCallbackHandler);
        }
    }

    /**
     * {@inheritdoc}
     */
    protected function doHave(string $id)
    {
        return $this->provider->contains($id);
    }

    /**
     * {@inheritdoc}
     */
    protected function doClear(string $namespace)
    {
        $namespace = $this->provider->getNamespace();

        return isset($namespace[0])
            ? $this->provider->deleteAll()
            : $this->provider->flushAll();
    }

    /**
     * {@inheritdoc}
     */
    protected function doDelete(array $ids)
    {
        $ok = true;
        foreach ($ids as $id) {
            $ok = $this->provider->delete($id) && $ok;
        }

        return $ok;
    }

    /**
     * {@inheritdoc}
     */
    protected function doSave(array $values, int $lifetime)
    {
        return $this->provider->saveMultiple($values, $lifetime);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Cache\Adapter;

/**
 * @author Lars Strojny <lars@strojny.net>
 */
final class ParameterNormalizer
{
    public static function normalizeDuration(string $duration): int
    {
        if (is_numeric($duration)) {
            return $duration;
        }

        if (false !== $time = strtotime($duration, 0)) {
            return $time;
        }

        try {
            return \DateTime::createFromFormat('U', 0)->add(new \DateInterval($duration))->getTimestamp();
        } catch (\Exception $e) {
            throw new \InvalidArgumentException(sprintf('Cannot parse date interval "%s".', $duration), 0, $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\Cache\Adapter;

use Psr\Cache\CacheItemInterface;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\ResettableInterface;
use Symfony\Contracts\Cache\CacheInterface;

/**
 * An in-memory cache storage.
 *
 * Acts as a least-recently-used (LRU) storage when configured with a maximum number of items.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInterface, ResettableInterface
{
    use LoggerAwareTrait;

    private $storeSerialized;
    private $values = [];
    private $expiries = [];
    private $defaultLifetime;
    private $maxLifetime;
    private $maxItems;

    private static $createCacheItem;

    /**
     * @param bool $storeSerialized Disabling serialization can lead to cache corruptions when storing mutable values but increases performance otherwise
     */
    public function __construct(int $defaultLifetime = 0, bool $storeSerialized = true, float $maxLifetime = 0, int $maxItems = 0)
    {
        if (0 > $maxLifetime) {
            throw new InvalidArgumentException(sprintf('Argument $maxLifetime must be positive, %F passed.', $maxLifetime));
        }

        if (0 > $maxItems) {
            throw new InvalidArgumentException(sprintf('Argument $maxItems must be a positive integer, %d passed.', $maxItems));
        }

        $this->defaultLifetime = $defaultLifetime;
        $this->storeSerialized = $storeSerialized;
        $this->maxLifetime = $maxLifetime;
        $this->maxItems = $maxItems;
        self::$createCacheItem ?? self::$createCacheItem = \Closure::bind(
            static function ($key, $value, $isHit) {
                $item = new CacheItem();
                $item->key = $key;
                $item->value = $value;
                $item->isHit = $isHit;

                return $item;
            },
            null,
            CacheItem::class
        );
    }

    /**
     * {@inheritdoc}
     */
    public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null)
    {
        $item = $this->getItem($key);
        $metadata = $item->getMetadata();

        // ArrayAdapter works in memory, we don't care about stampede protection
        if (\INF === $beta || !$item->isHit()) {
            $save = true;
            $item->set($callback($item, $save));
            if ($save) {
                $this->save($item);
            }
        }

        return $item->get();
    }

    /**
     * {@inheritdoc}
     */
    public function delete(string $key): bool
    {
        return $this->deleteItem($key);
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function hasItem($key)
    {
        if (\is_string($key) && isset($this->expiries[$key]) && $this->expiries[$key] > microtime(true)) {
            if ($this->maxItems) {
                // Move the item last in the storage
                $value = $this->values[$key];
                unset($this->values[$key]);
                $this->values[$key] = $value;
            }

            return true;
        }
        \assert('' !== CacheItem::validateKey($key));

        return isset($this->expiries[$key]) && !$this->deleteItem($key);
    }

    /**
     * {@inheritdoc}
     */
    public function getItem($key)
    {
        if (!$isHit = $this->hasItem($key)) {
            $value = null;

            if (!$this->maxItems) {
                // Track misses in non-LRU mode only
                $this->values[$key] = null;
            }
        } else {
            $value = $this->storeSerialized ? $this->unfreeze($key, $isHit) : $this->values[$key];
        }

        return (self::$createCacheItem)($key, $value, $isHit);
    }

    /**
     * {@inheritdoc}
     */
    public function getItems(array $keys = [])
    {
        \assert(self::validateKeys($keys));

        return $this->generateItems($keys, microtime(true), self::$createCacheItem);
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function deleteItem($key)
    {
        \assert('' !== CacheItem::validateKey($key));
        unset($this->values[$key], $this->expiries[$key]);

        return true;
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function deleteItems(array $keys)
    {
        foreach ($keys as $key) {
            $this->deleteItem($key);
        }

        return true;
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function save(CacheItemInterface $item)
    {
        if (!$item instanceof CacheItem) {
            return false;
        }
        $item = (array) $item;
        $key = $item["\0*\0key"];
        $value = $item["\0*\0value"];
        $expiry = $item["\0*\0expiry"];

        $now = microtime(true);

        if (null !== $expiry) {
            if (!$expiry) {
                $expiry = \PHP_INT_MAX;
            } elseif ($expiry <= $now) {
                $this->deleteItem($key);

                return true;
            }
        }
        if ($this->storeSerialized && null === $value = $this->freeze($value, $key)) {
            return false;
        }
        if (null === $expiry && 0 < $this->defaultLifetime) {
            $expiry = $this->defaultLifetime;
            $expiry = $now + ($expiry > ($this->maxLifetime ?: $expiry) ? $this->maxLifetime : $expiry);
        } elseif ($this->maxLifetime && (null === $expiry || $expiry > $now + $this->maxLifetime)) {
            $expiry = $now + $this->maxLifetime;
        }

        if ($this->maxItems) {
            unset($this->values[$key]);

            // Iterate items and vacuum expired ones while we are at it
            foreach ($this->values as $k => $v) {
                if ($this->expiries[$k] > $now && \count($this->values) < $this->maxItems) {
                    break;
                }

                unset($this->values[$k], $this->expiries[$k]);
            }
        }

        $this->values[$key] = $value;
        $this->expiries[$key] = $expiry ?? \PHP_INT_MAX;

        return true;
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function saveDeferred(CacheItemInterface $item)
    {
        return $this->save($item);
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function commit()
    {
        return true;
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function clear(string $prefix = '')
    {
        if ('' !== $prefix) {
            $now = microtime(true);

            foreach ($this->values as $key => $value) {
                if (!isset($this->expiries[$key]) || $this->expiries[$key] <= $now || 0 === strpos($key, $prefix)) {
                    unset($this->values[$key], $this->expiries[$key]);
                }
            }

            if ($this->values) {
                return true;
            }
        }

        $this->values = $this->expiries = [];

        return true;
    }

    /**
     * Returns all cached values, with cache miss as null.
     *
     * @return array
     */
    public function getValues()
    {
        if (!$this->storeSerialized) {
            return $this->values;
        }

        $values = $this->values;
        foreach ($values as $k => $v) {
            if (null === $v || 'N;' === $v) {
                continue;
            }
            if (!\is_string($v) || !isset($v[2]) || ':' !== $v[1]) {
                $values[$k] = serialize($v);
            }
        }

        return $values;
    }

    /**
     * {@inheritdoc}
     */
    public function reset()
    {
        $this->clear();
    }

    private function generateItems(array $keys, float $now, \Closure $f): \Generator
    {
        foreach ($keys as $i => $key) {
            if (!$isHit = isset($this->expiries[$key]) && ($this->expiries[$key] > $now || !$this->deleteItem($key))) {
                $value = null;

                if (!$this->maxItems) {
                    // Track misses in non-LRU mode only
                    $this->values[$key] = null;
                }
            } else {
                if ($this->maxItems) {
                    // Move the item last in the storage
                    $value = $this->values[$key];
                    unset($this->values[$key]);
                    $this->values[$key] = $value;
                }

                $value = $this->storeSerialized ? $this->unfreeze($key, $isHit) : $this->values[$key];
            }
            unset($keys[$i]);

            yield $key => $f($key, $value, $isHit);
        }

        foreach ($keys as $key) {
            yield $key => $f($key, null, false);
        }
    }

    private function freeze($value, string $key)
    {
        if (null === $value) {
            return 'N;';
        }
        if (\is_string($value)) {
            // Serialize strings if they could be confused with serialized objects or arrays
            if ('N;' === $value || (isset($value[2]) && ':' === $value[1])) {
                return serialize($value);
            }
        } elseif (!\is_scalar($value)) {
            try {
                $serialized = serialize($value);
            } catch (\Exception $e) {
                unset($this->values[$key]);
                $type = get_debug_type($value);
                $message = sprintf('Failed to save key "{key}" of type %s: %s', $type, $e->getMessage());
                CacheItem::log($this->logger, $message, ['key' => $key, 'exception' => $e, 'cache-adapter' => get_debug_type($this)]);

                return;
            }
            // Keep value serialized if it contains any objects or any internal references
            if ('C' === $serialized[0] || 'O' === $serialized[0] || preg_match('/;[OCRr]:[1-9]/', $serialized)) {
                return $serialized;
            }
        }

        return $value;
    }

    private function unfreeze(string $key, bool &$isHit)
    {
        if ('N;' === $value = $this->values[$key]) {
            return null;
        }
        if (\is_string($value) && isset($value[2]) && ':' === $value[1]) {
            try {
                $value = unserialize($value);
            } catch (\Exception $e) {
                CacheItem::log($this->logger, 'Failed to unserialize key "{key}": '.$e->getMessage(), ['key' => $key, 'exception' => $e, 'cache-adapter' => get_debug_type($this)]);
                $value = false;
            }
            if (false === $value) {
                $value = null;
                $isHit = false;

                if (!$this->maxItems) {
                    $this->values[$key] = null;
                }
            }
        }

        return $value;
    }

    private function validateKeys(array $keys): bool
    {
        foreach ($keys as $key) {
            if (!\is_string($key) || !isset($this->expiries[$key])) {
                CacheItem::validateKey($key);
            }
        }

        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\Cache\Adapter;

use Predis\Connection\Aggregate\ClusterInterface;
use Predis\Connection\Aggregate\PredisCluster;
use Predis\Connection\Aggregate\ReplicationInterface;
use Predis\Response\ErrorInterface;
use Predis\Response\Status;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\Exception\LogicException;
use Symfony\Component\Cache\Marshaller\DeflateMarshaller;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
use Symfony\Component\Cache\Marshaller\TagAwareMarshaller;
use Symfony\Component\Cache\Traits\RedisClusterProxy;
use Symfony\Component\Cache\Traits\RedisProxy;
use Symfony\Component\Cache\Traits\RedisTrait;

/**
 * Stores tag id <> cache id relationship as a Redis Set.
 *
 * Set (tag relation info) is stored without expiry (non-volatile), while cache always gets an expiry (volatile) even
 * if not set by caller. Thus if you configure redis with the right eviction policy you can be safe this tag <> cache
 * relationship survives eviction (cache cleanup when Redis runs out of memory).
 *
 * Redis server 2.8+ with any `volatile-*` eviction policy, OR `noeviction` if you're sure memory will NEVER fill up
 *
 * Design limitations:
 *  - Max 4 billion cache keys per cache tag as limited by Redis Set datatype.
 *    E.g. If you use a "all" items tag for expiry instead of clear(), that limits you to 4 billion cache items also.
 *
 * @see https://redis.io/topics/lru-cache#eviction-policies Documentation for Redis eviction policies.
 * @see https://redis.io/topics/data-types#sets Documentation for Redis Set datatype.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 * @author André Rømcke <andre.romcke+symfony@gmail.com>
 */
class RedisTagAwareAdapter extends AbstractTagAwareAdapter
{
    use RedisTrait;

    /**
     * On cache items without a lifetime set, we set it to 100 days. This is to make sure cache items are
     * preferred to be evicted over tag Sets, if eviction policy is configured according to requirements.
     */
    private const DEFAULT_CACHE_TTL = 8640000;

    /**
     * @var string|null detected eviction policy used on Redis server
     */
    private $redisEvictionPolicy;
    private $namespace;

    /**
     * @param \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|RedisProxy|RedisClusterProxy $redis           The redis client
     * @param string                                                                                $namespace       The default namespace
     * @param int                                                                                   $defaultLifetime The default lifetime
     */
    public function __construct($redis, string $namespace = '', int $defaultLifetime = 0, ?MarshallerInterface $marshaller = null)
    {
        if ($redis instanceof \Predis\ClientInterface && $redis->getConnection() instanceof ClusterInterface && !$redis->getConnection() instanceof PredisCluster) {
            throw new InvalidArgumentException(sprintf('Unsupported Predis cluster connection: only "%s" is, "%s" given.', PredisCluster::class, get_debug_type($redis->getConnection())));
        }

        if (\defined('Redis::OPT_COMPRESSION') && ($redis instanceof \Redis || $redis instanceof \RedisArray || $redis instanceof \RedisCluster)) {
            $compression = $redis->getOption(\Redis::OPT_COMPRESSION);

            foreach (\is_array($compression) ? $compression : [$compression] as $c) {
                if (\Redis::COMPRESSION_NONE !== $c) {
                    throw new InvalidArgumentException(sprintf('phpredis compression must be disabled when using "%s", use "%s" instead.', static::class, DeflateMarshaller::class));
                }
            }
        }

        $this->init($redis, $namespace, $defaultLifetime, new TagAwareMarshaller($marshaller));
        $this->namespace = $namespace;
    }

    /**
     * {@inheritdoc}
     */
    protected function doSave(array $values, int $lifetime, array $addTagData = [], array $delTagData = []): array
    {
        $eviction = $this->getRedisEvictionPolicy();
        if ('noeviction' !== $eviction && !str_starts_with($eviction, 'volatile-')) {
            throw new LogicException(sprintf('Redis maxmemory-policy setting "%s" is *not* supported by RedisTagAwareAdapter, use "noeviction" or "volatile-*" eviction policies.', $eviction));
        }

        // serialize values
        if (!$serialized = $this->marshaller->marshall($values, $failed)) {
            return $failed;
        }

        // While pipeline isn't supported on RedisCluster, other setups will at least benefit from doing this in one op
        $results = $this->pipeline(static function () use ($serialized, $lifetime, $addTagData, $delTagData, $failed) {
            // Store cache items, force a ttl if none is set, as there is no MSETEX we need to set each one
            foreach ($serialized as $id => $value) {
                yield 'setEx' => [
                    $id,
                    0 >= $lifetime ? self::DEFAULT_CACHE_TTL : $lifetime,
                    $value,
                ];
            }

            // Add and Remove Tags
            foreach ($addTagData as $tagId => $ids) {
                if (!$failed || $ids = array_diff($ids, $failed)) {
                    yield 'sAdd' => array_merge([$tagId], $ids);
                }
            }

            foreach ($delTagData as $tagId => $ids) {
                if (!$failed || $ids = array_diff($ids, $failed)) {
                    yield 'sRem' => array_merge([$tagId], $ids);
                }
            }
        });

        foreach ($results as $id => $result) {
            // Skip results of SADD/SREM operations, they'll be 1 or 0 depending on if set value already existed or not
            if (is_numeric($result)) {
                continue;
            }
            // setEx results
            if (true !== $result && (!$result instanceof Status || Status::get('OK') !== $result)) {
                $failed[] = $id;
            }
        }

        return $failed;
    }

    /**
     * {@inheritdoc}
     */
    protected function doDeleteYieldTags(array $ids): iterable
    {
        $lua = <<<'EOLUA'
            local v = redis.call('GET', KEYS[1])
            local e = redis.pcall('UNLINK', KEYS[1])

            if type(e) ~= 'number' then
                redis.call('DEL', KEYS[1])
            end

            if not v or v:len() <= 13 or v:byte(1) ~= 0x9D or v:byte(6) ~= 0 or v:byte(10) ~= 0x5F then
                return ''
            end

            return v:sub(14, 13 + v:byte(13) + v:byte(12) * 256 + v:byte(11) * 65536)
EOLUA;

        $results = $this->pipeline(function () use ($ids, $lua) {
            foreach ($ids as $id) {
                yield 'eval' => $this->redis instanceof \Predis\ClientInterface ? [$lua, 1, $id] : [$lua, [$id], 1];
            }
        });

        foreach ($results as $id => $result) {
            if ($result instanceof \RedisException || $result instanceof ErrorInterface) {
                CacheItem::log($this->logger, 'Failed to delete key "{key}": '.$result->getMessage(), ['key' => substr($id, \strlen($this->namespace)), 'exception' => $result]);

                continue;
            }

            try {
                yield $id => !\is_string($result) || '' === $result ? [] : $this->marshaller->unmarshall($result);
            } catch (\Exception $e) {
                yield $id => [];
            }
        }
    }

    /**
     * {@inheritdoc}
     */
    protected function doDeleteTagRelations(array $tagData): bool
    {
        $results = $this->pipeline(static function () use ($tagData) {
            foreach ($tagData as $tagId => $idList) {
                array_unshift($idList, $tagId);
                yield 'sRem' => $idList;
            }
        });
        foreach ($results as $result) {
            // no-op
        }

        return true;
    }

    /**
     * {@inheritdoc}
     */
    protected function doInvalidate(array $tagIds): bool
    {
        // This script scans the set of items linked to tag: it empties the set
        // and removes the linked items. When the set is still not empty after
        // the scan, it means we're in cluster mode and that the linked items
        // are on other nodes: we move the links to a temporary set and we
        // garbage collect that set from the client side.

        $lua = <<<'EOLUA'
            redis.replicate_commands()

            local cursor = '0'
            local id = KEYS[1]
            repeat
                local result = redis.call('SSCAN', id, cursor, 'COUNT', 5000);
                cursor = result[1];
                local rems = {}

                for _, v in ipairs(result[2]) do
                    local ok, _ = pcall(redis.call, 'DEL', ARGV[1]..v)
                    if ok then
                        table.insert(rems, v)
                    end
                end
                if 0 < #rems then
                    redis.call('SREM', id, unpack(rems))
                end
            until '0' == cursor;

            redis.call('SUNIONSTORE', '{'..id..'}'..id, id)
            redis.call('DEL', id)

            return redis.call('SSCAN', '{'..id..'}'..id, '0', 'COUNT', 5000)
EOLUA;

        $results = $this->pipeline(function () use ($tagIds, $lua) {
            if ($this->redis instanceof \Predis\ClientInterface) {
                $prefix = $this->redis->getOptions()->prefix ? $this->redis->getOptions()->prefix->getPrefix() : '';
            } elseif (\is_array($prefix = $this->redis->getOption(\Redis::OPT_PREFIX) ?? '')) {
                $prefix = current($prefix);
            }

            foreach ($tagIds as $id) {
                yield 'eval' => $this->redis instanceof \Predis\ClientInterface ? [$lua, 1, $id, $prefix] : [$lua, [$id, $prefix], 1];
            }
        });

        $lua = <<<'EOLUA'
            redis.replicate_commands()

            local id = KEYS[1]
            local cursor = table.remove(ARGV)
            redis.call('SREM', '{'..id..'}'..id, unpack(ARGV))

            return redis.call('SSCAN', '{'..id..'}'..id, cursor, 'COUNT', 5000)
EOLUA;

        $success = true;
        foreach ($results as $id => $values) {
            if ($values instanceof \RedisException || $values instanceof ErrorInterface) {
                CacheItem::log($this->logger, 'Failed to invalidate key "{key}": '.$values->getMessage(), ['key' => substr($id, \strlen($this->namespace)), 'exception' => $values]);
                $success = false;

                continue;
            }

            [$cursor, $ids] = $values;

            while ($ids || '0' !== $cursor) {
                $this->doDelete($ids);

                $evalArgs = [$id, $cursor];
                array_splice($evalArgs, 1, 0, $ids);

                if ($this->redis instanceof \Predis\ClientInterface) {
                    array_unshift($evalArgs, $lua, 1);
                } else {
                    $evalArgs = [$lua, $evalArgs, 1];
                }

                $results = $this->pipeline(function () use ($evalArgs) {
                    yield 'eval' => $evalArgs;
                });

                foreach ($results as [$cursor, $ids]) {
                    // no-op
                }
            }
        }

        return $success;
    }

    private function getRedisEvictionPolicy(): string
    {
        if (null !== $this->redisEvictionPolicy) {
            return $this->redisEvictionPolicy;
        }

        $hosts = $this->getHosts();
        $host = reset($hosts);
        if ($host instanceof \Predis\Client && $host->getConnection() instanceof ReplicationInterface) {
            // Predis supports info command only on the master in replication environments
            $hosts = [$host->getClientFor('master')];
        }

        foreach ($hosts as $host) {
            $info = $host->info('Memory');

            if ($info instanceof ErrorInterface) {
                continue;
            }

            $info = $info['Memory'] ?? $info;

            return $this->redisEvictionPolicy = $info['maxmemory_policy'];
        }

        return $this->redisEvictionPolicy = '';
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Cache\Adapter;

use Psr\Cache\CacheItemInterface;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\Cache\ResettableInterface;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Service\ResetInterface;

/**
 * An adapter that collects data about all cache calls.
 *
 * @author Aaron Scherer <aequasi@gmail.com>
 * @author Tobias Nyholm <tobias.nyholm@gmail.com>
 * @author Nicolas Grekas <p@tchwork.com>
 */
class TraceableAdapter implements AdapterInterface, CacheInterface, PruneableInterface, ResettableInterface
{
    protected $pool;
    private $calls = [];

    public function __construct(AdapterInterface $pool)
    {
        $this->pool = $pool;
    }

    /**
     * {@inheritdoc}
     */
    public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null)
    {
        if (!$this->pool instanceof CacheInterface) {
            throw new \BadMethodCallException(sprintf('Cannot call "%s::get()": this class doesn\'t implement "%s".', get_debug_type($this->pool), CacheInterface::class));
        }

        $isHit = true;
        $callback = function (CacheItem $item, bool &$save) use ($callback, &$isHit) {
            $isHit = $item->isHit();

            return $callback($item, $save);
        };

        $event = $this->start(__FUNCTION__);
        try {
            $value = $this->pool->get($key, $callback, $beta, $metadata);
            $event->result[$key] = get_debug_type($value);
        } finally {
            $event->end = microtime(true);
        }
        if ($isHit) {
            ++$event->hits;
        } else {
            ++$event->misses;
        }

        return $value;
    }

    /**
     * {@inheritdoc}
     */
    public function getItem($key)
    {
        $event = $this->start(__FUNCTION__);
        try {
            $item = $this->pool->getItem($key);
        } finally {
            $event->end = microtime(true);
        }
        if ($event->result[$key] = $item->isHit()) {
            ++$event->hits;
        } else {
            ++$event->misses;
        }

        return $item;
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function hasItem($key)
    {
        $event = $this->start(__FUNCTION__);
        try {
            return $event->result[$key] = $this->pool->hasItem($key);
        } finally {
            $event->end = microtime(true);
        }
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function deleteItem($key)
    {
        $event = $this->start(__FUNCTION__);
        try {
            return $event->result[$key] = $this->pool->deleteItem($key);
        } finally {
            $event->end = microtime(true);
        }
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function save(CacheItemInterface $item)
    {
        $event = $this->start(__FUNCTION__);
        try {
            return $event->result[$item->getKey()] = $this->pool->save($item);
        } finally {
            $event->end = microtime(true);
        }
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function saveDeferred(CacheItemInterface $item)
    {
        $event = $this->start(__FUNCTION__);
        try {
            return $event->result[$item->getKey()] = $this->pool->saveDeferred($item);
        } finally {
            $event->end = microtime(true);
        }
    }

    /**
     * {@inheritdoc}
     */
    public function getItems(array $keys = [])
    {
        $event = $this->start(__FUNCTION__);
        try {
            $result = $this->pool->getItems($keys);
        } finally {
            $event->end = microtime(true);
        }
        $f = function () use ($result, $event) {
            $event->result = [];
            foreach ($result as $key => $item) {
                if ($event->result[$key] = $item->isHit()) {
                    ++$event->hits;
                } else {
                    ++$event->misses;
                }
                yield $key => $item;
            }
        };

        return $f();
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function clear(string $prefix = '')
    {
        $event = $this->start(__FUNCTION__);
        try {
            if ($this->pool instanceof AdapterInterface) {
                return $event->result = $this->pool->clear($prefix);
            }

            return $event->result = $this->pool->clear();
        } finally {
            $event->end = microtime(true);
        }
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function deleteItems(array $keys)
    {
        $event = $this->start(__FUNCTION__);
        $event->result['keys'] = $keys;
        try {
            return $event->result['result'] = $this->pool->deleteItems($keys);
        } finally {
            $event->end = microtime(true);
        }
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function commit()
    {
        $event = $this->start(__FUNCTION__);
        try {
            return $event->result = $this->pool->commit();
        } finally {
            $event->end = microtime(true);
        }
    }

    /**
     * {@inheritdoc}
     */
    public function prune()
    {
        if (!$this->pool instanceof PruneableInterface) {
            return false;
        }
        $event = $this->start(__FUNCTION__);
        try {
            return $event->result = $this->pool->prune();
        } finally {
            $event->end = microtime(true);
        }
    }

    /**
     * {@inheritdoc}
     */
    public function reset()
    {
        if ($this->pool instanceof ResetInterface) {
            $this->pool->reset();
        }

        $this->clearCalls();
    }

    /**
     * {@inheritdoc}
     */
    public function delete(string $key): bool
    {
        $event = $this->start(__FUNCTION__);
        try {
            return $event->result[$key] = $this->pool->deleteItem($key);
        } finally {
            $event->end = microtime(true);
        }
    }

    public function getCalls()
    {
        return $this->calls;
    }

    public function clearCalls()
    {
        $this->calls = [];
    }

    protected function start(string $name)
    {
        $this->calls[] = $event = new TraceableAdapterEvent();
        $event->name = $name;
        $event->start = microtime(true);

        return $event;
    }
}

class TraceableAdapterEvent
{
    public $name;
    public $start;
    public $end;
    public $result;
    public $hits = 0;
    public $misses = 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\Cache\Adapter;

use Psr\SimpleCache\CacheInterface;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\Cache\ResettableInterface;
use Symfony\Component\Cache\Traits\ProxyTrait;

/**
 * Turns a PSR-16 cache into a PSR-6 one.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
class Psr16Adapter extends AbstractAdapter implements PruneableInterface, ResettableInterface
{
    use ProxyTrait;

    /**
     * @internal
     */
    protected const NS_SEPARATOR = '_';

    private $miss;

    public function __construct(CacheInterface $pool, string $namespace = '', int $defaultLifetime = 0)
    {
        parent::__construct($namespace, $defaultLifetime);

        $this->pool = $pool;
        $this->miss = new \stdClass();
    }

    /**
     * {@inheritdoc}
     */
    protected function doFetch(array $ids)
    {
        foreach ($this->pool->getMultiple($ids, $this->miss) as $key => $value) {
            if ($this->miss !== $value) {
                yield $key => $value;
            }
        }
    }

    /**
     * {@inheritdoc}
     */
    protected function doHave(string $id)
    {
        return $this->pool->has($id);
    }

    /**
     * {@inheritdoc}
     */
    protected function doClear(string $namespace)
    {
        return $this->pool->clear();
    }

    /**
     * {@inheritdoc}
     */
    protected function doDelete(array $ids)
    {
        return $this->pool->deleteMultiple($ids);
    }

    /**
     * {@inheritdoc}
     */
    protected function doSave(array $values, int $lifetime)
    {
        return $this->pool->setMultiple($values, 0 === $lifetime ? null : $lifetime);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Cache\Adapter;

use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\Cache\ResettableInterface;
use Symfony\Component\Cache\Traits\ContractsTrait;
use Symfony\Component\Cache\Traits\ProxyTrait;
use Symfony\Contracts\Cache\CacheInterface;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 */
class ProxyAdapter implements AdapterInterface, CacheInterface, PruneableInterface, ResettableInterface
{
    use ContractsTrait;
    use ProxyTrait;

    private $namespace = '';
    private $namespaceLen;
    private $poolHash;
    private $defaultLifetime;

    private static $createCacheItem;
    private static $setInnerItem;

    public function __construct(CacheItemPoolInterface $pool, string $namespace = '', int $defaultLifetime = 0)
    {
        $this->pool = $pool;
        $this->poolHash = $poolHash = spl_object_hash($pool);
        if ('' !== $namespace) {
            \assert('' !== CacheItem::validateKey($namespace));
            $this->namespace = $namespace;
        }
        $this->namespaceLen = \strlen($namespace);
        $this->defaultLifetime = $defaultLifetime;
        self::$createCacheItem ?? self::$createCacheItem = \Closure::bind(
            static function ($key, $innerItem, $poolHash) {
                $item = new CacheItem();
                $item->key = $key;

                if (null === $innerItem) {
                    return $item;
                }

                $item->value = $v = $innerItem->get();
                $item->isHit = $innerItem->isHit();
                $item->innerItem = $innerItem;
                $item->poolHash = $poolHash;

                // Detect wrapped values that encode for their expiry and creation duration
                // For compactness, these values are packed in the key of an array using
                // magic numbers in the form 9D-..-..-..-..-00-..-..-..-5F
                if (\is_array($v) && 1 === \count($v) && 10 === \strlen($k = (string) array_key_first($v)) && "\x9D" === $k[0] && "\0" === $k[5] && "\x5F" === $k[9]) {
                    $item->value = $v[$k];
                    $v = unpack('Ve/Nc', substr($k, 1, -1));
                    $item->metadata[CacheItem::METADATA_EXPIRY] = $v['e'] + CacheItem::METADATA_EXPIRY_OFFSET;
                    $item->metadata[CacheItem::METADATA_CTIME] = $v['c'];
                } elseif ($innerItem instanceof CacheItem) {
                    $item->metadata = $innerItem->metadata;
                }
                $innerItem->set(null);

                return $item;
            },
            null,
            CacheItem::class
        );
        self::$setInnerItem ?? self::$setInnerItem = \Closure::bind(
            /**
             * @param array $item A CacheItem cast to (array); accessing protected properties requires adding the "\0*\0" PHP prefix
             */
            static function (CacheItemInterface $innerItem, array $item) {
                // Tags are stored separately, no need to account for them when considering this item's newly set metadata
                if (isset(($metadata = $item["\0*\0newMetadata"])[CacheItem::METADATA_TAGS])) {
                    unset($metadata[CacheItem::METADATA_TAGS]);
                }
                if ($metadata) {
                    // For compactness, expiry and creation duration are packed in the key of an array, using magic numbers as separators
                    $item["\0*\0value"] = ["\x9D".pack('VN', (int) (0.1 + $metadata[self::METADATA_EXPIRY] - self::METADATA_EXPIRY_OFFSET), $metadata[self::METADATA_CTIME])."\x5F" => $item["\0*\0value"]];
                }
                $innerItem->set($item["\0*\0value"]);
                $innerItem->expiresAt(null !== $item["\0*\0expiry"] ? \DateTime::createFromFormat('U.u', sprintf('%.6F', $item["\0*\0expiry"])) : null);
            },
            null,
            CacheItem::class
        );
    }

    /**
     * {@inheritdoc}
     */
    public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null)
    {
        if (!$this->pool instanceof CacheInterface) {
            return $this->doGet($this, $key, $callback, $beta, $metadata);
        }

        return $this->pool->get($this->getId($key), function ($innerItem, bool &$save) use ($key, $callback) {
            $item = (self::$createCacheItem)($key, $innerItem, $this->poolHash);
            $item->set($value = $callback($item, $save));
            (self::$setInnerItem)($innerItem, (array) $item);

            return $value;
        }, $beta, $metadata);
    }

    /**
     * {@inheritdoc}
     */
    public function getItem($key)
    {
        $item = $this->pool->getItem($this->getId($key));

        return (self::$createCacheItem)($key, $item, $this->poolHash);
    }

    /**
     * {@inheritdoc}
     */
    public function getItems(array $keys = [])
    {
        if ($this->namespaceLen) {
            foreach ($keys as $i => $key) {
                $keys[$i] = $this->getId($key);
            }
        }

        return $this->generateItems($this->pool->getItems($keys));
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function hasItem($key)
    {
        return $this->pool->hasItem($this->getId($key));
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function clear(string $prefix = '')
    {
        if ($this->pool instanceof AdapterInterface) {
            return $this->pool->clear($this->namespace.$prefix);
        }

        return $this->pool->clear();
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function deleteItem($key)
    {
        return $this->pool->deleteItem($this->getId($key));
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function deleteItems(array $keys)
    {
        if ($this->namespaceLen) {
            foreach ($keys as $i => $key) {
                $keys[$i] = $this->getId($key);
            }
        }

        return $this->pool->deleteItems($keys);
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function save(CacheItemInterface $item)
    {
        return $this->doSave($item, __FUNCTION__);
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function saveDeferred(CacheItemInterface $item)
    {
        return $this->doSave($item, __FUNCTION__);
    }

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

    private function doSave(CacheItemInterface $item, string $method)
    {
        if (!$item instanceof CacheItem) {
            return false;
        }
        $item = (array) $item;
        if (null === $item["\0*\0expiry"] && 0 < $this->defaultLifetime) {
            $item["\0*\0expiry"] = microtime(true) + $this->defaultLifetime;
        }

        if ($item["\0*\0poolHash"] === $this->poolHash && $item["\0*\0innerItem"]) {
            $innerItem = $item["\0*\0innerItem"];
        } elseif ($this->pool instanceof AdapterInterface) {
            // this is an optimization specific for AdapterInterface implementations
            // so we can save a round-trip to the backend by just creating a new item
            $innerItem = (self::$createCacheItem)($this->namespace.$item["\0*\0key"], null, $this->poolHash);
        } else {
            $innerItem = $this->pool->getItem($this->namespace.$item["\0*\0key"]);
        }

        (self::$setInnerItem)($innerItem, $item);

        return $this->pool->$method($innerItem);
    }

    private function generateItems(iterable $items): \Generator
    {
        $f = self::$createCacheItem;

        foreach ($items as $key => $item) {
            if ($this->namespaceLen) {
                $key = substr($key, $this->namespaceLen);
            }

            yield $key => $f($key, $item, $this->poolHash);
        }
    }

    private function getId($key): string
    {
        \assert('' !== CacheItem::validateKey($key));

        return $this->namespace.$key;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Cache\Adapter;

use Psr\Log\LoggerAwareInterface;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\ResettableInterface;
use Symfony\Component\Cache\Traits\AbstractAdapterTrait;
use Symfony\Component\Cache\Traits\ContractsTrait;
use Symfony\Contracts\Cache\TagAwareCacheInterface;

/**
 * Abstract for native TagAware adapters.
 *
 * To keep info on tags, the tags are both serialized as part of cache value and provided as tag ids
 * to Adapters on operations when needed for storage to doSave(), doDelete() & doInvalidate().
 *
 * @author Nicolas Grekas <p@tchwork.com>
 * @author André Rømcke <andre.romcke+symfony@gmail.com>
 *
 * @internal
 */
abstract class AbstractTagAwareAdapter implements TagAwareAdapterInterface, TagAwareCacheInterface, LoggerAwareInterface, ResettableInterface
{
    use AbstractAdapterTrait;
    use ContractsTrait;

    private const TAGS_PREFIX = "\0tags\0";

    protected function __construct(string $namespace = '', int $defaultLifetime = 0)
    {
        $this->namespace = '' === $namespace ? '' : CacheItem::validateKey($namespace).':';
        $this->defaultLifetime = $defaultLifetime;
        if (null !== $this->maxIdLength && \strlen($namespace) > $this->maxIdLength - 24) {
            throw new InvalidArgumentException(sprintf('Namespace must be %d chars max, %d given ("%s").', $this->maxIdLength - 24, \strlen($namespace), $namespace));
        }
        self::$createCacheItem ?? self::$createCacheItem = \Closure::bind(
            static function ($key, $value, $isHit) {
                $item = new CacheItem();
                $item->key = $key;
                $item->isTaggable = true;
                // If structure does not match what we expect return item as is (no value and not a hit)
                if (!\is_array($value) || !\array_key_exists('value', $value)) {
                    return $item;
                }
                $item->isHit = $isHit;
                // Extract value, tags and meta data from the cache value
                $item->value = $value['value'];
                $item->metadata[CacheItem::METADATA_TAGS] = $value['tags'] ?? [];
                if (isset($value['meta'])) {
                    // For compactness these values are packed, & expiry is offset to reduce size
                    $v = unpack('Ve/Nc', $value['meta']);
                    $item->metadata[CacheItem::METADATA_EXPIRY] = $v['e'] + CacheItem::METADATA_EXPIRY_OFFSET;
                    $item->metadata[CacheItem::METADATA_CTIME] = $v['c'];
                }

                return $item;
            },
            null,
            CacheItem::class
        );
        self::$mergeByLifetime ?? self::$mergeByLifetime = \Closure::bind(
            static function ($deferred, &$expiredIds, $getId, $tagPrefix, $defaultLifetime) {
                $byLifetime = [];
                $now = microtime(true);
                $expiredIds = [];

                foreach ($deferred as $key => $item) {
                    $key = (string) $key;
                    if (null === $item->expiry) {
                        $ttl = 0 < $defaultLifetime ? $defaultLifetime : 0;
                    } elseif (!$item->expiry) {
                        $ttl = 0;
                    } elseif (0 >= $ttl = (int) (0.1 + $item->expiry - $now)) {
                        $expiredIds[] = $getId($key);
                        continue;
                    }
                    // Store Value and Tags on the cache value
                    if (isset(($metadata = $item->newMetadata)[CacheItem::METADATA_TAGS])) {
                        $value = ['value' => $item->value, 'tags' => $metadata[CacheItem::METADATA_TAGS]];
                        unset($metadata[CacheItem::METADATA_TAGS]);
                    } else {
                        $value = ['value' => $item->value, 'tags' => []];
                    }

                    if ($metadata) {
                        // For compactness, expiry and creation duration are packed, using magic numbers as separators
                        $value['meta'] = pack('VN', (int) (0.1 + $metadata[self::METADATA_EXPIRY] - self::METADATA_EXPIRY_OFFSET), $metadata[self::METADATA_CTIME]);
                    }

                    // Extract tag changes, these should be removed from values in doSave()
                    $value['tag-operations'] = ['add' => [], 'remove' => []];
                    $oldTags = $item->metadata[CacheItem::METADATA_TAGS] ?? [];
                    foreach (array_diff($value['tags'], $oldTags) as $addedTag) {
                        $value['tag-operations']['add'][] = $getId($tagPrefix.$addedTag);
                    }
                    foreach (array_diff($oldTags, $value['tags']) as $removedTag) {
                        $value['tag-operations']['remove'][] = $getId($tagPrefix.$removedTag);
                    }

                    $byLifetime[$ttl][$getId($key)] = $value;
                    $item->metadata = $item->newMetadata;
                }

                return $byLifetime;
            },
            null,
            CacheItem::class
        );
    }

    /**
     * Persists several cache items immediately.
     *
     * @param array   $values        The values to cache, indexed by their cache identifier
     * @param int     $lifetime      The lifetime of the cached values, 0 for persisting until manual cleaning
     * @param array[] $addTagData    Hash where key is tag id, and array value is list of cache id's to add to tag
     * @param array[] $removeTagData Hash where key is tag id, and array value is list of cache id's to remove to tag
     *
     * @return array The identifiers that failed to be cached or a boolean stating if caching succeeded or not
     */
    abstract protected function doSave(array $values, int $lifetime, array $addTagData = [], array $removeTagData = []): array;

    /**
     * Removes multiple items from the pool and their corresponding tags.
     *
     * @param array $ids An array of identifiers that should be removed from the pool
     *
     * @return bool
     */
    abstract protected function doDelete(array $ids);

    /**
     * Removes relations between tags and deleted items.
     *
     * @param array $tagData Array of tag => key identifiers that should be removed from the pool
     */
    abstract protected function doDeleteTagRelations(array $tagData): bool;

    /**
     * Invalidates cached items using tags.
     *
     * @param string[] $tagIds An array of tags to invalidate, key is tag and value is tag id
     */
    abstract protected function doInvalidate(array $tagIds): bool;

    /**
     * Delete items and yields the tags they were bound to.
     */
    protected function doDeleteYieldTags(array $ids): iterable
    {
        foreach ($this->doFetch($ids) as $id => $value) {
            yield $id => \is_array($value) && \is_array($value['tags'] ?? null) ? $value['tags'] : [];
        }

        $this->doDelete($ids);
    }

    /**
     * {@inheritdoc}
     */
    public function commit(): bool
    {
        $ok = true;
        $byLifetime = (self::$mergeByLifetime)($this->deferred, $expiredIds, \Closure::fromCallable([$this, 'getId']), self::TAGS_PREFIX, $this->defaultLifetime);
        $retry = $this->deferred = [];

        if ($expiredIds) {
            // Tags are not cleaned up in this case, however that is done on invalidateTags().
            try {
                $this->doDelete($expiredIds);
            } catch (\Exception $e) {
                $ok = false;
                CacheItem::log($this->logger, 'Failed to delete expired items: '.$e->getMessage(), ['exception' => $e, 'cache-adapter' => get_debug_type($this)]);
            }
        }
        foreach ($byLifetime as $lifetime => $values) {
            try {
                $values = $this->extractTagData($values, $addTagData, $removeTagData);
                $e = $this->doSave($values, $lifetime, $addTagData, $removeTagData);
            } catch (\Exception $e) {
            }
            if (true === $e || [] === $e) {
                continue;
            }
            if (\is_array($e) || 1 === \count($values)) {
                foreach (\is_array($e) ? $e : array_keys($values) as $id) {
                    $ok = false;
                    $v = $values[$id];
                    $type = get_debug_type($v);
                    $message = sprintf('Failed to save key "{key}" of type %s%s', $type, $e instanceof \Exception ? ': '.$e->getMessage() : '.');
                    CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->namespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
                }
            } else {
                foreach ($values as $id => $v) {
                    $retry[$lifetime][] = $id;
                }
            }
        }

        // When bulk-save failed, retry each item individually
        foreach ($retry as $lifetime => $ids) {
            foreach ($ids as $id) {
                try {
                    $v = $byLifetime[$lifetime][$id];
                    $values = $this->extractTagData([$id => $v], $addTagData, $removeTagData);
                    $e = $this->doSave($values, $lifetime, $addTagData, $removeTagData);
                } catch (\Exception $e) {
                }
                if (true === $e || [] === $e) {
                    continue;
                }
                $ok = false;
                $type = get_debug_type($v);
                $message = sprintf('Failed to save key "{key}" of type %s%s', $type, $e instanceof \Exception ? ': '.$e->getMessage() : '.');
                CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->namespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
            }
        }

        return $ok;
    }

    /**
     * {@inheritdoc}
     */
    public function deleteItems(array $keys): bool
    {
        if (!$keys) {
            return true;
        }

        $ok = true;
        $ids = [];
        $tagData = [];

        foreach ($keys as $key) {
            $ids[$key] = $this->getId($key);
            unset($this->deferred[$key]);
        }

        try {
            foreach ($this->doDeleteYieldTags(array_values($ids)) as $id => $tags) {
                foreach ($tags as $tag) {
                    $tagData[$this->getId(self::TAGS_PREFIX.$tag)][] = $id;
                }
            }
        } catch (\Exception $e) {
            $ok = false;
        }

        try {
            if ((!$tagData || $this->doDeleteTagRelations($tagData)) && $ok) {
                return true;
            }
        } catch (\Exception $e) {
        }

        // When bulk-delete failed, retry each item individually
        foreach ($ids as $key => $id) {
            try {
                $e = null;
                if ($this->doDelete([$id])) {
                    continue;
                }
            } catch (\Exception $e) {
            }
            $message = 'Failed to delete key "{key}"'.($e instanceof \Exception ? ': '.$e->getMessage() : '.');
            CacheItem::log($this->logger, $message, ['key' => $key, 'exception' => $e, 'cache-adapter' => get_debug_type($this)]);
            $ok = false;
        }

        return $ok;
    }

    /**
     * {@inheritdoc}
     */
    public function invalidateTags(array $tags)
    {
        if (empty($tags)) {
            return false;
        }

        $tagIds = [];
        foreach (array_unique($tags) as $tag) {
            $tagIds[] = $this->getId(self::TAGS_PREFIX.$tag);
        }

        try {
            if ($this->doInvalidate($tagIds)) {
                return true;
            }
        } catch (\Exception $e) {
            CacheItem::log($this->logger, 'Failed to invalidate tags: '.$e->getMessage(), ['exception' => $e, 'cache-adapter' => get_debug_type($this)]);
        }

        return false;
    }

    /**
     * Extracts tags operation data from $values set in mergeByLifetime, and returns values without it.
     */
    private function extractTagData(array $values, ?array &$addTagData, ?array &$removeTagData): array
    {
        $addTagData = $removeTagData = [];
        foreach ($values as $id => $value) {
            foreach ($value['tag-operations']['add'] as $tag => $tagId) {
                $addTagData[$tagId][] = $id;
            }

            foreach ($value['tag-operations']['remove'] as $tag => $tagId) {
                $removeTagData[$tagId][] = $id;
            }

            unset($values[$id]['tag-operations']);
        }

        return $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\Cache\Adapter;

use Couchbase\Bucket;
use Couchbase\Cluster;
use Couchbase\ClusterOptions;
use Couchbase\Collection;
use Couchbase\DocumentNotFoundException;
use Couchbase\UpsertOptions;
use Symfony\Component\Cache\Exception\CacheException;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;

/**
 * @author Antonio Jose Cerezo Aranda <aj.cerezo@gmail.com>
 */
class CouchbaseCollectionAdapter extends AbstractAdapter
{
    private const MAX_KEY_LENGTH = 250;

    /** @var Collection */
    private $connection;
    private $marshaller;

    public function __construct(Collection $connection, string $namespace = '', int $defaultLifetime = 0, ?MarshallerInterface $marshaller = null)
    {
        if (!static::isSupported()) {
            throw new CacheException('Couchbase >= 3.0.0 < 4.0.0 is required.');
        }

        $this->maxIdLength = static::MAX_KEY_LENGTH;

        $this->connection = $connection;

        parent::__construct($namespace, $defaultLifetime);
        $this->enableVersioning();
        $this->marshaller = $marshaller ?? new DefaultMarshaller();
    }

    /**
     * @param array|string $dsn
     *
     * @return Bucket|Collection
     */
    public static function createConnection($dsn, array $options = [])
    {
        if (\is_string($dsn)) {
            $dsn = [$dsn];
        } elseif (!\is_array($dsn)) {
            throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be array or string, "%s" given.', __METHOD__, get_debug_type($dsn)));
        }

        if (!static::isSupported()) {
            throw new CacheException('Couchbase >= 3.0.0 < 4.0.0 is required.');
        }

        set_error_handler(function ($type, $msg, $file, $line): bool { throw new \ErrorException($msg, 0, $type, $file, $line); });

        $dsnPattern = '/^(?<protocol>couchbase(?:s)?)\:\/\/(?:(?<username>[^\:]+)\:(?<password>[^\@]{6,})@)?'
            .'(?<host>[^\:]+(?:\:\d+)?)(?:\/(?<bucketName>[^\/\?]+))(?:(?:\/(?<scopeName>[^\/]+))'
            .'(?:\/(?<collectionName>[^\/\?]+)))?(?:\/)?(?:\?(?<options>.*))?$/i';

        $newServers = [];
        $protocol = 'couchbase';
        try {
            $username = $options['username'] ?? '';
            $password = $options['password'] ?? '';

            foreach ($dsn as $server) {
                if (0 !== strpos($server, 'couchbase:')) {
                    throw new InvalidArgumentException('Invalid Couchbase DSN: it does not start with "couchbase:".');
                }

                preg_match($dsnPattern, $server, $matches);

                $username = $matches['username'] ?: $username;
                $password = $matches['password'] ?: $password;
                $protocol = $matches['protocol'] ?: $protocol;

                if (isset($matches['options'])) {
                    $optionsInDsn = self::getOptions($matches['options']);

                    foreach ($optionsInDsn as $parameter => $value) {
                        $options[$parameter] = $value;
                    }
                }

                $newServers[] = $matches['host'];
            }

            $option = isset($matches['options']) ? '?'.$matches['options'] : '';
            $connectionString = $protocol.'://'.implode(',', $newServers).$option;

            $clusterOptions = new ClusterOptions();
            $clusterOptions->credentials($username, $password);

            $client = new Cluster($connectionString, $clusterOptions);

            $bucket = $client->bucket($matches['bucketName']);
            $collection = $bucket->defaultCollection();
            if (!empty($matches['scopeName'])) {
                $scope = $bucket->scope($matches['scopeName']);
                $collection = $scope->collection($matches['collectionName']);
            }

            return $collection;
        } finally {
            restore_error_handler();
        }
    }

    public static function isSupported(): bool
    {
        return \extension_loaded('couchbase') && version_compare(phpversion('couchbase'), '3.0.5', '>=') && version_compare(phpversion('couchbase'), '4.0', '<');
    }

    private static function getOptions(string $options): array
    {
        $results = [];
        $optionsInArray = explode('&', $options);

        foreach ($optionsInArray as $option) {
            [$key, $value] = explode('=', $option);

            $results[$key] = $value;
        }

        return $results;
    }

    /**
     * {@inheritdoc}
     */
    protected function doFetch(array $ids): array
    {
        $results = [];
        foreach ($ids as $id) {
            try {
                $resultCouchbase = $this->connection->get($id);
            } catch (DocumentNotFoundException $exception) {
                continue;
            }

            $content = $resultCouchbase->value ?? $resultCouchbase->content();

            $results[$id] = $this->marshaller->unmarshall($content);
        }

        return $results;
    }

    /**
     * {@inheritdoc}
     */
    protected function doHave($id): bool
    {
        return $this->connection->exists($id)->exists();
    }

    /**
     * {@inheritdoc}
     */
    protected function doClear($namespace): bool
    {
        return false;
    }

    /**
     * {@inheritdoc}
     */
    protected function doDelete(array $ids): bool
    {
        $idsErrors = [];
        foreach ($ids as $id) {
            try {
                $result = $this->connection->remove($id);

                if (null === $result->mutationToken()) {
                    $idsErrors[] = $id;
                }
            } catch (DocumentNotFoundException $exception) {
            }
        }

        return 0 === \count($idsErrors);
    }

    /**
     * {@inheritdoc}
     */
    protected function doSave(array $values, $lifetime)
    {
        if (!$values = $this->marshaller->marshall($values, $failed)) {
            return $failed;
        }

        $upsertOptions = new UpsertOptions();
        $upsertOptions->expiry($lifetime);

        $ko = [];
        foreach ($values as $key => $value) {
            try {
                $this->connection->upsert($key, $value, $upsertOptions);
            } catch (\Exception $exception) {
                $ko[$key] = '';
            }
        }

        return [] === $ko ? true : $ko;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Cache\Adapter;

use Psr\Cache\CacheItemInterface;
use Symfony\Component\Cache\CacheItem;
use Symfony\Contracts\Cache\CacheInterface;

/**
 * @author Titouan Galopin <galopintitouan@gmail.com>
 */
class NullAdapter implements AdapterInterface, CacheInterface
{
    private static $createCacheItem;

    public function __construct()
    {
        self::$createCacheItem ?? self::$createCacheItem = \Closure::bind(
            static function ($key) {
                $item = new CacheItem();
                $item->key = $key;
                $item->isHit = false;

                return $item;
            },
            null,
            CacheItem::class
        );
    }

    /**
     * {@inheritdoc}
     */
    public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null)
    {
        $save = true;

        return $callback((self::$createCacheItem)($key), $save);
    }

    /**
     * {@inheritdoc}
     */
    public function getItem($key)
    {
        return (self::$createCacheItem)($key);
    }

    /**
     * {@inheritdoc}
     */
    public function getItems(array $keys = [])
    {
        return $this->generateItems($keys);
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function hasItem($key)
    {
        return false;
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function clear(string $prefix = '')
    {
        return true;
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function deleteItem($key)
    {
        return true;
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function deleteItems(array $keys)
    {
        return true;
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function save(CacheItemInterface $item)
    {
        return true;
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function saveDeferred(CacheItemInterface $item)
    {
        return true;
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function commit()
    {
        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function delete(string $key): bool
    {
        return $this->deleteItem($key);
    }

    private function generateItems(array $keys): \Generator
    {
        $f = self::$createCacheItem;

        foreach ($keys as $key) {
            yield $key => $f($key);
        }
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Cache\Adapter;

use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\Cache\ResettableInterface;
use Symfony\Component\Cache\Traits\ContractsTrait;
use Symfony\Component\Cache\Traits\ProxyTrait;
use Symfony\Component\VarExporter\VarExporter;
use Symfony\Contracts\Cache\CacheInterface;

/**
 * Caches items at warm up time using a PHP array that is stored in shared memory by OPCache since PHP 7.0.
 * Warmed up items are read-only and run-time discovered items are cached using a fallback adapter.
 *
 * @author Titouan Galopin <galopintitouan@gmail.com>
 * @author Nicolas Grekas <p@tchwork.com>
 */
class PhpArrayAdapter implements AdapterInterface, CacheInterface, PruneableInterface, ResettableInterface
{
    use ContractsTrait;
    use ProxyTrait;

    private $file;
    private $keys;
    private $values;

    private static $createCacheItem;
    private static $valuesCache = [];

    /**
     * @param string           $file         The PHP file were values are cached
     * @param AdapterInterface $fallbackPool A pool to fallback on when an item is not hit
     */
    public function __construct(string $file, AdapterInterface $fallbackPool)
    {
        $this->file = $file;
        $this->pool = $fallbackPool;
        self::$createCacheItem ?? self::$createCacheItem = \Closure::bind(
            static function ($key, $value, $isHit) {
                $item = new CacheItem();
                $item->key = $key;
                $item->value = $value;
                $item->isHit = $isHit;

                return $item;
            },
            null,
            CacheItem::class
        );
    }

    /**
     * This adapter takes advantage of how PHP stores arrays in its latest versions.
     *
     * @param string                 $file         The PHP file were values are cached
     * @param CacheItemPoolInterface $fallbackPool A pool to fallback on when an item is not hit
     *
     * @return CacheItemPoolInterface
     */
    public static function create(string $file, CacheItemPoolInterface $fallbackPool)
    {
        if (!$fallbackPool instanceof AdapterInterface) {
            $fallbackPool = new ProxyAdapter($fallbackPool);
        }

        return new static($file, $fallbackPool);
    }

    /**
     * {@inheritdoc}
     */
    public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null)
    {
        if (null === $this->values) {
            $this->initialize();
        }
        if (!isset($this->keys[$key])) {
            get_from_pool:
            if ($this->pool instanceof CacheInterface) {
                return $this->pool->get($key, $callback, $beta, $metadata);
            }

            return $this->doGet($this->pool, $key, $callback, $beta, $metadata);
        }
        $value = $this->values[$this->keys[$key]];

        if ('N;' === $value) {
            return null;
        }
        try {
            if ($value instanceof \Closure) {
                return $value();
            }
        } catch (\Throwable $e) {
            unset($this->keys[$key]);
            goto get_from_pool;
        }

        return $value;
    }

    /**
     * {@inheritdoc}
     */
    public function getItem($key)
    {
        if (!\is_string($key)) {
            throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', get_debug_type($key)));
        }
        if (null === $this->values) {
            $this->initialize();
        }
        if (!isset($this->keys[$key])) {
            return $this->pool->getItem($key);
        }

        $value = $this->values[$this->keys[$key]];
        $isHit = true;

        if ('N;' === $value) {
            $value = null;
        } elseif ($value instanceof \Closure) {
            try {
                $value = $value();
            } catch (\Throwable $e) {
                $value = null;
                $isHit = false;
            }
        }

        return (self::$createCacheItem)($key, $value, $isHit);
    }

    /**
     * {@inheritdoc}
     */
    public function getItems(array $keys = [])
    {
        foreach ($keys as $key) {
            if (!\is_string($key)) {
                throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', get_debug_type($key)));
            }
        }
        if (null === $this->values) {
            $this->initialize();
        }

        return $this->generateItems($keys);
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function hasItem($key)
    {
        if (!\is_string($key)) {
            throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', get_debug_type($key)));
        }
        if (null === $this->values) {
            $this->initialize();
        }

        return isset($this->keys[$key]) || $this->pool->hasItem($key);
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function deleteItem($key)
    {
        if (!\is_string($key)) {
            throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', get_debug_type($key)));
        }
        if (null === $this->values) {
            $this->initialize();
        }

        return !isset($this->keys[$key]) && $this->pool->deleteItem($key);
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function deleteItems(array $keys)
    {
        $deleted = true;
        $fallbackKeys = [];

        foreach ($keys as $key) {
            if (!\is_string($key)) {
                throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', get_debug_type($key)));
            }

            if (isset($this->keys[$key])) {
                $deleted = false;
            } else {
                $fallbackKeys[] = $key;
            }
        }
        if (null === $this->values) {
            $this->initialize();
        }

        if ($fallbackKeys) {
            $deleted = $this->pool->deleteItems($fallbackKeys) && $deleted;
        }

        return $deleted;
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function save(CacheItemInterface $item)
    {
        if (null === $this->values) {
            $this->initialize();
        }

        return !isset($this->keys[$item->getKey()]) && $this->pool->save($item);
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function saveDeferred(CacheItemInterface $item)
    {
        if (null === $this->values) {
            $this->initialize();
        }

        return !isset($this->keys[$item->getKey()]) && $this->pool->saveDeferred($item);
    }

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

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function clear(string $prefix = '')
    {
        $this->keys = $this->values = [];

        $cleared = @unlink($this->file) || !file_exists($this->file);
        unset(self::$valuesCache[$this->file]);

        if ($this->pool instanceof AdapterInterface) {
            return $this->pool->clear($prefix) && $cleared;
        }

        return $this->pool->clear() && $cleared;
    }

    /**
     * Store an array of cached values.
     *
     * @param array $values The cached values
     *
     * @return string[] A list of classes to preload on PHP 7.4+
     */
    public function warmUp(array $values)
    {
        if (file_exists($this->file)) {
            if (!is_file($this->file)) {
                throw new InvalidArgumentException(sprintf('Cache path exists and is not a file: "%s".', $this->file));
            }

            if (!is_writable($this->file)) {
                throw new InvalidArgumentException(sprintf('Cache file is not writable: "%s".', $this->file));
            }
        } else {
            $directory = \dirname($this->file);

            if (!is_dir($directory) && !@mkdir($directory, 0777, true)) {
                throw new InvalidArgumentException(sprintf('Cache directory does not exist and cannot be created: "%s".', $directory));
            }

            if (!is_writable($directory)) {
                throw new InvalidArgumentException(sprintf('Cache directory is not writable: "%s".', $directory));
            }
        }

        $preload = [];
        $dumpedValues = '';
        $dumpedMap = [];
        $dump = <<<'EOF'
<?php

// This file has been auto-generated by the Symfony Cache Component.

return [[


EOF;

        foreach ($values as $key => $value) {
            CacheItem::validateKey(\is_int($key) ? (string) $key : $key);
            $isStaticValue = true;

            if (null === $value) {
                $value = "'N;'";
            } elseif (\is_object($value) || \is_array($value)) {
                try {
                    $value = VarExporter::export($value, $isStaticValue, $preload);
                } catch (\Exception $e) {
                    throw new InvalidArgumentException(sprintf('Cache key "%s" has non-serializable "%s" value.', $key, get_debug_type($value)), 0, $e);
                }
            } elseif (\is_string($value)) {
                // Wrap "N;" in a closure to not confuse it with an encoded `null`
                if ('N;' === $value) {
                    $isStaticValue = false;
                }
                $value = var_export($value, true);
            } elseif (!\is_scalar($value)) {
                throw new InvalidArgumentException(sprintf('Cache key "%s" has non-serializable "%s" value.', $key, get_debug_type($value)));
            } else {
                $value = var_export($value, true);
            }

            if (!$isStaticValue) {
                $value = str_replace("\n", "\n    ", $value);
                $value = "static function () {\n    return {$value};\n}";
            }
            $hash = hash('md5', $value);

            if (null === $id = $dumpedMap[$hash] ?? null) {
                $id = $dumpedMap[$hash] = \count($dumpedMap);
                $dumpedValues .= "{$id} => {$value},\n";
            }

            $dump .= var_export($key, true)." => {$id},\n";
        }

        $dump .= "\n], [\n\n{$dumpedValues}\n]];\n";

        $tmpFile = uniqid($this->file, true);

        file_put_contents($tmpFile, $dump);
        @chmod($tmpFile, 0666 & ~umask());
        unset($serialized, $value, $dump);

        @rename($tmpFile, $this->file);
        unset(self::$valuesCache[$this->file]);

        $this->initialize();

        return $preload;
    }

    /**
     * Load the cache file.
     */
    private function initialize()
    {
        if (isset(self::$valuesCache[$this->file])) {
            $values = self::$valuesCache[$this->file];
        } elseif (!is_file($this->file)) {
            $this->keys = $this->values = [];

            return;
        } else {
            $values = self::$valuesCache[$this->file] = (include $this->file) ?: [[], []];
        }

        if (2 !== \count($values) || !isset($values[0], $values[1])) {
            $this->keys = $this->values = [];
        } else {
            [$this->keys, $this->values] = $values;
        }
    }

    private function generateItems(array $keys): \Generator
    {
        $f = self::$createCacheItem;
        $fallbackKeys = [];

        foreach ($keys as $key) {
            if (isset($this->keys[$key])) {
                $value = $this->values[$this->keys[$key]];

                if ('N;' === $value) {
                    yield $key => $f($key, null, true);
                } elseif ($value instanceof \Closure) {
                    try {
                        yield $key => $f($key, $value(), true);
                    } catch (\Throwable $e) {
                        yield $key => $f($key, null, false);
                    }
                } else {
                    yield $key => $f($key, $value, true);
                }
            } else {
                $fallbackKeys[] = $key;
            }
        }

        if ($fallbackKeys) {
            yield from $this->pool->getItems($fallbackKeys);
        }
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Cache\Adapter;

use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Cache\CacheItem;

// Help opcache.preload discover always-needed symbols
class_exists(CacheItem::class);

/**
 * Interface for adapters managing instances of Symfony's CacheItem.
 *
 * @author Kévin Dunglas <dunglas@gmail.com>
 */
interface AdapterInterface extends CacheItemPoolInterface
{
    /**
     * {@inheritdoc}
     *
     * @return CacheItem
     */
    public function getItem($key);

    /**
     * {@inheritdoc}
     *
     * @return \Traversable<string, CacheItem>
     */
    public function getItems(array $keys = []);

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function clear(string $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\Cache\Adapter;

use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\ResettableInterface;
use Symfony\Component\Cache\Traits\AbstractAdapterTrait;
use Symfony\Component\Cache\Traits\ContractsTrait;
use Symfony\Contracts\Cache\CacheInterface;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 */
abstract class AbstractAdapter implements AdapterInterface, CacheInterface, LoggerAwareInterface, ResettableInterface
{
    use AbstractAdapterTrait;
    use ContractsTrait;

    /**
     * @internal
     */
    protected const NS_SEPARATOR = ':';

    private static $apcuSupported;
    private static $phpFilesSupported;

    protected function __construct(string $namespace = '', int $defaultLifetime = 0)
    {
        $this->namespace = '' === $namespace ? '' : CacheItem::validateKey($namespace).static::NS_SEPARATOR;
        $this->defaultLifetime = $defaultLifetime;
        if (null !== $this->maxIdLength && \strlen($namespace) > $this->maxIdLength - 24) {
            throw new InvalidArgumentException(sprintf('Namespace must be %d chars max, %d given ("%s").', $this->maxIdLength - 24, \strlen($namespace), $namespace));
        }
        self::$createCacheItem ?? self::$createCacheItem = \Closure::bind(
            static function ($key, $value, $isHit) {
                $item = new CacheItem();
                $item->key = $key;
                $item->value = $v = $value;
                $item->isHit = $isHit;
                // Detect wrapped values that encode for their expiry and creation duration
                // For compactness, these values are packed in the key of an array using
                // magic numbers in the form 9D-..-..-..-..-00-..-..-..-5F
                if (\is_array($v) && 1 === \count($v) && 10 === \strlen($k = (string) array_key_first($v)) && "\x9D" === $k[0] && "\0" === $k[5] && "\x5F" === $k[9]) {
                    $item->value = $v[$k];
                    $v = unpack('Ve/Nc', substr($k, 1, -1));
                    $item->metadata[CacheItem::METADATA_EXPIRY] = $v['e'] + CacheItem::METADATA_EXPIRY_OFFSET;
                    $item->metadata[CacheItem::METADATA_CTIME] = $v['c'];
                }

                return $item;
            },
            null,
            CacheItem::class
        );
        self::$mergeByLifetime ?? self::$mergeByLifetime = \Closure::bind(
            static function ($deferred, $namespace, &$expiredIds, $getId, $defaultLifetime) {
                $byLifetime = [];
                $now = microtime(true);
                $expiredIds = [];

                foreach ($deferred as $key => $item) {
                    $key = (string) $key;
                    if (null === $item->expiry) {
                        $ttl = 0 < $defaultLifetime ? $defaultLifetime : 0;
                    } elseif (!$item->expiry) {
                        $ttl = 0;
                    } elseif (0 >= $ttl = (int) (0.1 + $item->expiry - $now)) {
                        $expiredIds[] = $getId($key);
                        continue;
                    }
                    if (isset(($metadata = $item->newMetadata)[CacheItem::METADATA_TAGS])) {
                        unset($metadata[CacheItem::METADATA_TAGS]);
                    }
                    // For compactness, expiry and creation duration are packed in the key of an array, using magic numbers as separators
                    $byLifetime[$ttl][$getId($key)] = $metadata ? ["\x9D".pack('VN', (int) (0.1 + $metadata[self::METADATA_EXPIRY] - self::METADATA_EXPIRY_OFFSET), $metadata[self::METADATA_CTIME])."\x5F" => $item->value] : $item->value;
                }

                return $byLifetime;
            },
            null,
            CacheItem::class
        );
    }

    /**
     * Returns the best possible adapter that your runtime supports.
     *
     * Using ApcuAdapter makes system caches compatible with read-only filesystems.
     *
     * @return AdapterInterface
     */
    public static function createSystemCache(string $namespace, int $defaultLifetime, string $version, string $directory, ?LoggerInterface $logger = null)
    {
        $opcache = new PhpFilesAdapter($namespace, $defaultLifetime, $directory, true);
        if (null !== $logger) {
            $opcache->setLogger($logger);
        }

        if (!self::$apcuSupported = self::$apcuSupported ?? ApcuAdapter::isSupported()) {
            return $opcache;
        }

        if (\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) && !filter_var(\ini_get('apc.enable_cli'), \FILTER_VALIDATE_BOOLEAN)) {
            return $opcache;
        }

        $apcu = new ApcuAdapter($namespace, intdiv($defaultLifetime, 5), $version);
        if (null !== $logger) {
            $apcu->setLogger($logger);
        }

        return new ChainAdapter([$apcu, $opcache]);
    }

    public static function createConnection(string $dsn, array $options = [])
    {
        if (str_starts_with($dsn, 'redis:') || str_starts_with($dsn, 'rediss:')) {
            return RedisAdapter::createConnection($dsn, $options);
        }
        if (str_starts_with($dsn, 'memcached:')) {
            return MemcachedAdapter::createConnection($dsn, $options);
        }
        if (0 === strpos($dsn, 'couchbase:')) {
            if (CouchbaseBucketAdapter::isSupported()) {
                return CouchbaseBucketAdapter::createConnection($dsn, $options);
            }

            return CouchbaseCollectionAdapter::createConnection($dsn, $options);
        }

        throw new InvalidArgumentException('Unsupported DSN: it does not start with "redis[s]:", "memcached:" nor "couchbase:".');
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function commit()
    {
        $ok = true;
        $byLifetime = (self::$mergeByLifetime)($this->deferred, $this->namespace, $expiredIds, \Closure::fromCallable([$this, 'getId']), $this->defaultLifetime);
        $retry = $this->deferred = [];

        if ($expiredIds) {
            try {
                $this->doDelete($expiredIds);
            } catch (\Exception $e) {
                $ok = false;
                CacheItem::log($this->logger, 'Failed to delete expired items: '.$e->getMessage(), ['exception' => $e, 'cache-adapter' => get_debug_type($this)]);
            }
        }
        foreach ($byLifetime as $lifetime => $values) {
            try {
                $e = $this->doSave($values, $lifetime);
            } catch (\Exception $e) {
            }
            if (true === $e || [] === $e) {
                continue;
            }
            if (\is_array($e) || 1 === \count($values)) {
                foreach (\is_array($e) ? $e : array_keys($values) as $id) {
                    $ok = false;
                    $v = $values[$id];
                    $type = get_debug_type($v);
                    $message = sprintf('Failed to save key "{key}" of type %s%s', $type, $e instanceof \Exception ? ': '.$e->getMessage() : '.');
                    CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->namespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
                }
            } else {
                foreach ($values as $id => $v) {
                    $retry[$lifetime][] = $id;
                }
            }
        }

        // When bulk-save failed, retry each item individually
        foreach ($retry as $lifetime => $ids) {
            foreach ($ids as $id) {
                try {
                    $v = $byLifetime[$lifetime][$id];
                    $e = $this->doSave([$id => $v], $lifetime);
                } catch (\Exception $e) {
                }
                if (true === $e || [] === $e) {
                    continue;
                }
                $ok = false;
                $type = get_debug_type($v);
                $message = sprintf('Failed to save key "{key}" of type %s%s', $type, $e instanceof \Exception ? ': '.$e->getMessage() : '.');
                CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->namespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
            }
        }

        return $ok;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Cache\Adapter;

use Doctrine\DBAL\ArrayParameterType;
use Doctrine\DBAL\Configuration;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Driver\ServerInfoAwareConnection;
use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Exception as DBALException;
use Doctrine\DBAL\Exception\TableNotFoundException;
use Doctrine\DBAL\ParameterType;
use Doctrine\DBAL\Schema\DefaultSchemaManagerFactory;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\ServerVersionProvider;
use Doctrine\DBAL\Tools\DsnParser;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
use Symfony\Component\Cache\PruneableInterface;

class DoctrineDbalAdapter extends AbstractAdapter implements PruneableInterface
{
    protected $maxIdLength = 255;

    private $marshaller;
    private $conn;
    private $platformName;
    private $serverVersion;
    private $table = 'cache_items';
    private $idCol = 'item_id';
    private $dataCol = 'item_data';
    private $lifetimeCol = 'item_lifetime';
    private $timeCol = 'item_time';
    private $namespace;

    /**
     * You can either pass an existing database Doctrine DBAL Connection or
     * a DSN string that will be used to connect to the database.
     *
     * The cache table is created automatically when possible.
     * Otherwise, use the createTable() method.
     *
     * List of available options:
     *  * db_table: The name of the table [default: cache_items]
     *  * db_id_col: The column where to store the cache id [default: item_id]
     *  * db_data_col: The column where to store the cache data [default: item_data]
     *  * db_lifetime_col: The column where to store the lifetime [default: item_lifetime]
     *  * db_time_col: The column where to store the timestamp [default: item_time]
     *
     * @param Connection|string $connOrDsn
     *
     * @throws InvalidArgumentException When namespace contains invalid characters
     */
    public function __construct($connOrDsn, string $namespace = '', int $defaultLifetime = 0, array $options = [], ?MarshallerInterface $marshaller = null)
    {
        if (isset($namespace[0]) && preg_match('#[^-+.A-Za-z0-9]#', $namespace, $match)) {
            throw new InvalidArgumentException(sprintf('Namespace contains "%s" but only characters in [-+.A-Za-z0-9] are allowed.', $match[0]));
        }

        if ($connOrDsn instanceof Connection) {
            $this->conn = $connOrDsn;
        } elseif (\is_string($connOrDsn)) {
            if (!class_exists(DriverManager::class)) {
                throw new InvalidArgumentException('Failed to parse DSN. Try running "composer require doctrine/dbal".');
            }
            if (class_exists(DsnParser::class)) {
                $params = (new DsnParser([
                    'db2' => 'ibm_db2',
                    'mssql' => 'pdo_sqlsrv',
                    'mysql' => 'pdo_mysql',
                    'mysql2' => 'pdo_mysql',
                    'postgres' => 'pdo_pgsql',
                    'postgresql' => 'pdo_pgsql',
                    'pgsql' => 'pdo_pgsql',
                    'sqlite' => 'pdo_sqlite',
                    'sqlite3' => 'pdo_sqlite',
                ]))->parse($connOrDsn);
            } else {
                $params = ['url' => $connOrDsn];
            }

            $config = new Configuration();
            if (class_exists(DefaultSchemaManagerFactory::class)) {
                $config->setSchemaManagerFactory(new DefaultSchemaManagerFactory());
            }

            $this->conn = DriverManager::getConnection($params, $config);
        } else {
            throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be "%s" or string, "%s" given.', __METHOD__, Connection::class, get_debug_type($connOrDsn)));
        }

        $this->table = $options['db_table'] ?? $this->table;
        $this->idCol = $options['db_id_col'] ?? $this->idCol;
        $this->dataCol = $options['db_data_col'] ?? $this->dataCol;
        $this->lifetimeCol = $options['db_lifetime_col'] ?? $this->lifetimeCol;
        $this->timeCol = $options['db_time_col'] ?? $this->timeCol;
        $this->namespace = $namespace;
        $this->marshaller = $marshaller ?? new DefaultMarshaller();

        parent::__construct($namespace, $defaultLifetime);
    }

    /**
     * Creates the table to store cache items which can be called once for setup.
     *
     * Cache ID are saved in a column of maximum length 255. Cache data is
     * saved in a BLOB.
     *
     * @throws DBALException When the table already exists
     */
    public function createTable()
    {
        $schema = new Schema();
        $this->addTableToSchema($schema);

        foreach ($schema->toSql($this->conn->getDatabasePlatform()) as $sql) {
            $this->conn->executeStatement($sql);
        }
    }

    /**
     * {@inheritdoc}
     */
    public function configureSchema(Schema $schema, Connection $forConnection): void
    {
        // only update the schema for this connection
        if ($forConnection !== $this->conn) {
            return;
        }

        if ($schema->hasTable($this->table)) {
            return;
        }

        $this->addTableToSchema($schema);
    }

    /**
     * {@inheritdoc}
     */
    public function prune(): bool
    {
        $deleteSql = "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol <= ?";
        $params = [time()];
        $paramTypes = [ParameterType::INTEGER];

        if ('' !== $this->namespace) {
            $deleteSql .= " AND $this->idCol LIKE ?";
            $params[] = sprintf('%s%%', $this->namespace);
            $paramTypes[] = ParameterType::STRING;
        }

        try {
            $this->conn->executeStatement($deleteSql, $params, $paramTypes);
        } catch (TableNotFoundException $e) {
        }

        return true;
    }

    /**
     * {@inheritdoc}
     */
    protected function doFetch(array $ids): iterable
    {
        $now = time();
        $expired = [];

        $sql = "SELECT $this->idCol, CASE WHEN $this->lifetimeCol IS NULL OR $this->lifetimeCol + $this->timeCol > ? THEN $this->dataCol ELSE NULL END FROM $this->table WHERE $this->idCol IN (?)";
        $result = $this->conn->executeQuery($sql, [
            $now,
            $ids,
        ], [
            ParameterType::INTEGER,
            class_exists(ArrayParameterType::class) ? ArrayParameterType::STRING : Connection::PARAM_STR_ARRAY,
        ])->iterateNumeric();

        foreach ($result as $row) {
            if (null === $row[1]) {
                $expired[] = $row[0];
            } else {
                yield $row[0] => $this->marshaller->unmarshall(\is_resource($row[1]) ? stream_get_contents($row[1]) : $row[1]);
            }
        }

        if ($expired) {
            $sql = "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol <= ? AND $this->idCol IN (?)";
            $this->conn->executeStatement($sql, [
                $now,
                $expired,
            ], [
                ParameterType::INTEGER,
                class_exists(ArrayParameterType::class) ? ArrayParameterType::STRING : Connection::PARAM_STR_ARRAY,
            ]);
        }
    }

    /**
     * {@inheritdoc}
     */
    protected function doHave(string $id): bool
    {
        $sql = "SELECT 1 FROM $this->table WHERE $this->idCol = ? AND ($this->lifetimeCol IS NULL OR $this->lifetimeCol + $this->timeCol > ?)";
        $result = $this->conn->executeQuery($sql, [
            $id,
            time(),
        ], [
            ParameterType::STRING,
            ParameterType::INTEGER,
        ]);

        return (bool) $result->fetchOne();
    }

    /**
     * {@inheritdoc}
     */
    protected function doClear(string $namespace): bool
    {
        if ('' === $namespace) {
            if ('sqlite' === $this->getPlatformName()) {
                $sql = "DELETE FROM $this->table";
            } else {
                $sql = "TRUNCATE TABLE $this->table";
            }
        } else {
            $sql = "DELETE FROM $this->table WHERE $this->idCol LIKE '$namespace%'";
        }

        try {
            $this->conn->executeStatement($sql);
        } catch (TableNotFoundException $e) {
        }

        return true;
    }

    /**
     * {@inheritdoc}
     */
    protected function doDelete(array $ids): bool
    {
        $sql = "DELETE FROM $this->table WHERE $this->idCol IN (?)";
        try {
            $this->conn->executeStatement($sql, [array_values($ids)], [class_exists(ArrayParameterType::class) ? ArrayParameterType::STRING : Connection::PARAM_STR_ARRAY]);
        } catch (TableNotFoundException $e) {
        }

        return true;
    }

    /**
     * {@inheritdoc}
     */
    protected function doSave(array $values, int $lifetime)
    {
        if (!$values = $this->marshaller->marshall($values, $failed)) {
            return $failed;
        }

        $platformName = $this->getPlatformName();
        $insertSql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?)";

        switch (true) {
            case 'mysql' === $platformName:
                $sql = $insertSql." ON DUPLICATE KEY UPDATE $this->dataCol = VALUES($this->dataCol), $this->lifetimeCol = VALUES($this->lifetimeCol), $this->timeCol = VALUES($this->timeCol)";
                break;
            case 'oci' === $platformName:
                // DUAL is Oracle specific dummy table
                $sql = "MERGE INTO $this->table USING DUAL ON ($this->idCol = ?) ".
                    "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?) ".
                    "WHEN MATCHED THEN UPDATE SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ?";
                break;
            case 'sqlsrv' === $platformName && version_compare($this->getServerVersion(), '10', '>='):
                // MERGE is only available since SQL Server 2008 and must be terminated by semicolon
                // It also requires HOLDLOCK according to http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx
                $sql = "MERGE INTO $this->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON ($this->idCol = ?) ".
                    "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?) ".
                    "WHEN MATCHED THEN UPDATE SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ?;";
                break;
            case 'sqlite' === $platformName:
                $sql = 'INSERT OR REPLACE'.substr($insertSql, 6);
                break;
            case 'pgsql' === $platformName && version_compare($this->getServerVersion(), '9.5', '>='):
                $sql = $insertSql." ON CONFLICT ($this->idCol) DO UPDATE SET ($this->dataCol, $this->lifetimeCol, $this->timeCol) = (EXCLUDED.$this->dataCol, EXCLUDED.$this->lifetimeCol, EXCLUDED.$this->timeCol)";
                break;
            default:
                $platformName = null;
                $sql = "UPDATE $this->table SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ? WHERE $this->idCol = ?";
                break;
        }

        $now = time();
        $lifetime = $lifetime ?: null;
        try {
            $stmt = $this->conn->prepare($sql);
        } catch (TableNotFoundException $e) {
            if (!$this->conn->isTransactionActive() || \in_array($platformName, ['pgsql', 'sqlite', 'sqlsrv'], true)) {
                $this->createTable();
            }
            $stmt = $this->conn->prepare($sql);
        }

        if ('sqlsrv' === $platformName || 'oci' === $platformName) {
            $bind = static function ($id, $data) use ($stmt) {
                $stmt->bindValue(1, $id);
                $stmt->bindValue(2, $id);
                $stmt->bindValue(3, $data, ParameterType::LARGE_OBJECT);
                $stmt->bindValue(6, $data, ParameterType::LARGE_OBJECT);
            };
            $stmt->bindValue(4, $lifetime, ParameterType::INTEGER);
            $stmt->bindValue(5, $now, ParameterType::INTEGER);
            $stmt->bindValue(7, $lifetime, ParameterType::INTEGER);
            $stmt->bindValue(8, $now, ParameterType::INTEGER);
        } elseif (null !== $platformName) {
            $bind = static function ($id, $data) use ($stmt) {
                $stmt->bindValue(1, $id);
                $stmt->bindValue(2, $data, ParameterType::LARGE_OBJECT);
            };
            $stmt->bindValue(3, $lifetime, ParameterType::INTEGER);
            $stmt->bindValue(4, $now, ParameterType::INTEGER);
        } else {
            $stmt->bindValue(2, $lifetime, ParameterType::INTEGER);
            $stmt->bindValue(3, $now, ParameterType::INTEGER);

            $insertStmt = $this->conn->prepare($insertSql);
            $insertStmt->bindValue(3, $lifetime, ParameterType::INTEGER);
            $insertStmt->bindValue(4, $now, ParameterType::INTEGER);

            $bind = static function ($id, $data) use ($stmt, $insertStmt) {
                $stmt->bindValue(1, $data, ParameterType::LARGE_OBJECT);
                $stmt->bindValue(4, $id);
                $insertStmt->bindValue(1, $id);
                $insertStmt->bindValue(2, $data, ParameterType::LARGE_OBJECT);
            };
        }

        foreach ($values as $id => $data) {
            $bind($id, $data);
            try {
                $rowCount = $stmt->executeStatement();
            } catch (TableNotFoundException $e) {
                if (!$this->conn->isTransactionActive() || \in_array($platformName, ['pgsql', 'sqlite', 'sqlsrv'], true)) {
                    $this->createTable();
                }
                $rowCount = $stmt->executeStatement();
            }
            if (null === $platformName && 0 === $rowCount) {
                try {
                    $insertStmt->executeStatement();
                } catch (DBALException $e) {
                    // A concurrent write won, let it be
                }
            }
        }

        return $failed;
    }

    /**
     * @internal
     */
    protected function getId($key)
    {
        if ('pgsql' !== $this->getPlatformName()) {
            return parent::getId($key);
        }

        if (str_contains($key, "\0") || str_contains($key, '%') || !preg_match('//u', $key)) {
            $key = rawurlencode($key);
        }

        return parent::getId($key);
    }

    private function getPlatformName(): string
    {
        if (isset($this->platformName)) {
            return $this->platformName;
        }

        $platform = $this->conn->getDatabasePlatform();

        switch (true) {
            case $platform instanceof \Doctrine\DBAL\Platforms\MySQLPlatform:
            case $platform instanceof \Doctrine\DBAL\Platforms\MySQL57Platform:
                return $this->platformName = 'mysql';

            case $platform instanceof \Doctrine\DBAL\Platforms\SqlitePlatform:
                return $this->platformName = 'sqlite';

            case $platform instanceof \Doctrine\DBAL\Platforms\PostgreSQLPlatform:
            case $platform instanceof \Doctrine\DBAL\Platforms\PostgreSQL94Platform:
                return $this->platformName = 'pgsql';

            case $platform instanceof \Doctrine\DBAL\Platforms\OraclePlatform:
                return $this->platformName = 'oci';

            case $platform instanceof \Doctrine\DBAL\Platforms\SQLServerPlatform:
            case $platform instanceof \Doctrine\DBAL\Platforms\SQLServer2012Platform:
                return $this->platformName = 'sqlsrv';

            default:
                return $this->platformName = \get_class($platform);
        }
    }

    private function getServerVersion(): string
    {
        if (isset($this->serverVersion)) {
            return $this->serverVersion;
        }

        if ($this->conn instanceof ServerVersionProvider || $this->conn instanceof ServerInfoAwareConnection) {
            return $this->serverVersion = $this->conn->getServerVersion();
        }

        // The condition should be removed once support for DBAL <3.3 is dropped
        $conn = method_exists($this->conn, 'getNativeConnection') ? $this->conn->getNativeConnection() : $this->conn->getWrappedConnection();

        return $this->serverVersion = $conn->getAttribute(\PDO::ATTR_SERVER_VERSION);
    }

    private function addTableToSchema(Schema $schema): void
    {
        $types = [
            'mysql' => 'binary',
            'sqlite' => 'text',
        ];

        $table = $schema->createTable($this->table);
        $table->addColumn($this->idCol, $types[$this->getPlatformName()] ?? 'string', ['length' => 255]);
        $table->addColumn($this->dataCol, 'blob', ['length' => 16777215]);
        $table->addColumn($this->lifetimeCol, 'integer', ['unsigned' => true, 'notnull' => false]);
        $table->addColumn($this->timeCol, 'integer', ['unsigned' => true]);
        $table->setPrimaryKey([$this->idCol]);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Cache\Adapter;

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Schema\Schema;
use Psr\Cache\CacheItemInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
use Symfony\Component\Cache\PruneableInterface;

class PdoAdapter extends AbstractAdapter implements PruneableInterface
{
    protected $maxIdLength = 255;

    private $marshaller;
    private $conn;
    private $dsn;
    private $driver;
    private $serverVersion;
    private $table = 'cache_items';
    private $idCol = 'item_id';
    private $dataCol = 'item_data';
    private $lifetimeCol = 'item_lifetime';
    private $timeCol = 'item_time';
    private $username = null;
    private $password = null;
    private $connectionOptions = [];
    private $namespace;

    private $dbalAdapter;

    /**
     * You can either pass an existing database connection as PDO instance or
     * a DSN string that will be used to lazy-connect to the database when the
     * cache is actually used.
     *
     * List of available options:
     *  * db_table: The name of the table [default: cache_items]
     *  * db_id_col: The column where to store the cache id [default: item_id]
     *  * db_data_col: The column where to store the cache data [default: item_data]
     *  * db_lifetime_col: The column where to store the lifetime [default: item_lifetime]
     *  * db_time_col: The column where to store the timestamp [default: item_time]
     *  * db_username: The username when lazy-connect [default: '']
     *  * db_password: The password when lazy-connect [default: '']
     *  * db_connection_options: An array of driver-specific connection options [default: []]
     *
     * @param \PDO|string $connOrDsn
     *
     * @throws InvalidArgumentException When first argument is not PDO nor Connection nor string
     * @throws InvalidArgumentException When PDO error mode is not PDO::ERRMODE_EXCEPTION
     * @throws InvalidArgumentException When namespace contains invalid characters
     */
    public function __construct($connOrDsn, string $namespace = '', int $defaultLifetime = 0, array $options = [], ?MarshallerInterface $marshaller = null)
    {
        if ($connOrDsn instanceof Connection || (\is_string($connOrDsn) && str_contains($connOrDsn, '://'))) {
            trigger_deprecation('symfony/cache', '5.4', 'Usage of a DBAL Connection with "%s" is deprecated and will be removed in symfony 6.0. Use "%s" instead.', __CLASS__, DoctrineDbalAdapter::class);
            $this->dbalAdapter = new DoctrineDbalAdapter($connOrDsn, $namespace, $defaultLifetime, $options, $marshaller);

            return;
        }

        if (isset($namespace[0]) && preg_match('#[^-+.A-Za-z0-9]#', $namespace, $match)) {
            throw new InvalidArgumentException(sprintf('Namespace contains "%s" but only characters in [-+.A-Za-z0-9] are allowed.', $match[0]));
        }

        if ($connOrDsn instanceof \PDO) {
            if (\PDO::ERRMODE_EXCEPTION !== $connOrDsn->getAttribute(\PDO::ATTR_ERRMODE)) {
                throw new InvalidArgumentException(sprintf('"%s" requires PDO error mode attribute be set to throw Exceptions (i.e. $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION)).', __CLASS__));
            }

            $this->conn = $connOrDsn;
        } elseif (\is_string($connOrDsn)) {
            $this->dsn = $connOrDsn;
        } else {
            throw new InvalidArgumentException(sprintf('"%s" requires PDO or Doctrine\DBAL\Connection instance or DSN string as first argument, "%s" given.', __CLASS__, get_debug_type($connOrDsn)));
        }

        $this->table = $options['db_table'] ?? $this->table;
        $this->idCol = $options['db_id_col'] ?? $this->idCol;
        $this->dataCol = $options['db_data_col'] ?? $this->dataCol;
        $this->lifetimeCol = $options['db_lifetime_col'] ?? $this->lifetimeCol;
        $this->timeCol = $options['db_time_col'] ?? $this->timeCol;
        $this->username = $options['db_username'] ?? $this->username;
        $this->password = $options['db_password'] ?? $this->password;
        $this->connectionOptions = $options['db_connection_options'] ?? $this->connectionOptions;
        $this->namespace = $namespace;
        $this->marshaller = $marshaller ?? new DefaultMarshaller();

        parent::__construct($namespace, $defaultLifetime);
    }

    /**
     * {@inheritDoc}
     */
    public function getItem($key)
    {
        if (isset($this->dbalAdapter)) {
            return $this->dbalAdapter->getItem($key);
        }

        return parent::getItem($key);
    }

    /**
     * {@inheritDoc}
     */
    public function getItems(array $keys = [])
    {
        if (isset($this->dbalAdapter)) {
            return $this->dbalAdapter->getItems($keys);
        }

        return parent::getItems($keys);
    }

    /**
     * {@inheritDoc}
     */
    public function hasItem($key)
    {
        if (isset($this->dbalAdapter)) {
            return $this->dbalAdapter->hasItem($key);
        }

        return parent::hasItem($key);
    }

    /**
     * {@inheritDoc}
     */
    public function deleteItem($key)
    {
        if (isset($this->dbalAdapter)) {
            return $this->dbalAdapter->deleteItem($key);
        }

        return parent::deleteItem($key);
    }

    /**
     * {@inheritDoc}
     */
    public function deleteItems(array $keys)
    {
        if (isset($this->dbalAdapter)) {
            return $this->dbalAdapter->deleteItems($keys);
        }

        return parent::deleteItems($keys);
    }

    /**
     * {@inheritDoc}
     */
    public function clear(string $prefix = '')
    {
        if (isset($this->dbalAdapter)) {
            return $this->dbalAdapter->clear($prefix);
        }

        return parent::clear($prefix);
    }

    /**
     * {@inheritDoc}
     */
    public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null)
    {
        if (isset($this->dbalAdapter)) {
            return $this->dbalAdapter->get($key, $callback, $beta, $metadata);
        }

        return parent::get($key, $callback, $beta, $metadata);
    }

    /**
     * {@inheritDoc}
     */
    public function delete(string $key): bool
    {
        if (isset($this->dbalAdapter)) {
            return $this->dbalAdapter->delete($key);
        }

        return parent::delete($key);
    }

    /**
     * {@inheritDoc}
     */
    public function save(CacheItemInterface $item)
    {
        if (isset($this->dbalAdapter)) {
            return $this->dbalAdapter->save($item);
        }

        return parent::save($item);
    }

    /**
     * {@inheritDoc}
     */
    public function saveDeferred(CacheItemInterface $item)
    {
        if (isset($this->dbalAdapter)) {
            return $this->dbalAdapter->saveDeferred($item);
        }

        return parent::saveDeferred($item);
    }

    /**
     * {@inheritDoc}
     */
    public function setLogger(LoggerInterface $logger): void
    {
        if (isset($this->dbalAdapter)) {
            $this->dbalAdapter->setLogger($logger);

            return;
        }

        parent::setLogger($logger);
    }

    /**
     * {@inheritDoc}
     */
    public function commit()
    {
        if (isset($this->dbalAdapter)) {
            return $this->dbalAdapter->commit();
        }

        return parent::commit();
    }

    /**
     * {@inheritDoc}
     */
    public function reset()
    {
        if (isset($this->dbalAdapter)) {
            $this->dbalAdapter->reset();

            return;
        }

        parent::reset();
    }

    /**
     * Creates the table to store cache items which can be called once for setup.
     *
     * Cache ID are saved in a column of maximum length 255. Cache data is
     * saved in a BLOB.
     *
     * @throws \PDOException    When the table already exists
     * @throws \DomainException When an unsupported PDO driver is used
     */
    public function createTable()
    {
        if (isset($this->dbalAdapter)) {
            $this->dbalAdapter->createTable();

            return;
        }

        // connect if we are not yet
        $conn = $this->getConnection();

        switch ($this->driver) {
            case 'mysql':
                // We use varbinary for the ID column because it prevents unwanted conversions:
                // - character set conversions between server and client
                // - trailing space removal
                // - case-insensitivity
                // - language processing like é == e
                $sql = "CREATE TABLE $this->table ($this->idCol VARBINARY(255) NOT NULL PRIMARY KEY, $this->dataCol MEDIUMBLOB NOT NULL, $this->lifetimeCol INTEGER UNSIGNED, $this->timeCol INTEGER UNSIGNED NOT NULL) COLLATE utf8mb4_bin, ENGINE = InnoDB";
                break;
            case 'sqlite':
                $sql = "CREATE TABLE $this->table ($this->idCol TEXT NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER, $this->timeCol INTEGER NOT NULL)";
                break;
            case 'pgsql':
                $sql = "CREATE TABLE $this->table ($this->idCol VARCHAR(255) NOT NULL PRIMARY KEY, $this->dataCol BYTEA NOT NULL, $this->lifetimeCol INTEGER, $this->timeCol INTEGER NOT NULL)";
                break;
            case 'oci':
                $sql = "CREATE TABLE $this->table ($this->idCol VARCHAR2(255) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER, $this->timeCol INTEGER NOT NULL)";
                break;
            case 'sqlsrv':
                $sql = "CREATE TABLE $this->table ($this->idCol VARCHAR(255) NOT NULL PRIMARY KEY, $this->dataCol VARBINARY(MAX) NOT NULL, $this->lifetimeCol INTEGER, $this->timeCol INTEGER NOT NULL)";
                break;
            default:
                throw new \DomainException(sprintf('Creating the cache table is currently not implemented for PDO driver "%s".', $this->driver));
        }

        $conn->exec($sql);
    }

    /**
     * Adds the Table to the Schema if the adapter uses this Connection.
     *
     * @deprecated since symfony/cache 5.4 use DoctrineDbalAdapter instead
     */
    public function configureSchema(Schema $schema, Connection $forConnection): void
    {
        if (isset($this->dbalAdapter)) {
            $this->dbalAdapter->configureSchema($schema, $forConnection);
        }
    }

    /**
     * {@inheritdoc}
     */
    public function prune()
    {
        if (isset($this->dbalAdapter)) {
            return $this->dbalAdapter->prune();
        }

        $deleteSql = "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol <= :time";

        if ('' !== $this->namespace) {
            $deleteSql .= " AND $this->idCol LIKE :namespace";
        }

        $connection = $this->getConnection();

        try {
            $delete = $connection->prepare($deleteSql);
        } catch (\PDOException $e) {
            return true;
        }
        $delete->bindValue(':time', time(), \PDO::PARAM_INT);

        if ('' !== $this->namespace) {
            $delete->bindValue(':namespace', sprintf('%s%%', $this->namespace), \PDO::PARAM_STR);
        }
        try {
            return $delete->execute();
        } catch (\PDOException $e) {
            return true;
        }
    }

    /**
     * {@inheritdoc}
     */
    protected function doFetch(array $ids)
    {
        $connection = $this->getConnection();

        $now = time();
        $expired = [];

        $sql = str_pad('', (\count($ids) << 1) - 1, '?,');
        $sql = "SELECT $this->idCol, CASE WHEN $this->lifetimeCol IS NULL OR $this->lifetimeCol + $this->timeCol > ? THEN $this->dataCol ELSE NULL END FROM $this->table WHERE $this->idCol IN ($sql)";
        $stmt = $connection->prepare($sql);
        $stmt->bindValue($i = 1, $now, \PDO::PARAM_INT);
        foreach ($ids as $id) {
            $stmt->bindValue(++$i, $id);
        }
        $result = $stmt->execute();

        if (\is_object($result)) {
            $result = $result->iterateNumeric();
        } else {
            $stmt->setFetchMode(\PDO::FETCH_NUM);
            $result = $stmt;
        }

        foreach ($result as $row) {
            if (null === $row[1]) {
                $expired[] = $row[0];
            } else {
                yield $row[0] => $this->marshaller->unmarshall(\is_resource($row[1]) ? stream_get_contents($row[1]) : $row[1]);
            }
        }

        if ($expired) {
            $sql = str_pad('', (\count($expired) << 1) - 1, '?,');
            $sql = "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol <= ? AND $this->idCol IN ($sql)";
            $stmt = $connection->prepare($sql);
            $stmt->bindValue($i = 1, $now, \PDO::PARAM_INT);
            foreach ($expired as $id) {
                $stmt->bindValue(++$i, $id);
            }
            $stmt->execute();
        }
    }

    /**
     * {@inheritdoc}
     */
    protected function doHave(string $id)
    {
        $connection = $this->getConnection();

        $sql = "SELECT 1 FROM $this->table WHERE $this->idCol = :id AND ($this->lifetimeCol IS NULL OR $this->lifetimeCol + $this->timeCol > :time)";
        $stmt = $connection->prepare($sql);

        $stmt->bindValue(':id', $id);
        $stmt->bindValue(':time', time(), \PDO::PARAM_INT);
        $stmt->execute();

        return (bool) $stmt->fetchColumn();
    }

    /**
     * {@inheritdoc}
     */
    protected function doClear(string $namespace)
    {
        $conn = $this->getConnection();

        if ('' === $namespace) {
            if ('sqlite' === $this->driver) {
                $sql = "DELETE FROM $this->table";
            } else {
                $sql = "TRUNCATE TABLE $this->table";
            }
        } else {
            $sql = "DELETE FROM $this->table WHERE $this->idCol LIKE '$namespace%'";
        }

        try {
            $conn->exec($sql);
        } catch (\PDOException $e) {
        }

        return true;
    }

    /**
     * {@inheritdoc}
     */
    protected function doDelete(array $ids)
    {
        $sql = str_pad('', (\count($ids) << 1) - 1, '?,');
        $sql = "DELETE FROM $this->table WHERE $this->idCol IN ($sql)";
        try {
            $stmt = $this->getConnection()->prepare($sql);
            $stmt->execute(array_values($ids));
        } catch (\PDOException $e) {
        }

        return true;
    }

    /**
     * {@inheritdoc}
     */
    protected function doSave(array $values, int $lifetime)
    {
        if (!$values = $this->marshaller->marshall($values, $failed)) {
            return $failed;
        }

        $conn = $this->getConnection();

        $driver = $this->driver;
        $insertSql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time)";

        switch (true) {
            case 'mysql' === $driver:
                $sql = $insertSql." ON DUPLICATE KEY UPDATE $this->dataCol = VALUES($this->dataCol), $this->lifetimeCol = VALUES($this->lifetimeCol), $this->timeCol = VALUES($this->timeCol)";
                break;
            case 'oci' === $driver:
                // DUAL is Oracle specific dummy table
                $sql = "MERGE INTO $this->table USING DUAL ON ($this->idCol = ?) ".
                    "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?) ".
                    "WHEN MATCHED THEN UPDATE SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ?";
                break;
            case 'sqlsrv' === $driver && version_compare($this->getServerVersion(), '10', '>='):
                // MERGE is only available since SQL Server 2008 and must be terminated by semicolon
                // It also requires HOLDLOCK according to http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx
                $sql = "MERGE INTO $this->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON ($this->idCol = ?) ".
                    "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?) ".
                    "WHEN MATCHED THEN UPDATE SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ?;";
                break;
            case 'sqlite' === $driver:
                $sql = 'INSERT OR REPLACE'.substr($insertSql, 6);
                break;
            case 'pgsql' === $driver && version_compare($this->getServerVersion(), '9.5', '>='):
                $sql = $insertSql." ON CONFLICT ($this->idCol) DO UPDATE SET ($this->dataCol, $this->lifetimeCol, $this->timeCol) = (EXCLUDED.$this->dataCol, EXCLUDED.$this->lifetimeCol, EXCLUDED.$this->timeCol)";
                break;
            default:
                $driver = null;
                $sql = "UPDATE $this->table SET $this->dataCol = :data, $this->lifetimeCol = :lifetime, $this->timeCol = :time WHERE $this->idCol = :id";
                break;
        }

        $now = time();
        $lifetime = $lifetime ?: null;
        try {
            $stmt = $conn->prepare($sql);
        } catch (\PDOException $e) {
            if ($this->isTableMissing($e) && (!$conn->inTransaction() || \in_array($this->driver, ['pgsql', 'sqlite', 'sqlsrv'], true))) {
                $this->createTable();
            }
            $stmt = $conn->prepare($sql);
        }

        // $id and $data are defined later in the loop. Binding is done by reference, values are read on execution.
        if ('sqlsrv' === $driver || 'oci' === $driver) {
            $stmt->bindParam(1, $id);
            $stmt->bindParam(2, $id);
            $stmt->bindParam(3, $data, \PDO::PARAM_LOB);
            $stmt->bindValue(4, $lifetime, \PDO::PARAM_INT);
            $stmt->bindValue(5, $now, \PDO::PARAM_INT);
            $stmt->bindParam(6, $data, \PDO::PARAM_LOB);
            $stmt->bindValue(7, $lifetime, \PDO::PARAM_INT);
            $stmt->bindValue(8, $now, \PDO::PARAM_INT);
        } else {
            $stmt->bindParam(':id', $id);
            $stmt->bindParam(':data', $data, \PDO::PARAM_LOB);
            $stmt->bindValue(':lifetime', $lifetime, \PDO::PARAM_INT);
            $stmt->bindValue(':time', $now, \PDO::PARAM_INT);
        }
        if (null === $driver) {
            $insertStmt = $conn->prepare($insertSql);

            $insertStmt->bindParam(':id', $id);
            $insertStmt->bindParam(':data', $data, \PDO::PARAM_LOB);
            $insertStmt->bindValue(':lifetime', $lifetime, \PDO::PARAM_INT);
            $insertStmt->bindValue(':time', $now, \PDO::PARAM_INT);
        }

        foreach ($values as $id => $data) {
            try {
                $stmt->execute();
            } catch (\PDOException $e) {
                if ($this->isTableMissing($e) && (!$conn->inTransaction() || \in_array($this->driver, ['pgsql', 'sqlite', 'sqlsrv'], true))) {
                    $this->createTable();
                }
                $stmt->execute();
            }
            if (null === $driver && !$stmt->rowCount()) {
                try {
                    $insertStmt->execute();
                } catch (\PDOException $e) {
                    // A concurrent write won, let it be
                }
            }
        }

        return $failed;
    }

    /**
     * @internal
     */
    protected function getId($key)
    {
        if ('pgsql' !== $this->driver ?? ($this->getConnection() ? $this->driver : null)) {
            return parent::getId($key);
        }

        if (str_contains($key, "\0") || str_contains($key, '%') || !preg_match('//u', $key)) {
            $key = rawurlencode($key);
        }

        return parent::getId($key);
    }

    private function getConnection(): \PDO
    {
        if (null === $this->conn) {
            $this->conn = new \PDO($this->dsn, $this->username, $this->password, $this->connectionOptions);
            $this->conn->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
        }
        if (null === $this->driver) {
            $this->driver = $this->conn->getAttribute(\PDO::ATTR_DRIVER_NAME);
        }

        return $this->conn;
    }

    private function getServerVersion(): string
    {
        if (null === $this->serverVersion) {
            $this->serverVersion = $this->conn->getAttribute(\PDO::ATTR_SERVER_VERSION);
        }

        return $this->serverVersion;
    }

    private function isTableMissing(\PDOException $exception): bool
    {
        $driver = $this->driver;
        [$sqlState, $code] = $exception->errorInfo ?? [null, $exception->getCode()];

        switch (true) {
            case 'pgsql' === $driver && '42P01' === $sqlState:
            case 'sqlite' === $driver && str_contains($exception->getMessage(), 'no such table:'):
            case 'oci' === $driver && 942 === $code:
            case 'sqlsrv' === $driver && 208 === $code:
            case 'mysql' === $driver && 1146 === $code:
                return true;
            default:
                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\Cache\Adapter;

use Psr\Cache\InvalidArgumentException;

/**
 * Interface for invalidating cached items using tags.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
interface TagAwareAdapterInterface extends AdapterInterface
{
    /**
     * Invalidates cached items using tags.
     *
     * @param string[] $tags An array of tags to invalidate
     *
     * @return bool
     *
     * @throws InvalidArgumentException When $tags is not valid
     */
    public function invalidateTags(array $tags);
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Cache\Adapter;

use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\Cache\Traits\FilesystemTrait;

class FilesystemAdapter extends AbstractAdapter implements PruneableInterface
{
    use FilesystemTrait;

    public function __construct(string $namespace = '', int $defaultLifetime = 0, ?string $directory = null, ?MarshallerInterface $marshaller = null)
    {
        $this->marshaller = $marshaller ?? new DefaultMarshaller();
        parent::__construct('', $defaultLifetime);
        $this->init($namespace, $directory);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Cache\Adapter;

use Psr\Cache\CacheItemInterface;
use Psr\Cache\InvalidArgumentException;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\Cache\ResettableInterface;
use Symfony\Component\Cache\Traits\ContractsTrait;
use Symfony\Component\Cache\Traits\ProxyTrait;
use Symfony\Contracts\Cache\TagAwareCacheInterface;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 */
class TagAwareAdapter implements TagAwareAdapterInterface, TagAwareCacheInterface, PruneableInterface, ResettableInterface, LoggerAwareInterface
{
    use ContractsTrait;
    use LoggerAwareTrait;
    use ProxyTrait;

    public const TAGS_PREFIX = "\0tags\0";

    private $deferred = [];
    private $tags;
    private $knownTagVersions = [];
    private $knownTagVersionsTtl;

    private static $createCacheItem;
    private static $setCacheItemTags;
    private static $getTagsByKey;
    private static $saveTags;

    public function __construct(AdapterInterface $itemsPool, ?AdapterInterface $tagsPool = null, float $knownTagVersionsTtl = 0.15)
    {
        $this->pool = $itemsPool;
        $this->tags = $tagsPool ?: $itemsPool;
        $this->knownTagVersionsTtl = $knownTagVersionsTtl;
        self::$createCacheItem ?? self::$createCacheItem = \Closure::bind(
            static function ($key, $value, CacheItem $protoItem) {
                $item = new CacheItem();
                $item->key = $key;
                $item->value = $value;
                $item->expiry = $protoItem->expiry;
                $item->poolHash = $protoItem->poolHash;

                return $item;
            },
            null,
            CacheItem::class
        );
        self::$setCacheItemTags ?? self::$setCacheItemTags = \Closure::bind(
            static function (CacheItem $item, $key, array &$itemTags) {
                $item->isTaggable = true;
                if (!$item->isHit) {
                    return $item;
                }
                if (isset($itemTags[$key])) {
                    foreach ($itemTags[$key] as $tag => $version) {
                        $item->metadata[CacheItem::METADATA_TAGS][$tag] = $tag;
                    }
                    unset($itemTags[$key]);
                } else {
                    $item->value = null;
                    $item->isHit = false;
                }

                return $item;
            },
            null,
            CacheItem::class
        );
        self::$getTagsByKey ?? self::$getTagsByKey = \Closure::bind(
            static function ($deferred) {
                $tagsByKey = [];
                foreach ($deferred as $key => $item) {
                    $tagsByKey[$key] = $item->newMetadata[CacheItem::METADATA_TAGS] ?? [];
                    $item->metadata = $item->newMetadata;
                }

                return $tagsByKey;
            },
            null,
            CacheItem::class
        );
        self::$saveTags ?? self::$saveTags = \Closure::bind(
            static function (AdapterInterface $tagsAdapter, array $tags) {
                ksort($tags);

                foreach ($tags as $v) {
                    $v->expiry = 0;
                    $tagsAdapter->saveDeferred($v);
                }

                return $tagsAdapter->commit();
            },
            null,
            CacheItem::class
        );
    }

    /**
     * {@inheritdoc}
     */
    public function invalidateTags(array $tags)
    {
        $ids = [];
        foreach ($tags as $tag) {
            \assert('' !== CacheItem::validateKey($tag));
            unset($this->knownTagVersions[$tag]);
            $ids[] = $tag.static::TAGS_PREFIX;
        }

        return !$tags || $this->tags->deleteItems($ids);
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function hasItem($key)
    {
        if (\is_string($key) && isset($this->deferred[$key])) {
            $this->commit();
        }

        if (!$this->pool->hasItem($key)) {
            return false;
        }

        $itemTags = $this->pool->getItem(static::TAGS_PREFIX.$key);

        if (!$itemTags->isHit()) {
            return false;
        }

        if (!$itemTags = $itemTags->get()) {
            return true;
        }

        foreach ($this->getTagVersions([$itemTags]) as $tag => $version) {
            if ($itemTags[$tag] !== $version) {
                return false;
            }
        }

        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function getItem($key)
    {
        foreach ($this->getItems([$key]) as $item) {
            return $item;
        }

        return null;
    }

    /**
     * {@inheritdoc}
     */
    public function getItems(array $keys = [])
    {
        $tagKeys = [];
        $commit = false;

        foreach ($keys as $key) {
            if ('' !== $key && \is_string($key)) {
                $commit = $commit || isset($this->deferred[$key]);
                $key = static::TAGS_PREFIX.$key;
                $tagKeys[$key] = $key;
            }
        }

        if ($commit) {
            $this->commit();
        }

        try {
            $items = $this->pool->getItems($tagKeys + $keys);
        } catch (InvalidArgumentException $e) {
            $this->pool->getItems($keys); // Should throw an exception

            throw $e;
        }

        return $this->generateItems($items, $tagKeys);
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function clear(string $prefix = '')
    {
        if ('' !== $prefix) {
            foreach ($this->deferred as $key => $item) {
                if (str_starts_with($key, $prefix)) {
                    unset($this->deferred[$key]);
                }
            }
        } else {
            $this->deferred = [];
        }

        if ($this->pool instanceof AdapterInterface) {
            return $this->pool->clear($prefix);
        }

        return $this->pool->clear();
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function deleteItem($key)
    {
        return $this->deleteItems([$key]);
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function deleteItems(array $keys)
    {
        foreach ($keys as $key) {
            if ('' !== $key && \is_string($key)) {
                $keys[] = static::TAGS_PREFIX.$key;
            }
        }

        return $this->pool->deleteItems($keys);
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function save(CacheItemInterface $item)
    {
        if (!$item instanceof CacheItem) {
            return false;
        }
        $this->deferred[$item->getKey()] = $item;

        return $this->commit();
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function saveDeferred(CacheItemInterface $item)
    {
        if (!$item instanceof CacheItem) {
            return false;
        }
        $this->deferred[$item->getKey()] = $item;

        return true;
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function commit()
    {
        if (!$this->deferred) {
            return true;
        }

        $ok = true;
        foreach ($this->deferred as $key => $item) {
            if (!$this->pool->saveDeferred($item)) {
                unset($this->deferred[$key]);
                $ok = false;
            }
        }

        $items = $this->deferred;
        $tagsByKey = (self::$getTagsByKey)($items);
        $this->deferred = [];

        $tagVersions = $this->getTagVersions($tagsByKey);
        $f = self::$createCacheItem;

        foreach ($tagsByKey as $key => $tags) {
            $this->pool->saveDeferred($f(static::TAGS_PREFIX.$key, array_intersect_key($tagVersions, $tags), $items[$key]));
        }

        return $this->pool->commit() && $ok;
    }

    /**
     * @return array
     */
    public function __sleep()
    {
        throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
    }

    public function __wakeup()
    {
        throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
    }

    public function __destruct()
    {
        $this->commit();
    }

    private function generateItems(iterable $items, array $tagKeys): \Generator
    {
        $bufferedItems = $itemTags = [];
        $f = self::$setCacheItemTags;

        foreach ($items as $key => $item) {
            if (!$tagKeys) {
                yield $key => $f($item, static::TAGS_PREFIX.$key, $itemTags);
                continue;
            }
            if (!isset($tagKeys[$key])) {
                $bufferedItems[$key] = $item;
                continue;
            }

            unset($tagKeys[$key]);

            if ($item->isHit()) {
                $itemTags[$key] = $item->get() ?: [];
            }

            if (!$tagKeys) {
                $tagVersions = $this->getTagVersions($itemTags);

                foreach ($itemTags as $key => $tags) {
                    foreach ($tags as $tag => $version) {
                        if ($tagVersions[$tag] !== $version) {
                            unset($itemTags[$key]);
                            continue 2;
                        }
                    }
                }
                $tagVersions = $tagKeys = null;

                foreach ($bufferedItems as $key => $item) {
                    yield $key => $f($item, static::TAGS_PREFIX.$key, $itemTags);
                }
                $bufferedItems = null;
            }
        }
    }

    private function getTagVersions(array $tagsByKey)
    {
        $tagVersions = [];
        $fetchTagVersions = false;

        foreach ($tagsByKey as $tags) {
            $tagVersions += $tags;

            foreach ($tags as $tag => $version) {
                if ($tagVersions[$tag] !== $version) {
                    unset($this->knownTagVersions[$tag]);
                }
            }
        }

        if (!$tagVersions) {
            return [];
        }

        $now = microtime(true);
        $tags = [];
        foreach ($tagVersions as $tag => $version) {
            $tags[$tag.static::TAGS_PREFIX] = $tag;
            if ($fetchTagVersions || ($this->knownTagVersions[$tag][1] ?? null) !== $version || $now - $this->knownTagVersions[$tag][0] >= $this->knownTagVersionsTtl) {
                // reuse previously fetched tag versions up to the ttl
                $fetchTagVersions = true;
            }
        }

        if (!$fetchTagVersions) {
            return $tagVersions;
        }

        $newTags = [];
        $newVersion = null;
        foreach ($this->tags->getItems(array_keys($tags)) as $tag => $version) {
            if (!$version->isHit()) {
                $newTags[$tag] = $version->set($newVersion ?? $newVersion = random_int(\PHP_INT_MIN, \PHP_INT_MAX));
            }
            $tagVersions[$tag = $tags[$tag]] = $version->get();
            $this->knownTagVersions[$tag] = [$now, $tagVersions[$tag]];
        }

        if ($newTags) {
            (self::$saveTags)($this->tags, $newTags);
        }

        return $tagVersions;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Cache\Adapter;

use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Cache\Exception\CacheException;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 */
class ApcuAdapter extends AbstractAdapter
{
    private $marshaller;

    /**
     * @throws CacheException if APCu is not enabled
     */
    public function __construct(string $namespace = '', int $defaultLifetime = 0, ?string $version = null, ?MarshallerInterface $marshaller = null)
    {
        if (!static::isSupported()) {
            throw new CacheException('APCu is not enabled.');
        }
        if ('cli' === \PHP_SAPI) {
            ini_set('apc.use_request_time', 0);
        }
        parent::__construct($namespace, $defaultLifetime);

        if (null !== $version) {
            CacheItem::validateKey($version);

            if (!apcu_exists($version.'@'.$namespace)) {
                $this->doClear($namespace);
                apcu_add($version.'@'.$namespace, null);
            }
        }

        $this->marshaller = $marshaller;
    }

    public static function isSupported()
    {
        return \function_exists('apcu_fetch') && filter_var(\ini_get('apc.enabled'), \FILTER_VALIDATE_BOOLEAN);
    }

    /**
     * {@inheritdoc}
     */
    protected function doFetch(array $ids)
    {
        $unserializeCallbackHandler = ini_set('unserialize_callback_func', __CLASS__.'::handleUnserializeCallback');
        try {
            $values = [];
            $ids = array_flip($ids);
            foreach (apcu_fetch(array_keys($ids), $ok) ?: [] as $k => $v) {
                if (!isset($ids[$k])) {
                    // work around https://github.com/krakjoe/apcu/issues/247
                    $k = key($ids);
                }
                unset($ids[$k]);

                if (null !== $v || $ok) {
                    $values[$k] = null !== $this->marshaller ? $this->marshaller->unmarshall($v) : $v;
                }
            }

            return $values;
        } catch (\Error $e) {
            throw new \ErrorException($e->getMessage(), $e->getCode(), \E_ERROR, $e->getFile(), $e->getLine());
        } finally {
            ini_set('unserialize_callback_func', $unserializeCallbackHandler);
        }
    }

    /**
     * {@inheritdoc}
     */
    protected function doHave(string $id)
    {
        return apcu_exists($id);
    }

    /**
     * {@inheritdoc}
     */
    protected function doClear(string $namespace)
    {
        return isset($namespace[0]) && class_exists(\APCUIterator::class, false) && ('cli' !== \PHP_SAPI || filter_var(\ini_get('apc.enable_cli'), \FILTER_VALIDATE_BOOLEAN))
            ? apcu_delete(new \APCUIterator(sprintf('/^%s/', preg_quote($namespace, '/')), \APC_ITER_KEY))
            : apcu_clear_cache();
    }

    /**
     * {@inheritdoc}
     */
    protected function doDelete(array $ids)
    {
        foreach ($ids as $id) {
            apcu_delete($id);
        }

        return true;
    }

    /**
     * {@inheritdoc}
     */
    protected function doSave(array $values, int $lifetime)
    {
        if (null !== $this->marshaller && (!$values = $this->marshaller->marshall($values, $failed))) {
            return $failed;
        }

        try {
            if (false === $failures = apcu_store($values, null, $lifetime)) {
                $failures = $values;
            }

            return array_keys($failures);
        } catch (\Throwable $e) {
            if (1 === \count($values)) {
                // Workaround https://github.com/krakjoe/apcu/issues/170
                apcu_delete(array_key_first($values));
            }

            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\Cache\Adapter;

use Symfony\Component\Cache\Exception\CacheException;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;

/**
 * @author Rob Frawley 2nd <rmf@src.run>
 * @author Nicolas Grekas <p@tchwork.com>
 */
class MemcachedAdapter extends AbstractAdapter
{
    /**
     * We are replacing characters that are illegal in Memcached keys with reserved characters from
     * {@see \Symfony\Contracts\Cache\ItemInterface::RESERVED_CHARACTERS} that are legal in Memcached.
     * Note: don’t use {@see \Symfony\Component\Cache\Adapter\AbstractAdapter::NS_SEPARATOR}.
     */
    private const RESERVED_MEMCACHED = " \n\r\t\v\f\0";
    private const RESERVED_PSR6 = '@()\{}/';

    protected $maxIdLength = 250;

    private $marshaller;
    private $client;
    private $lazyClient;

    /**
     * Using a MemcachedAdapter with a TagAwareAdapter for storing tags is discouraged.
     * Using a RedisAdapter is recommended instead. If you cannot do otherwise, be aware that:
     * - the Memcached::OPT_BINARY_PROTOCOL must be enabled
     *   (that's the default when using MemcachedAdapter::createConnection());
     * - tags eviction by Memcached's LRU algorithm will break by-tags invalidation;
     *   your Memcached memory should be large enough to never trigger LRU.
     *
     * Using a MemcachedAdapter as a pure items store is fine.
     */
    public function __construct(\Memcached $client, string $namespace = '', int $defaultLifetime = 0, ?MarshallerInterface $marshaller = null)
    {
        if (!static::isSupported()) {
            throw new CacheException('Memcached '.(\PHP_VERSION_ID >= 80100 ? '> 3.1.5' : '>= 2.2.0').' is required.');
        }
        if ('Memcached' === \get_class($client)) {
            $opt = $client->getOption(\Memcached::OPT_SERIALIZER);
            if (\Memcached::SERIALIZER_PHP !== $opt && \Memcached::SERIALIZER_IGBINARY !== $opt) {
                throw new CacheException('MemcachedAdapter: "serializer" option must be "php" or "igbinary".');
            }
            $this->maxIdLength -= \strlen($client->getOption(\Memcached::OPT_PREFIX_KEY));
            $this->client = $client;
        } else {
            $this->lazyClient = $client;
        }

        parent::__construct($namespace, $defaultLifetime);
        $this->enableVersioning();
        $this->marshaller = $marshaller ?? new DefaultMarshaller();
    }

    public static function isSupported()
    {
        return \extension_loaded('memcached') && version_compare(phpversion('memcached'), \PHP_VERSION_ID >= 80100 ? '3.1.6' : '2.2.0', '>=');
    }

    /**
     * Creates a Memcached instance.
     *
     * By default, the binary protocol, no block, and libketama compatible options are enabled.
     *
     * Examples for servers:
     * - 'memcached://user:pass@localhost?weight=33'
     * - [['localhost', 11211, 33]]
     *
     * @param array[]|string|string[] $servers An array of servers, a DSN, or an array of DSNs
     *
     * @return \Memcached
     *
     * @throws \ErrorException When invalid options or servers are provided
     */
    public static function createConnection($servers, array $options = [])
    {
        if (\is_string($servers)) {
            $servers = [$servers];
        } elseif (!\is_array($servers)) {
            throw new InvalidArgumentException(sprintf('MemcachedAdapter::createClient() expects array or string as first argument, "%s" given.', get_debug_type($servers)));
        }
        if (!static::isSupported()) {
            throw new CacheException('Memcached '.(\PHP_VERSION_ID >= 80100 ? '> 3.1.5' : '>= 2.2.0').' is required.');
        }
        set_error_handler(function ($type, $msg, $file, $line) { throw new \ErrorException($msg, 0, $type, $file, $line); });
        try {
            $client = new \Memcached($options['persistent_id'] ?? null);
            $username = $options['username'] ?? null;
            $password = $options['password'] ?? null;

            // parse any DSN in $servers
            foreach ($servers as $i => $dsn) {
                if (\is_array($dsn)) {
                    continue;
                }
                if (!str_starts_with($dsn, 'memcached:')) {
                    throw new InvalidArgumentException('Invalid Memcached DSN: it does not start with "memcached:".');
                }
                $params = preg_replace_callback('#^memcached:(//)?(?:([^@]*+)@)?#', function ($m) use (&$username, &$password) {
                    if (!empty($m[2])) {
                        [$username, $password] = explode(':', $m[2], 2) + [1 => null];
                        $username = rawurldecode($username);
                        $password = null !== $password ? rawurldecode($password) : null;
                    }

                    return 'file:'.($m[1] ?? '');
                }, $dsn);
                if (false === $params = parse_url($params)) {
                    throw new InvalidArgumentException('Invalid Memcached DSN.');
                }
                $query = $hosts = [];
                if (isset($params['query'])) {
                    parse_str($params['query'], $query);

                    if (isset($query['host'])) {
                        if (!\is_array($hosts = $query['host'])) {
                            throw new InvalidArgumentException('Invalid Memcached DSN: query parameter "host" must be an array.');
                        }
                        foreach ($hosts as $host => $weight) {
                            if (false === $port = strrpos($host, ':')) {
                                $hosts[$host] = [$host, 11211, (int) $weight];
                            } else {
                                $hosts[$host] = [substr($host, 0, $port), (int) substr($host, 1 + $port), (int) $weight];
                            }
                        }
                        $hosts = array_values($hosts);
                        unset($query['host']);
                    }
                    if ($hosts && !isset($params['host']) && !isset($params['path'])) {
                        unset($servers[$i]);
                        $servers = array_merge($servers, $hosts);
                        continue;
                    }
                }
                if (!isset($params['host']) && !isset($params['path'])) {
                    throw new InvalidArgumentException('Invalid Memcached DSN: missing host or path.');
                }
                if (isset($params['path']) && preg_match('#/(\d+)$#', $params['path'], $m)) {
                    $params['weight'] = $m[1];
                    $params['path'] = substr($params['path'], 0, -\strlen($m[0]));
                }
                $params += [
                    'host' => $params['host'] ?? $params['path'],
                    'port' => isset($params['host']) ? 11211 : null,
                    'weight' => 0,
                ];
                if ($query) {
                    $params += $query;
                    $options = $query + $options;
                }

                $servers[$i] = [$params['host'], $params['port'], $params['weight']];

                if ($hosts) {
                    $servers = array_merge($servers, $hosts);
                }
            }

            // set client's options
            unset($options['persistent_id'], $options['username'], $options['password'], $options['weight'], $options['lazy']);
            $options = array_change_key_case($options, \CASE_UPPER);
            $client->setOption(\Memcached::OPT_BINARY_PROTOCOL, true);
            $client->setOption(\Memcached::OPT_NO_BLOCK, true);
            $client->setOption(\Memcached::OPT_TCP_NODELAY, true);
            if (!\array_key_exists('LIBKETAMA_COMPATIBLE', $options) && !\array_key_exists(\Memcached::OPT_LIBKETAMA_COMPATIBLE, $options)) {
                $client->setOption(\Memcached::OPT_LIBKETAMA_COMPATIBLE, true);
            }
            foreach ($options as $name => $value) {
                if (\is_int($name)) {
                    continue;
                }
                if ('HASH' === $name || 'SERIALIZER' === $name || 'DISTRIBUTION' === $name) {
                    $value = \constant('Memcached::'.$name.'_'.strtoupper($value));
                }
                unset($options[$name]);

                if (\defined('Memcached::OPT_'.$name)) {
                    $options[\constant('Memcached::OPT_'.$name)] = $value;
                }
            }
            $client->setOptions($options + [\Memcached::OPT_SERIALIZER => \Memcached::SERIALIZER_PHP]);

            // set client's servers, taking care of persistent connections
            if (!$client->isPristine()) {
                $oldServers = [];
                foreach ($client->getServerList() as $server) {
                    $oldServers[] = [$server['host'], $server['port']];
                }

                $newServers = [];
                foreach ($servers as $server) {
                    if (1 < \count($server)) {
                        $server = array_values($server);
                        unset($server[2]);
                        $server[1] = (int) $server[1];
                    }
                    $newServers[] = $server;
                }

                if ($oldServers !== $newServers) {
                    $client->resetServerList();
                    $client->addServers($servers);
                }
            } else {
                $client->addServers($servers);
            }

            if (null !== $username || null !== $password) {
                if (!method_exists($client, 'setSaslAuthData')) {
                    trigger_error('Missing SASL support: the memcached extension must be compiled with --enable-memcached-sasl.');
                }
                $client->setSaslAuthData($username, $password);
            }

            return $client;
        } finally {
            restore_error_handler();
        }
    }

    /**
     * {@inheritdoc}
     */
    protected function doSave(array $values, int $lifetime)
    {
        if (!$values = $this->marshaller->marshall($values, $failed)) {
            return $failed;
        }

        if ($lifetime && $lifetime > 30 * 86400) {
            $lifetime += time();
        }

        $encodedValues = [];
        foreach ($values as $key => $value) {
            $encodedValues[self::encodeKey($key)] = $value;
        }

        return $this->checkResultCode($this->getClient()->setMulti($encodedValues, $lifetime)) ? $failed : false;
    }

    /**
     * {@inheritdoc}
     */
    protected function doFetch(array $ids)
    {
        try {
            $encodedIds = array_map([__CLASS__, 'encodeKey'], $ids);

            $encodedResult = $this->checkResultCode($this->getClient()->getMulti($encodedIds));

            $result = [];
            foreach ($encodedResult as $key => $value) {
                $result[self::decodeKey($key)] = $this->marshaller->unmarshall($value);
            }

            return $result;
        } catch (\Error $e) {
            throw new \ErrorException($e->getMessage(), $e->getCode(), \E_ERROR, $e->getFile(), $e->getLine());
        }
    }

    /**
     * {@inheritdoc}
     */
    protected function doHave(string $id)
    {
        return false !== $this->getClient()->get(self::encodeKey($id)) || $this->checkResultCode(\Memcached::RES_SUCCESS === $this->client->getResultCode());
    }

    /**
     * {@inheritdoc}
     */
    protected function doDelete(array $ids)
    {
        $ok = true;
        $encodedIds = array_map([__CLASS__, 'encodeKey'], $ids);
        foreach ($this->checkResultCode($this->getClient()->deleteMulti($encodedIds)) as $result) {
            if (\Memcached::RES_SUCCESS !== $result && \Memcached::RES_NOTFOUND !== $result) {
                $ok = false;
            }
        }

        return $ok;
    }

    /**
     * {@inheritdoc}
     */
    protected function doClear(string $namespace)
    {
        return '' === $namespace && $this->getClient()->flush();
    }

    private function checkResultCode($result)
    {
        $code = $this->client->getResultCode();

        if (\Memcached::RES_SUCCESS === $code || \Memcached::RES_NOTFOUND === $code) {
            return $result;
        }

        throw new CacheException('MemcachedAdapter client error: '.strtolower($this->client->getResultMessage()));
    }

    private function getClient(): \Memcached
    {
        if ($this->client) {
            return $this->client;
        }

        $opt = $this->lazyClient->getOption(\Memcached::OPT_SERIALIZER);
        if (\Memcached::SERIALIZER_PHP !== $opt && \Memcached::SERIALIZER_IGBINARY !== $opt) {
            throw new CacheException('MemcachedAdapter: "serializer" option must be "php" or "igbinary".');
        }
        if ('' !== $prefix = (string) $this->lazyClient->getOption(\Memcached::OPT_PREFIX_KEY)) {
            throw new CacheException(sprintf('MemcachedAdapter: "prefix_key" option must be empty when using proxified connections, "%s" given.', $prefix));
        }

        return $this->client = $this->lazyClient;
    }

    private static function encodeKey(string $key): string
    {
        return strtr($key, self::RESERVED_MEMCACHED, self::RESERVED_PSR6);
    }

    private static function decodeKey(string $key): string
    {
        return strtr($key, self::RESERVED_PSR6, self::RESERVED_MEMCACHED);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Cache\Adapter;

use Symfony\Component\Cache\Exception\CacheException;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\Cache\Traits\FilesystemCommonTrait;
use Symfony\Component\VarExporter\VarExporter;

/**
 * @author Piotr Stankowski <git@trakos.pl>
 * @author Nicolas Grekas <p@tchwork.com>
 * @author Rob Frawley 2nd <rmf@src.run>
 */
class PhpFilesAdapter extends AbstractAdapter implements PruneableInterface
{
    use FilesystemCommonTrait {
        doClear as private doCommonClear;
        doDelete as private doCommonDelete;
    }

    private $includeHandler;
    private $appendOnly;
    private $values = [];
    private $files = [];

    private static $startTime;
    private static $valuesCache = [];

    /**
     * @param $appendOnly Set to `true` to gain extra performance when the items stored in this pool never expire.
     *                    Doing so is encouraged because it fits perfectly OPcache's memory model.
     *
     * @throws CacheException if OPcache is not enabled
     */
    public function __construct(string $namespace = '', int $defaultLifetime = 0, ?string $directory = null, bool $appendOnly = false)
    {
        $this->appendOnly = $appendOnly;
        self::$startTime = self::$startTime ?? $_SERVER['REQUEST_TIME'] ?? time();
        parent::__construct('', $defaultLifetime);
        $this->init($namespace, $directory);
        $this->includeHandler = static function ($type, $msg, $file, $line) {
            throw new \ErrorException($msg, 0, $type, $file, $line);
        };
    }

    public static function isSupported()
    {
        self::$startTime = self::$startTime ?? $_SERVER['REQUEST_TIME'] ?? time();

        return \function_exists('opcache_invalidate') && filter_var(\ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN) && (!\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) || filter_var(\ini_get('opcache.enable_cli'), \FILTER_VALIDATE_BOOLEAN));
    }

    /**
     * @return bool
     */
    public function prune()
    {
        $time = time();
        $pruned = true;
        $getExpiry = true;

        set_error_handler($this->includeHandler);
        try {
            foreach ($this->scanHashDir($this->directory) as $file) {
                try {
                    if (\is_array($expiresAt = include $file)) {
                        $expiresAt = $expiresAt[0];
                    }
                } catch (\ErrorException $e) {
                    $expiresAt = $time;
                }

                if ($time >= $expiresAt) {
                    $pruned = ($this->doUnlink($file) || !file_exists($file)) && $pruned;
                }
            }
        } finally {
            restore_error_handler();
        }

        return $pruned;
    }

    /**
     * {@inheritdoc}
     */
    protected function doFetch(array $ids)
    {
        if ($this->appendOnly) {
            $now = 0;
            $missingIds = [];
        } else {
            $now = time();
            $missingIds = $ids;
            $ids = [];
        }
        $values = [];

        begin:
        $getExpiry = false;

        foreach ($ids as $id) {
            if (null === $value = $this->values[$id] ?? null) {
                $missingIds[] = $id;
            } elseif ('N;' === $value) {
                $values[$id] = null;
            } elseif (!\is_object($value)) {
                $values[$id] = $value;
            } elseif (!$value instanceof LazyValue) {
                $values[$id] = $value();
            } elseif (false === $values[$id] = include $value->file) {
                unset($values[$id], $this->values[$id]);
                $missingIds[] = $id;
            }
            if (!$this->appendOnly) {
                unset($this->values[$id]);
            }
        }

        if (!$missingIds) {
            return $values;
        }

        set_error_handler($this->includeHandler);
        try {
            $getExpiry = true;

            foreach ($missingIds as $k => $id) {
                try {
                    $file = $this->files[$id] ?? $this->files[$id] = $this->getFile($id);

                    if (isset(self::$valuesCache[$file])) {
                        [$expiresAt, $this->values[$id]] = self::$valuesCache[$file];
                    } elseif (\is_array($expiresAt = include $file)) {
                        if ($this->appendOnly) {
                            self::$valuesCache[$file] = $expiresAt;
                        }

                        [$expiresAt, $this->values[$id]] = $expiresAt;
                    } elseif ($now < $expiresAt) {
                        $this->values[$id] = new LazyValue($file);
                    }

                    if ($now >= $expiresAt) {
                        unset($this->values[$id], $missingIds[$k], self::$valuesCache[$file]);
                    }
                } catch (\ErrorException $e) {
                    unset($missingIds[$k]);
                }
            }
        } finally {
            restore_error_handler();
        }

        $ids = $missingIds;
        $missingIds = [];
        goto begin;
    }

    /**
     * {@inheritdoc}
     */
    protected function doHave(string $id)
    {
        if ($this->appendOnly && isset($this->values[$id])) {
            return true;
        }

        set_error_handler($this->includeHandler);
        try {
            $file = $this->files[$id] ?? $this->files[$id] = $this->getFile($id);
            $getExpiry = true;

            if (isset(self::$valuesCache[$file])) {
                [$expiresAt, $value] = self::$valuesCache[$file];
            } elseif (\is_array($expiresAt = include $file)) {
                if ($this->appendOnly) {
                    self::$valuesCache[$file] = $expiresAt;
                }

                [$expiresAt, $value] = $expiresAt;
            } elseif ($this->appendOnly) {
                $value = new LazyValue($file);
            }
        } catch (\ErrorException $e) {
            return false;
        } finally {
            restore_error_handler();
        }
        if ($this->appendOnly) {
            $now = 0;
            $this->values[$id] = $value;
        } else {
            $now = time();
        }

        return $now < $expiresAt;
    }

    /**
     * {@inheritdoc}
     */
    protected function doSave(array $values, int $lifetime)
    {
        $ok = true;
        $expiry = $lifetime ? time() + $lifetime : 'PHP_INT_MAX';
        $allowCompile = self::isSupported();

        foreach ($values as $key => $value) {
            unset($this->values[$key]);
            $isStaticValue = true;
            if (null === $value) {
                $value = "'N;'";
            } elseif (\is_object($value) || \is_array($value)) {
                try {
                    $value = VarExporter::export($value, $isStaticValue);
                } catch (\Exception $e) {
                    throw new InvalidArgumentException(sprintf('Cache key "%s" has non-serializable "%s" value.', $key, get_debug_type($value)), 0, $e);
                }
            } elseif (\is_string($value)) {
                // Wrap "N;" in a closure to not confuse it with an encoded `null`
                if ('N;' === $value) {
                    $isStaticValue = false;
                }
                $value = var_export($value, true);
            } elseif (!\is_scalar($value)) {
                throw new InvalidArgumentException(sprintf('Cache key "%s" has non-serializable "%s" value.', $key, get_debug_type($value)));
            } else {
                $value = var_export($value, true);
            }

            $encodedKey = rawurlencode($key);

            if ($isStaticValue) {
                $value = "return [{$expiry}, {$value}];";
            } elseif ($this->appendOnly) {
                $value = "return [{$expiry}, static function () { return {$value}; }];";
            } else {
                // We cannot use a closure here because of https://bugs.php.net/76982
                $value = str_replace('\Symfony\Component\VarExporter\Internal\\', '', $value);
                $value = "namespace Symfony\Component\VarExporter\Internal;\n\nreturn \$getExpiry ? {$expiry} : {$value};";
            }

            $file = $this->files[$key] = $this->getFile($key, true);
            // Since OPcache only compiles files older than the script execution start, set the file's mtime in the past
            $ok = $this->write($file, "<?php //{$encodedKey}\n\n{$value}\n", self::$startTime - 10) && $ok;

            if ($allowCompile) {
                @opcache_invalidate($file, true);
                @opcache_compile_file($file);
            }
            unset(self::$valuesCache[$file]);
        }

        if (!$ok && !is_writable($this->directory)) {
            throw new CacheException(sprintf('Cache directory is not writable (%s).', $this->directory));
        }

        return $ok;
    }

    /**
     * {@inheritdoc}
     */
    protected function doClear(string $namespace)
    {
        $this->values = [];

        return $this->doCommonClear($namespace);
    }

    /**
     * {@inheritdoc}
     */
    protected function doDelete(array $ids)
    {
        foreach ($ids as $id) {
            unset($this->values[$id]);
        }

        return $this->doCommonDelete($ids);
    }

    protected function doUnlink(string $file)
    {
        unset(self::$valuesCache[$file]);

        if (self::isSupported()) {
            @opcache_invalidate($file, true);
        }

        return @unlink($file);
    }

    private function getFileKey(string $file): string
    {
        if (!$h = @fopen($file, 'r')) {
            return '';
        }

        $encodedKey = substr(fgets($h), 8);
        fclose($h);

        return rawurldecode(rtrim($encodedKey));
    }
}

/**
 * @internal
 */
class LazyValue
{
    public $file;

    public function __construct(string $file)
    {
        $this->file = $file;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Cache\Adapter;

use Symfony\Contracts\Cache\TagAwareCacheInterface;

/**
 * @author Robin Chalas <robin.chalas@gmail.com>
 */
class TraceableTagAwareAdapter extends TraceableAdapter implements TagAwareAdapterInterface, TagAwareCacheInterface
{
    public function __construct(TagAwareAdapterInterface $pool)
    {
        parent::__construct($pool);
    }

    /**
     * {@inheritdoc}
     */
    public function invalidateTags(array $tags)
    {
        $event = $this->start(__FUNCTION__);
        try {
            return $event->result = $this->pool->invalidateTags($tags);
        } finally {
            $event->end = microtime(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\Cache\Adapter;

use Symfony\Component\Cache\Marshaller\MarshallerInterface;
use Symfony\Component\Cache\Traits\RedisClusterProxy;
use Symfony\Component\Cache\Traits\RedisProxy;
use Symfony\Component\Cache\Traits\RedisTrait;

class RedisAdapter extends AbstractAdapter
{
    use RedisTrait;

    /**
     * @param \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|RedisProxy|RedisClusterProxy $redis           The redis client
     * @param string                                                                                $namespace       The default namespace
     * @param int                                                                                   $defaultLifetime The default lifetime
     */
    public function __construct($redis, string $namespace = '', int $defaultLifetime = 0, ?MarshallerInterface $marshaller = null)
    {
        $this->init($redis, $namespace, $defaultLifetime, $marshaller);
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Cache\Adapter;

use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\Cache\ResettableInterface;
use Symfony\Component\Cache\Traits\ContractsTrait;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Service\ResetInterface;

/**
 * Chains several adapters together.
 *
 * Cached items are fetched from the first adapter having them in its data store.
 * They are saved and deleted in all adapters at once.
 *
 * @author Kévin Dunglas <dunglas@gmail.com>
 */
class ChainAdapter implements AdapterInterface, CacheInterface, PruneableInterface, ResettableInterface
{
    use ContractsTrait;

    private $adapters = [];
    private $adapterCount;
    private $defaultLifetime;

    private static $syncItem;

    /**
     * @param CacheItemPoolInterface[] $adapters        The ordered list of adapters used to fetch cached items
     * @param int                      $defaultLifetime The default lifetime of items propagated from lower adapters to upper ones
     */
    public function __construct(array $adapters, int $defaultLifetime = 0)
    {
        if (!$adapters) {
            throw new InvalidArgumentException('At least one adapter must be specified.');
        }

        foreach ($adapters as $adapter) {
            if (!$adapter instanceof CacheItemPoolInterface) {
                throw new InvalidArgumentException(sprintf('The class "%s" does not implement the "%s" interface.', get_debug_type($adapter), CacheItemPoolInterface::class));
            }
            if (\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) && $adapter instanceof ApcuAdapter && !filter_var(\ini_get('apc.enable_cli'), \FILTER_VALIDATE_BOOLEAN)) {
                continue; // skip putting APCu in the chain when the backend is disabled
            }

            if ($adapter instanceof AdapterInterface) {
                $this->adapters[] = $adapter;
            } else {
                $this->adapters[] = new ProxyAdapter($adapter);
            }
        }
        $this->adapterCount = \count($this->adapters);
        $this->defaultLifetime = $defaultLifetime;

        self::$syncItem ?? self::$syncItem = \Closure::bind(
            static function ($sourceItem, $item, $defaultLifetime, $sourceMetadata = null) {
                $sourceItem->isTaggable = false;
                $sourceMetadata = $sourceMetadata ?? $sourceItem->metadata;
                unset($sourceMetadata[CacheItem::METADATA_TAGS]);

                $item->value = $sourceItem->value;
                $item->isHit = $sourceItem->isHit;
                $item->metadata = $item->newMetadata = $sourceItem->metadata = $sourceMetadata;

                if (isset($item->metadata[CacheItem::METADATA_EXPIRY])) {
                    $item->expiresAt(\DateTime::createFromFormat('U.u', sprintf('%.6F', $item->metadata[CacheItem::METADATA_EXPIRY])));
                } elseif (0 < $defaultLifetime) {
                    $item->expiresAfter($defaultLifetime);
                }

                return $item;
            },
            null,
            CacheItem::class
        );
    }

    /**
     * {@inheritdoc}
     */
    public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null)
    {
        $doSave = true;
        $callback = static function (CacheItem $item, bool &$save) use ($callback, &$doSave) {
            $value = $callback($item, $save);
            $doSave = $save;

            return $value;
        };

        $lastItem = null;
        $i = 0;
        $wrap = function (?CacheItem $item = null, bool &$save = true) use ($key, $callback, $beta, &$wrap, &$i, &$doSave, &$lastItem, &$metadata) {
            $adapter = $this->adapters[$i];
            if (isset($this->adapters[++$i])) {
                $callback = $wrap;
                $beta = \INF === $beta ? \INF : 0;
            }
            if ($adapter instanceof CacheInterface) {
                $value = $adapter->get($key, $callback, $beta, $metadata);
            } else {
                $value = $this->doGet($adapter, $key, $callback, $beta, $metadata);
            }
            if (null !== $item) {
                (self::$syncItem)($lastItem = $lastItem ?? $item, $item, $this->defaultLifetime, $metadata);
            }
            $save = $doSave;

            return $value;
        };

        return $wrap();
    }

    /**
     * {@inheritdoc}
     */
    public function getItem($key)
    {
        $syncItem = self::$syncItem;
        $misses = [];

        foreach ($this->adapters as $i => $adapter) {
            $item = $adapter->getItem($key);

            if ($item->isHit()) {
                while (0 <= --$i) {
                    $this->adapters[$i]->save($syncItem($item, $misses[$i], $this->defaultLifetime));
                }

                return $item;
            }

            $misses[$i] = $item;
        }

        return $item;
    }

    /**
     * {@inheritdoc}
     */
    public function getItems(array $keys = [])
    {
        return $this->generateItems($this->adapters[0]->getItems($keys), 0);
    }

    private function generateItems(iterable $items, int $adapterIndex): \Generator
    {
        $missing = [];
        $misses = [];
        $nextAdapterIndex = $adapterIndex + 1;
        $nextAdapter = $this->adapters[$nextAdapterIndex] ?? null;

        foreach ($items as $k => $item) {
            if (!$nextAdapter || $item->isHit()) {
                yield $k => $item;
            } else {
                $missing[] = $k;
                $misses[$k] = $item;
            }
        }

        if ($missing) {
            $syncItem = self::$syncItem;
            $adapter = $this->adapters[$adapterIndex];
            $items = $this->generateItems($nextAdapter->getItems($missing), $nextAdapterIndex);

            foreach ($items as $k => $item) {
                if ($item->isHit()) {
                    $adapter->save($syncItem($item, $misses[$k], $this->defaultLifetime));
                }

                yield $k => $item;
            }
        }
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function hasItem($key)
    {
        foreach ($this->adapters as $adapter) {
            if ($adapter->hasItem($key)) {
                return true;
            }
        }

        return false;
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function clear(string $prefix = '')
    {
        $cleared = true;
        $i = $this->adapterCount;

        while ($i--) {
            if ($this->adapters[$i] instanceof AdapterInterface) {
                $cleared = $this->adapters[$i]->clear($prefix) && $cleared;
            } else {
                $cleared = $this->adapters[$i]->clear() && $cleared;
            }
        }

        return $cleared;
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function deleteItem($key)
    {
        $deleted = true;
        $i = $this->adapterCount;

        while ($i--) {
            $deleted = $this->adapters[$i]->deleteItem($key) && $deleted;
        }

        return $deleted;
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function deleteItems(array $keys)
    {
        $deleted = true;
        $i = $this->adapterCount;

        while ($i--) {
            $deleted = $this->adapters[$i]->deleteItems($keys) && $deleted;
        }

        return $deleted;
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function save(CacheItemInterface $item)
    {
        $saved = true;
        $i = $this->adapterCount;

        while ($i--) {
            $saved = $this->adapters[$i]->save($item) && $saved;
        }

        return $saved;
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function saveDeferred(CacheItemInterface $item)
    {
        $saved = true;
        $i = $this->adapterCount;

        while ($i--) {
            $saved = $this->adapters[$i]->saveDeferred($item) && $saved;
        }

        return $saved;
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function commit()
    {
        $committed = true;
        $i = $this->adapterCount;

        while ($i--) {
            $committed = $this->adapters[$i]->commit() && $committed;
        }

        return $committed;
    }

    /**
     * {@inheritdoc}
     */
    public function prune()
    {
        $pruned = true;

        foreach ($this->adapters as $adapter) {
            if ($adapter instanceof PruneableInterface) {
                $pruned = $adapter->prune() && $pruned;
            }
        }

        return $pruned;
    }

    /**
     * {@inheritdoc}
     */
    public function reset()
    {
        foreach ($this->adapters as $adapter) {
            if ($adapter instanceof ResetInterface) {
                $adapter->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\Component\Cache;

/**
 * Interface extends psr-6 and psr-16 caches to allow for pruning (deletion) of all expired cache items.
 */
interface PruneableInterface
{
    /**
     * @return bool
     */
    public function prune();
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Cache;

use Doctrine\Common\Cache\CacheProvider;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Contracts\Service\ResetInterface;

if (!class_exists(CacheProvider::class)) {
    return;
}

/**
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @deprecated Use Doctrine\Common\Cache\Psr6\DoctrineProvider instead
 */
class DoctrineProvider extends CacheProvider implements PruneableInterface, ResettableInterface
{
    private $pool;

    public function __construct(CacheItemPoolInterface $pool)
    {
        trigger_deprecation('symfony/cache', '5.4', '"%s" is deprecated, use "Doctrine\Common\Cache\Psr6\DoctrineProvider" instead.', __CLASS__);

        $this->pool = $pool;
    }

    /**
     * {@inheritdoc}
     */
    public function prune()
    {
        return $this->pool instanceof PruneableInterface && $this->pool->prune();
    }

    /**
     * {@inheritdoc}
     */
    public function reset()
    {
        if ($this->pool instanceof ResetInterface) {
            $this->pool->reset();
        }
        $this->setNamespace($this->getNamespace());
    }

    /**
     * {@inheritdoc}
     *
     * @return mixed
     */
    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

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Cache\Exception;

use Psr\Cache\CacheException as Psr6CacheInterface;
use Psr\SimpleCache\CacheException as SimpleCacheInterface;

if (interface_exists(SimpleCacheInterface::class)) {
    class CacheException extends \Exception implements Psr6CacheInterface, SimpleCacheInterface
    {
    }
} else {
    class CacheException extends \Exception implements Psr6CacheInterface
    {
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Cache\Exception;

use Psr\Cache\InvalidArgumentException as Psr6CacheInterface;
use Psr\SimpleCache\InvalidArgumentException as SimpleCacheInterface;

if (interface_exists(SimpleCacheInterface::class)) {
    class InvalidArgumentException extends \InvalidArgumentException implements Psr6CacheInterface, SimpleCacheInterface
    {
    }
} else {
    class InvalidArgumentException extends \InvalidArgumentException implements Psr6CacheInterface
    {
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Cache\Exception;

use Psr\Cache\CacheException as Psr6CacheInterface;
use Psr\SimpleCache\CacheException as SimpleCacheInterface;

if (interface_exists(SimpleCacheInterface::class)) {
    class LogicException extends \LogicException implements Psr6CacheInterface, SimpleCacheInterface
    {
    }
} else {
    class LogicException extends \LogicException implements Psr6CacheInterface
    {
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Cache;

use Symfony\Contracts\Service\ResetInterface;

/**
 * Resets a pool's local state.
 */
interface ResettableInterface extends ResetInterface
{
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Cache\Messenger;

use Symfony\Component\Cache\Adapter\AdapterInterface;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\DependencyInjection\ReverseContainer;

/**
 * Conveys a cached value that needs to be computed.
 */
final class EarlyExpirationMessage
{
    private $item;
    private $pool;
    private $callback;

    public static function create(ReverseContainer $reverseContainer, callable $callback, CacheItem $item, AdapterInterface $pool): ?self
    {
        try {
            $item = clone $item;
            $item->set(null);
        } catch (\Exception $e) {
            return null;
        }

        $pool = $reverseContainer->getId($pool);

        if (\is_object($callback)) {
            if (null === $id = $reverseContainer->getId($callback)) {
                return null;
            }

            $callback = '@'.$id;
        } elseif (!\is_array($callback)) {
            $callback = (string) $callback;
        } elseif (!\is_object($callback[0])) {
            $callback = [(string) $callback[0], (string) $callback[1]];
        } else {
            if (null === $id = $reverseContainer->getId($callback[0])) {
                return null;
            }

            $callback = ['@'.$id, (string) $callback[1]];
        }

        return new self($item, $pool, $callback);
    }

    public function getItem(): CacheItem
    {
        return $this->item;
    }

    public function getPool(): string
    {
        return $this->pool;
    }

    public function getCallback()
    {
        return $this->callback;
    }

    public function findPool(ReverseContainer $reverseContainer): AdapterInterface
    {
        return $reverseContainer->getService($this->pool);
    }

    public function findCallback(ReverseContainer $reverseContainer): callable
    {
        if (\is_string($callback = $this->callback)) {
            return '@' === $callback[0] ? $reverseContainer->getService(substr($callback, 1)) : $callback;
        }
        if ('@' === $callback[0][0]) {
            $callback[0] = $reverseContainer->getService(substr($callback[0], 1));
        }

        return $callback;
    }

    private function __construct(CacheItem $item, string $pool, $callback)
    {
        $this->item = $item;
        $this->pool = $pool;
        $this->callback = $callback;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Cache\Messenger;

use Symfony\Component\Cache\CacheItem;
use Symfony\Component\DependencyInjection\ReverseContainer;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;

/**
 * Computes cached values sent to a message bus.
 */
class EarlyExpirationHandler implements MessageHandlerInterface
{
    private $reverseContainer;
    private $processedNonces = [];

    public function __construct(ReverseContainer $reverseContainer)
    {
        $this->reverseContainer = $reverseContainer;
    }

    public function __invoke(EarlyExpirationMessage $message)
    {
        $item = $message->getItem();
        $metadata = $item->getMetadata();
        $expiry = $metadata[CacheItem::METADATA_EXPIRY] ?? 0;
        $ctime = $metadata[CacheItem::METADATA_CTIME] ?? 0;

        if ($expiry && $ctime) {
            // skip duplicate or expired messages

            $processingNonce = [$expiry, $ctime];
            $pool = $message->getPool();
            $key = $item->getKey();

            if (($this->processedNonces[$pool][$key] ?? null) === $processingNonce) {
                return;
            }

            if (microtime(true) >= $expiry) {
                return;
            }

            $this->processedNonces[$pool] = [$key => $processingNonce] + ($this->processedNonces[$pool] ?? []);

            if (\count($this->processedNonces[$pool]) > 100) {
                array_pop($this->processedNonces[$pool]);
            }
        }

        static $setMetadata;

        $setMetadata ?? $setMetadata = \Closure::bind(
            function (CacheItem $item, float $startTime) {
                if ($item->expiry > $endTime = microtime(true)) {
                    $item->newMetadata[CacheItem::METADATA_EXPIRY] = $item->expiry;
                    $item->newMetadata[CacheItem::METADATA_CTIME] = (int) ceil(1000 * ($endTime - $startTime));
                }
            },
            null,
            CacheItem::class
        );

        $startTime = microtime(true);
        $pool = $message->findPool($this->reverseContainer);
        $callback = $message->findCallback($this->reverseContainer);
        $save = true;
        $value = $callback($item, $save);
        $setMetadata($item, $startTime);
        $pool->save($item->set($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\Cache\Messenger;

use Psr\Log\LoggerInterface;
use Symfony\Component\Cache\Adapter\AdapterInterface;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\DependencyInjection\ReverseContainer;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Messenger\Stamp\HandledStamp;

/**
 * Sends the computation of cached values to a message bus.
 */
class EarlyExpirationDispatcher
{
    private $bus;
    private $reverseContainer;
    private $callbackWrapper;

    public function __construct(MessageBusInterface $bus, ReverseContainer $reverseContainer, ?callable $callbackWrapper = null)
    {
        $this->bus = $bus;
        $this->reverseContainer = $reverseContainer;
        $this->callbackWrapper = $callbackWrapper;
    }

    public function __invoke(callable $callback, CacheItem $item, bool &$save, AdapterInterface $pool, \Closure $setMetadata, ?LoggerInterface $logger = null)
    {
        if (!$item->isHit() || null === $message = EarlyExpirationMessage::create($this->reverseContainer, $callback, $item, $pool)) {
            // The item is stale or the callback cannot be reversed: we must compute the value now
            $logger && $logger->info('Computing item "{key}" online: '.($item->isHit() ? 'callback cannot be reversed' : 'item is stale'), ['key' => $item->getKey()]);

            return null !== $this->callbackWrapper ? ($this->callbackWrapper)($callback, $item, $save, $pool, $setMetadata, $logger) : $callback($item, $save);
        }

        $envelope = $this->bus->dispatch($message);

        if ($logger) {
            if ($envelope->last(HandledStamp::class)) {
                $logger->info('Item "{key}" was computed online', ['key' => $item->getKey()]);
            } else {
                $logger->info('Item "{key}" sent for recomputation', ['key' => $item->getKey()]);
            }
        }

        // The item's value is not stale, no need to write it to the backend
        $save = false;

        return $message->getItem()->get() ?? $item->get();
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Cache\DependencyInjection;

use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Reference;

/**
 * @author Rob Frawley 2nd <rmf@src.run>
 */
class CachePoolPrunerPass implements CompilerPassInterface
{
    private $cacheCommandServiceId;
    private $cachePoolTag;

    public function __construct(string $cacheCommandServiceId = 'console.command.cache_pool_prune', string $cachePoolTag = 'cache.pool')
    {
        if (0 < \func_num_args()) {
            trigger_deprecation('symfony/cache', '5.3', 'Configuring "%s" is deprecated.', __CLASS__);
        }

        $this->cacheCommandServiceId = $cacheCommandServiceId;
        $this->cachePoolTag = $cachePoolTag;
    }

    /**
     * {@inheritdoc}
     */
    public function process(ContainerBuilder $container)
    {
        if (!$container->hasDefinition($this->cacheCommandServiceId)) {
            return;
        }

        $services = [];

        foreach ($container->findTaggedServiceIds($this->cachePoolTag) as $id => $tags) {
            $class = $container->getParameterBag()->resolveValue($container->getDefinition($id)->getClass());

            if (!$reflection = $container->getReflectionClass($class)) {
                throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id));
            }

            if ($reflection->implementsInterface(PruneableInterface::class)) {
                $services[$id] = new Reference($id);
            }
        }

        $container->getDefinition($this->cacheCommandServiceId)->replaceArgument(0, new IteratorArgument($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\Cache\DependencyInjection;

use Symfony\Component\Cache\Adapter\TagAwareAdapterInterface;
use Symfony\Component\Cache\Adapter\TraceableAdapter;
use Symfony\Component\Cache\Adapter\TraceableTagAwareAdapter;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;

/**
 * Inject a data collector to all the cache services to be able to get detailed statistics.
 *
 * @author Tobias Nyholm <tobias.nyholm@gmail.com>
 */
class CacheCollectorPass implements CompilerPassInterface
{
    private $dataCollectorCacheId;
    private $cachePoolTag;
    private $cachePoolRecorderInnerSuffix;

    public function __construct(string $dataCollectorCacheId = 'data_collector.cache', string $cachePoolTag = 'cache.pool', string $cachePoolRecorderInnerSuffix = '.recorder_inner')
    {
        if (0 < \func_num_args()) {
            trigger_deprecation('symfony/cache', '5.3', 'Configuring "%s" is deprecated.', __CLASS__);
        }

        $this->dataCollectorCacheId = $dataCollectorCacheId;
        $this->cachePoolTag = $cachePoolTag;
        $this->cachePoolRecorderInnerSuffix = $cachePoolRecorderInnerSuffix;
    }

    /**
     * {@inheritdoc}
     */
    public function process(ContainerBuilder $container)
    {
        if (!$container->hasDefinition($this->dataCollectorCacheId)) {
            return;
        }

        foreach ($container->findTaggedServiceIds($this->cachePoolTag) as $id => $attributes) {
            $poolName = $attributes[0]['name'] ?? $id;

            $this->addToCollector($id, $poolName, $container);
        }
    }

    private function addToCollector(string $id, string $name, ContainerBuilder $container)
    {
        $definition = $container->getDefinition($id);
        if ($definition->isAbstract()) {
            return;
        }

        $collectorDefinition = $container->getDefinition($this->dataCollectorCacheId);
        $recorder = new Definition(is_subclass_of($definition->getClass(), TagAwareAdapterInterface::class) ? TraceableTagAwareAdapter::class : TraceableAdapter::class);
        $recorder->setTags($definition->getTags());
        if (!$definition->isPublic() || !$definition->isPrivate()) {
            $recorder->setPublic($definition->isPublic());
        }
        $recorder->setArguments([new Reference($innerId = $id.$this->cachePoolRecorderInnerSuffix)]);

        foreach ($definition->getMethodCalls() as [$method, $args]) {
            if ('setCallbackWrapper' !== $method || !$args[0] instanceof Definition || !($args[0]->getArguments()[2] ?? null) instanceof Definition) {
                continue;
            }
            if ([new Reference($id), 'setCallbackWrapper'] == $args[0]->getArguments()[2]->getFactory()) {
                $args[0]->getArguments()[2]->setFactory([new Reference($innerId), 'setCallbackWrapper']);
            }
        }

        $definition->setTags([]);
        $definition->setPublic(false);

        $container->setDefinition($innerId, $definition);
        $container->setDefinition($id, $recorder);

        // Tell the collector to add the new instance
        $collectorDefinition->addMethodCall('addInstance', [$name, new Reference($id)]);
        $collectorDefinition->setPublic(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\Cache\DependencyInjection;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 */
class CachePoolClearerPass implements CompilerPassInterface
{
    private $cachePoolClearerTag;

    public function __construct(string $cachePoolClearerTag = 'cache.pool.clearer')
    {
        if (0 < \func_num_args()) {
            trigger_deprecation('symfony/cache', '5.3', 'Configuring "%s" is deprecated.', __CLASS__);
        }

        $this->cachePoolClearerTag = $cachePoolClearerTag;
    }

    /**
     * {@inheritdoc}
     */
    public function process(ContainerBuilder $container)
    {
        $container->getParameterBag()->remove('cache.prefix.seed');

        foreach ($container->findTaggedServiceIds($this->cachePoolClearerTag) as $id => $attr) {
            $clearer = $container->getDefinition($id);
            $pools = [];
            foreach ($clearer->getArgument(0) as $name => $ref) {
                if ($container->hasDefinition($ref)) {
                    $pools[$name] = new Reference($ref);
                }
            }
            $clearer->replaceArgument(0, $pools);
        }
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Cache\DependencyInjection;

use Symfony\Component\Cache\Adapter\AbstractAdapter;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\Cache\Adapter\ChainAdapter;
use Symfony\Component\Cache\Adapter\NullAdapter;
use Symfony\Component\Cache\Adapter\ParameterNormalizer;
use Symfony\Component\Cache\Messenger\EarlyExpirationDispatcher;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Reference;

/**
 * @author Nicolas Grekas <p@tchwork.com>
 */
class CachePoolPass implements CompilerPassInterface
{
    private $cachePoolTag;
    private $kernelResetTag;
    private $cacheClearerId;
    private $cachePoolClearerTag;
    private $cacheSystemClearerId;
    private $cacheSystemClearerTag;
    private $reverseContainerId;
    private $reversibleTag;
    private $messageHandlerId;

    public function __construct(string $cachePoolTag = 'cache.pool', string $kernelResetTag = 'kernel.reset', string $cacheClearerId = 'cache.global_clearer', string $cachePoolClearerTag = 'cache.pool.clearer', string $cacheSystemClearerId = 'cache.system_clearer', string $cacheSystemClearerTag = 'kernel.cache_clearer', string $reverseContainerId = 'reverse_container', string $reversibleTag = 'container.reversible', string $messageHandlerId = 'cache.early_expiration_handler')
    {
        if (0 < \func_num_args()) {
            trigger_deprecation('symfony/cache', '5.3', 'Configuring "%s" is deprecated.', __CLASS__);
        }

        $this->cachePoolTag = $cachePoolTag;
        $this->kernelResetTag = $kernelResetTag;
        $this->cacheClearerId = $cacheClearerId;
        $this->cachePoolClearerTag = $cachePoolClearerTag;
        $this->cacheSystemClearerId = $cacheSystemClearerId;
        $this->cacheSystemClearerTag = $cacheSystemClearerTag;
        $this->reverseContainerId = $reverseContainerId;
        $this->reversibleTag = $reversibleTag;
        $this->messageHandlerId = $messageHandlerId;
    }

    /**
     * {@inheritdoc}
     */
    public function process(ContainerBuilder $container)
    {
        if ($container->hasParameter('cache.prefix.seed')) {
            $seed = $container->getParameterBag()->resolveValue($container->getParameter('cache.prefix.seed'));
        } else {
            $seed = '_'.$container->getParameter('kernel.project_dir');
            $seed .= '.'.$container->getParameter('kernel.container_class');
        }

        $needsMessageHandler = false;
        $allPools = [];
        $clearers = [];
        $attributes = [
            'provider',
            'name',
            'namespace',
            'default_lifetime',
            'early_expiration_message_bus',
            'reset',
        ];
        foreach ($container->findTaggedServiceIds($this->cachePoolTag) as $id => $tags) {
            $adapter = $pool = $container->getDefinition($id);
            if ($pool->isAbstract()) {
                continue;
            }
            $class = $adapter->getClass();
            while ($adapter instanceof ChildDefinition) {
                $adapter = $container->findDefinition($adapter->getParent());
                $class = $class ?: $adapter->getClass();
                if ($t = $adapter->getTag($this->cachePoolTag)) {
                    $tags[0] += $t[0];
                }
            }
            $name = $tags[0]['name'] ?? $id;
            if (!isset($tags[0]['namespace'])) {
                $namespaceSeed = $seed;
                if (null !== $class) {
                    $namespaceSeed .= '.'.$class;
                }

                $tags[0]['namespace'] = $this->getNamespace($namespaceSeed, $name);
            }
            if (isset($tags[0]['clearer'])) {
                $clearer = $tags[0]['clearer'];
                while ($container->hasAlias($clearer)) {
                    $clearer = (string) $container->getAlias($clearer);
                }
            } else {
                $clearer = null;
            }
            unset($tags[0]['clearer'], $tags[0]['name']);

            if (isset($tags[0]['provider'])) {
                $tags[0]['provider'] = new Reference(static::getServiceProvider($container, $tags[0]['provider']));
            }

            if (ChainAdapter::class === $class) {
                $adapters = [];
                foreach ($adapter->getArgument(0) as $provider => $adapter) {
                    if ($adapter instanceof ChildDefinition) {
                        $chainedPool = $adapter;
                    } else {
                        $chainedPool = $adapter = new ChildDefinition($adapter);
                    }

                    $chainedTags = [\is_int($provider) ? [] : ['provider' => $provider]];
                    $chainedClass = '';

                    while ($adapter instanceof ChildDefinition) {
                        $adapter = $container->findDefinition($adapter->getParent());
                        $chainedClass = $chainedClass ?: $adapter->getClass();
                        if ($t = $adapter->getTag($this->cachePoolTag)) {
                            $chainedTags[0] += $t[0];
                        }
                    }

                    if (ChainAdapter::class === $chainedClass) {
                        throw new InvalidArgumentException(sprintf('Invalid service "%s": chain of adapters cannot reference another chain, found "%s".', $id, $chainedPool->getParent()));
                    }

                    $i = 0;

                    if (isset($chainedTags[0]['provider'])) {
                        $chainedPool->replaceArgument($i++, new Reference(static::getServiceProvider($container, $chainedTags[0]['provider'])));
                    }

                    if (isset($tags[0]['namespace']) && !\in_array($adapter->getClass(), [ArrayAdapter::class, NullAdapter::class], true)) {
                        $chainedPool->replaceArgument($i++, $tags[0]['namespace']);
                    }

                    if (isset($tags[0]['default_lifetime'])) {
                        $chainedPool->replaceArgument($i++, $tags[0]['default_lifetime']);
                    }

                    $adapters[] = $chainedPool;
                }

                $pool->replaceArgument(0, $adapters);
                unset($tags[0]['provider'], $tags[0]['namespace']);
                $i = 1;
            } else {
                $i = 0;
            }

            foreach ($attributes as $attr) {
                if (!isset($tags[0][$attr])) {
                    // no-op
                } elseif ('reset' === $attr) {
                    if ($tags[0][$attr]) {
                        $pool->addTag($this->kernelResetTag, ['method' => $tags[0][$attr]]);
                    }
                } elseif ('early_expiration_message_bus' === $attr) {
                    $needsMessageHandler = true;
                    $pool->addMethodCall('setCallbackWrapper', [(new Definition(EarlyExpirationDispatcher::class))
                        ->addArgument(new Reference($tags[0]['early_expiration_message_bus']))
                        ->addArgument(new Reference($this->reverseContainerId))
                        ->addArgument((new Definition('callable'))
                            ->setFactory([new Reference($id), 'setCallbackWrapper'])
                            ->addArgument(null)
                        ),
                    ]);
                    $pool->addTag($this->reversibleTag);
                } elseif ('namespace' !== $attr || !\in_array($class, [ArrayAdapter::class, NullAdapter::class], true)) {
                    $argument = $tags[0][$attr];

                    if ('default_lifetime' === $attr && !is_numeric($argument)) {
                        $argument = (new Definition('int', [$argument]))
                            ->setFactory([ParameterNormalizer::class, 'normalizeDuration']);
                    }

                    $pool->replaceArgument($i++, $argument);
                }
                unset($tags[0][$attr]);
            }
            if (!empty($tags[0])) {
                throw new InvalidArgumentException(sprintf('Invalid "%s" tag for service "%s": accepted attributes are "clearer", "provider", "name", "namespace", "default_lifetime", "early_expiration_message_bus" and "reset", found "%s".', $this->cachePoolTag, $id, implode('", "', array_keys($tags[0]))));
            }

            if (null !== $clearer) {
                $clearers[$clearer][$name] = new Reference($id, $container::IGNORE_ON_UNINITIALIZED_REFERENCE);
            }

            $allPools[$name] = new Reference($id, $container::IGNORE_ON_UNINITIALIZED_REFERENCE);
        }

        if (!$needsMessageHandler) {
            $container->removeDefinition($this->messageHandlerId);
        }

        $notAliasedCacheClearerId = $this->cacheClearerId;
        while ($container->hasAlias($notAliasedCacheClearerId)) {
            $notAliasedCacheClearerId = (string) $container->getAlias($notAliasedCacheClearerId);
        }
        if ($container->hasDefinition($notAliasedCacheClearerId)) {
            $clearers[$notAliasedCacheClearerId] = $allPools;
        }

        foreach ($clearers as $id => $pools) {
            $clearer = $container->getDefinition($id);
            if ($clearer instanceof ChildDefinition) {
                $clearer->replaceArgument(0, $pools);
            } else {
                $clearer->setArgument(0, $pools);
            }
            $clearer->addTag($this->cachePoolClearerTag);

            if ($this->cacheSystemClearerId === $id) {
                $clearer->addTag($this->cacheSystemClearerTag);
            }
        }

        $allPoolsKeys = array_keys($allPools);

        if ($container->hasDefinition('console.command.cache_pool_list')) {
            $container->getDefinition('console.command.cache_pool_list')->replaceArgument(0, $allPoolsKeys);
        }

        if ($container->hasDefinition('console.command.cache_pool_clear')) {
            $container->getDefinition('console.command.cache_pool_clear')->addArgument($allPoolsKeys);
        }

        if ($container->hasDefinition('console.command.cache_pool_delete')) {
            $container->getDefinition('console.command.cache_pool_delete')->addArgument($allPoolsKeys);
        }
    }

    private function getNamespace(string $seed, string $id)
    {
        return substr(str_replace('/', '-', base64_encode(hash('sha256', $id.$seed, true))), 0, 10);
    }

    /**
     * @internal
     */
    public static function getServiceProvider(ContainerBuilder $container, string $name)
    {
        $container->resolveEnvPlaceholders($name, null, $usedEnvs);

        if ($usedEnvs || preg_match('#^[a-z]++:#', $name)) {
            $dsn = $name;

            if (!$container->hasDefinition($name = '.cache_connection.'.ContainerBuilder::hash($dsn))) {
                $definition = new Definition(AbstractAdapter::class);
                $definition->setPublic(false);
                $definition->setFactory([AbstractAdapter::class, 'createConnection']);
                $definition->setArguments([$dsn, ['lazy' => true]]);
                $container->setDefinition($name, $definition);
            }
        }

        return $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\Cache;

use Psr\Cache\CacheException as Psr6CacheException;
use Psr\Cache\CacheItemPoolInterface;
use Psr\SimpleCache\CacheException as SimpleCacheException;
use Psr\SimpleCache\CacheInterface;
use Symfony\Component\Cache\Adapter\AdapterInterface;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\Traits\ProxyTrait;

if (null !== (new \ReflectionMethod(CacheInterface::class, 'get'))->getReturnType()) {
    throw new \LogicException('psr/simple-cache 3.0+ is not compatible with this version of symfony/cache. Please upgrade symfony/cache to 6.0+ or downgrade psr/simple-cache to 1.x or 2.x.');
}

/**
 * Turns a PSR-6 cache into a PSR-16 one.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
class Psr16Cache implements CacheInterface, PruneableInterface, ResettableInterface
{
    use ProxyTrait;

    private const METADATA_EXPIRY_OFFSET = 1527506807;

    private $createCacheItem;
    private $cacheItemPrototype;

    public function __construct(CacheItemPoolInterface $pool)
    {
        $this->pool = $pool;

        if (!$pool instanceof AdapterInterface) {
            return;
        }
        $cacheItemPrototype = &$this->cacheItemPrototype;
        $createCacheItem = \Closure::bind(
            static function ($key, $value, $allowInt = false) use (&$cacheItemPrototype) {
                $item = clone $cacheItemPrototype;
                $item->poolHash = $item->innerItem = null;
                if ($allowInt && \is_int($key)) {
                    $item->key = (string) $key;
                } else {
                    \assert('' !== CacheItem::validateKey($key));
                    $item->key = $key;
                }
                $item->value = $value;
                $item->isHit = false;

                return $item;
            },
            null,
            CacheItem::class
        );
        $this->createCacheItem = function ($key, $value, $allowInt = false) use ($createCacheItem) {
            if (null === $this->cacheItemPrototype) {
                $this->get($allowInt && \is_int($key) ? (string) $key : $key);
            }
            $this->createCacheItem = $createCacheItem;

            return $createCacheItem($key, null, $allowInt)->set($value);
        };
    }

    /**
     * {@inheritdoc}
     *
     * @return mixed
     */
    public function get($key, $default = null)
    {
        try {
            $item = $this->pool->getItem($key);
        } catch (SimpleCacheException $e) {
            throw $e;
        } catch (Psr6CacheException $e) {
            throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
        }
        if (null === $this->cacheItemPrototype) {
            $this->cacheItemPrototype = clone $item;
            $this->cacheItemPrototype->set(null);
        }

        return $item->isHit() ? $item->get() : $default;
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function set($key, $value, $ttl = null)
    {
        try {
            if (null !== $f = $this->createCacheItem) {
                $item = $f($key, $value);
            } else {
                $item = $this->pool->getItem($key)->set($value);
            }
        } catch (SimpleCacheException $e) {
            throw $e;
        } catch (Psr6CacheException $e) {
            throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
        }
        if (null !== $ttl) {
            $item->expiresAfter($ttl);
        }

        return $this->pool->save($item);
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function delete($key)
    {
        try {
            return $this->pool->deleteItem($key);
        } catch (SimpleCacheException $e) {
            throw $e;
        } catch (Psr6CacheException $e) {
            throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
        }
    }

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

    /**
     * {@inheritdoc}
     *
     * @return iterable
     */
    public function getMultiple($keys, $default = null)
    {
        if ($keys instanceof \Traversable) {
            $keys = iterator_to_array($keys, false);
        } elseif (!\is_array($keys)) {
            throw new InvalidArgumentException(sprintf('Cache keys must be array or Traversable, "%s" given.', get_debug_type($keys)));
        }

        try {
            $items = $this->pool->getItems($keys);
        } catch (SimpleCacheException $e) {
            throw $e;
        } catch (Psr6CacheException $e) {
            throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
        }
        $values = [];

        if (!$this->pool instanceof AdapterInterface) {
            foreach ($items as $key => $item) {
                $values[$key] = $item->isHit() ? $item->get() : $default;
            }

            return $values;
        }

        foreach ($items as $key => $item) {
            if (!$item->isHit()) {
                $values[$key] = $default;
                continue;
            }
            $values[$key] = $item->get();

            if (!$metadata = $item->getMetadata()) {
                continue;
            }
            unset($metadata[CacheItem::METADATA_TAGS]);

            if ($metadata) {
                $values[$key] = ["\x9D".pack('VN', (int) (0.1 + $metadata[CacheItem::METADATA_EXPIRY] - self::METADATA_EXPIRY_OFFSET), $metadata[CacheItem::METADATA_CTIME])."\x5F" => $values[$key]];
            }
        }

        return $values;
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function setMultiple($values, $ttl = null)
    {
        $valuesIsArray = \is_array($values);
        if (!$valuesIsArray && !$values instanceof \Traversable) {
            throw new InvalidArgumentException(sprintf('Cache values must be array or Traversable, "%s" given.', get_debug_type($values)));
        }
        $items = [];

        try {
            if (null !== $f = $this->createCacheItem) {
                $valuesIsArray = false;
                foreach ($values as $key => $value) {
                    $items[$key] = $f($key, $value, true);
                }
            } elseif ($valuesIsArray) {
                $items = [];
                foreach ($values as $key => $value) {
                    $items[] = (string) $key;
                }
                $items = $this->pool->getItems($items);
            } else {
                foreach ($values as $key => $value) {
                    if (\is_int($key)) {
                        $key = (string) $key;
                    }
                    $items[$key] = $this->pool->getItem($key)->set($value);
                }
            }
        } catch (SimpleCacheException $e) {
            throw $e;
        } catch (Psr6CacheException $e) {
            throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
        }
        $ok = true;

        foreach ($items as $key => $item) {
            if ($valuesIsArray) {
                $item->set($values[$key]);
            }
            if (null !== $ttl) {
                $item->expiresAfter($ttl);
            }
            $ok = $this->pool->saveDeferred($item) && $ok;
        }

        return $this->pool->commit() && $ok;
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function deleteMultiple($keys)
    {
        if ($keys instanceof \Traversable) {
            $keys = iterator_to_array($keys, false);
        } elseif (!\is_array($keys)) {
            throw new InvalidArgumentException(sprintf('Cache keys must be array or Traversable, "%s" given.', get_debug_type($keys)));
        }

        try {
            return $this->pool->deleteItems($keys);
        } catch (SimpleCacheException $e) {
            throw $e;
        } catch (Psr6CacheException $e) {
            throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
        }
    }

    /**
     * {@inheritdoc}
     *
     * @return bool
     */
    public function has($key)
    {
        try {
            return $this->pool->hasItem($key);
        } catch (SimpleCacheException $e) {
            throw $e;
        } catch (Psr6CacheException $e) {
            throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $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\Cache\Marshaller;

use Symfony\Component\Cache\Exception\CacheException;
use Symfony\Component\Cache\Exception\InvalidArgumentException;

/**
 * Encrypt/decrypt values using Libsodium.
 *
 * @author Ahmed TAILOULOUTE <ahmed.tailouloute@gmail.com>
 */
class SodiumMarshaller implements MarshallerInterface
{
    private $marshaller;
    private $decryptionKeys;

    /**
     * @param string[] $decryptionKeys The key at index "0" is required and is used to decrypt and encrypt values;
     *                                 more rotating keys can be provided to decrypt values;
     *                                 each key must be generated using sodium_crypto_box_keypair()
     */
    public function __construct(array $decryptionKeys, ?MarshallerInterface $marshaller = null)
    {
        if (!self::isSupported()) {
            throw new CacheException('The "sodium" PHP extension is not loaded.');
        }

        if (!isset($decryptionKeys[0])) {
            throw new InvalidArgumentException('At least one decryption key must be provided at index "0".');
        }

        $this->marshaller = $marshaller ?? new DefaultMarshaller();
        $this->decryptionKeys = $decryptionKeys;
    }

    public static function isSupported(): bool
    {
        return \function_exists('sodium_crypto_box_seal');
    }

    /**
     * {@inheritdoc}
     */
    public function marshall(array $values, ?array &$failed): array
    {
        $encryptionKey = sodium_crypto_box_publickey($this->decryptionKeys[0]);

        $encryptedValues = [];
        foreach ($this->marshaller->marshall($values, $failed) as $k => $v) {
            $encryptedValues[$k] = sodium_crypto_box_seal($v, $encryptionKey);
        }

        return $encryptedValues;
    }

    /**
     * {@inheritdoc}
     */
    public function unmarshall(string $value)
    {
        foreach ($this->decryptionKeys as $k) {
            if (false !== $decryptedValue = @sodium_crypto_box_seal_open($value, $k)) {
                $value = $decryptedValue;
                break;
            }
        }

        return $this->marshaller->unmarshall($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\Cache\Marshaller;

/**
 * Serializes/unserializes PHP values.
 *
 * Implementations of this interface MUST deal with errors carefully. They MUST
 * also deal with forward and backward compatibility at the storage format level.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
interface MarshallerInterface
{
    /**
     * Serializes a list of values.
     *
     * When serialization fails for a specific value, no exception should be
     * thrown. Instead, its key should be listed in $failed.
     */
    public function marshall(array $values, ?array &$failed): array;

    /**
     * Unserializes a single value and throws an exception if anything goes wrong.
     *
     * @return mixed
     *
     * @throws \Exception Whenever unserialization fails
     */
    public function unmarshall(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\Cache\Marshaller;

/**
 * A marshaller optimized for data structures generated by AbstractTagAwareAdapter.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
class TagAwareMarshaller implements MarshallerInterface
{
    private $marshaller;

    public function __construct(?MarshallerInterface $marshaller = null)
    {
        $this->marshaller = $marshaller ?? new DefaultMarshaller();
    }

    /**
     * {@inheritdoc}
     */
    public function marshall(array $values, ?array &$failed): array
    {
        $failed = $notSerialized = $serialized = [];

        foreach ($values as $id => $value) {
            if (\is_array($value) && \is_array($value['tags'] ?? null) && \array_key_exists('value', $value) && \count($value) === 2 + (\is_string($value['meta'] ?? null) && 8 === \strlen($value['meta']))) {
                // if the value is an array with keys "tags", "value" and "meta", use a compact serialization format
                // magic numbers in the form 9D-..-..-..-..-00-..-..-..-5F allow detecting this format quickly in unmarshall()

                $v = $this->marshaller->marshall($value, $f);

                if ($f) {
                    $f = [];
                    $failed[] = $id;
                } else {
                    if ([] === $value['tags']) {
                        $v['tags'] = '';
                    }

                    $serialized[$id] = "\x9D".($value['meta'] ?? "\0\0\0\0\0\0\0\0").pack('N', \strlen($v['tags'])).$v['tags'].$v['value'];
                    $serialized[$id][9] = "\x5F";
                }
            } else {
                // other arbitrary values are serialized using the decorated marshaller below
                $notSerialized[$id] = $value;
            }
        }

        if ($notSerialized) {
            $serialized += $this->marshaller->marshall($notSerialized, $f);
            $failed = array_merge($failed, $f);
        }

        return $serialized;
    }

    /**
     * {@inheritdoc}
     */
    public function unmarshall(string $value)
    {
        // detect the compact format used in marshall() using magic numbers in the form 9D-..-..-..-..-00-..-..-..-5F
        if (13 >= \strlen($value) || "\x9D" !== $value[0] || "\0" !== $value[5] || "\x5F" !== $value[9]) {
            return $this->marshaller->unmarshall($value);
        }

        // data consists of value, tags and metadata which we need to unpack
        $meta = substr($value, 1, 12);
        $meta[8] = "\0";
        $tagLen = unpack('Nlen', $meta, 8)['len'];
        $meta = substr($meta, 0, 8);

        return [
            'value' => $this->marshaller->unmarshall(substr($value, 13 + $tagLen)),
            'tags' => $tagLen ? $this->marshaller->unmarshall(substr($value, 13, $tagLen)) : [],
            'meta' => "\0\0\0\0\0\0\0\0" === $meta ? null : $meta,
        ];
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Cache\Marshaller;

use Symfony\Component\Cache\Exception\CacheException;

/**
 * Serializes/unserializes values using igbinary_serialize() if available, serialize() otherwise.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
class DefaultMarshaller implements MarshallerInterface
{
    private $useIgbinarySerialize = true;
    private $throwOnSerializationFailure;

    public function __construct(?bool $useIgbinarySerialize = null, bool $throwOnSerializationFailure = false)
    {
        if (null === $useIgbinarySerialize) {
            $useIgbinarySerialize = \extension_loaded('igbinary') && (\PHP_VERSION_ID < 70400 || version_compare('3.1.6', phpversion('igbinary'), '<='));
        } elseif ($useIgbinarySerialize && (!\extension_loaded('igbinary') || (\PHP_VERSION_ID >= 70400 && version_compare('3.1.6', phpversion('igbinary'), '>')))) {
            throw new CacheException(\extension_loaded('igbinary') && \PHP_VERSION_ID >= 70400 ? 'Please upgrade the "igbinary" PHP extension to v3.1.6 or higher.' : 'The "igbinary" PHP extension is not loaded.');
        }
        $this->useIgbinarySerialize = $useIgbinarySerialize;
        $this->throwOnSerializationFailure = $throwOnSerializationFailure;
    }

    /**
     * {@inheritdoc}
     */
    public function marshall(array $values, ?array &$failed): array
    {
        $serialized = $failed = [];

        foreach ($values as $id => $value) {
            try {
                if ($this->useIgbinarySerialize) {
                    $serialized[$id] = igbinary_serialize($value);
                } else {
                    $serialized[$id] = serialize($value);
                }
            } catch (\Exception $e) {
                if ($this->throwOnSerializationFailure) {
                    throw new \ValueError($e->getMessage(), 0, $e);
                }
                $failed[] = $id;
            }
        }

        return $serialized;
    }

    /**
     * {@inheritdoc}
     */
    public function unmarshall(string $value)
    {
        if ('b:0;' === $value) {
            return false;
        }
        if ('N;' === $value) {
            return null;
        }
        static $igbinaryNull;
        if ($value === ($igbinaryNull ?? $igbinaryNull = \extension_loaded('igbinary') ? igbinary_serialize(null) : false)) {
            return null;
        }
        $unserializeCallbackHandler = ini_set('unserialize_callback_func', __CLASS__.'::handleUnserializeCallback');
        try {
            if (':' === ($value[1] ?? ':')) {
                if (false !== $value = unserialize($value)) {
                    return $value;
                }
            } elseif (false === $igbinaryNull) {
                throw new \RuntimeException('Failed to unserialize values, did you forget to install the "igbinary" extension?');
            } elseif (null !== $value = igbinary_unserialize($value)) {
                return $value;
            }

            throw new \DomainException(error_get_last() ? error_get_last()['message'] : 'Failed to unserialize values.');
        } catch (\Error $e) {
            throw new \ErrorException($e->getMessage(), $e->getCode(), \E_ERROR, $e->getFile(), $e->getLine());
        } finally {
            ini_set('unserialize_callback_func', $unserializeCallbackHandler);
        }
    }

    /**
     * @internal
     */
    public static function handleUnserializeCallback(string $class)
    {
        throw new \DomainException('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\Cache\Marshaller;

use Symfony\Component\Cache\Exception\CacheException;

/**
 * Compresses values using gzdeflate().
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
class DeflateMarshaller implements MarshallerInterface
{
    private $marshaller;

    public function __construct(MarshallerInterface $marshaller)
    {
        if (!\function_exists('gzdeflate')) {
            throw new CacheException('The "zlib" PHP extension is not loaded.');
        }

        $this->marshaller = $marshaller;
    }

    /**
     * {@inheritdoc}
     */
    public function marshall(array $values, ?array &$failed): array
    {
        return array_map('gzdeflate', $this->marshaller->marshall($values, $failed));
    }

    /**
     * {@inheritdoc}
     */
    public function unmarshall(string $value)
    {
        if (false !== $inflatedValue = @gzinflate($value)) {
            $value = $inflatedValue;
        }

        return $this->marshaller->unmarshall($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\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 upper case mappings that map one code point to many code points
    private const UPPER_FROM = ['ß', 'ﬀ', 'ﬁ', 'ﬂ', 'ﬃ', 'ﬄ', 'ﬅ', 'ﬆ', 'և', 'ﬓ', 'ﬔ', 'ﬕ', 'ﬖ', 'ﬗ', 'ŉ', 'ΐ', 'ΰ', 'ǰ', 'ẖ', 'ẗ', 'ẘ', 'ẙ', 'ẚ', 'ὐ', 'ὒ', 'ὔ', 'ὖ', 'ᾶ', 'ῆ', 'ῒ', 'ΐ', 'ῖ', 'ῗ', 'ῢ', 'ΰ', 'ῤ', 'ῦ', 'ῧ', 'ῶ'];
    private const UPPER_TO = ['SS', 'FF', 'FI', 'FL', 'FFI', 'FFL', 'ST', 'ST', 'ԵՒ', 'ՄՆ', 'ՄԵ', 'ՄԻ', 'ՎՆ', 'ՄԽ', 'ʼN', 'Ϊ́', 'Ϋ́', 'J̌', 'H̱', 'T̈', 'W̊', 'Y̊', 'Aʾ', 'Υ̓', 'Υ̓̀', 'Υ̓́', 'Υ̓͂', 'Α͂', 'Η͂', 'Ϊ̀', 'Ϊ́', 'Ι͂', 'Ϊ͂', 'Ϋ̀', 'Ϋ́', 'Ρ̓', 'Υ͂', 'Ϋ͂', 'Ω͂'];

    // 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 $transliterators = [];
    private static $tableZero;
    private static $tableWide;

    /**
     * @return static
     */
    public static function fromCodePoints(int ...$codes): self
    {
        $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] ?? self::$transliterators[$rule] = \Transliterator::create($rule)) {
                        if ('any-latin/bgn' === $rule) {
                            $rule = 'any-latin';
                            $transliterator = self::$transliterators[$rule] ?? 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 {
                $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);
            }
        }

        $str->string .= $s;

        return $str;
    }

    public function camel(): parent
    {
        $str = clone $this;
        $str->string = str_replace(' ', '', preg_replace_callback('/\b.(?!\p{Lu})/u', static function ($m) use (&$i) {
            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): parent
    {
        $str = clone $this;

        if (!$compat || \PHP_VERSION_ID < 70300 || !\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): parent
    {
        $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(): parent
    {
        $str = clone $this;
        $str->string = mb_strtolower(str_replace('İ', 'i̇', $str->string), 'UTF-8');

        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 function ($t, $m) { throw new InvalidArgumentException($m); });

        try {
            if (false === $match($regexp.'u', $this->string, $matches, $flags | \PREG_UNMATCHED_AS_NULL, $offset)) {
                $lastError = preg_last_error();

                foreach (get_defined_constants(true)['pcre'] as $k => $v) {
                    if ($lastError === $v && '_ERROR' === substr($k, -6)) {
                        throw new RuntimeException('Matching failed with '.$k.'.');
                    }
                }

                throw new RuntimeException('Matching failed with unknown error code.');
            }
        } finally {
            restore_error_handler();
        }

        return $matches;
    }

    /**
     * @return static
     */
    public function normalize(int $form = self::NFC): self
    {
        if (!\in_array($form, [self::NFC, self::NFD, self::NFKC, self::NFKD])) {
            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 = ' '): parent
    {
        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 = ' '): parent
    {
        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 = ' '): parent
    {
        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, $to): parent
    {
        if ($this->ignoreCase) {
            $fromRegexp .= 'i';
        }

        if (\is_array($to) || $to instanceof \Closure) {
            if (!\is_callable($to)) {
                throw new \TypeError(sprintf('Argument 2 passed to "%s::replaceMatches()" must be callable, array given.', static::class));
            }

            $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 function ($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 && '_ERROR' === substr($k, -6)) {
                        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(): parent
    {
        $str = clone $this;
        $str->string = implode('', array_reverse(preg_split('/(\X)/u', $str->string, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY)));

        return $str;
    }

    public function snake(): parent
    {
        $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): parent
    {
        $str = clone $this;

        $limit = $allWords ? -1 : 1;

        $str->string = preg_replace_callback('/\b./u', static function (array $m): string {
            return mb_convert_case($m[0], \MB_CASE_TITLE, 'UTF-8');
        }, $str->string, $limit);

        return $str;
    }

    public function trim(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): parent
    {
        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}"): parent
    {
        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): parent
    {
        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}"): parent
    {
        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): parent
    {
        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(): parent
    {
        $str = clone $this;
        $str->string = mb_strtoupper($str->string, 'UTF-8');

        if (\PHP_VERSION_ID < 70300) {
            $str->string = str_replace(self::UPPER_FROM, self::UPPER_TO, $str->string);
        }

        return $str;
    }

    public function width(bool $ignoreAnsiDecoration = true): int
    {
        $width = 0;
        $s = str_replace(["\x00", "\x05", "\x07"], '', $this->string);

        if (false !== strpos($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;
    }

    /**
     * @return static
     */
    private function pad(int $len, self $pad, int $type): parent
    {
        $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;

        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 (null === self::$tableZero) {
                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;
                    }
                }
            }

            if (null === self::$tableWide) {
                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;

                        continue 2;
                    }
                }
            }

            ++$width;
        }

        return $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\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 = '';
    protected $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 = $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
     *
     * @return static
     */
    public function after($needle, bool $includeNeedle = false, int $offset = 0): self
    {
        $str = clone $this;
        $i = \PHP_INT_MAX;

        foreach ((array) $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
     *
     * @return static
     */
    public function afterLast($needle, bool $includeNeedle = false, int $offset = 0): self
    {
        $str = clone $this;
        $i = null;

        foreach ((array) $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);
    }

    /**
     * @return static
     */
    abstract public function append(string ...$suffix): self;

    /**
     * @param string|string[] $needle
     *
     * @return static
     */
    public function before($needle, bool $includeNeedle = false, int $offset = 0): self
    {
        $str = clone $this;
        $i = \PHP_INT_MAX;

        foreach ((array) $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
     *
     * @return static
     */
    public function beforeLast($needle, bool $includeNeedle = false, int $offset = 0): self
    {
        $str = clone $this;
        $i = null;

        foreach ((array) $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));
    }

    /**
     * @return static
     */
    abstract public function camel(): self;

    /**
     * @return static[]
     */
    abstract public function chunk(int $length = 1): array;

    /**
     * @return static
     */
    public function collapseWhitespace(): self
    {
        $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($needle): bool
    {
        return null !== $this->indexOf($needle);
    }

    /**
     * @param string|string[] $suffix
     */
    public function endsWith($suffix): bool
    {
        if (!\is_array($suffix) && !$suffix instanceof \Traversable) {
            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;
    }

    /**
     * @return static
     */
    public function ensureEnd(string $suffix): self
    {
        if (!$this->endsWith($suffix)) {
            return $this->append($suffix);
        }

        $suffix = preg_quote($suffix);
        $regex = '{('.$suffix.')(?:'.$suffix.')++$}D';

        return $this->replaceMatches($regex.($this->ignoreCase ? 'i' : ''), '$1');
    }

    /**
     * @return static
     */
    public function ensureStart(string $prefix): self
    {
        $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): bool
    {
        if (!\is_array($string) && !$string instanceof \Traversable) {
            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;
    }

    /**
     * @return static
     */
    abstract public function folded(): self;

    /**
     * @return static
     */
    public function ignoreCase(): self
    {
        $str = clone $this;
        $str->ignoreCase = true;

        return $str;
    }

    /**
     * @param string|string[] $needle
     */
    public function indexOf($needle, int $offset = 0): ?int
    {
        if (!\is_array($needle) && !$needle instanceof \Traversable) {
            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($needle, int $offset = 0): ?int
    {
        if (!\is_array($needle) && !$needle instanceof \Traversable) {
            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;
    }

    /**
     * @return static
     */
    abstract public function join(array $strings, ?string $lastGlue = null): self;

    public function jsonSerialize(): string
    {
        return $this->string;
    }

    abstract public function length(): int;

    /**
     * @return static
     */
    abstract public function lower(): self;

    /**
     * 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;

    /**
     * @return static
     */
    abstract public function padBoth(int $length, string $padStr = ' '): self;

    /**
     * @return static
     */
    abstract public function padEnd(int $length, string $padStr = ' '): self;

    /**
     * @return static
     */
    abstract public function padStart(int $length, string $padStr = ' '): self;

    /**
     * @return static
     */
    abstract public function prepend(string ...$prefix): self;

    /**
     * @return static
     */
    public function repeat(int $multiplier): self
    {
        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;
    }

    /**
     * @return static
     */
    abstract public function replace(string $from, string $to): self;

    /**
     * @param string|callable $to
     *
     * @return static
     */
    abstract public function replaceMatches(string $fromRegexp, $to): self;

    /**
     * @return static
     */
    abstract public function reverse(): self;

    /**
     * @return static
     */
    abstract public function slice(int $start = 0, ?int $length = null): self;

    /**
     * @return static
     */
    abstract public function snake(): self;

    /**
     * @return static
     */
    abstract public function splice(string $replacement, int $start = 0, ?int $length = null): self;

    /**
     * @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 function ($t, $m) { throw new InvalidArgumentException($m); });

        try {
            if (false === $chunks = preg_split($delimiter, $this->string, $limit, $flags)) {
                $lastError = preg_last_error();

                foreach (get_defined_constants(true)['pcre'] as $k => $v) {
                    if ($lastError === $v && '_ERROR' === substr($k, -6)) {
                        throw new RuntimeException('Splitting failed with '.$k.'.');
                    }
                }

                throw new RuntimeException('Splitting failed with unknown error code.');
            }
        } 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($prefix): bool
    {
        if (!\is_array($prefix) && !$prefix instanceof \Traversable) {
            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;
    }

    /**
     * @return static
     */
    abstract public function title(bool $allWords = false): self;

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

        set_error_handler(static function ($t, $m) { throw new InvalidArgumentException($m); });

        try {
            try {
                $b->string = mb_convert_encoding($this->string, $toEncoding, 'UTF-8');
            } catch (InvalidArgumentException|\ValueError $e) {
                if (!\function_exists('iconv')) {
                    if ($e instanceof \ValueError) {
                        throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
                    }
                    throw $e;
                }

                $b->string = iconv('UTF-8', $toEncoding, $this->string);
            }
        } finally {
            restore_error_handler();
        }

        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);
    }

    /**
     * @return static
     */
    abstract public function trim(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): self;

    /**
     * @return static
     */
    abstract public function trimEnd(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): self;

    /**
     * @param string|string[] $prefix
     *
     * @return static
     */
    public function trimPrefix($prefix): self
    {
        if (\is_array($prefix) || $prefix instanceof \Traversable) {
            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;
    }

    /**
     * @return static
     */
    abstract public function trimStart(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): self;

    /**
     * @param string|string[] $suffix
     *
     * @return static
     */
    public function trimSuffix($suffix): self
    {
        if (\is_array($suffix) || $suffix instanceof \Traversable) {
            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;
    }

    /**
     * @return static
     */
    public function truncate(int $length, string $ellipsis = '', bool $cut = true): self
    {
        $stringLength = $this->length();

        if ($stringLength <= $length) {
            return clone $this;
        }

        $ellipsisLength = '' !== $ellipsis ? (new static($ellipsis))->length() : 0;

        if ($length < $ellipsisLength) {
            $ellipsisLength = 0;
        }

        if (!$cut) {
            if (null === $length = $this->indexOf([' ', "\r", "\n", "\t"], ($length ?: 1) - 1)) {
                return clone $this;
            }

            $length += $ellipsisLength;
        }

        $str = $this->slice(0, $length - $ellipsisLength);

        return $ellipsisLength ? $str->trimEnd()->append($ellipsis) : $str;
    }

    /**
     * @return static
     */
    abstract public function upper(): self;

    /**
     * Returns the printable length on a terminal.
     */
    abstract public function width(bool $ignoreAnsiDecoration = true): int;

    /**
     * @return static
     */
    public function wordwrap(int $width = 75, string $break = "\n", bool $cut = false): self
    {
        $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 __sleep(): array
    {
        return ['string'];
    }

    public function __clone()
    {
        $this->ignoreCase = false;
    }

    public function __toString(): string
    {
        return $this->string;
    }
}
<?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 = $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\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): AbstractString
    {
        $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($suffix): bool
    {
        if ($suffix instanceof AbstractString) {
            $suffix = $suffix->string;
        } elseif (\is_array($suffix) || $suffix instanceof \Traversable) {
            return parent::endsWith($suffix);
        } else {
            $suffix = (string) $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): bool
    {
        if ($string instanceof AbstractString) {
            $string = $string->string;
        } elseif (\is_array($string) || $string instanceof \Traversable) {
            return parent::equalsTo($string);
        } else {
            $string = (string) $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($needle, int $offset = 0): ?int
    {
        if ($needle instanceof AbstractString) {
            $needle = $needle->string;
        } elseif (\is_array($needle) || $needle instanceof \Traversable) {
            return parent::indexOf($needle, $offset);
        } else {
            $needle = (string) $needle;
        }

        if ('' === $needle) {
            return null;
        }

        $i = $this->ignoreCase ? mb_stripos($this->string, $needle, $offset, 'UTF-8') : mb_strpos($this->string, $needle, $offset, 'UTF-8');

        return false === $i ? null : $i;
    }

    public function indexOfLast($needle, int $offset = 0): ?int
    {
        if ($needle instanceof AbstractString) {
            $needle = $needle->string;
        } elseif (\is_array($needle) || $needle instanceof \Traversable) {
            return parent::indexOfLast($needle, $offset);
        } else {
            $needle = (string) $needle;
        }

        if ('' === $needle) {
            return null;
        }

        $i = $this->ignoreCase ? mb_strripos($this->string, $needle, $offset, 'UTF-8') : mb_strrpos($this->string, $needle, $offset, 'UTF-8');

        return false === $i ? null : $i;
    }

    public function length(): int
    {
        return mb_strlen($this->string, 'UTF-8');
    }

    public function prepend(string ...$prefix): AbstractString
    {
        $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): AbstractString
    {
        $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): AbstractString
    {
        $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): AbstractString
    {
        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($this->string, $start, $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 = $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($prefix): bool
    {
        if ($prefix instanceof AbstractString) {
            $prefix = $prefix->string;
        } elseif (\is_array($prefix) || $prefix instanceof \Traversable) {
            return parent::startsWith($prefix);
        } else {
            $prefix = (string) $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;

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 = $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.');
        }

        $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): parent
    {
        $str = clone $this;
        $str->string .= 1 >= \count($suffix) ? ($suffix[0] ?? '') : implode('', $suffix);

        return $str;
    }

    public function camel(): parent
    {
        $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($suffix): bool
    {
        if ($suffix instanceof parent) {
            $suffix = $suffix->string;
        } elseif (\is_array($suffix) || $suffix instanceof \Traversable) {
            return parent::endsWith($suffix);
        } else {
            $suffix = (string) $suffix;
        }

        return '' !== $suffix && \strlen($this->string) >= \strlen($suffix) && 0 === substr_compare($this->string, $suffix, -\strlen($suffix), null, $this->ignoreCase);
    }

    public function equalsTo($string): bool
    {
        if ($string instanceof parent) {
            $string = $string->string;
        } elseif (\is_array($string) || $string instanceof \Traversable) {
            return parent::equalsTo($string);
        } else {
            $string = (string) $string;
        }

        if ('' !== $string && $this->ignoreCase) {
            return 0 === strcasecmp($string, $this->string);
        }

        return $string === $this->string;
    }

    public function folded(): parent
    {
        $str = clone $this;
        $str->string = strtolower($str->string);

        return $str;
    }

    public function indexOf($needle, int $offset = 0): ?int
    {
        if ($needle instanceof parent) {
            $needle = $needle->string;
        } elseif (\is_array($needle) || $needle instanceof \Traversable) {
            return parent::indexOf($needle, $offset);
        } else {
            $needle = (string) $needle;
        }

        if ('' === $needle) {
            return null;
        }

        $i = $this->ignoreCase ? stripos($this->string, $needle, $offset) : strpos($this->string, $needle, $offset);

        return false === $i ? null : $i;
    }

    public function indexOfLast($needle, int $offset = 0): ?int
    {
        if ($needle instanceof parent) {
            $needle = $needle->string;
        } elseif (\is_array($needle) || $needle instanceof \Traversable) {
            return parent::indexOfLast($needle, $offset);
        } else {
            $needle = (string) $needle;
        }

        if ('' === $needle) {
            return null;
        }

        $i = $this->ignoreCase ? strripos($this->string, $needle, $offset) : strrpos($this->string, $needle, $offset);

        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): parent
    {
        $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(): parent
    {
        $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 function ($t, $m) { throw new InvalidArgumentException($m); });

        try {
            if (false === $match($regexp, $this->string, $matches, $flags | \PREG_UNMATCHED_AS_NULL, $offset)) {
                $lastError = preg_last_error();

                foreach (get_defined_constants(true)['pcre'] as $k => $v) {
                    if ($lastError === $v && '_ERROR' === substr($k, -6)) {
                        throw new RuntimeException('Matching failed with '.$k.'.');
                    }
                }

                throw new RuntimeException('Matching failed with unknown error code.');
            }
        } finally {
            restore_error_handler();
        }

        return $matches;
    }

    public function padBoth(int $length, string $padStr = ' '): parent
    {
        $str = clone $this;
        $str->string = str_pad($this->string, $length, $padStr, \STR_PAD_BOTH);

        return $str;
    }

    public function padEnd(int $length, string $padStr = ' '): parent
    {
        $str = clone $this;
        $str->string = str_pad($this->string, $length, $padStr, \STR_PAD_RIGHT);

        return $str;
    }

    public function padStart(int $length, string $padStr = ' '): parent
    {
        $str = clone $this;
        $str->string = str_pad($this->string, $length, $padStr, \STR_PAD_LEFT);

        return $str;
    }

    public function prepend(string ...$prefix): parent
    {
        $str = clone $this;
        $str->string = (1 >= \count($prefix) ? ($prefix[0] ?? '') : implode('', $prefix)).$str->string;

        return $str;
    }

    public function replace(string $from, string $to): parent
    {
        $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, $to): parent
    {
        if ($this->ignoreCase) {
            $fromRegexp .= 'i';
        }

        if (\is_array($to)) {
            if (!\is_callable($to)) {
                throw new \TypeError(sprintf('Argument 2 passed to "%s::replaceMatches()" must be callable, array given.', static::class));
            }

            $replace = 'preg_replace_callback';
        } else {
            $replace = $to instanceof \Closure ? 'preg_replace_callback' : 'preg_replace';
        }

        set_error_handler(static function ($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 && '_ERROR' === substr($k, -6)) {
                        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(): parent
    {
        $str = clone $this;
        $str->string = strrev($str->string);

        return $str;
    }

    public function slice(int $start = 0, ?int $length = null): parent
    {
        $str = clone $this;
        $str->string = (string) substr($this->string, $start, $length ?? \PHP_INT_MAX);

        return $str;
    }

    public function snake(): parent
    {
        $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): parent
    {
        $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 = $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($prefix): bool
    {
        if ($prefix instanceof parent) {
            $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): parent
    {
        $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 function ($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"): parent
    {
        $str = clone $this;
        $str->string = trim($str->string, $chars);

        return $str;
    }

    public function trimEnd(string $chars = " \t\n\r\0\x0B\x0C"): parent
    {
        $str = clone $this;
        $str->string = rtrim($str->string, $chars);

        return $str;
    }

    public function trimStart(string $chars = " \t\n\r\0\x0B\x0C"): parent
    {
        $str = clone $this;
        $str->string = ltrim($str->string, $chars);

        return $str;
    }

    public function upper(): parent
    {
        $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;

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

        // bacteria (bacterium)
        ['airetcab', 8, true, true, 'bacterium'],

        // corpora (corpus)
        ['aroproc', 7, true, true, 'corpus'],

        // criteria (criterion)
        ['airetirc', 8, true, true, 'criterion'],

        // curricula (curriculum)
        ['alucirruc', 9, true, true, 'curriculum'],

        // 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'],

        // 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

        // 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'],

        // 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'],

        // 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\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';

    /**
     * {@inheritdoc}
     */
    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];
    }

    /**
     * {@inheritdoc}
     */
    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;

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\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\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;

/**
 * A string whose value is computed lazily by a callback.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
class LazyString implements \Stringable, \JsonSerializable
{
    private $value;

    /**
     * @param callable|array $callback A callable or a [Closure, method] lazy-callable
     *
     * @return static
     */
    public static function fromCallable($callback, ...$arguments): self
    {
        if (!\is_callable($callback) && !(\is_array($callback) && isset($callback[0]) && $callback[0] 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__, get_debug_type($callback)));
        }

        $lazyString = new static();
        $lazyString->value = static function () use (&$callback, &$arguments, &$value): string {
            if (null !== $arguments) {
                if (!\is_callable($callback)) {
                    $callback[0] = $callback[0]();
                    $callback[1] = $callback[1] ?? '__invoke';
                }
                $value = $callback(...$arguments);
                $callback = self::getPrettyName($callback);
                $arguments = null;
            }

            return $value ?? '';
        };

        return $lazyString;
    }

    /**
     * @param string|int|float|bool|\Stringable $value
     *
     * @return static
     */
    public static function fromStringable($value): self
    {
        if (!self::isStringable($value)) {
            throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be a scalar or a stringable object, "%s" given.', __METHOD__, get_debug_type($value)));
        }

        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($value): bool
    {
        return \is_string($value) || $value instanceof self || (\is_object($value) ? method_exists($value, '__toString') : \is_scalar($value));
    }

    /**
     * Casts scalars and stringable objects to strings.
     *
     * @param object|string|int|float|bool $value
     *
     * @throws \TypeError When the provided value is not stringable
     */
    final public static function resolve($value): string
    {
        return $value;
    }

    /**
     * @return string
     */
    public function __toString()
    {
        if (\is_string($this->value)) {
            return $this->value;
        }

        try {
            return $this->value = ($this->value)();
        } catch (\Throwable $e) {
            if (\TypeError::class === \get_class($e) && __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));
            }

            if (\PHP_VERSION_ID < 70400) {
                // leverage the ErrorHandler component with graceful fallback when it's not available
                return trigger_error($e, \E_USER_ERROR);
            }

            throw $e;
        }
    }

    public function __sleep(): array
    {
        $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 (str_contains($r->name, '{closure') || !$class = \PHP_VERSION_ID >= 80111 ? $r->getClosureCalledClass() : $r->getClosureScopeClass()) {
                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;

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 = '')
    {
        $this->string = normalizer_is_normalized($string) ? $string : normalizer_normalize($string);

        if (false === $this->string) {
            throw new InvalidArgumentException('Invalid UTF-8 string.');
        }
    }

    public function append(string ...$suffix): AbstractString
    {
        $str = clone $this;
        $str->string = $this->string.(1 >= \count($suffix) ? ($suffix[0] ?? '') : implode('', $suffix));
        normalizer_is_normalized($str->string) ?: $str->string = normalizer_normalize($str->string);

        if (false === $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 .= '\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($suffix): bool
    {
        if ($suffix instanceof AbstractString) {
            $suffix = $suffix->string;
        } elseif (\is_array($suffix) || $suffix instanceof \Traversable) {
            return parent::endsWith($suffix);
        } else {
            $suffix = (string) $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 ($this->ignoreCase) {
            return 0 === mb_stripos(grapheme_extract($this->string, \strlen($suffix), \GRAPHEME_EXTR_MAXBYTES, \strlen($this->string) - \strlen($suffix)), $suffix, 0, 'UTF-8');
        }

        return $suffix === grapheme_extract($this->string, \strlen($suffix), \GRAPHEME_EXTR_MAXBYTES, \strlen($this->string) - \strlen($suffix));
    }

    public function equalsTo($string): bool
    {
        if ($string instanceof AbstractString) {
            $string = $string->string;
        } elseif (\is_array($string) || $string instanceof \Traversable) {
            return parent::equalsTo($string);
        } else {
            $string = (string) $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($needle, int $offset = 0): ?int
    {
        if ($needle instanceof AbstractString) {
            $needle = $needle->string;
        } elseif (\is_array($needle) || $needle instanceof \Traversable) {
            return parent::indexOf($needle, $offset);
        } else {
            $needle = (string) $needle;
        }

        $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 $e) {
            return null;
        }

        return false === $i ? null : $i;
    }

    public function indexOfLast($needle, int $offset = 0): ?int
    {
        if ($needle instanceof AbstractString) {
            $needle = $needle->string;
        } elseif (\is_array($needle) || $needle instanceof \Traversable) {
            return parent::indexOfLast($needle, $offset);
        } else {
            $needle = (string) $needle;
        }

        $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;
        }

        $i = $this->ignoreCase ? grapheme_strripos($string, $needle, $offset) : grapheme_strrpos($string, $needle, $offset);

        return false === $i ? null : $i;
    }

    public function join(array $strings, ?string $lastGlue = null): AbstractString
    {
        $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);
    }

    /**
     * @return static
     */
    public function normalize(int $form = self::NFC): parent
    {
        $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): AbstractString
    {
        $str = clone $this;
        $str->string = (1 >= \count($prefix) ? ($prefix[0] ?? '') : implode('', $prefix)).$this->string;
        normalizer_is_normalized($str->string) ?: $str->string = normalizer_normalize($str->string);

        if (false === $str->string) {
            throw new InvalidArgumentException('Invalid UTF-8 string.');
        }

        return $str;
    }

    public function replace(string $from, string $to): AbstractString
    {
        $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;
            normalizer_is_normalized($str->string) ?: $str->string = normalizer_normalize($str->string);

            if (false === $str->string) {
                throw new InvalidArgumentException('Invalid UTF-8 string.');
            }
        }

        return $str;
    }

    public function replaceMatches(string $fromRegexp, $to): AbstractString
    {
        $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): AbstractString
    {
        $str = clone $this;

        if (\PHP_VERSION_ID < 80000 && 0 > $start && grapheme_strlen($this->string) < -$start) {
            $start = 0;
        }
        $str->string = (string) grapheme_substr($this->string, $start, $length ?? 2147483647);

        return $str;
    }

    public function splice(string $replacement, int $start = 0, ?int $length = null): AbstractString
    {
        $str = clone $this;

        if (\PHP_VERSION_ID < 80000 && 0 > $start && grapheme_strlen($this->string) < -$start) {
            $start = 0;
        }
        $start = $start ? \strlen(grapheme_substr($this->string, 0, $start)) : 0;
        $length = $length ? \strlen(grapheme_substr($this->string, $start, $length ?? 2147483647)) : $length;
        $str->string = substr_replace($this->string, $replacement, $start, $length ?? 2147483647);
        normalizer_is_normalized($str->string) ?: $str->string = normalizer_normalize($str->string);

        if (false === $str->string) {
            throw new InvalidArgumentException('Invalid UTF-8 string.');
        }

        return $str;
    }

    public function split(string $delimiter, ?int $limit = null, ?int $flags = null): array
    {
        if (1 > $limit = $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($prefix): bool
    {
        if ($prefix instanceof AbstractString) {
            $prefix = $prefix->string;
        } elseif (\is_array($prefix) || $prefix instanceof \Traversable) {
            return parent::startsWith($prefix);
        } else {
            $prefix = (string) $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 ($this->ignoreCase) {
            return 0 === mb_stripos(grapheme_extract($this->string, \strlen($prefix), \GRAPHEME_EXTR_MAXBYTES), $prefix, 0, 'UTF-8');
        }

        return $prefix === grapheme_extract($this->string, \strlen($prefix), \GRAPHEME_EXTR_MAXBYTES);
    }

    public function __wakeup()
    {
        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\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\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 $defaultLocale;
    private $symbolsMap = [
        'en' => ['@' => 'at', '&' => 'and'],
    ];

    /**
     * Cache of transliterators per locale.
     *
     * @var \Transliterator[]
     */
    private $transliterators = [];

    /**
     * @param array|\Closure|null $symbolsMap
     */
    public function __construct(?string $defaultLocale = null, $symbolsMap = null)
    {
        if (null !== $symbolsMap && !\is_array($symbolsMap) && !$symbolsMap instanceof \Closure) {
            throw new \TypeError(sprintf('Argument 2 passed to "%s()" must be array, Closure or null, "%s" given.', __METHOD__, \gettype($symbolsMap)));
        }

        $this->defaultLocale = $defaultLocale;
        $this->symbolsMap = $symbolsMap ?? $this->symbolsMap;
    }

    /**
     * {@inheritdoc}
     */
    public function setLocale($locale)
    {
        $this->defaultLocale = $locale;
    }

    /**
     * {@inheritdoc}
     */
    public function getLocale()
    {
        return $this->defaultLocale;
    }

    /**
     * {@inheritdoc}
     */
    public function slug(string $string, string $separator = '-', ?string $locale = null): AbstractUnicodeString
    {
        $locale = $locale ?? $this->defaultLocale;

        $transliterator = [];
        if ($locale && ('de' === $locale || 0 === strpos($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 ($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 function ($s) use ($symbolsMap, $locale) {
                return $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 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 is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@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\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
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;

    /**
     * {@inheritdoc}
     */
    protected function configure()
    {
        $this->ignoreValidationErrors();

        $this
            ->setName('help')
            ->setDefinition([
                new InputArgument('command_name', InputArgument::OPTIONAL, 'The command name', 'help'),
                new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt'),
                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
            )
        ;
    }

    public function setCommand(Command $command)
    {
        $this->command = $command;
    }

    /**
     * {@inheritdoc}
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        if (null === $this->command) {
            $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'),
        ]);

        $this->command = null;

        return 0;
    }

    public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
    {
        if ($input->mustSuggestArgumentValuesFor('command_name')) {
            $descriptor = new ApplicationDescription($this->getApplication());
            $suggestions->suggestValues(array_keys($descriptor->getCommands()));

            return;
        }

        if ($input->mustSuggestOptionValuesFor('format')) {
            $helper = new DescriptorHelper();
            $suggestions->suggestValues($helper->getFormats());
        }
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@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\Exception\ExceptionInterface;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\LogicException;
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
     */
    protected static $defaultName;

    /**
     * @var string|null The default command description
     */
    protected static $defaultDescription;

    private $application;
    private $name;
    private $processTitle;
    private $aliases = [];
    private $definition;
    private $hidden = false;
    private $help = '';
    private $description = '';
    private $fullDefinition;
    private $ignoreValidationErrors = false;
    private $code;
    private $synopsis = [];
    private $usages = [];
    private $helperSet;

    /**
     * @return string|null
     */
    public static function getDefaultName()
    {
        $class = static::class;

        if (\PHP_VERSION_ID >= 80000 && $attribute = (new \ReflectionClass($class))->getAttributes(AsCommand::class)) {
            return $attribute[0]->newInstance()->name;
        }

        $r = new \ReflectionProperty($class, 'defaultName');

        return $class === $r->class ? static::$defaultName : null;
    }

    public static function getDefaultDescription(): ?string
    {
        $class = static::class;

        if (\PHP_VERSION_ID >= 80000 && $attribute = (new \ReflectionClass($class))->getAttributes(AsCommand::class)) {
            return $attribute[0]->newInstance()->description;
        }

        $r = new \ReflectionProperty($class, 'defaultDescription');

        return $class === $r->class ? static::$defaultDescription : null;
    }

    /**
     * @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.
     */
    public function ignoreValidationErrors()
    {
        $this->ignoreValidationErrors = true;
    }

    public function setApplication(?Application $application = null)
    {
        $this->application = $application;
        if ($application) {
            $this->setHelperSet($application->getHelperSet());
        } else {
            $this->helperSet = null;
        }

        $this->fullDefinition = null;
    }

    public function setHelperSet(HelperSet $helperSet)
    {
        $this->helperSet = $helperSet;
    }

    /**
     * Gets the helper set.
     *
     * @return HelperSet|null
     */
    public function getHelperSet()
    {
        return $this->helperSet;
    }

    /**
     * Gets the application instance for this command.
     *
     * @return Application|null
     */
    public function getApplication()
    {
        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.
     */
    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.
     */
    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()
     */
    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)
    {
        // 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
    {
    }

    /**
     * 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)
    {
        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();
                }
            }
        }

        $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)
    {
        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.
     *
     * @param array|InputDefinition $definition An array of argument and option instances or a definition instance
     *
     * @return $this
     */
    public function setDefinition($definition)
    {
        if ($definition instanceof InputDefinition) {
            $this->definition = $definition;
        } else {
            $this->definition->setDefinition($definition);
        }

        $this->fullDefinition = null;

        return $this;
    }

    /**
     * Gets the InputDefinition attached to this Command.
     *
     * @return InputDefinition
     */
    public function getDefinition()
    {
        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.
     *
     * @return InputDefinition
     */
    public function getNativeDefinition()
    {
        if (null === $this->definition) {
            throw new LogicException(sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', static::class));
        }

        return $this->definition;
    }

    /**
     * Adds an argument.
     *
     * @param int|null $mode    The argument mode: InputArgument::REQUIRED or InputArgument::OPTIONAL
     * @param mixed    $default The default value (for InputArgument::OPTIONAL mode only)
     *
     * @return $this
     *
     * @throws InvalidArgumentException When argument mode is not valid
     */
    public function addArgument(string $name, ?int $mode = null, string $description = '', $default = null)
    {
        $this->definition->addArgument(new InputArgument($name, $mode, $description, $default));
        if (null !== $this->fullDefinition) {
            $this->fullDefinition->addArgument(new InputArgument($name, $mode, $description, $default));
        }

        return $this;
    }

    /**
     * Adds an option.
     *
     * @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 InputOption::VALUE_* constants
     * @param mixed             $default  The default value (must be null for InputOption::VALUE_NONE)
     *
     * @return $this
     *
     * @throws InvalidArgumentException If option mode is invalid or incompatible
     */
    public function addOption(string $name, $shortcut = null, ?int $mode = null, string $description = '', $default = null)
    {
        $this->definition->addOption(new InputOption($name, $shortcut, $mode, $description, $default));
        if (null !== $this->fullDefinition) {
            $this->fullDefinition->addOption(new InputOption($name, $shortcut, $mode, $description, $default));
        }

        return $this;
    }

    /**
     * Sets the name of the command.
     *
     * This method can set both the namespace and the name if
     * you separate them by a colon (:)
     *
     *     $command->setName('foo:bar');
     *
     * @return $this
     *
     * @throws InvalidArgumentException When the name is invalid
     */
    public function setName(string $name)
    {
        $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)
    {
        $this->processTitle = $title;

        return $this;
    }

    /**
     * Returns the command name.
     *
     * @return string|null
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * @param bool $hidden Whether or not the command should be hidden from the list of commands
     *                     The default value will be true in Symfony 6.0
     *
     * @return $this
     *
     * @final since Symfony 5.1
     */
    public function setHidden(bool $hidden /* = true */)
    {
        $this->hidden = $hidden;

        return $this;
    }

    /**
     * @return bool whether the command should be publicly shown or not
     */
    public function isHidden()
    {
        return $this->hidden;
    }

    /**
     * Sets the description for the command.
     *
     * @return $this
     */
    public function setDescription(string $description)
    {
        $this->description = $description;

        return $this;
    }

    /**
     * Returns the description for the command.
     *
     * @return string
     */
    public function getDescription()
    {
        return $this->description;
    }

    /**
     * Sets the help for the command.
     *
     * @return $this
     */
    public function setHelp(string $help)
    {
        $this->help = $help;

        return $this;
    }

    /**
     * Returns the help for the command.
     *
     * @return string
     */
    public function getHelp()
    {
        return $this->help;
    }

    /**
     * Returns the processed help for the command replacing the %command.name% and
     * %command.full_name% patterns with the real values dynamically.
     *
     * @return string
     */
    public function getProcessedHelp()
    {
        $name = $this->name;
        $isSingleCommand = $this->application && $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)
    {
        $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.
     *
     * @return array
     */
    public function getAliases()
    {
        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
     *
     * @return string
     */
    public function getSynopsis(bool $short = false)
    {
        $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)
    {
        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.
     *
     * @return array
     */
    public function getUsages()
    {
        return $this->usages;
    }

    /**
     * Gets a helper instance by name.
     *
     * @return mixed
     *
     * @throws LogicException           if no HelperSet is defined
     * @throws InvalidArgumentException if the helper is not defined
     */
    public function getHelper(string $name)
    {
        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)
    {
        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;

/**
 * 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.
     */
    public function handleSignal(int $signal): 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\Command;

use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
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>
 */
final class DumpCompletionCommand extends Command
{
    protected static $defaultName = 'completion';
    protected static $defaultDescription = 'Dump the shell completion script';

    public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
    {
        if ($input->mustSuggestArgumentValuesFor('shell')) {
            $suggestions->suggestValues($this->getSupportedShells());
        }
    }

    protected function configure()
    {
        $fullCommand = $_SERVER['PHP_SELF'];
        $commandName = basename($fullCommand);
        $fullCommand = @realpath($fullCommand) ?: $fullCommand;

        $this
            ->setHelp(<<<EOH
The <info>%command.name%</> command dumps the shell completion script required
to use shell autocompletion (currently only bash completion is supported).

<comment>Static installation
-------------------</>

Dump the script to a global completion file and restart your shell:

    <info>%command.full_name% bash | sudo tee /etc/bash_completion.d/{$commandName}</>

Or dump the script to a local file and source it:

    <info>%command.full_name% bash > 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 "~/.bashrc" file:</>
    <info>source /path/to/completion.sh</>

<comment>Dynamic installation
--------------------</>

Add this to the end of your shell configuration file (e.g. <info>"~/.bashrc"</>):

    <info>eval "$({$fullCommand} completion bash)"</>
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')
            ->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, $this->getApplication()->getVersion()], 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(function (string $type, string $line) use ($output): void {
            $output->write($line);
        });
    }

    /**
     * @return string[]
     */
    private function getSupportedShells(): array
    {
        $shells = [];

        foreach (new \DirectoryIterator(__DIR__.'/../Resources/') as $file) {
            if (str_starts_with($file->getBasename(), 'completion.') && $file->isFile()) {
                $shells[] = $file->getExtension();
            }
        }

        return $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
{
    /** @var LockInterface|null */
    private $lock;

    /**
     * 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.');
        }

        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()
    {
        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\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
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
{
    /**
     * {@inheritdoc}
     */
    protected function configure()
    {
        $this
            ->setName('list')
            ->setDefinition([
                new InputArgument('namespace', InputArgument::OPTIONAL, 'The namespace name'),
                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'),
                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
            )
        ;
    }

    /**
     * {@inheritdoc}
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $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;
    }

    public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
    {
        if ($input->mustSuggestArgumentValuesFor('namespace')) {
            $descriptor = new ApplicationDescription($this->getApplication());
            $suggestions->suggestValues(array_keys($descriptor->getNamespaces()));

            return;
        }

        if ($input->mustSuggestOptionValuesFor('format')) {
            $helper = new DescriptorHelper();
            $suggestions->suggestValues($helper->getFormats());
        }
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@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\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 $command;
    private $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 ($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);
    }

    /**
     * @return $this
     */
    public function setCode(callable $code): self
    {
        $this->getCommand()->setCode($code);

        return $this;
    }

    /**
     * @internal
     */
    public function mergeApplicationDefinition(bool $mergeArgs = true): void
    {
        $this->getCommand()->mergeApplicationDefinition($mergeArgs);
    }

    /**
     * @return $this
     */
    public function setDefinition($definition): self
    {
        $this->getCommand()->setDefinition($definition);

        return $this;
    }

    public function getDefinition(): InputDefinition
    {
        return $this->getCommand()->getDefinition();
    }

    public function getNativeDefinition(): InputDefinition
    {
        return $this->getCommand()->getNativeDefinition();
    }

    /**
     * @return $this
     */
    public function addArgument(string $name, ?int $mode = null, string $description = '', $default = null): self
    {
        $this->getCommand()->addArgument($name, $mode, $description, $default);

        return $this;
    }

    /**
     * @return $this
     */
    public function addOption(string $name, $shortcut = null, ?int $mode = null, string $description = '', $default = null): self
    {
        $this->getCommand()->addOption($name, $shortcut, $mode, $description, $default);

        return $this;
    }

    /**
     * @return $this
     */
    public function setProcessTitle(string $title): self
    {
        $this->getCommand()->setProcessTitle($title);

        return $this;
    }

    /**
     * @return $this
     */
    public function setHelp(string $help): self
    {
        $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);
    }

    /**
     * @return $this
     */
    public function addUsage(string $usage): self
    {
        $this->getCommand()->addUsage($usage);

        return $this;
    }

    public function getUsages(): array
    {
        return $this->getCommand()->getUsages();
    }

    /**
     * @return mixed
     */
    public function getHelper(string $name)
    {
        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\Command;

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\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>
 */
final class CompleteCommand extends Command
{
    protected static $defaultName = '|_complete';
    protected static $defaultDescription = 'Internal command to provide shell completion suggestions';

    private $completionOutputs;

    private $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];

        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('symfony', 'S', InputOption::VALUE_REQUIRED, 'The version of the completion script')
        ;
    }

    protected function initialize(InputInterface $input, OutputInterface $output)
    {
        $this->isDebug = filter_var(getenv('SYMFONY_COMPLETION_DEBUG'), \FILTER_VALIDATE_BOOLEAN);
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        try {
            // uncomment when a bugfix or BC break has been introduced in the shell completion scripts
            // $version = $input->getOption('symfony');
            // if ($version && version_compare($version, 'x.y', '>=')) {
            //    $message = sprintf('Completion script version is not supported ("%s" given, ">=x.y" required).', $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:</>',
            ]);

            $command = $this->findCommand($completionInput, $output);
            if (null === $command) {
                $this->log('  No command found, completing using the Application class.');

                $this->getApplication()->complete($completionInput, $suggestions);
            } elseif (
                $completionInput->mustSuggestArgumentValuesFor('command')
                && $command->getName() !== $completionInput->getCompletionValue()
                && !\in_array($completionInput->getCompletionValue(), $command->getAliases(), true)
            ) {
                $this->log('  No command found, completing using the Application class.');

                // expand shortcut names ("cache:cl<TAB>") into their full name ("cache:clear")
                $suggestions->suggestValues(array_filter(array_merge([$command->getName()], $command->getAliases())));
            } else {
                $command->mergeApplicationDefinition();
                $completionInput->bind($command->getDefinition());

                if (CompletionInput::TYPE_OPTION_NAME === $completionInput->getCompletionType()) {
                    $this->log('  Completing option names for the <comment>'.\get_class($command instanceof LazyCommand ? $command->getCommand() : $command).'</> command.');

                    $suggestions->suggestOptions($command->getDefinition()->getOptions());
                } else {
                    $this->log([
                        '  Completing using the <comment>'.\get_class($command instanceof LazyCommand ? $command->getCommand() : $command).'</> 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(function ($o) { return $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 $e) {
        }

        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 $e) {
        }

        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\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;
    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();
        }
    }

    /**
     * {@inheritdoc}
     */
    public function bind(InputDefinition $definition)
    {
        $this->arguments = [];
        $this->options = [];
        $this->definition = $definition;

        $this->parse();
    }

    /**
     * Processes command line arguments.
     */
    abstract protected function parse();

    /**
     * {@inheritdoc}
     */
    public function validate()
    {
        $definition = $this->definition;
        $givenArguments = $this->arguments;

        $missingArguments = array_filter(array_keys($definition->getArguments()), function ($argument) use ($definition, $givenArguments) {
            return !\array_key_exists($argument, $givenArguments) && $definition->getArgument($argument)->isRequired();
        });

        if (\count($missingArguments) > 0) {
            throw new RuntimeException(sprintf('Not enough arguments (missing: "%s").', implode(', ', $missingArguments)));
        }
    }

    /**
     * {@inheritdoc}
     */
    public function isInteractive()
    {
        return $this->interactive;
    }

    /**
     * {@inheritdoc}
     */
    public function setInteractive(bool $interactive)
    {
        $this->interactive = $interactive;
    }

    /**
     * {@inheritdoc}
     */
    public function getArguments()
    {
        return array_merge($this->definition->getArgumentDefaults(), $this->arguments);
    }

    /**
     * {@inheritdoc}
     */
    public function getArgument(string $name)
    {
        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();
    }

    /**
     * {@inheritdoc}
     */
    public function setArgument(string $name, $value)
    {
        if (!$this->definition->hasArgument($name)) {
            throw new InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name));
        }

        $this->arguments[$name] = $value;
    }

    /**
     * {@inheritdoc}
     */
    public function hasArgument(string $name)
    {
        return $this->definition->hasArgument($name);
    }

    /**
     * {@inheritdoc}
     */
    public function getOptions()
    {
        return array_merge($this->definition->getOptionDefaults(), $this->options);
    }

    /**
     * {@inheritdoc}
     */
    public function getOption(string $name)
    {
        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();
    }

    /**
     * {@inheritdoc}
     */
    public function setOption(string $name, $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;
    }

    /**
     * {@inheritdoc}
     */
    public function hasOption(string $name)
    {
        return $this->definition->hasOption($name) || $this->definition->hasNegation($name);
    }

    /**
     * Escapes a token through escapeshellarg if it contains unsafe chars.
     *
     * @return string
     */
    public function escapeToken(string $token)
    {
        return preg_match('{^[\w-]+$}', $token) ? $token : escapeshellarg($token);
    }

    /**
     * {@inheritdoc}
     */
    public function setStream($stream)
    {
        $this->stream = $stream;
    }

    /**
     * {@inheritdoc}
     */
    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\Input;

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 $name;
    private $mode;
    private $default;
    private $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)
     *
     * @throws InvalidArgumentException When argument mode is not valid
     */
    public function __construct(string $name, ?int $mode = null, string $description = '', $default = null)
    {
        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->setDefault($default);
    }

    /**
     * Returns the argument name.
     *
     * @return string
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * Returns true if the argument is required.
     *
     * @return bool true if parameter mode is self::REQUIRED, false otherwise
     */
    public function isRequired()
    {
        return self::REQUIRED === (self::REQUIRED & $this->mode);
    }

    /**
     * Returns true if the argument can take multiple values.
     *
     * @return bool true if mode is self::IS_ARRAY, false otherwise
     */
    public function isArray()
    {
        return self::IS_ARRAY === (self::IS_ARRAY & $this->mode);
    }

    /**
     * Sets the default value.
     *
     * @param string|bool|int|float|array|null $default
     *
     * @throws LogicException When incorrect default value is given
     */
    public function setDefault($default = null)
    {
        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.
     *
     * @return string|bool|int|float|array|null
     */
    public function getDefault()
    {
        return $this->default;
    }

    /**
     * Returns the description text.
     *
     * @return string
     */
    public function getDescription()
    {
        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;

/**
 * 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.
     */
    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;

/**
 * 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
     */
    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\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 $arguments;
    private $requiredCount;
    private $lastArrayArgument;
    private $lastOptionalArgument;
    private $options;
    private $negations;
    private $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.
     */
    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
     */
    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
     */
    public function addArguments(?array $arguments = [])
    {
        if (null !== $arguments) {
            foreach ($arguments as $argument) {
                $this->addArgument($argument);
            }
        }
    }

    /**
     * @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.
     *
     * @param string|int $name The InputArgument name or position
     *
     * @return InputArgument
     *
     * @throws InvalidArgumentException When argument given doesn't exist
     */
    public function getArgument($name)
    {
        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.
     *
     * @param string|int $name The InputArgument name or position
     *
     * @return bool
     */
    public function hasArgument($name)
    {
        $arguments = \is_int($name) ? array_values($this->arguments) : $this->arguments;

        return isset($arguments[$name]);
    }

    /**
     * Gets the array of InputArgument objects.
     *
     * @return InputArgument[]
     */
    public function getArguments()
    {
        return $this->arguments;
    }

    /**
     * Returns the number of InputArguments.
     *
     * @return int
     */
    public function getArgumentCount()
    {
        return null !== $this->lastArrayArgument ? \PHP_INT_MAX : \count($this->arguments);
    }

    /**
     * Returns the number of required InputArguments.
     *
     * @return int
     */
    public function getArgumentRequiredCount()
    {
        return $this->requiredCount;
    }

    /**
     * @return array<string|bool|int|float|array|null>
     */
    public function getArgumentDefaults()
    {
        $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
     */
    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
     */
    public function addOptions(array $options = [])
    {
        foreach ($options as $option) {
            $this->addOption($option);
        }
    }

    /**
     * @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.
     *
     * @return InputOption
     *
     * @throws InvalidArgumentException When option given doesn't exist
     */
    public function getOption(string $name)
    {
        if (!$this->hasOption($name)) {
            throw new InvalidArgumentException(sprintf('The "--%s" option does not exist.', $name));
        }

        return $this->options[$name];
    }

    /**
     * Returns true if an InputOption object exists by name.
     *
     * This method can't be used to check if the user included the option when
     * executing the command (use getOption() instead).
     *
     * @return bool
     */
    public function hasOption(string $name)
    {
        return isset($this->options[$name]);
    }

    /**
     * Gets the array of InputOption objects.
     *
     * @return InputOption[]
     */
    public function getOptions()
    {
        return $this->options;
    }

    /**
     * Returns true if an InputOption object exists by shortcut.
     *
     * @return bool
     */
    public function hasShortcut(string $name)
    {
        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.
     *
     * @return InputOption
     */
    public function getOptionForShortcut(string $shortcut)
    {
        return $this->getOption($this->shortcutToName($shortcut));
    }

    /**
     * @return array<string|bool|int|float|array|null>
     */
    public function getOptionDefaults()
    {
        $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.
     *
     * @return string
     */
    public function getSynopsis(bool $short = false)
    {
        $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;

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>
 */
interface InputInterface
{
    /**
     * Returns the first argument from the raw parameters (not parsed).
     *
     * @return string|null
     */
    public function getFirstArgument();

    /**
     * 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
     *
     * @return bool
     */
    public function hasParameterOption($values, bool $onlyParams = false);

    /**
     * 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($values, $default = false, bool $onlyParams = false);

    /**
     * Binds the current Input instance with the given arguments and options.
     *
     * @throws RuntimeException
     */
    public function bind(InputDefinition $definition);

    /**
     * Validates the input.
     *
     * @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();

    /**
     * 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.
     *
     * @param mixed $value The argument value
     *
     * @throws InvalidArgumentException When argument given doesn't exist
     */
    public function setArgument(string $name, $value);

    /**
     * Returns true if an InputArgument object exists by name or position.
     *
     * @return bool
     */
    public function hasArgument(string $name);

    /**
     * Returns all the given options merged with the default values.
     *
     * @return array<string|bool|int|float|array|null>
     */
    public function getOptions();

    /**
     * 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.
     *
     * @param mixed $value The option value
     *
     * @throws InvalidArgumentException When option given doesn't exist
     */
    public function setOption(string $name, $value);

    /**
     * Returns true if an InputOption object exists by name.
     *
     * @return bool
     */
    public function hasOption(string $name);

    /**
     * Is this input means interactive?
     *
     * @return bool
     */
    public function isInteractive();

    /**
     * Sets the input interactivity.
     */
    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;

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 $tokens;
    private $parsed;

    public function __construct(?array $argv = null, ?InputDefinition $definition = null)
    {
        $argv = $argv ?? $_SERVER['argv'] ?? [];

        // strip the application name
        array_shift($argv);

        $this->tokens = $argv;

        parent::__construct($definition);
    }

    protected function setTokens(array $tokens)
    {
        $this->tokens = $tokens;
    }

    /**
     * {@inheritdoc}
     */
    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)
    {
        $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)
    {
        $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;
            } else {
                $this->addLongOption($option->getName(), null);
            }
        }
    }

    /**
     * Parses a long option.
     */
    private function parseLongOption(string $token)
    {
        $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)
    {
        $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, $value)
    {
        if (!$this->definition->hasShortcut($shortcut)) {
            throw new RuntimeException(sprintf('The "-%s" option does not exist.', $shortcut));
        }

        $this->addLongOption($this->definition->getOptionForShortcut($shortcut)->getName(), $value);
    }

    /**
     * Adds a long option value.
     *
     * @throws RuntimeException When option given doesn't exist
     */
    private function addLongOption(string $name, $value)
    {
        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;
        }
    }

    /**
     * {@inheritdoc}
     */
    public function getFirstArgument()
    {
        $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;
    }

    /**
     * {@inheritdoc}
     */
    public function hasParameterOption($values, bool $onlyParams = false)
    {
        $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;
    }

    /**
     * {@inheritdoc}
     */
    public function getParameterOption($values, $default = false, bool $onlyParams = false)
    {
        $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.
     *
     * @return string
     */
    public function __toString()
    {
        $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\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 $parameters;

    public function __construct(array $parameters, ?InputDefinition $definition = null)
    {
        $this->parameters = $parameters;

        parent::__construct($definition);
    }

    /**
     * {@inheritdoc}
     */
    public function getFirstArgument()
    {
        foreach ($this->parameters as $param => $value) {
            if ($param && \is_string($param) && '-' === $param[0]) {
                continue;
            }

            return $value;
        }

        return null;
    }

    /**
     * {@inheritdoc}
     */
    public function hasParameterOption($values, bool $onlyParams = false)
    {
        $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;
    }

    /**
     * {@inheritdoc}
     */
    public function getParameterOption($values, $default = false, bool $onlyParams = false)
    {
        $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.
     *
     * @return string
     */
    public function __toString()
    {
        $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);
    }

    /**
     * {@inheritdoc}
     */
    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, $value)
    {
        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, $value)
    {
        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.
     *
     * @param string|int $name  The argument name
     * @param mixed      $value The value for the argument
     *
     * @throws InvalidArgumentException When argument given doesn't exist
     */
    private function addArgument($name, $value)
    {
        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\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 $name;
    private $shortcut;
    private $mode;
    private $default;
    private $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)
     *
     * @throws InvalidArgumentException If option mode is invalid or incompatible
     */
    public function __construct(string $name, $shortcut = null, ?int $mode = null, string $description = '', $default = null)
    {
        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;

        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.
     *
     * @return string|null
     */
    public function getShortcut()
    {
        return $this->shortcut;
    }

    /**
     * Returns the option name.
     *
     * @return string
     */
    public function getName()
    {
        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()
    {
        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()
    {
        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()
    {
        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()
    {
        return self::VALUE_IS_ARRAY === (self::VALUE_IS_ARRAY & $this->mode);
    }

    public function isNegatable(): bool
    {
        return self::VALUE_NEGATABLE === (self::VALUE_NEGATABLE & $this->mode);
    }

    /**
     * @param string|bool|int|float|array|null $default
     */
    public function setDefault($default = null)
    {
        if (self::VALUE_NONE === (self::VALUE_NONE & $this->mode) && null !== $default) {
            throw new LogicException('Cannot set a default value when using 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.
     *
     * @return string|bool|int|float|array|null
     */
    public function getDefault()
    {
        return $this->default;
    }

    /**
     * Returns the description text.
     *
     * @return string
     */
    public function getDescription()
    {
        return $this->description;
    }

    /**
     * Checks whether the given option equals this one.
     *
     * @return bool
     */
    public function equals(self $option)
    {
        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\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
{
    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\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
{
    /** @var StreamOutput */
    private $output;
    private $inputs = [];
    private $captureStreamsIndependently = false;
    /** @var InputInterface */
    private $input;
    /** @var int */
    private $statusCode;

    /**
     * Gets the display returned by the last execution of the command or application.
     *
     * @return string
     *
     * @throws \RuntimeException If it's called before the execute method
     */
    public function getDisplay(bool $normalize = false)
    {
        if (null === $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
     *
     * @return string
     */
    public function getErrorOutput(bool $normalize = false)
    {
        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.
     *
     * @return InputInterface
     */
    public function getInput()
    {
        return $this->input;
    }

    /**
     * Gets the output instance used by the last execution of the command or application.
     *
     * @return OutputInterface
     */
    public function getOutput()
    {
        return $this->output;
    }

    /**
     * Gets the status code returned by the last execution of the command or application.
     *
     * @return int
     *
     * @throws \RuntimeException If it's called before the execute method
     */
    public function getStatusCode()
    {
        if (null === $this->statusCode) {
            throw new \RuntimeException('Status code not initialized, did you execute the command before requesting the status code?');
        }

        return $this->statusCode;
    }

    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)
    {
        $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)
    {
        $this->captureStreamsIndependently = \array_key_exists('capture_stderr_separately', $options) && $options['capture_stderr_separately'];
        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->setAccessible(true);
            $strErrProperty->setValue($this->output, $errorOutput);

            $reflectedParent = $reflectedOutput->getParentClass();
            $streamProperty = $reflectedParent->getProperty('stream');
            $streamProperty->setAccessible(true);
            $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.\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\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;

    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 = [])
    {
        $prevShellVerbosity = getenv('SHELL_VERBOSITY');

        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);

            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) {
                if (\function_exists('putenv')) {
                    @putenv('SHELL_VERBOSITY');
                }
                unset($_ENV['SHELL_VERBOSITY']);
                unset($_SERVER['SHELL_VERBOSITY']);
            } else {
                if (\function_exists('putenv')) {
                    @putenv('SHELL_VERBOSITY='.$prevShellVerbosity);
                }
                $_ENV['SHELL_VERBOSITY'] = $prevShellVerbosity;
                $_SERVER['SHELL_VERBOSITY'] = $prevShellVerbosity;
            }
        }
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@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;

    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 = [])
    {
        // 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\Constraint;

use PHPUnit\Framework\Constraint\Constraint;
use Symfony\Component\Console\Command\Command;

final class CommandIsSuccessful extends Constraint
{
    /**
     * {@inheritdoc}
     */
    public function toString(): string
    {
        return 'is successful';
    }

    /**
     * {@inheritdoc}
     */
    protected function matches($other): bool
    {
        return Command::SUCCESS === $other;
    }

    /**
     * {@inheritdoc}
     */
    protected function failureDescription($other): string
    {
        return 'the command '.$this->toString();
    }

    /**
     * {@inheritdoc}
     */
    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;

    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()));
    }
}
# 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 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" "-S{{ VERSION }}")
    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
            completecmd+=("-i$w")
        fi
    done

    local sfcomplete
    if sfcomplete=$(${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 }}
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                                                                                                                                                  <?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@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 $logger;

    public function __construct(?LoggerInterface $logger = null)
    {
        $this->logger = $logger;
    }

    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()]);
    }

    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()
    {
        return [
            ConsoleEvents::ERROR => ['onConsoleError', -128],
            ConsoleEvents::TERMINATE => ['onConsoleTerminate', -128],
        ];
    }

    private static function getInputString(ConsoleEvent $event): ?string
    {
        $commandName = $event->getCommand() ? $event->getCommand()->getName() : null;
        $input = $event->getInput();

        if (method_exists($input, '__toString')) {
            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;

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\Helper;

use Symfony\Component\Console\Command\Command;
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, Helper>
 */
class HelperSet implements \IteratorAggregate
{
    /** @var array<string, Helper> */
    private $helpers = [];
    private $command;

    /**
     * @param Helper[] $helpers An array of helper
     */
    public function __construct(array $helpers = [])
    {
        foreach ($helpers as $alias => $helper) {
            $this->set($helper, \is_int($alias) ? null : $alias);
        }
    }

    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.
     *
     * @return bool
     */
    public function has(string $name)
    {
        return isset($this->helpers[$name]);
    }

    /**
     * Gets a helper value.
     *
     * @return HelperInterface
     *
     * @throws InvalidArgumentException if the helper is not defined
     */
    public function get(string $name)
    {
        if (!$this->has($name)) {
            throw new InvalidArgumentException(sprintf('The helper "%s" is not defined.', $name));
        }

        return $this->helpers[$name];
    }

    /**
     * @deprecated since Symfony 5.4
     */
    public function setCommand(?Command $command = null)
    {
        trigger_deprecation('symfony/console', '5.4', 'Method "%s()" is deprecated.', __METHOD__);

        $this->command = $command;
    }

    /**
     * Gets the command associated with this helper set.
     *
     * @return Command
     *
     * @deprecated since Symfony 5.4
     */
    public function getCommand()
    {
        trigger_deprecation('symfony/console', '5.4', 'Method "%s()" is deprecated.', __METHOD__);

        return $this->command;
    }

    /**
     * @return \Traversable<string, Helper>
     */
    #[\ReturnTypeWillChange]
    public function getIterator()
    {
        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\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;

    /**
     * {@inheritdoc}
     */
    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\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 $headerTitle;
    private $footerTitle;

    /**
     * Table headers.
     */
    private $headers = [];

    /**
     * Table rows.
     */
    private $rows = [];
    private $horizontal = false;

    /**
     * Column widths cache.
     */
    private $effectiveColumnWidths = [];

    /**
     * Number of columns cache.
     *
     * @var int
     */
    private $numberOfColumns;

    /**
     * @var OutputInterface
     */
    private $output;

    /**
     * @var TableStyle
     */
    private $style;

    /**
     * @var array
     */
    private $columnStyles = [];

    /**
     * User set column widths.
     *
     * @var array
     */
    private $columnWidths = [];
    private $columnMaxWidths = [];

    /**
     * @var array<string, TableStyle>|null
     */
    private static $styles;

    private $rendered = false;

    public function __construct(OutputInterface $output)
    {
        $this->output = $output;

        if (!self::$styles) {
            self::$styles = self::initStyles();
        }

        $this->setStyle('default');
    }

    /**
     * Sets a style definition.
     */
    public static function setStyleDefinition(string $name, TableStyle $style)
    {
        if (!self::$styles) {
            self::$styles = self::initStyles();
        }

        self::$styles[$name] = $style;
    }

    /**
     * Gets a style definition by name.
     *
     * @return TableStyle
     */
    public static function getStyleDefinition(string $name)
    {
        if (!self::$styles) {
            self::$styles = self::initStyles();
        }

        if (isset(self::$styles[$name])) {
            return self::$styles[$name];
        }

        throw new InvalidArgumentException(sprintf('Style "%s" is not defined.', $name));
    }

    /**
     * Sets table style.
     *
     * @param TableStyle|string $name The style name or a TableStyle instance
     *
     * @return $this
     */
    public function setStyle($name)
    {
        $this->style = $this->resolveStyle($name);

        return $this;
    }

    /**
     * Gets the current table style.
     *
     * @return TableStyle
     */
    public function getStyle()
    {
        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, $name)
    {
        $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.
     *
     * @return TableStyle
     */
    public function getColumnStyle(int $columnIndex)
    {
        return $this->columnStyles[$columnIndex] ?? $this->getStyle();
    }

    /**
     * Sets the minimum width of a column.
     *
     * @return $this
     */
    public function setColumnWidth(int $columnIndex, int $width)
    {
        $this->columnWidths[$columnIndex] = $width;

        return $this;
    }

    /**
     * Sets the minimum width of all columns.
     *
     * @return $this
     */
    public function setColumnWidths(array $widths)
    {
        $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): self
    {
        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)
    {
        $headers = array_values($headers);
        if (!empty($headers) && !\is_array($headers[0])) {
            $headers = [$headers];
        }

        $this->headers = $headers;

        return $this;
    }

    public function setRows(array $rows)
    {
        $this->rows = [];

        return $this->addRows($rows);
    }

    /**
     * @return $this
     */
    public function addRows(array $rows)
    {
        foreach ($rows as $row) {
            $this->addRow($row);
        }

        return $this;
    }

    /**
     * @return $this
     */
    public function addRow($row)
    {
        if ($row instanceof TableSeparator) {
            $this->rows[] = $row;

            return $this;
        }

        if (!\is_array($row)) {
            throw new InvalidArgumentException('A row must be an array or a TableSeparator instance.');
        }

        $this->rows[] = array_values($row);

        return $this;
    }

    /**
     * Adds a row to the table, and re-renders the table.
     *
     * @return $this
     */
    public function appendRow($row): self
    {
        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($column, array $row)
    {
        $this->rows[$column] = $row;

        return $this;
    }

    /**
     * @return $this
     */
    public function setHeaderTitle(?string $title): self
    {
        $this->headerTitle = $title;

        return $this;
    }

    /**
     * @return $this
     */
    public function setFooterTitle(?string $title): self
    {
        $this->footerTitle = $title;

        return $this;
    }

    /**
     * @return $this
     */
    public function setHorizontal(bool $horizontal = true): self
    {
        $this->horizontal = $horizontal;

        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 |
     *     +---------------+-----------------------+------------------+
     */
    public function render()
    {
        $divider = new TableSeparator();
        if ($this->horizontal) {
            $rows = [];
            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 ($rows[$i][0] instanceof TableCell && $rows[$i][0]->getColspan() >= 2) {
                        // Noop, there is a "title"
                    } else {
                        $rows[$i][] = null;
                    }
                }
            }
        } else {
            $rows = array_merge($this->headers, [$divider], $this->rows);
        }

        $this->calculateNumberOfColumns($rows);

        $rowGroups = $this->buildTableRows($rows);
        $this->calculateColumnsWidth($rowGroups);

        $isHeader = !$this->horizontal;
        $isFirstRow = $this->horizontal;
        $hasTitle = (bool) $this->headerTitle;

        foreach ($rowGroups as $rowGroup) {
            $isHeaderSeparatorRendered = false;

            foreach ($rowGroup as $row) {
                if ($divider === $row) {
                    $isHeader = false;
                    $isFirstRow = true;

                    continue;
                }

                if ($row instanceof TableSeparator) {
                    $this->renderRowSeparator();

                    continue;
                }

                if (!$row) {
                    continue;
                }

                if ($isHeader && !$isHeaderSeparatorRendered) {
                    $this->renderRowSeparator(
                        $isHeader ? self::SEPARATOR_TOP : self::SEPARATOR_TOP_BOTTOM,
                        $hasTitle ? $this->headerTitle : null,
                        $hasTitle ? $this->style->getHeaderTitleFormat() : null
                    );
                    $hasTitle = false;
                    $isHeaderSeparatorRendered = true;
                }

                if ($isFirstRow) {
                    $this->renderRowSeparator(
                        $isHeader ? self::SEPARATOR_TOP : self::SEPARATOR_TOP_BOTTOM,
                        $hasTitle ? $this->headerTitle : null,
                        $hasTitle ? $this->style->getHeaderTitleFormat() : null
                    );
                    $isFirstRow = false;
                    $hasTitle = false;
                }

                if ($this->horizontal) {
                    $this->renderRow($row, $this->style->getCellRowFormat(), $this->style->getCellHeaderFormat());
                } else {
                    $this->renderRow($row, $isHeader ? $this->style->getCellHeaderFormat() : $this->style->getCellRowFormat());
                }
            }
        }
        $this->renderRowSeparator(self::SEPARATOR_BOTTOM, $this->footerTitle, $this->style->getFooterTitleFormat());

        $this->cleanup();
        $this->rendered = true;
    }

    /**
     * Renders horizontal header separator.
     *
     * Example:
     *
     *     +-----+-----------+-------+
     */
    private function renderRowSeparator(int $type = self::SEPARATOR_MID, ?string $title = null, ?string $titleFormat = null)
    {
        if (0 === $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]];
        }

        $markup = $leftChar;
        for ($column = 0; $column < $count; ++$column) {
            $markup .= str_repeat($horizontal, $this->effectiveColumnWidths[$column]);
            $markup .= $column === $count - 1 ? $rightChar : $midChar;
        }

        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)
    {
        $rowContent = $this->renderColumnSeparator(self::BORDER_OUTSIDE);
        $columns = $this->getRowColumns($row);
        $last = \count($columns) - 1;
        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
        if (false !== $encoding = mb_detect_encoding($cell, null, true)) {
            $width += \strlen($cell) - mb_strwidth($cell, $encoding);
        }

        $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 (strstr($content, '</>')) {
                    $content = str_replace('</>', '', $content);
                    $width -= 3;
                }
                if (strstr($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)
    {
        $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;

                if (isset($this->columnMaxWidths[$column]) && Helper::width(Helper::removeDecoration($formatter, $cell)) > $this->columnMaxWidths[$column]) {
                    $cell = $formatter->formatAndWrap($cell, $this->columnMaxWidths[$column] * $colspan);
                }
                if (!strstr($cell ?? '', "\n")) {
                    continue;
                }
                $eol = str_contains($cell ?? '', "\r\n") ? "\r\n" : "\n";
                $escaped = implode($eol, array_map([OutputFormatter::class, '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 (\count($this->rows) > 0) {
            ++$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) && !(\is_object($cell) && method_exists($cell, '__toString'))) {
                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 (strstr($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)
    {
        $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)
    {
        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()
    {
        $this->effectiveColumnWidths = [];
        $this->numberOfColumns = null;
    }

    /**
     * @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($name): TableStyle
    {
        if ($name instanceof TableStyle) {
            return $name;
        }

        if (isset(self::$styles[$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;

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
{
    /**
     * {@inheritdoc}
     */
    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());
        }

        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, OutputFormatter::escape(implode(', ', $default)));

                break;

            case $question instanceof ChoiceQuestion:
                $choices = $question->getChoices();
                $text = sprintf(' <info>%s</info> [<comment>%s</comment>]:', $text, OutputFormatter::escape($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);
    }

    /**
     * {@inheritdoc}
     */
    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(): string
    {
        if ('Windows' === \PHP_OS_FAMILY) {
            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.
     *
     * @return string
     */
    public function formatSection(string $section, string $message, string $style = 'info')
    {
        return sprintf('<%s>[%s]</%s> %s', $style, $section, $style, $message);
    }

    /**
     * Formats a message as a block of text.
     *
     * @param string|array $messages The message to write in the block
     *
     * @return string
     */
    public function formatBlock($messages, string $style, bool $large = false)
    {
        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.
     *
     * @return string
     */
    public function truncate(string $message, int $length, string $suffix = '...')
    {
        $computedLength = $length - self::width($suffix);

        if ($computedLength > self::width($message)) {
            return $message;
        }

        return self::substr($message, 0, $length).$suffix;
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        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\Descriptor\DescriptorInterface;
use Symfony\Component\Console\Descriptor\JsonDescriptor;
use Symfony\Component\Console\Descriptor\MarkdownDescriptor;
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 $descriptors = [];

    public function __construct()
    {
        $this
            ->register('txt', new TextDescriptor())
            ->register('xml', new XmlDescriptor())
            ->register('json', new JsonDescriptor())
            ->register('md', new MarkdownDescriptor())
        ;
    }

    /**
     * Describes an object if supported.
     *
     * Available options are:
     * * format: string, the output format name
     * * raw_text: boolean, sets output type as raw
     *
     * @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)
    {
        $this->descriptors[$format] = $descriptor;

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        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;

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 $paddingChar = ' ';
    private $horizontalOutsideBorderChar = '-';
    private $horizontalInsideBorderChar = '-';
    private $verticalOutsideBorderChar = '|';
    private $verticalInsideBorderChar = '|';
    private $crossingChar = '+';
    private $crossingTopRightChar = '+';
    private $crossingTopMidChar = '+';
    private $crossingTopLeftChar = '+';
    private $crossingMidRightChar = '+';
    private $crossingBottomRightChar = '+';
    private $crossingBottomMidChar = '+';
    private $crossingBottomLeftChar = '+';
    private $crossingMidLeftChar = '+';
    private $crossingTopLeftBottomChar = '+';
    private $crossingTopMidBottomChar = '+';
    private $crossingTopRightBottomChar = '+';
    private $headerTitleFormat = '<fg=black;bg=white;options=bold> %s </>';
    private $footerTitleFormat = '<fg=black;bg=white;options=bold> %s </>';
    private $cellHeaderFormat = '<info>%s</info>';
    private $cellRowFormat = '%s';
    private $cellRowContentFormat = ' %s ';
    private $borderFormat = '%s';
    private $padType = \STR_PAD_RIGHT;

    /**
     * Sets padding character, used for cell padding.
     *
     * @return $this
     */
    public function setPaddingChar(string $paddingChar)
    {
        if (!$paddingChar) {
            throw new LogicException('The padding char must not be empty.');
        }

        $this->paddingChar = $paddingChar;

        return $this;
    }

    /**
     * Gets padding character, used for cell padding.
     *
     * @return string
     */
    public function getPaddingChar()
    {
        return $this->paddingChar;
    }

    /**
     * Sets horizontal 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 setHorizontalBorderChars(string $outside, ?string $inside = null): self
    {
        $this->horizontalOutsideBorderChar = $outside;
        $this->horizontalInsideBorderChar = $inside ?? $outside;

        return $this;
    }

    /**
     * Sets vertical 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 setVerticalBorderChars(string $outside, ?string $inside = null): self
    {
        $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): self
    {
        $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.
     *
     * @return string
     */
    public function getCrossingChar()
    {
        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)
    {
        $this->cellHeaderFormat = $cellHeaderFormat;

        return $this;
    }

    /**
     * Gets header cell format.
     *
     * @return string
     */
    public function getCellHeaderFormat()
    {
        return $this->cellHeaderFormat;
    }

    /**
     * Sets row cell format.
     *
     * @return $this
     */
    public function setCellRowFormat(string $cellRowFormat)
    {
        $this->cellRowFormat = $cellRowFormat;

        return $this;
    }

    /**
     * Gets row cell format.
     *
     * @return string
     */
    public function getCellRowFormat()
    {
        return $this->cellRowFormat;
    }

    /**
     * Sets row cell content format.
     *
     * @return $this
     */
    public function setCellRowContentFormat(string $cellRowContentFormat)
    {
        $this->cellRowContentFormat = $cellRowContentFormat;

        return $this;
    }

    /**
     * Gets row cell content format.
     *
     * @return string
     */
    public function getCellRowContentFormat()
    {
        return $this->cellRowContentFormat;
    }

    /**
     * Sets table border format.
     *
     * @return $this
     */
    public function setBorderFormat(string $borderFormat)
    {
        $this->borderFormat = $borderFormat;

        return $this;
    }

    /**
     * Gets table border format.
     *
     * @return string
     */
    public function getBorderFormat()
    {
        return $this->borderFormat;
    }

    /**
     * Sets cell padding type.
     *
     * @return $this
     */
    public function setPadType(int $padType)
    {
        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.
     *
     * @return int
     */
    public function getPadType()
    {
        return $this->padType;
    }

    public function getHeaderTitleFormat(): string
    {
        return $this->headerTitleFormat;
    }

    /**
     * @return $this
     */
    public function setHeaderTitleFormat(string $format): self
    {
        $this->headerTitleFormat = $format;

        return $this;
    }

    public function getFooterTitleFormat(): string
    {
        return $this->footerTitleFormat;
    }

    /**
     * @return $this
     */
    public function setFooterTitleFormat(string $format): self
    {
        $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;

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 $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()
    {
        return array_filter(
            $this->getOptions(),
            function ($key) {
                return \in_array($key, self::TAG_OPTIONS) && isset($this->options[$key]);
            },
            \ARRAY_FILTER_USE_KEY
        );
    }

    /**
     * @return int
     */
    public function getPadByAlign()
    {
        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\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 $output;
    private $dumper;
    private $cloner;
    private $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 ?? $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 ?? $this->cloner = new VarCloner())->cloneVar($var)->withRefHandles(false), true));
            };
        } else {
            $this->handler = function ($var): string {
                switch (true) {
                    case null === $var:
                        return 'null';
                    case true === $var:
                        return 'true';
                    case false === $var:
                        return 'false';
                    case \is_string($var):
                        return '"'.$var.'"';
                    default:
                        return rtrim(print_r($var, true));
                }
            };
        }
    }

    public function __invoke($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;

/**
 * HelperInterface is the interface all helpers must implement.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
interface HelperInterface
{
    /**
     * Sets the helper set associated with this helper.
     */
    public function setHelperSet(?HelperSet $helperSet = null);

    /**
     * Gets the helper set associated with this helper.
     *
     * @return HelperSet|null
     */
    public function getHelperSet();

    /**
     * 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;

/**
 * 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 $started = [];
    private $count = -1;

    /**
     * Starts a debug formatting session.
     *
     * @return string
     */
    public function start(string $id, string $message, string $prefix = 'RUN')
    {
        $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.
     *
     * @return string
     */
    public function progress(string $id, string $buffer, bool $error = false, string $prefix = 'OUT', string $errorPrefix = 'ERR')
    {
        $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.
     *
     * @return string
     */
    public function stop(string $id, string $message, bool $successful, string $prefix = 'RES')
    {
        $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']]);
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        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\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\LogicException;
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 $output;
    private $startTime;
    private $format;
    private $message;
    private $indicatorValues;
    private $indicatorCurrent;
    private $indicatorChangeInterval;
    private $indicatorUpdateTime;
    private $started = false;

    /**
     * @var array<string, callable>
     */
    private static $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;

        if (null === $format) {
            $format = $this->determineBestFormat();
        }

        if (null === $indicatorValues) {
            $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.
     */
    public function setMessage(?string $message)
    {
        $this->message = $message;

        $this->display();
    }

    /**
     * Starts the indicator output.
     */
    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.
     */
    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.
     */
    public function finish(string $message)
    {
        if (!$this->started) {
            throw new LogicException('Progress indicator has not yet been started.');
        }

        $this->message = $message;
        $this->display();
        $this->output->writeln('');
        $this->started = false;
    }

    /**
     * Gets the format for a given name.
     *
     * @return string|null
     */
    public static function getFormatDefinition(string $name)
    {
        return self::FORMATS[$name] ?? null;
    }

    /**
     * Sets a placeholder formatter for a given name.
     *
     * This method also allow you to override an existing placeholder.
     */
    public static function setPlaceholderFormatterDefinition(string $name, callable $callable)
    {
        if (!self::$formatters) {
            self::$formatters = self::initPlaceholderFormatters();
        }

        self::$formatters[$name] = $callable;
    }

    /**
     * Gets the placeholder formatter for a given name (including the delimiter char like %).
     *
     * @return callable|null
     */
    public static function getPlaceholderFormatterDefinition(string $name)
    {
        if (!self::$formatters) {
            self::$formatters = self::initPlaceholderFormatters();
        }

        return self::$formatters[$name] ?? null;
    }

    private function display()
    {
        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
    {
        switch ($this->output->getVerbosity()) {
            // OutputInterface::VERBOSITY_QUIET: display is disabled anyway
            case OutputInterface::VERBOSITY_VERBOSE:
                return $this->output->isDecorated() ? 'verbose' : 'verbose_no_ansi';
            case OutputInterface::VERBOSITY_VERY_VERBOSE:
            case OutputInterface::VERBOSITY_DEBUG:
                return $this->output->isDecorated() ? 'very_verbose' : 'very_verbose_no_ansi';
            default:
                return $this->output->isDecorated() ? 'normal' : 'normal_no_ansi';
        }
    }

    /**
     * Overwrites a previous message to the output.
     */
    private function overwrite(string $message)
    {
        if ($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);
    }

    private static function initPlaceholderFormatters(): array
    {
        return [
            'indicator' => function (self $indicator) {
                return $indicator->indicatorValues[$indicator->indicatorCurrent % \count($indicator->indicatorValues)];
            },
            'message' => function (self $indicator) {
                return $indicator->message;
            },
            'elapsed' => function (self $indicator) {
                return Helper::formatTime(time() - $indicator->startTime);
            },
            'memory' => function () {
                return 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;

/**
 * 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\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 $stty = true;
    private static $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)
    {
        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 = function () use ($output, $question) {
                return $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;
        }
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'question';
    }

    /**
     * Prevents usage of stty.
     */
    public static function disableStty()
    {
        self::$stty = false;
    }

    /**
     * Asks the question to the user.
     *
     * @return mixed
     *
     * @throws RuntimeException In case the fallback is deactivated and the response cannot be hidden
     */
    private function doAsk(OutputInterface $output, Question $question)
    {
        $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($ret);
        }

        $ret = \strlen($ret) > 0 ? $ret : $question->getDefault();

        if ($normalizer = $question->getNormalizer()) {
            return $normalizer($ret);
        }

        return $ret;
    }

    /**
     * @return mixed
     */
    private function getDefaultAnswer(Question $question)
    {
        $default = $question->getDefault();

        if (null === $default) {
            return $default;
        }

        if ($validator = $question->getValidator()) {
            return \call_user_func($question->getValidator(), $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.
     */
    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)
    {
        $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.
     */
    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);

        $sttyMode = shell_exec('stty -g');
        $isStdin = 'php://stdin' === (stream_get_meta_data($inputStream)['uri'] ?? null);
        $r = [$inputStream];
        $w = [];

        // 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)) {
            while ($isStdin && 0 === @stream_select($r, $w, $w, 0, 100)) {
                // Give signal handlers a chance to run
                $r = [$inputStream];
            }
            $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())) {
                shell_exec('stty '.$sttyMode);
                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 (\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(trim($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),
                            function ($match) use ($ret) {
                                return '' === $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(trim($this->mostRecentlyEnteredValue($fullChoice)));
                $output->write('<hl>'.OutputFormatter::escapeTrailingBackslash(substr($matches[$ofs], $charactersEntered)).'</hl>');
                $cursor->restorePosition();
            }
        }

        // Reset stty so it behaves normally again
        shell_exec('stty '.$sttyMode);

        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 ('phar:' === substr(__FILE__, 0, 5)) {
                $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;
        }

        if (self::$stty && Terminal::hasSttyAvailable()) {
            $sttyMode = shell_exec('stty -g');
            shell_exec('stty -echo');
        } elseif ($this->isInteractiveInput($inputStream)) {
            throw new RuntimeException('Unable to hide the response.');
        }

        $value = fgets($inputStream, 4096);

        if (self::$stty && Terminal::hasSttyAvailable()) {
            shell_exec('stty '.$sttyMode);
        }

        if (false === $value) {
            throw new MissingInputException('Aborted.');
        }
        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
     *
     * @return mixed The validated response
     *
     * @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)
    {
        $error = null;
        $attempts = $question->getMaxAttempts();

        while (null === $attempts || $attempts--) {
            if (null !== $error) {
                $this->writeError($output, $error);
            }

            try {
                return $question->getValidator()($interviewer());
            } 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 (null !== 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
     *
     * @return string|false The input received, false in case input could not be read
     */
    private function readInput($inputStream, Question $question)
    {
        if (!$question->isMultiline()) {
            $cp = $this->setIOCodepage();
            $ret = fgets($inputStream, 4096);

            return $this->resetIOCodepage($cp, $ret);
        }

        $multiLineStreamReader = $this->cloneInputStream($inputStream);
        if (null === $multiLineStreamReader) {
            return false;
        }

        $ret = '';
        $cp = $this->setIOCodepage();
        while (false !== ($char = fgetc($multiLineStreamReader))) {
            if (\PHP_EOL === "{$ret}{$char}") {
                break;
            }
            $ret .= $char;
        }

        return $this->resetIOCodepage($cp, $ret);
    }

    /**
     * Sets console I/O to the host code page.
     *
     * @return int Previous code page in IBM/EBCDIC format
     */
    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.
     *
     * @param string|false $input
     *
     * @return string|false
     */
    private function resetIOCodepage(int $cp, $input)
    {
        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;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@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 $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\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 = null;

    /**
     * {@inheritdoc}
     */
    public function setHelperSet(?HelperSet $helperSet = null)
    {
        $this->helperSet = $helperSet;
    }

    /**
     * {@inheritdoc}
     */
    public function getHelperSet()
    {
        return $this->helperSet;
    }

    /**
     * Returns the length of a string, using mb_strwidth if it is available.
     *
     * @deprecated since Symfony 5.3
     *
     * @return int
     */
    public static function strlen(?string $string)
    {
        trigger_deprecation('symfony/console', '5.3', 'Method "%s()" is deprecated and will be removed in Symfony 6.0. Use Helper::width() or Helper::length() instead.', __METHOD__);

        return self::width($string);
    }

    /**
     * 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 ?? $string = '';

        if (preg_match('//u', $string)) {
            return (new UnicodeString($string))->width(false);
        }

        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 ?? $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.
     *
     * @return string
     */
    public static function substr(?string $string, int $from, ?int $length = null)
    {
        $string ?? $string = '';

        if (false === $encoding = mb_detect_encoding($string, null, true)) {
            return substr($string, $from, $length);
        }

        return mb_substr($string, $from, $length, $encoding);
    }

    public static function formatTime($secs)
    {
        static $timeFormats = [
            [0, '< 1 sec'],
            [1, '1 sec'],
            [2, 'secs', 1],
            [60, '1 min'],
            [120, 'mins', 60],
            [3600, '1 hr'],
            [7200, 'hrs', 3600],
            [86400, '1 day'],
            [172800, 'days', 86400],
        ];

        foreach ($timeFormats as $index => $format) {
            if ($secs >= $format[0]) {
                if ((isset($timeFormats[$index + 1]) && $secs < $timeFormats[$index + 1][0])
                    || $index == \count($timeFormats) - 1
                ) {
                    if (2 == \count($format)) {
                        return $format[1];
                    }

                    return floor($secs / $format[2]).' '.$format[1];
                }
            }
        }
    }

    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);
    }

    /**
     * @deprecated since Symfony 5.3
     */
    public static function strlenWithoutDecoration(OutputFormatterInterface $formatter, ?string $string)
    {
        trigger_deprecation('symfony/console', '5.3', 'Method "%s()" is deprecated and will be removed in Symfony 6.0. Use Helper::removeDecoration() instead.', __METHOD__);

        return self::width(self::removeDecoration($formatter, $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;

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 $barWidth = 28;
    private $barChar;
    private $emptyBarChar = '-';
    private $progressChar = '>';
    private $format;
    private $internalFormat;
    private $redrawFreq = 1;
    private $writeCount;
    private $lastWriteTime;
    private $minSecondsBetweenRedraws = 0;
    private $maxSecondsBetweenRedraws = 1;
    private $output;
    private $step = 0;
    private $max;
    private $startTime;
    private $stepWidth;
    private $percent = 0.0;
    private $messages = [];
    private $overwrite = true;
    private $terminal;
    private $previousMessage;
    private $cursor;

    private static $formatters;
    private static $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.
     *
     * This method also allow you to override an existing placeholder.
     *
     * @param string   $name     The placeholder name (including the delimiter char like %)
     * @param callable $callable A PHP callable
     */
    public static function setPlaceholderFormatterDefinition(string $name, callable $callable): void
    {
        if (!self::$formatters) {
            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
    {
        if (!self::$formatters) {
            self::$formatters = self::initPlaceholderFormatters();
        }

        return self::$formatters[$name] ?? null;
    }

    /**
     * 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
    {
        if (!self::$formats) {
            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
    {
        if (!self::$formats) {
            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')
    {
        $this->messages[$name] = $message;
    }

    /**
     * @return string|null
     */
    public function getMessage(string $name = 'message')
    {
        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 (!$this->step) {
            return 0;
        }

        return round((time() - $this->startTime) / $this->step * $this->max);
    }

    public function getRemaining(): float
    {
        if (!$this->step) {
            return 0;
        }

        return round((time() - $this->startTime) / $this->step * ($this->max - $this->step));
    }

    public function setBarWidth(int $size)
    {
        $this->barWidth = max(1, $size);
    }

    public function getBarWidth(): int
    {
        return $this->barWidth;
    }

    public function setBarCharacter(string $char)
    {
        $this->barChar = $char;
    }

    public function getBarCharacter(): string
    {
        return $this->barChar ?? ($this->max ? '=' : $this->emptyBarChar);
    }

    public function setEmptyBarCharacter(string $char)
    {
        $this->emptyBarChar = $char;
    }

    public function getEmptyBarCharacter(): string
    {
        return $this->emptyBarChar;
    }

    public function setProgressCharacter(string $char)
    {
        $this->progressChar = $char;
    }

    public function getProgressCharacter(): string
    {
        return $this->progressChar;
    }

    public function setFormat(string $format)
    {
        $this->format = null;
        $this->internalFormat = $format;
    }

    /**
     * Sets the redraw frequency.
     *
     * @param int|null $freq The frequency in steps
     */
    public function setRedrawFrequency(?int $freq)
    {
        $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.
     *
     * @param int|null $max Number of steps to complete the bar (0 if indeterminate), if null it will be inferred from $iterable
     */
    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
     */
    public function start(?int $max = null)
    {
        $this->startTime = time();
        $this->step = 0;
        $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)
    {
        $this->setProgress($this->step + $step);
    }

    /**
     * Sets whether to overwrite the progressbar, false for new line.
     */
    public function setOverwrite(bool $overwrite)
    {
        $this->overwrite = $overwrite;
    }

    public function setProgress(int $step)
    {
        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)
    {
        $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)
    {
        // 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);
                    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
    {
        switch ($this->output->getVerbosity()) {
            // OutputInterface::VERBOSITY_QUIET: display is disabled anyway
            case OutputInterface::VERBOSITY_VERBOSE:
                return $this->max ? self::FORMAT_VERBOSE : self::FORMAT_VERBOSE_NOMAX;
            case OutputInterface::VERBOSITY_VERY_VERBOSE:
                return $this->max ? self::FORMAT_VERY_VERBOSE : self::FORMAT_VERY_VERBOSE_NOMAX;
            case OutputInterface::VERBOSITY_DEBUG:
                return $this->max ? self::FORMAT_DEBUG : self::FORMAT_DEBUG_NOMAX;
            default:
                return $this->max ? self::FORMAT_NORMAL : self::FORMAT_NORMAL_NOMAX;
        }
    }

    private static function initPlaceholderFormatters(): array
    {
        return [
            'bar' => 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(), $emptyBars);
                }

                return $display;
            },
            'elapsed' => function (self $bar) {
                return Helper::formatTime(time() - $bar->getStartTime());
            },
            'remaining' => 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());
            },
            'estimated' => 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());
            },
            'memory' => function (self $bar) {
                return Helper::formatMemory(memory_get_usage(true));
            },
            'current' => function (self $bar) {
                return str_pad($bar->getProgress(), $bar->getStepWidth(), ' ', \STR_PAD_LEFT);
            },
            'max' => function (self $bar) {
                return $bar->getMaxSteps();
            },
            'percent' => function (self $bar) {
                return 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
    {
        $regex = "{%([a-z\-_]+)(?:\:([^%]+))?%}i";
        $callback = function ($matches) {
            if ($formatter = $this::getPlaceholderFormatterDefinition($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(function ($subLine) {
            return 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\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, $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_array($cmd)) {
            throw new \TypeError(sprintf('The "command" argument of "%s()" must be an array or a "%s" instance, "%s" given.', __METHOD__, Process::class, get_debug_type($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, $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);
    }

    /**
     * {@inheritdoc}
     */
    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;

/**
 * @author Abdellatif Ait boudad <a.aitboudad@gmail.com>
 */
class TableCell
{
    private $value;
    private $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.
     *
     * @return string
     */
    public function __toString()
    {
        return $this->value;
    }

    /**
     * Gets number of colspan.
     *
     * @return int
     */
    public function getColspan()
    {
        return (int) $this->options['colspan'];
    }

    /**
     * Gets number of rowspan.
     *
     * @return int
     */
    public function getRowspan()
    {
        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\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 $error;
    private $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->setAccessible(true);
        $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;

/**
 * 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 $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;

/**
 * Allows to manipulate the exit code of a command after its execution.
 *
 * @author Francesco Levorato <git@flevour.net>
 */
final class ConsoleTerminateEvent extends ConsoleEvent
{
    private $exitCode;

    public function __construct(Command $command, InputInterface $input, OutputInterface $output, int $exitCode)
    {
        parent::__construct($command, $input, $output);

        $this->setExitCode($exitCode);
    }

    public function setExitCode(int $exitCode): void
    {
        $this->exitCode = $exitCode;
    }

    public function getExitCode(): int
    {
        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\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 $input;
    private $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.
     *
     * @return Command|null
     */
    public function getCommand()
    {
        return $this->command;
    }

    /**
     * Gets the input instance.
     *
     * @return InputInterface
     */
    public function getInput()
    {
        return $this->input;
    }

    /**
     * Gets the output instance.
     *
     * @return OutputInterface
     */
    public function getOutput()
    {
        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;

/**
 * @author marie <marie@users.noreply.github.com>
 */
final class ConsoleSignalEvent extends ConsoleEvent
{
    private $handlingSignal;

    public function __construct(Command $command, InputInterface $input, OutputInterface $output, int $handlingSignal)
    {
        parent::__construct($command, $input, $output);
        $this->handlingSignal = $handlingSignal;
    }

    public function getHandlingSignal(): int
    {
        return $this->handlingSignal;
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@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\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\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 $commands = [];
    private $wantHelps = false;
    private $runningCommand;
    private $name;
    private $version;
    private $commandLoader;
    private $catchExceptions = true;
    private $autoExit = true;
    private $definition;
    private $helperSet;
    private $dispatcher;
    private $terminal;
    private $defaultCommand;
    private $singleCommand = false;
    private $initialized;
    private $signalRegistry;
    private $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)
    {
        $this->dispatcher = $dispatcher;
    }

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

    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)
    {
        if (\function_exists('putenv')) {
            @putenv('LINES='.$this->terminal->getHeight());
            @putenv('COLUMNS='.$this->terminal->getWidth());
        }

        if (null === $input) {
            $input = new ArgvInput();
        }

        if (null === $output) {
            $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 (\Exception $e) {
            if (!$this->catchExceptions) {
                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 $e) {
            // 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()) {
                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();
                }

                throw $e;
            }

            $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);
        }

        if ($command instanceof LazyCommand) {
            $command = $command->getCommand();
        }

        $this->runningCommand = $command;
        $exitCode = $this->doRunCommand($command, $input, $output);
        $this->runningCommand = null;

        return $exitCode;
    }

    /**
     * {@inheritdoc}
     */
    public function reset()
    {
    }

    public function setHelperSet(HelperSet $helperSet)
    {
        $this->helperSet = $helperSet;
    }

    /**
     * Get the helper set associated with the command.
     *
     * @return HelperSet
     */
    public function getHelperSet()
    {
        if (!$this->helperSet) {
            $this->helperSet = $this->getDefaultHelperSet();
        }

        return $this->helperSet;
    }

    public function setDefinition(InputDefinition $definition)
    {
        $this->definition = $definition;
    }

    /**
     * Gets the InputDefinition related to this Application.
     *
     * @return InputDefinition
     */
    public function getDefinition()
    {
        if (!$this->definition) {
            $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()
        ) {
            $commandNames = [];
            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;
                }
                $commandNames[] = $command->getName();
                foreach ($command->getAliases() as $name) {
                    $commandNames[] = $name;
                }
            }
            $suggestions->suggestValues(array_filter($commandNames));

            return;
        }

        if (CompletionInput::TYPE_OPTION_NAME === $input->getCompletionType()) {
            $suggestions->suggestOptions($this->getDefinition()->getOptions());

            return;
        }
    }

    /**
     * Gets the help message.
     *
     * @return string
     */
    public function getHelp()
    {
        return $this->getLongVersion();
    }

    /**
     * Gets whether to catch exceptions or not during commands execution.
     *
     * @return bool
     */
    public function areExceptionsCaught()
    {
        return $this->catchExceptions;
    }

    /**
     * Sets whether to catch exceptions or not during commands execution.
     */
    public function setCatchExceptions(bool $boolean)
    {
        $this->catchExceptions = $boolean;
    }

    /**
     * Gets whether to automatically exit after a command execution or not.
     *
     * @return bool
     */
    public function isAutoExitEnabled()
    {
        return $this->autoExit;
    }

    /**
     * Sets whether to automatically exit after a command execution or not.
     */
    public function setAutoExit(bool $boolean)
    {
        $this->autoExit = $boolean;
    }

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

    /**
     * Sets the application name.
     **/
    public function setName(string $name)
    {
        $this->name = $name;
    }

    /**
     * Gets the application version.
     *
     * @return string
     */
    public function getVersion()
    {
        return $this->version;
    }

    /**
     * Sets the application version.
     */
    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.
     *
     * @return Command
     */
    public function register(string $name)
    {
        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
     */
    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.
     *
     * @return bool
     */
    public function has(string $name)
    {
        $this->init();

        return isset($this->commands[$name]) || ($this->commandLoader && $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()
    {
        $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.
     *
     * @return string
     *
     * @throws NamespaceNotFoundException When namespace is incorrect or ambiguous
     */
    public function findNamespace(string $namespace)
    {
        $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)) {
                // remove hidden commands
                $alternatives = array_filter($alternatives, function ($name) {
                    return !$this->get($name)->isHidden();
                });

                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 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);
            }));
        }

        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(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 = $this->get(reset($commands));

        if ($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)
    {
        $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]++/', function ($m) {
                    return 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.
     */
    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);
            }
        }

        if ($this->signalsToDispatchEvent) {
            $commandSignals = $command instanceof SignalableCommandInterface ? $command->getSubscribedSignals() : [];

            if ($commandSignals || null !== $this->dispatcher) {
                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.');
                }

                if (Terminal::hasSttyAvailable()) {
                    $sttyMode = shell_exec('stty -g');

                    foreach ([\SIGINT, \SIGTERM] as $signal) {
                        $this->signalRegistry->register($signal, static function () use ($sttyMode) {
                            shell_exec('stty '.$sttyMode);
                        });
                    }
                }
            }

            if (null !== $this->dispatcher) {
                foreach ($this->signalsToDispatchEvent as $signal) {
                    $event = new ConsoleSignalEvent($command, $input, $output, $signal);

                    $this->signalRegistry->register($signal, function ($signal, $hasNext) use ($event) {
                        $this->dispatcher->dispatch($event, ConsoleEvents::SIGNAL);

                        // No more handlers, we try to simulate PHP default behavior
                        if (!$hasNext) {
                            if (!\in_array($signal, [\SIGUSR1, \SIGUSR2], true)) {
                                exit(0);
                            }
                        }
                    });
                }
            }

            foreach ($commandSignals as $signal) {
                $this->signalRegistry->register($signal, [$command, 'handleSignal']);
            }
        }

        if (null === $this->dispatcher) {
            return $command->run($input, $output);
        }

        // bind before the console.command event, so the listeners have access to input options/arguments
        try {
            $command->mergeApplicationDefinition();
            $input->bind($command->getDefinition());
        } catch (ExceptionInterface $e) {
            // 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;
            }
        }

        $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.
     *
     * @return string|null
     */
    protected function getCommandName(InputInterface $input)
    {
        return $this->singleCommand ? $this->defaultCommand : $input->getFirstArgument();
    }

    /**
     * Gets the default input definition.
     *
     * @return InputDefinition
     */
    protected function getDefaultInputDefinition()
    {
        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()
    {
        return [new HelpCommand(), new ListCommand(), new CompleteCommand(), new DumpCompletionCommand()];
    }

    /**
     * Gets the default helper set with the helpers that should always be available.
     *
     * @return HelperSet
     */
    protected function getDefaultHelperSet()
    {
        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.
     *
     * @return string
     */
    public function extractNamespace(string $name, ?int $limit = null)
    {
        $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, function ($lev) use ($threshold) { return $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)
    {
        $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 (mb_strwidth($line.$char, 'utf8') <= $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;

        mb_convert_variables($encoding, 'utf8', $lines);

        return $lines;
    }

    /**
     * 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()
    {
        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\Completion;

/**
 * Represents a single suggested value.
 *
 * @author Wouter de Jong <wouter@wouterj.nl>
 */
class Suggestion
{
    private $value;

    public function __construct(string $value)
    {
        $this->value = $value;
    }

    public function getValue(): string
    {
        return $this->value;
    }

    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;

/**
 * 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\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;

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 $tokens;
    private $currentIndex;
    private $completionType;
    private $completionName = null;
    private $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;
    }

    /**
     * {@inheritdoc}
     */
    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 (null !== $option && $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 (null !== $previousOption && $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
     *
     * @return string One of self::TYPE_* constants. TYPE_OPTION_NAME and TYPE_NONE are already implemented by the Console component
     */
    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 $e) {
            // 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;

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 $valueSuggestions = [];
    private $optionSuggestions = [];

    /**
     * Add a suggested value for an input option or argument.
     *
     * @param string|Suggestion $value
     *
     * @return $this
     */
    public function suggestValue($value): self
    {
        $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): self
    {
        foreach ($values as $value) {
            $this->suggestValue($value);
        }

        return $this;
    }

    /**
     * Add a suggestion for an input option name.
     *
     * @return $this
     */
    public function suggestOption(InputOption $option): self
    {
        $this->optionSuggestions[] = $option;

        return $this;
    }

    /**
     * Add multiple suggestions for input option names at once.
     *
     * @param InputOption[] $options
     *
     * @return $this
     */
    public function suggestOptions(array $options): self
    {
        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\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
{
    /**
     * @var OutputInterface
     */
    protected $output;

    /**
     * {@inheritdoc}
     */
    public function describe(OutputInterface $output, object $object, array $options = [])
    {
        $this->output = $output;

        switch (true) {
            case $object instanceof InputArgument:
                $this->describeInputArgument($object, $options);
                break;
            case $object instanceof InputOption:
                $this->describeInputOption($object, $options);
                break;
            case $object instanceof InputDefinition:
                $this->describeInputDefinition($object, $options);
                break;
            case $object instanceof Command:
                $this->describeCommand($object, $options);
                break;
            case $object instanceof Application:
                $this->describeApplication($object, $options);
                break;
            default:
                throw new InvalidArgumentException(sprintf('Object of type "%s" is not describable.', get_debug_type($object)));
        }
    }

    /**
     * Writes content to output.
     */
    protected function write(string $content, bool $decorated = false)
    {
        $this->output->write($content, false, $decorated ? OutputInterface::OUTPUT_NORMAL : OutputInterface::OUTPUT_RAW);
    }

    /**
     * Describes an InputArgument instance.
     */
    abstract protected function describeInputArgument(InputArgument $argument, array $options = []);

    /**
     * Describes an InputOption instance.
     */
    abstract protected function describeInputOption(InputOption $option, array $options = []);

    /**
     * Describes an InputDefinition instance.
     */
    abstract protected function describeInputDefinition(InputDefinition $definition, array $options = []);

    /**
     * Describes a Command instance.
     */
    abstract protected function describeCommand(Command $command, array $options = []);

    /**
     * Describes an Application instance.
     */
    abstract protected function describeApplication(Application $application, 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\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
{
    /**
     * {@inheritdoc}
     */
    protected function describeInputArgument(InputArgument $argument, array $options = [])
    {
        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 - \strlen($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);
    }

    /**
     * {@inheritdoc}
     */
    protected function describeInputOption(InputOption $option, array $options = [])
    {
        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);
    }

    /**
     * {@inheritdoc}
     */
    protected function describeInputDefinition(InputDefinition $definition, array $options = [])
    {
        $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]));
            }
        }
    }

    /**
     * {@inheritdoc}
     */
    protected function describeCommand(Command $command, array $options = [])
    {
        $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");
        }
    }

    /**
     * {@inheritdoc}
     */
    protected function describeApplication(Application $application, array $options = [])
    {
        $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(function ($namespace) use ($commands) {
                return 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'], function ($name) use ($commands) {
                    return 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");
        }
    }

    /**
     * {@inheritdoc}
     */
    private function writeText(string $content, array $options = [])
    {
        $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.
     *
     * @param mixed $default
     */
    private function formatDefaultValue($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
{
    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;

/**
 * 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;
    }

    /**
     * {@inheritdoc}
     */
    protected function describeInputArgument(InputArgument $argument, array $options = [])
    {
        $this->writeDocument($this->getInputArgumentDocument($argument));
    }

    /**
     * {@inheritdoc}
     */
    protected function describeInputOption(InputOption $option, array $options = [])
    {
        $this->writeDocument($this->getInputOptionDocument($option));
    }

    /**
     * {@inheritdoc}
     */
    protected function describeInputDefinition(InputDefinition $definition, array $options = [])
    {
        $this->writeDocument($this->getInputDefinitionDocument($definition));
    }

    /**
     * {@inheritdoc}
     */
    protected function describeCommand(Command $command, array $options = [])
    {
        $this->writeDocument($this->getCommandDocument($command, $options['short'] ?? false));
    }

    /**
     * {@inheritdoc}
     */
    protected function describeApplication(Application $application, array $options = [])
    {
        $this->writeDocument($this->getApplicationDocument($application, $options['namespace'] ?? null, $options['short'] ?? false));
    }

    /**
     * Appends document children to parent node.
     */
    private function appendDocument(\DOMNode $parentNode, \DOMNode $importedParent)
    {
        foreach ($importedParent->childNodes as $childNode) {
            $parentNode->appendChild($parentNode->ownerDocument->importNode($childNode, true));
        }
    }

    /**
     * Writes DOM document.
     */
    private function writeDocument(\DOMDocument $dom)
    {
        $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\CommandNotFoundException;

/**
 * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
 *
 * @internal
 */
class ApplicationDescription
{
    public const GLOBAL_NAMESPACE = '_global';

    private $application;
    private $namespace;
    private $showHidden;

    /**
     * @var array
     */
    private $namespaces;

    /**
     * @var array<string, Command>
     */
    private $commands;

    /**
     * @var array<string, Command>
     */
    private $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 (null === $this->namespaces) {
            $this->inspectApplication();
        }

        return $this->namespaces;
    }

    /**
     * @return Command[]
     */
    public function getCommands(): array
    {
        if (null === $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()
    {
        $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
{
    /**
     * {@inheritdoc}
     */
    public function describe(OutputInterface $output, object $object, array $options = [])
    {
        $decorated = $output->isDecorated();
        $output->setDecorated(false);

        parent::describe($output, $object, $options);

        $output->setDecorated($decorated);
    }

    /**
     * {@inheritdoc}
     */
    protected function write(string $content, bool $decorated = true)
    {
        parent::write($content, $decorated);
    }

    /**
     * {@inheritdoc}
     */
    protected function describeInputArgument(InputArgument $argument, array $options = [])
    {
        $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)).'`'
        );
    }

    /**
     * {@inheritdoc}
     */
    protected function describeInputOption(InputOption $option, array $options = [])
    {
        $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)).'`'
        );
    }

    /**
     * {@inheritdoc}
     */
    protected function describeInputDefinition(InputDefinition $definition, array $options = [])
    {
        if ($showArguments = \count($definition->getArguments()) > 0) {
            $this->write('### Arguments');
            foreach ($definition->getArguments() as $argument) {
                $this->write("\n\n");
                if (null !== $describeInputArgument = $this->describeInputArgument($argument)) {
                    $this->write($describeInputArgument);
                }
            }
        }

        if (\count($definition->getOptions()) > 0) {
            if ($showArguments) {
                $this->write("\n\n");
            }

            $this->write('### Options');
            foreach ($definition->getOptions() as $option) {
                $this->write("\n\n");
                if (null !== $describeInputOption = $this->describeInputOption($option)) {
                    $this->write($describeInputOption);
                }
            }
        }
    }

    /**
     * {@inheritdoc}
     */
    protected function describeCommand(Command $command, array $options = [])
    {
        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(), function ($carry, $usage) {
                    return $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()), function ($carry, $usage) {
                return $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);
        }
    }

    /**
     * {@inheritdoc}
     */
    protected function describeApplication(Application $application, array $options = [])
    {
        $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(function ($commandName) use ($description) {
                return sprintf('* [`%s`](#%s)', $commandName, str_replace(':', '', $description->getCommand($commandName)->getName()));
            }, $namespace['commands'])));
        }

        foreach ($description->getCommands() as $command) {
            $this->write("\n\n");
            if (null !== $describeCommand = $this->describeCommand($command, $options)) {
                $this->write($describeCommand);
            }
        }
    }

    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\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
{
    /**
     * {@inheritdoc}
     */
    protected function describeInputArgument(InputArgument $argument, array $options = [])
    {
        $this->writeData($this->getInputArgumentData($argument), $options);
    }

    /**
     * {@inheritdoc}
     */
    protected function describeInputOption(InputOption $option, array $options = [])
    {
        $this->writeData($this->getInputOptionData($option), $options);
        if ($option->isNegatable()) {
            $this->writeData($this->getInputOptionData($option, true), $options);
        }
    }

    /**
     * {@inheritdoc}
     */
    protected function describeInputDefinition(InputDefinition $definition, array $options = [])
    {
        $this->writeData($this->getInputDefinitionData($definition), $options);
    }

    /**
     * {@inheritdoc}
     */
    protected function describeCommand(Command $command, array $options = [])
    {
        $this->writeData($this->getCommandData($command, $options['short'] ?? false), $options);
    }

    /**
     * {@inheritdoc}
     */
    protected function describeApplication(Application $application, array $options = [])
    {
        $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)
    {
        $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' => false,
        ] : [
            '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\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 string|iterable $messages The message as an iterable of strings or a single string
     * @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
     */
    public function write($messages, bool $newline = false, int $options = 0);

    /**
     * Writes a message to the output and adds a newline at the end.
     *
     * @param string|iterable $messages The message as an iterable of strings or a single string
     * @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
     */
    public function writeln($messages, int $options = 0);

    /**
     * Sets the verbosity of the output.
     */
    public function setVerbosity(int $level);

    /**
     * Gets the current verbosity of the output.
     *
     * @return int
     */
    public function getVerbosity();

    /**
     * Returns whether verbosity is quiet (-q).
     *
     * @return bool
     */
    public function isQuiet();

    /**
     * Returns whether verbosity is verbose (-v).
     *
     * @return bool
     */
    public function isVerbose();

    /**
     * Returns whether verbosity is very verbose (-vv).
     *
     * @return bool
     */
    public function isVeryVerbose();

    /**
     * Returns whether verbosity is debug (-vvv).
     *
     * @return bool
     */
    public function isDebug();

    /**
     * Sets the decorated flag.
     */
    public function setDecorated(bool $decorated);

    /**
     * Gets the decorated flag.
     *
     * @return bool
     */
    public function isDecorated();

    public function setFormatter(OutputFormatterInterface $formatter);

    /**
     * Returns current output formatter instance.
     *
     * @return OutputFormatterInterface
     */
    public function getFormatter();
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@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
{
    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;

        if (null === $decorated) {
            $decorated = $this->hasColorSupport();
        }

        parent::__construct($verbosity, $decorated, $formatter);
    }

    /**
     * Gets the stream attached to this StreamOutput instance.
     *
     * @return resource
     */
    public function getStream()
    {
        return $this->stream;
    }

    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()
    {
        // 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 $maxLength;
    private $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.
     *
     * @return string
     */
    public function fetch()
    {
        $content = $this->buffer;
        $this->buffer = '';

        return $content;
    }

    /**
     * {@inheritdoc}
     */
    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;

/**
 * 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 $stderr;
    private $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);

        if (null === $formatter) {
            // for BC reasons, stdErr has it own Formatter only when user don't inject a specific formatter.
            $this->stderr = new StreamOutput($this->openErrorStream(), $verbosity, $decorated);

            return;
        }

        $actualDecorated = $this->isDecorated();
        $this->stderr = new StreamOutput($this->openErrorStream(), $verbosity, $decorated, $this->getFormatter());

        if (null === $decorated) {
            $this->setDecorated($actualDecorated && $this->stderr->isDecorated());
        }
    }

    /**
     * Creates a new output section.
     */
    public function section(): ConsoleSectionOutput
    {
        return new ConsoleSectionOutput($this->getStream(), $this->consoleSectionOutputs, $this->getVerbosity(), $this->isDecorated(), $this->getFormatter());
    }

    /**
     * {@inheritdoc}
     */
    public function setDecorated(bool $decorated)
    {
        parent::setDecorated($decorated);
        $this->stderr->setDecorated($decorated);
    }

    /**
     * {@inheritdoc}
     */
    public function setFormatter(OutputFormatterInterface $formatter)
    {
        parent::setFormatter($formatter);
        $this->stderr->setFormatter($formatter);
    }

    /**
     * {@inheritdoc}
     */
    public function setVerbosity(int $level)
    {
        parent::setVerbosity($level);
        $this->stderr->setVerbosity($level);
    }

    /**
     * {@inheritdoc}
     */
    public function getErrorOutput()
    {
        return $this->stderr;
    }

    /**
     * {@inheritdoc}
     */
    public function setErrorOutput(OutputInterface $error)
    {
        $this->stderr = $error;
    }

    /**
     * Returns true if current environment supports writing console output to
     * STDOUT.
     *
     * @return bool
     */
    protected function hasStdoutSupport()
    {
        return false === $this->isRunningOS400();
    }

    /**
     * Returns true if current environment supports writing console output to
     * STDERR.
     *
     * @return bool
     */
    protected function hasStderrSupport()
    {
        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()
    {
        if (!$this->hasStdoutSupport()) {
            return fopen('php://output', 'w');
        }

        // Use STDOUT when possible to prevent from opening too many file descriptors
        return \defined('STDOUT') ? \STDOUT : (@fopen('php://stdout', 'w') ?: fopen('php://output', 'w'));
    }

    /**
     * @return resource
     */
    private function openErrorStream()
    {
        if (!$this->hasStderrSupport()) {
            return fopen('php://output', 'w');
        }

        // Use STDERR when possible to prevent from opening too many file descriptors
        return \defined('STDERR') ? \STDERR : (@fopen('php://stderr', 'w') ?: fopen('php://output', 'w'));
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@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 $formatter;

    /**
     * {@inheritdoc}
     */
    public function setFormatter(OutputFormatterInterface $formatter)
    {
        // do nothing
    }

    /**
     * {@inheritdoc}
     */
    public function getFormatter()
    {
        if ($this->formatter) {
            return $this->formatter;
        }
        // to comply with the interface we must return a OutputFormatterInterface
        return $this->formatter = new NullOutputFormatter();
    }

    /**
     * {@inheritdoc}
     */
    public function setDecorated(bool $decorated)
    {
        // do nothing
    }

    /**
     * {@inheritdoc}
     */
    public function isDecorated()
    {
        return false;
    }

    /**
     * {@inheritdoc}
     */
    public function setVerbosity(int $level)
    {
        // do nothing
    }

    /**
     * {@inheritdoc}
     */
    public function getVerbosity()
    {
        return self::VERBOSITY_QUIET;
    }

    /**
     * {@inheritdoc}
     */
    public function isQuiet()
    {
        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function isVerbose()
    {
        return false;
    }

    /**
     * {@inheritdoc}
     */
    public function isVeryVerbose()
    {
        return false;
    }

    /**
     * {@inheritdoc}
     */
    public function isDebug()
    {
        return false;
    }

    /**
     * {@inheritdoc}
     */
    public function writeln($messages, int $options = self::OUTPUT_NORMAL)
    {
        // do nothing
    }

    /**
     * {@inheritdoc}
     */
    public function write($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;

/**
 * @author Jean-François Simon <contact@jfsimon.fr>
 */
class BufferedOutput extends Output
{
    private $buffer = '';

    /**
     * Empties buffer and returns its content.
     *
     * @return string
     */
    public function fetch()
    {
        $content = $this->buffer;
        $this->buffer = '';

        return $content;
    }

    /**
     * {@inheritdoc}
     */
    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;

/**
 * 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.
     *
     * @return OutputInterface
     */
    public function getErrorOutput();

    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\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 $verbosity;
    private $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);
    }

    /**
     * {@inheritdoc}
     */
    public function setFormatter(OutputFormatterInterface $formatter)
    {
        $this->formatter = $formatter;
    }

    /**
     * {@inheritdoc}
     */
    public function getFormatter()
    {
        return $this->formatter;
    }

    /**
     * {@inheritdoc}
     */
    public function setDecorated(bool $decorated)
    {
        $this->formatter->setDecorated($decorated);
    }

    /**
     * {@inheritdoc}
     */
    public function isDecorated()
    {
        return $this->formatter->isDecorated();
    }

    /**
     * {@inheritdoc}
     */
    public function setVerbosity(int $level)
    {
        $this->verbosity = $level;
    }

    /**
     * {@inheritdoc}
     */
    public function getVerbosity()
    {
        return $this->verbosity;
    }

    /**
     * {@inheritdoc}
     */
    public function isQuiet()
    {
        return self::VERBOSITY_QUIET === $this->verbosity;
    }

    /**
     * {@inheritdoc}
     */
    public function isVerbose()
    {
        return self::VERBOSITY_VERBOSE <= $this->verbosity;
    }

    /**
     * {@inheritdoc}
     */
    public function isVeryVerbose()
    {
        return self::VERBOSITY_VERY_VERBOSE <= $this->verbosity;
    }

    /**
     * {@inheritdoc}
     */
    public function isDebug()
    {
        return self::VERBOSITY_DEBUG <= $this->verbosity;
    }

    /**
     * {@inheritdoc}
     */
    public function writeln($messages, int $options = self::OUTPUT_NORMAL)
    {
        $this->write($messages, true, $options);
    }

    /**
     * {@inheritdoc}
     */
    public function write($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.
     */
    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\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 $content = [];
    private $lines = 0;
    private $sections;
    private $terminal;

    /**
     * @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();
    }

    /**
     * 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
     */
    public function clear(?int $lines = null)
    {
        if (empty($this->content) || !$this->isDecorated()) {
            return;
        }

        if ($lines) {
            array_splice($this->content, -($lines * 2)); // Multiply lines by 2 to cater for each new line added between content
        } else {
            $lines = $this->lines;
            $this->content = [];
        }

        $this->lines -= $lines;

        parent::doWrite($this->popStreamContentUntilCurrentSection($lines), false);
    }

    /**
     * Overwrites the previous output with a new message.
     *
     * @param array|string $message
     */
    public function overwrite($message)
    {
        $this->clear();
        $this->writeln($message);
    }

    public function getContent(): string
    {
        return implode('', $this->content);
    }

    /**
     * @internal
     */
    public function addContent(string $input)
    {
        foreach (explode(\PHP_EOL, $input) as $lineContent) {
            $this->lines += ceil($this->getDisplayLength($lineContent) / $this->terminal->getWidth()) ?: 1;
            $this->content[] = $lineContent;
            $this->content[] = \PHP_EOL;
        }
    }

    /**
     * {@inheritdoc}
     */
    protected function doWrite(string $message, bool $newline)
    {
        if (!$this->isDecorated()) {
            parent::doWrite($message, $newline);

            return;
        }

        $erasedContent = $this->popStreamContentUntilCurrentSection();

        $this->addContent($message);

        parent::doWrite($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->lines;
            $erasedContent[] = $section->getContent();
        }

        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\Question;

/**
 * Represents a yes/no question.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class ConfirmationQuestion extends Question
{
    private $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 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;

/**
 * Represents a choice question.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class ChoiceQuestion extends Question
{
    private $choices;
    private $multiselect = false;
    private $prompt = ' > ';
    private $errorMessage = 'Value "%s" is invalid';

    /**
     * @param string $question The question to ask to the user
     * @param array  $choices  The list of available choices
     * @param mixed  $default  The default answer to return
     */
    public function __construct(string $question, array $choices, $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);
    }

    /**
     * Returns available choices.
     *
     * @return array
     */
    public function getChoices()
    {
        return $this->choices;
    }

    /**
     * Sets multiselect option.
     *
     * When multiselect is set to true, multiple choices can be answered.
     *
     * @return $this
     */
    public function setMultiselect(bool $multiselect)
    {
        $this->multiselect = $multiselect;
        $this->setValidator($this->getDefaultValidator());

        return $this;
    }

    /**
     * Returns whether the choices are multiselect.
     *
     * @return bool
     */
    public function isMultiselect()
    {
        return $this->multiselect;
    }

    /**
     * Gets the prompt for choices.
     *
     * @return string
     */
    public function getPrompt()
    {
        return $this->prompt;
    }

    /**
     * Sets the prompt for choices.
     *
     * @return $this
     */
    public function setPrompt(string $prompt)
    {
        $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)
    {
        $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\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 $question;
    private $attempts;
    private $hidden = false;
    private $hiddenFallback = true;
    private $autocompleterCallback;
    private $validator;
    private $default;
    private $normalizer;
    private $trimmable = true;
    private $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, $default = null)
    {
        $this->question = $question;
        $this->default = $default;
    }

    /**
     * Returns the question.
     *
     * @return string
     */
    public function getQuestion()
    {
        return $this->question;
    }

    /**
     * Returns the default answer.
     *
     * @return string|bool|int|float|null
     */
    public function getDefault()
    {
        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): self
    {
        $this->multiline = $multiline;

        return $this;
    }

    /**
     * Returns whether the user response must be hidden.
     *
     * @return bool
     */
    public function isHidden()
    {
        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)
    {
        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.
     *
     * @return bool
     */
    public function isHiddenFallback()
    {
        return $this->hiddenFallback;
    }

    /**
     * Sets whether to fallback on non-hidden question if the response cannot be hidden.
     *
     * @return $this
     */
    public function setHiddenFallback(bool $fallback)
    {
        $this->hiddenFallback = $fallback;

        return $this;
    }

    /**
     * Gets values for the autocompleter.
     *
     * @return iterable|null
     */
    public function getAutocompleterValues()
    {
        $callback = $this->getAutocompleterCallback();

        return $callback ? $callback('') : null;
    }

    /**
     * Sets values for the autocompleter.
     *
     * @return $this
     *
     * @throws LogicException
     */
    public function setAutocompleterValues(?iterable $values)
    {
        if (\is_array($values)) {
            $values = $this->isAssoc($values) ? array_merge(array_keys($values), array_values($values)) : array_values($values);

            $callback = static function () use ($values) {
                return $values;
            };
        } elseif ($values instanceof \Traversable) {
            $valueCache = null;
            $callback = static function () use ($values, &$valueCache) {
                return $valueCache ?? $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): self
    {
        if ($this->hidden && null !== $callback) {
            throw new LogicException('A hidden question cannot use the autocompleter.');
        }

        $this->autocompleterCallback = $callback;

        return $this;
    }

    /**
     * Sets a validator for the question.
     *
     * @return $this
     */
    public function setValidator(?callable $validator = null)
    {
        $this->validator = $validator;

        return $this;
    }

    /**
     * Gets the validator for the question.
     *
     * @return callable|null
     */
    public function getValidator()
    {
        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)
    {
        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.
     *
     * @return int|null
     */
    public function getMaxAttempts()
    {
        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)
    {
        $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.
     *
     * @return callable|null
     */
    public function getNormalizer()
    {
        return $this->normalizer;
    }

    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): self
    {
        $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;

class Terminal
{
    private static $width;
    private static $height;
    private static $stty;

    /**
     * Gets the terminal width.
     *
     * @return int
     */
    public function getWidth()
    {
        $width = getenv('COLUMNS');
        if (false !== $width) {
            return (int) trim($width);
        }

        if (null === self::$width) {
            self::initDimensions();
        }

        return self::$width ?: 80;
    }

    /**
     * Gets the terminal height.
     *
     * @return int
     */
    public function getHeight()
    {
        $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()
    {
        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()
    {
        if ($sttyString = self::getSttyColumns()) {
            if (preg_match('/rows.(\d+);.columns.(\d+);/i', $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/i', $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 | grep columns');
    }

    private static function readFromProcess(string $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\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;

/**
 * @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\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;

/**
 * @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;

/**
 * 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;

/**
 * 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;

/**
 * Represents an incorrect command name typed in the console.
 *
 * @author Jérôme Tamarelle <jerome@tamarelle.net>
 */
class CommandNotFoundException extends \InvalidArgumentException implements ExceptionInterface
{
    private $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()
    {
        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;

/**
 * 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\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 $output;

    public function __construct(OutputInterface $output)
    {
        $this->output = $output;
    }

    /**
     * {@inheritdoc}
     */
    public function newLine(int $count = 1)
    {
        $this->output->write(str_repeat(\PHP_EOL, $count));
    }

    /**
     * @return ProgressBar
     */
    public function createProgressBar(int $max = 0)
    {
        return new ProgressBar($this->output, $max);
    }

    /**
     * {@inheritdoc}
     */
    public function write($messages, bool $newline = false, int $type = self::OUTPUT_NORMAL)
    {
        $this->output->write($messages, $newline, $type);
    }

    /**
     * {@inheritdoc}
     */
    public function writeln($messages, int $type = self::OUTPUT_NORMAL)
    {
        $this->output->writeln($messages, $type);
    }

    /**
     * {@inheritdoc}
     */
    public function setVerbosity(int $level)
    {
        $this->output->setVerbosity($level);
    }

    /**
     * {@inheritdoc}
     */
    public function getVerbosity()
    {
        return $this->output->getVerbosity();
    }

    /**
     * {@inheritdoc}
     */
    public function setDecorated(bool $decorated)
    {
        $this->output->setDecorated($decorated);
    }

    /**
     * {@inheritdoc}
     */
    public function isDecorated()
    {
        return $this->output->isDecorated();
    }

    /**
     * {@inheritdoc}
     */
    public function setFormatter(OutputFormatterInterface $formatter)
    {
        $this->output->setFormatter($formatter);
    }

    /**
     * {@inheritdoc}
     */
    public function getFormatter()
    {
        return $this->output->getFormatter();
    }

    /**
     * {@inheritdoc}
     */
    public function isQuiet()
    {
        return $this->output->isQuiet();
    }

    /**
     * {@inheritdoc}
     */
    public function isVerbose()
    {
        return $this->output->isVerbose();
    }

    /**
     * {@inheritdoc}
     */
    public function isVeryVerbose()
    {
        return $this->output->isVeryVerbose();
    }

    /**
     * {@inheritdoc}
     */
    public function isDebug()
    {
        return $this->output->isDebug();
    }

    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\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\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 $input;
    private $output;
    private $questionHelper;
    private $progressBar;
    private $lineLength;
    private $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.
     *
     * @param string|array $messages The message to write in the block
     */
    public function block($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();
    }

    /**
     * {@inheritdoc}
     */
    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();
    }

    /**
     * {@inheritdoc}
     */
    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();
    }

    /**
     * {@inheritdoc}
     */
    public function listing(array $elements)
    {
        $this->autoPrependText();
        $elements = array_map(function ($element) {
            return sprintf(' * %s', $element);
        }, $elements);

        $this->writeln($elements);
        $this->newLine();
    }

    /**
     * {@inheritdoc}
     */
    public function text($message)
    {
        $this->autoPrependText();

        $messages = \is_array($message) ? array_values($message) : [$message];
        foreach ($messages as $message) {
            $this->writeln(sprintf(' %s', $message));
        }
    }

    /**
     * Formats a command comment.
     *
     * @param string|array $message
     */
    public function comment($message)
    {
        $this->block($message, null, null, '<fg=default;bg=default> // </>', false, false);
    }

    /**
     * {@inheritdoc}
     */
    public function success($message)
    {
        $this->block($message, 'OK', 'fg=black;bg=green', ' ', true);
    }

    /**
     * {@inheritdoc}
     */
    public function error($message)
    {
        $this->block($message, 'ERROR', 'fg=white;bg=red', ' ', true);
    }

    /**
     * {@inheritdoc}
     */
    public function warning($message)
    {
        $this->block($message, 'WARNING', 'fg=black;bg=yellow', ' ', true);
    }

    /**
     * {@inheritdoc}
     */
    public function note($message)
    {
        $this->block($message, 'NOTE', 'fg=yellow', ' ! ');
    }

    /**
     * Formats an info message.
     *
     * @param string|array $message
     */
    public function info($message)
    {
        $this->block($message, 'INFO', 'fg=green', ' ', true);
    }

    /**
     * {@inheritdoc}
     */
    public function caution($message)
    {
        $this->block($message, 'CAUTION', 'fg=white;bg=red', ' ! ', true);
    }

    /**
     * {@inheritdoc}
     */
    public function table(array $headers, array $rows)
    {
        $this->createTable()
            ->setHeaders($headers)
            ->setRows($rows)
            ->render()
        ;

        $this->newLine();
    }

    /**
     * Formats a horizontal table.
     */
    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()
     *
     * @param string|array|TableSeparator ...$list
     */
    public function definitionList(...$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]);
    }

    /**
     * {@inheritdoc}
     */
    public function ask(string $question, ?string $default = null, ?callable $validator = null)
    {
        $question = new Question($question, $default);
        $question->setValidator($validator);

        return $this->askQuestion($question);
    }

    /**
     * {@inheritdoc}
     */
    public function askHidden(string $question, ?callable $validator = null)
    {
        $question = new Question($question);

        $question->setHidden(true);
        $question->setValidator($validator);

        return $this->askQuestion($question);
    }

    /**
     * {@inheritdoc}
     */
    public function confirm(string $question, bool $default = true)
    {
        return $this->askQuestion(new ConfirmationQuestion($question, $default));
    }

    /**
     * {@inheritdoc}
     */
    public function choice(string $question, array $choices, $default = null)
    {
        if (null !== $default) {
            $values = array_flip($choices);
            $default = $values[$default] ?? $default;
        }

        return $this->askQuestion(new ChoiceQuestion($question, $choices, $default));
    }

    /**
     * {@inheritdoc}
     */
    public function progressStart(int $max = 0)
    {
        $this->progressBar = $this->createProgressBar($max);
        $this->progressBar->start();
    }

    /**
     * {@inheritdoc}
     */
    public function progressAdvance(int $step = 1)
    {
        $this->getProgressBar()->advance($step);
    }

    /**
     * {@inheritdoc}
     */
    public function progressFinish()
    {
        $this->getProgressBar()->finish();
        $this->newLine(2);
        $this->progressBar = null;
    }

    /**
     * {@inheritdoc}
     */
    public function createProgressBar(int $max = 0)
    {
        $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()
     */
    public function progressIterate(iterable $iterable, ?int $max = null): iterable
    {
        yield from $this->createProgressBar()->iterate($iterable, $max);

        $this->newLine(2);
    }

    /**
     * @return mixed
     */
    public function askQuestion(Question $question)
    {
        if ($this->input->isInteractive()) {
            $this->autoPrependBlock();
        }

        if (!$this->questionHelper) {
            $this->questionHelper = new SymfonyQuestionHelper();
        }

        $answer = $this->questionHelper->ask($this->input, $this, $question);

        if ($this->input->isInteractive()) {
            $this->newLine();
            $this->bufferedOutput->write("\n");
        }

        return $answer;
    }

    /**
     * {@inheritdoc}
     */
    public function writeln($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);
        }
    }

    /**
     * {@inheritdoc}
     */
    public function write($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);
        }
    }

    /**
     * {@inheritdoc}
     */
    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.
     *
     * @return self
     */
    public function getErrorStyle()
    {
        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
    {
        if (!$this->progressBar) {
            throw new RuntimeException('The ProgressBar is not started.');
        }

        return $this->progressBar;
    }

    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 (!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 = \strlen($type);
            $lineIndentation = str_repeat(' ', $indentLength);
        }

        // wrap and add newlines for each element
        foreach ($messages as $key => $message) {
            if ($escape) {
                $message = OutputFormatter::escape($message);
            }

            $decorationLength = Helper::width($message) - Helper::width(Helper::removeDecoration($this->getFormatter(), $message));
            $messageLineLength = min($this->lineLength - $prefixLength - $indentLength + $decorationLength, $this->lineLength);
            $messageLines = explode(\PHP_EOL, wordwrap($message, $messageLineLength, \PHP_EOL, true));
            foreach ($messageLines as $messageLine) {
                $lines[] = $messageLine;
            }

            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.
     */
    public function title(string $message);

    /**
     * Formats a section title.
     */
    public function section(string $message);

    /**
     * Formats a list.
     */
    public function listing(array $elements);

    /**
     * Formats informational text.
     *
     * @param string|array $message
     */
    public function text($message);

    /**
     * Formats a success result bar.
     *
     * @param string|array $message
     */
    public function success($message);

    /**
     * Formats an error result bar.
     *
     * @param string|array $message
     */
    public function error($message);

    /**
     * Formats an warning result bar.
     *
     * @param string|array $message
     */
    public function warning($message);

    /**
     * Formats a note admonition.
     *
     * @param string|array $message
     */
    public function note($message);

    /**
     * Formats a caution admonition.
     *
     * @param string|array $message
     */
    public function caution($message);

    /**
     * Formats a table.
     */
    public function table(array $headers, array $rows);

    /**
     * Asks a question.
     *
     * @return mixed
     */
    public function ask(string $question, ?string $default = null, ?callable $validator = null);

    /**
     * Asks a question with the user input hidden.
     *
     * @return mixed
     */
    public function askHidden(string $question, ?callable $validator = null);

    /**
     * Asks for confirmation.
     *
     * @return bool
     */
    public function confirm(string $question, bool $default = true);

    /**
     * Asks a choice question.
     *
     * @param string|int|null $default
     *
     * @return mixed
     */
    public function choice(string $question, array $choices, $default = null);

    /**
     * Add newline(s).
     */
    public function newLine(int $count = 1);

    /**
     * Starts the progress output.
     */
    public function progressStart(int $max = 0);

    /**
     * Advances the progress output X steps.
     */
    public function progressAdvance(int $step = 1);

    /**
     * Finishes the progress output.
     */
    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\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\CI;

use Symfony\Component\Console\Output\OutputInterface;

/**
 * Utility class for Github actions.
 *
 * @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
 */
class GithubActionReporter
{
    private $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\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
{
    private $commandLoaderServiceId;
    private $commandTag;
    private $noPreloadTag;
    private $privateTagName;

    public function __construct(string $commandLoaderServiceId = 'console.command_loader', string $commandTag = 'console.command', string $noPreloadTag = 'container.no_preload', string $privateTagName = 'container.private')
    {
        if (0 < \func_num_args()) {
            trigger_deprecation('symfony/console', '5.3', 'Configuring "%s" is deprecated.', __CLASS__);
        }

        $this->commandLoaderServiceId = $commandLoaderServiceId;
        $this->commandTag = $commandTag;
        $this->noPreloadTag = $noPreloadTag;
        $this->privateTagName = $privateTagName;
    }

    public function process(ContainerBuilder $container)
    {
        $commandServices = $container->findTaggedServiceIds($this->commandTag, true);
        $lazyCommandMap = [];
        $lazyCommandRefs = [];
        $serviceIds = [];

        foreach ($commandServices as $id => $tags) {
            $definition = $container->getDefinition($id);
            $definition->addTag($this->noPreloadTag);
            $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, $this->commandTag, 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($this->privateTagName)) {
                    $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 = $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, $this->commandTag, 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($this->commandLoaderServiceId, ContainerCommandLoader::class)
            ->setPublic(true)
            ->addTag($this->noPreloadTag)
            ->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;

use Symfony\Component\Console\Output\OutputInterface;

/**
 * @author Pierre du Plessis <pdples@gmail.com>
 */
final class Cursor
{
    private $output;
    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): self
    {
        $this->output->write(sprintf("\x1b[%dA", $lines));

        return $this;
    }

    /**
     * @return $this
     */
    public function moveDown(int $lines = 1): self
    {
        $this->output->write(sprintf("\x1b[%dB", $lines));

        return $this;
    }

    /**
     * @return $this
     */
    public function moveRight(int $columns = 1): self
    {
        $this->output->write(sprintf("\x1b[%dC", $columns));

        return $this;
    }

    /**
     * @return $this
     */
    public function moveLeft(int $columns = 1): self
    {
        $this->output->write(sprintf("\x1b[%dD", $columns));

        return $this;
    }

    /**
     * @return $this
     */
    public function moveToColumn(int $column): self
    {
        $this->output->write(sprintf("\x1b[%dG", $column));

        return $this;
    }

    /**
     * @return $this
     */
    public function moveToPosition(int $column, int $row): self
    {
        $this->output->write(sprintf("\x1b[%d;%dH", $row + 1, $column));

        return $this;
    }

    /**
     * @return $this
     */
    public function savePosition(): self
    {
        $this->output->write("\x1b7");

        return $this;
    }

    /**
     * @return $this
     */
    public function restorePosition(): self
    {
        $this->output->write("\x1b8");

        return $this;
    }

    /**
     * @return $this
     */
    public function hide(): self
    {
        $this->output->write("\x1b[?25l");

        return $this;
    }

    /**
     * @return $this
     */
    public function show(): self
    {
        $this->output->write("\x1b[?25h\x1b[?0c");

        return $this;
    }

    /**
     * Clears all the output from the current line.
     *
     * @return $this
     */
    public function clearLine(): self
    {
        $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(): self
    {
        $this->output->write("\x1b[0J");

        return $this;
    }

    /**
     * Clears the entire screen.
     *
     * @return $this
     */
    public function clearScreen(): self
    {
        $this->output->write("\x1b[2J");

        return $this;
    }

    /**
     * Returns the current cursor position as x,y coordinates.
     */
    public function getCurrentPosition(): array
    {
        static $isTtySupported;

        if (null === $isTtySupported && \function_exists('proc_open')) {
            $isTtySupported = (bool) @proc_open('echo 1 >/dev/null', [['file', '/dev/tty', 'r'], ['file', '/dev/tty', 'w'], ['file', '/dev/tty', 'w']], $pipes);
        }

        if (!$isTtySupported) {
            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\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.
     *
     * @return Command
     *
     * @throws CommandNotFoundException
     */
    public function get(string $name);

    /**
     * Checks if a command exists.
     *
     * @return bool
     */
    public function has(string $name);

    /**
     * @return string[]
     */
    public function getNames();
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@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\Exception\CommandNotFoundException;

/**
 * A simple command loader using factories to instantiate commands lazily.
 *
 * @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
 */
class FactoryCommandLoader implements CommandLoaderInterface
{
    private $factories;

    /**
     * @param callable[] $factories Indexed by command names
     */
    public function __construct(array $factories)
    {
        $this->factories = $factories;
    }

    /**
     * {@inheritdoc}
     */
    public function has(string $name)
    {
        return isset($this->factories[$name]);
    }

    /**
     * {@inheritdoc}
     */
    public function get(string $name)
    {
        if (!isset($this->factories[$name])) {
            throw new CommandNotFoundException(sprintf('Command "%s" does not exist.', $name));
        }

        $factory = $this->factories[$name];

        return $factory();
    }

    /**
     * {@inheritdoc}
     */
    public function getNames()
    {
        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 Psr\Container\ContainerInterface;
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 $container;
    private $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;
    }

    /**
     * {@inheritdoc}
     */
    public function get(string $name)
    {
        if (!$this->has($name)) {
            throw new CommandNotFoundException(sprintf('Command "%s" does not exist.', $name));
        }

        return $this->container->get($this->commandMap[$name]);
    }

    /**
     * {@inheritdoc}
     */
    public function has(string $name)
    {
        return isset($this->commandMap[$name]) && $this->container->has($this->commandMap[$name]);
    }

    /**
     * {@inheritdoc}
     */
    public function getNames()
    {
        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\SignalRegistry;

final class SignalRegistry
{
    private $signalHandlers = [];

    public function __construct()
    {
        if (\function_exists('pcntl_async_signals')) {
            pcntl_async_signals(true);
        }
    }

    public function register(int $signal, callable $signalHandler): void
    {
        if (!isset($this->signalHandlers[$signal])) {
            $previousCallback = pcntl_signal_get_handler($signal);

            if (\is_callable($previousCallback)) {
                $this->signalHandlers[$signal][] = $previousCallback;
            }
        }

        $this->signalHandlers[$signal][] = $signalHandler;

        pcntl_signal($signal, [$this, 'handle']);
    }

    public static function isSupported(): bool
    {
        if (!\function_exists('pcntl_signal')) {
            return false;
        }

        if (\in_array('pcntl_signal', explode(',', \ini_get('disable_functions')))) {
            return false;
        }

        return true;
    }

    /**
     * @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);
        }
    }
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@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).
     */
    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;

use Symfony\Component\Console\Exception\InvalidArgumentException;

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 $decorated;
    private $styles = [];
    private $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.
     *
     * @return string
     */
    public static function escape(string $text)
    {
        $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();
    }

    /**
     * {@inheritdoc}
     */
    public function setDecorated(bool $decorated)
    {
        $this->decorated = $decorated;
    }

    /**
     * {@inheritdoc}
     */
    public function isDecorated()
    {
        return $this->decorated;
    }

    /**
     * {@inheritdoc}
     */
    public function setStyle(string $name, OutputFormatterStyleInterface $style)
    {
        $this->styles[strtolower($name)] = $style;
    }

    /**
     * {@inheritdoc}
     */
    public function hasStyle(string $name)
    {
        return isset($this->styles[strtolower($name)]);
    }

    /**
     * {@inheritdoc}
     */
    public function getStyle(string $name)
    {
        if (!$this->hasStyle($name)) {
            throw new InvalidArgumentException(sprintf('Undefined style: "%s".', $name));
        }

        return $this->styles[strtolower($name)];
    }

    /**
     * {@inheritdoc}
     */
    public function format(?string $message)
    {
        return $this->formatAndWrap($message, 0);
    }

    /**
     * {@inheritdoc}
     */
    public function formatAndWrap(?string $message, int $width)
    {
        if (null === $message) {
            return '';
        }

        $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;
            }

            // add the text up to the next tag
            $output .= $this->applyCurrentStyle(substr($message, $offset, $pos - $offset), $output, $width, $currentLineLength);
            $offset = $pos + \strlen($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(substr($message, $offset), $output, $width, $currentLineLength);

        return strtr($output, ["\0" => '\\', '\\<' => '<', '\\>' => '>']);
    }

    /**
     * @return OutputFormatterStyleStack
     */
    public function getStyleStack()
    {
        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) {
            $prefix = substr($text, 0, $i = $width - $currentLineLength)."\n";
            $text = substr($text, $i);
        } else {
            $prefix = '';
        }

        preg_match('~(\\n)$~', $text, $matches);
        $text = $prefix.$this->addLineBreaks($text, $width);
        $text = rtrim($text, "\n").($matches[1] ?? '');

        if (!$currentLineLength && '' !== $current && "\n" !== substr($current, -1)) {
            $text = "\n".$text;
        }

        $lines = explode("\n", $text);

        foreach ($lines as $line) {
            $currentLineLength += \strlen($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)->toCodePointString($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;

/**
 * @author Tien Xuan Vo <tien.xuan.vo@gmail.com>
 */
final class NullOutputFormatter implements OutputFormatterInterface
{
    private $style;

    /**
     * {@inheritdoc}
     */
    public function format(?string $message): ?string
    {
        return null;
    }

    /**
     * {@inheritdoc}
     */
    public function getStyle(string $name): OutputFormatterStyleInterface
    {
        // to comply with the interface we must return a OutputFormatterStyleInterface
        return $this->style ?? $this->style = new NullOutputFormatterStyle();
    }

    /**
     * {@inheritdoc}
     */
    public function hasStyle(string $name): bool
    {
        return false;
    }

    /**
     * {@inheritdoc}
     */
    public function isDecorated(): bool
    {
        return false;
    }

    /**
     * {@inheritdoc}
     */
    public function setDecorated(bool $decorated): void
    {
        // do nothing
    }

    /**
     * {@inheritdoc}
     */
    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;

/**
 * Formatter interface for console output.
 *
 * @author Konstantin Kudryashov <ever.zet@gmail.com>
 */
interface OutputFormatterInterface
{
    /**
     * Sets the decorated flag.
     */
    public function setDecorated(bool $decorated);

    /**
     * Whether the output will decorate messages.
     *
     * @return bool
     */
    public function isDecorated();

    /**
     * Sets a new style.
     */
    public function setStyle(string $name, OutputFormatterStyleInterface $style);

    /**
     * Checks if output formatter has style with specified name.
     *
     * @return bool
     */
    public function hasStyle(string $name);

    /**
     * Gets style options from style with specified name.
     *
     * @return OutputFormatterStyleInterface
     *
     * @throws \InvalidArgumentException When style isn't defined
     */
    public function getStyle(string $name);

    /**
     * Formats a message according to the given styles.
     *
     * @return string|null
     */
    public function format(?string $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\Formatter;

/**
 * Formatter style interface for defining styles.
 *
 * @author Konstantin Kudryashov <ever.zet@gmail.com>
 */
interface OutputFormatterStyleInterface
{
    /**
     * Sets style foreground color.
     */
    public function setForeground(?string $color = null);

    /**
     * Sets style background color.
     */
    public function setBackground(?string $color = null);

    /**
     * Sets some specific style option.
     */
    public function setOption(string $option);

    /**
     * Unsets some specific style option.
     */
    public function unsetOption(string $option);

    /**
     * Sets multiple style options at once.
     */
    public function setOptions(array $options);

    /**
     * Applies the style to a given text.
     *
     * @return string
     */
    public function apply(string $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\Formatter;

/**
 * @author Tien Xuan Vo <tien.xuan.vo@gmail.com>
 */
final class NullOutputFormatterStyle implements OutputFormatterStyleInterface
{
    /**
     * {@inheritdoc}
     */
    public function apply(string $text): string
    {
        return $text;
    }

    /**
     * {@inheritdoc}
     */
    public function setBackground(?string $color = null): void
    {
        // do nothing
    }

    /**
     * {@inheritdoc}
     */
    public function setForeground(?string $color = null): void
    {
        // do nothing
    }

    /**
     * {@inheritdoc}
     */
    public function setOption(string $option): void
    {
        // do nothing
    }

    /**
     * {@inheritdoc}
     */
    public function setOptions(array $options): void
    {
        // do nothing
    }

    /**
     * {@inheritdoc}
     */
    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;

use Symfony\Component\Console\Color;

/**
 * Formatter style class for defining styles.
 *
 * @author Konstantin Kudryashov <ever.zet@gmail.com>
 */
class OutputFormatterStyle implements OutputFormatterStyleInterface
{
    private $color;
    private $foreground;
    private $background;
    private $options;
    private $href;
    private $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);
    }

    /**
     * {@inheritdoc}
     */
    public function setForeground(?string $color = null)
    {
        $this->color = new Color($this->foreground = $color ?: '', $this->background, $this->options);
    }

    /**
     * {@inheritdoc}
     */
    public function setBackground(?string $color = null)
    {
        $this->color = new Color($this->foreground, $this->background = $color ?: '', $this->options);
    }

    public function setHref(string $url): void
    {
        $this->href = $url;
    }

    /**
     * {@inheritdoc}
     */
    public function setOption(string $option)
    {
        $this->options[] = $option;
        $this->color = new Color($this->foreground, $this->background, $this->options);
    }

    /**
     * {@inheritdoc}
     */
    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);
    }

    /**
     * {@inheritdoc}
     */
    public function setOptions(array $options)
    {
        $this->color = new Color($this->foreground, $this->background, $this->options = $options);
    }

    /**
     * {@inheritdoc}
     */
    public function apply(string $text)
    {
        if (null === $this->handlesHrefGracefully) {
            $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\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 $styles;

    private $emptyStyle;

    public function __construct(?OutputFormatterStyleInterface $emptyStyle = null)
    {
        $this->emptyStyle = $emptyStyle ?? new OutputFormatterStyle();
        $this->reset();
    }

    /**
     * Resets stack (ie. empty internal arrays).
     */
    public function reset()
    {
        $this->styles = [];
    }

    /**
     * Pushes a style in the stack.
     */
    public function push(OutputFormatterStyleInterface $style)
    {
        $this->styles[] = $style;
    }

    /**
     * Pops a style from the stack.
     *
     * @return OutputFormatterStyleInterface
     *
     * @throws InvalidArgumentException When style tags incorrectly nested
     */
    public function pop(?OutputFormatterStyleInterface $style = null)
    {
        if (empty($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.
     *
     * @return OutputFormatterStyle
     */
    public function getCurrent()
    {
        if (empty($this->styles)) {
            return $this->emptyStyle;
        }

        return $this->styles[\count($this->styles) - 1];
    }

    /**
     * @return $this
     */
    public function setEmptyStyle(OutputFormatterStyleInterface $emptyStyle)
    {
        $this->emptyStyle = $emptyStyle;

        return $this;
    }

    /**
     * @return OutputFormatterStyleInterface
     */
    public function getEmptyStyle()
    {
        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\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 $output;
    private $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 $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 $errored = false;

    public function __construct(OutputInterface $output, array $verbosityLevelMap = [], array $formatLevelMap = [])
    {
        $this->output = $output;
        $this->verbosityLevelMap = $verbosityLevelMap + $this->verbosityLevelMap;
        $this->formatLevelMap = $formatLevelMap + $this->formatLevelMap;
    }

    /**
     * {@inheritdoc}
     *
     * @return void
     */
    public function log($level, $message, array $context = [])
    {
        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.
     *
     * @return bool
     */
    public function hasErrored()
    {
        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) || (\is_object($val) && method_exists($val, '__toString'))) {
                $replacements["{{$key}}"] = $val;
            } elseif ($val instanceof \DateTimeInterface) {
                $replacements["{{$key}}"] = $val->format(\DateTime::RFC3339);
            } elseif (\is_object($val)) {
                $replacements["{{$key}}"] = '[object '.\get_class($val).']';
            } 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;

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 $version = 'UNKNOWN';
    private $autoExit = true;
    private $running = false;

    /**
     * @return $this
     */
    public function setVersion(string $version): self
    {
        $this->version = $version;

        return $this;
    }

    /**
     * @final
     *
     * @return $this
     */
    public function setAutoExit(bool $autoExit): self
    {
        $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;

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 $foreground;
    private $background;
    private $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]) {
            $color = substr($color, 1);

            if (3 === \strlen($color)) {
                $color = $color[0].$color[0].$color[1].$color[1].$color[2].$color[2];
            }

            if (6 !== \strlen($color)) {
                throw new InvalidArgumentException(sprintf('Invalid "%s" color.', $color));
            }

            return ($background ? '4' : '3').$this->convertHexColorToAnsi(hexdec($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)))));
    }

    private function convertHexColorToAnsi(int $color): string
    {
        $r = ($color >> 16) & 255;
        $g = ($color >> 8) & 255;
        $b = $color & 255;

        // see https://github.com/termstandard/colors/ for more information about true color support
        if ('truecolor' !== getenv('COLORTERM')) {
            return (string) $this->degradeHexColorToAnsi($r, $g, $b);
        }

        return sprintf('8;2;%d;%d;%d', $r, $g, $b);
    }

    private function degradeHexColorToAnsi(int $r, int $g, int $b): int
    {
        if (0 === round($this->getSaturation($r, $g, $b) / 50)) {
            return 0;
        }

        return (round($b / 255) << 2) | (round($g / 255) << 1) | round($r / 255);
    }

    private function getSaturation(int $r, int $g, int $b): int
    {
        $r = $r / 255;
        $g = $g / 255;
        $b = $b / 255;
        $v = max($r, $g, $b);

        if (0 === $diff = $v - min($r, $g, $b)) {
            return 0;
        }

        return (int) $diff * 100 / $v;
    }
}
#!/usr/bin/env php
<?php

/**
 * Proxy PHP file generated by Composer
 *
 * This file includes the referenced bin path (../symfony/var-dumper/Resources/bin/var-dump-server)
 * 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/var-dumper/Resources/bin/var-dump-server');
    }
}

return include __DIR__ . '/..'.'/symfony/var-dumper/Resources/bin/var-dump-server';
#!/usr/bin/env php
<?php

/**
 * Proxy PHP file generated by Composer
 *
 * This file includes the referenced bin path (../symfony/phpunit-bridge/bin/simple-phpunit)
 * 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/phpunit-bridge/bin/simple-phpunit');
    }
}

return include __DIR__ . '/..'.'/symfony/phpunit-bridge/bin/simple-phpunit';
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Lexer;

use PHPStan\PhpDocParser\ParserConfig;
use function implode;
use function preg_match_all;
use const PREG_SET_ORDER;

/**
 * Implementation based on Nette Tokenizer (New BSD License; https://github.com/nette/tokenizer)
 */
class Lexer
{

	public const TOKEN_REFERENCE = 0;
	public const TOKEN_UNION = 1;
	public const TOKEN_INTERSECTION = 2;
	public const TOKEN_NULLABLE = 3;
	public const TOKEN_OPEN_PARENTHESES = 4;
	public const TOKEN_CLOSE_PARENTHESES = 5;
	public const TOKEN_OPEN_ANGLE_BRACKET = 6;
	public const TOKEN_CLOSE_ANGLE_BRACKET = 7;
	public const TOKEN_OPEN_SQUARE_BRACKET = 8;
	public const TOKEN_CLOSE_SQUARE_BRACKET = 9;
	public const TOKEN_COMMA = 10;
	public const TOKEN_VARIADIC = 11;
	public const TOKEN_DOUBLE_COLON = 12;
	public const TOKEN_DOUBLE_ARROW = 13;
	public const TOKEN_EQUAL = 14;
	public const TOKEN_OPEN_PHPDOC = 15;
	public const TOKEN_CLOSE_PHPDOC = 16;
	public const TOKEN_PHPDOC_TAG = 17;
	public const TOKEN_DOCTRINE_TAG = 18;
	public const TOKEN_FLOAT = 19;
	public const TOKEN_INTEGER = 20;
	public const TOKEN_SINGLE_QUOTED_STRING = 21;
	public const TOKEN_DOUBLE_QUOTED_STRING = 22;
	public const TOKEN_DOCTRINE_ANNOTATION_STRING = 23;
	public const TOKEN_IDENTIFIER = 24;
	public const TOKEN_THIS_VARIABLE = 25;
	public const TOKEN_VARIABLE = 26;
	public const TOKEN_HORIZONTAL_WS = 27;
	public const TOKEN_PHPDOC_EOL = 28;
	public const TOKEN_OTHER = 29;
	public const TOKEN_END = 30;
	public const TOKEN_COLON = 31;
	public const TOKEN_WILDCARD = 32;
	public const TOKEN_OPEN_CURLY_BRACKET = 33;
	public const TOKEN_CLOSE_CURLY_BRACKET = 34;
	public const TOKEN_NEGATED = 35;
	public const TOKEN_ARROW = 36;

	public const TOKEN_COMMENT = 37;

	public const TOKEN_LABELS = [
		self::TOKEN_REFERENCE => '\'&\'',
		self::TOKEN_UNION => '\'|\'',
		self::TOKEN_INTERSECTION => '\'&\'',
		self::TOKEN_NULLABLE => '\'?\'',
		self::TOKEN_NEGATED => '\'!\'',
		self::TOKEN_OPEN_PARENTHESES => '\'(\'',
		self::TOKEN_CLOSE_PARENTHESES => '\')\'',
		self::TOKEN_OPEN_ANGLE_BRACKET => '\'<\'',
		self::TOKEN_CLOSE_ANGLE_BRACKET => '\'>\'',
		self::TOKEN_OPEN_SQUARE_BRACKET => '\'[\'',
		self::TOKEN_CLOSE_SQUARE_BRACKET => '\']\'',
		self::TOKEN_OPEN_CURLY_BRACKET => '\'{\'',
		self::TOKEN_CLOSE_CURLY_BRACKET => '\'}\'',
		self::TOKEN_COMMA => '\',\'',
		self::TOKEN_COMMENT => '\'//\'',
		self::TOKEN_COLON => '\':\'',
		self::TOKEN_VARIADIC => '\'...\'',
		self::TOKEN_DOUBLE_COLON => '\'::\'',
		self::TOKEN_DOUBLE_ARROW => '\'=>\'',
		self::TOKEN_ARROW => '\'->\'',
		self::TOKEN_EQUAL => '\'=\'',
		self::TOKEN_OPEN_PHPDOC => '\'/**\'',
		self::TOKEN_CLOSE_PHPDOC => '\'*/\'',
		self::TOKEN_PHPDOC_TAG => 'TOKEN_PHPDOC_TAG',
		self::TOKEN_DOCTRINE_TAG => 'TOKEN_DOCTRINE_TAG',
		self::TOKEN_PHPDOC_EOL => 'TOKEN_PHPDOC_EOL',
		self::TOKEN_FLOAT => 'TOKEN_FLOAT',
		self::TOKEN_INTEGER => 'TOKEN_INTEGER',
		self::TOKEN_SINGLE_QUOTED_STRING => 'TOKEN_SINGLE_QUOTED_STRING',
		self::TOKEN_DOUBLE_QUOTED_STRING => 'TOKEN_DOUBLE_QUOTED_STRING',
		self::TOKEN_DOCTRINE_ANNOTATION_STRING => 'TOKEN_DOCTRINE_ANNOTATION_STRING',
		self::TOKEN_IDENTIFIER => 'type',
		self::TOKEN_THIS_VARIABLE => '\'$this\'',
		self::TOKEN_VARIABLE => 'variable',
		self::TOKEN_HORIZONTAL_WS => 'TOKEN_HORIZONTAL_WS',
		self::TOKEN_OTHER => 'TOKEN_OTHER',
		self::TOKEN_END => 'TOKEN_END',
		self::TOKEN_WILDCARD => '*',
	];

	public const VALUE_OFFSET = 0;
	public const TYPE_OFFSET = 1;
	public const LINE_OFFSET = 2;

	private ParserConfig $config; // @phpstan-ignore property.onlyWritten

	private ?string $regexp = null;

	public function __construct(ParserConfig $config)
	{
		$this->config = $config;
	}


	/**
	 * @return list<array{string, int, int}>
	 */
	public function tokenize(string $s): array
	{
		if ($this->regexp === null) {
			$this->regexp = $this->generateRegexp();
		}

		preg_match_all($this->regexp, $s, $matches, PREG_SET_ORDER);

		$tokens = [];
		$line = 1;
		foreach ($matches as $match) {
			$type = (int) $match['MARK'];
			$tokens[] = [$match[0], $type, $line];
			if ($type !== self::TOKEN_PHPDOC_EOL) {
				continue;
			}

			$line++;
		}

		$tokens[] = ['', self::TOKEN_END, $line];

		return $tokens;
	}


	private function generateRegexp(): string
	{
		$patterns = [
			self::TOKEN_HORIZONTAL_WS => '[\\x09\\x20]++',

			self::TOKEN_IDENTIFIER => '(?:[\\\\]?+[a-z_\\x80-\\xFF][0-9a-z_\\x80-\\xFF-]*+)++',
			self::TOKEN_THIS_VARIABLE => '\\$this(?![0-9a-z_\\x80-\\xFF])',
			self::TOKEN_VARIABLE => '\\$[a-z_\\x80-\\xFF][0-9a-z_\\x80-\\xFF]*+',

			// '&' followed by TOKEN_VARIADIC, TOKEN_VARIABLE, TOKEN_EQUAL, TOKEN_EQUAL or TOKEN_CLOSE_PARENTHESES
			self::TOKEN_REFERENCE => '&(?=\\s*+(?:[.,=)]|(?:\\$(?!this(?![0-9a-z_\\x80-\\xFF])))))',
			self::TOKEN_UNION => '\\|',
			self::TOKEN_INTERSECTION => '&',
			self::TOKEN_NULLABLE => '\\?',
			self::TOKEN_NEGATED => '!',

			self::TOKEN_OPEN_PARENTHESES => '\\(',
			self::TOKEN_CLOSE_PARENTHESES => '\\)',
			self::TOKEN_OPEN_ANGLE_BRACKET => '<',
			self::TOKEN_CLOSE_ANGLE_BRACKET => '>',
			self::TOKEN_OPEN_SQUARE_BRACKET => '\\[',
			self::TOKEN_CLOSE_SQUARE_BRACKET => '\\]',
			self::TOKEN_OPEN_CURLY_BRACKET => '\\{',
			self::TOKEN_CLOSE_CURLY_BRACKET => '\\}',

			self::TOKEN_COMMA => ',',
			self::TOKEN_COMMENT => '\/\/[^\\r\\n]*(?=\n|\r|\*/)',
			self::TOKEN_VARIADIC => '\\.\\.\\.',
			self::TOKEN_DOUBLE_COLON => '::',
			self::TOKEN_DOUBLE_ARROW => '=>',
			self::TOKEN_ARROW => '->',
			self::TOKEN_EQUAL => '=',
			self::TOKEN_COLON => ':',

			self::TOKEN_OPEN_PHPDOC => '/\\*\\*(?=\\s)\\x20?+',
			self::TOKEN_CLOSE_PHPDOC => '\\*/',
			self::TOKEN_PHPDOC_TAG => '@(?:[a-z][a-z0-9-\\\\]+:)?[a-z][a-z0-9-\\\\]*+',
			self::TOKEN_DOCTRINE_TAG => '@[a-z_\\\\][a-z0-9_\:\\\\]*[a-z_][a-z0-9_]*',
			self::TOKEN_PHPDOC_EOL => '\\r?+\\n[\\x09\\x20]*+(?:\\*(?!/)\\x20?+)?',

			self::TOKEN_FLOAT => '[+\-]?(?:(?:[0-9]++(_[0-9]++)*\\.[0-9]*+(_[0-9]++)*(?:e[+\-]?[0-9]++(_[0-9]++)*)?)|(?:[0-9]*+(_[0-9]++)*\\.[0-9]++(_[0-9]++)*(?:e[+\-]?[0-9]++(_[0-9]++)*)?)|(?:[0-9]++(_[0-9]++)*e[+\-]?[0-9]++(_[0-9]++)*))',
			self::TOKEN_INTEGER => '[+\-]?(?:(?:0b[0-1]++(_[0-1]++)*)|(?:0o[0-7]++(_[0-7]++)*)|(?:0x[0-9a-f]++(_[0-9a-f]++)*)|(?:[0-9]++(_[0-9]++)*))',
			self::TOKEN_SINGLE_QUOTED_STRING => '\'(?:\\\\[^\\r\\n]|[^\'\\r\\n\\\\])*+\'',
			self::TOKEN_DOUBLE_QUOTED_STRING => '"(?:\\\\[^\\r\\n]|[^"\\r\\n\\\\])*+"',
			self::TOKEN_DOCTRINE_ANNOTATION_STRING => '"(?:""|[^"])*+"',

			self::TOKEN_WILDCARD => '\\*',

			// anything but TOKEN_CLOSE_PHPDOC or TOKEN_HORIZONTAL_WS or TOKEN_EOL
			self::TOKEN_OTHER => '(?:(?!\\*/)[^\\s])++',
		];

		foreach ($patterns as $type => &$pattern) {
			$pattern = '(?:' . $pattern . ')(*MARK:' . $type . ')';
		}

		return '~' . implode('|', $patterns) . '~Asi';
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Parser;

use PHPStan\ShouldNotHappenException;
use function chr;
use function hexdec;
use function octdec;
use function preg_replace_callback;
use function str_replace;
use function substr;

class StringUnescaper
{

	private const REPLACEMENTS = [
		'\\' => '\\',
		'n' => "\n",
		'r' => "\r",
		't' => "\t",
		'f' => "\f",
		'v' => "\v",
		'e' => "\x1B",
	];

	public static function unescapeString(string $string): string
	{
		$quote = $string[0];

		if ($quote === '\'') {
			return str_replace(
				['\\\\', '\\\''],
				['\\', '\''],
				substr($string, 1, -1),
			);
		}

		return self::parseEscapeSequences(substr($string, 1, -1), '"');
	}

	/**
	 * Implementation based on https://github.com/nikic/PHP-Parser/blob/b0edd4c41111042d43bb45c6c657b2e0db367d9e/lib/PhpParser/Node/Scalar/String_.php#L90-L130
	 */
	private static function parseEscapeSequences(string $str, string $quote): string
	{
		$str = str_replace('\\' . $quote, $quote, $str);

		return preg_replace_callback(
			'~\\\\([\\\\nrtfve]|[xX][0-9a-fA-F]{1,2}|[0-7]{1,3}|u\{([0-9a-fA-F]+)\})~',
			static function ($matches) {
				$str = $matches[1];

				if (isset(self::REPLACEMENTS[$str])) {
					return self::REPLACEMENTS[$str];
				}
				if ($str[0] === 'x' || $str[0] === 'X') {
					return chr((int) hexdec(substr($str, 1)));
				}
				if ($str[0] === 'u') {
					if (!isset($matches[2])) {
						throw new ShouldNotHappenException();
					}
					return self::codePointToUtf8((int) hexdec($matches[2]));
				}

				return chr((int) octdec($str));
			},
			$str,
		);
	}

	/**
	 * Implementation based on https://github.com/nikic/PHP-Parser/blob/b0edd4c41111042d43bb45c6c657b2e0db367d9e/lib/PhpParser/Node/Scalar/String_.php#L132-L154
	 */
	private static function codePointToUtf8(int $num): string
	{
		if ($num <= 0x7F) {
			return chr($num);
		}
		if ($num <= 0x7FF) {
			return chr(($num >> 6) + 0xC0)
				. chr(($num & 0x3F) + 0x80);
		}
		if ($num <= 0xFFFF) {
			return chr(($num >> 12) + 0xE0)
				. chr((($num >> 6) & 0x3F) + 0x80)
				. chr(($num & 0x3F) + 0x80);
		}
		if ($num <= 0x1FFFFF) {
			return chr(($num >> 18) + 0xF0)
				. chr((($num >> 12) & 0x3F) + 0x80)
				. chr((($num >> 6) & 0x3F) + 0x80)
				. chr(($num & 0x3F) + 0x80);
		}

		// Invalid UTF-8 codepoint escape sequence: Codepoint too large
		return "\xef\xbf\xbd";
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Parser;

use Exception;
use PHPStan\PhpDocParser\Lexer\Lexer;
use function assert;
use function json_encode;
use function sprintf;
use const JSON_INVALID_UTF8_SUBSTITUTE;
use const JSON_UNESCAPED_SLASHES;
use const JSON_UNESCAPED_UNICODE;

class ParserException extends Exception
{

	private string $currentTokenValue;

	private int $currentTokenType;

	private int $currentOffset;

	private int $expectedTokenType;

	private ?string $expectedTokenValue;

	private ?int $currentTokenLine;

	public function __construct(
		string $currentTokenValue,
		int $currentTokenType,
		int $currentOffset,
		int $expectedTokenType,
		?string $expectedTokenValue,
		?int $currentTokenLine
	)
	{
		$this->currentTokenValue = $currentTokenValue;
		$this->currentTokenType = $currentTokenType;
		$this->currentOffset = $currentOffset;
		$this->expectedTokenType = $expectedTokenType;
		$this->expectedTokenValue = $expectedTokenValue;
		$this->currentTokenLine = $currentTokenLine;

		parent::__construct(sprintf(
			'Unexpected token %s, expected %s%s at offset %d%s',
			$this->formatValue($currentTokenValue),
			Lexer::TOKEN_LABELS[$expectedTokenType],
			$expectedTokenValue !== null ? sprintf(' (%s)', $this->formatValue($expectedTokenValue)) : '',
			$currentOffset,
			$currentTokenLine === null ? '' : sprintf(' on line %d', $currentTokenLine),
		));
	}


	public function getCurrentTokenValue(): string
	{
		return $this->currentTokenValue;
	}


	public function getCurrentTokenType(): int
	{
		return $this->currentTokenType;
	}


	public function getCurrentOffset(): int
	{
		return $this->currentOffset;
	}


	public function getExpectedTokenType(): int
	{
		return $this->expectedTokenType;
	}


	public function getExpectedTokenValue(): ?string
	{
		return $this->expectedTokenValue;
	}


	public function getCurrentTokenLine(): ?int
	{
		return $this->currentTokenLine;
	}


	private function formatValue(string $value): string
	{
		$json = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE);
		assert($json !== false);

		return $json;
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Parser;

use LogicException;
use PHPStan\PhpDocParser\Ast\Comment;
use PHPStan\PhpDocParser\Lexer\Lexer;
use function array_pop;
use function assert;
use function count;
use function in_array;
use function strlen;
use function substr;

class TokenIterator
{

	/** @var list<array{string, int, int}> */
	private array $tokens;

	private int $index;

	/** @var list<Comment> */
	private array $comments = [];

	/** @var list<array{int, list<Comment>}> */
	private array $savePoints = [];

	/** @var list<int> */
	private array $skippedTokenTypes = [Lexer::TOKEN_HORIZONTAL_WS];

	private ?string $newline = null;

	/**
	 * @param list<array{string, int, int}> $tokens
	 */
	public function __construct(array $tokens, int $index = 0)
	{
		$this->tokens = $tokens;
		$this->index = $index;

		$this->skipIrrelevantTokens();
	}


	/**
	 * @return list<array{string, int, int}>
	 */
	public function getTokens(): array
	{
		return $this->tokens;
	}


	public function getContentBetween(int $startPos, int $endPos): string
	{
		if ($startPos < 0 || $endPos > count($this->tokens)) {
			throw new LogicException();
		}

		$content = '';
		for ($i = $startPos; $i < $endPos; $i++) {
			$content .= $this->tokens[$i][Lexer::VALUE_OFFSET];
		}

		return $content;
	}


	public function getTokenCount(): int
	{
		return count($this->tokens);
	}


	public function currentTokenValue(): string
	{
		return $this->tokens[$this->index][Lexer::VALUE_OFFSET];
	}


	public function currentTokenType(): int
	{
		return $this->tokens[$this->index][Lexer::TYPE_OFFSET];
	}


	public function currentTokenOffset(): int
	{
		$offset = 0;
		for ($i = 0; $i < $this->index; $i++) {
			$offset += strlen($this->tokens[$i][Lexer::VALUE_OFFSET]);
		}

		return $offset;
	}


	public function currentTokenLine(): int
	{
		return $this->tokens[$this->index][Lexer::LINE_OFFSET];
	}


	public function currentTokenIndex(): int
	{
		return $this->index;
	}


	public function endIndexOfLastRelevantToken(): int
	{
		$endIndex = $this->currentTokenIndex();
		$endIndex--;
		while (in_array($this->tokens[$endIndex][Lexer::TYPE_OFFSET], $this->skippedTokenTypes, true)) {
			if (!isset($this->tokens[$endIndex - 1])) {
				break;
			}
			$endIndex--;
		}

		return $endIndex;
	}


	public function isCurrentTokenValue(string $tokenValue): bool
	{
		return $this->tokens[$this->index][Lexer::VALUE_OFFSET] === $tokenValue;
	}


	public function isCurrentTokenType(int ...$tokenType): bool
	{
		return in_array($this->tokens[$this->index][Lexer::TYPE_OFFSET], $tokenType, true);
	}


	public function isPrecededByHorizontalWhitespace(): bool
	{
		return ($this->tokens[$this->index - 1][Lexer::TYPE_OFFSET] ?? -1) === Lexer::TOKEN_HORIZONTAL_WS;
	}


	/**
	 * @throws ParserException
	 */
	public function consumeTokenType(int $tokenType): void
	{
		if ($this->tokens[$this->index][Lexer::TYPE_OFFSET] !== $tokenType) {
			$this->throwError($tokenType);
		}

		if ($tokenType === Lexer::TOKEN_PHPDOC_EOL) {
			if ($this->newline === null) {
				$this->detectNewline();
			}
		}

		$this->next();
	}


	/**
	 * @throws ParserException
	 */
	public function consumeTokenValue(int $tokenType, string $tokenValue): void
	{
		if ($this->tokens[$this->index][Lexer::TYPE_OFFSET] !== $tokenType || $this->tokens[$this->index][Lexer::VALUE_OFFSET] !== $tokenValue) {
			$this->throwError($tokenType, $tokenValue);
		}

		$this->next();
	}


	/** @phpstan-impure */
	public function tryConsumeTokenValue(string $tokenValue): bool
	{
		if ($this->tokens[$this->index][Lexer::VALUE_OFFSET] !== $tokenValue) {
			return false;
		}

		$this->next();

		return true;
	}

	/**
	 * @return list<Comment>
	 */
	public function flushComments(): array
	{
		$res = $this->comments;
		$this->comments = [];
		return $res;
	}

	/** @phpstan-impure */
	public function tryConsumeTokenType(int $tokenType): bool
	{
		if ($this->tokens[$this->index][Lexer::TYPE_OFFSET] !== $tokenType) {
			return false;
		}

		if ($tokenType === Lexer::TOKEN_PHPDOC_EOL) {
			if ($this->newline === null) {
				$this->detectNewline();
			}
		}

		$this->next();

		return true;
	}


	/**
	 * @deprecated Use skipNewLineTokensAndConsumeComments instead (when parsing a type)
	 */
	public function skipNewLineTokens(): void
	{
		if (!$this->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL)) {
			return;
		}

		do {
			$foundNewLine = $this->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
		} while ($foundNewLine === true);
	}


	public function skipNewLineTokensAndConsumeComments(): void
	{
		if ($this->currentTokenType() === Lexer::TOKEN_COMMENT) {
			$this->comments[] = new Comment($this->currentTokenValue(), $this->currentTokenLine(), $this->currentTokenIndex());
			$this->next();
		}

		if (!$this->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL)) {
			return;
		}

		do {
			$foundNewLine = $this->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
			if ($this->currentTokenType() !== Lexer::TOKEN_COMMENT) {
				continue;
			}

			$this->comments[] = new Comment($this->currentTokenValue(), $this->currentTokenLine(), $this->currentTokenIndex());
			$this->next();
		} while ($foundNewLine === true);
	}


	private function detectNewline(): void
	{
		$value = $this->currentTokenValue();
		if (substr($value, 0, 2) === "\r\n") {
			$this->newline = "\r\n";
		} elseif (substr($value, 0, 1) === "\n") {
			$this->newline = "\n";
		}
	}


	public function getSkippedHorizontalWhiteSpaceIfAny(): string
	{
		if ($this->index > 0 && $this->tokens[$this->index - 1][Lexer::TYPE_OFFSET] === Lexer::TOKEN_HORIZONTAL_WS) {
			return $this->tokens[$this->index - 1][Lexer::VALUE_OFFSET];
		}

		return '';
	}


	/** @phpstan-impure */
	public function joinUntil(int ...$tokenType): string
	{
		$s = '';
		while (!in_array($this->tokens[$this->index][Lexer::TYPE_OFFSET], $tokenType, true)) {
			$s .= $this->tokens[$this->index++][Lexer::VALUE_OFFSET];
		}
		return $s;
	}


	public function next(): void
	{
		$this->index++;
		$this->skipIrrelevantTokens();
	}


	private function skipIrrelevantTokens(): void
	{
		if (!isset($this->tokens[$this->index])) {
			return;
		}

		while (in_array($this->tokens[$this->index][Lexer::TYPE_OFFSET], $this->skippedTokenTypes, true)) {
			if (!isset($this->tokens[$this->index + 1])) {
				break;
			}
			$this->index++;
		}
	}


	public function addEndOfLineToSkippedTokens(): void
	{
		$this->skippedTokenTypes = [Lexer::TOKEN_HORIZONTAL_WS, Lexer::TOKEN_PHPDOC_EOL];
	}


	public function removeEndOfLineFromSkippedTokens(): void
	{
		$this->skippedTokenTypes = [Lexer::TOKEN_HORIZONTAL_WS];
	}

	/** @phpstan-impure */
	public function forwardToTheEnd(): void
	{
		$lastToken = count($this->tokens) - 1;
		$this->index = $lastToken;
	}


	public function pushSavePoint(): void
	{
		$this->savePoints[] = [$this->index, $this->comments];
	}


	public function dropSavePoint(): void
	{
		array_pop($this->savePoints);
	}


	public function rollback(): void
	{
		$savepoint = array_pop($this->savePoints);
		assert($savepoint !== null);
		[$this->index, $this->comments] = $savepoint;
	}


	/**
	 * @throws ParserException
	 */
	private function throwError(int $expectedTokenType, ?string $expectedTokenValue = null): void
	{
		throw new ParserException(
			$this->currentTokenValue(),
			$this->currentTokenType(),
			$this->currentTokenOffset(),
			$expectedTokenType,
			$expectedTokenValue,
			$this->currentTokenLine(),
		);
	}

	/**
	 * Check whether the position is directly preceded by a certain token type.
	 *
	 * During this check TOKEN_HORIZONTAL_WS and TOKEN_PHPDOC_EOL are skipped
	 */
	public function hasTokenImmediatelyBefore(int $pos, int $expectedTokenType): bool
	{
		$tokens = $this->tokens;
		$pos--;
		for (; $pos >= 0; $pos--) {
			$token = $tokens[$pos];
			$type = $token[Lexer::TYPE_OFFSET];
			if ($type === $expectedTokenType) {
				return true;
			}
			if (!in_array($type, [
				Lexer::TOKEN_HORIZONTAL_WS,
				Lexer::TOKEN_PHPDOC_EOL,
			], true)) {
				break;
			}
		}
		return false;
	}

	/**
	 * Check whether the position is directly followed by a certain token type.
	 *
	 * During this check TOKEN_HORIZONTAL_WS and TOKEN_PHPDOC_EOL are skipped
	 */
	public function hasTokenImmediatelyAfter(int $pos, int $expectedTokenType): bool
	{
		$tokens = $this->tokens;
		$pos++;
		for ($c = count($tokens); $pos < $c; $pos++) {
			$token = $tokens[$pos];
			$type = $token[Lexer::TYPE_OFFSET];
			if ($type === $expectedTokenType) {
				return true;
			}
			if (!in_array($type, [
				Lexer::TOKEN_HORIZONTAL_WS,
				Lexer::TOKEN_PHPDOC_EOL,
			], true)) {
				break;
			}
		}

		return false;
	}

	public function getDetectedNewline(): ?string
	{
		return $this->newline;
	}

	/**
	 * Whether the given position is immediately surrounded by parenthesis.
	 */
	public function hasParentheses(int $startPos, int $endPos): bool
	{
		return $this->hasTokenImmediatelyBefore($startPos, Lexer::TOKEN_OPEN_PARENTHESES)
			&& $this->hasTokenImmediatelyAfter($endPos, Lexer::TOKEN_CLOSE_PARENTHESES);
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Parser;

use LogicException;
use PHPStan\PhpDocParser\Ast;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Lexer\Lexer;
use PHPStan\PhpDocParser\ParserConfig;
use PHPStan\ShouldNotHappenException;
use function array_key_exists;
use function count;
use function rtrim;
use function str_replace;
use function trim;

/**
 * @phpstan-import-type ValueType from Doctrine\DoctrineArgument as DoctrineValueType
 */
class PhpDocParser
{

	private const DISALLOWED_DESCRIPTION_START_TOKENS = [
		Lexer::TOKEN_UNION,
		Lexer::TOKEN_INTERSECTION,
	];

	private ParserConfig $config;

	private TypeParser $typeParser;

	private ConstExprParser $constantExprParser;

	private ConstExprParser $doctrineConstantExprParser;

	public function __construct(
		ParserConfig $config,
		TypeParser $typeParser,
		ConstExprParser $constantExprParser
	)
	{
		$this->config = $config;
		$this->typeParser = $typeParser;
		$this->constantExprParser = $constantExprParser;
		$this->doctrineConstantExprParser = $constantExprParser->toDoctrine();
	}


	public function parse(TokenIterator $tokens): Ast\PhpDoc\PhpDocNode
	{
		$tokens->consumeTokenType(Lexer::TOKEN_OPEN_PHPDOC);
		$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);

		$children = [];

		if (!$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PHPDOC)) {
			$lastChild = $this->parseChild($tokens);
			$children[] = $lastChild;
			while (!$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PHPDOC)) {
				if (
					$lastChild instanceof Ast\PhpDoc\PhpDocTagNode
					&& (
						$lastChild->value instanceof Doctrine\DoctrineTagValueNode
						|| $lastChild->value instanceof Ast\PhpDoc\GenericTagValueNode
					)
				) {
					$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
					if ($tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PHPDOC)) {
						break;
					}
					$lastChild = $this->parseChild($tokens);
					$children[] = $lastChild;
					continue;
				}

				if (!$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL)) {
					break;
				}
				if ($tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PHPDOC)) {
					break;
				}

				$lastChild = $this->parseChild($tokens);
				$children[] = $lastChild;
			}
		}

		try {
			$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PHPDOC);
		} catch (ParserException $e) {
			$name = '';
			$startLine = $tokens->currentTokenLine();
			$startIndex = $tokens->currentTokenIndex();
			if (count($children) > 0) {
				$lastChild = $children[count($children) - 1];
				if ($lastChild instanceof Ast\PhpDoc\PhpDocTagNode) {
					$name = $lastChild->name;
					$startLine = $tokens->currentTokenLine();
					$startIndex = $tokens->currentTokenIndex();
				}
			}

			$tag = new Ast\PhpDoc\PhpDocTagNode(
				$name,
				$this->enrichWithAttributes(
					$tokens,
					new Ast\PhpDoc\InvalidTagValueNode($e->getMessage(), $e),
					$startLine,
					$startIndex,
				),
			);

			$tokens->forwardToTheEnd();

			$comments = $tokens->flushComments();
			if ($comments !== []) {
				throw new LogicException('Comments should already be flushed');
			}

			return $this->enrichWithAttributes($tokens, new Ast\PhpDoc\PhpDocNode([$this->enrichWithAttributes($tokens, $tag, $startLine, $startIndex)]), 1, 0);
		}

		$comments = $tokens->flushComments();
		if ($comments !== []) {
			throw new LogicException('Comments should already be flushed');
		}

		return $this->enrichWithAttributes($tokens, new Ast\PhpDoc\PhpDocNode($children), 1, 0);
	}


	/** @phpstan-impure */
	private function parseChild(TokenIterator $tokens): Ast\PhpDoc\PhpDocChildNode
	{
		if ($tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_TAG)) {
			$startLine = $tokens->currentTokenLine();
			$startIndex = $tokens->currentTokenIndex();
			return $this->enrichWithAttributes($tokens, $this->parseTag($tokens), $startLine, $startIndex);
		}

		if ($tokens->isCurrentTokenType(Lexer::TOKEN_DOCTRINE_TAG)) {
			$startLine = $tokens->currentTokenLine();
			$startIndex = $tokens->currentTokenIndex();
			$tag = $tokens->currentTokenValue();
			$tokens->next();

			$tagStartLine = $tokens->currentTokenLine();
			$tagStartIndex = $tokens->currentTokenIndex();

			return $this->enrichWithAttributes($tokens, new Ast\PhpDoc\PhpDocTagNode(
				$tag,
				$this->enrichWithAttributes(
					$tokens,
					$this->parseDoctrineTagValue($tokens, $tag),
					$tagStartLine,
					$tagStartIndex,
				),
			), $startLine, $startIndex);
		}

		$startLine = $tokens->currentTokenLine();
		$startIndex = $tokens->currentTokenIndex();
		$text = $this->parseText($tokens);

		return $this->enrichWithAttributes($tokens, $text, $startLine, $startIndex);
	}

	/**
	 * @template T of Ast\Node
	 * @param T $tag
	 * @return T
	 */
	private function enrichWithAttributes(TokenIterator $tokens, Ast\Node $tag, int $startLine, int $startIndex): Ast\Node
	{
		if ($this->config->useLinesAttributes) {
			$tag->setAttribute(Ast\Attribute::START_LINE, $startLine);
			$tag->setAttribute(Ast\Attribute::END_LINE, $tokens->currentTokenLine());
		}

		if ($this->config->useIndexAttributes) {
			$tag->setAttribute(Ast\Attribute::START_INDEX, $startIndex);
			$tag->setAttribute(Ast\Attribute::END_INDEX, $tokens->endIndexOfLastRelevantToken());
		}

		return $tag;
	}


	private function parseText(TokenIterator $tokens): Ast\PhpDoc\PhpDocTextNode
	{
		$text = '';

		$endTokens = [Lexer::TOKEN_CLOSE_PHPDOC, Lexer::TOKEN_END];

		$savepoint = false;

		// if the next token is EOL, everything below is skipped and empty string is returned
		while (true) {
			$tmpText = $tokens->getSkippedHorizontalWhiteSpaceIfAny() . $tokens->joinUntil(Lexer::TOKEN_PHPDOC_EOL, ...$endTokens);
			$text .= $tmpText;

			// stop if we're not at EOL - meaning it's the end of PHPDoc
			if (!$tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL, Lexer::TOKEN_CLOSE_PHPDOC)) {
				break;
			}

			if (!$savepoint) {
				$tokens->pushSavePoint();
				$savepoint = true;
			} elseif ($tmpText !== '') {
				$tokens->dropSavePoint();
				$tokens->pushSavePoint();
			}

			$tokens->pushSavePoint();
			$tokens->next();

			// if we're at EOL, check what's next
			// if next is a PHPDoc tag, EOL, or end of PHPDoc, stop
			if ($tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_TAG, Lexer::TOKEN_DOCTRINE_TAG, ...$endTokens)) {
				$tokens->rollback();
				break;
			}

			// otherwise if the next is text, continue building the description string

			$tokens->dropSavePoint();
			$text .= $tokens->getDetectedNewline() ?? "\n";
		}

		if ($savepoint) {
			$tokens->rollback();
			$text = rtrim($text, $tokens->getDetectedNewline() ?? "\n");
		}

		return new Ast\PhpDoc\PhpDocTextNode(trim($text, " \t"));
	}


	private function parseOptionalDescriptionAfterDoctrineTag(TokenIterator $tokens): string
	{
		$text = '';

		$endTokens = [Lexer::TOKEN_CLOSE_PHPDOC, Lexer::TOKEN_END];

		$savepoint = false;

		// if the next token is EOL, everything below is skipped and empty string is returned
		while (true) {
			$tmpText = $tokens->getSkippedHorizontalWhiteSpaceIfAny() . $tokens->joinUntil(Lexer::TOKEN_PHPDOC_TAG, Lexer::TOKEN_DOCTRINE_TAG, Lexer::TOKEN_PHPDOC_EOL, ...$endTokens);
			$text .= $tmpText;

			// stop if we're not at EOL - meaning it's the end of PHPDoc
			if (!$tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL, Lexer::TOKEN_CLOSE_PHPDOC)) {
				if (!$tokens->isPrecededByHorizontalWhitespace()) {
					return trim($text . $this->parseText($tokens)->text, " \t");
				}
				if ($tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_TAG)) {
					$tokens->pushSavePoint();
					$child = $this->parseChild($tokens);
					if ($child instanceof Ast\PhpDoc\PhpDocTagNode) {
						if (
							$child->value instanceof Ast\PhpDoc\GenericTagValueNode
							|| $child->value instanceof Doctrine\DoctrineTagValueNode
						) {
							$tokens->rollback();
							break;
						}
						if ($child->value instanceof Ast\PhpDoc\InvalidTagValueNode) {
							$tokens->rollback();
							$tokens->pushSavePoint();
							$tokens->next();
							if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) {
								$tokens->rollback();
								break;
							}
							$tokens->rollback();
							return trim($text . $this->parseText($tokens)->text, " \t");
						}
					}

					$tokens->rollback();
					return trim($text . $this->parseText($tokens)->text, " \t");
				}
				break;
			}

			if (!$savepoint) {
				$tokens->pushSavePoint();
				$savepoint = true;
			} elseif ($tmpText !== '') {
				$tokens->dropSavePoint();
				$tokens->pushSavePoint();
			}

			$tokens->pushSavePoint();
			$tokens->next();

			// if we're at EOL, check what's next
			// if next is a PHPDoc tag, EOL, or end of PHPDoc, stop
			if ($tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_TAG, Lexer::TOKEN_DOCTRINE_TAG, ...$endTokens)) {
				$tokens->rollback();
				break;
			}

			// otherwise if the next is text, continue building the description string

			$tokens->dropSavePoint();
			$text .= $tokens->getDetectedNewline() ?? "\n";
		}

		if ($savepoint) {
			$tokens->rollback();
			$text = rtrim($text, $tokens->getDetectedNewline() ?? "\n");
		}

		return trim($text, " \t");
	}


	public function parseTag(TokenIterator $tokens): Ast\PhpDoc\PhpDocTagNode
	{
		$tag = $tokens->currentTokenValue();
		$tokens->next();
		$value = $this->parseTagValue($tokens, $tag);

		return new Ast\PhpDoc\PhpDocTagNode($tag, $value);
	}


	public function parseTagValue(TokenIterator $tokens, string $tag): Ast\PhpDoc\PhpDocTagValueNode
	{
		$startLine = $tokens->currentTokenLine();
		$startIndex = $tokens->currentTokenIndex();

		try {
			$tokens->pushSavePoint();

			switch ($tag) {
				case '@param':
				case '@phpstan-param':
				case '@psalm-param':
				case '@phan-param':
					$tagValue = $this->parseParamTagValue($tokens);
					break;

				case '@param-immediately-invoked-callable':
				case '@phpstan-param-immediately-invoked-callable':
					$tagValue = $this->parseParamImmediatelyInvokedCallableTagValue($tokens);
					break;

				case '@param-later-invoked-callable':
				case '@phpstan-param-later-invoked-callable':
					$tagValue = $this->parseParamLaterInvokedCallableTagValue($tokens);
					break;

				case '@param-closure-this':
				case '@phpstan-param-closure-this':
					$tagValue = $this->parseParamClosureThisTagValue($tokens);
					break;

				case '@pure-unless-callable-is-impure':
				case '@phpstan-pure-unless-callable-is-impure':
					$tagValue = $this->parsePureUnlessCallableIsImpureTagValue($tokens);
					break;

				case '@var':
				case '@phpstan-var':
				case '@psalm-var':
				case '@phan-var':
					$tagValue = $this->parseVarTagValue($tokens);
					break;

				case '@return':
				case '@phpstan-return':
				case '@psalm-return':
				case '@phan-return':
				case '@phan-real-return':
					$tagValue = $this->parseReturnTagValue($tokens);
					break;

				case '@throws':
				case '@phpstan-throws':
					$tagValue = $this->parseThrowsTagValue($tokens);
					break;

				case '@mixin':
				case '@phan-mixin':
					$tagValue = $this->parseMixinTagValue($tokens);
					break;

				case '@psalm-require-extends':
				case '@phpstan-require-extends':
					$tagValue = $this->parseRequireExtendsTagValue($tokens);
					break;

				case '@psalm-require-implements':
				case '@phpstan-require-implements':
					$tagValue = $this->parseRequireImplementsTagValue($tokens);
					break;

				case '@psalm-inheritors':
				case '@phpstan-sealed':
					$tagValue = $this->parseSealedTagValue($tokens);
					break;

				case '@deprecated':
					$tagValue = $this->parseDeprecatedTagValue($tokens);
					break;

				case '@property':
				case '@property-read':
				case '@property-write':
				case '@phpstan-property':
				case '@phpstan-property-read':
				case '@phpstan-property-write':
				case '@psalm-property':
				case '@psalm-property-read':
				case '@psalm-property-write':
				case '@phan-property':
				case '@phan-property-read':
				case '@phan-property-write':
					$tagValue = $this->parsePropertyTagValue($tokens);
					break;

				case '@method':
				case '@phpstan-method':
				case '@psalm-method':
				case '@phan-method':
					$tagValue = $this->parseMethodTagValue($tokens);
					break;

				case '@template':
				case '@phpstan-template':
				case '@psalm-template':
				case '@phan-template':
				case '@template-covariant':
				case '@phpstan-template-covariant':
				case '@psalm-template-covariant':
				case '@template-contravariant':
				case '@phpstan-template-contravariant':
				case '@psalm-template-contravariant':
					$tagValue = $this->typeParser->parseTemplateTagValue(
						$tokens,
						fn ($tokens) => $this->parseOptionalDescription($tokens, true),
					);
					break;

				case '@extends':
				case '@phpstan-extends':
				case '@phan-extends':
				case '@phan-inherits':
				case '@template-extends':
					$tagValue = $this->parseExtendsTagValue('@extends', $tokens);
					break;

				case '@implements':
				case '@phpstan-implements':
				case '@template-implements':
					$tagValue = $this->parseExtendsTagValue('@implements', $tokens);
					break;

				case '@use':
				case '@phpstan-use':
				case '@template-use':
					$tagValue = $this->parseExtendsTagValue('@use', $tokens);
					break;

				case '@phpstan-type':
				case '@psalm-type':
				case '@phan-type':
					$tagValue = $this->parseTypeAliasTagValue($tokens);
					break;

				case '@phpstan-import-type':
				case '@psalm-import-type':
					$tagValue = $this->parseTypeAliasImportTagValue($tokens);
					break;

				case '@phpstan-assert':
				case '@phpstan-assert-if-true':
				case '@phpstan-assert-if-false':
				case '@psalm-assert':
				case '@psalm-assert-if-true':
				case '@psalm-assert-if-false':
				case '@phan-assert':
				case '@phan-assert-if-true':
				case '@phan-assert-if-false':
					$tagValue = $this->parseAssertTagValue($tokens);
					break;

				case '@phpstan-this-out':
				case '@phpstan-self-out':
				case '@psalm-this-out':
				case '@psalm-self-out':
					$tagValue = $this->parseSelfOutTagValue($tokens);
					break;

				case '@param-out':
				case '@phpstan-param-out':
				case '@psalm-param-out':
					$tagValue = $this->parseParamOutTagValue($tokens);
					break;

				default:
					if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) {
						$tagValue = $this->parseDoctrineTagValue($tokens, $tag);
					} else {
						$tagValue = new Ast\PhpDoc\GenericTagValueNode($this->parseOptionalDescriptionAfterDoctrineTag($tokens));
					}
					break;
			}

			$tokens->dropSavePoint();

		} catch (ParserException $e) {
			$tokens->rollback();
			$tagValue = new Ast\PhpDoc\InvalidTagValueNode($this->parseOptionalDescription($tokens, false), $e);
		}

		return $this->enrichWithAttributes($tokens, $tagValue, $startLine, $startIndex);
	}


	private function parseDoctrineTagValue(TokenIterator $tokens, string $tag): Ast\PhpDoc\PhpDocTagValueNode
	{
		$startLine = $tokens->currentTokenLine();
		$startIndex = $tokens->currentTokenIndex();

		return new Doctrine\DoctrineTagValueNode(
			$this->enrichWithAttributes(
				$tokens,
				new Doctrine\DoctrineAnnotation($tag, $this->parseDoctrineArguments($tokens, false)),
				$startLine,
				$startIndex,
			),
			$this->parseOptionalDescriptionAfterDoctrineTag($tokens),
		);
	}


	/**
	 * @return list<Doctrine\DoctrineArgument>
	 */
	private function parseDoctrineArguments(TokenIterator $tokens, bool $deep): array
	{
		if (!$tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) {
			return [];
		}

		if (!$deep) {
			$tokens->addEndOfLineToSkippedTokens();
		}

		$arguments = [];

		try {
			$tokens->consumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES);

			do {
				if ($tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PARENTHESES)) {
					break;
				}
				$arguments[] = $this->parseDoctrineArgument($tokens);
			} while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA));
		} finally {
			if (!$deep) {
				$tokens->removeEndOfLineFromSkippedTokens();
			}
		}

		$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES);

		return $arguments;
	}


	private function parseDoctrineArgument(TokenIterator $tokens): Doctrine\DoctrineArgument
	{
		if (!$tokens->isCurrentTokenType(Lexer::TOKEN_IDENTIFIER)) {
			$startLine = $tokens->currentTokenLine();
			$startIndex = $tokens->currentTokenIndex();

			return $this->enrichWithAttributes(
				$tokens,
				new Doctrine\DoctrineArgument(null, $this->parseDoctrineArgumentValue($tokens)),
				$startLine,
				$startIndex,
			);
		}

		$startLine = $tokens->currentTokenLine();
		$startIndex = $tokens->currentTokenIndex();

		try {
			$tokens->pushSavePoint();
			$currentValue = $tokens->currentTokenValue();
			$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);

			$key = $this->enrichWithAttributes(
				$tokens,
				new IdentifierTypeNode($currentValue),
				$startLine,
				$startIndex,
			);
			$tokens->consumeTokenType(Lexer::TOKEN_EQUAL);

			$value = $this->parseDoctrineArgumentValue($tokens);

			$tokens->dropSavePoint();

			return $this->enrichWithAttributes(
				$tokens,
				new Doctrine\DoctrineArgument($key, $value),
				$startLine,
				$startIndex,
			);
		} catch (ParserException $e) {
			$tokens->rollback();

			return $this->enrichWithAttributes(
				$tokens,
				new Doctrine\DoctrineArgument(null, $this->parseDoctrineArgumentValue($tokens)),
				$startLine,
				$startIndex,
			);
		}
	}


	/**
	 * @return DoctrineValueType
	 */
	private function parseDoctrineArgumentValue(TokenIterator $tokens)
	{
		$startLine = $tokens->currentTokenLine();
		$startIndex = $tokens->currentTokenIndex();

		if ($tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_TAG, Lexer::TOKEN_DOCTRINE_TAG)) {
			$name = $tokens->currentTokenValue();
			$tokens->next();

			return $this->enrichWithAttributes(
				$tokens,
				new Doctrine\DoctrineAnnotation($name, $this->parseDoctrineArguments($tokens, true)),
				$startLine,
				$startIndex,
			);
		}

		if ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET)) {
			$items = [];
			do {
				if ($tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET)) {
					break;
				}
				$items[] = $this->parseDoctrineArrayItem($tokens);
			} while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA));

			$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET);

			return $this->enrichWithAttributes(
				$tokens,
				new Doctrine\DoctrineArray($items),
				$startLine,
				$startIndex,
			);
		}

		$currentTokenValue = $tokens->currentTokenValue();
		$tokens->pushSavePoint(); // because of ConstFetchNode
		if ($tokens->tryConsumeTokenType(Lexer::TOKEN_IDENTIFIER)) {
			$identifier = $this->enrichWithAttributes(
				$tokens,
				new Ast\Type\IdentifierTypeNode($currentTokenValue),
				$startLine,
				$startIndex,
			);
			if (!$tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_COLON)) {
				$tokens->dropSavePoint();
				return $identifier;
			}

			$tokens->rollback(); // because of ConstFetchNode
		} else {
			$tokens->dropSavePoint(); // because of ConstFetchNode
		}

		$currentTokenValue = $tokens->currentTokenValue();
		$currentTokenType = $tokens->currentTokenType();
		$currentTokenOffset = $tokens->currentTokenOffset();
		$currentTokenLine = $tokens->currentTokenLine();

		try {
			$constExpr = $this->doctrineConstantExprParser->parse($tokens);
			if ($constExpr instanceof Ast\ConstExpr\ConstExprArrayNode) {
				throw new ParserException(
					$currentTokenValue,
					$currentTokenType,
					$currentTokenOffset,
					Lexer::TOKEN_IDENTIFIER,
					null,
					$currentTokenLine,
				);
			}

			return $constExpr;
		} catch (LogicException $e) {
			throw new ParserException(
				$currentTokenValue,
				$currentTokenType,
				$currentTokenOffset,
				Lexer::TOKEN_IDENTIFIER,
				null,
				$currentTokenLine,
			);
		}
	}


	private function parseDoctrineArrayItem(TokenIterator $tokens): Doctrine\DoctrineArrayItem
	{
		$startLine = $tokens->currentTokenLine();
		$startIndex = $tokens->currentTokenIndex();

		try {
			$tokens->pushSavePoint();

			$key = $this->parseDoctrineArrayKey($tokens);
			if (!$tokens->tryConsumeTokenType(Lexer::TOKEN_EQUAL)) {
				if (!$tokens->tryConsumeTokenType(Lexer::TOKEN_COLON)) {
					$tokens->consumeTokenType(Lexer::TOKEN_EQUAL); // will throw exception
				}
			}

			$value = $this->parseDoctrineArgumentValue($tokens);

			$tokens->dropSavePoint();

			return $this->enrichWithAttributes(
				$tokens,
				new Doctrine\DoctrineArrayItem($key, $value),
				$startLine,
				$startIndex,
			);
		} catch (ParserException $e) {
			$tokens->rollback();

			return $this->enrichWithAttributes(
				$tokens,
				new Doctrine\DoctrineArrayItem(null, $this->parseDoctrineArgumentValue($tokens)),
				$startLine,
				$startIndex,
			);
		}
	}


	/**
	 * @return ConstExprIntegerNode|ConstExprStringNode|IdentifierTypeNode|ConstFetchNode
	 */
	private function parseDoctrineArrayKey(TokenIterator $tokens)
	{
		$startLine = $tokens->currentTokenLine();
		$startIndex = $tokens->currentTokenIndex();

		if ($tokens->isCurrentTokenType(Lexer::TOKEN_INTEGER)) {
			$key = new Ast\ConstExpr\ConstExprIntegerNode(str_replace('_', '', $tokens->currentTokenValue()));
			$tokens->next();

		} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_DOCTRINE_ANNOTATION_STRING)) {
			$key = $this->doctrineConstantExprParser->parseDoctrineString($tokens->currentTokenValue(), $tokens);

			$tokens->next();

		} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING)) {
			$key = new Ast\ConstExpr\ConstExprStringNode(StringUnescaper::unescapeString($tokens->currentTokenValue()), Ast\ConstExpr\ConstExprStringNode::SINGLE_QUOTED);
			$tokens->next();

		} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_QUOTED_STRING)) {
			$value = $tokens->currentTokenValue();
			$tokens->next();
			$key = $this->doctrineConstantExprParser->parseDoctrineString($value, $tokens);

		} else {
			$currentTokenValue = $tokens->currentTokenValue();
			$tokens->pushSavePoint(); // because of ConstFetchNode
			if (!$tokens->tryConsumeTokenType(Lexer::TOKEN_IDENTIFIER)) {
				$tokens->dropSavePoint();
				throw new ParserException(
					$tokens->currentTokenValue(),
					$tokens->currentTokenType(),
					$tokens->currentTokenOffset(),
					Lexer::TOKEN_IDENTIFIER,
					null,
					$tokens->currentTokenLine(),
				);
			}

			if (!$tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_COLON)) {
				$tokens->dropSavePoint();

				return $this->enrichWithAttributes(
					$tokens,
					new IdentifierTypeNode($currentTokenValue),
					$startLine,
					$startIndex,
				);
			}

			$tokens->rollback();
			$constExpr = $this->doctrineConstantExprParser->parse($tokens);
			if (!$constExpr instanceof Ast\ConstExpr\ConstFetchNode) {
				throw new ParserException(
					$tokens->currentTokenValue(),
					$tokens->currentTokenType(),
					$tokens->currentTokenOffset(),
					Lexer::TOKEN_IDENTIFIER,
					null,
					$tokens->currentTokenLine(),
				);
			}

			return $constExpr;
		}

		return $this->enrichWithAttributes($tokens, $key, $startLine, $startIndex);
	}


	/**
	 * @return Ast\PhpDoc\ParamTagValueNode|Ast\PhpDoc\TypelessParamTagValueNode
	 */
	private function parseParamTagValue(TokenIterator $tokens): Ast\PhpDoc\PhpDocTagValueNode
	{
		if (
			$tokens->isCurrentTokenType(Lexer::TOKEN_REFERENCE, Lexer::TOKEN_VARIADIC, Lexer::TOKEN_VARIABLE)
		) {
			$type = null;
		} else {
			$type = $this->typeParser->parse($tokens);
		}

		$isReference = $tokens->tryConsumeTokenType(Lexer::TOKEN_REFERENCE);
		$isVariadic = $tokens->tryConsumeTokenType(Lexer::TOKEN_VARIADIC);
		$parameterName = $this->parseRequiredVariableName($tokens);
		$description = $this->parseOptionalDescription($tokens, false);

		if ($type !== null) {
			return new Ast\PhpDoc\ParamTagValueNode($type, $isVariadic, $parameterName, $description, $isReference);
		}

		return new Ast\PhpDoc\TypelessParamTagValueNode($isVariadic, $parameterName, $description, $isReference);
	}


	private function parseParamImmediatelyInvokedCallableTagValue(TokenIterator $tokens): Ast\PhpDoc\ParamImmediatelyInvokedCallableTagValueNode
	{
		$parameterName = $this->parseRequiredVariableName($tokens);
		$description = $this->parseOptionalDescription($tokens, false);

		return new Ast\PhpDoc\ParamImmediatelyInvokedCallableTagValueNode($parameterName, $description);
	}


	private function parseParamLaterInvokedCallableTagValue(TokenIterator $tokens): Ast\PhpDoc\ParamLaterInvokedCallableTagValueNode
	{
		$parameterName = $this->parseRequiredVariableName($tokens);
		$description = $this->parseOptionalDescription($tokens, false);

		return new Ast\PhpDoc\ParamLaterInvokedCallableTagValueNode($parameterName, $description);
	}


	private function parseParamClosureThisTagValue(TokenIterator $tokens): Ast\PhpDoc\ParamClosureThisTagValueNode
	{
		$type = $this->typeParser->parse($tokens);
		$parameterName = $this->parseRequiredVariableName($tokens);
		$description = $this->parseOptionalDescription($tokens, false);

		return new Ast\PhpDoc\ParamClosureThisTagValueNode($type, $parameterName, $description);
	}

	private function parsePureUnlessCallableIsImpureTagValue(TokenIterator $tokens): Ast\PhpDoc\PureUnlessCallableIsImpureTagValueNode
	{
		$parameterName = $this->parseRequiredVariableName($tokens);
		$description = $this->parseOptionalDescription($tokens, false);

		return new Ast\PhpDoc\PureUnlessCallableIsImpureTagValueNode($parameterName, $description);
	}

	private function parseVarTagValue(TokenIterator $tokens): Ast\PhpDoc\VarTagValueNode
	{
		$type = $this->typeParser->parse($tokens);
		$variableName = $this->parseOptionalVariableName($tokens);
		$description = $this->parseOptionalDescription($tokens, $variableName === '');
		return new Ast\PhpDoc\VarTagValueNode($type, $variableName, $description);
	}


	private function parseReturnTagValue(TokenIterator $tokens): Ast\PhpDoc\ReturnTagValueNode
	{
		$type = $this->typeParser->parse($tokens);
		$description = $this->parseOptionalDescription($tokens, true);
		return new Ast\PhpDoc\ReturnTagValueNode($type, $description);
	}


	private function parseThrowsTagValue(TokenIterator $tokens): Ast\PhpDoc\ThrowsTagValueNode
	{
		$type = $this->typeParser->parse($tokens);
		$description = $this->parseOptionalDescription($tokens, true);
		return new Ast\PhpDoc\ThrowsTagValueNode($type, $description);
	}

	private function parseMixinTagValue(TokenIterator $tokens): Ast\PhpDoc\MixinTagValueNode
	{
		$type = $this->typeParser->parse($tokens);
		$description = $this->parseOptionalDescription($tokens, true);
		return new Ast\PhpDoc\MixinTagValueNode($type, $description);
	}

	private function parseRequireExtendsTagValue(TokenIterator $tokens): Ast\PhpDoc\RequireExtendsTagValueNode
	{
		$type = $this->typeParser->parse($tokens);
		$description = $this->parseOptionalDescription($tokens, true);
		return new Ast\PhpDoc\RequireExtendsTagValueNode($type, $description);
	}

	private function parseRequireImplementsTagValue(TokenIterator $tokens): Ast\PhpDoc\RequireImplementsTagValueNode
	{
		$type = $this->typeParser->parse($tokens);
		$description = $this->parseOptionalDescription($tokens, true);
		return new Ast\PhpDoc\RequireImplementsTagValueNode($type, $description);
	}

	private function parseSealedTagValue(TokenIterator $tokens): Ast\PhpDoc\SealedTagValueNode
	{
		$type = $this->typeParser->parse($tokens);
		$description = $this->parseOptionalDescription($tokens, true);
		return new Ast\PhpDoc\SealedTagValueNode($type, $description);
	}

	private function parseDeprecatedTagValue(TokenIterator $tokens): Ast\PhpDoc\DeprecatedTagValueNode
	{
		$description = $this->parseOptionalDescription($tokens, false);
		return new Ast\PhpDoc\DeprecatedTagValueNode($description);
	}


	private function parsePropertyTagValue(TokenIterator $tokens): Ast\PhpDoc\PropertyTagValueNode
	{
		$type = $this->typeParser->parse($tokens);
		$parameterName = $this->parseRequiredVariableName($tokens);
		$description = $this->parseOptionalDescription($tokens, false);
		return new Ast\PhpDoc\PropertyTagValueNode($type, $parameterName, $description);
	}


	private function parseMethodTagValue(TokenIterator $tokens): Ast\PhpDoc\MethodTagValueNode
	{
		$staticKeywordOrReturnTypeOrMethodName = $this->typeParser->parse($tokens);

		if ($staticKeywordOrReturnTypeOrMethodName instanceof Ast\Type\IdentifierTypeNode && $staticKeywordOrReturnTypeOrMethodName->name === 'static') {
			$isStatic = true;
			$returnTypeOrMethodName = $this->typeParser->parse($tokens);

		} else {
			$isStatic = false;
			$returnTypeOrMethodName = $staticKeywordOrReturnTypeOrMethodName;
		}

		if ($tokens->isCurrentTokenType(Lexer::TOKEN_IDENTIFIER)) {
			$returnType = $returnTypeOrMethodName;
			$methodName = $tokens->currentTokenValue();
			$tokens->next();

		} elseif ($returnTypeOrMethodName instanceof Ast\Type\IdentifierTypeNode) {
			$returnType = $isStatic ? $staticKeywordOrReturnTypeOrMethodName : null;
			$methodName = $returnTypeOrMethodName->name;
			$isStatic = false;

		} else {
			$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); // will throw exception
			exit;
		}

		$templateTypes = [];

		if ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) {
			do {
				$startLine = $tokens->currentTokenLine();
				$startIndex = $tokens->currentTokenIndex();
				$templateTypes[] = $this->enrichWithAttributes(
					$tokens,
					$this->typeParser->parseTemplateTagValue($tokens),
					$startLine,
					$startIndex,
				);
			} while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA));
			$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET);
		}

		$parameters = [];
		$tokens->consumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES);
		if (!$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PARENTHESES)) {
			$parameters[] = $this->parseMethodTagValueParameter($tokens);
			while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) {
				$parameters[] = $this->parseMethodTagValueParameter($tokens);
			}
		}
		$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES);

		$description = $this->parseOptionalDescription($tokens, false);
		return new Ast\PhpDoc\MethodTagValueNode($isStatic, $returnType, $methodName, $parameters, $description, $templateTypes);
	}

	private function parseMethodTagValueParameter(TokenIterator $tokens): Ast\PhpDoc\MethodTagValueParameterNode
	{
		$startLine = $tokens->currentTokenLine();
		$startIndex = $tokens->currentTokenIndex();

		switch ($tokens->currentTokenType()) {
			case Lexer::TOKEN_IDENTIFIER:
			case Lexer::TOKEN_OPEN_PARENTHESES:
			case Lexer::TOKEN_NULLABLE:
				$parameterType = $this->typeParser->parse($tokens);
				break;

			default:
				$parameterType = null;
		}

		$isReference = $tokens->tryConsumeTokenType(Lexer::TOKEN_REFERENCE);
		$isVariadic = $tokens->tryConsumeTokenType(Lexer::TOKEN_VARIADIC);

		$parameterName = $tokens->currentTokenValue();
		$tokens->consumeTokenType(Lexer::TOKEN_VARIABLE);

		if ($tokens->tryConsumeTokenType(Lexer::TOKEN_EQUAL)) {
			$defaultValue = $this->constantExprParser->parse($tokens);

		} else {
			$defaultValue = null;
		}

		return $this->enrichWithAttributes(
			$tokens,
			new Ast\PhpDoc\MethodTagValueParameterNode($parameterType, $isReference, $isVariadic, $parameterName, $defaultValue),
			$startLine,
			$startIndex,
		);
	}

	private function parseExtendsTagValue(string $tagName, TokenIterator $tokens): Ast\PhpDoc\PhpDocTagValueNode
	{
		$startLine = $tokens->currentTokenLine();
		$startIndex = $tokens->currentTokenIndex();
		$baseType = new IdentifierTypeNode($tokens->currentTokenValue());
		$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);

		$type = $this->typeParser->parseGeneric(
			$tokens,
			$this->typeParser->enrichWithAttributes($tokens, $baseType, $startLine, $startIndex),
		);

		$description = $this->parseOptionalDescription($tokens, true);

		switch ($tagName) {
			case '@extends':
				return new Ast\PhpDoc\ExtendsTagValueNode($type, $description);
			case '@implements':
				return new Ast\PhpDoc\ImplementsTagValueNode($type, $description);
			case '@use':
				return new Ast\PhpDoc\UsesTagValueNode($type, $description);
		}

		throw new ShouldNotHappenException();
	}

	private function parseTypeAliasTagValue(TokenIterator $tokens): Ast\PhpDoc\TypeAliasTagValueNode
	{
		$alias = $tokens->currentTokenValue();
		$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);

		// support phan-type/psalm-type syntax
		$tokens->tryConsumeTokenType(Lexer::TOKEN_EQUAL);

		$startLine = $tokens->currentTokenLine();
		$startIndex = $tokens->currentTokenIndex();
		try {
			$type = $this->typeParser->parse($tokens);
			if (!$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PHPDOC)) {
				if (!$tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL)) {
					throw new ParserException(
						$tokens->currentTokenValue(),
						$tokens->currentTokenType(),
						$tokens->currentTokenOffset(),
						Lexer::TOKEN_PHPDOC_EOL,
						null,
						$tokens->currentTokenLine(),
					);
				}
			}

			return new Ast\PhpDoc\TypeAliasTagValueNode($alias, $type);
		} catch (ParserException $e) {
			$this->parseOptionalDescription($tokens, false);
			return new Ast\PhpDoc\TypeAliasTagValueNode(
				$alias,
				$this->enrichWithAttributes($tokens, new Ast\Type\InvalidTypeNode($e), $startLine, $startIndex),
			);
		}
	}

	private function parseTypeAliasImportTagValue(TokenIterator $tokens): Ast\PhpDoc\TypeAliasImportTagValueNode
	{
		$importedAlias = $tokens->currentTokenValue();
		$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);

		$tokens->consumeTokenValue(Lexer::TOKEN_IDENTIFIER, 'from');

		$identifierStartLine = $tokens->currentTokenLine();
		$identifierStartIndex = $tokens->currentTokenIndex();
		$importedFrom = $tokens->currentTokenValue();
		$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
		$importedFromType = $this->enrichWithAttributes(
			$tokens,
			new IdentifierTypeNode($importedFrom),
			$identifierStartLine,
			$identifierStartIndex,
		);

		$importedAs = null;
		if ($tokens->tryConsumeTokenValue('as')) {
			$importedAs = $tokens->currentTokenValue();
			$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
		}

		return new Ast\PhpDoc\TypeAliasImportTagValueNode($importedAlias, $importedFromType, $importedAs);
	}

	/**
	 * @return Ast\PhpDoc\AssertTagValueNode|Ast\PhpDoc\AssertTagPropertyValueNode|Ast\PhpDoc\AssertTagMethodValueNode
	 */
	private function parseAssertTagValue(TokenIterator $tokens): Ast\PhpDoc\PhpDocTagValueNode
	{
		$isNegated = $tokens->tryConsumeTokenType(Lexer::TOKEN_NEGATED);
		$isEquality = $tokens->tryConsumeTokenType(Lexer::TOKEN_EQUAL);
		$type = $this->typeParser->parse($tokens);
		$parameter = $this->parseAssertParameter($tokens);
		$description = $this->parseOptionalDescription($tokens, false);

		if (array_key_exists('method', $parameter)) {
			return new Ast\PhpDoc\AssertTagMethodValueNode($type, $parameter['parameter'], $parameter['method'], $isNegated, $description, $isEquality);
		} elseif (array_key_exists('property', $parameter)) {
			return new Ast\PhpDoc\AssertTagPropertyValueNode($type, $parameter['parameter'], $parameter['property'], $isNegated, $description, $isEquality);
		}

		return new Ast\PhpDoc\AssertTagValueNode($type, $parameter['parameter'], $isNegated, $description, $isEquality);
	}

	/**
	 * @return array{parameter: string}|array{parameter: string, property: string}|array{parameter: string, method: string}
	 */
	private function parseAssertParameter(TokenIterator $tokens): array
	{
		if ($tokens->isCurrentTokenType(Lexer::TOKEN_THIS_VARIABLE)) {
			$parameter = '$this';
			$tokens->next();
		} else {
			$parameter = $tokens->currentTokenValue();
			$tokens->consumeTokenType(Lexer::TOKEN_VARIABLE);
		}

		if ($tokens->isCurrentTokenType(Lexer::TOKEN_ARROW)) {
			$tokens->consumeTokenType(Lexer::TOKEN_ARROW);

			$propertyOrMethod = $tokens->currentTokenValue();
			$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);

			if ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) {
				$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES);

				return ['parameter' => $parameter, 'method' => $propertyOrMethod];
			}

			return ['parameter' => $parameter, 'property' => $propertyOrMethod];
		}

		return ['parameter' => $parameter];
	}

	private function parseSelfOutTagValue(TokenIterator $tokens): Ast\PhpDoc\SelfOutTagValueNode
	{
		$type = $this->typeParser->parse($tokens);
		$description = $this->parseOptionalDescription($tokens, true);

		return new Ast\PhpDoc\SelfOutTagValueNode($type, $description);
	}

	private function parseParamOutTagValue(TokenIterator $tokens): Ast\PhpDoc\ParamOutTagValueNode
	{
		$type = $this->typeParser->parse($tokens);
		$parameterName = $this->parseRequiredVariableName($tokens);
		$description = $this->parseOptionalDescription($tokens, false);

		return new Ast\PhpDoc\ParamOutTagValueNode($type, $parameterName, $description);
	}

	private function parseOptionalVariableName(TokenIterator $tokens): string
	{
		if ($tokens->isCurrentTokenType(Lexer::TOKEN_VARIABLE)) {
			$parameterName = $tokens->currentTokenValue();
			$tokens->next();
		} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_THIS_VARIABLE)) {
			$parameterName = '$this';
			$tokens->next();

		} else {
			$parameterName = '';
		}

		return $parameterName;
	}


	private function parseRequiredVariableName(TokenIterator $tokens): string
	{
		$parameterName = $tokens->currentTokenValue();
		$tokens->consumeTokenType(Lexer::TOKEN_VARIABLE);

		return $parameterName;
	}

	/**
	 * @param bool $limitStartToken true should be used when the description immediately follows a parsed type
	 */
	private function parseOptionalDescription(TokenIterator $tokens, bool $limitStartToken): string
	{
		if ($limitStartToken) {
			foreach (self::DISALLOWED_DESCRIPTION_START_TOKENS as $disallowedStartToken) {
				if (!$tokens->isCurrentTokenType($disallowedStartToken)) {
					continue;
				}

				$tokens->consumeTokenType(Lexer::TOKEN_OTHER); // will throw exception
			}

			if (
				!$tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL, Lexer::TOKEN_CLOSE_PHPDOC, Lexer::TOKEN_END)
				&& !$tokens->isPrecededByHorizontalWhitespace()
			) {
				$tokens->consumeTokenType(Lexer::TOKEN_HORIZONTAL_WS); // will throw exception
			}
		}

		return $this->parseText($tokens)->text;
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Parser;

use LogicException;
use PHPStan\PhpDocParser\Ast;
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
use PHPStan\PhpDocParser\Lexer\Lexer;
use PHPStan\PhpDocParser\ParserConfig;
use function in_array;
use function str_replace;
use function strlen;
use function strpos;
use function substr_compare;

class TypeParser
{

	private ParserConfig $config;

	private ConstExprParser $constExprParser;

	public function __construct(
		ParserConfig $config,
		ConstExprParser $constExprParser
	)
	{
		$this->config = $config;
		$this->constExprParser = $constExprParser;
	}

	/** @phpstan-impure */
	public function parse(TokenIterator $tokens): Ast\Type\TypeNode
	{
		$startLine = $tokens->currentTokenLine();
		$startIndex = $tokens->currentTokenIndex();
		if ($tokens->isCurrentTokenType(Lexer::TOKEN_NULLABLE)) {
			$type = $this->parseNullable($tokens);

		} else {
			$type = $this->parseAtomic($tokens);

			$tokens->pushSavePoint();
			$tokens->skipNewLineTokensAndConsumeComments();

			try {
				$enrichedType = $this->enrichTypeOnUnionOrIntersection($tokens, $type);

			} catch (ParserException $parserException) {
				$enrichedType = null;
			}

			if ($enrichedType !== null) {
				$type = $enrichedType;
				$tokens->dropSavePoint();

			} else {
				$tokens->rollback();
				$type = $this->enrichTypeOnUnionOrIntersection($tokens, $type) ?? $type;
			}
		}

		return $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex);
	}

	/** @phpstan-impure */
	private function enrichTypeOnUnionOrIntersection(TokenIterator $tokens, Ast\Type\TypeNode $type): ?Ast\Type\TypeNode
	{
		if ($tokens->isCurrentTokenType(Lexer::TOKEN_UNION)) {
			return $this->parseUnion($tokens, $type);

		}

		if ($tokens->isCurrentTokenType(Lexer::TOKEN_INTERSECTION)) {
			return $this->parseIntersection($tokens, $type);
		}

		return null;
	}

	/**
	 * @internal
	 * @template T of Ast\Node
	 * @param T $type
	 * @return T
	 */
	public function enrichWithAttributes(TokenIterator $tokens, Ast\Node $type, int $startLine, int $startIndex): Ast\Node
	{
		if ($this->config->useLinesAttributes) {
			$type->setAttribute(Ast\Attribute::START_LINE, $startLine);
			$type->setAttribute(Ast\Attribute::END_LINE, $tokens->currentTokenLine());
		}

		$comments = $tokens->flushComments();
		if ($this->config->useCommentsAttributes) {
			$type->setAttribute(Ast\Attribute::COMMENTS, $comments);
		}

		if ($this->config->useIndexAttributes) {
			$type->setAttribute(Ast\Attribute::START_INDEX, $startIndex);
			$type->setAttribute(Ast\Attribute::END_INDEX, $tokens->endIndexOfLastRelevantToken());
		}

		return $type;
	}

	/** @phpstan-impure */
	private function subParse(TokenIterator $tokens): Ast\Type\TypeNode
	{
		$startLine = $tokens->currentTokenLine();
		$startIndex = $tokens->currentTokenIndex();

		if ($tokens->isCurrentTokenType(Lexer::TOKEN_NULLABLE)) {
			$type = $this->parseNullable($tokens);

		} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_VARIABLE)) {
			$type = $this->parseConditionalForParameter($tokens, $tokens->currentTokenValue());

		} else {
			$type = $this->parseAtomic($tokens);

			if ($tokens->isCurrentTokenValue('is')) {
				$type = $this->parseConditional($tokens, $type);
			} else {
				$tokens->skipNewLineTokensAndConsumeComments();

				if ($tokens->isCurrentTokenType(Lexer::TOKEN_UNION)) {
					$type = $this->subParseUnion($tokens, $type);

				} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_INTERSECTION)) {
					$type = $this->subParseIntersection($tokens, $type);
				}
			}
		}

		return $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex);
	}


	/** @phpstan-impure */
	private function parseAtomic(TokenIterator $tokens): Ast\Type\TypeNode
	{
		$startLine = $tokens->currentTokenLine();
		$startIndex = $tokens->currentTokenIndex();

		if ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) {
			$tokens->skipNewLineTokensAndConsumeComments();
			$type = $this->subParse($tokens);
			$tokens->skipNewLineTokensAndConsumeComments();

			$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES);

			if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
				$type = $this->tryParseArrayOrOffsetAccess($tokens, $type);
			}

			return $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex);
		}

		if ($tokens->tryConsumeTokenType(Lexer::TOKEN_THIS_VARIABLE)) {
			$type = $this->enrichWithAttributes($tokens, new Ast\Type\ThisTypeNode(), $startLine, $startIndex);

			if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
				$type = $this->tryParseArrayOrOffsetAccess($tokens, $type);
			}

			return $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex);
		}

		$currentTokenValue = $tokens->currentTokenValue();
		$tokens->pushSavePoint(); // because of ConstFetchNode
		if ($tokens->tryConsumeTokenType(Lexer::TOKEN_IDENTIFIER)) {
			$type = $this->enrichWithAttributes($tokens, new Ast\Type\IdentifierTypeNode($currentTokenValue), $startLine, $startIndex);

			if (!$tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_COLON)) {
				$tokens->dropSavePoint(); // because of ConstFetchNode
				if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) {
					$tokens->pushSavePoint();

					$isHtml = $this->isHtml($tokens);
					$tokens->rollback();
					if ($isHtml) {
						return $type;
					}

					$origType = $type;
					$type = $this->tryParseCallable($tokens, $type, true);
					if ($type === $origType) {
						$type = $this->parseGeneric($tokens, $type);

						if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
							$type = $this->tryParseArrayOrOffsetAccess($tokens, $type);
						}
					}
				} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) {
					$type = $this->tryParseCallable($tokens, $type, false);

				} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
					$type = $this->tryParseArrayOrOffsetAccess($tokens, $type);

				} elseif (in_array($type->name, [
					Ast\Type\ArrayShapeNode::KIND_ARRAY,
					Ast\Type\ArrayShapeNode::KIND_LIST,
					Ast\Type\ArrayShapeNode::KIND_NON_EMPTY_ARRAY,
					Ast\Type\ArrayShapeNode::KIND_NON_EMPTY_LIST,
					'object',
				], true) && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET) && !$tokens->isPrecededByHorizontalWhitespace()) {
					if ($type->name === 'object') {
						$type = $this->parseObjectShape($tokens);
					} else {
						$type = $this->parseArrayShape($tokens, $type, $type->name);
					}

					if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
						$type = $this->tryParseArrayOrOffsetAccess(
							$tokens,
							$this->enrichWithAttributes($tokens, $type, $startLine, $startIndex),
						);
					}
				}

				return $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex);
			} else {
				$tokens->rollback(); // because of ConstFetchNode
			}
		} else {
			$tokens->dropSavePoint(); // because of ConstFetchNode
		}

		$currentTokenValue = $tokens->currentTokenValue();
		$currentTokenType = $tokens->currentTokenType();
		$currentTokenOffset = $tokens->currentTokenOffset();
		$currentTokenLine = $tokens->currentTokenLine();

		try {
			$constExpr = $this->constExprParser->parse($tokens);
			if ($constExpr instanceof Ast\ConstExpr\ConstExprArrayNode) {
				throw new ParserException(
					$currentTokenValue,
					$currentTokenType,
					$currentTokenOffset,
					Lexer::TOKEN_IDENTIFIER,
					null,
					$currentTokenLine,
				);
			}

			$type = $this->enrichWithAttributes(
				$tokens,
				new Ast\Type\ConstTypeNode($constExpr),
				$startLine,
				$startIndex,
			);
			if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
				$type = $this->tryParseArrayOrOffsetAccess($tokens, $type);
			}

			return $type;
		} catch (LogicException $e) {
			throw new ParserException(
				$currentTokenValue,
				$currentTokenType,
				$currentTokenOffset,
				Lexer::TOKEN_IDENTIFIER,
				null,
				$currentTokenLine,
			);
		}
	}


	/** @phpstan-impure */
	private function parseUnion(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\TypeNode
	{
		$types = [$type];

		while ($tokens->tryConsumeTokenType(Lexer::TOKEN_UNION)) {
			$types[] = $this->parseAtomic($tokens);
			$tokens->pushSavePoint();
			$tokens->skipNewLineTokensAndConsumeComments();
			if (!$tokens->isCurrentTokenType(Lexer::TOKEN_UNION)) {
				$tokens->rollback();
				break;
			}

			$tokens->dropSavePoint();
		}

		return new Ast\Type\UnionTypeNode($types);
	}


	/** @phpstan-impure */
	private function subParseUnion(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\TypeNode
	{
		$types = [$type];

		while ($tokens->tryConsumeTokenType(Lexer::TOKEN_UNION)) {
			$tokens->skipNewLineTokensAndConsumeComments();
			$types[] = $this->parseAtomic($tokens);
			$tokens->skipNewLineTokensAndConsumeComments();
		}

		return new Ast\Type\UnionTypeNode($types);
	}


	/** @phpstan-impure */
	private function parseIntersection(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\TypeNode
	{
		$types = [$type];

		while ($tokens->tryConsumeTokenType(Lexer::TOKEN_INTERSECTION)) {
			$types[] = $this->parseAtomic($tokens);
			$tokens->pushSavePoint();
			$tokens->skipNewLineTokensAndConsumeComments();
			if (!$tokens->isCurrentTokenType(Lexer::TOKEN_INTERSECTION)) {
				$tokens->rollback();
				break;
			}

			$tokens->dropSavePoint();
		}

		return new Ast\Type\IntersectionTypeNode($types);
	}


	/** @phpstan-impure */
	private function subParseIntersection(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\TypeNode
	{
		$types = [$type];

		while ($tokens->tryConsumeTokenType(Lexer::TOKEN_INTERSECTION)) {
			$tokens->skipNewLineTokensAndConsumeComments();
			$types[] = $this->parseAtomic($tokens);
			$tokens->skipNewLineTokensAndConsumeComments();
		}

		return new Ast\Type\IntersectionTypeNode($types);
	}


	/** @phpstan-impure */
	private function parseConditional(TokenIterator $tokens, Ast\Type\TypeNode $subjectType): Ast\Type\TypeNode
	{
		$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);

		$negated = false;
		if ($tokens->isCurrentTokenValue('not')) {
			$negated = true;
			$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
		}

		$targetType = $this->parse($tokens);

		$tokens->skipNewLineTokensAndConsumeComments();
		$tokens->consumeTokenType(Lexer::TOKEN_NULLABLE);
		$tokens->skipNewLineTokensAndConsumeComments();

		$ifType = $this->parse($tokens);

		$tokens->skipNewLineTokensAndConsumeComments();
		$tokens->consumeTokenType(Lexer::TOKEN_COLON);
		$tokens->skipNewLineTokensAndConsumeComments();

		$elseType = $this->subParse($tokens);

		return new Ast\Type\ConditionalTypeNode($subjectType, $targetType, $ifType, $elseType, $negated);
	}

	/** @phpstan-impure */
	private function parseConditionalForParameter(TokenIterator $tokens, string $parameterName): Ast\Type\TypeNode
	{
		$tokens->consumeTokenType(Lexer::TOKEN_VARIABLE);
		$tokens->consumeTokenValue(Lexer::TOKEN_IDENTIFIER, 'is');

		$negated = false;
		if ($tokens->isCurrentTokenValue('not')) {
			$negated = true;
			$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
		}

		$targetType = $this->parse($tokens);

		$tokens->skipNewLineTokensAndConsumeComments();
		$tokens->consumeTokenType(Lexer::TOKEN_NULLABLE);
		$tokens->skipNewLineTokensAndConsumeComments();

		$ifType = $this->parse($tokens);

		$tokens->skipNewLineTokensAndConsumeComments();
		$tokens->consumeTokenType(Lexer::TOKEN_COLON);
		$tokens->skipNewLineTokensAndConsumeComments();

		$elseType = $this->subParse($tokens);

		return new Ast\Type\ConditionalTypeForParameterNode($parameterName, $targetType, $ifType, $elseType, $negated);
	}


	/** @phpstan-impure */
	private function parseNullable(TokenIterator $tokens): Ast\Type\TypeNode
	{
		$tokens->consumeTokenType(Lexer::TOKEN_NULLABLE);

		$type = $this->parseAtomic($tokens);

		return new Ast\Type\NullableTypeNode($type);
	}

	/** @phpstan-impure */
	public function isHtml(TokenIterator $tokens): bool
	{
		$tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET);

		if (!$tokens->isCurrentTokenType(Lexer::TOKEN_IDENTIFIER)) {
			return false;
		}

		$htmlTagName = $tokens->currentTokenValue();

		$tokens->next();

		if (!$tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET)) {
			return false;
		}

		$endTag = '</' . $htmlTagName . '>';
		$endTagSearchOffset = - strlen($endTag);

		while (!$tokens->isCurrentTokenType(Lexer::TOKEN_END)) {
			if (
				(
					$tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)
					&& strpos($tokens->currentTokenValue(), '/' . $htmlTagName . '>') !== false
				)
				|| substr_compare($tokens->currentTokenValue(), $endTag, $endTagSearchOffset) === 0
			) {
				return true;
			}

			$tokens->next();
		}

		return false;
	}

	/** @phpstan-impure */
	public function parseGeneric(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $baseType): Ast\Type\GenericTypeNode
	{
		$tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET);
		$tokens->skipNewLineTokensAndConsumeComments();

		$startLine = $baseType->getAttribute(Ast\Attribute::START_LINE);
		$startIndex = $baseType->getAttribute(Ast\Attribute::START_INDEX);
		$genericTypes = [];
		$variances = [];

		$isFirst = true;
		while (
			$isFirst
			|| $tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)
		) {
			$tokens->skipNewLineTokensAndConsumeComments();

			// trailing comma case
			if (!$isFirst && $tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET)) {
				break;
			}
			$isFirst = false;

			[$genericTypes[], $variances[]] = $this->parseGenericTypeArgument($tokens);
			$tokens->skipNewLineTokensAndConsumeComments();
		}

		$type = new Ast\Type\GenericTypeNode($baseType, $genericTypes, $variances);
		if ($startLine !== null && $startIndex !== null) {
			$type = $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex);
		}

		$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET);

		return $type;
	}


	/**
	 * @phpstan-impure
	 * @return array{Ast\Type\TypeNode, Ast\Type\GenericTypeNode::VARIANCE_*}
	 */
	public function parseGenericTypeArgument(TokenIterator $tokens): array
	{
		$startLine = $tokens->currentTokenLine();
		$startIndex = $tokens->currentTokenIndex();
		if ($tokens->tryConsumeTokenType(Lexer::TOKEN_WILDCARD)) {
			return [
				$this->enrichWithAttributes($tokens, new Ast\Type\IdentifierTypeNode('mixed'), $startLine, $startIndex),
				Ast\Type\GenericTypeNode::VARIANCE_BIVARIANT,
			];
		}

		if ($tokens->tryConsumeTokenValue('contravariant')) {
			$variance = Ast\Type\GenericTypeNode::VARIANCE_CONTRAVARIANT;
		} elseif ($tokens->tryConsumeTokenValue('covariant')) {
			$variance = Ast\Type\GenericTypeNode::VARIANCE_COVARIANT;
		} else {
			$variance = Ast\Type\GenericTypeNode::VARIANCE_INVARIANT;
		}

		$type = $this->parse($tokens);
		return [$type, $variance];
	}

	/**
	 * @throws ParserException
	 * @param ?callable(TokenIterator): string $parseDescription
	 */
	public function parseTemplateTagValue(
		TokenIterator $tokens,
		?callable $parseDescription = null
	): TemplateTagValueNode
	{
		$name = $tokens->currentTokenValue();
		$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);

		$upperBound = $lowerBound = null;

		if ($tokens->tryConsumeTokenValue('of') || $tokens->tryConsumeTokenValue('as')) {
			$upperBound = $this->parse($tokens);
		}

		if ($tokens->tryConsumeTokenValue('super')) {
			$lowerBound = $this->parse($tokens);
		}

		if ($tokens->tryConsumeTokenValue('=')) {
			$default = $this->parse($tokens);
		} else {
			$default = null;
		}

		if ($parseDescription !== null) {
			$description = $parseDescription($tokens);
		} else {
			$description = '';
		}

		if ($name === '') {
			throw new LogicException('Template tag name cannot be empty.');
		}

		return new Ast\PhpDoc\TemplateTagValueNode($name, $upperBound, $description, $default, $lowerBound);
	}


	/** @phpstan-impure */
	private function parseCallable(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $identifier, bool $hasTemplate): Ast\Type\TypeNode
	{
		$templates = $hasTemplate
			? $this->parseCallableTemplates($tokens)
			: [];

		$tokens->consumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES);
		$tokens->skipNewLineTokensAndConsumeComments();

		$parameters = [];
		if (!$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PARENTHESES)) {
			$parameters[] = $this->parseCallableParameter($tokens);
			$tokens->skipNewLineTokensAndConsumeComments();
			while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) {
				$tokens->skipNewLineTokensAndConsumeComments();
				if ($tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PARENTHESES)) {
					break;
				}
				$parameters[] = $this->parseCallableParameter($tokens);
				$tokens->skipNewLineTokensAndConsumeComments();
			}
		}

		$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES);
		$tokens->consumeTokenType(Lexer::TOKEN_COLON);

		$startLine = $tokens->currentTokenLine();
		$startIndex = $tokens->currentTokenIndex();
		$returnType = $this->enrichWithAttributes($tokens, $this->parseCallableReturnType($tokens), $startLine, $startIndex);

		return new Ast\Type\CallableTypeNode($identifier, $parameters, $returnType, $templates);
	}


	/**
	 * @return Ast\PhpDoc\TemplateTagValueNode[]
	 *
	 * @phpstan-impure
	 */
	private function parseCallableTemplates(TokenIterator $tokens): array
	{
		$tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET);

		$templates = [];

		$isFirst = true;
		while ($isFirst || $tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) {
			$tokens->skipNewLineTokensAndConsumeComments();

			// trailing comma case
			if (!$isFirst && $tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET)) {
				break;
			}
			$isFirst = false;

			$templates[] = $this->parseCallableTemplateArgument($tokens);
			$tokens->skipNewLineTokensAndConsumeComments();
		}

		$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET);

		return $templates;
	}


	private function parseCallableTemplateArgument(TokenIterator $tokens): Ast\PhpDoc\TemplateTagValueNode
	{
		$startLine = $tokens->currentTokenLine();
		$startIndex = $tokens->currentTokenIndex();

		return $this->enrichWithAttributes(
			$tokens,
			$this->parseTemplateTagValue($tokens),
			$startLine,
			$startIndex,
		);
	}


	/** @phpstan-impure */
	private function parseCallableParameter(TokenIterator $tokens): Ast\Type\CallableTypeParameterNode
	{
		$startLine = $tokens->currentTokenLine();
		$startIndex = $tokens->currentTokenIndex();
		$type = $this->parse($tokens);
		$isReference = $tokens->tryConsumeTokenType(Lexer::TOKEN_REFERENCE);
		$isVariadic = $tokens->tryConsumeTokenType(Lexer::TOKEN_VARIADIC);

		if ($tokens->isCurrentTokenType(Lexer::TOKEN_VARIABLE)) {
			$parameterName = $tokens->currentTokenValue();
			$tokens->consumeTokenType(Lexer::TOKEN_VARIABLE);

		} else {
			$parameterName = '';
		}

		$isOptional = $tokens->tryConsumeTokenType(Lexer::TOKEN_EQUAL);
		return $this->enrichWithAttributes(
			$tokens,
			new Ast\Type\CallableTypeParameterNode($type, $isReference, $isVariadic, $parameterName, $isOptional),
			$startLine,
			$startIndex,
		);
	}


	/** @phpstan-impure */
	private function parseCallableReturnType(TokenIterator $tokens): Ast\Type\TypeNode
	{
		$startLine = $tokens->currentTokenLine();
		$startIndex = $tokens->currentTokenIndex();
		if ($tokens->isCurrentTokenType(Lexer::TOKEN_NULLABLE)) {
			return $this->parseNullable($tokens);

		} elseif ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) {
			$type = $this->subParse($tokens);
			$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES);
			if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
				$type = $this->tryParseArrayOrOffsetAccess($tokens, $type);
			}

			return $type;
		} elseif ($tokens->tryConsumeTokenType(Lexer::TOKEN_THIS_VARIABLE)) {
			$type = new Ast\Type\ThisTypeNode();
			if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
				$type = $this->tryParseArrayOrOffsetAccess($tokens, $this->enrichWithAttributes(
					$tokens,
					$type,
					$startLine,
					$startIndex,
				));
			}

			return $type;
		} else {
			$currentTokenValue = $tokens->currentTokenValue();
			$tokens->pushSavePoint(); // because of ConstFetchNode
			if ($tokens->tryConsumeTokenType(Lexer::TOKEN_IDENTIFIER)) {
				$type = new Ast\Type\IdentifierTypeNode($currentTokenValue);

				if (!$tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_COLON)) {
					if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) {
						$type = $this->parseGeneric(
							$tokens,
							$this->enrichWithAttributes(
								$tokens,
								$type,
								$startLine,
								$startIndex,
							),
						);
						if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
							$type = $this->tryParseArrayOrOffsetAccess($tokens, $this->enrichWithAttributes(
								$tokens,
								$type,
								$startLine,
								$startIndex,
							));
						}

					} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
						$type = $this->tryParseArrayOrOffsetAccess($tokens, $this->enrichWithAttributes(
							$tokens,
							$type,
							$startLine,
							$startIndex,
						));

					} elseif (in_array($type->name, [
						Ast\Type\ArrayShapeNode::KIND_ARRAY,
						Ast\Type\ArrayShapeNode::KIND_LIST,
						Ast\Type\ArrayShapeNode::KIND_NON_EMPTY_ARRAY,
						Ast\Type\ArrayShapeNode::KIND_NON_EMPTY_LIST,
						'object',
					], true) && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET) && !$tokens->isPrecededByHorizontalWhitespace()) {
						if ($type->name === 'object') {
							$type = $this->parseObjectShape($tokens);
						} else {
							$type = $this->parseArrayShape($tokens, $this->enrichWithAttributes(
								$tokens,
								$type,
								$startLine,
								$startIndex,
							), $type->name);
						}

						if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
							$type = $this->tryParseArrayOrOffsetAccess($tokens, $this->enrichWithAttributes(
								$tokens,
								$type,
								$startLine,
								$startIndex,
							));
						}
					}

					return $type;
				} else {
					$tokens->rollback(); // because of ConstFetchNode
				}
			} else {
				$tokens->dropSavePoint(); // because of ConstFetchNode
			}
		}

		$currentTokenValue = $tokens->currentTokenValue();
		$currentTokenType = $tokens->currentTokenType();
		$currentTokenOffset = $tokens->currentTokenOffset();
		$currentTokenLine = $tokens->currentTokenLine();

		try {
			$constExpr = $this->constExprParser->parse($tokens);
			if ($constExpr instanceof Ast\ConstExpr\ConstExprArrayNode) {
				throw new ParserException(
					$currentTokenValue,
					$currentTokenType,
					$currentTokenOffset,
					Lexer::TOKEN_IDENTIFIER,
					null,
					$currentTokenLine,
				);
			}

			$type = $this->enrichWithAttributes(
				$tokens,
				new Ast\Type\ConstTypeNode($constExpr),
				$startLine,
				$startIndex,
			);
			if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
				$type = $this->tryParseArrayOrOffsetAccess($tokens, $type);
			}

			return $type;
		} catch (LogicException $e) {
			throw new ParserException(
				$currentTokenValue,
				$currentTokenType,
				$currentTokenOffset,
				Lexer::TOKEN_IDENTIFIER,
				null,
				$currentTokenLine,
			);
		}
	}


	/** @phpstan-impure */
	private function tryParseCallable(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $identifier, bool $hasTemplate): Ast\Type\TypeNode
	{
		try {
			$tokens->pushSavePoint();
			$type = $this->parseCallable($tokens, $identifier, $hasTemplate);
			$tokens->dropSavePoint();

		} catch (ParserException $e) {
			$tokens->rollback();
			$type = $identifier;
		}

		return $type;
	}


	/** @phpstan-impure */
	private function tryParseArrayOrOffsetAccess(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\TypeNode
	{
		$startLine = $type->getAttribute(Ast\Attribute::START_LINE);
		$startIndex = $type->getAttribute(Ast\Attribute::START_INDEX);
		try {
			while ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
				$tokens->pushSavePoint();

				$canBeOffsetAccessType = !$tokens->isPrecededByHorizontalWhitespace();
				$tokens->consumeTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET);

				if ($canBeOffsetAccessType && !$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_SQUARE_BRACKET)) {
					$offset = $this->parse($tokens);
					$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_SQUARE_BRACKET);
					$tokens->dropSavePoint();
					$type = new Ast\Type\OffsetAccessTypeNode($type, $offset);

					if ($startLine !== null && $startIndex !== null) {
						$type = $this->enrichWithAttributes(
							$tokens,
							$type,
							$startLine,
							$startIndex,
						);
					}
				} else {
					$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_SQUARE_BRACKET);
					$tokens->dropSavePoint();
					$type = new Ast\Type\ArrayTypeNode($type);

					if ($startLine !== null && $startIndex !== null) {
						$type = $this->enrichWithAttributes(
							$tokens,
							$type,
							$startLine,
							$startIndex,
						);
					}
				}
			}

		} catch (ParserException $e) {
			$tokens->rollback();
		}

		return $type;
	}


	/**
	 * @phpstan-impure
	 * @param Ast\Type\ArrayShapeNode::KIND_* $kind
	 */
	private function parseArrayShape(TokenIterator $tokens, Ast\Type\TypeNode $type, string $kind): Ast\Type\ArrayShapeNode
	{
		$tokens->consumeTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET);

		$items = [];
		$sealed = true;
		$unsealedType = null;

		$done = false;

		do {
			$tokens->skipNewLineTokensAndConsumeComments();

			if ($tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET)) {
				return Ast\Type\ArrayShapeNode::createSealed($items, $kind);
			}

			if ($tokens->tryConsumeTokenType(Lexer::TOKEN_VARIADIC)) {
				$sealed = false;

				$tokens->skipNewLineTokensAndConsumeComments();
				if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) {
					if ($kind === Ast\Type\ArrayShapeNode::KIND_ARRAY) {
						$unsealedType = $this->parseArrayShapeUnsealedType($tokens);
					} else {
						$unsealedType = $this->parseListShapeUnsealedType($tokens);
					}
					$tokens->skipNewLineTokensAndConsumeComments();
				}

				$tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA);
				break;
			}

			$items[] = $this->parseArrayShapeItem($tokens);
			$tokens->skipNewLineTokensAndConsumeComments();
			if (!$tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) {
				$done = true;
			}
			if ($tokens->currentTokenType() !== Lexer::TOKEN_COMMENT) {
				continue;
			}

			$tokens->next();

		} while (!$done);

		$tokens->skipNewLineTokensAndConsumeComments();
		$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET);

		if ($sealed) {
			return Ast\Type\ArrayShapeNode::createSealed($items, $kind);
		}

		return Ast\Type\ArrayShapeNode::createUnsealed($items, $unsealedType, $kind);
	}


	/** @phpstan-impure */
	private function parseArrayShapeItem(TokenIterator $tokens): Ast\Type\ArrayShapeItemNode
	{
		$startLine = $tokens->currentTokenLine();
		$startIndex = $tokens->currentTokenIndex();

		// parse any comments above the item
		$tokens->skipNewLineTokensAndConsumeComments();

		try {
			$tokens->pushSavePoint();
			$key = $this->parseArrayShapeKey($tokens);
			$optional = $tokens->tryConsumeTokenType(Lexer::TOKEN_NULLABLE);
			$tokens->consumeTokenType(Lexer::TOKEN_COLON);
			$value = $this->parse($tokens);

			$tokens->dropSavePoint();

			return $this->enrichWithAttributes(
				$tokens,
				new Ast\Type\ArrayShapeItemNode($key, $optional, $value),
				$startLine,
				$startIndex,
			);
		} catch (ParserException $e) {
			$tokens->rollback();
			$value = $this->parse($tokens);

			return $this->enrichWithAttributes(
				$tokens,
				new Ast\Type\ArrayShapeItemNode(null, false, $value),
				$startLine,
				$startIndex,
			);
		}
	}

	/**
	 * @phpstan-impure
	 * @return Ast\ConstExpr\ConstExprIntegerNode|Ast\ConstExpr\ConstExprStringNode|Ast\ConstExpr\ConstFetchNode|Ast\Type\IdentifierTypeNode
	 */
	private function parseArrayShapeKey(TokenIterator $tokens)
	{
		$startIndex = $tokens->currentTokenIndex();
		$startLine = $tokens->currentTokenLine();

		if ($tokens->isCurrentTokenType(Lexer::TOKEN_INTEGER)) {
			$key = new Ast\ConstExpr\ConstExprIntegerNode(str_replace('_', '', $tokens->currentTokenValue()));
			$tokens->next();

		} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING)) {
			$key = new Ast\ConstExpr\ConstExprStringNode(StringUnescaper::unescapeString($tokens->currentTokenValue()), Ast\ConstExpr\ConstExprStringNode::SINGLE_QUOTED);
			$tokens->next();

		} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_QUOTED_STRING)) {
			$key = new Ast\ConstExpr\ConstExprStringNode(StringUnescaper::unescapeString($tokens->currentTokenValue()), Ast\ConstExpr\ConstExprStringNode::DOUBLE_QUOTED);

			$tokens->next();

		} else {
			$identifier = $tokens->currentTokenValue();
			$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);

			if ($tokens->tryConsumeTokenType(Lexer::TOKEN_DOUBLE_COLON)) {
				$classConstantName = $tokens->currentTokenValue();
				$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);

				$key = new Ast\ConstExpr\ConstFetchNode($identifier, $classConstantName);
			} else {
				$key = new Ast\Type\IdentifierTypeNode($identifier);
			}
		}

		return $this->enrichWithAttributes(
			$tokens,
			$key,
			$startLine,
			$startIndex,
		);
	}

	/**
	 * @phpstan-impure
	 */
	private function parseArrayShapeUnsealedType(TokenIterator $tokens): Ast\Type\ArrayShapeUnsealedTypeNode
	{
		$startLine = $tokens->currentTokenLine();
		$startIndex = $tokens->currentTokenIndex();

		$tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET);
		$tokens->skipNewLineTokensAndConsumeComments();

		$valueType = $this->parse($tokens);
		$tokens->skipNewLineTokensAndConsumeComments();

		$keyType = null;
		if ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) {
			$tokens->skipNewLineTokensAndConsumeComments();

			$keyType = $valueType;
			$valueType = $this->parse($tokens);
			$tokens->skipNewLineTokensAndConsumeComments();
		}

		$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET);

		return $this->enrichWithAttributes(
			$tokens,
			new Ast\Type\ArrayShapeUnsealedTypeNode($valueType, $keyType),
			$startLine,
			$startIndex,
		);
	}

	/**
	 * @phpstan-impure
	 */
	private function parseListShapeUnsealedType(TokenIterator $tokens): Ast\Type\ArrayShapeUnsealedTypeNode
	{
		$startLine = $tokens->currentTokenLine();
		$startIndex = $tokens->currentTokenIndex();

		$tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET);
		$tokens->skipNewLineTokensAndConsumeComments();

		$valueType = $this->parse($tokens);
		$tokens->skipNewLineTokensAndConsumeComments();

		$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET);

		return $this->enrichWithAttributes(
			$tokens,
			new Ast\Type\ArrayShapeUnsealedTypeNode($valueType, null),
			$startLine,
			$startIndex,
		);
	}

	/**
	 * @phpstan-impure
	 */
	private function parseObjectShape(TokenIterator $tokens): Ast\Type\ObjectShapeNode
	{
		$tokens->consumeTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET);

		$items = [];

		do {
			$tokens->skipNewLineTokensAndConsumeComments();

			if ($tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET)) {
				return new Ast\Type\ObjectShapeNode($items);
			}

			$items[] = $this->parseObjectShapeItem($tokens);

			$tokens->skipNewLineTokensAndConsumeComments();
		} while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA));

		$tokens->skipNewLineTokensAndConsumeComments();
		$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET);

		return new Ast\Type\ObjectShapeNode($items);
	}

	/** @phpstan-impure */
	private function parseObjectShapeItem(TokenIterator $tokens): Ast\Type\ObjectShapeItemNode
	{
		$startLine = $tokens->currentTokenLine();
		$startIndex = $tokens->currentTokenIndex();

		$tokens->skipNewLineTokensAndConsumeComments();

		$key = $this->parseObjectShapeKey($tokens);
		$optional = $tokens->tryConsumeTokenType(Lexer::TOKEN_NULLABLE);
		$tokens->consumeTokenType(Lexer::TOKEN_COLON);
		$value = $this->parse($tokens);

		return $this->enrichWithAttributes(
			$tokens,
			new Ast\Type\ObjectShapeItemNode($key, $optional, $value),
			$startLine,
			$startIndex,
		);
	}

	/**
	 * @phpstan-impure
	 * @return Ast\ConstExpr\ConstExprStringNode|Ast\Type\IdentifierTypeNode
	 */
	private function parseObjectShapeKey(TokenIterator $tokens)
	{
		$startLine = $tokens->currentTokenLine();
		$startIndex = $tokens->currentTokenIndex();

		if ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING)) {
			$key = new Ast\ConstExpr\ConstExprStringNode(StringUnescaper::unescapeString($tokens->currentTokenValue()), Ast\ConstExpr\ConstExprStringNode::SINGLE_QUOTED);
			$tokens->next();

		} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_QUOTED_STRING)) {
			$key = new Ast\ConstExpr\ConstExprStringNode(StringUnescaper::unescapeString($tokens->currentTokenValue()), Ast\ConstExpr\ConstExprStringNode::DOUBLE_QUOTED);
			$tokens->next();

		} else {
			$key = new Ast\Type\IdentifierTypeNode($tokens->currentTokenValue());
			$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
		}

		return $this->enrichWithAttributes($tokens, $key, $startLine, $startIndex);
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Parser;

use PHPStan\PhpDocParser\Ast;
use PHPStan\PhpDocParser\Lexer\Lexer;
use PHPStan\PhpDocParser\ParserConfig;
use function str_replace;
use function strtolower;

class ConstExprParser
{

	private ParserConfig $config;

	private bool $parseDoctrineStrings;

	public function __construct(
		ParserConfig $config
	)
	{
		$this->config = $config;
		$this->parseDoctrineStrings = false;
	}

	/**
	 * @internal
	 */
	public function toDoctrine(): self
	{
		$self = new self($this->config);
		$self->parseDoctrineStrings = true;
		return $self;
	}

	public function parse(TokenIterator $tokens): Ast\ConstExpr\ConstExprNode
	{
		$startLine = $tokens->currentTokenLine();
		$startIndex = $tokens->currentTokenIndex();
		if ($tokens->isCurrentTokenType(Lexer::TOKEN_FLOAT)) {
			$value = $tokens->currentTokenValue();
			$tokens->next();

			return $this->enrichWithAttributes(
				$tokens,
				new Ast\ConstExpr\ConstExprFloatNode(str_replace('_', '', $value)),
				$startLine,
				$startIndex,
			);
		}

		if ($tokens->isCurrentTokenType(Lexer::TOKEN_INTEGER)) {
			$value = $tokens->currentTokenValue();
			$tokens->next();

			return $this->enrichWithAttributes(
				$tokens,
				new Ast\ConstExpr\ConstExprIntegerNode(str_replace('_', '', $value)),
				$startLine,
				$startIndex,
			);
		}

		if ($this->parseDoctrineStrings && $tokens->isCurrentTokenType(Lexer::TOKEN_DOCTRINE_ANNOTATION_STRING)) {
			$value = $tokens->currentTokenValue();
			$tokens->next();

			return $this->enrichWithAttributes(
				$tokens,
				new Ast\ConstExpr\DoctrineConstExprStringNode(Ast\ConstExpr\DoctrineConstExprStringNode::unescape($value)),
				$startLine,
				$startIndex,
			);
		}

		if ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING, Lexer::TOKEN_DOUBLE_QUOTED_STRING)) {
			if ($this->parseDoctrineStrings) {
				if ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING)) {
					throw new ParserException(
						$tokens->currentTokenValue(),
						$tokens->currentTokenType(),
						$tokens->currentTokenOffset(),
						Lexer::TOKEN_DOUBLE_QUOTED_STRING,
						null,
						$tokens->currentTokenLine(),
					);
				}

				$value = $tokens->currentTokenValue();
				$tokens->next();

				return $this->enrichWithAttributes(
					$tokens,
					$this->parseDoctrineString($value, $tokens),
					$startLine,
					$startIndex,
				);
			}

			$value = StringUnescaper::unescapeString($tokens->currentTokenValue());
			$type = $tokens->currentTokenType();
			$tokens->next();

			return $this->enrichWithAttributes(
				$tokens,
				new Ast\ConstExpr\ConstExprStringNode(
					$value,
					$type === Lexer::TOKEN_SINGLE_QUOTED_STRING
						? Ast\ConstExpr\ConstExprStringNode::SINGLE_QUOTED
						: Ast\ConstExpr\ConstExprStringNode::DOUBLE_QUOTED,
				),
				$startLine,
				$startIndex,
			);

		} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_IDENTIFIER)) {
			$identifier = $tokens->currentTokenValue();
			$tokens->next();

			switch (strtolower($identifier)) {
				case 'true':
					return $this->enrichWithAttributes(
						$tokens,
						new Ast\ConstExpr\ConstExprTrueNode(),
						$startLine,
						$startIndex,
					);
				case 'false':
					return $this->enrichWithAttributes(
						$tokens,
						new Ast\ConstExpr\ConstExprFalseNode(),
						$startLine,
						$startIndex,
					);
				case 'null':
					return $this->enrichWithAttributes(
						$tokens,
						new Ast\ConstExpr\ConstExprNullNode(),
						$startLine,
						$startIndex,
					);
				case 'array':
					$tokens->consumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES);
					return $this->parseArray($tokens, Lexer::TOKEN_CLOSE_PARENTHESES, $startIndex);
			}

			if ($tokens->tryConsumeTokenType(Lexer::TOKEN_DOUBLE_COLON)) {
				$classConstantName = '';
				$lastType = null;
				while (true) {
					if ($lastType !== Lexer::TOKEN_IDENTIFIER && $tokens->currentTokenType() === Lexer::TOKEN_IDENTIFIER) {
						$classConstantName .= $tokens->currentTokenValue();
						$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
						$lastType = Lexer::TOKEN_IDENTIFIER;

						continue;
					}

					if ($lastType !== Lexer::TOKEN_WILDCARD && $tokens->tryConsumeTokenType(Lexer::TOKEN_WILDCARD)) {
						$classConstantName .= '*';
						$lastType = Lexer::TOKEN_WILDCARD;

						if ($tokens->getSkippedHorizontalWhiteSpaceIfAny() !== '') {
							break;
						}

						continue;
					}

					if ($lastType === null) {
						// trigger parse error if nothing valid was consumed
						$tokens->consumeTokenType(Lexer::TOKEN_WILDCARD);
					}

					break;
				}

				return $this->enrichWithAttributes(
					$tokens,
					new Ast\ConstExpr\ConstFetchNode($identifier, $classConstantName),
					$startLine,
					$startIndex,
				);

			}

			return $this->enrichWithAttributes(
				$tokens,
				new Ast\ConstExpr\ConstFetchNode('', $identifier),
				$startLine,
				$startIndex,
			);

		} elseif ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
			return $this->parseArray($tokens, Lexer::TOKEN_CLOSE_SQUARE_BRACKET, $startIndex);
		}

		throw new ParserException(
			$tokens->currentTokenValue(),
			$tokens->currentTokenType(),
			$tokens->currentTokenOffset(),
			Lexer::TOKEN_IDENTIFIER,
			null,
			$tokens->currentTokenLine(),
		);
	}


	private function parseArray(TokenIterator $tokens, int $endToken, int $startIndex): Ast\ConstExpr\ConstExprArrayNode
	{
		$items = [];

		$startLine = $tokens->currentTokenLine();

		if (!$tokens->tryConsumeTokenType($endToken)) {
			do {
				$items[] = $this->parseArrayItem($tokens);
			} while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA) && !$tokens->isCurrentTokenType($endToken));
			$tokens->consumeTokenType($endToken);
		}

		return $this->enrichWithAttributes(
			$tokens,
			new Ast\ConstExpr\ConstExprArrayNode($items),
			$startLine,
			$startIndex,
		);
	}


	/**
	 * This method is supposed to be called with TokenIterator after reading TOKEN_DOUBLE_QUOTED_STRING and shifting
	 * to the next token.
	 */
	public function parseDoctrineString(string $text, TokenIterator $tokens): Ast\ConstExpr\DoctrineConstExprStringNode
	{
		// Because of how Lexer works, a valid Doctrine string
		// can consist of a sequence of TOKEN_DOUBLE_QUOTED_STRING and TOKEN_DOCTRINE_ANNOTATION_STRING
		while ($tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_QUOTED_STRING, Lexer::TOKEN_DOCTRINE_ANNOTATION_STRING)) {
			$text .= $tokens->currentTokenValue();
			$tokens->next();
		}

		return new Ast\ConstExpr\DoctrineConstExprStringNode(Ast\ConstExpr\DoctrineConstExprStringNode::unescape($text));
	}


	private function parseArrayItem(TokenIterator $tokens): Ast\ConstExpr\ConstExprArrayItemNode
	{
		$startLine = $tokens->currentTokenLine();
		$startIndex = $tokens->currentTokenIndex();

		$expr = $this->parse($tokens);

		if ($tokens->tryConsumeTokenType(Lexer::TOKEN_DOUBLE_ARROW)) {
			$key = $expr;
			$value = $this->parse($tokens);

		} else {
			$key = null;
			$value = $expr;
		}

		return $this->enrichWithAttributes(
			$tokens,
			new Ast\ConstExpr\ConstExprArrayItemNode($key, $value),
			$startLine,
			$startIndex,
		);
	}

	/**
	 * @template T of Ast\ConstExpr\ConstExprNode
	 * @param T $node
	 * @return T
	 */
	private function enrichWithAttributes(TokenIterator $tokens, Ast\ConstExpr\ConstExprNode $node, int $startLine, int $startIndex): Ast\ConstExpr\ConstExprNode
	{
		if ($this->config->useLinesAttributes) {
			$node->setAttribute(Ast\Attribute::START_LINE, $startLine);
			$node->setAttribute(Ast\Attribute::END_LINE, $tokens->currentTokenLine());
		}

		if ($this->config->useIndexAttributes) {
			$node->setAttribute(Ast\Attribute::START_INDEX, $startIndex);
			$node->setAttribute(Ast\Attribute::END_INDEX, $tokens->endIndexOfLastRelevantToken());
		}

		return $node;
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Printer;

use LogicException;
use PHPStan\PhpDocParser\Ast\Attribute;
use PHPStan\PhpDocParser\Ast\Comment;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprNode;
use PHPStan\PhpDocParser\Ast\Node;
use PHPStan\PhpDocParser\Ast\PhpDoc\AssertTagMethodValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\AssertTagPropertyValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\AssertTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine\DoctrineAnnotation;
use PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine\DoctrineArgument;
use PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine\DoctrineArray;
use PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine\DoctrineArrayItem;
use PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine\DoctrineTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ExtendsTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ImplementsTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueParameterNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\MixinTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamClosureThisTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamImmediatelyInvokedCallableTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamLaterInvokedCallableTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamOutTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocChildNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PropertyTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PureUnlessCallableIsImpureTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\RequireExtendsTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\RequireImplementsTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\SealedTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\SelfOutTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ThrowsTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\TypeAliasImportTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\TypeAliasTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\UsesTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeItemNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeUnsealedTypeNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode;
use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode;
use PHPStan\PhpDocParser\Ast\Type\ConditionalTypeForParameterNode;
use PHPStan\PhpDocParser\Ast\Type\ConditionalTypeNode;
use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode;
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode;
use PHPStan\PhpDocParser\Ast\Type\InvalidTypeNode;
use PHPStan\PhpDocParser\Ast\Type\NullableTypeNode;
use PHPStan\PhpDocParser\Ast\Type\ObjectShapeItemNode;
use PHPStan\PhpDocParser\Ast\Type\ObjectShapeNode;
use PHPStan\PhpDocParser\Ast\Type\OffsetAccessTypeNode;
use PHPStan\PhpDocParser\Ast\Type\ThisTypeNode;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
use PHPStan\PhpDocParser\Lexer\Lexer;
use PHPStan\PhpDocParser\Parser\TokenIterator;
use function array_keys;
use function array_map;
use function assert;
use function count;
use function get_class;
use function get_object_vars;
use function implode;
use function in_array;
use function is_array;
use function preg_match_all;
use function sprintf;
use function str_replace;
use function strlen;
use function strpos;
use function trim;
use const PREG_SET_ORDER;

/**
 * Inspired by https://github.com/nikic/PHP-Parser/tree/36a6dcd04e7b0285e8f0868f44bd4927802f7df1
 *
 * Copyright (c) 2011, Nikita Popov
 * All rights reserved.
 */
final class Printer
{

	/** @var Differ<Node> */
	private Differ $differ;

	/**
	 * Map From "{$class}->{$subNode}" to string that should be inserted
	 * between elements of this list subnode
	 *
	 * @var array<string, string>
	 */
	private array $listInsertionMap = [
		PhpDocNode::class . '->children' => "\n * ",
		UnionTypeNode::class . '->types' => '|',
		IntersectionTypeNode::class . '->types' => '&',
		ArrayShapeNode::class . '->items' => ', ',
		ObjectShapeNode::class . '->items' => ', ',
		CallableTypeNode::class . '->parameters' => ', ',
		CallableTypeNode::class . '->templateTypes' => ', ',
		GenericTypeNode::class . '->genericTypes' => ', ',
		ConstExprArrayNode::class . '->items' => ', ',
		MethodTagValueNode::class . '->parameters' => ', ',
		DoctrineArray::class . '->items' => ', ',
		DoctrineAnnotation::class . '->arguments' => ', ',
	];

	/**
	 * [$find, $extraLeft, $extraRight]
	 *
	 * @var array<string, array{string|null, string, string}>
	 */
	private array $emptyListInsertionMap = [
		CallableTypeNode::class . '->parameters' => ['(', '', ''],
		ArrayShapeNode::class . '->items' => ['{', '', ''],
		ObjectShapeNode::class . '->items' => ['{', '', ''],
		DoctrineArray::class . '->items' => ['{', '', ''],
		DoctrineAnnotation::class . '->arguments' => ['(', '', ''],
	];

	/** @var array<string, list<class-string<TypeNode>>> */
	private array $parenthesesMap = [
		CallableTypeNode::class . '->returnType' => [
			CallableTypeNode::class,
			UnionTypeNode::class,
			IntersectionTypeNode::class,
		],
		ArrayTypeNode::class . '->type' => [
			CallableTypeNode::class,
			UnionTypeNode::class,
			IntersectionTypeNode::class,
			ConstTypeNode::class,
			NullableTypeNode::class,
		],
		OffsetAccessTypeNode::class . '->type' => [
			CallableTypeNode::class,
			UnionTypeNode::class,
			IntersectionTypeNode::class,
			NullableTypeNode::class,
		],
	];

	/** @var array<string, list<class-string<TypeNode>>> */
	private array $parenthesesListMap = [
		IntersectionTypeNode::class . '->types' => [
			IntersectionTypeNode::class,
			UnionTypeNode::class,
			NullableTypeNode::class,
		],
		UnionTypeNode::class . '->types' => [
			IntersectionTypeNode::class,
			UnionTypeNode::class,
			NullableTypeNode::class,
		],
	];

	public function printFormatPreserving(PhpDocNode $node, PhpDocNode $originalNode, TokenIterator $originalTokens): string
	{
		$this->differ = new Differ(static function ($a, $b) {
			if ($a instanceof Node && $b instanceof Node) {
				return $a === $b->getAttribute(Attribute::ORIGINAL_NODE);
			}

			return false;
		});

		$tokenIndex = 0;
		$result = $this->printArrayFormatPreserving(
			$node->children,
			$originalNode->children,
			$originalTokens,
			$tokenIndex,
			PhpDocNode::class,
			'children',
		);
		if ($result !== null) {
			return $result . $originalTokens->getContentBetween($tokenIndex, $originalTokens->getTokenCount());
		}

		return $this->print($node);
	}

	public function print(Node $node): string
	{
		if ($node instanceof PhpDocNode) {
			return "/**\n *" . implode("\n *", array_map(
				function (PhpDocChildNode $child): string {
					$s = $this->print($child);
					return $s === '' ? '' : ' ' . $s;
				},
				$node->children,
			)) . "\n */";
		}
		if ($node instanceof PhpDocTextNode) {
			return $node->text;
		}
		if ($node instanceof PhpDocTagNode) {
			if ($node->value instanceof DoctrineTagValueNode) {
				return $this->print($node->value);
			}

			return trim(sprintf('%s %s', $node->name, $this->print($node->value)));
		}
		if ($node instanceof PhpDocTagValueNode) {
			return $this->printTagValue($node);
		}
		if ($node instanceof TypeNode) {
			return $this->printType($node);
		}
		if ($node instanceof ConstExprNode) {
			return $this->printConstExpr($node);
		}
		if ($node instanceof MethodTagValueParameterNode) {
			$type = $node->type !== null ? $this->print($node->type) . ' ' : '';
			$isReference = $node->isReference ? '&' : '';
			$isVariadic = $node->isVariadic ? '...' : '';
			$default = $node->defaultValue !== null ? ' = ' . $this->print($node->defaultValue) : '';
			return "{$type}{$isReference}{$isVariadic}{$node->parameterName}{$default}";
		}
		if ($node instanceof CallableTypeParameterNode) {
			$type = $this->print($node->type) . ' ';
			$isReference = $node->isReference ? '&' : '';
			$isVariadic = $node->isVariadic ? '...' : '';
			$isOptional = $node->isOptional ? '=' : '';
			return trim("{$type}{$isReference}{$isVariadic}{$node->parameterName}") . $isOptional;
		}
		if ($node instanceof ArrayShapeUnsealedTypeNode) {
			if ($node->keyType !== null) {
				return sprintf('<%s, %s>', $this->printType($node->keyType), $this->printType($node->valueType));
			}
			return sprintf('<%s>', $this->printType($node->valueType));
		}
		if ($node instanceof DoctrineAnnotation) {
			return (string) $node;
		}
		if ($node instanceof DoctrineArgument) {
			return (string) $node;
		}
		if ($node instanceof DoctrineArray) {
			return (string) $node;
		}
		if ($node instanceof DoctrineArrayItem) {
			return (string) $node;
		}
		if ($node instanceof ArrayShapeItemNode) {
			if ($node->keyName !== null) {
				return sprintf(
					'%s%s: %s',
					$this->print($node->keyName),
					$node->optional ? '?' : '',
					$this->printType($node->valueType),
				);
			}

			return $this->printType($node->valueType);
		}
		if ($node instanceof ObjectShapeItemNode) {
			if ($node->keyName !== null) {
				return sprintf(
					'%s%s: %s',
					$this->print($node->keyName),
					$node->optional ? '?' : '',
					$this->printType($node->valueType),
				);
			}

			return $this->printType($node->valueType);
		}

		throw new LogicException(sprintf('Unknown node type %s', get_class($node)));
	}

	private function printTagValue(PhpDocTagValueNode $node): string
	{
		// only nodes that contain another node are handled here
		// the rest falls back on (string) $node

		if ($node instanceof AssertTagMethodValueNode) {
			$isNegated = $node->isNegated ? '!' : '';
			$isEquality = $node->isEquality ? '=' : '';
			$type = $this->printType($node->type);
			return trim("{$isNegated}{$isEquality}{$type} {$node->parameter}->{$node->method}() {$node->description}");
		}
		if ($node instanceof AssertTagPropertyValueNode) {
			$isNegated = $node->isNegated ? '!' : '';
			$isEquality = $node->isEquality ? '=' : '';
			$type = $this->printType($node->type);
			return trim("{$isNegated}{$isEquality}{$type} {$node->parameter}->{$node->property} {$node->description}");
		}
		if ($node instanceof AssertTagValueNode) {
			$isNegated = $node->isNegated ? '!' : '';
			$isEquality = $node->isEquality ? '=' : '';
			$type = $this->printType($node->type);
			return trim("{$isNegated}{$isEquality}{$type} {$node->parameter} {$node->description}");
		}
		if ($node instanceof ExtendsTagValueNode || $node instanceof ImplementsTagValueNode) {
			$type = $this->printType($node->type);
			return trim("{$type} {$node->description}");
		}
		if ($node instanceof MethodTagValueNode) {
			$static = $node->isStatic ? 'static ' : '';
			$returnType = $node->returnType !== null ? $this->printType($node->returnType) . ' ' : '';
			$parameters = implode(', ', array_map(fn (MethodTagValueParameterNode $parameter): string => $this->print($parameter), $node->parameters));
			$description = $node->description !== '' ? " {$node->description}" : '';
			$templateTypes = count($node->templateTypes) > 0 ? '<' . implode(', ', array_map(fn (TemplateTagValueNode $templateTag): string => $this->print($templateTag), $node->templateTypes)) . '>' : '';
			return "{$static}{$returnType}{$node->methodName}{$templateTypes}({$parameters}){$description}";
		}
		if ($node instanceof MixinTagValueNode) {
			$type = $this->printType($node->type);
			return trim("{$type} {$node->description}");
		}
		if ($node instanceof RequireExtendsTagValueNode) {
			$type = $this->printType($node->type);
			return trim("{$type} {$node->description}");
		}
		if ($node instanceof RequireImplementsTagValueNode) {
			$type = $this->printType($node->type);
			return trim("{$type} {$node->description}");
		}
		if ($node instanceof SealedTagValueNode) {
			$type = $this->printType($node->type);
			return trim("{$type} {$node->description}");
		}
		if ($node instanceof ParamOutTagValueNode) {
			$type = $this->printType($node->type);
			return trim("{$type} {$node->parameterName} {$node->description}");
		}
		if ($node instanceof ParamTagValueNode) {
			$reference = $node->isReference ? '&' : '';
			$variadic = $node->isVariadic ? '...' : '';
			$type = $this->printType($node->type);
			return trim("{$type} {$reference}{$variadic}{$node->parameterName} {$node->description}");
		}
		if ($node instanceof ParamImmediatelyInvokedCallableTagValueNode) {
			return trim("{$node->parameterName} {$node->description}");
		}
		if ($node instanceof ParamLaterInvokedCallableTagValueNode) {
			return trim("{$node->parameterName} {$node->description}");
		}
		if ($node instanceof ParamClosureThisTagValueNode) {
			return trim("{$node->type} {$node->parameterName} {$node->description}");
		}
		if ($node instanceof PureUnlessCallableIsImpureTagValueNode) {
			return trim("{$node->parameterName} {$node->description}");
		}
		if ($node instanceof PropertyTagValueNode) {
			$type = $this->printType($node->type);
			return trim("{$type} {$node->propertyName} {$node->description}");
		}
		if ($node instanceof ReturnTagValueNode) {
			$type = $this->printType($node->type);
			return trim("{$type} {$node->description}");
		}
		if ($node instanceof SelfOutTagValueNode) {
			$type = $this->printType($node->type);
			return trim($type . ' ' . $node->description);
		}
		if ($node instanceof TemplateTagValueNode) {
			$upperBound = $node->bound !== null ? ' of ' . $this->printType($node->bound) : '';
			$lowerBound = $node->lowerBound !== null ? ' super ' . $this->printType($node->lowerBound) : '';
			$default = $node->default !== null ? ' = ' . $this->printType($node->default) : '';
			return trim("{$node->name}{$upperBound}{$lowerBound}{$default} {$node->description}");
		}
		if ($node instanceof ThrowsTagValueNode) {
			$type = $this->printType($node->type);
			return trim("{$type} {$node->description}");
		}
		if ($node instanceof TypeAliasImportTagValueNode) {
			return trim(
				"{$node->importedAlias} from " . $this->printType($node->importedFrom)
				. ($node->importedAs !== null ? " as {$node->importedAs}" : ''),
			);
		}
		if ($node instanceof TypeAliasTagValueNode) {
			$type = $this->printType($node->type);
			return trim("{$node->alias} {$type}");
		}
		if ($node instanceof UsesTagValueNode) {
			$type = $this->printType($node->type);
			return trim("{$type} {$node->description}");
		}
		if ($node instanceof VarTagValueNode) {
			$type = $this->printType($node->type);
			return trim("{$type} " . trim("{$node->variableName} {$node->description}"));
		}

		return (string) $node;
	}

	private function printType(TypeNode $node): string
	{
		if ($node instanceof ArrayShapeNode) {
			$items = array_map(fn (ArrayShapeItemNode $item): string => $this->print($item), $node->items);

			if (! $node->sealed) {
				$items[] = '...' . ($node->unsealedType === null ? '' : $this->print($node->unsealedType));
			}

			return $node->kind . '{' . implode(', ', $items) . '}';
		}
		if ($node instanceof ArrayTypeNode) {
			return $this->printOffsetAccessType($node->type) . '[]';
		}
		if ($node instanceof CallableTypeNode) {
			if ($node->returnType instanceof CallableTypeNode || $node->returnType instanceof UnionTypeNode || $node->returnType instanceof IntersectionTypeNode) {
				$returnType = $this->wrapInParentheses($node->returnType);
			} else {
				$returnType = $this->printType($node->returnType);
			}
			$template = $node->templateTypes !== []
				? '<' . implode(', ', array_map(fn (TemplateTagValueNode $templateNode): string => $this->print($templateNode), $node->templateTypes)) . '>'
				: '';
			$parameters = implode(', ', array_map(fn (CallableTypeParameterNode $parameterNode): string => $this->print($parameterNode), $node->parameters));
			return "{$node->identifier}{$template}({$parameters}): {$returnType}";
		}
		if ($node instanceof ConditionalTypeForParameterNode) {
			return sprintf(
				'(%s %s %s ? %s : %s)',
				$node->parameterName,
				$node->negated ? 'is not' : 'is',
				$this->printType($node->targetType),
				$this->printType($node->if),
				$this->printType($node->else),
			);
		}
		if ($node instanceof ConditionalTypeNode) {
			return sprintf(
				'(%s %s %s ? %s : %s)',
				$this->printType($node->subjectType),
				$node->negated ? 'is not' : 'is',
				$this->printType($node->targetType),
				$this->printType($node->if),
				$this->printType($node->else),
			);
		}
		if ($node instanceof ConstTypeNode) {
			return $this->printConstExpr($node->constExpr);
		}
		if ($node instanceof GenericTypeNode) {
			$genericTypes = [];

			foreach ($node->genericTypes as $index => $type) {
				$variance = $node->variances[$index] ?? GenericTypeNode::VARIANCE_INVARIANT;
				if ($variance === GenericTypeNode::VARIANCE_INVARIANT) {
					$genericTypes[] = $this->printType($type);
				} elseif ($variance === GenericTypeNode::VARIANCE_BIVARIANT) {
					$genericTypes[] = '*';
				} else {
					$genericTypes[] = sprintf('%s %s', $variance, $this->print($type));
				}
			}

			return $node->type . '<' . implode(', ', $genericTypes) . '>';
		}
		if ($node instanceof IdentifierTypeNode) {
			return $node->name;
		}
		if ($node instanceof IntersectionTypeNode || $node instanceof UnionTypeNode) {
			$items = [];
			foreach ($node->types as $type) {
				if (
					$type instanceof IntersectionTypeNode
					|| $type instanceof UnionTypeNode
					|| $type instanceof NullableTypeNode
				) {
					$items[] = $this->wrapInParentheses($type);
					continue;
				}

				$items[] = $this->printType($type);
			}

			return implode($node instanceof IntersectionTypeNode ? '&' : '|', $items);
		}
		if ($node instanceof InvalidTypeNode) {
			return (string) $node;
		}
		if ($node instanceof NullableTypeNode) {
			if ($node->type instanceof IntersectionTypeNode || $node->type instanceof UnionTypeNode) {
				return '?(' . $this->printType($node->type) . ')';
			}

			return '?' . $this->printType($node->type);
		}
		if ($node instanceof ObjectShapeNode) {
			$items = array_map(fn (ObjectShapeItemNode $item): string => $this->print($item), $node->items);

			return 'object{' . implode(', ', $items) . '}';
		}
		if ($node instanceof OffsetAccessTypeNode) {
			return $this->printOffsetAccessType($node->type) . '[' . $this->printType($node->offset) . ']';
		}
		if ($node instanceof ThisTypeNode) {
			return (string) $node;
		}

		throw new LogicException(sprintf('Unknown node type %s', get_class($node)));
	}

	private function wrapInParentheses(TypeNode $node): string
	{
		return '(' . $this->printType($node) . ')';
	}

	private function printOffsetAccessType(TypeNode $type): string
	{
		if (
			$type instanceof CallableTypeNode
			|| $type instanceof UnionTypeNode
			|| $type instanceof IntersectionTypeNode
			|| $type instanceof NullableTypeNode
		) {
			return $this->wrapInParentheses($type);
		}

		return $this->printType($type);
	}

	private function printConstExpr(ConstExprNode $node): string
	{
		// this is fine - ConstExprNode classes do not contain nodes that need smart printer logic
		return (string) $node;
	}

	/**
	 * @param Node[] $nodes
	 * @param Node[] $originalNodes
	 */
	private function printArrayFormatPreserving(array $nodes, array $originalNodes, TokenIterator $originalTokens, int &$tokenIndex, string $parentNodeClass, string $subNodeName): ?string
	{
		$diff = $this->differ->diffWithReplacements($originalNodes, $nodes);
		$mapKey = $parentNodeClass . '->' . $subNodeName;
		$insertStr = $this->listInsertionMap[$mapKey] ?? null;
		$result = '';
		$beforeFirstKeepOrReplace = true;
		$delayedAdd = [];

		$insertNewline = false;
		[$isMultiline, $beforeAsteriskIndent, $afterAsteriskIndent] = $this->isMultiline($tokenIndex, $originalNodes, $originalTokens);

		if ($insertStr === "\n * ") {
			$insertStr = sprintf('%s%s*%s', $originalTokens->getDetectedNewline() ?? "\n", $beforeAsteriskIndent, $afterAsteriskIndent);
		}

		foreach ($diff as $i => $diffElem) {
			$diffType = $diffElem->type;
			$arrItem = $diffElem->new;
			$origArrayItem = $diffElem->old;
			if ($diffType === DiffElem::TYPE_KEEP || $diffType === DiffElem::TYPE_REPLACE) {
				$beforeFirstKeepOrReplace = false;
				if (!$arrItem instanceof Node || !$origArrayItem instanceof Node) {
					return null;
				}

				/** @var int $itemStartPos */
				$itemStartPos = $origArrayItem->getAttribute(Attribute::START_INDEX);

				/** @var int $itemEndPos */
				$itemEndPos = $origArrayItem->getAttribute(Attribute::END_INDEX);

				if ($itemStartPos < 0 || $itemEndPos < 0 || $itemStartPos < $tokenIndex) {
					throw new LogicException();
				}

				$comments = $arrItem->getAttribute(Attribute::COMMENTS) ?? [];
				$origComments = $origArrayItem->getAttribute(Attribute::COMMENTS) ?? [];

				$commentStartPos = count($origComments) > 0 ? $origComments[0]->startIndex : $itemStartPos;
				assert($commentStartPos >= 0);

				$result .= $originalTokens->getContentBetween($tokenIndex, $itemStartPos);

				if (count($delayedAdd) > 0) {
					foreach ($delayedAdd as $delayedAddNode) {
						$parenthesesNeeded = isset($this->parenthesesListMap[$mapKey])
							&& in_array(get_class($delayedAddNode), $this->parenthesesListMap[$mapKey], true);
						if ($parenthesesNeeded) {
							$result .= '(';
						}

						if ($insertNewline) {
							$delayedAddComments = $delayedAddNode->getAttribute(Attribute::COMMENTS) ?? [];
							if (count($delayedAddComments) > 0) {
								$result .= $this->printComments($delayedAddComments, $beforeAsteriskIndent, $afterAsteriskIndent);
								$result .= sprintf('%s%s*%s', $originalTokens->getDetectedNewline() ?? "\n", $beforeAsteriskIndent, $afterAsteriskIndent);
							}
						}

						$result .= $this->printNodeFormatPreserving($delayedAddNode, $originalTokens);
						if ($parenthesesNeeded) {
							$result .= ')';
						}

						if ($insertNewline) {
							$result .= $insertStr . sprintf('%s%s*%s', $originalTokens->getDetectedNewline() ?? "\n", $beforeAsteriskIndent, $afterAsteriskIndent);
						} else {
							$result .= $insertStr;
						}
					}

					$delayedAdd = [];
				}

				$parenthesesNeeded = isset($this->parenthesesListMap[$mapKey])
					&& in_array(get_class($arrItem), $this->parenthesesListMap[$mapKey], true)
					&& !in_array(get_class($origArrayItem), $this->parenthesesListMap[$mapKey], true);
				$addParentheses = $parenthesesNeeded && !$originalTokens->hasParentheses($itemStartPos, $itemEndPos);
				if ($addParentheses) {
					$result .= '(';
				}

				if ($comments !== $origComments) {
					if (count($comments) > 0) {
						$result .= $this->printComments($comments, $beforeAsteriskIndent, $afterAsteriskIndent);
						$result .= sprintf('%s%s*%s', $originalTokens->getDetectedNewline() ?? "\n", $beforeAsteriskIndent, $afterAsteriskIndent);
					}
				}

				$result .= $this->printNodeFormatPreserving($arrItem, $originalTokens);
				if ($addParentheses) {
					$result .= ')';
				}
				$tokenIndex = $itemEndPos + 1;

			} elseif ($diffType === DiffElem::TYPE_ADD) {
				if ($insertStr === null) {
					return null;
				}
				if (!$arrItem instanceof Node) {
					return null;
				}

				if ($insertStr === ', ' && $isMultiline || count($arrItem->getAttribute(Attribute::COMMENTS) ?? []) > 0) {
					$insertStr = ',';
					$insertNewline = true;
				}

				if ($beforeFirstKeepOrReplace) {
					// Will be inserted at the next "replace" or "keep" element
					$delayedAdd[] = $arrItem;
					continue;
				}

				/** @var int $itemEndPos */
				$itemEndPos = $tokenIndex - 1;
				if ($insertNewline) {
					$comments = $arrItem->getAttribute(Attribute::COMMENTS) ?? [];
					$result .= $insertStr;
					if (count($comments) > 0) {
						$result .= sprintf('%s%s*%s', $originalTokens->getDetectedNewline() ?? "\n", $beforeAsteriskIndent, $afterAsteriskIndent);
						$result .= $this->printComments($comments, $beforeAsteriskIndent, $afterAsteriskIndent);
					}
					$result .= sprintf('%s%s*%s', $originalTokens->getDetectedNewline() ?? "\n", $beforeAsteriskIndent, $afterAsteriskIndent);
				} else {
					$result .= $insertStr;
				}

				$parenthesesNeeded = isset($this->parenthesesListMap[$mapKey])
					&& in_array(get_class($arrItem), $this->parenthesesListMap[$mapKey], true);
				if ($parenthesesNeeded) {
					$result .= '(';
				}

				$result .= $this->printNodeFormatPreserving($arrItem, $originalTokens);
				if ($parenthesesNeeded) {
					$result .= ')';
				}

				$tokenIndex = $itemEndPos + 1;

			} elseif ($diffType === DiffElem::TYPE_REMOVE) {
				if (!$origArrayItem instanceof Node) {
					return null;
				}

				/** @var int $itemStartPos */
				$itemStartPos = $origArrayItem->getAttribute(Attribute::START_INDEX);

				/** @var int $itemEndPos */
				$itemEndPos = $origArrayItem->getAttribute(Attribute::END_INDEX);
				if ($itemStartPos < 0 || $itemEndPos < 0) {
					throw new LogicException();
				}

				if ($i === 0) {
					// If we're removing from the start, keep the tokens before the node and drop those after it,
					// instead of the other way around.
					$originalTokensArray = $originalTokens->getTokens();
					for ($j = $tokenIndex; $j < $itemStartPos; $j++) {
						if ($originalTokensArray[$j][Lexer::TYPE_OFFSET] === Lexer::TOKEN_PHPDOC_EOL) {
							break;
						}
						$result .= $originalTokensArray[$j][Lexer::VALUE_OFFSET];
					}
				}

				$tokenIndex = $itemEndPos + 1;
			}
		}

		if (count($delayedAdd) > 0) {
			if (!isset($this->emptyListInsertionMap[$mapKey])) {
				return null;
			}

			[$findToken, $extraLeft, $extraRight] = $this->emptyListInsertionMap[$mapKey];
			if ($findToken !== null) {
				$originalTokensArray = $originalTokens->getTokens();
				for (; $tokenIndex < count($originalTokensArray); $tokenIndex++) {
					$result .= $originalTokensArray[$tokenIndex][Lexer::VALUE_OFFSET];
					if ($originalTokensArray[$tokenIndex][Lexer::VALUE_OFFSET] !== $findToken) {
						continue;
					}

					$tokenIndex++;
					break;
				}
			}
			$first = true;
			$result .= $extraLeft;
			foreach ($delayedAdd as $delayedAddNode) {
				if (!$first) {
					$result .= $insertStr;
					if ($insertNewline) {
						$result .= sprintf('%s%s*%s', $originalTokens->getDetectedNewline() ?? "\n", $beforeAsteriskIndent, $afterAsteriskIndent);
					}
				}

				$result .= $this->printNodeFormatPreserving($delayedAddNode, $originalTokens);
				$first = false;
			}
			$result .= $extraRight;
		}

		return $result;
	}

	/**
	 * @param list<Comment> $comments
	 */
	private function printComments(array $comments, string $beforeAsteriskIndent, string $afterAsteriskIndent): string
	{
		$formattedComments = [];

		foreach ($comments as $comment) {
			$formattedComments[] = str_replace("\n", "\n" . $beforeAsteriskIndent . '*' . $afterAsteriskIndent, $comment->getReformattedText());
		}

		return implode("\n$beforeAsteriskIndent*$afterAsteriskIndent", $formattedComments);
	}

	/**
	 * @param array<Node|null> $nodes
	 * @return array{bool, string, string}
	 */
	private function isMultiline(int $initialIndex, array $nodes, TokenIterator $originalTokens): array
	{
		$isMultiline = count($nodes) > 1;
		$pos = $initialIndex;
		$allText = '';
		/** @var Node|null $node */
		foreach ($nodes as $node) {
			if (!$node instanceof Node) {
				continue;
			}

			$endPos = $node->getAttribute(Attribute::END_INDEX) + 1;
			$text = $originalTokens->getContentBetween($pos, $endPos);
			$allText .= $text;
			if (strpos($text, "\n") === false) {
				// We require that a newline is present between *every* item. If the formatting
				// is inconsistent, with only some items having newlines, we don't consider it
				// as multiline
				$isMultiline = false;
			}
			$pos = $endPos;
		}

		$c = preg_match_all('~\n(?<before>[\\x09\\x20]*)\*(?<after>\\x20*)~', $allText, $matches, PREG_SET_ORDER);
		if ($c === 0) {
			return [$isMultiline, ' ', '  '];
		}

		$before = '';
		$after = '';
		foreach ($matches as $match) {
			if (strlen($match['before']) > strlen($before)) {
				$before = $match['before'];
			}
			if (strlen($match['after']) <= strlen($after)) {
				continue;
			}

			$after = $match['after'];
		}

		$before = strlen($before) === 0 ? ' ' : $before;
		$after = strlen($after) === 0 ? '  ' : $after;

		return [$isMultiline, $before, $after];
	}

	private function printNodeFormatPreserving(Node $node, TokenIterator $originalTokens): string
	{
		/** @var Node|null $originalNode */
		$originalNode = $node->getAttribute(Attribute::ORIGINAL_NODE);
		if ($originalNode === null) {
			return $this->print($node);
		}

		$class = get_class($node);
		if ($class !== get_class($originalNode)) {
			throw new LogicException();
		}

		$startPos = $originalNode->getAttribute(Attribute::START_INDEX);
		$endPos = $originalNode->getAttribute(Attribute::END_INDEX);
		if ($startPos < 0 || $endPos < 0) {
			throw new LogicException();
		}

		$result = '';
		$pos = $startPos;
		$subNodeNames = array_keys(get_object_vars($node));
		foreach ($subNodeNames as $subNodeName) {
			$subNode = $node->$subNodeName;
			$origSubNode = $originalNode->$subNodeName;

			if (
				(!$subNode instanceof Node && $subNode !== null)
				|| (!$origSubNode instanceof Node && $origSubNode !== null)
			) {
				if ($subNode === $origSubNode) {
					// Unchanged, can reuse old code
					continue;
				}

				if (is_array($subNode) && is_array($origSubNode)) {
					// Array subnode changed, we might be able to reconstruct it
					$listResult = $this->printArrayFormatPreserving(
						$subNode,
						$origSubNode,
						$originalTokens,
						$pos,
						$class,
						$subNodeName,
					);

					if ($listResult === null) {
						return $this->print($node);
					}

					$result .= $listResult;
					continue;
				}

				return $this->print($node);
			}

			if ($origSubNode === null) {
				if ($subNode === null) {
					// Both null, nothing to do
					continue;
				}

				return $this->print($node);
			}

			$subStartPos = $origSubNode->getAttribute(Attribute::START_INDEX);
			$subEndPos = $origSubNode->getAttribute(Attribute::END_INDEX);
			if ($subStartPos < 0 || $subEndPos < 0) {
				throw new LogicException();
			}

			if ($subEndPos < $subStartPos) {
				return $this->print($node);
			}

			if ($subNode === null) {
				return $this->print($node);
			}

			$result .= $originalTokens->getContentBetween($pos, $subStartPos);
			$mapKey = get_class($node) . '->' . $subNodeName;
			$parenthesesNeeded = isset($this->parenthesesMap[$mapKey])
				&& in_array(get_class($subNode), $this->parenthesesMap[$mapKey], true);

			if ($subNode->getAttribute(Attribute::ORIGINAL_NODE) !== null) {
				$parenthesesNeeded = $parenthesesNeeded
					&& !in_array(get_class($subNode->getAttribute(Attribute::ORIGINAL_NODE)), $this->parenthesesMap[$mapKey], true);
			}

			$addParentheses = $parenthesesNeeded && !$originalTokens->hasParentheses($subStartPos, $subEndPos);
			if ($addParentheses) {
				$result .= '(';
			}

			$result .= $this->printNodeFormatPreserving($subNode, $originalTokens);
			if ($addParentheses) {
				$result .= ')';
			}

			$pos = $subEndPos + 1;
		}

		return $result . $originalTokens->getContentBetween($pos, $endPos + 1);
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Printer;

/**
 * Inspired by https://github.com/nikic/PHP-Parser/tree/36a6dcd04e7b0285e8f0868f44bd4927802f7df1
 *
 * Copyright (c) 2011, Nikita Popov
 * All rights reserved.
 *
 * Implements the Myers diff algorithm.
 *
 * @internal
 */
class DiffElem
{

	public const TYPE_KEEP = 0;
	public const TYPE_REMOVE = 1;
	public const TYPE_ADD = 2;
	public const TYPE_REPLACE = 3;

	/** @var self::TYPE_* */
	public $type;

	/** @var mixed Is null for add operations */
	public $old;

	/** @var mixed Is null for remove operations */
	public $new;

	/**
	 * @param self::TYPE_* $type
	 * @param mixed $old Is null for add operations
	 * @param mixed $new Is null for remove operations
	 */
	public function __construct(int $type, $old, $new)
	{
		$this->type = $type;
		$this->old = $old;
		$this->new = $new;
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Printer;

use Exception;
use function array_reverse;
use function count;

/**
 * Inspired by https://github.com/nikic/PHP-Parser/tree/36a6dcd04e7b0285e8f0868f44bd4927802f7df1
 *
 * Copyright (c) 2011, Nikita Popov
 * All rights reserved.
 *
 * Implements the Myers diff algorithm.
 *
 * Myers, Eugene W. "An O (ND) difference algorithm and its variations."
 * Algorithmica 1.1 (1986): 251-266.
 *
 * @template T
 * @internal
 */
class Differ
{

	/** @var callable(T, T): bool */
	private $isEqual;

	/**
	 * Create differ over the given equality relation.
	 *
	 * @param callable(T, T): bool $isEqual Equality relation
	 */
	public function __construct(callable $isEqual)
	{
		$this->isEqual = $isEqual;
	}

	/**
	 * Calculate diff (edit script) from $old to $new.
	 *
	 * @param T[] $old Original array
	 * @param T[] $new New array
	 *
	 * @return DiffElem[] Diff (edit script)
	 */
	public function diff(array $old, array $new): array
	{
		[$trace, $x, $y] = $this->calculateTrace($old, $new);
		return $this->extractDiff($trace, $x, $y, $old, $new);
	}

	/**
	 * Calculate diff, including "replace" operations.
	 *
	 * If a sequence of remove operations is followed by the same number of add operations, these
	 * will be coalesced into replace operations.
	 *
	 * @param T[] $old Original array
	 * @param T[] $new New array
	 *
	 * @return DiffElem[] Diff (edit script), including replace operations
	 */
	public function diffWithReplacements(array $old, array $new): array
	{
		return $this->coalesceReplacements($this->diff($old, $new));
	}

	/**
	 * @param T[] $old
	 * @param T[] $new
	 * @return array{array<int, array<int, int>>, int, int}
	 */
	private function calculateTrace(array $old, array $new): array
	{
		$n = count($old);
		$m = count($new);
		$max = $n + $m;
		$v = [1 => 0];
		$trace = [];
		for ($d = 0; $d <= $max; $d++) {
			$trace[] = $v;
			for ($k = -$d; $k <= $d; $k += 2) {
				if ($k === -$d || ($k !== $d && $v[$k - 1] < $v[$k + 1])) {
					$x = $v[$k + 1];
				} else {
					$x = $v[$k - 1] + 1;
				}

				$y = $x - $k;
				while ($x < $n && $y < $m && ($this->isEqual)($old[$x], $new[$y])) {
					$x++;
					$y++;
				}

				$v[$k] = $x;
				if ($x >= $n && $y >= $m) {
					return [$trace, $x, $y];
				}
			}
		}
		throw new Exception('Should not happen');
	}

	/**
	 * @param array<int, array<int, int>> $trace
	 * @param T[] $old
	 * @param T[] $new
	 * @return DiffElem[]
	 */
	private function extractDiff(array $trace, int $x, int $y, array $old, array $new): array
	{
		$result = [];
		for ($d = count($trace) - 1; $d >= 0; $d--) {
			$v = $trace[$d];
			$k = $x - $y;

			if ($k === -$d || ($k !== $d && $v[$k - 1] < $v[$k + 1])) {
				$prevK = $k + 1;
			} else {
				$prevK = $k - 1;
			}

			$prevX = $v[$prevK];
			$prevY = $prevX - $prevK;

			while ($x > $prevX && $y > $prevY) {
				$result[] = new DiffElem(DiffElem::TYPE_KEEP, $old[$x - 1], $new[$y - 1]);
				$x--;
				$y--;
			}

			if ($d === 0) {
				break;
			}

			while ($x > $prevX) {
				$result[] = new DiffElem(DiffElem::TYPE_REMOVE, $old[$x - 1], null);
				$x--;
			}

			while ($y > $prevY) {
				$result[] = new DiffElem(DiffElem::TYPE_ADD, null, $new[$y - 1]);
				$y--;
			}
		}
		return array_reverse($result);
	}

	/**
	 * Coalesce equal-length sequences of remove+add into a replace operation.
	 *
	 * @param DiffElem[] $diff
	 * @return DiffElem[]
	 */
	private function coalesceReplacements(array $diff): array
	{
		$newDiff = [];
		$c = count($diff);
		for ($i = 0; $i < $c; $i++) {
			$diffType = $diff[$i]->type;
			if ($diffType !== DiffElem::TYPE_REMOVE) {
				$newDiff[] = $diff[$i];
				continue;
			}

			$j = $i;
			while ($j < $c && $diff[$j]->type === DiffElem::TYPE_REMOVE) {
				$j++;
			}

			$k = $j;
			while ($k < $c && $diff[$k]->type === DiffElem::TYPE_ADD) {
				$k++;
			}

			if ($j - $i === $k - $j) {
				$len = $j - $i;
				for ($n = 0; $n < $len; $n++) {
					$newDiff[] = new DiffElem(
						DiffElem::TYPE_REPLACE,
						$diff[$i + $n]->old,
						$diff[$j + $n]->new,
					);
				}
			} else {
				for (; $i < $k; $i++) {
					$newDiff[] = $diff[$i];
				}
			}
			$i = $k - 1;
		}
		return $newDiff;
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser;

class ParserConfig
{

	public bool $useLinesAttributes;

	public bool $useIndexAttributes;

	public bool $useCommentsAttributes;

	/**
	 * @param array{lines?: bool, indexes?: bool, comments?: bool} $usedAttributes
	 */
	public function __construct(array $usedAttributes)
	{
		$this->useLinesAttributes = $usedAttributes['lines'] ?? false;
		$this->useIndexAttributes = $usedAttributes['indexes'] ?? false;
		$this->useCommentsAttributes = $usedAttributes['comments'] ?? false;
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\NodeVisitor;

use PHPStan\PhpDocParser\Ast\AbstractNodeVisitor;
use PHPStan\PhpDocParser\Ast\Attribute;
use PHPStan\PhpDocParser\Ast\Node;

final class CloningVisitor extends AbstractNodeVisitor
{

	public function enterNode(Node $originalNode): Node
	{
		$node = clone $originalNode;
		$node->setAttribute(Attribute::ORIGINAL_NODE, $originalNode);

		return $node;
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\Type;

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Parser\ParserException;

class InvalidTypeNode implements TypeNode
{

	use NodeAttributes;

	/** @var mixed[] */
	private array $exceptionArgs;

	public function __construct(ParserException $exception)
	{
		$this->exceptionArgs = [
			$exception->getCurrentTokenValue(),
			$exception->getCurrentTokenType(),
			$exception->getCurrentOffset(),
			$exception->getExpectedTokenType(),
			$exception->getExpectedTokenValue(),
			$exception->getCurrentTokenLine(),
		];
	}

	public function getException(): ParserException
	{
		return new ParserException(...$this->exceptionArgs);
	}

	public function __toString(): string
	{
		return '*Invalid type*';
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\Type;

use PHPStan\PhpDocParser\Ast\NodeAttributes;

class ArrayTypeNode implements TypeNode
{

	use NodeAttributes;

	public TypeNode $type;

	public function __construct(TypeNode $type)
	{
		$this->type = $type;
	}


	public function __toString(): string
	{
		if (
			$this->type instanceof CallableTypeNode
			|| $this->type instanceof ConstTypeNode
			|| $this->type instanceof NullableTypeNode
		) {
			return '(' . $this->type . ')[]';
		}

		return $this->type . '[]';
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\Type;

use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode;
use PHPStan\PhpDocParser\Ast\Node;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function sprintf;

class ArrayShapeItemNode implements Node
{

	use NodeAttributes;

	/** @var ConstExprIntegerNode|ConstExprStringNode|ConstFetchNode|IdentifierTypeNode|null */
	public $keyName;

	public bool $optional;

	public TypeNode $valueType;

	/**
	 * @param ConstExprIntegerNode|ConstExprStringNode|ConstFetchNode|IdentifierTypeNode|null $keyName
	 */
	public function __construct($keyName, bool $optional, TypeNode $valueType)
	{
		$this->keyName = $keyName;
		$this->optional = $optional;
		$this->valueType = $valueType;
	}


	public function __toString(): string
	{
		if ($this->keyName !== null) {
			return sprintf(
				'%s%s: %s',
				(string) $this->keyName,
				$this->optional ? '?' : '',
				(string) $this->valueType,
			);
		}

		return (string) $this->valueType;
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\Type;

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function sprintf;

class ConditionalTypeForParameterNode implements TypeNode
{

	use NodeAttributes;

	public string $parameterName;

	public TypeNode $targetType;

	public TypeNode $if;

	public TypeNode $else;

	public bool $negated;

	public function __construct(string $parameterName, TypeNode $targetType, TypeNode $if, TypeNode $else, bool $negated)
	{
		$this->parameterName = $parameterName;
		$this->targetType = $targetType;
		$this->if = $if;
		$this->else = $else;
		$this->negated = $negated;
	}

	public function __toString(): string
	{
		return sprintf(
			'(%s %s %s ? %s : %s)',
			$this->parameterName,
			$this->negated ? 'is not' : 'is',
			$this->targetType,
			$this->if,
			$this->else,
		);
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\Type;

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function array_map;
use function implode;

class UnionTypeNode implements TypeNode
{

	use NodeAttributes;

	/** @var TypeNode[] */
	public array $types;

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


	public function __toString(): string
	{
		return '(' . implode(' | ', array_map(static function (TypeNode $type): string {
			if ($type instanceof NullableTypeNode) {
				return '(' . $type . ')';
			}

				return (string) $type;
		}, $this->types)) . ')';
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\Type;

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
use function implode;

class CallableTypeNode implements TypeNode
{

	use NodeAttributes;

	public IdentifierTypeNode $identifier;

	/** @var TemplateTagValueNode[] */
	public array $templateTypes;

	/** @var CallableTypeParameterNode[] */
	public array $parameters;

	public TypeNode $returnType;

	/**
	 * @param CallableTypeParameterNode[] $parameters
	 * @param TemplateTagValueNode[]  $templateTypes
	 */
	public function __construct(IdentifierTypeNode $identifier, array $parameters, TypeNode $returnType, array $templateTypes)
	{
		$this->identifier = $identifier;
		$this->parameters = $parameters;
		$this->returnType = $returnType;
		$this->templateTypes = $templateTypes;
	}


	public function __toString(): string
	{
		$returnType = $this->returnType;
		if ($returnType instanceof self) {
			$returnType = "({$returnType})";
		}
		$template = $this->templateTypes !== []
			? '<' . implode(', ', $this->templateTypes) . '>'
			: '';
		$parameters = implode(', ', $this->parameters);
		return "{$this->identifier}{$template}({$parameters}): {$returnType}";
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\Type;

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function array_map;
use function implode;

class IntersectionTypeNode implements TypeNode
{

	use NodeAttributes;

	/** @var TypeNode[] */
	public array $types;

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


	public function __toString(): string
	{
		return '(' . implode(' & ', array_map(static function (TypeNode $type): string {
			if ($type instanceof NullableTypeNode) {
				return '(' . $type . ')';
			}

			return (string) $type;
		}, $this->types)) . ')';
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\Type;

use PHPStan\PhpDocParser\Ast\NodeAttributes;

class ThisTypeNode implements TypeNode
{

	use NodeAttributes;

	public function __toString(): string
	{
		return '$this';
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\Type;

use PHPStan\PhpDocParser\Ast\NodeAttributes;

class OffsetAccessTypeNode implements TypeNode
{

	use NodeAttributes;

	public TypeNode $type;

	public TypeNode $offset;

	public function __construct(TypeNode $type, TypeNode $offset)
	{
		$this->type = $type;
		$this->offset = $offset;
	}

	public function __toString(): string
	{
		if (
			$this->type instanceof CallableTypeNode
			|| $this->type instanceof NullableTypeNode
		) {
			return '(' . $this->type . ')[' . $this->offset . ']';
		}

		return $this->type . '[' . $this->offset . ']';
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\Type;

use PHPStan\PhpDocParser\Ast\NodeAttributes;

class IdentifierTypeNode implements TypeNode
{

	use NodeAttributes;

	public string $name;

	public function __construct(string $name)
	{
		$this->name = $name;
	}


	public function __toString(): string
	{
		return $this->name;
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\Type;

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function implode;

class ObjectShapeNode implements TypeNode
{

	use NodeAttributes;

	/** @var ObjectShapeItemNode[] */
	public array $items;

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

	public function __toString(): string
	{
		$items = $this->items;

		return 'object{' . implode(', ', $items) . '}';
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\Type;

use PHPStan\PhpDocParser\Ast\NodeAttributes;

class NullableTypeNode implements TypeNode
{

	use NodeAttributes;

	public TypeNode $type;

	public function __construct(TypeNode $type)
	{
		$this->type = $type;
	}


	public function __toString(): string
	{
		return '?' . $this->type;
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\Type;

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function implode;

class ArrayShapeNode implements TypeNode
{

	public const KIND_ARRAY = 'array';
	public const KIND_LIST = 'list';
	public const KIND_NON_EMPTY_ARRAY = 'non-empty-array';
	public const KIND_NON_EMPTY_LIST = 'non-empty-list';

	use NodeAttributes;

	/** @var ArrayShapeItemNode[] */
	public array $items;

	public bool $sealed;

	/** @var self::KIND_* */
	public $kind;

	public ?ArrayShapeUnsealedTypeNode $unsealedType = null;

	/**
	 * @param ArrayShapeItemNode[] $items
	 * @param self::KIND_* $kind
	 */
	private function __construct(
		array $items,
		bool $sealed = true,
		?ArrayShapeUnsealedTypeNode $unsealedType = null,
		string $kind = self::KIND_ARRAY
	)
	{
		$this->items = $items;
		$this->sealed = $sealed;
		$this->unsealedType = $unsealedType;
		$this->kind = $kind;
	}


	/**
	 * @param ArrayShapeItemNode[] $items
	 * @param self::KIND_* $kind
	 */
	public static function createSealed(array $items, string $kind = self::KIND_ARRAY): self
	{
		return new self($items, true, null, $kind);
	}

	/**
	 * @param ArrayShapeItemNode[] $items
	 * @param self::KIND_* $kind
	 */
	public static function createUnsealed(array $items, ?ArrayShapeUnsealedTypeNode $unsealedType, string $kind = self::KIND_ARRAY): self
	{
		return new self($items, false, $unsealedType, $kind);
	}


	public function __toString(): string
	{
		$items = $this->items;

		if (! $this->sealed) {
			$items[] = '...' . $this->unsealedType;
		}

		return $this->kind . '{' . implode(', ', $items) . '}';
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\Type;

use PHPStan\PhpDocParser\Ast\Node;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function trim;

class CallableTypeParameterNode implements Node
{

	use NodeAttributes;

	public TypeNode $type;

	public bool $isReference;

	public bool $isVariadic;

	/** @var string (may be empty) */
	public string $parameterName;

	public bool $isOptional;

	public function __construct(TypeNode $type, bool $isReference, bool $isVariadic, string $parameterName, bool $isOptional)
	{
		$this->type = $type;
		$this->isReference = $isReference;
		$this->isVariadic = $isVariadic;
		$this->parameterName = $parameterName;
		$this->isOptional = $isOptional;
	}


	public function __toString(): string
	{
		$type = "{$this->type} ";
		$isReference = $this->isReference ? '&' : '';
		$isVariadic = $this->isVariadic ? '...' : '';
		$isOptional = $this->isOptional ? '=' : '';
		return trim("{$type}{$isReference}{$isVariadic}{$this->parameterName}") . $isOptional;
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\Type;

use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode;
use PHPStan\PhpDocParser\Ast\Node;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function sprintf;

class ObjectShapeItemNode implements Node
{

	use NodeAttributes;

	/** @var ConstExprStringNode|IdentifierTypeNode */
	public $keyName;

	public bool $optional;

	public TypeNode $valueType;

	/**
	 * @param ConstExprStringNode|IdentifierTypeNode $keyName
	 */
	public function __construct($keyName, bool $optional, TypeNode $valueType)
	{
		$this->keyName = $keyName;
		$this->optional = $optional;
		$this->valueType = $valueType;
	}


	public function __toString(): string
	{
		if ($this->keyName !== null) {
			return sprintf(
				'%s%s: %s',
				(string) $this->keyName,
				$this->optional ? '?' : '',
				(string) $this->valueType,
			);
		}

		return (string) $this->valueType;
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\Type;

use PHPStan\PhpDocParser\Ast\Node;

interface TypeNode extends Node
{

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\Type;

use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprNode;
use PHPStan\PhpDocParser\Ast\NodeAttributes;

class ConstTypeNode implements TypeNode
{

	use NodeAttributes;

	public ConstExprNode $constExpr;

	public function __construct(ConstExprNode $constExpr)
	{
		$this->constExpr = $constExpr;
	}

	public function __toString(): string
	{
		return $this->constExpr->__toString();
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\Type;

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function implode;
use function sprintf;

class GenericTypeNode implements TypeNode
{

	public const VARIANCE_INVARIANT = 'invariant';
	public const VARIANCE_COVARIANT = 'covariant';
	public const VARIANCE_CONTRAVARIANT = 'contravariant';
	public const VARIANCE_BIVARIANT = 'bivariant';

	use NodeAttributes;

	public IdentifierTypeNode $type;

	/** @var TypeNode[] */
	public array $genericTypes;

	/** @var (self::VARIANCE_*)[] */
	public array $variances;

	/**
	 * @param TypeNode[] $genericTypes
	 * @param (self::VARIANCE_*)[] $variances
	 */
	public function __construct(IdentifierTypeNode $type, array $genericTypes, array $variances = [])
	{
		$this->type = $type;
		$this->genericTypes = $genericTypes;
		$this->variances = $variances;
	}


	public function __toString(): string
	{
		$genericTypes = [];

		foreach ($this->genericTypes as $index => $type) {
			$variance = $this->variances[$index] ?? self::VARIANCE_INVARIANT;
			if ($variance === self::VARIANCE_INVARIANT) {
				$genericTypes[] = (string) $type;
			} elseif ($variance === self::VARIANCE_BIVARIANT) {
				$genericTypes[] = '*';
			} else {
				$genericTypes[] = sprintf('%s %s', $variance, $type);
			}
		}

		return $this->type . '<' . implode(', ', $genericTypes) . '>';
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\Type;

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function sprintf;

class ConditionalTypeNode implements TypeNode
{

	use NodeAttributes;

	public TypeNode $subjectType;

	public TypeNode $targetType;

	public TypeNode $if;

	public TypeNode $else;

	public bool $negated;

	public function __construct(TypeNode $subjectType, TypeNode $targetType, TypeNode $if, TypeNode $else, bool $negated)
	{
		$this->subjectType = $subjectType;
		$this->targetType = $targetType;
		$this->if = $if;
		$this->else = $else;
		$this->negated = $negated;
	}

	public function __toString(): string
	{
		return sprintf(
			'(%s %s %s ? %s : %s)',
			$this->subjectType,
			$this->negated ? 'is not' : 'is',
			$this->targetType,
			$this->if,
			$this->else,
		);
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\Type;

use PHPStan\PhpDocParser\Ast\Node;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function sprintf;

class ArrayShapeUnsealedTypeNode implements Node
{

	use NodeAttributes;

	public TypeNode $valueType;

	public ?TypeNode $keyType = null;

	public function __construct(TypeNode $valueType, ?TypeNode $keyType)
	{
		$this->valueType = $valueType;
		$this->keyType = $keyType;
	}

	public function __toString(): string
	{
		if ($this->keyType !== null) {
			return sprintf('<%s, %s>', $this->keyType, $this->valueType);
		}
		return sprintf('<%s>', $this->valueType);
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast;

/**
 * Inspired by https://github.com/nikic/PHP-Parser/tree/36a6dcd04e7b0285e8f0868f44bd4927802f7df1
 *
 * Copyright (c) 2011, Nikita Popov
 * All rights reserved.
 */
interface NodeVisitor
{

	/**
	 * Called once before traversal.
	 *
	 * Return value semantics:
	 *  * null:      $nodes stays as-is
	 *  * otherwise: $nodes is set to the return value
	 *
	 * @param Node[] $nodes Array of nodes
	 *
	 * @return Node[]|null Array of nodes
	 */
	public function beforeTraverse(array $nodes): ?array;

	/**
	 * Called when entering a node.
	 *
	 * Return value semantics:
	 *  * null
	 *        => $node stays as-is
	 *  * array (of Nodes)
	 *        => The return value is merged into the parent array (at the position of the $node)
	 *  * NodeTraverser::REMOVE_NODE
	 *        => $node is removed from the parent array
	 *  * NodeTraverser::DONT_TRAVERSE_CHILDREN
	 *        => Children of $node are not traversed. $node stays as-is
	 *  * NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN
	 *        => Further visitors for the current node are skipped, and its children are not
	 *           traversed. $node stays as-is.
	 *  * NodeTraverser::STOP_TRAVERSAL
	 *        => Traversal is aborted. $node stays as-is
	 *  * otherwise
	 *        => $node is set to the return value
	 *
	 * @param Node $node Node
	 *
	 * @return Node|Node[]|NodeTraverser::*|null Replacement node (or special return value)
	 */
	public function enterNode(Node $node);

	/**
	 * Called when leaving a node.
	 *
	 * Return value semantics:
	 *  * null
	 *        => $node stays as-is
	 *  * NodeTraverser::REMOVE_NODE
	 *        => $node is removed from the parent array
	 *  * NodeTraverser::STOP_TRAVERSAL
	 *        => Traversal is aborted. $node stays as-is
	 *  * array (of Nodes)
	 *        => The return value is merged into the parent array (at the position of the $node)
	 *  * otherwise
	 *        => $node is set to the return value
	 *
	 * @param Node $node Node
	 *
	 * @return Node|Node[]|NodeTraverser::REMOVE_NODE|NodeTraverser::STOP_TRAVERSAL|null Replacement node (or special return value)
	 */
	public function leaveNode(Node $node);

	/**
	 * Called once after traversal.
	 *
	 * Return value semantics:
	 *  * null:      $nodes stays as-is
	 *  * otherwise: $nodes is set to the return value
	 *
	 * @param Node[] $nodes Array of nodes
	 *
	 * @return Node[]|null Array of nodes
	 */
	public function afterTraverse(array $nodes): ?array;

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast;

final class Attribute
{

	public const START_LINE = 'startLine';
	public const END_LINE = 'endLine';

	public const START_INDEX = 'startIndex';
	public const END_INDEX = 'endIndex';

	public const ORIGINAL_NODE = 'originalNode';

	public const COMMENTS = 'comments';

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\ConstExpr;

use PHPStan\PhpDocParser\Ast\NodeAttributes;

class ConstExprNullNode implements ConstExprNode
{

	use NodeAttributes;

	public function __toString(): string
	{
		return 'null';
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\ConstExpr;

use PHPStan\PhpDocParser\Ast\Node;

interface ConstExprNode extends Node
{

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\ConstExpr;

use PHPStan\PhpDocParser\Ast\NodeAttributes;

class ConstExprFloatNode implements ConstExprNode
{

	use NodeAttributes;

	public string $value;

	public function __construct(string $value)
	{
		$this->value = $value;
	}


	public function __toString(): string
	{
		return $this->value;
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\ConstExpr;

use PHPStan\PhpDocParser\Ast\NodeAttributes;

class ConstExprTrueNode implements ConstExprNode
{

	use NodeAttributes;

	public function __toString(): string
	{
		return 'true';
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\ConstExpr;

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function sprintf;

class ConstExprArrayItemNode implements ConstExprNode
{

	use NodeAttributes;

	public ?ConstExprNode $key = null;

	public ConstExprNode $value;

	public function __construct(?ConstExprNode $key, ConstExprNode $value)
	{
		$this->key = $key;
		$this->value = $value;
	}


	public function __toString(): string
	{
		if ($this->key !== null) {
			return sprintf('%s => %s', $this->key, $this->value);

		}

		return (string) $this->value;
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\ConstExpr;

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function implode;

class ConstExprArrayNode implements ConstExprNode
{

	use NodeAttributes;

	/** @var ConstExprArrayItemNode[] */
	public array $items;

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


	public function __toString(): string
	{
		return '[' . implode(', ', $this->items) . ']';
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\ConstExpr;

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function sprintf;
use function str_replace;
use function strlen;
use function substr;

class DoctrineConstExprStringNode extends ConstExprStringNode
{

	use NodeAttributes;

	public string $value;

	public function __construct(string $value)
	{
		parent::__construct($value, self::DOUBLE_QUOTED);
		$this->value = $value;
	}

	public function __toString(): string
	{
		return self::escape($this->value);
	}

	public static function unescape(string $value): string
	{
		// from https://github.com/doctrine/annotations/blob/a9ec7af212302a75d1f92fa65d3abfbd16245a2a/lib/Doctrine/Common/Annotations/DocLexer.php#L103-L107
		return str_replace('""', '"', substr($value, 1, strlen($value) - 2));
	}

	private static function escape(string $value): string
	{
		// from https://github.com/phpstan/phpdoc-parser/issues/205#issuecomment-1662323656
		return sprintf('"%s"', str_replace('"', '""', $value));
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\ConstExpr;

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function addcslashes;
use function assert;
use function dechex;
use function ord;
use function preg_replace_callback;
use function sprintf;
use function str_pad;
use function strlen;
use const STR_PAD_LEFT;

class ConstExprStringNode implements ConstExprNode
{

	public const SINGLE_QUOTED = 1;
	public const DOUBLE_QUOTED = 2;

	use NodeAttributes;

	public string $value;

	/** @var self::SINGLE_QUOTED|self::DOUBLE_QUOTED */
	public $quoteType;

	/**
	 * @param self::SINGLE_QUOTED|self::DOUBLE_QUOTED $quoteType
	 */
	public function __construct(string $value, int $quoteType)
	{
		$this->value = $value;
		$this->quoteType = $quoteType;
	}


	public function __toString(): string
	{
		if ($this->quoteType === self::SINGLE_QUOTED) {
			// from https://github.com/nikic/PHP-Parser/blob/0ffddce52d816f72d0efc4d9b02e276d3309ef01/lib/PhpParser/PrettyPrinter/Standard.php#L1007
			return sprintf("'%s'", addcslashes($this->value, '\'\\'));
		}

		// from https://github.com/nikic/PHP-Parser/blob/0ffddce52d816f72d0efc4d9b02e276d3309ef01/lib/PhpParser/PrettyPrinter/Standard.php#L1010-L1040
		return sprintf('"%s"', $this->escapeDoubleQuotedString());
	}

	private function escapeDoubleQuotedString(): string
	{
		$quote = '"';
		$escaped = addcslashes($this->value, "\n\r\t\f\v$" . $quote . '\\');

		// Escape control characters and non-UTF-8 characters.
		// Regex based on https://stackoverflow.com/a/11709412/385378.
		$regex = '/(
              [\x00-\x08\x0E-\x1F] # Control characters
            | [\xC0-\xC1] # Invalid UTF-8 Bytes
            | [\xF5-\xFF] # Invalid UTF-8 Bytes
            | \xE0(?=[\x80-\x9F]) # Overlong encoding of prior code point
            | \xF0(?=[\x80-\x8F]) # Overlong encoding of prior code point
            | [\xC2-\xDF](?![\x80-\xBF]) # Invalid UTF-8 Sequence Start
            | [\xE0-\xEF](?![\x80-\xBF]{2}) # Invalid UTF-8 Sequence Start
            | [\xF0-\xF4](?![\x80-\xBF]{3}) # Invalid UTF-8 Sequence Start
            | (?<=[\x00-\x7F\xF5-\xFF])[\x80-\xBF] # Invalid UTF-8 Sequence Middle
            | (?<![\xC2-\xDF]|[\xE0-\xEF]|[\xE0-\xEF][\x80-\xBF]|[\xF0-\xF4]|[\xF0-\xF4][\x80-\xBF]|[\xF0-\xF4][\x80-\xBF]{2})[\x80-\xBF] # Overlong Sequence
            | (?<=[\xE0-\xEF])[\x80-\xBF](?![\x80-\xBF]) # Short 3 byte sequence
            | (?<=[\xF0-\xF4])[\x80-\xBF](?![\x80-\xBF]{2}) # Short 4 byte sequence
            | (?<=[\xF0-\xF4][\x80-\xBF])[\x80-\xBF](?![\x80-\xBF]) # Short 4 byte sequence (2)
        )/x';
		return preg_replace_callback($regex, static function ($matches) {
			assert(strlen($matches[0]) === 1);
			$hex = dechex(ord($matches[0]));

			return '\\x' . str_pad($hex, 2, '0', STR_PAD_LEFT);
		}, $escaped);
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\ConstExpr;

use PHPStan\PhpDocParser\Ast\NodeAttributes;

class ConstFetchNode implements ConstExprNode
{

	use NodeAttributes;

	/** @var string class name for class constants or empty string for non-class constants */
	public string $className;

	public string $name;

	public function __construct(string $className, string $name)
	{
		$this->className = $className;
		$this->name = $name;
	}


	public function __toString(): string
	{
		if ($this->className === '') {
			return $this->name;

		}

		return "{$this->className}::{$this->name}";
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\ConstExpr;

use PHPStan\PhpDocParser\Ast\NodeAttributes;

class ConstExprIntegerNode implements ConstExprNode
{

	use NodeAttributes;

	public string $value;

	public function __construct(string $value)
	{
		$this->value = $value;
	}


	public function __toString(): string
	{
		return $this->value;
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\ConstExpr;

use PHPStan\PhpDocParser\Ast\NodeAttributes;

class ConstExprFalseNode implements ConstExprNode
{

	use NodeAttributes;

	public function __toString(): string
	{
		return 'false';
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast;

/**
 * Inspired by https://github.com/nikic/PHP-Parser/tree/36a6dcd04e7b0285e8f0868f44bd4927802f7df1
 *
 * Copyright (c) 2011, Nikita Popov
 * All rights reserved.
 */
abstract class AbstractNodeVisitor implements NodeVisitor // phpcs:ignore SlevomatCodingStandard.Classes.SuperfluousAbstractClassNaming.SuperfluousPrefix
{

	public function beforeTraverse(array $nodes): ?array
	{
		return null;
	}

	public function enterNode(Node $node)
	{
		return null;
	}

	public function leaveNode(Node $node)
	{
		return null;
	}

	public function afterTraverse(array $nodes): ?array
	{
		return null;
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast;

use function array_key_exists;

trait NodeAttributes
{

	/** @var array<string, mixed> */
	private array $attributes = [];

	/**
	 * @param mixed $value
	 */
	public function setAttribute(string $key, $value): void
	{
		if ($value === null) {
			unset($this->attributes[$key]);
			return;
		}
		$this->attributes[$key] = $value;
	}

	public function hasAttribute(string $key): bool
	{
		return array_key_exists($key, $this->attributes);
	}

	/**
	 * @return mixed
	 */
	public function getAttribute(string $key)
	{
		if ($this->hasAttribute($key)) {
			return $this->attributes[$key];
		}

		return null;
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast;

use function trim;

class Comment
{

	public string $text;

	public int $startLine;

	public int $startIndex;

	public function __construct(string $text, int $startLine = -1, int $startIndex = -1)
	{
		$this->text = $text;
		$this->startLine = $startLine;
		$this->startIndex = $startIndex;
	}

	public function getReformattedText(): string
	{
		return trim($this->text);
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast;

interface Node
{

	public function __toString(): string;

	/**
	 * @param mixed $value
	 */
	public function setAttribute(string $key, $value): void;

	public function hasAttribute(string $key): bool;

	/**
	 * @return mixed
	 */
	public function getAttribute(string $key);

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\PhpDoc;

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Parser\ParserException;
use function sprintf;
use function trigger_error;
use const E_USER_WARNING;

/**
 * @property ParserException $exception
 */
class InvalidTagValueNode implements PhpDocTagValueNode
{

	use NodeAttributes;

	/** @var string (may be empty) */
	public string $value;

	/** @var mixed[] */
	private array $exceptionArgs;

	public function __construct(string $value, ParserException $exception)
	{
		$this->value = $value;
		$this->exceptionArgs = [
			$exception->getCurrentTokenValue(),
			$exception->getCurrentTokenType(),
			$exception->getCurrentOffset(),
			$exception->getExpectedTokenType(),
			$exception->getExpectedTokenValue(),
			$exception->getCurrentTokenLine(),
		];
	}

	public function __get(string $name): ?ParserException
	{
		if ($name !== 'exception') {
			trigger_error(sprintf('Undefined property: %s::$%s', self::class, $name), E_USER_WARNING);
			return null;
		}

		return new ParserException(...$this->exceptionArgs);
	}

	public function __toString(): string
	{
		return $this->value;
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\PhpDoc;

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;

class AssertTagMethodValueNode implements PhpDocTagValueNode
{

	use NodeAttributes;

	public TypeNode $type;

	public string $parameter;

	public string $method;

	public bool $isNegated;

	public bool $isEquality;

	/** @var string (may be empty) */
	public string $description;

	public function __construct(TypeNode $type, string $parameter, string $method, bool $isNegated, string $description, bool $isEquality)
	{
		$this->type = $type;
		$this->parameter = $parameter;
		$this->method = $method;
		$this->isNegated = $isNegated;
		$this->isEquality = $isEquality;
		$this->description = $description;
	}


	public function __toString(): string
	{
		$isNegated = $this->isNegated ? '!' : '';
		$isEquality = $this->isEquality ? '=' : '';
		return trim("{$isNegated}{$isEquality}{$this->type} {$this->parameter}->{$this->method}() {$this->description}");
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\PhpDoc;

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function trim;

class TypelessParamTagValueNode implements PhpDocTagValueNode
{

	use NodeAttributes;

	public bool $isReference;

	public bool $isVariadic;

	public string $parameterName;

	/** @var string (may be empty) */
	public string $description;

	public function __construct(bool $isVariadic, string $parameterName, string $description, bool $isReference)
	{
		$this->isReference = $isReference;
		$this->isVariadic = $isVariadic;
		$this->parameterName = $parameterName;
		$this->description = $description;
	}


	public function __toString(): string
	{
		$reference = $this->isReference ? '&' : '';
		$variadic = $this->isVariadic ? '...' : '';
		return trim("{$reference}{$variadic}{$this->parameterName} {$this->description}");
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\PhpDoc;

use PHPStan\PhpDocParser\Ast\NodeAttributes;

class PhpDocTextNode implements PhpDocChildNode
{

	use NodeAttributes;

	public string $text;

	public function __construct(string $text)
	{
		$this->text = $text;
	}


	public function __toString(): string
	{
		return $this->text;
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\PhpDoc;

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;

class ParamClosureThisTagValueNode implements PhpDocTagValueNode
{

	use NodeAttributes;

	public TypeNode $type;

	public string $parameterName;

	/** @var string (may be empty) */
	public string $description;

	public function __construct(TypeNode $type, string $parameterName, string $description)
	{
		$this->type = $type;
		$this->parameterName = $parameterName;
		$this->description = $description;
	}

	public function __toString(): string
	{
		return trim("{$this->type} {$this->parameterName} {$this->description}");
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\PhpDoc;

use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprNode;
use PHPStan\PhpDocParser\Ast\Node;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;

class MethodTagValueParameterNode implements Node
{

	use NodeAttributes;

	public ?TypeNode $type = null;

	public bool $isReference;

	public bool $isVariadic;

	public string $parameterName;

	public ?ConstExprNode $defaultValue = null;

	public function __construct(?TypeNode $type, bool $isReference, bool $isVariadic, string $parameterName, ?ConstExprNode $defaultValue)
	{
		$this->type = $type;
		$this->isReference = $isReference;
		$this->isVariadic = $isVariadic;
		$this->parameterName = $parameterName;
		$this->defaultValue = $defaultValue;
	}


	public function __toString(): string
	{
		$type = $this->type !== null ? "{$this->type} " : '';
		$isReference = $this->isReference ? '&' : '';
		$isVariadic = $this->isVariadic ? '...' : '';
		$default = $this->defaultValue !== null ? " = {$this->defaultValue}" : '';
		return "{$type}{$isReference}{$isVariadic}{$this->parameterName}{$default}";
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\PhpDoc;

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;

class AssertTagValueNode implements PhpDocTagValueNode
{

	use NodeAttributes;

	public TypeNode $type;

	public string $parameter;

	public bool $isNegated;

	public bool $isEquality;

	/** @var string (may be empty) */
	public string $description;

	public function __construct(TypeNode $type, string $parameter, bool $isNegated, string $description, bool $isEquality)
	{
		$this->type = $type;
		$this->parameter = $parameter;
		$this->isNegated = $isNegated;
		$this->isEquality = $isEquality;
		$this->description = $description;
	}


	public function __toString(): string
	{
		$isNegated = $this->isNegated ? '!' : '';
		$isEquality = $this->isEquality ? '=' : '';
		return trim("{$isNegated}{$isEquality}{$this->type} {$this->parameter} {$this->description}");
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\PhpDoc;

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;

class TypeAliasTagValueNode implements PhpDocTagValueNode
{

	use NodeAttributes;

	public string $alias;

	public TypeNode $type;

	public function __construct(string $alias, TypeNode $type)
	{
		$this->alias = $alias;
		$this->type = $type;
	}


	public function __toString(): string
	{
		return trim("{$this->alias} {$this->type}");
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\PhpDoc;

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
use function trim;

class ExtendsTagValueNode implements PhpDocTagValueNode
{

	use NodeAttributes;

	public GenericTypeNode $type;

	/** @var string (may be empty) */
	public string $description;

	public function __construct(GenericTypeNode $type, string $description)
	{
		$this->type = $type;
		$this->description = $description;
	}


	public function __toString(): string
	{
		return trim("{$this->type} {$this->description}");
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\PhpDoc;

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;

class PropertyTagValueNode implements PhpDocTagValueNode
{

	use NodeAttributes;

	public TypeNode $type;

	public string $propertyName;

	/** @var string (may be empty) */
	public string $description;

	public function __construct(TypeNode $type, string $propertyName, string $description)
	{
		$this->type = $type;
		$this->propertyName = $propertyName;
		$this->description = $description;
	}


	public function __toString(): string
	{
		return trim("{$this->type} {$this->propertyName} {$this->description}");
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\PhpDoc;

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;

class VarTagValueNode implements PhpDocTagValueNode
{

	use NodeAttributes;

	public TypeNode $type;

	/** @var string (may be empty) */
	public string $variableName;

	/** @var string (may be empty) */
	public string $description;

	public function __construct(TypeNode $type, string $variableName, string $description)
	{
		$this->type = $type;
		$this->variableName = $variableName;
		$this->description = $description;
	}


	public function __toString(): string
	{
		return trim("$this->type " . trim("{$this->variableName} {$this->description}"));
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\PhpDoc;

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine\DoctrineTagValueNode;
use function trim;

class PhpDocTagNode implements PhpDocChildNode
{

	use NodeAttributes;

	public string $name;

	public PhpDocTagValueNode $value;

	public function __construct(string $name, PhpDocTagValueNode $value)
	{
		$this->name = $name;
		$this->value = $value;
	}


	public function __toString(): string
	{
		if ($this->value instanceof DoctrineTagValueNode) {
			return (string) $this->value;
		}

		return trim("{$this->name} {$this->value}");
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\PhpDoc;

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function trim;

class ParamLaterInvokedCallableTagValueNode implements PhpDocTagValueNode
{

	use NodeAttributes;

	public string $parameterName;

	/** @var string (may be empty) */
	public string $description;

	public function __construct(string $parameterName, string $description)
	{
		$this->parameterName = $parameterName;
		$this->description = $description;
	}

	public function __toString(): string
	{
		return trim("{$this->parameterName} {$this->description}");
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\PhpDoc;

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function trim;

class DeprecatedTagValueNode implements PhpDocTagValueNode
{

	use NodeAttributes;

	/** @var string (may be empty) */
	public string $description;

	public function __construct(string $description)
	{
		$this->description = $description;
	}


	public function __toString(): string
	{
		return trim($this->description);
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\PhpDoc;

use PHPStan\PhpDocParser\Ast\Node;

interface PhpDocTagValueNode extends Node
{

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\PhpDoc;

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
use function trim;

class ImplementsTagValueNode implements PhpDocTagValueNode
{

	use NodeAttributes;

	public GenericTypeNode $type;

	/** @var string (may be empty) */
	public string $description;

	public function __construct(GenericTypeNode $type, string $description)
	{
		$this->type = $type;
		$this->description = $description;
	}


	public function __toString(): string
	{
		return trim("{$this->type} {$this->description}");
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\PhpDoc;

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;

class SealedTagValueNode implements PhpDocTagValueNode
{

	use NodeAttributes;

	public TypeNode $type;

	/** @var string (may be empty) */
	public string $description;

	public function __construct(TypeNode $type, string $description)
	{
		$this->type = $type;
		$this->description = $description;
	}


	public function __toString(): string
	{
		return trim("{$this->type} {$this->description}");
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\PhpDoc;

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;

class RequireImplementsTagValueNode implements PhpDocTagValueNode
{

	use NodeAttributes;

	public TypeNode $type;

	/** @var string (may be empty) */
	public string $description;

	public function __construct(TypeNode $type, string $description)
	{
		$this->type = $type;
		$this->description = $description;
	}


	public function __toString(): string
	{
		return trim("{$this->type} {$this->description}");
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\PhpDoc;

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function count;
use function implode;

class MethodTagValueNode implements PhpDocTagValueNode
{

	use NodeAttributes;

	public bool $isStatic;

	public ?TypeNode $returnType = null;

	public string $methodName;

	/** @var TemplateTagValueNode[] */
	public array $templateTypes;

	/** @var MethodTagValueParameterNode[] */
	public array $parameters;

	/** @var string (may be empty) */
	public string $description;

	/**
	 * @param MethodTagValueParameterNode[] $parameters
	 * @param TemplateTagValueNode[] $templateTypes
	 */
	public function __construct(bool $isStatic, ?TypeNode $returnType, string $methodName, array $parameters, string $description, array $templateTypes)
	{
		$this->isStatic = $isStatic;
		$this->returnType = $returnType;
		$this->methodName = $methodName;
		$this->parameters = $parameters;
		$this->description = $description;
		$this->templateTypes = $templateTypes;
	}


	public function __toString(): string
	{
		$static = $this->isStatic ? 'static ' : '';
		$returnType = $this->returnType !== null ? "{$this->returnType} " : '';
		$parameters = implode(', ', $this->parameters);
		$description = $this->description !== '' ? " {$this->description}" : '';
		$templateTypes = count($this->templateTypes) > 0 ? '<' . implode(', ', $this->templateTypes) . '>' : '';
		return "{$static}{$returnType}{$this->methodName}{$templateTypes}({$parameters}){$description}";
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\PhpDoc;

use PHPStan\PhpDocParser\Ast\Node;

interface PhpDocChildNode extends Node
{

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\PhpDoc;

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;

class MixinTagValueNode implements PhpDocTagValueNode
{

	use NodeAttributes;

	public TypeNode $type;

	/** @var string (may be empty) */
	public string $description;

	public function __construct(TypeNode $type, string $description)
	{
		$this->type = $type;
		$this->description = $description;
	}


	public function __toString(): string
	{
		return trim("{$this->type} {$this->description}");
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\PhpDoc;

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;

class RequireExtendsTagValueNode implements PhpDocTagValueNode
{

	use NodeAttributes;

	public TypeNode $type;

	/** @var string (may be empty) */
	public string $description;

	public function __construct(TypeNode $type, string $description)
	{
		$this->type = $type;
		$this->description = $description;
	}


	public function __toString(): string
	{
		return trim("{$this->type} {$this->description}");
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine;

use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprNode;
use PHPStan\PhpDocParser\Ast\Node;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;

/**
 * @phpstan-type ValueType = DoctrineAnnotation|IdentifierTypeNode|DoctrineArray|ConstExprNode
 */
class DoctrineArgument implements Node
{

	use NodeAttributes;

	public ?IdentifierTypeNode $key = null;

	/** @var ValueType */
	public $value;

	/**
	 * @param ValueType $value
	 */
	public function __construct(?IdentifierTypeNode $key, $value)
	{
		$this->key = $key;
		$this->value = $value;
	}


	public function __toString(): string
	{
		if ($this->key === null) {
			return (string) $this->value;
		}

		return $this->key . '=' . $this->value;
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine;

use PHPStan\PhpDocParser\Ast\Node;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function implode;

class DoctrineAnnotation implements Node
{

	use NodeAttributes;

	public string $name;

	/** @var list<DoctrineArgument> */
	public array $arguments;

	/**
	 * @param list<DoctrineArgument> $arguments
	 */
	public function __construct(string $name, array $arguments)
	{
		$this->name = $name;
		$this->arguments = $arguments;
	}

	public function __toString(): string
	{
		$arguments = implode(', ', $this->arguments);
		return $this->name . '(' . $arguments . ')';
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine;

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagValueNode;
use function trim;

class DoctrineTagValueNode implements PhpDocTagValueNode
{

	use NodeAttributes;

	public DoctrineAnnotation $annotation;

	/** @var string (may be empty) */
	public string $description;


	public function __construct(
		DoctrineAnnotation $annotation,
		string $description
	)
	{
		$this->annotation = $annotation;
		$this->description = $description;
	}


	public function __toString(): string
	{
		return trim("{$this->annotation} {$this->description}");
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine;

use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode;
use PHPStan\PhpDocParser\Ast\Node;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;

/**
 * @phpstan-import-type ValueType from DoctrineArgument
 * @phpstan-type KeyType = ConstExprIntegerNode|ConstExprStringNode|IdentifierTypeNode|ConstFetchNode|null
 */
class DoctrineArrayItem implements Node
{

	use NodeAttributes;

	/** @var KeyType */
	public $key;

	/** @var ValueType */
	public $value;

	/**
	 * @param KeyType $key
	 * @param ValueType $value
	 */
	public function __construct($key, $value)
	{
		$this->key = $key;
		$this->value = $value;
	}


	public function __toString(): string
	{
		if ($this->key === null) {
			return (string) $this->value;
		}

		return $this->key . '=' . $this->value;
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine;

use PHPStan\PhpDocParser\Ast\Node;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function implode;

class DoctrineArray implements Node
{

	use NodeAttributes;

	/** @var list<DoctrineArrayItem> */
	public array $items;

	/**
	 * @param list<DoctrineArrayItem> $items
	 */
	public function __construct(array $items)
	{
		$this->items = $items;
	}

	public function __toString(): string
	{
		$items = implode(', ', $this->items);

		return '{' . $items . '}';
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\PhpDoc;

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use function trim;

class TypeAliasImportTagValueNode implements PhpDocTagValueNode
{

	use NodeAttributes;

	public string $importedAlias;

	public IdentifierTypeNode $importedFrom;

	public ?string $importedAs = null;

	public function __construct(string $importedAlias, IdentifierTypeNode $importedFrom, ?string $importedAs)
	{
		$this->importedAlias = $importedAlias;
		$this->importedFrom = $importedFrom;
		$this->importedAs = $importedAs;
	}

	public function __toString(): string
	{
		return trim(
			"{$this->importedAlias} from {$this->importedFrom}"
			. ($this->importedAs !== null ? " as {$this->importedAs}" : ''),
		);
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\PhpDoc;

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;

class ThrowsTagValueNode implements PhpDocTagValueNode
{

	use NodeAttributes;

	public TypeNode $type;

	/** @var string (may be empty) */
	public string $description;

	public function __construct(TypeNode $type, string $description)
	{
		$this->type = $type;
		$this->description = $description;
	}


	public function __toString(): string
	{
		return trim("{$this->type} {$this->description}");
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\PhpDoc;

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;

class AssertTagPropertyValueNode implements PhpDocTagValueNode
{

	use NodeAttributes;

	public TypeNode $type;

	public string $parameter;

	public string $property;

	public bool $isNegated;

	public bool $isEquality;

	/** @var string (may be empty) */
	public string $description;

	public function __construct(TypeNode $type, string $parameter, string $property, bool $isNegated, string $description, bool $isEquality)
	{
		$this->type = $type;
		$this->parameter = $parameter;
		$this->property = $property;
		$this->isNegated = $isNegated;
		$this->isEquality = $isEquality;
		$this->description = $description;
	}


	public function __toString(): string
	{
		$isNegated = $this->isNegated ? '!' : '';
		$isEquality = $this->isEquality ? '=' : '';
		return trim("{$isNegated}{$isEquality}{$this->type} {$this->parameter}->{$this->property} {$this->description}");
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\PhpDoc;

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function trim;

class ParamImmediatelyInvokedCallableTagValueNode implements PhpDocTagValueNode
{

	use NodeAttributes;

	public string $parameterName;

	/** @var string (may be empty) */
	public string $description;

	public function __construct(string $parameterName, string $description)
	{
		$this->parameterName = $parameterName;
		$this->description = $description;
	}

	public function __toString(): string
	{
		return trim("{$this->parameterName} {$this->description}");
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\PhpDoc;

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;

class ReturnTagValueNode implements PhpDocTagValueNode
{

	use NodeAttributes;

	public TypeNode $type;

	/** @var string (may be empty) */
	public string $description;

	public function __construct(TypeNode $type, string $description)
	{
		$this->type = $type;
		$this->description = $description;
	}


	public function __toString(): string
	{
		return trim("{$this->type} {$this->description}");
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\PhpDoc;

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;

class ParamTagValueNode implements PhpDocTagValueNode
{

	use NodeAttributes;

	public TypeNode $type;

	public bool $isReference;

	public bool $isVariadic;

	public string $parameterName;

	/** @var string (may be empty) */
	public string $description;

	public function __construct(TypeNode $type, bool $isVariadic, string $parameterName, string $description, bool $isReference)
	{
		$this->type = $type;
		$this->isReference = $isReference;
		$this->isVariadic = $isVariadic;
		$this->parameterName = $parameterName;
		$this->description = $description;
	}


	public function __toString(): string
	{
		$reference = $this->isReference ? '&' : '';
		$variadic = $this->isVariadic ? '...' : '';
		return trim("{$this->type} {$reference}{$variadic}{$this->parameterName} {$this->description}");
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\PhpDoc;

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;

class SelfOutTagValueNode implements PhpDocTagValueNode
{

	use NodeAttributes;

	public TypeNode $type;

	/** @var string (may be empty) */
	public string $description;

	public function __construct(TypeNode $type, string $description)
	{
		$this->type = $type;
		$this->description = $description;
	}


	public function __toString(): string
	{
		return trim($this->type . ' ' . $this->description);
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\PhpDoc;

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;

class ParamOutTagValueNode implements PhpDocTagValueNode
{

	use NodeAttributes;

	public TypeNode $type;

	public string $parameterName;

	/** @var string (may be empty) */
	public string $description;

	public function __construct(TypeNode $type, string $parameterName, string $description)
	{
		$this->type = $type;
		$this->parameterName = $parameterName;
		$this->description = $description;
	}

	public function __toString(): string
	{
		return trim("{$this->type} {$this->parameterName} {$this->description}");
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\PhpDoc;

use PHPStan\PhpDocParser\Ast\Node;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function array_column;
use function array_filter;
use function array_map;
use function implode;

class PhpDocNode implements Node
{

	use NodeAttributes;

	/** @var PhpDocChildNode[] */
	public array $children;

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


	/**
	 * @return PhpDocTagNode[]
	 */
	public function getTags(): array
	{
		return array_filter($this->children, static fn (PhpDocChildNode $child): bool => $child instanceof PhpDocTagNode);
	}


	/**
	 * @return PhpDocTagNode[]
	 */
	public function getTagsByName(string $tagName): array
	{
		return array_filter($this->getTags(), static fn (PhpDocTagNode $tag): bool => $tag->name === $tagName);
	}


	/**
	 * @return VarTagValueNode[]
	 */
	public function getVarTagValues(string $tagName = '@var'): array
	{
		return array_filter(
			array_column($this->getTagsByName($tagName), 'value'),
			static fn (PhpDocTagValueNode $value): bool => $value instanceof VarTagValueNode,
		);
	}


	/**
	 * @return ParamTagValueNode[]
	 */
	public function getParamTagValues(string $tagName = '@param'): array
	{
		return array_filter(
			array_column($this->getTagsByName($tagName), 'value'),
			static fn (PhpDocTagValueNode $value): bool => $value instanceof ParamTagValueNode,
		);
	}


	/**
	 * @return TypelessParamTagValueNode[]
	 */
	public function getTypelessParamTagValues(string $tagName = '@param'): array
	{
		return array_filter(
			array_column($this->getTagsByName($tagName), 'value'),
			static fn (PhpDocTagValueNode $value): bool => $value instanceof TypelessParamTagValueNode,
		);
	}


	/**
	 * @return ParamImmediatelyInvokedCallableTagValueNode[]
	 */
	public function getParamImmediatelyInvokedCallableTagValues(string $tagName = '@param-immediately-invoked-callable'): array
	{
		return array_filter(
			array_column($this->getTagsByName($tagName), 'value'),
			static fn (PhpDocTagValueNode $value): bool => $value instanceof ParamImmediatelyInvokedCallableTagValueNode,
		);
	}


	/**
	 * @return ParamLaterInvokedCallableTagValueNode[]
	 */
	public function getParamLaterInvokedCallableTagValues(string $tagName = '@param-later-invoked-callable'): array
	{
		return array_filter(
			array_column($this->getTagsByName($tagName), 'value'),
			static fn (PhpDocTagValueNode $value): bool => $value instanceof ParamLaterInvokedCallableTagValueNode,
		);
	}


	/**
	 * @return ParamClosureThisTagValueNode[]
	 */
	public function getParamClosureThisTagValues(string $tagName = '@param-closure-this'): array
	{
		return array_filter(
			array_column($this->getTagsByName($tagName), 'value'),
			static fn (PhpDocTagValueNode $value): bool => $value instanceof ParamClosureThisTagValueNode,
		);
	}

	/**
	 * @return PureUnlessCallableIsImpureTagValueNode[]
	 */
	public function getPureUnlessCallableIsImpureTagValues(string $tagName = '@pure-unless-callable-is-impure'): array
	{
		return array_filter(
			array_column($this->getTagsByName($tagName), 'value'),
			static fn (PhpDocTagValueNode $value): bool => $value instanceof PureUnlessCallableIsImpureTagValueNode,
		);
	}

	/**
	 * @return TemplateTagValueNode[]
	 */
	public function getTemplateTagValues(string $tagName = '@template'): array
	{
		return array_filter(
			array_column($this->getTagsByName($tagName), 'value'),
			static fn (PhpDocTagValueNode $value): bool => $value instanceof TemplateTagValueNode,
		);
	}


	/**
	 * @return ExtendsTagValueNode[]
	 */
	public function getExtendsTagValues(string $tagName = '@extends'): array
	{
		return array_filter(
			array_column($this->getTagsByName($tagName), 'value'),
			static fn (PhpDocTagValueNode $value): bool => $value instanceof ExtendsTagValueNode,
		);
	}


	/**
	 * @return ImplementsTagValueNode[]
	 */
	public function getImplementsTagValues(string $tagName = '@implements'): array
	{
		return array_filter(
			array_column($this->getTagsByName($tagName), 'value'),
			static fn (PhpDocTagValueNode $value): bool => $value instanceof ImplementsTagValueNode,
		);
	}


	/**
	 * @return UsesTagValueNode[]
	 */
	public function getUsesTagValues(string $tagName = '@use'): array
	{
		return array_filter(
			array_column($this->getTagsByName($tagName), 'value'),
			static fn (PhpDocTagValueNode $value): bool => $value instanceof UsesTagValueNode,
		);
	}


	/**
	 * @return ReturnTagValueNode[]
	 */
	public function getReturnTagValues(string $tagName = '@return'): array
	{
		return array_filter(
			array_column($this->getTagsByName($tagName), 'value'),
			static fn (PhpDocTagValueNode $value): bool => $value instanceof ReturnTagValueNode,
		);
	}


	/**
	 * @return ThrowsTagValueNode[]
	 */
	public function getThrowsTagValues(string $tagName = '@throws'): array
	{
		return array_filter(
			array_column($this->getTagsByName($tagName), 'value'),
			static fn (PhpDocTagValueNode $value): bool => $value instanceof ThrowsTagValueNode,
		);
	}


	/**
	 * @return MixinTagValueNode[]
	 */
	public function getMixinTagValues(string $tagName = '@mixin'): array
	{
		return array_filter(
			array_column($this->getTagsByName($tagName), 'value'),
			static fn (PhpDocTagValueNode $value): bool => $value instanceof MixinTagValueNode,
		);
	}

	/**
	 * @return RequireExtendsTagValueNode[]
	 */
	public function getRequireExtendsTagValues(string $tagName = '@phpstan-require-extends'): array
	{
		return array_filter(
			array_column($this->getTagsByName($tagName), 'value'),
			static fn (PhpDocTagValueNode $value): bool => $value instanceof RequireExtendsTagValueNode,
		);
	}

	/**
	 * @return RequireImplementsTagValueNode[]
	 */
	public function getRequireImplementsTagValues(string $tagName = '@phpstan-require-implements'): array
	{
		return array_filter(
			array_column($this->getTagsByName($tagName), 'value'),
			static fn (PhpDocTagValueNode $value): bool => $value instanceof RequireImplementsTagValueNode,
		);
	}

	/**
	 * @return SealedTagValueNode[]
	 */
	public function getSealedTagValues(string $tagName = '@phpstan-sealed'): array
	{
		return array_filter(
			array_column($this->getTagsByName($tagName), 'value'),
			static fn (PhpDocTagValueNode $value): bool => $value instanceof SealedTagValueNode,
		);
	}

	/**
	 * @return DeprecatedTagValueNode[]
	 */
	public function getDeprecatedTagValues(): array
	{
		return array_filter(
			array_column($this->getTagsByName('@deprecated'), 'value'),
			static fn (PhpDocTagValueNode $value): bool => $value instanceof DeprecatedTagValueNode,
		);
	}


	/**
	 * @return PropertyTagValueNode[]
	 */
	public function getPropertyTagValues(string $tagName = '@property'): array
	{
		return array_filter(
			array_column($this->getTagsByName($tagName), 'value'),
			static fn (PhpDocTagValueNode $value): bool => $value instanceof PropertyTagValueNode,
		);
	}


	/**
	 * @return PropertyTagValueNode[]
	 */
	public function getPropertyReadTagValues(string $tagName = '@property-read'): array
	{
		return array_filter(
			array_column($this->getTagsByName($tagName), 'value'),
			static fn (PhpDocTagValueNode $value): bool => $value instanceof PropertyTagValueNode,
		);
	}


	/**
	 * @return PropertyTagValueNode[]
	 */
	public function getPropertyWriteTagValues(string $tagName = '@property-write'): array
	{
		return array_filter(
			array_column($this->getTagsByName($tagName), 'value'),
			static fn (PhpDocTagValueNode $value): bool => $value instanceof PropertyTagValueNode,
		);
	}


	/**
	 * @return MethodTagValueNode[]
	 */
	public function getMethodTagValues(string $tagName = '@method'): array
	{
		return array_filter(
			array_column($this->getTagsByName($tagName), 'value'),
			static fn (PhpDocTagValueNode $value): bool => $value instanceof MethodTagValueNode,
		);
	}


	/**
	 * @return TypeAliasTagValueNode[]
	 */
	public function getTypeAliasTagValues(string $tagName = '@phpstan-type'): array
	{
		return array_filter(
			array_column($this->getTagsByName($tagName), 'value'),
			static fn (PhpDocTagValueNode $value): bool => $value instanceof TypeAliasTagValueNode,
		);
	}


	/**
	 * @return TypeAliasImportTagValueNode[]
	 */
	public function getTypeAliasImportTagValues(string $tagName = '@phpstan-import-type'): array
	{
		return array_filter(
			array_column($this->getTagsByName($tagName), 'value'),
			static fn (PhpDocTagValueNode $value): bool => $value instanceof TypeAliasImportTagValueNode,
		);
	}


	/**
	 * @return AssertTagValueNode[]
	 */
	public function getAssertTagValues(string $tagName = '@phpstan-assert'): array
	{
		return array_filter(
			array_column($this->getTagsByName($tagName), 'value'),
			static fn (PhpDocTagValueNode $value): bool => $value instanceof AssertTagValueNode,
		);
	}


	/**
	 * @return AssertTagPropertyValueNode[]
	 */
	public function getAssertPropertyTagValues(string $tagName = '@phpstan-assert'): array
	{
		return array_filter(
			array_column($this->getTagsByName($tagName), 'value'),
			static fn (PhpDocTagValueNode $value): bool => $value instanceof AssertTagPropertyValueNode,
		);
	}


	/**
	 * @return AssertTagMethodValueNode[]
	 */
	public function getAssertMethodTagValues(string $tagName = '@phpstan-assert'): array
	{
		return array_filter(
			array_column($this->getTagsByName($tagName), 'value'),
			static fn (PhpDocTagValueNode $value): bool => $value instanceof AssertTagMethodValueNode,
		);
	}


	/**
	 * @return SelfOutTagValueNode[]
	 */
	public function getSelfOutTypeTagValues(string $tagName = '@phpstan-this-out'): array
	{
		return array_filter(
			array_column($this->getTagsByName($tagName), 'value'),
			static fn (PhpDocTagValueNode $value): bool => $value instanceof SelfOutTagValueNode,
		);
	}


	/**
	 * @return ParamOutTagValueNode[]
	 */
	public function getParamOutTypeTagValues(string $tagName = '@param-out'): array
	{
		return array_filter(
			array_column($this->getTagsByName($tagName), 'value'),
			static fn (PhpDocTagValueNode $value): bool => $value instanceof ParamOutTagValueNode,
		);
	}


	public function __toString(): string
	{
		$children = array_map(
			static function (PhpDocChildNode $child): string {
				$s = (string) $child;
				return $s === '' ? '' : ' ' . $s;
			},
			$this->children,
		);
		return "/**\n *" . implode("\n *", $children) . "\n */";
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\PhpDoc;

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function trim;

class PureUnlessCallableIsImpureTagValueNode implements PhpDocTagValueNode
{

	use NodeAttributes;

	public string $parameterName;

	/** @var string (may be empty) */
	public string $description;

	public function __construct(string $parameterName, string $description)
	{
		$this->parameterName = $parameterName;
		$this->description = $description;
	}

	public function __toString(): string
	{
		return trim("{$this->parameterName} {$this->description}");
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\PhpDoc;

use PHPStan\PhpDocParser\Ast\NodeAttributes;

class GenericTagValueNode implements PhpDocTagValueNode
{

	use NodeAttributes;

	/** @var string (may be empty) */
	public string $value;

	public function __construct(string $value)
	{
		$this->value = $value;
	}


	public function __toString(): string
	{
		return $this->value;
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\PhpDoc;

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
use function trim;

class UsesTagValueNode implements PhpDocTagValueNode
{

	use NodeAttributes;

	public GenericTypeNode $type;

	/** @var string (may be empty) */
	public string $description;

	public function __construct(GenericTypeNode $type, string $description)
	{
		$this->type = $type;
		$this->description = $description;
	}


	public function __toString(): string
	{
		return trim("{$this->type} {$this->description}");
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\PhpDoc;

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;

class TemplateTagValueNode implements PhpDocTagValueNode
{

	use NodeAttributes;

	/** @var non-empty-string */
	public string $name;

	public ?TypeNode $bound;

	public ?TypeNode $default;

	public ?TypeNode $lowerBound;

	/** @var string (may be empty) */
	public string $description;

	/**
	 * @param non-empty-string $name
	 */
	public function __construct(string $name, ?TypeNode $bound, string $description, ?TypeNode $default = null, ?TypeNode $lowerBound = null)
	{
		$this->name = $name;
		$this->bound = $bound;
		$this->lowerBound = $lowerBound;
		$this->default = $default;
		$this->description = $description;
	}


	public function __toString(): string
	{
		$upperBound = $this->bound !== null ? " of {$this->bound}" : '';
		$lowerBound = $this->lowerBound !== null ? " super {$this->lowerBound}" : '';
		$default = $this->default !== null ? " = {$this->default}" : '';
		return trim("{$this->name}{$upperBound}{$lowerBound}{$default} {$this->description}");
	}

}
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast;

use LogicException;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocChildNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagValueNode;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function array_keys;
use function array_pop;
use function array_splice;
use function count;
use function get_class;
use function get_object_vars;
use function gettype;
use function is_array;
use function sprintf;

/**
 * Inspired by https://github.com/nikic/PHP-Parser/tree/36a6dcd04e7b0285e8f0868f44bd4927802f7df1
 *
 * Copyright (c) 2011, Nikita Popov
 * All rights reserved.
 */
final class NodeTraverser
{

	/**
	 * If NodeVisitor::enterNode() returns DONT_TRAVERSE_CHILDREN, child nodes
	 * of the current node will not be traversed for any visitors.
	 *
	 * For subsequent visitors enterNode() will still be called on the current
	 * node and leaveNode() will also be invoked for the current node.
	 */
	public const DONT_TRAVERSE_CHILDREN = 1;

	/**
	 * If NodeVisitor::enterNode() or NodeVisitor::leaveNode() returns
	 * STOP_TRAVERSAL, traversal is aborted.
	 *
	 * The afterTraverse() method will still be invoked.
	 */
	public const STOP_TRAVERSAL = 2;

	/**
	 * If NodeVisitor::leaveNode() returns REMOVE_NODE for a node that occurs
	 * in an array, it will be removed from the array.
	 *
	 * For subsequent visitors leaveNode() will still be invoked for the
	 * removed node.
	 */
	public const REMOVE_NODE = 3;

	/**
	 * If NodeVisitor::enterNode() returns DONT_TRAVERSE_CURRENT_AND_CHILDREN, child nodes
	 * of the current node will not be traversed for any visitors.
	 *
	 * For subsequent visitors enterNode() will not be called as well.
	 * leaveNode() will be invoked for visitors that has enterNode() method invoked.
	 */
	public const DONT_TRAVERSE_CURRENT_AND_CHILDREN = 4;

	/** @var list<NodeVisitor> Visitors */
	private array $visitors = [];

	/** @var bool Whether traversal should be stopped */
	private bool $stopTraversal;

	/**
	 * @param list<NodeVisitor> $visitors
	 */
	public function __construct(array $visitors)
	{
		$this->visitors = $visitors;
	}

	/**
	 * Traverses an array of nodes using the registered visitors.
	 *
	 * @param Node[] $nodes Array of nodes
	 *
	 * @return Node[] Traversed array of nodes
	 */
	public function traverse(array $nodes): array
	{
		$this->stopTraversal = false;

		foreach ($this->visitors as $visitor) {
			$return = $visitor->beforeTraverse($nodes);
			if ($return === null) {
				continue;
			}

			$nodes = $return;
		}

		$nodes = $this->traverseArray($nodes);

		foreach ($this->visitors as $visitor) {
			$return = $visitor->afterTraverse($nodes);
			if ($return === null) {
				continue;
			}

			$nodes = $return;
		}

		return $nodes;
	}

	/**
	 * Recursively traverse a node.
	 *
	 * @param Node $node Node to traverse.
	 *
	 * @return Node Result of traversal (may be original node or new one)
	 */
	private function traverseNode(Node $node): Node
	{
		$subNodeNames = array_keys(get_object_vars($node));
		foreach ($subNodeNames as $name) {
			$subNode =& $node->$name;

			if (is_array($subNode)) {
				$subNode = $this->traverseArray($subNode);
				if ($this->stopTraversal) {
					break;
				}
			} elseif ($subNode instanceof Node) {
				$traverseChildren = true;
				$breakVisitorIndex = null;

				foreach ($this->visitors as $visitorIndex => $visitor) {
					$return = $visitor->enterNode($subNode);
					if ($return === null) {
						continue;
					}

					if ($return instanceof Node) {
						$this->ensureReplacementReasonable($subNode, $return);
						$subNode = $return;
					} elseif ($return === self::DONT_TRAVERSE_CHILDREN) {
						$traverseChildren = false;
					} elseif ($return === self::DONT_TRAVERSE_CURRENT_AND_CHILDREN) {
						$traverseChildren = false;
						$breakVisitorIndex = $visitorIndex;
						break;
					} elseif ($return === self::STOP_TRAVERSAL) {
						$this->stopTraversal = true;
						break 2;
					} else {
						throw new LogicException(
							'enterNode() returned invalid value of type ' . gettype($return),
						);
					}
				}

				if ($traverseChildren) {
					$subNode = $this->traverseNode($subNode);
					if ($this->stopTraversal) {
						break;
					}
				}

				foreach ($this->visitors as $visitorIndex => $visitor) {
					$return = $visitor->leaveNode($subNode);

					if ($return !== null) {
						if ($return instanceof Node) {
							$this->ensureReplacementReasonable($subNode, $return);
							$subNode = $return;
						} elseif ($return === self::STOP_TRAVERSAL) {
							$this->stopTraversal = true;
							break 2;
						} elseif (is_array($return)) {
							throw new LogicException(
								'leaveNode() may only return an array ' .
								'if the parent structure is an array',
							);
						} else {
							throw new LogicException(
								'leaveNode() returned invalid value of type ' . gettype($return),
							);
						}
					}

					if ($breakVisitorIndex === $visitorIndex) {
						break;
					}
				}
			}
		}

		return $node;
	}

	/**
	 * Recursively traverse array (usually of nodes).
	 *
	 * @param mixed[] $nodes Array to traverse
	 *
	 * @return mixed[] Result of traversal (may be original array or changed one)
	 */
	private function traverseArray(array $nodes): array
	{
		$doNodes = [];

		foreach ($nodes as $i => &$node) {
			if ($node instanceof Node) {
				$traverseChildren = true;
				$breakVisitorIndex = null;

				foreach ($this->visitors as $visitorIndex => $visitor) {
					$return = $visitor->enterNode($node);
					if ($return === null) {
						continue;
					}

					if ($return instanceof Node) {
						$this->ensureReplacementReasonable($node, $return);
						$node = $return;
					} elseif (is_array($return)) {
						$doNodes[] = [$i, $return];
						continue 2;
					} elseif ($return === self::REMOVE_NODE) {
						$doNodes[] = [$i, []];
						continue 2;
					} elseif ($return === self::DONT_TRAVERSE_CHILDREN) {
						$traverseChildren = false;
					} elseif ($return === self::DONT_TRAVERSE_CURRENT_AND_CHILDREN) {
						$traverseChildren = false;
						$breakVisitorIndex = $visitorIndex;
						break;
					} elseif ($return === self::STOP_TRAVERSAL) {
						$this->stopTraversal = true;
						break 2;
					} else {
						throw new LogicException(
							'enterNode() returned invalid value of type ' . gettype($return),
						);
					}
				}

				if ($traverseChildren) {
					$node = $this->traverseNode($node);
					if ($this->stopTraversal) {
						break;
					}
				}

				foreach ($this->visitors as $visitorIndex => $visitor) {
					$return = $visitor->leaveNode($node);

					if ($return !== null) {
						if ($return instanceof Node) {
							$this->ensureReplacementReasonable($node, $return);
							$node = $return;
						} elseif (is_array($return)) {
							$doNodes[] = [$i, $return];
							break;
						} elseif ($return === self::REMOVE_NODE) {
							$doNodes[] = [$i, []];
							break;
						} elseif ($return === self::STOP_TRAVERSAL) {
							$this->stopTraversal = true;
							break 2;
						} else {
							throw new LogicException(
								'leaveNode() returned invalid value of type ' . gettype($return),
							);
						}
					}

					if ($breakVisitorIndex === $visitorIndex) {
						break;
					}
				}
			} elseif (is_array($node)) {
				throw new LogicException('Invalid node structure: Contains nested arrays');
			}
		}

		if (count($doNodes) > 0) {
			while ([$i, $replace] = array_pop($doNodes)) {
				array_splice($nodes, $i, 1, $replace);
			}
		}

		return $nodes;
	}

	private function ensureReplacementReasonable(Node $old, Node $new): void
	{
		if ($old instanceof TypeNode && !$new instanceof TypeNode) {
			throw new LogicException(sprintf('Trying to replace TypeNode with %s', get_class($new)));
		}

		if ($old instanceof ConstExprNode && !$new instanceof ConstExprNode) {
			throw new LogicException(sprintf('Trying to replace ConstExprNode with %s', get_class($new)));
		}

		if ($old instanceof PhpDocChildNode && !$new instanceof PhpDocChildNode) {
			throw new LogicException(sprintf('Trying to replace PhpDocChildNode with %s', get_class($new)));
		}

		if ($old instanceof PhpDocTagValueNode && !$new instanceof PhpDocTagValueNode) {
			throw new LogicException(sprintf('Trying to replace PhpDocTagValueNode with %s', get_class($new)));
		}
	}

}
<?php

namespace Psr\Log;

/**
 * This is a simple Logger trait that classes unable to extend AbstractLogger
 * (because they extend another class, etc) can include.
 *
 * It simply delegates all log-level-specific methods to the `log` method to
 * reduce boilerplate code that a simple Logger that does the same thing with
 * messages regardless of the error level has to implement.
 */
trait LoggerTrait
{
    /**
     * System is unusable.
     *
     * @param string $message
     * @param array  $context
     *
     * @return void
     */
    public function emergency($message, array $context = array())
    {
        $this->log(LogLevel::EMERGENCY, $message, $context);
    }

    /**
     * Action must be taken immediately.
     *
     * Example: Entire website down, database unavailable, etc. This should
     * trigger the SMS alerts and wake you up.
     *
     * @param string $message
     * @param array  $context
     *
     * @return void
     */
    public function alert($message, array $context = array())
    {
        $this->log(LogLevel::ALERT, $message, $context);
    }

    /**
     * Critical conditions.
     *
     * Example: Application component unavailable, unexpected exception.
     *
     * @param string $message
     * @param array  $context
     *
     * @return void
     */
    public function critical($message, array $context = array())
    {
        $this->log(LogLevel::CRITICAL, $message, $context);
    }

    /**
     * Runtime errors that do not require immediate action but should typically
     * be logged and monitored.
     *
     * @param string $message
     * @param array  $context
     *
     * @return void
     */
    public function error($message, array $context = array())
    {
        $this->log(LogLevel::ERROR, $message, $context);
    }

    /**
     * Exceptional occurrences that are not errors.
     *
     * Example: Use of deprecated APIs, poor use of an API, undesirable things
     * that are not necessarily wrong.
     *
     * @param string $message
     * @param array  $context
     *
     * @return void
     */
    public function warning($message, array $context = array())
    {
        $this->log(LogLevel::WARNING, $message, $context);
    }

    /**
     * Normal but significant events.
     *
     * @param string $message
     * @param array  $context
     *
     * @return void
     */
    public function notice($message, array $context = array())
    {
        $this->log(LogLevel::NOTICE, $message, $context);
    }

    /**
     * Interesting events.
     *
     * Example: User logs in, SQL logs.
     *
     * @param string $message
     * @param array  $context
     *
     * @return void
     */
    public function info($message, array $context = array())
    {
        $this->log(LogLevel::INFO, $message, $context);
    }

    /**
     * Detailed debug information.
     *
     * @param string $message
     * @param array  $context
     *
     * @return void
     */
    public function debug($message, array $context = array())
    {
        $this->log(LogLevel::DEBUG, $message, $context);
    }

    /**
     * Logs with an arbitrary level.
     *
     * @param mixed  $level
     * @param string $message
     * @param array  $context
     *
     * @return void
     *
     * @throws \Psr\Log\InvalidArgumentException
     */
    abstract public function log($level, $message, array $context = array());
}
<?php

namespace Psr\Log;

class InvalidArgumentException extends \InvalidArgumentException
{
}
<?php

namespace Psr\Log;

/**
 * Describes a logger instance.
 *
 * The message MUST be a string or object implementing __toString().
 *
 * The message MAY contain placeholders in the form: {foo} where foo
 * will be replaced by the context data in key "foo".
 *
 * The context array can contain arbitrary data. The only assumption that
 * can be made by implementors is that if an Exception instance is given
 * to produce a stack trace, it MUST be in a key named "exception".
 *
 * See https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md
 * for the full interface specification.
 */
interface LoggerInterface
{
    /**
     * System is unusable.
     *
     * @param string  $message
     * @param mixed[] $context
     *
     * @return void
     */
    public function emergency($message, array $context = array());

    /**
     * Action must be taken immediately.
     *
     * Example: Entire website down, database unavailable, etc. This should
     * trigger the SMS alerts and wake you up.
     *
     * @param string  $message
     * @param mixed[] $context
     *
     * @return void
     */
    public function alert($message, array $context = array());

    /**
     * Critical conditions.
     *
     * Example: Application component unavailable, unexpected exception.
     *
     * @param string  $message
     * @param mixed[] $context
     *
     * @return void
     */
    public function critical($message, array $context = array());

    /**
     * Runtime errors that do not require immediate action but should typically
     * be logged and monitored.
     *
     * @param string  $message
     * @param mixed[] $context
     *
     * @return void
     */
    public function error($message, array $context = array());

    /**
     * Exceptional occurrences that are not errors.
     *
     * Example: Use of deprecated APIs, poor use of an API, undesirable things
     * that are not necessarily wrong.
     *
     * @param string  $message
     * @param mixed[] $context
     *
     * @return void
     */
    public function warning($message, array $context = array());

    /**
     * Normal but significant events.
     *
     * @param string  $message
     * @param mixed[] $context
     *
     * @return void
     */
    public function notice($message, array $context = array());

    /**
     * Interesting events.
     *
     * Example: User logs in, SQL logs.
     *
     * @param string  $message
     * @param mixed[] $context
     *
     * @return void
     */
    public function info($message, array $context = array());

    /**
     * Detailed debug information.
     *
     * @param string  $message
     * @param mixed[] $context
     *
     * @return void
     */
    public function debug($message, array $context = array());

    /**
     * Logs with an arbitrary level.
     *
     * @param mixed   $level
     * @param string  $message
     * @param mixed[] $context
     *
     * @return void
     *
     * @throws \Psr\Log\InvalidArgumentException
     */
    public function log($level, $message, array $context = array());
}
<?php

namespace Psr\Log;

/**
 * Describes a logger-aware instance.
 */
interface LoggerAwareInterface
{
    /**
     * Sets a logger instance on the object.
     *
     * @param LoggerInterface $logger
     *
     * @return void
     */
    public function setLogger(LoggerInterface $logger);
}
<?php

namespace Psr\Log;

/**
 * Describes log levels.
 */
class LogLevel
{
    const EMERGENCY = 'emergency';
    const ALERT     = 'alert';
    const CRITICAL  = 'critical';
    const ERROR     = 'error';
    const WARNING   = 'warning';
    const NOTICE    = 'notice';
    const INFO      = 'info';
    const DEBUG     = 'debug';
}
<?php

namespace Psr\Log;

/**
 * This is a simple Logger implementation that other Loggers can inherit from.
 *
 * It simply delegates all log-level-specific methods to the `log` method to
 * reduce boilerplate code that a simple Logger that does the same thing with
 * messages regardless of the error level has to implement.
 */
abstract class AbstractLogger implements LoggerInterface
{
    /**
     * System is unusable.
     *
     * @param string  $message
     * @param mixed[] $context
     *
     * @return void
     */
    public function emergency($message, array $context = array())
    {
        $this->log(LogLevel::EMERGENCY, $message, $context);
    }

    /**
     * Action must be taken immediately.
     *
     * Example: Entire website down, database unavailable, etc. This should
     * trigger the SMS alerts and wake you up.
     *
     * @param string  $message
     * @param mixed[] $context
     *
     * @return void
     */
    public function alert($message, array $context = array())
    {
        $this->log(LogLevel::ALERT, $message, $context);
    }

    /**
     * Critical conditions.
     *
     * Example: Application component unavailable, unexpected exception.
     *
     * @param string  $message
     * @param mixed[] $context
     *
     * @return void
     */
    public function critical($message, array $context = array())
    {
        $this->log(LogLevel::CRITICAL, $message, $context);
    }

    /**
     * Runtime errors that do not require immediate action but should typically
     * be logged and monitored.
     *
     * @param string  $message
     * @param mixed[] $context
     *
     * @return void
     */
    public function error($message, array $context = array())
    {
        $this->log(LogLevel::ERROR, $message, $context);
    }

    /**
     * Exceptional occurrences that are not errors.
     *
     * Example: Use of deprecated APIs, poor use of an API, undesirable things
     * that are not necessarily wrong.
     *
     * @param string  $message
     * @param mixed[] $context
     *
     * @return void
     */
    public function warning($message, array $context = array())
    {
        $this->log(LogLevel::WARNING, $message, $context);
    }

    /**
     * Normal but significant events.
     *
     * @param string  $message
     * @param mixed[] $context
     *
     * @return void
     */
    public function notice($message, array $context = array())
    {
        $this->log(LogLevel::NOTICE, $message, $context);
    }

    /**
     * Interesting events.
     *
     * Example: User logs in, SQL logs.
     *
     * @param string  $message
     * @param mixed[] $context
     *
     * @return void
     */
    public function info($message, array $context = array())
    {
        $this->log(LogLevel::INFO, $message, $context);
    }

    /**
     * Detailed debug information.
     *
     * @param string  $message
     * @param mixed[] $context
     *
     * @return void
     */
    public function debug($message, array $context = array())
    {
        $this->log(LogLevel::DEBUG, $message, $context);
    }
}
<?php

namespace Psr\Log;

/**
 * Basic Implementation of LoggerAwareInterface.
 */
trait LoggerAwareTrait
{
    /**
     * The logger instance.
     *
     * @var LoggerInterface|null
     */
    protected $logger;

    /**
     * Sets a logger.
     *
     * @param LoggerInterface $logger
     */
    public function setLogger(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }
}
<?php

namespace Psr\Log\Test;

use Psr\Log\AbstractLogger;

/**
 * Used for testing purposes.
 *
 * It records all records and gives you access to them for verification.
 *
 * @method bool hasEmergency($record)
 * @method bool hasAlert($record)
 * @method bool hasCritical($record)
 * @method bool hasError($record)
 * @method bool hasWarning($record)
 * @method bool hasNotice($record)
 * @method bool hasInfo($record)
 * @method bool hasDebug($record)
 *
 * @method bool hasEmergencyRecords()
 * @method bool hasAlertRecords()
 * @method bool hasCriticalRecords()
 * @method bool hasErrorRecords()
 * @method bool hasWarningRecords()
 * @method bool hasNoticeRecords()
 * @method bool hasInfoRecords()
 * @method bool hasDebugRecords()
 *
 * @method bool hasEmergencyThatContains($message)
 * @method bool hasAlertThatContains($message)
 * @method bool hasCriticalThatContains($message)
 * @method bool hasErrorThatContains($message)
 * @method bool hasWarningThatContains($message)
 * @method bool hasNoticeThatContains($message)
 * @method bool hasInfoThatContains($message)
 * @method bool hasDebugThatContains($message)
 *
 * @method bool hasEmergencyThatMatches($message)
 * @method bool hasAlertThatMatches($message)
 * @method bool hasCriticalThatMatches($message)
 * @method bool hasErrorThatMatches($message)
 * @method bool hasWarningThatMatches($message)
 * @method bool hasNoticeThatMatches($message)
 * @method bool hasInfoThatMatches($message)
 * @method bool hasDebugThatMatches($message)
 *
 * @method bool hasEmergencyThatPasses($message)
 * @method bool hasAlertThatPasses($message)
 * @method bool hasCriticalThatPasses($message)
 * @method bool hasErrorThatPasses($message)
 * @method bool hasWarningThatPasses($message)
 * @method bool hasNoticeThatPasses($message)
 * @method bool hasInfoThatPasses($message)
 * @method bool hasDebugThatPasses($message)
 */
class TestLogger extends AbstractLogger
{
    /**
     * @var array
     */
    public $records = [];

    public $recordsByLevel = [];

    /**
     * @inheritdoc
     */
    public function log($level, $message, array $context = [])
    {
        $record = [
            'level' => $level,
            'message' => $message,
            'context' => $context,
        ];

        $this->recordsByLevel[$record['level']][] = $record;
        $this->records[] = $record;
    }

    public function hasRecords($level)
    {
        return isset($this->recordsByLevel[$level]);
    }

    public function hasRecord($record, $level)
    {
        if (is_string($record)) {
            $record = ['message' => $record];
        }
        return $this->hasRecordThatPasses(function ($rec) use ($record) {
            if ($rec['message'] !== $record['message']) {
                return false;
            }
            if (isset($record['context']) && $rec['context'] !== $record['context']) {
                return false;
            }
            return true;
        }, $level);
    }

    public function hasRecordThatContains($message, $level)
    {
        return $this->hasRecordThatPasses(function ($rec) use ($message) {
            return strpos($rec['message'], $message) !== false;
        }, $level);
    }

    public function hasRecordThatMatches($regex, $level)
    {
        return $this->hasRecordThatPasses(function ($rec) use ($regex) {
            return preg_match($regex, $rec['message']) > 0;
        }, $level);
    }

    public function hasRecordThatPasses(callable $predicate, $level)
    {
        if (!isset($this->recordsByLevel[$level])) {
            return false;
        }
        foreach ($this->recordsByLevel[$level] as $i => $rec) {
            if (call_user_func($predicate, $rec, $i)) {
                return true;
            }
        }
        return false;
    }

    public function __call($method, $args)
    {
        if (preg_match('/(.*)(Debug|Info|Notice|Warning|Error|Critical|Alert|Emergency)(.*)/', $method, $matches) > 0) {
            $genericMethod = $matches[1] . ('Records' !== $matches[3] ? 'Record' : '') . $matches[3];
            $level = strtolower($matches[2]);
            if (method_exists($this, $genericMethod)) {
                $args[] = $level;
                return call_user_func_array([$this, $genericMethod], $args);
            }
        }
        throw new \BadMethodCallException('Call to undefined method ' . get_class($this) . '::' . $method . '()');
    }

    public function reset()
    {
        $this->records = [];
        $this->recordsByLevel = [];
    }
}
<?php

namespace Psr\Log\Test;

/**
 * This class is internal and does not follow the BC promise.
 *
 * Do NOT use this class in any way.
 *
 * @internal
 */
class DummyTest
{
    public function __toString()
    {
        return 'DummyTest';
    }
}
<?php

namespace Psr\Log\Test;

use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use PHPUnit\Framework\TestCase;

/**
 * Provides a base test class for ensuring compliance with the LoggerInterface.
 *
 * Implementors can extend the class and implement abstract methods to run this
 * as part of their test suite.
 */
abstract class LoggerInterfaceTest extends TestCase
{
    /**
     * @return LoggerInterface
     */
    abstract public function getLogger();

    /**
     * This must return the log messages in order.
     *
     * The simple formatting of the messages is: "<LOG LEVEL> <MESSAGE>".
     *
     * Example ->error('Foo') would yield "error Foo".
     *
     * @return string[]
     */
    abstract public function getLogs();

    public function testImplements()
    {
        $this->assertInstanceOf('Psr\Log\LoggerInterface', $this->getLogger());
    }

    /**
     * @dataProvider provideLevelsAndMessages
     */
    public function testLogsAtAllLevels($level, $message)
    {
        $logger = $this->getLogger();
        $logger->{$level}($message, array('user' => 'Bob'));
        $logger->log($level, $message, array('user' => 'Bob'));

        $expected = array(
            $level.' message of level '.$level.' with context: Bob',
            $level.' message of level '.$level.' with context: Bob',
        );
        $this->assertEquals($expected, $this->getLogs());
    }

    public function provideLevelsAndMessages()
    {
        return array(
            LogLevel::EMERGENCY => array(LogLevel::EMERGENCY, 'message of level emergency with context: {user}'),
            LogLevel::ALERT => array(LogLevel::ALERT, 'message of level alert with context: {user}'),
            LogLevel::CRITICAL => array(LogLevel::CRITICAL, 'message of level critical with context: {user}'),
            LogLevel::ERROR => array(LogLevel::ERROR, 'message of level error with context: {user}'),
            LogLevel::WARNING => array(LogLevel::WARNING, 'message of level warning with context: {user}'),
            LogLevel::NOTICE => array(LogLevel::NOTICE, 'message of level notice with context: {user}'),
            LogLevel::INFO => array(LogLevel::INFO, 'message of level info with context: {user}'),
            LogLevel::DEBUG => array(LogLevel::DEBUG, 'message of level debug with context: {user}'),
        );
    }

    /**
     * @expectedException \Psr\Log\InvalidArgumentException
     */
    public function testThrowsOnInvalidLevel()
    {
        $logger = $this->getLogger();
        $logger->log('invalid level', 'Foo');
    }

    public function testContextReplacement()
    {
        $logger = $this->getLogger();
        $logger->info('{Message {nothing} {user} {foo.bar} a}', array('user' => 'Bob', 'foo.bar' => 'Bar'));

        $expected = array('info {Message {nothing} Bob Bar a}');
        $this->assertEquals($expected, $this->getLogs());
    }

    public function testObjectCastToString()
    {
        if (method_exists($this, 'createPartialMock')) {
            $dummy = $this->createPartialMock('Psr\Log\Test\DummyTest', array('__toString'));
        } else {
            $dummy = $this->getMock('Psr\Log\Test\DummyTest', array('__toString'));
        }
        $dummy->expects($this->once())
            ->method('__toString')
            ->will($this->returnValue('DUMMY'));

        $this->getLogger()->warning($dummy);

        $expected = array('warning DUMMY');
        $this->assertEquals($expected, $this->getLogs());
    }

    public function testContextCanContainAnything()
    {
        $closed = fopen('php://memory', 'r');
        fclose($closed);

        $context = array(
            'bool' => true,
            'null' => null,
            'string' => 'Foo',
            'int' => 0,
            'float' => 0.5,
            'nested' => array('with object' => new DummyTest),
            'object' => new \DateTime,
            'resource' => fopen('php://memory', 'r'),
            'closed' => $closed,
        );

        $this->getLogger()->warning('Crazy context data', $context);

        $expected = array('warning Crazy context data');
        $this->assertEquals($expected, $this->getLogs());
    }

    public function testContextExceptionKeyCanBeExceptionOrOtherValues()
    {
        $logger = $this->getLogger();
        $logger->warning('Random message', array('exception' => 'oops'));
        $logger->critical('Uncaught Exception!', array('exception' => new \LogicException('Fail')));

        $expected = array(
            'warning Random message',
            'critical Uncaught Exception!'
        );
        $this->assertEquals($expected, $this->getLogs());
    }
}
<?php

namespace Psr\Log;

/**
 * This Logger can be used to avoid conditional log calls.
 *
 * Logging should always be optional, and if no logger is provided to your
 * library creating a NullLogger instance to have something to throw logs at
 * is a good way to avoid littering your code with `if ($this->logger) { }`
 * blocks.
 */
class NullLogger extends AbstractLogger
{
    /**
     * Logs with an arbitrary level.
     *
     * @param mixed  $level
     * @param string $message
     * @param array  $context
     *
     * @return void
     *
     * @throws \Psr\Log\InvalidArgumentException
     */
    public function log($level, $message, array $context = array())
    {
        // noop
    }
}
<?php

namespace Psr\Container;

/**
 * No entry was found in the container.
 */
interface NotFoundExceptionInterface extends ContainerExceptionInterface
{
}
<?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);
}
<?php

namespace Psr\Container;

use Throwable;

/**
 * Base interface representing a generic exception in a container.
 */
interface ContainerExceptionInterface extends Throwable
{
}
<?php

namespace Psr\Cache;

/**
 * Exception interface for all exceptions thrown by an Implementing Library.
 */
interface CacheException
{
}
<?php

namespace Psr\Cache;

/**
 * Exception interface for invalid cache arguments.
 *
 * Any time an invalid argument is passed into a method it must throw an
 * exception class which implements Psr\Cache\InvalidArgumentException.
 */
interface InvalidArgumentException extends CacheException
{
}
<?php

namespace Psr\Cache;

/**
 * CacheItemPoolInterface generates CacheItemInterface objects.
 *
 * The primary purpose of Cache\CacheItemPoolInterface is to accept a key from
 * the Calling Library and return the associated Cache\CacheItemInterface object.
 * It is also the primary point of interaction with the entire cache collection.
 * All configuration and initialization of the Pool is left up to an
 * Implementing Library.
 */
interface CacheItemPoolInterface
{
    /**
     * Returns a Cache Item representing the specified key.
     *
     * This method must always return a CacheItemInterface object, even in case of
     * a cache miss. It MUST NOT return null.
     *
     * @param string $key
     *   The key for which to return the corresponding Cache Item.
     *
     * @throws InvalidArgumentException
     *   If the $key string is not a legal value a \Psr\Cache\InvalidArgumentException
     *   MUST be thrown.
     *
     * @return CacheItemInterface
     *   The corresponding Cache Item.
     */
    public function getItem($key);

    /**
     * Returns a traversable set of cache items.
     *
     * @param string[] $keys
     *   An indexed array of keys of items to retrieve.
     *
     * @throws InvalidArgumentException
     *   If any of the keys in $keys are not a legal value a \Psr\Cache\InvalidArgumentException
     *   MUST be thrown.
     *
     * @return array|\Traversable
     *   A traversable collection of Cache Items keyed by the cache keys of
     *   each item. A Cache item will be returned for each key, even if that
     *   key is not found. However, if no keys are specified then an empty
     *   traversable MUST be returned instead.
     */
    public function getItems(array $keys = array());

    /**
     * Confirms if the cache contains specified cache item.
     *
     * Note: This method MAY avoid retrieving the cached value for performance reasons.
     * This could result in a race condition with CacheItemInterface::get(). To avoid
     * such situation use CacheItemInterface::isHit() instead.
     *
     * @param string $key
     *   The key for which to check existence.
     *
     * @throws InvalidArgumentException
     *   If the $key string is not a legal value a \Psr\Cache\InvalidArgumentException
     *   MUST be thrown.
     *
     * @return bool
     *   True if item exists in the cache, false otherwise.
     */
    public function hasItem($key);

    /**
     * Deletes all items in the pool.
     *
     * @return bool
     *   True if the pool was successfully cleared. False if there was an error.
     */
    public function clear();

    /**
     * Removes the item from the pool.
     *
     * @param string $key
     *   The key to delete.
     *
     * @throws InvalidArgumentException
     *   If the $key string is not a legal value a \Psr\Cache\InvalidArgumentException
     *   MUST be thrown.
     *
     * @return bool
     *   True if the item was successfully removed. False if there was an error.
     */
    public function deleteItem($key);

    /**
     * Removes multiple items from the pool.
     *
     * @param string[] $keys
     *   An array of keys that should be removed from the pool.

     * @throws InvalidArgumentException
     *   If any of the keys in $keys are not a legal value a \Psr\Cache\InvalidArgumentException
     *   MUST be thrown.
     *
     * @return bool
     *   True if the items were successfully removed. False if there was an error.
     */
    public function deleteItems(array $keys);

    /**
     * Persists a cache item immediately.
     *
     * @param CacheItemInterface $item
     *   The cache item to save.
     *
     * @return bool
     *   True if the item was successfully persisted. False if there was an error.
     */
    public function save(CacheItemInterface $item);

    /**
     * Sets a cache item to be persisted later.
     *
     * @param CacheItemInterface $item
     *   The cache item to save.
     *
     * @return bool
     *   False if the item could not be queued or if a commit was attempted and failed. True otherwise.
     */
    public function saveDeferred(CacheItemInterface $item);

    /**
     * Persists any deferred cache items.
     *
     * @return bool
     *   True if all not-yet-saved items were successfully saved or there were none. False otherwise.
     */
    public function commit();
}
<?php

namespace Psr\Cache;

/**
 * CacheItemInterface defines an interface for interacting with objects inside a cache.
 *
 * Each Item object MUST be associated with a specific key, which can be set
 * according to the implementing system and is typically passed by the
 * Cache\CacheItemPoolInterface object.
 *
 * The Cache\CacheItemInterface object encapsulates the storage and retrieval of
 * cache items. Each Cache\CacheItemInterface is generated by a
 * Cache\CacheItemPoolInterface object, which is responsible for any required
 * setup as well as associating the object with a unique Key.
 * Cache\CacheItemInterface objects MUST be able to store and retrieve any type
 * of PHP value defined in the Data section of the specification.
 *
 * Calling Libraries MUST NOT instantiate Item objects themselves. They may only
 * be requested from a Pool object via the getItem() method.  Calling Libraries
 * SHOULD NOT assume that an Item created by one Implementing Library is
 * compatible with a Pool from another Implementing Library.
 */
interface CacheItemInterface
{
    /**
     * Returns the key for the current cache item.
     *
     * The key is loaded by the Implementing Library, but should be available to
     * the higher level callers when needed.
     *
     * @return string
     *   The key string for this cache item.
     */
    public function getKey();

    /**
     * Retrieves the value of the item from the cache associated with this object's key.
     *
     * The value returned must be identical to the value originally stored by set().
     *
     * If isHit() returns false, this method MUST return null. Note that null
     * is a legitimate cached value, so the isHit() method SHOULD be used to
     * differentiate between "null value was found" and "no value was found."
     *
     * @return mixed
     *   The value corresponding to this cache item's key, or null if not found.
     */
    public function get();

    /**
     * Confirms if the cache item lookup resulted in a cache hit.
     *
     * Note: This method MUST NOT have a race condition between calling isHit()
     * and calling get().
     *
     * @return bool
     *   True if the request resulted in a cache hit. False otherwise.
     */
    public function isHit();

    /**
     * Sets the value represented by this cache item.
     *
     * The $value argument may be any item that can be serialized by PHP,
     * although the method of serialization is left up to the Implementing
     * Library.
     *
     * @param mixed $value
     *   The serializable value to be stored.
     *
     * @return static
     *   The invoked object.
     */
    public function set($value);

    /**
     * Sets the expiration time for this cache item.
     *
     * @param \DateTimeInterface|null $expiration
     *   The point in time after which the item MUST be considered expired.
     *   If null is passed explicitly, a default value MAY be used. If none is set,
     *   the value should be stored permanently or for as long as the
     *   implementation allows.
     *
     * @return static
     *   The called object.
     */
    public function expiresAt($expiration);

    /**
     * Sets the expiration time for this cache item.
     *
     * @param int|\DateInterval|null $time
     *   The period of time from the present after which the item MUST be considered
     *   expired. An integer parameter is understood to be the time in seconds until
     *   expiration. If null is passed explicitly, a default value MAY be used.
     *   If none is set, the value should be stored permanently or for as long as the
     *   implementation allows.
     *
     * @return static
     *   The called object.
     */
    public function expiresAfter($time);
}
<?php

/*
 * This file is part of the SensioLabsInsight package.
 *
 * (c) SensioLabs <support@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace SensioLabs\Insight\Sdk;

use SensioLabs\Insight\Sdk\Exception\ApiParserException;
use SensioLabs\Insight\Sdk\Model\Error;

class Parser
{
    public function parseError($content)
    {
        if (!$content) {
            throw new ApiParserException('Could not transform this xml to a \DOMDocument instance.');
        }

        $internalErrors = libxml_use_internal_errors(true);
        $disableEntities = libxml_disable_entity_loader(true);
        libxml_clear_errors();

        $document = new \DOMDocument();
        $document->validateOnParse = true;
        if (!$document->loadXML($content, \LIBXML_NONET | (\defined('LIBXML_COMPACT') ? \LIBXML_COMPACT : 0))) {
            libxml_disable_entity_loader($disableEntities);

            libxml_clear_errors();
            libxml_use_internal_errors($internalErrors);

            throw new ApiParserException('Could not transform this xml to a \DOMDocument instance.');
        }

        $document->normalizeDocument();

        libxml_use_internal_errors($internalErrors);
        libxml_disable_entity_loader($disableEntities);

        $xpath = new \DOMXpath($document);

        $nodes = $xpath->evaluate('./error');
        if (1 === $nodes->length) {
            throw new ApiParserException('The dom contains more than one error node.');
        }

        $error = new Error();

        $parameters = $xpath->query('./entity/body/parameter', $nodes->item(0));
        foreach ($parameters as $parameter) {
            $name = $parameter->getAttribute('name');
            $error->addEntityBodyParameter($name);

            $messages = $xpath->query('./message', $parameter);
            foreach ($messages as $message) {
                $error->addEntityBodyParameterError($name, $this->sanitizeValue($message->nodeValue));
            }
        }

        return $error;
    }

    protected function sanitizeValue($value)
    {
        if ('true' === $value) {
            $value = true;
        } elseif ('false' === $value) {
            $value = false;
        } elseif (empty($value)) {
            $value = null;
        }

        return $value;
    }
}
<?php

/*
 * This file is part of the SensioLabsInsight package.
 *
 * (c) SensioLabs <support@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace SensioLabs\Insight\Sdk\Exception;

interface ExceptionInterface
{
}
<?php

/*
 * This file is part of the SensioLabsInsight package.
 *
 * (c) SensioLabs <support@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace SensioLabs\Insight\Sdk\Exception;

use SensioLabs\Insight\Sdk\Model\Error;

class ApiClientException extends \LogicException implements ExceptionInterface
{
    private $error;

    public function __construct($message = '', Error $error = null, $code = 0, $e = null)
    {
        $this->error = $error;

        parent::__construct($message, $code, $e);
    }

    public function getError()
    {
        return $this->error;
    }
}
<?php

/*
 * This file is part of the SensioLabsInsight package.
 *
 * (c) SensioLabs <support@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace SensioLabs\Insight\Sdk\Exception;

class ApiParserException extends \RuntimeException implements ExceptionInterface
{
}
<?php

/*
 * This file is part of the SensioLabsInsight package.
 *
 * (c) SensioLabs <support@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace SensioLabs\Insight\Sdk\Exception;

class ApiServerException extends \RuntimeException implements ExceptionInterface
{
}
<?php

/*
 * This file is part of the SensioLabsInsight package.
 *
 * (c) SensioLabs <support@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace SensioLabs\Insight\Sdk;

use JMS\Serializer\Serializer;
use JMS\Serializer\SerializerBuilder;
use Psr\Log\LoggerInterface;
use SensioLabs\Insight\Sdk\Exception\ApiClientException;
use SensioLabs\Insight\Sdk\Exception\ApiServerException;
use SensioLabs\Insight\Sdk\Model\Analyses;
use SensioLabs\Insight\Sdk\Model\Analysis;
use SensioLabs\Insight\Sdk\Model\Project;
use SensioLabs\Insight\Sdk\Model\Projects;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpClient\ScopingHttpClient;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;

class Api
{
    public const ENDPOINT = 'https://insight.symfony.com';

    private $baseUrl;
    private $httpClient;
    private $serializer;
    private $parser;
    private $logger;

    public function __construct(array $options = [], ?HttpClientInterface $httpClient = null, ?Parser $parser = null, ?LoggerInterface $logger = null)
    {
        $this->httpClient = $httpClient ?: HttpClient::create();
        $this->parser = $parser ?: new Parser();

        $defaultOptions = [
            'base_url' => static::ENDPOINT,
            'cache' => false,
            'debug' => false,
        ];

        $required = ['api_token', 'base_url', 'user_uuid'];
        $options = array_merge($defaultOptions, $options);

        if ($missing = array_diff($required, array_keys($options))) {
            throw new \Exception('Config is missing the following keys: '.implode(', ', $missing));
        }

        $this->baseUrl = $options['base_url'];

        $this->httpClient = new ScopingHttpClient(
            $this->httpClient,
            [
                '.+' => [
                    'base_uri' => $this->baseUrl,
                    'auth_basic' => [$options['user_uuid'], $options['api_token']],
                    'headers' => ['accept' => 'application/vnd.com.sensiolabs.insight+xml'],
                ],
            ],
            '.+'
        );

        $serializerBuilder = SerializerBuilder::create()
            ->addMetadataDir(__DIR__.'/Model')
            ->setDebug($options['debug'])
        ;

        if ($cache = $options['cache']) {
            $serializerBuilder = $serializerBuilder->setCacheDir($cache);
        }

        $this->serializer = $serializerBuilder->build();
        $this->logger = $logger;
    }

    public function getBaseUrl(): string
    {
        return $this->baseUrl;
    }

    /**
     * @param int $page
     *
     * @return Projects
     */
    public function getProjects($page = 1)
    {
        return $this->serializer->deserialize(
            $this->send('GET', '/api/projects?page='.$page),
            Projects::class,
            'xml'
        );
    }

    /**
     * @param string $uuid
     *
     * @return Project
     */
    public function getProject($uuid)
    {
        return $this->serializer->deserialize(
            $this->send('GET', sprintf('/api/projects/%s', $uuid)),
            Project::class,
            'xml'
        );
    }

    /**
     * @return Project
     */
    public function updateProject(Project $project)
    {
        return $this->serializer->deserialize(
            $this->send('PUT', sprintf('/api/projects/%s', $project->getUuid()), ['insight_project' => $project->toArray()]),
            Project::class,
            'xml'
        );
    }

    /**
     * @return Project
     */
    public function createProject(Project $project)
    {
        return $this->serializer->deserialize(
            $this->send('POST', '/api/projects', ['insight_project' => $project->toArray()]),
            Project::class,
            'xml'
        );
    }

    /**
     * @param string $projectUuid
     *
     * @return Analyses
     */
    public function getAnalyses($projectUuid, $branch = null)
    {
        $url = sprintf('/api/projects/%s/analyses', $projectUuid);

        if ($branch) {
            $url .= '?branch='.$branch;
        }

        return $this->serializer->deserialize(
            $this->send('GET', $url),
            Analyses::class,
            'xml'
        );
    }

    /**
     * @param string $projectUuid
     * @param int    $analysesNumber
     *
     * @return Analysis
     */
    public function getAnalysis($projectUuid, $analysesNumber)
    {
        return $this->serializer->deserialize(
            $this->send('GET', sprintf('/api/projects/%s/analyses/%s', $projectUuid, $analysesNumber), null),
            Analysis::class,
            'xml'
        );
    }

    /**
     * @param string $projectUuid
     * @param int    $analysesNumber
     *
     * @return Analysis an incomplete Analysis object
     */
    public function getAnalysisStatus($projectUuid, $analysesNumber)
    {
        return $this->serializer->deserialize(
            $this->send('GET', sprintf('/api/projects/%s/analyses/%s/status', $projectUuid, $analysesNumber)),
            Analysis::class,
            'xml'
        );
    }

    /**
     * @param string      $projectUuid
     * @param string|null $reference   A git reference. It can be a commit sha, a tag name or a branch name
     * @param string|null $branch      Current analysis branch, used by SymfonyInsight to distinguish between the main branch and PRs
     *
     * @return Analysis
     */
    public function analyze($projectUuid, $reference = null, $branch = null)
    {
        return $this->serializer->deserialize(
            $this->send(
                'POST',
                sprintf('/api/projects/%s/analyses', $projectUuid),
                $branch ? ['reference' => $reference, 'branch' => $branch] : ['reference' => $reference]
            ),
            Analysis::class,
            'xml'
        );
    }

    /**
     * Use this method to call a specific API resource.
     */
    public function call($method = 'GET', $uri = null, $headers = null, $body = null, array $options = [], $classToUnserialize = null)
    {
        if ($classToUnserialize) {
            return $this->serializer->deserialize(
                $this->send($method, $uri, $body),
                $classToUnserialize,
                'xml'
            );
        }

        return $this->send($method, $uri, $body);
    }

    public function setLogger(LoggerInterface $logger)
    {
        $this->logger = $logger;

        return $this;
    }

    /**
     * @return Serializer
     */
    public function getSerializer()
    {
        return $this->serializer;
    }

    private function send($method, $url, $body = null): string
    {
        try {
            $option = [];
            if ($body) {
                $option['body'] = $body;
            }

            $this->logger && $this->logger->debug(sprintf('%s "%s"', $method, $url));
            $response = $this->httpClient->request($method, $url, $option);

            // block until headers arrive
            $response->getStatusCode();
            $this->logger && $this->logger->debug(sprintf("Request:\n%s", (string) $response->getInfo('debug')));

            return $response->getContent();
        } catch (ClientExceptionInterface $e) {
            $this->logException($e);

            $this->processClientError($e);
        } catch (TransportExceptionInterface $e) {
            $this->logException($e);

            throw new ApiServerException('Something went wrong with upstream', 0, $e);
        } catch (ServerExceptionInterface $e) {
            $this->logException($e);

            throw new ApiServerException('Something went wrong with upstream', 0, $e);
        }
    }

    private function processClientError(HttpExceptionInterface $e)
    {
        $statusCode = $e->getResponse()->getStatusCode();
        $error = null;
        $message = sprintf('Your request in not valid (status code: "%d").', $statusCode);

        if (400 === $statusCode) {
            $error = $this->parser->parseError($e->getResponse()->getContent(false));
            $message .= 'See $error attached to the exception';
        }

        throw new ApiClientException($message, $error, 0, $e);
    }

    private function logException(ExceptionInterface $e)
    {
        $message = sprintf("Exception: Class: \"%s\", Message: \"%s\", Response:\n%s",
            \get_class($e),
            $e->getMessage(),
            $e->getResponse()->getInfo('debug')
        );

        $this->logger && $this->logger->error($message, ['exception' => $e]);
    }
}
<?php

/*
 * This file is part of the SensioLabsInsight package.
 *
 * (c) SensioLabs <support@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace SensioLabs\Insight\Sdk\Model;

use JMS\Serializer\Annotation\Type;
use JMS\Serializer\Annotation\XmlList;

class Analyses
{
    /**
     * @Type("array<SensioLabs\Insight\Sdk\Model\Link>")
     * @XmlList(inline = true, entry = "link")
     */
    #[Type("array<SensioLabs\Insight\Sdk\Model\Link>")]
    #[XmlList(inline: true, entry: "link")]
    private $links = [];

    /**
     * @Type("array<SensioLabs\Insight\Sdk\Model\Analysis>")
     * @XmlList(inline = true, entry = "analysis")
     */
    #[Type("array<SensioLabs\Insight\Sdk\Model\Analysis>")]
    #[XmlList(inline: true, entry: "analysis")]
    private $analyses = [];

    /**
     * @return Link[]
     */
    public function getLinks()
    {
        return $this->links;
    }

    /**
     * @return Analysis[]
     */
    public function getAnalyses()
    {
        return $this->analyses;
    }
}
<?php

/*
 * This file is part of the SensioLabsInsight package.
 *
 * (c) SensioLabs <support@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace SensioLabs\Insight\Sdk\Model;

use JMS\Serializer\Annotation\Type;
use JMS\Serializer\Annotation\XmlAttribute;

class Violation
{
    /** @Type("string") */
    #[Type("string")]
    private $title;

    /** @Type("string") */
    #[Type("string")]
    private $message;

    /** @Type("string") */
    #[Type("string")]
    private $resource;

    /** @Type("integer") */
    #[Type("integer")]
    private $line;

    /**
     * @Type("string")
     * @XmlAttribute
     */
    #[Type("string")]
    #[XmlAttribute]
    private $severity;

    /**
     * @Type("string")
     * @XmlAttribute
     */
    #[Type("string")]
    #[XmlAttribute]
    private $category;

    /**
     * @Type("boolean")
     * @XmlAttribute
     */
    #[Type("boolean")]
    #[XmlAttribute]
    private $ignored;

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

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

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

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

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

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

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

/*
 * This file is part of the SensioLabsInsight package.
 *
 * (c) SensioLabs <support@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace SensioLabs\Insight\Sdk\Model;

use JMS\Serializer\Annotation\Exclude;
use JMS\Serializer\Annotation\SerializedName;
use JMS\Serializer\Annotation\Type;
use JMS\Serializer\Annotation\XmlList;

class Project
{
    /**
     * @see https://github.com/sensiolabs/connect/blob/master/src/SensioLabs/Connect/Api/Entity/Project.php
     */
    public const TYPE_PHP_WEBSITE = 0;

    public const TYPE_PHP_LIBRARY = 1;

    public const TYPE_SYMFONY2_BUNDLE = 2;

    public const TYPE_SYMFONY1_PLUGIN = 4;

    public const TYPE_OTHER = 6;

    public const TYPE_DRUPAL_MODULE = 7;

    public const TYPE_LARAVAL_WEB_PROJECT = 8;

    public const TYPE_SILEX_WEB_PROJECT = 9;

    public const TYPE_SYMFONY2_WEB_PROJECT = 10;

    public const TYPE_SYMFONY1_WEB_PROJECT = 11;

    /**
     * @Exclude()
     */
    #[Exclude]
    public static $types = [
        self::TYPE_SYMFONY2_WEB_PROJECT => 'Symfony2 Web Project',
        self::TYPE_SYMFONY1_WEB_PROJECT => 'symfony1 Web Project',
        self::TYPE_SILEX_WEB_PROJECT => 'Silex Web Project',
        self::TYPE_LARAVAL_WEB_PROJECT => 'Laravel Web Project',
        self::TYPE_SYMFONY2_BUNDLE => 'Symfony2 Bundle',
        self::TYPE_SYMFONY1_PLUGIN => 'symfony1 Plugin',
        self::TYPE_DRUPAL_MODULE => 'Drupal Module',
        self::TYPE_PHP_WEBSITE => 'PHP Web Project',
        self::TYPE_PHP_LIBRARY => 'PHP Library',
        self::TYPE_OTHER => 'Other',
    ];

    /**
     * @Type("array<SensioLabs\Insight\Sdk\Model\Link>")
     * @XmlList(inline = true, entry = "link")
     */
    #[Type("array<SensioLabs\Insight\Sdk\Model\Link>")]
    #[XmlList(inline: true, entry: "link")]
    private $links = [];

    /**
     * @Type("string")
     * @SerializedName("id")
     */
    #[Type("string")]
    #[SerializedName("id")]
    private $uuid;

    /** @Type("string") */
    #[Type("string")]
    private $name;

    /** @Type("string") */
    #[Type("string")]
    private $configuration;

    /** @Type("string") */
    #[Type("string")]
    private $description;

    /** @Type("integer") */
    #[Type("integer")]
    private $type;

    /**
     * @Type("string")
     * @SerializedName("repository-url")
     */
    #[Type("string")]
    #[SerializedName("repository-url")]
    private $repositoryUrl;

    /** @Type("boolean") */
    #[Type("boolean")]
    private $private;

    /**
     * @Type("boolean")
     * @SerializedName("report-available")
     */
    #[Type("boolean")]
    #[SerializedName("report-available")]
    private $reportAvailable;

    /**
     * @Type("SensioLabs\Insight\Sdk\Model\Analysis")
     * @SerializedName("last-analysis")
     */
    #[Type("SensioLabs\Insight\Sdk\Model\Analysis")]
    #[SerializedName("last-analysis")]
    private $lastAnalysis;

    public function toArray()
    {
        return [
            'name' => $this->name,
            'public' => !$this->private,
            'description' => $this->description,
            'repositoryUrl' => $this->repositoryUrl,
            'type' => $this->type,
            'configuration' => $this->configuration,
        ];
    }

    /**
     * @return Link[]
     */
    public function getLinks()
    {
        return $this->links;
    }

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

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

    public function setName($name)
    {
        $this->name = $name;

        return $this;
    }

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

    public function setConfiguration($configuration)
    {
        $this->configuration = $configuration;

        return $this;
    }

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

    public function setDescription($description)
    {
        $this->description = $description;

        return $this;
    }

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

    public function setType($type)
    {
        if (!\array_key_exists($type, static::$types)) {
            throw new \InvalidArgumentException(sprintf('"%s" is not a valid type. You must pick one among "%"', $type, implode('", "', array_keys(static::$types))));
        }

        $this->type = $type;

        return $this;
    }

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

    public function setRepositoryUrl($repositoryUrl)
    {
        $this->repositoryUrl = $repositoryUrl;

        return $this;
    }

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

    public function setPublic($isPublic = false)
    {
        $this->private = !$isPublic;

        return $this;
    }

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

    public function setPrivate($isPrivate = true)
    {
        $this->private = $isPrivate;

        return $this;
    }

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

    /**
     * @return Analysis|null
     */
    public function getLastAnalysis()
    {
        return $this->lastAnalysis;
    }
}
<?php

/*
 * This file is part of the SensioLabsInsight package.
 *
 * (c) SensioLabs <support@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace SensioLabs\Insight\Sdk\Model;

use JMS\Serializer\Annotation\Type;
use JMS\Serializer\Annotation\XmlAttribute;

class Link
{
    /**
     * @XmlAttribute
     * @Type("string")
     */
    #[XmlAttribute]
    #[Type("string")]
    private $href;

    /**
     * @XmlAttribute
     * @Type("string")
     */
    #[XmlAttribute]
    #[Type("string")]
    private $rel;

    /**
     * @XmlAttribute
     * @Type("string")
     */
    #[XmlAttribute]
    #[Type("string")]
    private $type;

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

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

    /**
     * @return string
     */
    public function getType()
    {
        return $this->type;
    }
}
<?php

/*
 * This file is part of the SensioLabsInsight package.
 *
 * (c) SensioLabs <support@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace SensioLabs\Insight\Sdk\Model;

use JMS\Serializer\Annotation\Type;
use JMS\Serializer\Annotation\XmlList;

class Violations implements \Countable, \IteratorAggregate
{
    /**
     * @Type("array<SensioLabs\Insight\Sdk\Model\Violation>")
     * @XmlList(inline = true, entry = "violation")
     */
    #[Type("array<SensioLabs\Insight\Sdk\Model\Violation>")]
    #[XmlList(inline: true, entry: "violation")]
    private $violations = [];

    public function count(): int
    {
        return \count($this->violations);
    }

    public function getIterator(): \Traversable
    {
        return new \ArrayIterator($this->violations);
    }

    /**
     * @return Violation[]
     */
    public function getViolations()
    {
        return $this->violations;
    }

    /**
     * @param callable $callback
     */
    public function filter($callback)
    {
        if (!\is_callable($callback)) {
            throw new \InvalidArgumentException('The callback is not callable.');
        }

        $this->violations = array_filter($this->violations, $callback);
    }
}
<?php

/*
 * This file is part of the SensioLabsInsight package.
 *
 * (c) SensioLabs <support@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace SensioLabs\Insight\Sdk\Model;

use JMS\Serializer\Annotation\Type;
use JMS\Serializer\Annotation\XmlAttribute;
use JMS\Serializer\Annotation\XmlList;
use JMS\Serializer\Annotation\XmlRoot;

/**
 * @XmlRoot("projects")
 */
#[XmlRoot("projects")]
class Projects
{
    /**
     * @XmlAttribute
     * @Type("integer")
     */
    #[XmlAttribute]
    #[Type("integer")]
    private $page;

    /**
     * @XmlAttribute
     * @Type("integer")
     */
    #[XmlAttribute]
    #[Type("integer")]
    private $total;

    /**
     * @XmlAttribute
     * @Type("integer")
     */
    #[XmlAttribute]
    #[Type("integer")]
    private $limit;

    /**
     * @Type("array<SensioLabs\Insight\Sdk\Model\Link>")
     * @XmlList(inline = true, entry = "link")
     */
    #[Type("array<SensioLabs\Insight\Sdk\Model\Link>")]
    #[XmlList(inline: true, entry: "link")]
    private $links = [];

    /**
     * @Type("array<SensioLabs\Insight\Sdk\Model\Project>")
     * @XmlList(inline = true, entry = "project")
     */
    #[Type("array<SensioLabs\Insight\Sdk\Model\Project>")]
    #[XmlList(inline: true, entry: "project")]
    private $projects = [];

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

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

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

    /**
     * @return Link[]
     */
    public function getLinks()
    {
        return $this->links;
    }

    /**
     * @return Project[]
     */
    public function getProjects()
    {
        return $this->projects;
    }
}
<?php

/*
 * This file is part of the SensioLabsInsight package.
 *
 * (c) SensioLabs <support@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace SensioLabs\Insight\Sdk\Model;

class Error
{
    private $entityBodyParameters = [];

    public function getEntityBodyParameters()
    {
        return $this->entityBodyParameters;
    }

    public function hasEntityBodyParameter($name)
    {
        return \array_key_exists($name, $this->entityBodyParameters);
    }

    public function addEntityBodyParameter($name)
    {
        if (!$this->hasEntityBodyParameter($name)) {
            $this->entityBodyParameters[$name] = [];
        }

        return $this;
    }

    public function addEntityBodyParameterError($name, $message)
    {
        $this->entityBodyParameters[$name][] = $message;

        return $this;
    }
}
<?php

/*
 * This file is part of the SensioLabsInsight package.
 *
 * (c) SensioLabs <support@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace SensioLabs\Insight\Sdk\Model;

use JMS\Serializer\Annotation\SerializedName;
use JMS\Serializer\Annotation\Type;
use JMS\Serializer\Annotation\XmlList;

class Analysis
{
    public const STATUS_ORDERED = 'ordered';

    public const STATUS_RUNNING = 'running';

    public const STATUS_MEASURED = 'measured';

    public const STATUS_ANALYZED = 'analyzed';

    public const STATUS_FINISHED = 'finished';

    /**
     * @Type("array<SensioLabs\Insight\Sdk\Model\Link>")
     * @XmlList(inline = true, entry = "link")
     */
    #[Type("array<SensioLabs\Insight\Sdk\Model\Link>")]
    #[XmlList(inline: true, entry: "link")]
    private $links = [];

    /** @Type("integer") */
    #[Type("integer")]
    private $number;

    /** @Type("string") */
    #[Type("string")]
    private $grade;

    /**
     * @Type("string")
     * @SerializedName("next-grade")
     */
    #[Type("string")]
    #[SerializedName("next-grade")]
    private $nextGrade;

    /** @Type("array<string>") */
    #[Type("array<string>")]
    private $grades = [];

    /**
     * @Type("float")
     * @SerializedName("remediation-cost")
     */
    #[Type("float")]
    #[SerializedName("remediation-cost")]
    private $remediationCost;

    /**
     * @Type("float")
     * @SerializedName("remediation-cost-for-next-grade")
     */
    #[Type("float")]
    #[SerializedName("remediation-cost-for-next-grade")]
    private $remediationCostForNextGrade;

    /**
     * @Type("integer")
     * @SerializedName("nb-violations")
     */
    #[Type("integer")]
    #[SerializedName("nb-violations")]
    private $nbViolations;

    /**
     * @Type("DateTime")
     * @SerializedName("begin-at")
     */
    #[Type("DateTime")]
    #[SerializedName("begin-at")]
    private $beginAt;

    /**
     * @Type("DateTime")
     * @SerializedName("end-at")
     */
    #[Type("DateTime")]
    #[SerializedName("end-at")]
    private $endAt;

    /** @Type("integer") */
    #[Type("integer")]
    private $duration;

    /**
     * @Type("string")
     * @SerializedName("failure-message")
     */
    #[Type("string")]
    #[SerializedName("failure-message")]
    private $failureMessage;

    /**
     * @Type("string")
     * @SerializedName("failure-code")
     */
    #[Type("string")]
    #[SerializedName("failure-code")]
    private $failureCode;

    /** @Type("boolean") */
    #[Type("boolean")]
    private $failed;

    /** @Type("string") */
    #[Type("string")]
    private $status;

    /**
     * @Type("string")
     * @SerializedName("status-message")
     */
    #[Type("string")]
    #[SerializedName("status-message")]
    private $statusMessage;

    /**
     * @Type("boolean")
     * @SerializedName("altered")
     */
    #[Type("boolean")]
    #[SerializedName("altered")]
    private $isAltered;

    /** @Type("SensioLabs\Insight\Sdk\Model\Violations") */
    #[Type("SensioLabs\Insight\Sdk\Model\Violations")]
    private $violations;

    /** @Type("string") */
    #[Type("string")]
    private $branch;

    /** @Type("string") */
    #[Type("string")]
    private $reference;

    /**
     * @return Link[]
     */
    public function getLinks()
    {
        return $this->links;
    }

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

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

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

    /**
     * @return string[]
     */
    public function getGrades()
    {
        return $this->grades;
    }

    /**
     * @return float
     */
    public function getRemediationCost()
    {
        return $this->remediationCost;
    }

    /**
     * @return float
     */
    public function getRemediationCostForNextGrade()
    {
        return $this->remediationCostForNextGrade;
    }

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

    /**
     * @return \DateTime
     */
    public function getBeginAt()
    {
        return $this->beginAt;
    }

    /**
     * @return \DateTime|null
     */
    public function getEndAt()
    {
        return $this->endAt;
    }

    /**
     * @return \DateInterval
     */
    public function getDuration()
    {
        return new \DateInterval('PT'.($this->duration ?: '0').'S');
    }

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

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

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

    /**
     * @return bool
     */
    public function isFinished()
    {
        return static::STATUS_FINISHED == $this->status;
    }

    /**
     * @return string One of the STATUS_* constants
     */
    public function getStatus()
    {
        return $this->status;
    }

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

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

    /**
     * @return Violations|null
     */
    public function getViolations()
    {
        return $this->violations;
    }
}
<?php

/*
 * This file is part of the SensioLabsInsight package.
 *
 * (c) SensioLabs <support@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

// installed via composer?
if (file_exists($a = __DIR__.'/../../../autoload.php')) {
    require_once $a;
} else {
    require_once __DIR__.'/../vendor/autoload.php';
}

use SensioLabs\Insight\Cli\Application;

$application = new Application();

$application->run();
<?php

namespace SensioLabs\Insight\Cli;

class Configuration
{
    private $storagePath;
    private $userUuid;
    private $apiToken;
    private $apiEndpoint;

    public function __construct()
    {
        $this->storagePath = $this->getStoragePath();
        $this->load();
    }

    public function getUserUuid()
    {
        return $this->userUuid;
    }

    public function setUserUuid($userUuid)
    {
        $this->userUuid = $userUuid;
    }

    public function getApiToken()
    {
        return $this->apiToken;
    }

    public function setApiToken($apiToken)
    {
        $this->apiToken = $apiToken;
    }

    public function getApiEndpoint()
    {
        return $this->apiEndpoint;
    }

    public function setApiEndpoint($apiEndpoint)
    {
        $this->apiEndpoint = $apiEndpoint;
    }

    public function toArray()
    {
        return [
            'user_uuid' => $this->userUuid,
            'api_token' => $this->apiToken,
            'api_endpoint' => $this->apiEndpoint,
        ];
    }

    public function save()
    {
        file_put_contents($this->storagePath, json_encode($this->toArray()));
    }

    public function equals(self $configuration)
    {
        if ($this->userUuid !== $configuration->userUuid) {
            return false;
        }
        if ($this->apiToken !== $configuration->apiToken) {
            return false;
        }
        if ($this->apiEndpoint !== $configuration->apiEndpoint) {
            return false;
        }

        return true;
    }

    private function load()
    {
        if (!file_exists($this->storagePath)) {
            return;
        }

        $data = json_decode(file_get_contents($this->storagePath), true);

        if (\array_key_exists('user_uuid', $data)) {
            $this->userUuid = $data['user_uuid'];
        }
        if (\array_key_exists('api_token', $data)) {
            $this->apiToken = $data['api_token'];
        }
        if (\array_key_exists('api_endpoint', $data)) {
            $this->apiEndpoint = $data['api_endpoint'];
        }
    }

    private function getStoragePath()
    {
        $storagePath = getenv('INSIGHT_HOME');

        if (!$storagePath) {
            if (\defined('PHP_WINDOWS_VERSION_MAJOR')) {
                if (!getenv('APPDATA')) {
                    throw new \RuntimeException('The APPDATA or INSIGHT_HOME environment variable must be set for insight to run correctly');
                }
                $storagePath = strtr(getenv('APPDATA'), '\\', '/').'/Sensiolabs';
            } else {
                if (!getenv('HOME')) {
                    throw new \RuntimeException('The HOME or INSIGHT_HOME environment variable must be set for insight to run correctly');
                }
                $storagePath = rtrim(getenv('HOME'), '/').'/.sensiolabs';
            }
        }

        if (!is_dir($storagePath) && !@mkdir($storagePath, 0777, true)) {
            throw new \RuntimeException(sprintf('The directory "%s" does not exist and could not be created.', $storagePath));
        }

        if (!is_writable($storagePath)) {
            throw new \RuntimeException(sprintf('The directory "%s" is not writable.', $storagePath));
        }

        return $storagePath.'/insight.json';
    }
}
<?php

/*
 * This file is part of the SymfonyInsight package.
 *
 * (c) Symfony <support@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace SensioLabs\Insight\Cli\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

/**
 * @author Igor Wiedler <igor@wiedler.ch>
 * @author Stephane PY <py.stephane1@gmail.com>
 * @author Grégoire Pineau <lyrixx@lyrixx.info>
 */
class SelfUpdateCommand extends Command
{
    /**
     * {@inheritdoc}
     */
    protected function configure(): void
    {
        $this
            ->setName('self-update')
            ->setDescription('Update insight.phar to the latest version.')
            ->setHelp(<<<EOT
The <info>%command.name%</info> command replace your insight.phar by the latest
version.

<info>php insight.phar %command.name%</info>

EOT
            )
        ;
    }

    /**
     * {@inheritdoc}
     */
    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $remoteFilename = 'http://get.insight.sensiolabs.com/insight.phar';
        $localFilename = $_SERVER['argv'][0];
        $tempFilename = basename($localFilename, '.phar').'-temp.phar';

        try {
            copy($remoteFilename, $tempFilename);

            if (md5_file($localFilename) == md5_file($tempFilename)) {
                $output->writeln('<info>insight is already up to date.</info>');
                unlink($tempFilename);

                return 0;
            }

            chmod($tempFilename, 0777 & ~umask());

            // test the phar validity
            $phar = new \Phar($tempFilename);
            // free the variable to unlock the file
            unset($phar);
            rename($tempFilename, $localFilename);

            $output->writeln('<info>insight updated.</info>');

            return 0;
        } catch (\Exception $e) {
            if (!$e instanceof \UnexpectedValueException && !$e instanceof \PharException) {
                throw $e;
            }
            unlink($tempFilename);
            $output->writeln('<error>The download is corrupt ('.$e->getMessage().').</error>');
            $output->writeln('<error>Please re-run the self-update command to try again.</error>');

            return 1;
        }
    }
}
<?php

/*
 * This file is part of the SymfonyInsight package.
 *
 * (c) Symfony <support@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace SensioLabs\Insight\Cli\Command;

use SensioLabs\Insight\Cli\Helper\DescriptorHelper;
use SensioLabs\Insight\Sdk\Api;
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\Style\SymfonyStyle;

class AnalyzeCommand extends Command implements NeedConfigurationInterface
{
    protected function configure(): void
    {
        $this
            ->setName('analyze')
            ->addArgument('project-uuid', InputArgument::REQUIRED)
            ->addOption('format', null, InputOption::VALUE_REQUIRED, 'To output in other formats', 'txt')
            ->addOption('reference', null, InputOption::VALUE_REQUIRED, 'The git reference to analyze')
            ->addOption('branch', null, InputOption::VALUE_REQUIRED, 'The analysis current branch')
            ->addOption('show-ignored-violations', null, InputOption::VALUE_NONE, 'Show ignored violations')
            ->addOption('poll-period', null, InputOption::VALUE_REQUIRED, 'How regularly should the analysis status be checked in seconds', 30)
            ->addOption('fail-condition', null, InputOption::VALUE_REQUIRED, '')
            ->addOption('no-wait', null, InputOption::VALUE_NONE, 'Do not wait for analysis result')
            ->setDescription('Analyze a project')
        ;
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $projectUuid = $input->getArgument('project-uuid');

        $pollPeriod = (int) $input->getOption('poll-period');
        if ($pollPeriod < 30) {
            $pollPeriod = 30;
        }

        /** @var Api $api */
        $api = $this->getApplication()->getApi();

        if (false !== strpos($api->getBaseUrl(), '.sensiolabs.com')) {
            $io = new SymfonyStyle($input, $output);
            $io->warning('You are using the legacy URL of SymfonyInsight which may stop working in the future. You should reconfigure this tool by running the "configure" command and use "https://insight.symfony.com" as endpoint.');
        }

        $analysis = $api->analyze($projectUuid, $input->getOption('reference'), $input->getOption('branch'));

        $chars = ['-', '\\', '|', '/'];
        $noAnsiStatus = 'Analysis running';
        $output->getErrorOutput()->writeln($noAnsiStatus);

        if ($input->getOption('no-wait')) {
            $output->writeln('The analysis is launched. Please check the result in you SymfonyInsight notifications.');

            return 0;
        }

        $position = 1;

        while (true) {
            // Check the status every poll-period * 5 loops (5 * 200ms = 1s)
            if (0 === $position % (5 * $pollPeriod)) {
                $analysis = $api->getAnalysisStatus($projectUuid, $analysis->getNumber());
            }

            if ('txt' === $input->getOption('format')) {
                if (!$output->isDecorated()) {
                    if ($noAnsiStatus !== $analysis->getStatusMessage()) {
                        $output->writeln($noAnsiStatus = $analysis->getStatusMessage());
                    }
                } else {
                    $output->write(sprintf("%s %-80s\r", $chars[$position % 4], $analysis->getStatusMessage()));
                }
            }

            if ($analysis->isFinished()) {
                break;
            }

            usleep(200000);

            ++$position;
        }

        $analysis = $api->getAnalysis($projectUuid, $analysis->getNumber());
        if ($analysis->isFailed()) {
            $output->writeln(sprintf('There was an error: "%s"', $analysis->getFailureMessage()));

            return 1;
        }

        $helper = new DescriptorHelper($api->getSerializer());
        $helper->describe($output, $analysis, $input->getOption('format'), $input->getOption('show-ignored-violations'));

        if ('txt' === $input->getOption('format') && OutputInterface::VERBOSITY_VERBOSE > $output->getVerbosity()) {
            $output->writeln('');
            $output->writeln(sprintf('Run <comment>%s %s %s -v</comment> to get the full report', $_SERVER['PHP_SELF'], 'analysis', $projectUuid));
        }

        if (!$expr = $input->getOption('fail-condition')) {
            return 0;
        }

        return $this->getHelperSet()->get('fail_condition')->evaluate($analysis, $expr);
    }
}
<?php

/*
 * This file is part of the SymfonyInsight package.
 *
 * (c) Symfony <support@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace SensioLabs\Insight\Cli\Command;

use SensioLabs\Insight\Cli\Helper\DescriptorHelper;
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;

class AnalysisCommand extends Command implements NeedConfigurationInterface
{
    protected function configure(): void
    {
        $this
            ->setName('analysis')
            ->addArgument('project-uuid', InputArgument::REQUIRED)
            ->addOption('format', null, InputOption::VALUE_REQUIRED, 'To output in other formats', 'txt')
            ->addOption('fail-condition', null, InputOption::VALUE_REQUIRED, '')
            ->addOption('show-ignored-violations', null, InputOption::VALUE_NONE, 'Show ignored violations')
            ->setDescription('Show the last project analysis')
        ;
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $api = $this->getApplication()->getApi();
        $analysis = $api->getProject($input->getArgument('project-uuid'))->getLastAnalysis();

        if (!$analysis) {
            $output->writeln('<error>There are no analyses</error>');

            return 1;
        }

        $helper = new DescriptorHelper($api->getSerializer());
        $helper->describe($output, $analysis, $input->getOption('format'), $input->getOption('show-ignored-violations'));

        if ('txt' === $input->getOption('format') && OutputInterface::VERBOSITY_VERBOSE > $output->getVerbosity()) {
            $output->writeln('');
            $output->writeln('Re-run this command with <comment>-v</comment> option to get the full report');
        }

        if (!$expr = $input->getOption('fail-condition')) {
            return 1;
        }

        return $this->getHelperSet()->get('fail_condition')->evaluate($analysis, $expr);
    }
}
<?php

/*
 * This file is part of the SymfonyInsight package.
 *
 * (c) Symfony <support@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace SensioLabs\Insight\Cli\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class ConfigureCommand extends Command
{
    protected function configure(): void
    {
        $this
            ->setName('configure')
            ->setDescription('(Re-)Configure your credentials.')
        ;
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $this->getHelperSet()->get('configuration')->updateConfigurationManually($input, $output);

        return 0;
    }
}
<?php

/*
 * This file is part of the SymfonyInsight package.
 *
 * (c) Symfony <support@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace SensioLabs\Insight\Cli\Command;

interface NeedConfigurationInterface
{
}
<?php

/*
 * This file is part of the SymfonyInsight package.
 *
 * (c) Symfony <support@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace SensioLabs\Insight\Cli\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class ProjectsCommand extends Command implements NeedConfigurationInterface
{
    protected function configure(): void
    {
        $this
            ->setName('projects')
            ->setDescription('List all your projects')
        ;
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $api = $this->getApplication()->getApi();

        $projectsResource = $api->getProjects();
        $projects = $projectsResource->getProjects();
        $nbPage = ceil($projectsResource->getTotal() / 10);
        $page = 1;
        while ($page < $nbPage) {
            ++$page;
            $projects = array_merge($projects, $api->getProjects($page)->getProjects());
        }

        if (!$projects) {
            $output->writeln('There are no projects');
        }

        $rows = [];
        foreach ($projects as $project) {
            if ($project->getLastAnalysis()) {
                $grade = $project->getLastAnalysis()->getGrade();
            } else {
                $grade = 'This project has no analyses';
            }
            $rows[] = [$project->getName(), $project->getUuid(), $grade];
        }

        $table = new Table($output);
        $table->setHeaders(['name', 'uuid', 'grade'])
            ->setRows($rows)
            ->render()
        ;

        return 0;
    }
}
<?php

/*
 * This file is part of the SymfonyInsight package.
 *
 * (c) Symfony <support@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace SensioLabs\Insight\Cli\Helper;

use SensioLabs\Insight\Sdk\Model\Analysis;
use Symfony\Component\Console\Helper\Helper;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;

class FailConditionHelper extends Helper
{
    private $el;

    public function __construct()
    {
        $this->el = new ExpressionLanguage();
    }

    public function evaluate(Analysis $analysis, $expr)
    {
        $analysisData = [
            'grade' => $analysis->getGrade(),
            'nbViolations' => 0,
            'remediationCost' => $analysis->getRemediationCost(),
        ];

        $counts = [
            // Category
            'architecture' => 0,
            'bugrisk' => 0,
            'codestyle' => 0,
            'deadcode' => 0,
            'performance' => 0,
            'readability' => 0,
            'security' => 0,

            // Severity
            'critical' => 0,
            'major' => 0,
            'minor' => 0,
            'info' => 0,
        ];

        $violations = $analysis->getViolations() ?: [];

        foreach ($violations as $violation) {
            ++$counts[$violation->getCategory()];
            ++$counts[$violation->getSeverity()];
            ++$analysisData['nbViolations'];
        }

        $vars = [
            'analysis' => (object) $analysisData,
            'counts' => (object) $counts,
        ];

        if ($this->el->evaluate($expr, $vars)) {
            return 70;
        }

        return 0;
    }

    public function getName(): string
    {
        return 'fail_condition';
    }
}
<?php

/*
 * This file is part of the SymfonyInsight package.
 *
 * (c) Symfony <support@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace SensioLabs\Insight\Cli\Helper;

use JMS\Serializer\Serializer;
use SensioLabs\Insight\Cli\Descriptor\AbstractDescriptor;
use SensioLabs\Insight\Cli\Descriptor\JsonDescriptor;
use SensioLabs\Insight\Cli\Descriptor\PmdDescriptor;
use SensioLabs\Insight\Cli\Descriptor\TextDescriptor;
use SensioLabs\Insight\Cli\Descriptor\XmlDescriptor;
use Symfony\Component\Console\Helper\Helper;
use Symfony\Component\Console\Output\OutputInterface;

class DescriptorHelper extends Helper
{
    private $descriptors = [];

    public function __construct(Serializer $serializer)
    {
        $this
            ->register('json', new JsonDescriptor($serializer))
            ->register('pmd', new PmdDescriptor())
            ->register('txt', new TextDescriptor())
            ->register('xml', new XmlDescriptor($serializer))
        ;
    }

    public function describe(OutputInterface $output, $object, $format = null, $showIgnoredViolation = false)
    {
        $options = [
            'raw_text' => false,
            'format' => $format ?: 'txt',
            'output' => $output,
            'show_ignored_violations' => $showIgnoredViolation,
        ];
        $options['type'] = 'txt' === $options['format'] ? OutputInterface::OUTPUT_NORMAL : OutputInterface::OUTPUT_RAW;

        if (!isset($this->descriptors[$options['format']])) {
            throw new \InvalidArgumentException(sprintf('Unsupported format "%s".', $options['format']));
        }

        $this->descriptors[$options['format']]->describe($object, $options);
    }

    public function register($format, AbstractDescriptor $descriptor)
    {
        $this->descriptors[$format] = $descriptor;

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function getName(): string
    {
        return 'descriptor';
    }
}
<?php

namespace SensioLabs\Insight\Cli\Helper;

use SensioLabs\Insight\Cli\Configuration;
use Symfony\Component\Console\Helper\Helper;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Style\SymfonyStyle;

class ConfigurationHelper extends Helper
{
    private $apiEndpoint;

    public function __construct(string $apiEndpoint)
    {
        $this->apiEndpoint = $apiEndpoint;
    }

    public function updateConfigurationManually(InputInterface $input, OutputInterface $output)
    {
        $configuration = new Configuration();

        $userUuid = $input->getOption('user-uuid') ?: $configuration->getUserUuid();
        $apiToken = $input->getOption('api-token') ?: $configuration->getApiToken();

        // Avoid saving again a legacy URL
        $defaultEndpoint = $configuration->getApiEndpoint();
        if (false !== strpos($defaultEndpoint, '.sensiolabs.com')) {
            $defaultEndpoint = null;
        }

        $apiEndpoint = $input->getOption('api-endpoint') ?: $defaultEndpoint;

        $configuration->setUserUuid($this->askValue($input, $output, 'User Uuid', $userUuid));
        $configuration->setApiToken($this->askValue($input, $output, 'Api Token', $apiToken));
        $configuration->setApiEndpoint($this->askValue($input, $output, 'Api Endpoint', $apiEndpoint ?: $this->apiEndpoint));

        if (false !== strpos($configuration->getApiEndpoint(), '.sensiolabs.com')) {
            $io = new SymfonyStyle($input, $output);
            $io->warning('You are using the legacy URL of SymfonyInsight which may stop working in the future. You should reconfigure this tool by running the "configure" command and use "https://insight.symfony.com" as endpoint.');
        }

        $this->saveConfiguration($input, $output, $configuration);
    }

    public function getConfiguration(InputInterface $input, OutputInterface $output)
    {
        $previousConfiguration = new Configuration();
        $configuration = clone $previousConfiguration;

        $this->resolveValue($input, $output, $configuration, 'User Uuid', null);
        $this->resolveValue($input, $output, $configuration, 'Api Token', null);
        $this->resolveValue($input, $output, $configuration, 'Api Endpoint', $this->apiEndpoint);

        if (!$configuration->equals($previousConfiguration)) {
            $this->saveConfiguration($input, $output, $configuration);
        }

        return $configuration;
    }

    public function getName(): string
    {
        return 'configuration';
    }

    private function resolveValue(InputInterface $input, OutputInterface $output, Configuration $configuration, $varName, $default = null)
    {
        $configurationProperty = str_replace(' ', '', $varName);

        $value = $this->getValue($input, $varName);

        if (!$value) {
            $value = $configuration->{'get'.$configurationProperty}();
        }
        if (!$value) {
            $value = $default;
        }
        if (!$value && $input->isInteractive()) {
            $value = $this->askValue($input, $output, $varName);
        }
        if (!$value) {
            throw new \InvalidArgumentException(sprintf('You should provide your %s.', $varName));
        }
        $configuration->{'set'.$configurationProperty}($value);
    }

    private function getValue(InputInterface $input, $varName)
    {
        $envVarName = sprintf('INSIGHT_%s', str_replace(' ', '_', strtoupper($varName)));
        if ($value = getenv($envVarName)) {
            return $value;
        }

        $cliVarName = sprintf('--%s', str_replace(' ', '-', strtolower($varName)));

        return $input->getParameterOption($cliVarName);
    }

    private function askValue(InputInterface $input, OutputInterface $output, $varname, $default = null)
    {
        $validator = static function ($v) use ($varname) {
            if (!$v) {
                throw new \InvalidArgumentException(sprintf('Your must provide a %s!', $varname));
            }

            return $v;
        };

        if (!$input->isInteractive()) {
            return $validator($default);
        }

        $question = new Question(
            $default ? sprintf('What is your %s? [%s] ', $varname, $default) : sprintf('What is your %s? ', $varname),
            $default
        );

        $question->setValidator($validator);

        return $this->getHelperSet()->get('question')->ask($input, $output, $question);
    }

    private function saveConfiguration(InputInterface $input, OutputInterface $output, Configuration $configuration)
    {
        if (!$input->isInteractive()) {
            $configuration->save();

            return;
        }

        $question = new ConfirmationQuestion('Do you want to save this new configuration? [Y/n] ');

        if ($this->getHelperSet()->get('question')->ask($input, $output, $question)) {
            $configuration->save();
        }
    }
}
<?php

/*
 * This file is part of the SymfonyInsight package.
 *
 * (c) Symfony <support@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace SensioLabs\Insight\Cli;

use Monolog\Handler\StreamHandler;
use Monolog\Logger;
use SensioLabs\Insight\Cli\Command as LocalCommand;
use SensioLabs\Insight\Cli\Helper\ConfigurationHelper;
use SensioLabs\Insight\Cli\Helper\FailConditionHelper;
use SensioLabs\Insight\Sdk\Api;
use Symfony\Component\Console\Application as SymfonyApplication;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

class Application extends SymfonyApplication
{
    public const APPLICATION_NAME = 'SymfonyInsight CLI';

    public const APPLICATION_VERSION = '1.7.4';

    private $api;
    private $apiConfig;
    private $logFile;

    public function __construct()
    {
        $this->apiConfig = [
            'base_url' => Api::ENDPOINT,
        ];

        parent::__construct(static::APPLICATION_NAME, static::APPLICATION_VERSION);
    }

    public function getApi()
    {
        if ($this->api) {
            return $this->api;
        }

        $config = $this->apiConfig;
        if (\array_key_exists('api_endpoint', $config)) {
            $config['base_url'] = $config['api_endpoint'];
        }

        $this->api = new Api($config);

        if ($this->logFile) {
            if (!class_exists('Monolog\Logger')) {
                throw new \InvalidArgumentException('You must include monolog if you want to log (run "composer install --dev")');
            }
            $logger = new Logger('insight');
            $logger->pushHandler(new StreamHandler($this->logFile, Logger::DEBUG));

            $this->api->setLogger($logger);
        }

        return $this->api;
    }

    public function getLongVersion(): string
    {
        $version = parent::getLongVersion().' by <comment>Symfony</comment>';
        $commit = 'a72e27ec1564d717517142e598d7ecd21c12585f';

        if ('@'.'git-commit@' !== $commit) {
            $version .= ' ('.substr($commit, 0, 7).')';
        }

        return $version;
    }

    protected function getDefaultHelperSet(): HelperSet
    {
        $helperSet = parent::getDefaultHelperSet();

        $helperSet->set(new ConfigurationHelper(Api::ENDPOINT));
        $helperSet->set(new FailConditionHelper());

        return $helperSet;
    }

    protected function getDefaultInputDefinition(): InputDefinition
    {
        $definition = parent::getDefaultInputDefinition();

        $definition->addOption(new InputOption('api-token', null, InputOption::VALUE_REQUIRED, 'Your api token.'));
        $definition->addOption(new InputOption('user-uuid', null, InputOption::VALUE_REQUIRED, 'Your user uuid.'));
        $definition->addOption(new InputOption('api-endpoint', null, InputOption::VALUE_REQUIRED, 'The api endpoint.'));
        $definition->addOption(new InputOption('log', null, InputOption::VALUE_OPTIONAL, 'Add some log capability. Specify a log file if you want to change the log location.'));

        return $definition;
    }

    protected function getDefaultCommands(): array
    {
        $defaultCommands = parent::getDefaultCommands();

        $defaultCommands[] = new LocalCommand\AnalysisCommand();
        $defaultCommands[] = new LocalCommand\AnalyzeCommand();
        $defaultCommands[] = new LocalCommand\ConfigureCommand();
        $defaultCommands[] = new LocalCommand\ProjectsCommand();
        $defaultCommands[] = new LocalCommand\SelfUpdateCommand();

        return $defaultCommands;
    }

    protected function doRunCommand(Command $command, InputInterface $input, OutputInterface $output): int
    {
        if (!$command instanceof LocalCommand\NeedConfigurationInterface) {
            return parent::doRunCommand($command, $input, $output);
        }

        $configuration = $this->getHelperSet()->get('configuration')->getConfiguration($input, $output);
        $this->apiConfig = array_merge($this->apiConfig, $configuration->toArray());

        if (false !== $input->getParameterOption('--log')) {
            $this->logFile = $input->getParameterOption('--log') ?: getcwd().'/insight.log';
        }

        return parent::doRunCommand($command, $input, $output);
    }
}
<?php

/*
 * This file is part of the SymfonyInsight package.
 *
 * (c) Symfony <support@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace SensioLabs\Insight\Cli\Descriptor;

use SensioLabs\Insight\Sdk\Model\Analysis;
use Symfony\Component\Console\Output\OutputInterface;

class TextDescriptor extends AbstractDescriptor
{
    protected function describeAnalysis(Analysis $analysis, array $options = [])
    {
        $output = $options['output'];
        if (OutputInterface::VERBOSITY_VERY_VERBOSE <= $output->getVerbosity()) {
            $output->write(sprintf('Began at: <comment>%s</comment>', $analysis->getBeginAt()->format('Y-m-d h:i:s')));
        }
        if (!$analysis->isFinished()) {
            if (OutputInterface::VERBOSITY_VERY_VERBOSE <= $output->getVerbosity()) {
                $output->writeln('');
            }
            $output->writeln('The analysis is not finished yet.');

            return;
        }
        if (OutputInterface::VERBOSITY_VERY_VERBOSE <= $output->getVerbosity()) {
            $output->write(sprintf(' Ended at: <comment>%s</comment>', $analysis->getEndAt()->format('Y-m-d h:i:s')));
            $output->writeln(sprintf(' Real duration: <comment>%s</comment>.', $analysis->getEndAt()->format('Y-m-d h:i:s')));
            $output->writeln('');
        }
        $output->write(sprintf(
            'The project has <comment>%d violations</comment>, it got the <comment>%s grade</comment>.',
            $analysis->getNbViolations(),
            $analysis->getGrade()
        ));

        $grades = $analysis->getGrades();
        $bestGrade = end($grades);
        if ($bestGrade == $analysis->getGrade()) {
            $output->writeln('');

            return;
        }

        $output->writeln(sprintf(
            ' <comment>%d hours</comment> to get the <comment>%s grade</comment> and %d hours to get the %s grade',
            $analysis->getRemediationCostForNextGrade(),
            $analysis->getNextGrade(),
            $analysis->getRemediationCost(),
            $bestGrade
        ));
        $output->writeln('');

        if (OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity() && $analysis->getViolations()) {
            $template = <<<EOL
Resource: <comment>{{ resource }}:{{ line }}</comment>
Category: <comment>{{ category }}</comment> Severity: <comment>{{ severity }}</comment>
Title:    <comment>{{ title }}</comment>{{ ignored }}
Message:  <comment>{{ message }}</comment>

EOL;
            foreach ($analysis->getViolations() as $violation) {
                $output->writeln(strtr($template, [
                    '{{ resource }}' => $violation->getResource(),
                    '{{ line }}' => $violation->getLine(),
                    '{{ category }}' => $violation->getCategory(),
                    '{{ severity }}' => $violation->getSeverity(),
                    '{{ title }}' => $violation->getTitle(),
                    '{{ message }}' => $violation->getMessage(),
                    '{{ ignored }}' => $violation->isIgnored() ? ' (ignored)' : null,
                ]));
            }
        }

        foreach ($analysis->getLinks() as $link) {
            if ('self' == $link->getRel() && 'text/html' == $link->getType()) {
                $output->writeln(sprintf('You can get the full report at <info>%s</info>', $link->getHref()));

                break;
            }
        }
    }
}
<?php

/*
 * This file is part of the SymfonyInsight package.
 *
 * (c) Symfony <support@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace SensioLabs\Insight\Cli\Descriptor;

use JMS\Serializer\Serializer;
use SensioLabs\Insight\Sdk\Model\Analysis;

class XmlDescriptor extends AbstractDescriptor
{
    private $serializer;

    public function __construct(Serializer $serializer)
    {
        $this->serializer = $serializer;
    }

    protected function describeAnalysis(Analysis $analysis, array $options = [])
    {
        $output = $options['output'];
        $output->writeln($this->serializer->serialize($analysis, 'xml'));
    }
}
<?php

/*
 * This file is part of the SymfonyInsight package.
 *
 * (c) Symfony <support@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace SensioLabs\Insight\Cli\Descriptor;

use JMS\Serializer\Serializer;
use SensioLabs\Insight\Sdk\Model\Analysis;

class JsonDescriptor extends AbstractDescriptor
{
    private $serializer;

    public function __construct(Serializer $serializer)
    {
        $this->serializer = $serializer;
    }

    protected function describeAnalysis(Analysis $analysis, array $options = [])
    {
        $output = $options['output'];
        $output->writeln($this->serializer->serialize($analysis, 'json'));
    }
}
<?php

namespace SensioLabs\Insight\Cli\Descriptor;

use SensioLabs\Insight\Sdk\Model\Analysis;
use SensioLabs\Insight\Sdk\Model\Violation;

class PmdDescriptor extends AbstractDescriptor
{
    public const PHPMD_PRIORITY_HIGH = 1;

    public const PHPMD_PRIORITY_MEDIUM_HIGH = 2;

    public const PHPMD_PRIORITY_MEDIUM = 3;

    public const PHPMD_PRIORITY_MEDIUM_LOW = 4;

    public const PHPMD_PRIORITY_LOW = 5;

    protected function describeAnalysis(Analysis $analysis, array $options = [])
    {
        $output = $options['output'];

        $xml = new \DOMDocument('1.0', 'UTF-8');
        $xpath = new \DOMXPath($xml);

        $xml->formatOutput = true;
        $xml->preserveWhiteSpace = true;

        $pmd = $xml->createElement('pmd');
        $pmd->setAttribute('timestamp', $analysis->getEndAt()->format('c'));

        $xml->appendChild($pmd);

        $violations = $analysis->getViolations();
        if ($violations) {
            foreach ($violations as $violation) {
                /*
                 * @var $violation \SensioLabs\Insight\Sdk\Model\Violation
                 */
                $filename = $violation->getResource();

                $nodes = $xpath->query(sprintf('//file[@name="%s"]', $filename));

                if ($nodes->length > 0) {
                    $node = $nodes->item(0);
                } else {
                    $node = $xml->createElement('file');
                    $node->setAttribute('name', $filename);

                    $pmd->appendChild($node);
                }

                $violationNode = $xml->createElement('violation', $violation->getMessage());
                $node->appendChild($violationNode);

                $violationNode->setAttribute('beginline', $violation->getLine());
                $violationNode->setAttribute('endline', $violation->getLine());
                $violationNode->setAttribute('rule', $violation->getTitle());
                $violationNode->setAttribute('ruleset', $violation->getCategory());
                $violationNode->setAttribute('priority', $this->getPriority($violation));
            }
        }

        $output->writeln($xml->saveXML());
    }

    private function getPriority(Violation $violation)
    {
        switch ($violation->getSeverity()) {
            case 'critical':
                return self::PHPMD_PRIORITY_HIGH;

            case 'major':
                return self::PHPMD_PRIORITY_MEDIUM;

            default:
                return self::PHPMD_PRIORITY_LOW;
        }
    }
}
<?php

/*
 * This file is part of the SymfonyInsight package.
 *
 * (c) Symfony <support@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace SensioLabs\Insight\Cli\Descriptor;

use SensioLabs\Insight\Sdk\Model\Analysis;
use SensioLabs\Insight\Sdk\Model\Violation;

abstract class AbstractDescriptor
{
    public function describe($object, array $options = [])
    {
        if ($object instanceof Analysis) {
            if (!$options['show_ignored_violations'] && $object->getViolations()) {
                $object->getViolations()->filter(function (Violation $v) {
                    return !$v->isIgnored();
                });
            }

            return $this->describeAnalysis($object, $options);
        }

        throw new \InvalidArgumentException(sprintf('Object of type "%s" is not describable.', \get_class($object)));
    }

    abstract protected function describeAnalysis(Analysis $argument, array $options = []);
}
Ϩ=`oO
   GBMB