Skip to content

Commit

Permalink
Merge pull request #686 from hydephp/extract-service-helper-to-base-c…
Browse files Browse the repository at this point in the history
…ommand-class

Extract service helper to base command class
  • Loading branch information
caendesilva authored Nov 23, 2022
2 parents 3ee0365 + fabdb6c commit a427943
Show file tree
Hide file tree
Showing 5 changed files with 195 additions and 72 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Exception;
use Hyde\Console\Commands\Interfaces\CommandHandleInterface;
use Hyde\Console\Concerns\ValidatingCommand;
use Hyde\Framework\Actions\CreatesNewPublicationFile;
use Hyde\Framework\Features\Publications\PublicationService;
use Illuminate\Support\Str;
Expand All @@ -18,7 +19,7 @@
*
* @see \Hyde\Framework\Testing\Feature\Commands\MakePublicationCommandTest
*/
class MakePublicationCommand extends Command implements CommandHandleInterface
class MakePublicationCommand extends ValidatingCommand implements CommandHandleInterface
{
/** @var string */
protected $signature = 'make:publication
Expand Down Expand Up @@ -46,7 +47,7 @@ public function handle(): int
$offset++;
$this->line(" $offset: $pubType->name");
}
$selected = (int) PublicationService::askWithValidation($this, 'selected', "Publication type (1-$offset)", ['required', 'integer', "between:1,$offset"]);
$selected = (int) $this->askWithValidation('selected', "Publication type (1-$offset)", ['required', 'integer', "between:1,$offset"]);
$pubType = $pubTypes->{$pubTypes->keys()[$selected - 1]};
}

Expand All @@ -65,8 +66,7 @@ public function handle(): int
// Useful for debugging
//$this->output->writeln("xxx " . $exception->getTraceAsString());
$this->output->writeln("<bg=red;fg=white>$msg</>");
$overwrite = PublicationService::askWithValidation(
$this,
$overwrite = $this->askWithValidation(
'overwrite',
'Do you wish to overwrite the existing file (y/n)',
['required', 'string', 'in:y,n'],
Expand Down Expand Up @@ -128,7 +128,7 @@ protected function captureFieldInput(object $field, Collection $mediaFiles): str
$offset = $index + 1;
$this->output->writeln(" $offset: $file");
}
$selected = PublicationService::askWithValidation($this, $field->name, $field->name, ['required', 'integer', "between:1,$offset"]);
$selected = $this->askWithValidation($field->name, $field->name, ['required', 'integer', "between:1,$offset"]);
$file = $mediaFiles->{$selected - 1};

return '_media/'.Str::of($file)->after('media/')->toString();
Expand All @@ -153,7 +153,7 @@ protected function captureFieldInput(object $field, Collection $mediaFiles): str
}
}

return PublicationService::askWithValidation($this, $field->name, $field->name, $fieldRules);
return $this->askWithValidation($field->name, $field->name, $fieldRules);
}

protected function getValidationRulesPerType(): Collection
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Exception;
use Hyde\Console\Commands\Interfaces\CommandHandleInterface;
use Hyde\Console\Concerns\ValidatingCommand;
use Hyde\Framework\Actions\CreatesNewPublicationType;
use Hyde\Framework\Features\Publications\PublicationService;
use InvalidArgumentException;
Expand All @@ -17,7 +18,7 @@
*
* @see \Hyde\Framework\Testing\Feature\Commands\MakePublicationTypeCommandTest
*/
class MakePublicationTypeCommand extends Command implements CommandHandleInterface
class MakePublicationTypeCommand extends ValidatingCommand implements CommandHandleInterface
{
/** @var string */
protected $signature = 'make:publicationType
Expand All @@ -32,7 +33,7 @@ public function handle(): int

$title = $this->argument('title');
if (! $title) {
$title = trim(PublicationService::askWithValidation($this, 'name', 'Publication type name', ['required', 'string']));
$title = trim($this->askWithValidation('name', 'Publication type name', ['required', 'string']));
$dirname = PublicationService::formatNameForStorage($title);
if (file_exists($dirname)) {
throw new InvalidArgumentException("Storage path [$dirname] already exists");
Expand All @@ -48,27 +49,25 @@ public function handle(): int
$offset = $k + 1;
$this->line(" $offset: $v[name]");
}
$selected = (int) PublicationService::askWithValidation($this, 'selected', "Sort field (0-$offset)", ['required', 'integer', "between:0,$offset"], 0);
$selected = (int) $this->askWithValidation('selected', "Sort field (0-$offset)", ['required', 'integer', "between:0,$offset"], 0);
$sortField = $selected ? $fields[$selected - 1]['name'] : '__createdAt';

$this->output->writeln('<bg=magenta;fg=white>Choose the default sort direction:</>');
$this->line(' 1 - Ascending (oldest items first if sorting by dateCreated)');
$this->line(' 2 - Descending (newest items first if sorting by dateCreated)');
$selected = (int) PublicationService::askWithValidation($this, 'selected', 'Sort field (1-2)', ['required', 'integer', 'between:1,2'], 2);
$selected = (int) $this->askWithValidation('selected', 'Sort field (1-2)', ['required', 'integer', 'between:1,2'], 2);
$sortDirection = match ($selected) {
1 => 'ASC',
2 => 'DESC',
};

$pageSize = (int) PublicationService::askWithValidation(
$this,
$pageSize = (int) $this->askWithValidation(
'pageSize',
'Enter the pageSize (0 for no limit)',
['required', 'integer', 'between:0,100'],
25
);
$prevNextLinks = (bool) PublicationService::askWithValidation(
$this,
$prevNextLinks = (bool) $this->askWithValidation(
'prevNextLinks',
'Generate previous/next links in detail view (y/n)',
['required', 'string', 'in:y,n'],
Expand All @@ -82,7 +81,7 @@ public function handle(): int
$this->line(" $offset: $v->name");
}
}
$selected = (int) PublicationService::askWithValidation($this, 'selected', "Canonical field (1-$offset)", ['required', 'integer', "between:1,$offset"], 1);
$selected = (int) $this->askWithValidation('selected', "Canonical field (1-$offset)", ['required', 'integer', "between:1,$offset"], 1);
$canonicalField = $fields[$selected - 1]['name'];

try {
Expand All @@ -109,7 +108,7 @@ protected function captureFieldsDefinitions(): Collection
$this->output->writeln("<bg=cyan;fg=white>Field #$count:</>");

$field = Collection::create();
$field->name = PublicationService::askWithValidation($this, 'name', 'Field name', ['required']);
$field->name = $this->askWithValidation('name', 'Field name', ['required']);
$this->line('Field type:');
$this->line(' 1 - String');
$this->line(' 2 - Boolean ');
Expand All @@ -120,18 +119,18 @@ protected function captureFieldsDefinitions(): Collection
$this->line(' 7 - Array');
$this->line(' 8 - Text');
$this->line(' 9 - Local Image');
$type = (int) PublicationService::askWithValidation($this, 'type', 'Field type (1-9)', ['required', 'integer', 'between:1,9'], 1);
$type = (int) $this->askWithValidation('type', 'Field type (1-9)', ['required', 'integer', 'between:1,9'], 1);
do {
// TODO This should only be done for types that can have length restrictions right?
$field->min = PublicationService::askWithValidation($this, 'min', 'Min value (for strings, this refers to string length)', ['required', 'string'], 0);
$field->max = PublicationService::askWithValidation($this, 'max', 'Max value (for strings, this refers to string length)', ['required', 'string'], 0);
$field->min = $this->askWithValidation('min', 'Min value (for strings, this refers to string length)', ['required', 'string'], 0);
$field->max = $this->askWithValidation('max', 'Max value (for strings, this refers to string length)', ['required', 'string'], 0);
$lengthsValid = true;
if ($field->max < $field->min) {
$lengthsValid = false;
$this->output->warning('Field length [max] must be [>=] than [min]');
}
} while (! $lengthsValid);
$addAnother = PublicationService::askWithValidation($this, 'addAnother', 'Add another field (y/n)', ['required', 'string', 'in:y,n'], 'y');
$addAnother = $this->askWithValidation('addAnother', 'Add another field (y/n)', ['required', 'string', 'in:y,n'], 'y');

// map field choice to actual field type
$field->type = match ($type) {
Expand Down
66 changes: 66 additions & 0 deletions packages/framework/src/Console/Concerns/ValidatingCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

declare(strict_types=1);

namespace Hyde\Console\Concerns;

use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Support\Facades\Validator;
use LaravelZero\Framework\Commands\Command;
use RuntimeException;
use function ucfirst;

/**
* An extended Command class that provides validation methods.
*
* @see \Hyde\Framework\Testing\Feature\ValidatingCommandTest
*/
class ValidatingCommand extends Command
{
/** @var int How many times can the validation loop run? Guards against infinite loops. */
protected final const MAX_RETRIES = 30;

/**
* Ask for a CLI input value until we pass validation rules.
*
* @param string $name
* @param string $question
* @param \Illuminate\Contracts\Support\Arrayable|array $rules
* @param mixed|null $default
* @param int $retryCount How many times has the question been asked?
* @return mixed
*
* @throws RuntimeException
*/
public function askWithValidation(
string $name,
string $question,
Arrayable|array $rules = [],
mixed $default = null,
int $retryCount = 0
): mixed {
if ($rules instanceof Arrayable) {
$rules = $rules->toArray();
}

$answer = $this->ask(ucfirst($question), $default);
$validator = Validator::make([$name => $answer], [$name => $rules]);

if ($validator->passes()) {
return $answer;
}

foreach ($validator->errors()->all() as $error) {
$this->error($error);
}

$retryCount++;

if ($retryCount >= self::MAX_RETRIES) {
// Prevent infinite loops that may happen, for example when testing. The retry count is high enough to not affect normal usage.
throw new RuntimeException(sprintf("Too many validation errors trying to validate '$name' with rules: [%s]", implode(', ', $rules)));
}

return $this->askWithValidation($name, $question, $rules, null, $retryCount);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,68 +8,16 @@
use Hyde\Framework\Features\Publications\Models\PublicationType;
use Hyde\Hyde;
use Hyde\Pages\PublicationPage;
use Illuminate\Contracts\Validation\Factory as ValidationFactory;
use Illuminate\Support\Str;
use LaravelZero\Framework\Commands\Command;
use Rgasch\Collection\Collection;
use RuntimeException;
use function Safe\file_get_contents;
use Spatie\YamlFrontMatter\YamlFrontMatter;
use function ucfirst;

/**
* @see \Hyde\Framework\Testing\Feature\Services\PublicationServiceTest
*/
class PublicationService
{
/** @var int How many times can the validation loop run? Guards against infinite loops. */
protected final const RETRY_COUNT = 10;

/**
* Ask for a CLI input value until we pass validation rules.
*
* @param \LaravelZero\Framework\Commands\Command $command
* @param string $name
* @param string $message
* @param \Rgasch\Collection\Collection|array $rules
* @param mixed|null $default
* @param bool $isBeingRetried
* @return mixed
*
* @throws RuntimeException
*/
public static function askWithValidation(Command $command, string $name, string $message, Collection|array $rules = [], mixed $default = null, bool $isBeingRetried = false): mixed
{
static $tries = 0;
if (! $isBeingRetried) {
$tries = 0;
}

if ($rules instanceof Collection) {
$rules = $rules->toArray();
}

$answer = $command->ask(ucfirst($message), $default);
$factory = app(ValidationFactory::class);
$validator = $factory->make([$name => $answer], [$name => $rules]);

if ($validator->passes()) {
return $answer;
}

foreach ($validator->errors()->all() as $error) {
$command->error($error);
}

$tries++;

if ($tries >= self::RETRY_COUNT) {
throw new RuntimeException(sprintf("Too many validation errors trying to validate '$name' with rules: [%s]", implode(', ', $rules)));
}

return self::askWithValidation($command, $name, $message, $rules, isBeingRetried: true);
}

/**
* Format the publication type name to a suitable representation for file storage.
*/
Expand Down
110 changes: 110 additions & 0 deletions packages/framework/tests/Feature/ValidatingCommandTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<?php

declare(strict_types=1);

namespace Hyde\Framework\Testing\Feature;

use Hyde\Console\Concerns\ValidatingCommand;
use Hyde\Testing\TestCase;
use Illuminate\Console\OutputStyle;
use Illuminate\Support\Facades\Validator;
use Mockery;
use RuntimeException;
use function str_starts_with;

/**
* @covers \Hyde\Console\Concerns\ValidatingCommand
*/
class ValidatingCommandTest extends TestCase
{
public function testAskWithValidationCapturesInput()
{
$command = new ValidationTestCommand();

$output = Mockery::mock(OutputStyle::class);

$output->shouldReceive('ask')->once()->withArgs(function (string $question) {
return $question === 'What is your name?';
})->andReturn('Jane Doe');

$output->shouldReceive('writeln')->once()->withArgs(function (string $message) {
return $message === 'Hello Jane Doe!';
});

$command->setOutput($output);
$command->handle();
}

public function testAskWithValidationRetries()
{
$command = new ValidationTestCommand();
$output = Mockery::mock(OutputStyle::class);

$output->shouldReceive('ask')->times(2)->withArgs(function (string $question) {
return $question === 'What is your name?';
})->andReturn('', 'Jane Doe');

$output->shouldReceive('writeln')->times(1)->withArgs(function (string $message) {
return $message === '<error>validation.required</error>';
});

$output->shouldReceive('writeln')->once()->withArgs(function (string $message) {
return $message === 'Hello Jane Doe!';
});

$command->setOutput($output);
$command->handle();
}

public function testAskWithValidationRetriesTooManyTimes()
{
$command = new ValidationTestCommand();
$output = Mockery::mock(OutputStyle::class);

$output->shouldReceive('ask')->times(30)->withArgs(function (string $question) {
return $question === 'What is your name?';
})->andReturn('');

$output->shouldReceive('writeln')->times(30)->withArgs(function (string $message) {
return $message === '<error>validation.required</error>';
});

$this->expectException(RuntimeException::class);
$this->expectExceptionMessage("Too many validation errors trying to validate 'name' with rules: [required]");

$command->setOutput($output);
$command->handle();

$output->shouldNotReceive('writeln')->once()->withArgs(function (string $message) {
return str_starts_with($message, 'Hello');
});
}

public function testValidationIsCalled()
{
$command = new ValidationTestCommand();
$output = Mockery::mock(OutputStyle::class);

$output->shouldReceive('ask')->once()->andReturn('Jane Doe');
$output->shouldReceive('writeln')->once();

$validator = Validator::spy();
$validator->shouldReceive('make')->once()->withArgs(function (array $data, array $rules) {
return $data === ['name' => 'Jane Doe']
&& $rules === ['name' => ['required']];
})->andReturn($validator);
$validator->shouldReceive('passes')->once()->andReturn(true);

$command->setOutput($output);
$command->handle();
}
}

class ValidationTestCommand extends ValidatingCommand
{
public function handle()
{
$name = $this->askWithValidation('name', 'What is your name?', ['required'], 'John Doe');
$this->output->writeln("Hello $name!");
}
}

0 comments on commit a427943

Please sign in to comment.