*/ protected array $fieldValues; protected Source $source; /** * Used e.g. for Pods Blocks * @var array|null */ protected ?array $localData; public function __construct( DataVendors $dataVendors, Twig $twig, string $markup, ViewData $viewData, Source $source, FieldMarkup $fieldMarkup, int $pageId, string $classes = '' ) { parent::__construct($twig, $viewData, $markup, $classes); $this->viewData = $viewData; $this->dataVendors = $dataVendors; $this->source = $source; $this->pageId = $pageId; $this->fieldMarkup = $fieldMarkup; $this->fieldValues = []; $this->localData = null; } /** * @param mixed $fieldValue * @return array */ protected function getTwigArgsForVariable( ItemData $item, FieldMetaInterface $fieldMeta, Source $source, $fieldValue, bool $isForValidation ): array { $twigArgs = $this->fieldMarkup->getFieldTwigArgs( $this->viewData, $item, $item->field, $fieldMeta, $source, $fieldValue, $isForValidation ); return [ $item->field->getTwigFieldId() => array_merge( $twigArgs, [ 'label' => $item->field->getLabelTranslation(), ] ), ]; } protected function renderTwig(bool $isForValidation = false): void { if (!$this->viewData->isRenderWhenEmpty && !$isForValidation) { $isEmpty = true; foreach ($this->twigVariables as $twigVariableName => $twigVariableValue) { $isEmptyValue = is_array($twigVariableValue) && key_exists('value', $twigVariableValue) && in_array($twigVariableValue['value'], ['', [], null,], true); // ignore the system variables if (in_array($twigVariableValue, ['', [], null,], true) || '_view' === $twigVariableName || $isEmptyValue) { continue; } $isEmpty = false; break; } if ($isEmpty) { $this->html = ''; // do not render, as Twig saves template in cache // so if it's first, then it'll use the empty one for all next calls of this view return; } } $this->html = $this->twig->render( $this->viewData->getUniqueId(), $this->html, $this->twigVariables, $isForValidation ); } /** * @return mixed */ protected function getFieldValue(ItemData $itemData, FieldMetaInterface $fieldMeta) { return $this->dataVendors->getFieldValue( $itemData->field, $fieldMeta, $this->source, false, $this->localData ); } /** * @param array $customArguments */ protected function setTwigVariables(bool $isForValidation = false, array $customArguments = []): void { $objectId = !$isForValidation ? strval($this->source->getId()) : '0'; $this->fieldValues = []; // internal variables $this->twigVariables = [ '_view' => [ 'classes' => $this->getClasses(), 'id' => $this->viewData->getMarkupId(), // replace for others: term_6 to term-6 'object_id' => str_replace('_', '-', $objectId), ], ]; foreach ($this->viewData->items as $item) { $fieldMeta = $item->field->getFieldMeta(); $fieldValue = false === $isForValidation ? $this->getFieldValue($item, $fieldMeta) : null; $isEmptyValue = in_array($fieldValue, ['', [], null,], true); // 1. default value from our plugin. Note: custom field types don't support default values if ($isEmptyValue && !in_array($fieldMeta->getVendorName(), [WpDataVendor::NAME, WooDataVendor::NAME,], true)) { $fieldValue = $item->field->defaultValue; } $isEmptyValue = in_array($fieldValue, ['', [], null,], true); // 2. default value from ACF. Note: custom field types don't support default values if ($isEmptyValue && !in_array($fieldMeta->getVendorName(), [WpDataVendor::NAME, WooDataVendor::NAME,], true)) { $fieldValue = $fieldMeta->getDefaultValue(); } $this->fieldValues[$item->field->id] = $fieldValue; $this->twigVariables = array_merge( $this->twigVariables, $this->getTwigArgsForVariable( $item, $fieldMeta, $this->source, $fieldValue, $isForValidation ) ); } } /** * @return array */ protected function getArrayFieldNamesFromMarkup(string $markup): array { preg_match_all( '/{% for [a-z0-9_]+ in ([a-z0-9_]+)\.value %}/', $markup, $arraysInfo, PREG_OFFSET_CAPTURE | PREG_SET_ORDER ); $arrayFieldNames = []; foreach ($arraysInfo as $arrayInfo) { $charPosition = $arrayInfo[0][1] ?? -1; $arrayName = $arrayInfo[1][0] ?? ''; if ('' === $arrayName || -1 === $charPosition) { continue; } $lineNumber = substr_count(mb_substr($markup, 0, $charPosition), PHP_EOL) + 1; $arrayFieldNames[$arrayName] = $lineNumber; } return $arrayFieldNames; } /** * @param array $variables * @return string[] */ protected function getArrayFieldNames(array $variables): array { return array_keys( array_filter($variables, function ($fieldValue) { return is_array($fieldValue) && key_exists('value', $fieldValue) && is_array($fieldValue['value']); }) ); } /** * @param string[] $canonicalArrayFieldNames * @param array $presentArrayFields * @return string */ protected function getArrayExpectationErrors(array $canonicalArrayFieldNames, array $presentArrayFields): string { $unexpectedArrays = array_diff(array_keys($presentArrayFields), $canonicalArrayFieldNames); $errors = ''; foreach ($unexpectedArrays as $unexpectedArray) { $lineNumber = $presentArrayFields[$unexpectedArray]; $lineNumber = is_numeric($lineNumber) ? (int)$lineNumber : 0; $errors .= sprintf( __('The "%s" field is incorrectly expected to be an array. Line %d', 'acf-views'), $unexpectedArray, $lineNumber ); } return $errors; } /** * @param string[] $canonicalArrayFieldNames * @param array $presentArrayFields */ protected function getMissingArrayErrors( array $canonicalArrayFieldNames, array $presentArrayFields, string $customMarkup ): string { $missingArrays = array_diff($canonicalArrayFieldNames, array_keys($presentArrayFields)); $errors = ''; foreach ($missingArrays as $missingArray) { $fieldPosition = strpos($customMarkup, $missingArray . '.value'); // skip error if the field is not used in the markup (e.g. newly added) // our goal is only to validate the existing markup if (false === $fieldPosition) { continue; } $lineNumber = substr_count(mb_substr($customMarkup, 0, $fieldPosition), PHP_EOL) + 1; $errors .= sprintf( __('The "%s" field is incorrectly expected to be a string. Line %d', 'acf-views'), $missingArray, $lineNumber ); } return $errors; } /** * @param array $customArguments */ public function insertFields(bool $isMinifyMarkup = true, array $customArguments = []): void { if ($isMinifyMarkup) { // remove special symbols that used in the markup for a preview // exactly here, before the fields are inserted, to avoid affecting them $this->html = str_replace(["\t", "\n", "\r"], '', $this->html); } $this->setTwigVariables(false, $customArguments); $this->renderTwig(); } public function getHTML(): string { return $this->html; } public function getViewData(): ViewData { return $this->viewData; } public function getMarkupValidationError(): string { $markupValidationError = parent::getMarkupValidationError(); $customMarkup = trim($this->viewData->customMarkup); if ('' !== $markupValidationError || '' === $customMarkup) { return $markupValidationError; } $canonicalArrayFieldNames = $this->getArrayFieldNames($this->twigVariables); $presentArrayFields = $this->getArrayFieldNamesFromMarkup($customMarkup); $markupValidationError .= $this->getArrayExpectationErrors($canonicalArrayFieldNames, $presentArrayFields); $markupValidationError .= $this->getMissingArrayErrors( $canonicalArrayFieldNames, $presentArrayFields, $customMarkup ); return $markupValidationError; } /** * @param array|null $localData */ public function setLocalData(?array $localData): void { $this->localData = $localData; } }