Skip to content

Commit

Permalink
controller cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
markhuot committed Nov 17, 2023
1 parent e7a453f commit 4425b88
Show file tree
Hide file tree
Showing 14 changed files with 244 additions and 42 deletions.
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"minimum-stability": "dev",
"autoload": {
"psr-4": {
"markhuot\\keystone\\": "src/"
"markhuot\\keystone\\": "src/",
"markhuot\\keystone\\tests\\": "tests/"
},
"files": [
"src/helpers/event.php",
Expand Down
22 changes: 20 additions & 2 deletions src/actions/MakeModelFromArray.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use craft\base\FieldInterface;
use craft\base\Model;
use markhuot\keystone\db\ActiveRecord;
use markhuot\keystone\tests\models\ElementToElementIdTest;
use yii\base\ModelEvent;

use function markhuot\keystone\helpers\base\app;
Expand Down Expand Up @@ -89,14 +90,22 @@ protected function load(\yii\base\Model $model, mixed $data): void

$reflect = new \ReflectionClass($model);

foreach ($data as $key => &$value) {
foreach ($data as $key => $value) {
// If we were passed something like elementId over the wire, but want to deal with
// a property of `->element` then check for that here and swap the keys out.
if (str_ends_with($key, 'Id') && ! $reflect->hasProperty($key)) {
unset($data[$key]);
$key = substr($key, 0, -2);
}

if ($reflect->hasProperty($key)) {
$property = $reflect->getProperty($key);
$type = $property->getType()->getName();

if (enum_exists($type)) {
$value = $type::from($value);
} elseif (class_exists($type) || interface_exists($type)) {
}
elseif (class_exists($type) || interface_exists($type)) {
$value = (new static)
->handle(
className: $type,
Expand All @@ -107,8 +116,17 @@ className: $type,
);
}
}

$data[$key] = $value;
}

// We can't set a non-null property to null, or it would error out here during the load
// phase instead of during the validation phase. So, if a value is null but the model doesn't
// support that remove the null value from the data array. This will leave the value unset
// in the model and it will later fail validation.
//
// Basically, we're punting null errors further down in the processing so it can catch during
// validation and not throw a runtime error here during load.
$reflect = new \ReflectionClass($model);
foreach ($reflect->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) {
if (! $property->getType()?->allowsNull() && ($data[$property->getName()] ?? null) === null) {
Expand Down
33 changes: 33 additions & 0 deletions src/base/Model.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace markhuot\keystone\base;

use markhuot\keystone\validators\Required;
use markhuot\keystone\validators\Safe;

class Model extends \craft\base\Model
{
public function safeAttributes()
{
$reflect = new \ReflectionClass($this);
$properties = collect($reflect->getProperties(\ReflectionProperty::IS_PUBLIC))
->filter(fn (\ReflectionProperty $property) => $property->getAttributes(Safe::class))
->map(fn (\ReflectionProperty $property) => $property->getName())
->all();

return array_merge(parent::safeAttributes(), $properties);
}

public function rules(): array
{
$reflect = new \ReflectionClass($this);
$required = collect($reflect->getProperties(\ReflectionProperty::IS_PUBLIC))
->filter(fn (\ReflectionProperty $property) => $property->getAttributes(Required::class))
->map(fn (\ReflectionProperty $property) => $property->getName())
->all();

return [
[$required, 'required'],
];
}
}
30 changes: 29 additions & 1 deletion src/behaviors/BodyParamObjectBehavior.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,34 @@ public function getBodyParamObject(string $class, string $formName = '')
// Get the post data
$data = $this->owner->getBodyParams();

return $this->handleData($data, $class, $formName);
}

/**
* @template T
*
* @param class-string<T> $class
* @return T
*/
public function getQueryParamObject(string $class, string $formName = '')
{
return $this->handleData($this->owner->getQueryParams(), $class, $formName);
}

public function getQueryParamObjectOrFail(string $class, string $formName = '')
{
return $this->handleData($this->owner->getQueryParams(), $class, $formName, true, false);
}

/**
* @template T
*
* @param array<mixed> $data
* @param class-string<T> $class
* @return T
*/
protected function handleData(array $data, string $class, string $formName = '', bool $errorOnMissing = false, bool $createOnMissing = true)
{
// Yii doesn't support nested form names so manually pull out
// the right data using Laravel's data_get() and then drop the
// form name from the Yii call
Expand All @@ -48,7 +76,7 @@ public function getBodyParamObject(string $class, string $formName = '')
}

// Create our model
$model = (new MakeModelFromArray)->handle($class, $data);
$model = (new MakeModelFromArray)->handle($class, $data, $errorOnMissing, $createOnMissing);

// Validate the model
if ($model->hasErrors()) {
Expand Down
28 changes: 10 additions & 18 deletions src/controllers/ComponentsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use markhuot\keystone\behaviors\BodyParamObjectBehavior;
use markhuot\keystone\models\Component;
use markhuot\keystone\models\http\AddComponentRequest;
use markhuot\keystone\models\http\StoreComponentRequest;
use markhuot\keystone\models\http\MoveComponentRequest;
use yii\web\Request;

Expand All @@ -23,32 +24,26 @@ class ComponentsController extends Controller
{
public function actionAdd()
{
$elementId = $this->request->getRequiredQueryParam('elementId');
$element = Craft::$app->elements->getElementById($elementId);
$fieldId = $this->request->getRequiredQueryParam('fieldId');
$field = Craft::$app->fields->getFieldById($fieldId);
$path = $this->request->getQueryParam('path');
$slot = $this->request->getQueryParam('slot');
$sortOrder = $this->request->getRequiredQueryParam('sortOrder');
$parent = (new GetParentFromPath)->handle($elementId, $fieldId, $path);
$data = $this->request->getQueryParamObject(AddComponentRequest::class);
$parent = (new GetParentFromPath)->handle($data->element->id, $data->field->id, $data->path);

return $this->asCpScreen()
->title('Add component')
->action('keystone/components/store')
->contentTemplate('keystone/select', [
'element' => $element,
'field' => $field,
'path' => $path,
'slot' => $slot,
'element' => $data->element,
'field' => $data->field,
'path' => $data->path,
'slot' => $data->slot,
'parent' => $parent,
'groups' => (new GetComponentType())->all()->groupBy(fn ($t) => $t->getCategory()),
'sortOrder' => $sortOrder,
'sortOrder' => $data->sortOrder,
]);
}

public function actionStore()
{
$data = $this->request->getBodyParamObject(AddComponentRequest::class);
$data = $this->request->getBodyParamObject(StoreComponentRequest::class);

(new AddComponent)->handle(
elementId: $data->element->id,
Expand All @@ -66,10 +61,7 @@ public function actionStore()

public function actionEdit()
{
$id = $this->request->getRequiredQueryParam('id');
$elementId = $this->request->getRequiredQueryParam('elementId');
$fieldId = $this->request->getRequiredQueryParam('fieldId');
$component = Component::findOne(['id' => $id, 'elementId' => $elementId, 'fieldId' => $fieldId]);
$component = $this->request->getQueryParamObjectOrFail(Component::class);
$hasContentFields = $component->getType()->getFieldDefinitions()->isNotEmpty();

return $this->asCpScreen()
Expand Down
28 changes: 10 additions & 18 deletions src/models/http/AddComponentRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,36 +5,28 @@
use craft\base\ElementInterface;
use craft\base\FieldInterface;
use craft\base\Model;
use markhuot\keystone\validators\Required;
use markhuot\keystone\validators\Safe;

class AddComponentRequest extends Model
{
#[Required]
public ElementInterface $element;

#[Required]
public FieldInterface $field;

public ?string $path;
#[Safe]
public ?string $path=null;

public ?string $slot;
#[Safe]
public ?string $slot=null;

#[Required]
public int $sortOrder;

public string $type;

public function safeAttributes()
public function getParent()
{
return [...parent::safeAttributes(), 'path', 'slot'];
}

/**
* @return array<mixed>
*/
public function rules(): array
{
return [
['element', 'required'],
['field', 'required'],
['sortOrder', 'required'],
['type', 'required'],
];
}
}
11 changes: 11 additions & 0 deletions src/models/http/EditComponentRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace markhuot\keystone\models\http;

use markhuot\keystone\base\Model;
use markhuot\keystone\models\Component;

class EditComponentRequest extends Model
{
public Component $component;
}
30 changes: 30 additions & 0 deletions src/models/http/StoreComponentRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

namespace markhuot\keystone\models\http;

use craft\base\ElementInterface;
use craft\base\FieldInterface;
use markhuot\keystone\base\Model;
use markhuot\keystone\validators\Required;
use markhuot\keystone\validators\Safe;

class StoreComponentRequest extends Model
{
#[Required]
public ElementInterface $element;

#[Required]
public FieldInterface $field;

#[Safe]
public ?string $path=null;

#[Safe]
public ?string $slot=null;

#[Required]
public int $sortOrder;

#[Required]
public string $type;
}
11 changes: 11 additions & 0 deletions src/validators/Required.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace markhuot\keystone\validators;

use Attribute;

#[Attribute(Attribute::TARGET_PROPERTY)]
class Required
{

}
11 changes: 11 additions & 0 deletions src/validators/Safe.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace markhuot\keystone\validators;

use Attribute;

#[Attribute(Attribute::TARGET_PROPERTY)]
class Safe
{

}
28 changes: 26 additions & 2 deletions tests/RouteModelBindingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@
use markhuot\keystone\actions\MakeModelFromArray;
use markhuot\keystone\enums\MoveComponentPosition;
use markhuot\keystone\models\Component;
use markhuot\keystone\models\http\AddComponentRequest;
use markhuot\keystone\models\http\StoreComponentRequest;
use markhuot\keystone\models\http\MoveComponentRequest;
use markhuot\keystone\tests\models\ElementToElementIdTest;
use markhuot\keystone\tests\models\OptionalFieldsTest;
use markhuot\keystone\tests\models\RequiredFieldsTest;

it('loads top level models', function () {
$createdComponent = Component::factory()->create();
Expand All @@ -25,7 +28,7 @@

it('gets elementinterface properties', function () {
$entry = Entry::factory()->create();
$formData = (new MakeModelFromArray)->handle(AddComponentRequest::class, ['element' => $entry->id]);
$formData = (new MakeModelFromArray)->handle(StoreComponentRequest::class, ['element' => $entry->id]);

expect($formData)->element->id->toBe($entry->id);
});
Expand All @@ -36,3 +39,24 @@

expect($formData)->id->toBe($entry->id);
});

it('automatically translates element to elementId', function () {
$entry = Entry::factory()->create();
$formData = (new MakeModelFromArray)->handle(ElementToElementIdTest::class, ['elementId' => $entry->id]);

expect($formData)->element->id->toBe($entry->id);
});

it('supports #[Required] attribute', function () {
$rules = (new RequiredFieldsTest)->rules();

expect($rules)->toHaveCount(1);
expect($rules[0][0])->toBe(['foo']);
expect($rules[0][1])->toBe('required');
});

it('supports #[Safe] attribute', function () {
$formData = (new MakeModelFromArray)->handle(OptionalFieldsTest::class, ['foo' => 'foo', 'bar' => 'bar']);

expect($formData)->foo->toBe('foo')->bar->toBe('bar');
});
18 changes: 18 additions & 0 deletions tests/models/ElementToElementIdTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace markhuot\keystone\tests\models;

use craft\base\ElementInterface;
use craft\base\Model;

class ElementToElementIdTest extends Model
{
public ElementInterface $element;

public function rules(): array
{
return [
[['element'], 'required'],
];
}
}
Loading

0 comments on commit 4425b88

Please sign in to comment.