/
www
/
wwwroot
/
alo88.autos
/
wp-content
/
plugins
/
wp-content-crawler
/
app
/
Objects
/
Filtering
/
Commands
/
ActionCommands
/
Base
/
Upload File
HOME
<?php /** * Created by PhpStorm. * User: turgutsaricam * Date: 14/03/2020 * Time: 14:21 * * @since 1.11.0 */ namespace WPCCrawler\Objects\Filtering\Commands\ActionCommands\Base; use Closure; use Exception; use Illuminate\Support\Arr; use WPCCrawler\Objects\Crawling\Bot\CategoryBot; use WPCCrawler\Objects\Crawling\Bot\PostBot; use WPCCrawler\Objects\Enums\ValueType; use WPCCrawler\Objects\Filtering\Commands\Base\AbstractBaseCommand; use WPCCrawler\Objects\Filtering\Enums\FilterOptionKey; use WPCCrawler\Objects\Filtering\Explaining\Loggers\ActionCommandLogger; use WPCCrawler\Objects\Filtering\FilterDependencyProvider\Page\PageFilterDependencyProvider; use WPCCrawler\Objects\Filtering\Property\Base\AbstractActionProperty; use WPCCrawler\Objects\Informing\Informer; use WPCCrawler\Objects\ShortCode\ShortCodeApplier; use WPCCrawler\Objects\Transformation\Interfaces\Transformable; use WPCCrawler\Objects\Value\ValueSetter; use WPCCrawler\Objects\ValueType\Interfaces\Outputs; use WPCCrawler\Objects\ValueType\ValueTypeService; abstract class AbstractActionCommand extends AbstractBaseCommand { /** * @var array|null A sequential array of dot keys. This command will be executed only for these keys if * {@link shouldLimitSubjects()} returns true. If the subjects should be limited but this is null, the * execution will still take place. For the execution to be constrained, this should be an array. So, if this * is an empty array when {@link shouldLimitSubjects()} returns true, no execution will be done. */ private $allowedSubjects = null; /** @var bool|null True if only the allowed subjects should be used. Otherwise, false. */ private $onlyAllowed = false; /** * @var null|ActionCommandLogger If this {@link isVerbose()}, then this logger will log details about this * command's execution. */ private $logger = null; /** @var int[]|false|null */ private $outputTypesCache = false; /** * Execute this action by retrieving the subject key from {@link AbstractBaseCommand::getFieldKey()} and the * options from {@link AbstractBaseCommand::getOptions()} * * @param string|int|null $key Dot key for the $subjectValue. The dot key is the address of the * $subjectValue in the data source (see {@link getDataSource()}). If subject * value does not exist and {@link doesNeedSubjectValue()} returns true, this * key will be null. If this command uses custom subject items, this key will * be an integer. If the subject values are calculated by a property, this key * might not be a valid dot key. * @param mixed|null $subjectValue Value of the subject. If subject value does not exist and * {@link doesNeedSubjectValue()} returns true, this will be null. * @return mixed New subject value. If this returns void, then it is considered as null is returned. In that case, * if {@link shouldReassignNewValues()} returns true, the values will be set * as null in the data source. If what this method returns should not be * assigned in the data source, then {@link shouldReassignNewValues()} should * return false to prevent unwanted nullification. * @since 1.11.0 */ abstract protected function onExecute($key, $subjectValue); /** * Get the data types of the values that this command can output * * @return int[] Constants defined in {@link ValueType} * @since 1.11.0 */ public function getOutputDataTypes(): array { if ($this->outputTypesCache === false) { $this->outputTypesCache = $this->isOutputTypeSameAsInputType() ? $this->getInputDataTypes() : ValueTypeService::getInstance()->getOutputTypes($this); } return $this->outputTypesCache ?: []; } /** * @return bool True if {@link getOutputDataTypes()} should return the same types as * {@link AbstractBaseCommand::getInputDataTypes()} returns. If this return false, then the output * types will be determined from the {@link Outputs} interfaces implemented by this class. * @since 1.11.0 */ protected function isOutputTypeSameAsInputType(): bool { return false; } /** * @return bool True if the results collected from {@link onExecute()} method should be reassigned to the data * source when {@link doesNeedSubjectValue()} returns true. Otherwise, false. * @since 1.11.0 */ protected function shouldReassignNewValues(): bool { return true; } /** * @return bool True if "only matched items" checkbox should be shown in the front-end. This defaults to the return * value of {@link doesNeedSubjectValue()}. * @since 1.11.0 */ protected function showUseMatchedItemsCheckbox(): bool { return $this->doesNeedSubjectValue(); } /** * @return bool True if this action command should be constrained by subjects matched by a condition command * @since 1.11.0 */ public function shouldLimitSubjects(): bool { return (bool) $this->getOption(FilterOptionKey::CMD_OPTION_ONLY_MATCHED_ITEMS, false); } /** * @param array|null $allowedSubjects See {@link $allowedSubjects} * @return $this * @since 1.11.0 */ public function setAllowedSubjects(?array $allowedSubjects): self { $this->allowedSubjects = $allowedSubjects; return $this; } /** * @return array|null See {@link $allowedSubjects} * @since 1.11.0 */ public function getAllowedSubjects(): ?array { return $this->allowedSubjects; } /** * Replace an allowed subject. If the old subject does not exist, the replacement will not be done. * * @param mixed $oldSubject The old subject * @param mixed|mixed[] $newSubject The new subject that will be put in place of the old subject * @return self * @since 1.12.0 * @noinspection PhpPluralMixedCanBeReplacedWithArrayInspection */ public function replaceAllowedSubject($oldSubject, $newSubject): self { if (!is_array($this->allowedSubjects) || !$this->allowedSubjects || $oldSubject === null || $newSubject === null || $newSubject === [] ) { return $this; } $existingKey = array_search($oldSubject, $this->allowedSubjects, true); if ($existingKey === false) return $this; // If the new subject is not an array, directly replace the previous value with it. if (!is_array($newSubject)) { $this->allowedSubjects[$existingKey] = $newSubject; return $this; } // The new subject is an array. Replace the old value with the first value of the new subject. Then, append the // remaining new subject items to the allowed subjects array. $newSubjects = array_values($newSubject); $first = Arr::pull($newSubjects, "0"); if ($first !== null) { $this->allowedSubjects[$existingKey] = $first; foreach($newSubjects as $newSubjectItem) { $this->allowedSubjects[] = $newSubjectItem; } } return $this; } /** * @return ActionCommandLogger|null See {@link logger} * @since 1.11.0 */ public function getLogger(): ?ActionCommandLogger { return $this->logger; } /** * Execute this action * * @since 1.11.0 */ public function execute(): void { $this->setExecuted(true); if ($this->isVerbose()) { $this->logger = new ActionCommandLogger(); $this->logger->tick(); } $this->doExecute(); $logger = $this->getLogger(); if ($logger) $logger->tock(); $this->executionFinished(); } /** * Execute this action * * @since 1.11.0 */ protected function doExecute(): void { $logger = $this->getLogger(); if (!$this->doesNeedSubjectValue()) { $this->onExecute(null, null); return; } // Check if the subjects should be considered. If the subjects must be considered but there are no subjects, // stop. if ($this->shouldConsiderAllowedSubjects() === null) { if ($logger) $logger ->addMessage(_wpcc('Command is not executed because there are no subjects that can be used.')); $this->setExecuted(false); return; } if ($logger) $logger->setOnlyAllowed($this->onlyAllowed ?: false); // Execute for all of the values $subjectValues = $this->getSubjectValues(); if (!$subjectValues) { if ($logger) $logger ->addMessage(_wpcc('Command is not executed because there are no subjects that can be used.')); $this->setExecuted(false); return; } $field = $this->getField(); $newSubjectValues = []; foreach($subjectValues as $k => $v) { // If there is a field, let it decide what should be the subject item. Otherwise, use the key, which is // probably a dot key. $subjectItem = $field ? $field->getSubjectItem($k, $v) : $k; if ($subjectItem === null) continue; if (!$this->isSubjectAllowed($subjectItem)) { if ($logger) $logger->addDeniedSubjectItem($field ? $field->getSubjectItemForHumans($k, $v, $subjectItem) : $v); continue; } if ($logger) $logger->addSubjectItem($field ? $field->getSubjectItemForHumans($k, $v, $subjectItem) : $v); // Collect the output. If the onExecute method returns void, then it is considered as null by PHP. So, in // that case, the new value will be null. $newValue = $this->onExecute($k, $v); $newSubjectValues[$k] = $newValue; } // If the new values should be reassigned, reassign them in the data source. $this->reassignFieldValues($newSubjectValues, $subjectValues); } protected function onTest($subject): ?array { // If this command does not need a subject value, it means it does not change the data source. Instead, it does // something different such that we cannot generalize that and retrieve the test result here. So, stop. The // command should override this method and provide its own test logic. if (!$this->doesNeedSubjectValue()) return null; // Execute the command for some key, which is not important here, and get the result $result = $this->onExecute('some-key', $subject); $this->setExecuted(true)->executionFinished(); return [$result]; } /** * Reassigns the modified field values in their data source. * * @param array $newSubjectValues This has the same structure as the return value of {@link getSubjectValues()} * method. The only difference is that the values are the return values retrieved * from {@link onExecute()}. * @param array $oldSubjectValues The old subject values. This has the same structure as the return value of * {@link getSubjectValues()} method. * @since 1.11.0 */ protected function reassignFieldValues(array $newSubjectValues, array $oldSubjectValues): void { $property = $this->getProperty(); // If the new values should not be reassigned, stop. $shouldReassignNewValues = $this->shouldReassignNewValues(); $canPropertyAssignNewValue = $property instanceof AbstractActionProperty && $property->canAssignNewValue(); if (!$shouldReassignNewValues && !$canPropertyAssignNewValue) return; // If a property calculation was performed for an action command, revert the structure of the array into a // structure we can use to reassign the subject values. if ($this->isPropertyCalculated() && $property instanceof AbstractActionProperty) { $newSubjectValues = $property->revertStructure($newSubjectValues) ?: []; } $logger = $this->getLogger(); foreach($newSubjectValues as $dotKey => $newSubjectValue) { if ($logger && $newSubjectValue !== ($oldSubjectValues[$dotKey] ?? null)) { $logger->addModifiedSubjectItem($newSubjectValue); } // If the assignment should be done by the property, let it. Otherwise, go on with the regular operation. if ($property instanceof AbstractActionProperty && $property->canAssignNewValue()) { $property->assignNewValue($dotKey, $newSubjectValue, $this); } else { $this->setFieldValue($dotKey, $newSubjectValue, null); } } } /** * Check if this command is allowed to be executed for the given subject * * @param mixed $subject The same type as the types of the items in {@link allowedSubjects}. The subjects can be * dot keys or objects. * @return bool False if this command should be executed only for the allowed subjects and the key is not allowed. * Otherwise, true. * @since 1.11.0 */ protected function isSubjectAllowed($subject): bool { return !$this->onlyAllowed || in_array($subject, $this->getAllowedSubjects() ?: [], true); } /** * This sets the return value to {@link onlyAllowed} as well. After calling this, whether a subject is allowed or * not can be found by calling {@link isSubjectAllowed()}. * * @return bool|null If the execution should not occur, returns null. Otherwise, true if the allowed subjects should * be considered and false if they should not be. * @since 1.11.0 */ protected function shouldConsiderAllowedSubjects(): ?bool { $allowedSubjects = $this->getAllowedSubjects(); $onlyAllowed = $this->shouldLimitSubjects(); if ($onlyAllowed) { // If this should be executed only for the allowed subjects but there is no allowed subject, do not execute. // Here, we allow null values. Null means "only allowed" rule does not apply. if (is_array($allowedSubjects) && !$allowedSubjects) { return $this->onlyAllowed = null; } // If the "allowed subjects" is null, "only allowed" rule does not apply. if ($allowedSubjects === null) { $logger = $this->getLogger(); if ($logger) $logger ->addMessage(_wpcc('Only allowed subjects should have been used but all subjects are allowed since there were no subjects found by a condition.')); $onlyAllowed = false; } } return $this->onlyAllowed = $onlyAllowed; } /* * */ public function toArray(): array { return array_merge(parent::toArray(), [ 'needSubjectValue' => $this->showUseMatchedItemsCheckbox(), 'outputTypes' => $this->getOutputDataTypes(), ]); } /* * */ protected function getFieldLists() { $transformable = $this->getDataSource(); return $transformable === null ? null : $transformable->getActionCommandFields(); } /** * Set value of something in the data source * * @param string|null $dotKey Dot key of the item in the data source. See {@link ValueSetter::set()} for more * information. * @param mixed $newValue The item's new value. This will be tried to be casted to the old value's type if the * type is not the same as the old value's. * @param mixed $oldValue Old value of the field. The new value will be set only if the new value is different * than the old value. * @uses \WPCCrawler\Objects\Value\ValueSetter::set() * @since 1.11.0 */ protected function setFieldValue(?string $dotKey, $newValue, $oldValue): void { // If the old value is the same as the new value, stop. if ($oldValue !== null && $oldValue === $newValue) return; // Get the data source $dataSource = $this->getDataSource(); // Prepare the new value by making sure its type is correct. If the new value could not be prepared, stop. if (!$this->prepareNewValue($dataSource, $newValue, $oldValue)) return; // Try to set the new value try { (new ValueSetter())->set($dataSource, [$dotKey => $newValue]); } catch (Exception $e) { // Notify the user in case of an exception Informer::addError(sprintf( _wpcc('New value could not be set in "%1$s" command.'), $this->getName() )) ->setException($e) ->addAsLog(); } } /** * TODO: Make sure this method works as expected * Prepare the new value for {@link setFieldValue()}. This method checks if the new value's type is one of the valid * types, which can be safely used. If not, this method tries to cast the type of the value into a valid data type. * * @param Transformable|null $dataSource The data source of this command. It can be retrieved by * {@link getDataSource()}. * @param mixed $newValue The new value. This will be casted to the correct type if it is not the * correct type. * @param mixed $oldValue The old value. This will be used to retrieve the correct type of the new * value. * @return bool True if the new value can be used. Otherwise, false. * @since 1.11.0 */ protected function prepareNewValue(?Transformable $dataSource, &$newValue, $oldValue): bool { // If there is no data source, notify the user and stop. if ($dataSource === null) { Informer::addError(sprintf( _wpcc('New value could not be set in "%1$s" command because there is no data source.'), $this->getName() )); return false; } $isOutputTypeValid = $this->isOutputTypeValid($newValue, $oldValue); if ($isOutputTypeValid === null) { Informer::addError(sprintf( _wpcc('New value could not be set in "%1$s" command because the command cannot output the value in a required data type.'), $this->getName() )); return false; } // If the output type is not valid, try to cast it to a correct type. If the new value is null, do not try to // cast it, use it directly. if ($isOutputTypeValid === false && $newValue !== null) { // Try to cast the output to a correct type. If it cannot be casted, do not set the new value. $preparedNewValue = $this->castNewValue($newValue, $oldValue); if ($preparedNewValue === null) { $valueTypeService = ValueTypeService::getInstance(); Informer::addError(sprintf( _wpcc('New value could not be set in "%1$s" command because it cannot be casted to the correct type. (Type of new value: "%2$s", type of old value: "%3$s")'), $this->getName(), implode(', ', (array) $valueTypeService->getTypeOf($newValue)), $oldValue === null ? 'null' : implode(', ', (array) $valueTypeService->getTypeOf($oldValue)) )); return false; } $newValue = $preparedNewValue; } return true; } /** * TODO: Make sure this method works as expected * Check if the output type is suitable for the field. This method checks the primitive types, inheritance, and * the ability of this class to provide a data type suitable for the field. In other words, if this action can * output a value with a type suitable for the field and the primitive types, including inheritance, of the new and * the old value are OK, this method returns true. * * @param mixed $newValue New value of the subject * @param mixed $oldValue Old value of the subject * @return bool|null True if the output type is suitable for the field of this command. False if the type is not * suitable. If any of this command's output types does not exist in the field's data types, * returns null. * @since 1.11.0 */ protected function isOutputTypeValid($newValue, $oldValue): ?bool { // If the new value is null, return true, because null is always allowed. if ($newValue === null) return true; // If there is no field, we cannot check the type. If that is the case, just allow all output types. $field = $this->getField(); if ($field === null) return true; // If the field does not have one of the valid output types, return null. If the property is calculated, then // the values follow these steps: Field -> Property -> Command -> Property -> Field. The property takes the // value from the field, converts it and passes to the command. Then, the command processes it and gives it to // the property so that the property can convert it back to one of its input types. So, if the property is // calculated, the valid output types are the input types of the property. if ($this->isPropertyCalculated()) { $property = $this->getProperty(); if (!$property || !$field->containsDataType($property->getInputDataTypes())) return null; } else { // If the property is not calculated, then the valid output types are the output types of this command, // since the data follows these steps: Field -> Command -> Field if(!$field->containsDataType($this->getOutputDataTypes())) return null; } // If the old value is null, we cannot check the types. Return false. if ($oldValue === null) return false; // If the values are not of the same type, return false. if (gettype($newValue) !== gettype($oldValue)) return false; // The types of the values are the same. // If the types are object, then the values must be instances of the same class or the same base class if (is_object($oldValue)) { // If the old value is an object but the new value is not, return false. if (!is_object($newValue)) return false; $cls1 = get_class($oldValue); $cls2 = get_class($newValue); $cls1Parents = class_parents($cls1); $cls2Parents = class_parents($cls2); // If the classes are not the same and they do not have a common parent, return false. if ($cls1 !== $cls2 && (!$cls1Parents || !$cls2Parents || !array_intersect($cls1Parents, $cls2Parents))) { return false; } } return true; } /** * TODO: Make sure this method works as expected * Cast a value (newValue) into the type of another value (oldValue) if the data type of the new value is NOT one * of the suitable data types of this command's transformable field. If the old value is null, then the new value * will be tried to be casted to one of the suitable data types. * * @param mixed $newValue The new value. If the new value is null, it cannot be casted to anything. If it is * null, this method should not be called. Otherwise, this will return null. * @param mixed|null $oldValue The old value. The new value will be casted to the old value's type if its type is * one of the suitable types of this command's transformable field. * @return mixed|null If casting was successful, the new value casted to a suitable data type. Otherwise, null. * @since 1.11.0 */ protected function castNewValue($newValue, $oldValue) { // If the new value is null, we cannot cast it to anything. In that case, return null. if ($newValue === null) return null; // Get the field. We will retrieve the data types from it. If there is no field, indicate that casting is not // successful by returning null. $field = $this->getField(); if ($field === null) return null; $valueTypeService = ValueTypeService::getInstance(); // Get the new value's type. If the type could not be retrieved, indicate the casting is not successful by // returning null. $newValueType = $valueTypeService->getTypeOf($newValue); if ($newValueType === null) return null; // If the new value's type is among the data types of the field, return the new value without casting it to // anything. The new value is OK to use. if ($field->containsDataType($newValueType)) return $newValue; // The new value needs to be casted. First, try to cast it to the old value's type. This is because in case // there are multiple types defined in the field, instead of randomly selecting one of the data types, using the // old value's type is more robust. if ($oldValue !== null) { $oldValueType = $valueTypeService->getTypeOf($oldValue); // Cast it only if the old value's type is one of the suitable data types. if ($oldValueType !== null) { foreach((array) $oldValueType as $oldValueTypeItem) { if (!$field->containsDataType($oldValueTypeItem)) { continue; } return $valueTypeService->castTo($this, $newValue, $oldValueTypeItem); } } } // We could not retrieve the old value's type or the old value's type was not suitable. Try to cast the new // value to one of the types of the field. // Get the suitable data types. If there is no data type, the new value cannot be made OK. In that case, // indicate that the casting is not successful by returning null. $suitableTypes = $field->getDataTypes(); if (!$suitableTypes) return null; // Now, try to cast the value to one of the suitable data types until the casting is valid. foreach($suitableTypes as $suitableType) { $candidate = $valueTypeService->castTo($this, $newValue, $suitableType); if ($candidate !== null) return $candidate; } // The value could not be casted to a valid data type. return null; } /** * Creates a short code applier by injecting the site settings * * @param array<string, scalar|scalar[]|Closure> $valueMap See {@link ShortCodeApplier::__construct()} * @return ShortCodeApplier A new applier * @since 1.13.0 */ protected function createShortCodeApplier(array $valueMap): ShortCodeApplier { $provider = $this->getProvider(); $settings = $provider instanceof PageFilterDependencyProvider ? $provider->getBot()->getSettingsImpl() : null; return new ShortCodeApplier($valueMap, $settings); } /** * @return array<string, string>|null An associative array where keys are short code names without brackets, and * the values are the actual values of the short codes. If there is no bot, or * the bot does not have the map, `null` is returned. * @since 1.13.0 */ protected function getShortCodeValueMap(): ?array { // Get the short code value map from the bot $provider = $this->getProvider(); $bot = $provider instanceof PageFilterDependencyProvider ? $provider->getBot() : null; if (!$bot) return null; if ($bot instanceof PostBot) { return $bot->getPostData()->getShortCodeValueMap(); } else if ($bot instanceof CategoryBot) { $categoryData = $bot->getCategoryData(); return $categoryData ? $categoryData->getShortCodeValueMap() : null; } return null; } }