File: D:/HostingSpaces/slenders/slenders.nl/app/Komma/Components/Componentables/ComponentableService.php
<?php
namespace App\Komma\Components\Componentables;
use App\Helpers\KommaHelpers;
use App\Komma\Components\Component\Component;
use App\Komma\Components\Component\ComponentableTrait;
use App\Komma\Components\Component\ComponentAttributeKey;
use App\Komma\Components\Component\ComponentSaveState;
use App\Komma\Components\Component\ComponentableInterface;
use App\Komma\Components\ComponentType;
use App\Komma\Components\ComponentTypeResolver;
use App\Komma\Kms\Core\Attributes\Attribute;
use App\Komma\Kms\Core\Attributes\MultiSelect;
use App\Komma\Kms\Core\Sections\AttributeKey;
use App\Komma\Globalization\Languages\Models\Language;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Illuminate\Support\Collection;
/**
* Class ComponentableService
*
* Knows which models (Componentables) are linked to specific components.
* So that it can save and load those models to these components
*
* @package App\Komma\Components\Component
*/
class ComponentableService implements ComponentableServiceInterface
{
/**
* @param Component $component
* @param ComponentType $componentType
* @param ComponentSaveState $componentSaveState
* @param AttributeKey $componentAreaAttributeKey
* @return Component
*/
public function saveComponentablesForComponent(Component $component, ComponentType $componentType, ComponentSaveState $componentSaveState, AttributeKey $componentAreaAttributeKey): Component
{
$componentType->componentableTypes()->get()->each(function(ComponentableType $componentableType, $key) use($componentAreaAttributeKey, $componentSaveState, $component) {
//Get the input data for the componentable type
$multiSelect = $this->getComponentableMultiSelectForComponent(collect(), $componentAreaAttributeKey, $componentSaveState->getId(), $key);
$componentableIdsString = \Input::get($multiSelect->getKey());
$componentableIds = [];
if($componentableIdsString !== '') {
//Convert the id string (example; "1,2,3") to an array containing those integers.
$componentableIdsStringArray = explode(',', $componentableIdsString);
$componentableIds = array_map(function ($item) {
return (int)$item;
}, $componentableIdsStringArray);
}
$componentableMethod = self::getAndValidateComponentableMethodNameOnComponentClassUsingComponentable($componentableType->componentable_type);
$component->$componentableMethod()->sync($componentableIds);
});
return $component;
}
/**
* Return an array containing auto complete input field names as keys. And a comma seperated string representing the selected items for them.
*
* @param Component $component
* @param ComponentSaveState $componentSaveState
* @param AttributeKey $componentAreaAttributeKey
* @return array
*/
public function loadComponentablesDataForComponent(Component $component, ComponentSaveState $componentSaveState, AttributeKey $componentAreaAttributeKey): array
{
$componentablesData = [];
$componentType = ComponentTypeResolver::resolve($componentSaveState->getComponentTypeId());
$componentType->componentableTypes()->get()->each(function(ComponentableType $componentableType, $key) use($componentAreaAttributeKey, $componentSaveState, $component, &$componentablesData) {
$multiSelect = $this->getComponentableMultiSelectForComponent(collect(), $componentAreaAttributeKey, $componentSaveState->getId(), $key);
$methodName = self::getAndValidateComponentableMethodNameOnComponentClassUsingComponentable($componentableType->componentable_type);
$modelIds = $component->$methodName()->get()->map(function (Model $componentable, $key) use($componentAreaAttributeKey, $componentSaveState, $component) {
return $componentable->id;
})->toArray();
$componentablesData[(string) $multiSelect->getKey()] = implode(',', $modelIds);
});
return $componentablesData;
}
/**
* Returns an MultiSelect instance with the selectable items you pass as the first parameter as a Collection.`
* They must be SelectOptions. The second parameter must be the key of a ComponentAreaAttribute, represented as a string.
* The last parameter must be a ComponentSaveState. Both this parameter and the ComponentAreaAttribute key will be used
* to generate a unique key for the multiSelect
*
* @param Collection $itemsCollection an array of select options
* @param AttributeKey $componentAreaAttributeKey
* @param int $componentId
* @param int $componentableNumber
* @return MultiSelect
*/
public static function getComponentableMultiSelectForComponent(Collection $itemsCollection, AttributeKey $componentAreaAttributeKey, int $componentId, int $componentableNumber) {
$componentAreaAttributeKey = clone $componentAreaAttributeKey; //Leaves the original component area key like it is
$componentAreaAttributeKey->setValuePart($componentAreaAttributeKey->getValuePart().'_componentable');
$attributeKey = ComponentAttributeKey::createInstanceFromAttributeKey($componentAreaAttributeKey, $componentId, $componentAreaAttributeKey->getValuePart());
$attribute = new MultiSelect();
$attribute->setItems($itemsCollection->toArray());
$attribute->setLabelText(__('components.link_to'));
if($attributeKey->getTranslationIso2()) {
$language = Language::where('iso_2', '=', $attributeKey->getTranslationIso2())->first();
$attribute->setAssociatedLanguage($language);
}
$attribute->mapValueFrom(Attribute::ValueFromItself, str_random());
$attribute->setKey($attributeKey);
return $attribute;
}
/**
* Validates and returns the componentable method name on the components class using a HasComponentsInteface implementation.
* For example: The User class implements the HasComponentsInterface by using a trait. Because it uses that trait it has
* a components method that refers to the component class. Because of that, the component class must have a method called users that
* references to the Users class. The system relies on that relation. The Users method in this example is also known as the "Componentable Method name".
*
* @param $componentable
* @return string
* @throws \ReflectionException
*/
public static function getAndValidateComponentableMethodNameOnComponentClassUsingComponentable(string $componentable)
{
if(!is_a($componentable, ComponentableInterface::class, true)) throw new \InvalidArgumentException('The class or class name you pass must be an implementation of "'.ComponentableInterface::class.'"');
// Validate that $hasComponentsInterfaceImplementation (Users for example) is a method on the Components model that returns a morphToMany relation
$methodName = KommaHelpers::getShortNameFromClass($componentable, true);
$methodNamePlural = str_plural($methodName);
if(!method_exists(Component::class, $methodNamePlural)) throw new \RuntimeException('Method "'.$methodNamePlural.'" does not exist on "'.Component::class.'". We expect this method because a "'.Component::class.'" can have many "'.$componentable.'". Create the method and let it return a "componentable" "'.MorphToMany::class.'" query builder instance like this: return $this->morphToMany('.$componentable.'::class, \'componentable\');');
$reflectionMethod = new \ReflectionMethod(Component::class, $methodNamePlural);
$reflectionType = $reflectionMethod->getReturnType();
$type = $reflectionType->getName();
if($type !== MorphToMany::class) throw new \RuntimeException('Method "'.$methodNamePlural.'" does exist on "'.Component::class.'" but does not return a "'.MorphToMany::class.'". Fix the method so that it returns a "componentable" "'.MorphToMany::class.'" query builder instance like this: return $this->morphToMany('.$componentable.'::class, \'componentable\');');
return $methodNamePlural;
}
/**
* validate the that the ComponentableInterface has a components method and return the name of the components method.
*
* @param $componentable
* @throws \ReflectionException
*/
public static function getAndValidateComponentsMethodNameOnComponentable(string $componentable)
{
if(!is_a($componentable, ComponentableInterface::class, true)) throw new \InvalidArgumentException('The class or class name you pass must be an implementation of "'.ComponentableInterface::class.'"');
$methodName = KommaHelpers::getShortNameFromClass(Component::class, true);
$methodNamePlural = str_plural($methodName);
if(!method_exists($componentable, $methodNamePlural)) throw new \RuntimeException('Method "'.$methodNamePlural.'" does not exist on "'.$componentable.'". We expect this method because a "'.$componentable.'" can have many "'.Component::class.'". Create the method and let it return a "componentable" "'.BelongsToMany::class.'" query builder instance like this: return $this->morphedByMany('.Component::class.'::class, \'componentable\');. Pro tip: Fix this by USEing the "'.ComponentableTrait::class.'" and the the "'.ComponentableInterface::class.'" on the class');
$reflectionMethod = new \ReflectionMethod($componentable, $methodNamePlural);
$reflectionType = $reflectionMethod->getReturnType();
$type = $reflectionType->getName();
if($type !== BelongsToMany::class) throw new \RuntimeException('Method "'.$methodNamePlural.'" does exist on "'.$componentable.'" but does not return a "'.BelongsToMany::class.'". Fix the method so that it returns a "componentable" "'.BelongsToMany::class.'" query builder instance like this: return $this->morphedByMany('.Component::class.'::class, \'componentable\');. Pro tip: Fix this by USEing the "'.ComponentableTrait::class.'" and the the "'.ComponentableInterface::class." on the class");
}
}