vendor/ezsystems/ezplatform-kernel/eZ/Publish/Core/Repository/Helper/NameSchemaService.php line 41

Open in your IDE?
  1. <?php
  2. /**
  3.  * @copyright Copyright (C) Ibexa AS. All rights reserved.
  4.  * @license For full copyright and license information view LICENSE file distributed with this source code.
  5.  */
  6. namespace eZ\Publish\Core\Repository\Helper;
  7. use eZ\Publish\Core\Base\Exceptions\InvalidArgumentType;
  8. use eZ\Publish\Core\FieldType\FieldTypeRegistry;
  9. use eZ\Publish\Core\Repository\Mapper\ContentTypeDomainMapper;
  10. use eZ\Publish\SPI\Persistence\Content\Type\Handler as ContentTypeHandler;
  11. use eZ\Publish\API\Repository\Values\Content\Content;
  12. use eZ\Publish\API\Repository\Values\ContentType\ContentType;
  13. use eZ\Publish\SPI\Persistence\Content\Type as SPIContentType;
  14. /**
  15.  * NameSchemaService is internal service for resolving content name and url alias patterns.
  16.  * This code supports content name pattern groups.
  17.  *
  18.  * Syntax:
  19.  * <code>
  20.  * &lt;attribute_identifier&gt;
  21.  * &lt;attribute_identifier&gt; &lt;2nd-identifier&gt;
  22.  * User text &lt;attribute_identifier&gt;|(&lt;2nd-identifier&gt;&lt;3rd-identifier&gt;)
  23.  * </code>
  24.  *
  25.  * Example:
  26.  * <code>
  27.  * &lt;nickname|(&lt;firstname&gt; &lt;lastname&gt;)&gt;
  28.  * </code>
  29.  *
  30.  * Tokens are looked up from left to right. If a match is found for the
  31.  * leftmost token, the 2nd token will not be used. Tokens are representations
  32.  * of fields. So a match means that that the current field has data.
  33.  *
  34.  * Tokens are the field definition identifiers which are used in the class edit-interface.
  35.  *
  36.  * @internal Meant for internal use by Repository.
  37.  */
  38. class NameSchemaService
  39. {
  40.     /**
  41.      * The string to use to signify group tokens.
  42.      *
  43.      * @var string
  44.      */
  45.     const META_STRING 'EZMETAGROUP_';
  46.     /** @var \eZ\Publish\SPI\Persistence\Content\Type\Handler */
  47.     protected $contentTypeHandler;
  48.     /** @var \eZ\Publish\Core\Repository\Mapper\ContentTypeDomainMapper */
  49.     protected $contentTypeDomainMapper;
  50.     /** @var \eZ\Publish\Core\Repository\Helper\FieldTypeRegistry */
  51.     protected $fieldTypeRegistry;
  52.     /** @var array */
  53.     protected $settings;
  54.     /**
  55.      * Constructs a object to resolve $nameSchema with $contentVersion fields values.
  56.      *
  57.      * @param \eZ\Publish\SPI\Persistence\Content\Type\Handler $contentTypeHandler
  58.      * @param \eZ\Publish\Core\Repository\Mapper\ContentTypeDomainMapper $contentTypeDomainMapper
  59.      * @param \eZ\Publish\Core\FieldType\FieldTypeRegistry $fieldTypeRegistry
  60.      * @param array $settings
  61.      */
  62.     public function __construct(
  63.         ContentTypeHandler $contentTypeHandler,
  64.         ContentTypeDomainMapper $contentTypeDomainMapper,
  65.         FieldTypeRegistry $fieldTypeRegistry,
  66.         array $settings = [])
  67.     {
  68.         $this->contentTypeHandler $contentTypeHandler;
  69.         $this->contentTypeDomainMapper $contentTypeDomainMapper;
  70.         $this->fieldTypeRegistry $fieldTypeRegistry;
  71.         // Union makes sure default settings are ignored if provided in argument
  72.         $this->settings $settings + [
  73.             'limit' => 150,
  74.             'sequence' => '...',
  75.         ];
  76.     }
  77.     /**
  78.      * Convenience method for resolving URL alias schema.
  79.      *
  80.      * @param \eZ\Publish\API\Repository\Values\Content\Content $content
  81.      * @param \eZ\Publish\API\Repository\Values\ContentType\ContentType|null $contentType
  82.      *
  83.      * @return array
  84.      */
  85.     public function resolveUrlAliasSchema(Content $contentContentType $contentType null)
  86.     {
  87.         if ($contentType === null) {
  88.             $contentType $this->contentTypeHandler->load($content->contentInfo->contentTypeId);
  89.         }
  90.         return $this->resolve(
  91.             strlen($contentType->urlAliasSchema) === $contentType->nameSchema $contentType->urlAliasSchema,
  92.             $contentType,
  93.             $content->fields,
  94.             $content->versionInfo->languageCodes
  95.         );
  96.     }
  97.     /**
  98.      * Convenience method for resolving name schema.
  99.      *
  100.      * @param \eZ\Publish\API\Repository\Values\Content\Content $content
  101.      * @param array $fieldMap
  102.      * @param array $languageCodes
  103.      * @param \eZ\Publish\API\Repository\Values\ContentType\ContentType|null $contentType
  104.      *
  105.      * @return array
  106.      */
  107.     public function resolveNameSchema(Content $content, array $fieldMap = [], array $languageCodes = [], ContentType $contentType null)
  108.     {
  109.         if ($contentType === null) {
  110.             $contentType $this->contentTypeHandler->load($content->contentInfo->contentTypeId);
  111.         }
  112.         $languageCodes $languageCodes ?: $content->versionInfo->languageCodes;
  113.         return $this->resolve(
  114.             $contentType->nameSchema,
  115.             $contentType,
  116.             $this->mergeFieldMap(
  117.                 $content,
  118.                 $fieldMap,
  119.                 $languageCodes
  120.             ),
  121.             $languageCodes
  122.         );
  123.     }
  124.     /**
  125.      * Convenience method for resolving name schema.
  126.      *
  127.      * @param \eZ\Publish\API\Repository\Values\Content\Content $content
  128.      * @param array $fieldMap
  129.      * @param array $languageCodes
  130.      *
  131.      * @return array
  132.      */
  133.     protected function mergeFieldMap(Content $content, array $fieldMap, array $languageCodes)
  134.     {
  135.         if (empty($fieldMap)) {
  136.             return $content->fields;
  137.         }
  138.         $mergedFieldMap = [];
  139.         foreach ($content->fields as $fieldIdentifier => $fieldLanguageMap) {
  140.             foreach ($languageCodes as $languageCode) {
  141.                 $mergedFieldMap[$fieldIdentifier][$languageCode] = isset($fieldMap[$fieldIdentifier][$languageCode])
  142.                     ? $fieldMap[$fieldIdentifier][$languageCode]
  143.                     : $fieldLanguageMap[$languageCode];
  144.             }
  145.         }
  146.         return $mergedFieldMap;
  147.     }
  148.     /**
  149.      * Returns the real name for a content name pattern.
  150.      *
  151.      * @param string $nameSchema
  152.      * @param \eZ\Publish\SPI\Persistence\Content\Type|\eZ\Publish\API\Repository\Values\ContentType\ContentType $contentType
  153.      * @param array $fieldMap
  154.      * @param array $languageCodes
  155.      *
  156.      * @return string[]
  157.      */
  158.     public function resolve($nameSchema$contentType, array $fieldMap, array $languageCodes)
  159.     {
  160.         list($filteredNameSchema$groupLookupTable) = $this->filterNameSchema($nameSchema);
  161.         $tokens $this->extractTokens($filteredNameSchema);
  162.         $schemaIdentifiers $this->getIdentifiers($nameSchema);
  163.         $names = [];
  164.         foreach ($languageCodes as $languageCode) {
  165.             // Fetch titles for language code
  166.             $titles $this->getFieldTitles($schemaIdentifiers$contentType$fieldMap$languageCode);
  167.             $name $filteredNameSchema;
  168.             // Replace tokens with real values
  169.             foreach ($tokens as $token) {
  170.                 $string $this->resolveToken($token$titles$groupLookupTable);
  171.                 $name str_replace($token$string$name);
  172.             }
  173.             // Make sure length is not longer then $limit unless it's 0
  174.             if ($this->settings['limit'] && mb_strlen($name) > $this->settings['limit']) {
  175.                 $name rtrim(mb_substr($name0$this->settings['limit'] - strlen($this->settings['sequence']))) . $this->settings['sequence'];
  176.             }
  177.             $names[$languageCode] = $name;
  178.         }
  179.         return $names;
  180.     }
  181.     /**
  182.      * Fetches the list of available Field identifiers in the token and returns
  183.      * an array of their current title value.
  184.      *
  185.      * @see \eZ\Publish\Core\Repository\FieldType::getName()
  186.      *
  187.      * @param string[] $schemaIdentifiers
  188.      * @param \eZ\Publish\SPI\Persistence\Content\Type|\eZ\Publish\API\Repository\Values\ContentType\ContentType $contentType
  189.      * @param array $fieldMap
  190.      * @param string $languageCode
  191.      *
  192.      * @throws \eZ\Publish\Core\Base\Exceptions\InvalidArgumentType
  193.      *
  194.      * @return string[] Key is the field identifier, value is the title value
  195.      */
  196.     protected function getFieldTitles(array $schemaIdentifiers$contentType, array $fieldMap$languageCode)
  197.     {
  198.         $fieldTitles = [];
  199.         foreach ($schemaIdentifiers as $fieldDefinitionIdentifier) {
  200.             if (isset($fieldMap[$fieldDefinitionIdentifier][$languageCode])) {
  201.                 if ($contentType instanceof SPIContentType) {
  202.                     $fieldDefinition null;
  203.                     foreach ($contentType->fieldDefinitions as $spiFieldDefinition) {
  204.                         if ($spiFieldDefinition->identifier === $fieldDefinitionIdentifier) {
  205.                             $fieldDefinition $this->contentTypeDomainMapper->buildFieldDefinitionDomainObject(
  206.                                 $spiFieldDefinition,
  207.                                 // This is probably not main language, but as we don't expose it, it's ok for now.
  208.                                 $languageCode
  209.                             );
  210.                             break;
  211.                         }
  212.                     }
  213.                     if ($fieldDefinition === null) {
  214.                         $fieldTitles[$fieldDefinitionIdentifier] = '';
  215.                         continue;
  216.                     }
  217.                 } elseif ($contentType instanceof ContentType) {
  218.                     $fieldDefinition $contentType->getFieldDefinition($fieldDefinitionIdentifier);
  219.                 } else {
  220.                     throw new InvalidArgumentType('$contentType''API or SPI variant of a Content Type');
  221.                 }
  222.                 $fieldTypeService $this->fieldTypeRegistry->getFieldType(
  223.                     $fieldDefinition->fieldTypeIdentifier
  224.                 );
  225.                 $fieldTitles[$fieldDefinitionIdentifier] = $fieldTypeService->getName(
  226.                     $fieldMap[$fieldDefinitionIdentifier][$languageCode],
  227.                     $fieldDefinition,
  228.                     $languageCode
  229.                 );
  230.             }
  231.         }
  232.         return $fieldTitles;
  233.     }
  234.     /**
  235.      * Extract all tokens from $namePattern.
  236.      *
  237.      * Example:
  238.      * <code>
  239.      * Text &lt;token&gt; more text ==&gt; &lt;token&gt;
  240.      * </code>
  241.      *
  242.      * @param string $nameSchema
  243.      *
  244.      * @return array
  245.      */
  246.     protected function extractTokens($nameSchema)
  247.     {
  248.         preg_match_all(
  249.             '|<([^>]+)>|U',
  250.             $nameSchema,
  251.             $tokenArray
  252.         );
  253.         return $tokenArray[0];
  254.     }
  255.     /**
  256.      * Looks up the value $token should be replaced with and returns this as
  257.      * a string. Meta strings denoting token groups are automatically
  258.      * inferred.
  259.      *
  260.      * @param string $token
  261.      * @param array $titles
  262.      * @param array $groupLookupTable
  263.      *
  264.      * @return string
  265.      */
  266.     protected function resolveToken($token$titles$groupLookupTable)
  267.     {
  268.         $replaceString '';
  269.         $tokenParts $this->tokenParts($token);
  270.         foreach ($tokenParts as $tokenPart) {
  271.             if ($this->isTokenGroup($tokenPart)) {
  272.                 $replaceString $groupLookupTable[$tokenPart];
  273.                 $groupTokenArray $this->extractTokens($replaceString);
  274.                 foreach ($groupTokenArray as $groupToken) {
  275.                     $replaceString str_replace(
  276.                         $groupToken,
  277.                         $this->resolveToken(
  278.                             $groupToken,
  279.                             $titles,
  280.                             $groupLookupTable
  281.                         ),
  282.                         $replaceString
  283.                     );
  284.                 }
  285.                 // We want to stop after the first matching token part / identifier is found
  286.                 // <id1|id2> if id1 has a value, id2 will not be used.
  287.                 // In this case id1 or id1 is a token group.
  288.                 break;
  289.             } else {
  290.                 if (array_key_exists($tokenPart$titles) && $titles[$tokenPart] !== '' && $titles[$tokenPart] !== null) {
  291.                     $replaceString $titles[$tokenPart];
  292.                     // We want to stop after the first matching token part / identifier is found
  293.                     // <id1|id2> if id1 has a value, id2 will not be used.
  294.                     break;
  295.                 }
  296.             }
  297.         }
  298.         return $replaceString;
  299.     }
  300.     /**
  301.      * Checks whether $identifier is a placeholder for a token group.
  302.      *
  303.      * @param string $identifier
  304.      *
  305.      * @return bool
  306.      */
  307.     protected function isTokenGroup($identifier)
  308.     {
  309.         if (strpos($identifierself::META_STRING) === false) {
  310.             return false;
  311.         }
  312.         return true;
  313.     }
  314.     /**
  315.      * Returns the different constituents of $token in an array.
  316.      * The normal case here is that the different identifiers within one token
  317.      * will be tokenized and returned.
  318.      *
  319.      * Example:
  320.      * <code>
  321.      * "&lt;title|text&gt;" ==&gt; array( 'title', 'text' )
  322.      * </code>
  323.      *
  324.      * @param string $token
  325.      *
  326.      * @return array
  327.      */
  328.     protected function tokenParts($token)
  329.     {
  330.         return preg_split('#\\W#'$token, -1PREG_SPLIT_NO_EMPTY);
  331.     }
  332.     /**
  333.      * Builds a lookup / translation table for groups in the $namePattern.
  334.      * The groups are referenced with a generated meta-token in the original
  335.      * name pattern.
  336.      *
  337.      * Returns intermediate name pattern where groups are replaced with meta-
  338.      * tokens.
  339.      *
  340.      * @param string $nameSchema
  341.      *
  342.      * @return string
  343.      */
  344.     protected function filterNameSchema($nameSchema)
  345.     {
  346.         $retNamePattern '';
  347.         $foundGroups preg_match_all('/[<|\\|](\\(.+\\))[\\||>]/U'$nameSchema$groupArray);
  348.         $groupLookupTable = [];
  349.         if ($foundGroups) {
  350.             $i 0;
  351.             foreach ($groupArray[1] as $group) {
  352.                 // Create meta-token for group
  353.                 $metaToken self::META_STRING $i;
  354.                 // Insert the group with its placeholder token
  355.                 $retNamePattern str_replace($group$metaToken$nameSchema);
  356.                 // Remove the pattern "(" ")" from the tokens
  357.                 $group str_replace(['('')'], ''$group);
  358.                 $groupLookupTable[$metaToken] = $group;
  359.                 ++$i;
  360.             }
  361.             $nameSchema $retNamePattern;
  362.         }
  363.         return [$nameSchema$groupLookupTable];
  364.     }
  365.     /**
  366.      * Returns all identifiers from all tokens in the name schema.
  367.      *
  368.      * @param string $schemaString
  369.      *
  370.      * @return array
  371.      */
  372.     protected function getIdentifiers($schemaString)
  373.     {
  374.         $allTokens '#<(.*)>#U';
  375.         $identifiers '#\\W#';
  376.         $tmpArray = [];
  377.         preg_match_all($allTokens$schemaString$matches);
  378.         foreach ($matches[1] as $match) {
  379.             $tmpArray[] = preg_split($identifiers$match, -1PREG_SPLIT_NO_EMPTY);
  380.         }
  381.         $retArray = [];
  382.         foreach ($tmpArray as $matchGroup) {
  383.             if (is_array($matchGroup)) {
  384.                 foreach ($matchGroup as $item) {
  385.                     $retArray[] = $item;
  386.                 }
  387.             } else {
  388.                 $retArray[] = $matchGroup;
  389.             }
  390.         }
  391.         return $retArray;
  392.     }
  393. }