diff --git a/common/models/WebHook.php b/common/models/WebHook.php index 586fa9e..8b6c6d7 100644 --- a/common/models/WebHook.php +++ b/common/models/WebHook.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace common\models; use yii\behaviors\TimestampBehavior; -use yii\db\ActiveQueryInterface; use yii\db\ActiveRecord; /** @@ -12,18 +11,16 @@ use yii\db\ActiveRecord; * @property int $id * @property string $url * @property string|null $secret + * @property string[] $events * @property int $created_at * - * Relations: - * @property WebHookEvent[] $events - * * Behaviors: * @mixin TimestampBehavior */ class WebHook extends ActiveRecord { public static function tableName(): string { - return '{{%webhooks}}'; + return 'webhooks'; } public function behaviors(): array { @@ -35,8 +32,4 @@ class WebHook extends ActiveRecord { ]; } - public function getEvents(): ActiveQueryInterface { - return $this->hasMany(WebHookEvent::class, ['webhook_id' => 'id']); - } - } diff --git a/common/models/WebHookEvent.php b/common/models/WebHookEvent.php deleted file mode 100644 index 280d4ab..0000000 --- a/common/models/WebHookEvent.php +++ /dev/null @@ -1,27 +0,0 @@ -hasOne(WebHook::class, ['id' => 'webhook_id']); - } - -} diff --git a/common/tasks/CreateWebHooksDeliveries.php b/common/tasks/CreateWebHooksDeliveries.php index ffc7e11..ac604a8 100644 --- a/common/tasks/CreateWebHooksDeliveries.php +++ b/common/tasks/CreateWebHooksDeliveries.php @@ -6,6 +6,7 @@ namespace common\tasks; use common\models\Account; use common\models\WebHook; use Yii; +use yii\db\Expression; use yii\queue\RetryableJobInterface; final class CreateWebHooksDeliveries implements RetryableJobInterface { @@ -67,8 +68,9 @@ final class CreateWebHooksDeliveries implements RetryableJobInterface { public function execute($queue): void { /** @var WebHook[] $targets */ $targets = WebHook::find() - ->joinWith('events e', false) - ->andWhere(['e.event_type' => $this->type]) + // It's very important to use exactly single quote to begin the string + // and double quote to specify the string value + ->andWhere(new Expression("JSON_CONTAINS(`events`, '\"{$this->type}\"')")) ->all(); foreach ($targets as $target) { $job = new DeliveryWebHook(); diff --git a/common/tests/fixtures/WebHooksEventsFixture.php b/common/tests/fixtures/WebHooksEventsFixture.php deleted file mode 100644 index 2b95a4f..0000000 --- a/common/tests/fixtures/WebHooksEventsFixture.php +++ /dev/null @@ -1,19 +0,0 @@ - 1, - 'event_type' => 'account.edit', - ], - [ - 'webhook_id' => 2, - 'event_type' => 'account.edit', - ], -]; diff --git a/common/tests/fixtures/data/webhooks.php b/common/tests/fixtures/data/webhooks.php index 238c90a..1ca2b03 100644 --- a/common/tests/fixtures/data/webhooks.php +++ b/common/tests/fixtures/data/webhooks.php @@ -4,18 +4,21 @@ return [ 'id' => 1, 'url' => 'http://localhost:80/webhooks/ely', 'secret' => 'my-secret', + 'events' => ['account.edit'], 'created_at' => 1531054333, ], 'webhook-without-secret' => [ 'id' => 2, 'url' => 'http://localhost:81/webhooks/ely', 'secret' => null, + 'events' => ['account.edit'], 'created_at' => 1531054837, ], 'webhook-without-events' => [ 'id' => 3, 'url' => 'http://localhost:82/webhooks/ely', 'secret' => null, + 'events' => [], 'created_at' => 1531054990, ], ]; diff --git a/common/tests/unit/tasks/CreateWebHooksDeliveriesTest.php b/common/tests/unit/tasks/CreateWebHooksDeliveriesTest.php index a8e5587..791ce59 100644 --- a/common/tests/unit/tasks/CreateWebHooksDeliveriesTest.php +++ b/common/tests/unit/tasks/CreateWebHooksDeliveriesTest.php @@ -18,7 +18,6 @@ class CreateWebHooksDeliveriesTest extends TestCase { public function _fixtures(): array { return [ 'webhooks' => fixtures\WebHooksFixture::class, - 'webhooksEvents' => fixtures\WebHooksEventsFixture::class, ]; } diff --git a/console/controllers/WebhooksController.php b/console/controllers/WebhooksController.php index 3855b3b..1008560 100644 --- a/console/controllers/WebhooksController.php +++ b/console/controllers/WebhooksController.php @@ -7,15 +7,46 @@ use common\models\WebHook; use console\models\WebHookForm; use yii\console\Controller; use yii\console\ExitCode; -use yii\helpers\Console; +use yii\console\widgets\Table; +use yii\helpers\Console as C; class WebhooksController extends Controller { - public function actionCreate(): int { - $form = new WebHookForm(new WebHook()); + public $defaultAction = 'list'; - $url = Console::prompt('Enter webhook url:', [ + public function actionList(): void { + $rows = []; + /** @var WebHook $webHook */ + foreach (WebHook::find()->with('events')->all() as $webHook) { + $rows[] = [$webHook->id, $webHook->url, $webHook->secret, implode(', ', $webHook->events)]; + } + + echo (new Table([ + 'headers' => ['id', 'url', 'secret', 'events'], + 'rows' => $rows, + ]))->run(); + } + + public function actionCreate(): int { + return $this->runForm(new WebHookForm(new WebHook())); + } + + public function actionUpdate(int $id): int { + /** @var WebHook|null $webHook */ + $webHook = WebHook::findOne(['id' => $id]); + if ($webHook === null) { + C::error("Entity with id {$id} isn't found."); + + return ExitCode::DATAERR; + } + + return $this->runForm(new WebHookForm($webHook)); + } + + private function runForm(WebHookForm $form): int { + C::prompt(C::ansiFormat('Enter webhook url:', [C::FG_GREY]), [ 'required' => true, + 'default' => $form->url, 'validator' => function(string $input, ?string &$error) use ($form): bool { $form->url = $input; if (!$form->validate('url')) { @@ -26,34 +57,48 @@ class WebhooksController extends Controller { return true; }, ]); - $secret = Console::prompt('Enter webhook secret (empty to no secret):'); - $options = $form::getEvents(); - $options[''] = 'Finish input'; // It's needed to allow finish input cycle - $events = []; - - do { - $availableOptions = array_diff($options, $events); - $eventIndex = Console::select('Choose wanted events (submit no input to finish):', $availableOptions); - if ($eventIndex !== '') { - $events[] = $options[$eventIndex]; - } - } while ($eventIndex !== '' || empty($events)); - - $form->url = $url; - $form->events = $events; + $secret = C::prompt(C::ansiFormat('Enter webhook secret (empty to no secret):', [C::FG_GREY]), [ + 'default' => $form->secret, + ]); if ($secret !== '') { $form->secret = $secret; } + $allEvents = WebHookForm::getEvents(); + do { + $options = []; + foreach ($allEvents as $id => $option) { + if (in_array($option, $form->events, true)) { + $options["-{$id}"] = $option; // Cast to string to create "-0" index + } else { + $options[$id] = $option; + } + } + + $options[''] = 'Finish input'; // This needed to allow finish input cycle + + $eventIndex = C::select( + C::ansiFormat('Choose wanted events (submit no input to finish):', [C::FG_GREY]), + $options, + ); + if ($eventIndex === '') { + continue; + } + + if ($eventIndex[0] === '-') { + unset($form->events[array_search($options[$eventIndex], $form->events, true)]); + } else { + $form->events[] = $options[$eventIndex]; + } + } while ($eventIndex !== '' || empty($form->events)); + if (!$form->save()) { - Console::error('Unable to create new webhook. Check errors list below' . PHP_EOL . Console::errorSummary($form)); + C::error('Unable to create new webhook. Check errors list below' . PHP_EOL . C::errorSummary($form)); return ExitCode::UNSPECIFIED_ERROR; } return ExitCode::OK; } - // TODO: add action to modify the webhook events - } diff --git a/console/migrations/m200613_204832_remove_webhooks_events_table.php b/console/migrations/m200613_204832_remove_webhooks_events_table.php new file mode 100644 index 0000000..ad115af --- /dev/null +++ b/console/migrations/m200613_204832_remove_webhooks_events_table.php @@ -0,0 +1,51 @@ +addColumn('webhooks', 'events', $this->json()->toString('events') . ' AFTER `secret`'); + $webhooksIds = $this->db->createCommand('SELECT id FROM webhooks')->queryColumn(); + foreach ($webhooksIds as $webhookId) { + $events = $this->db->createCommand("SELECT event_type FROM webhooks_events WHERE webhook_id = {$webhookId}")->queryColumn(); + if (empty($events)) { + continue; + } + + $this->execute('UPDATE webhooks SET events = JSON_ARRAY("' . implode('","', $events) . '")'); + } + + $this->dropTable('webhooks_events'); + } + + public function safeDown() { + $this->createTable('webhooks_events', [ + 'webhook_id' => $this->db->getTableSchema('webhooks')->getColumn('id')->dbType . ' NOT NULL', + 'event_type' => $this->string()->notNull(), + $this->primary('webhook_id', 'event_type'), + ]); + $this->addForeignKey('FK_webhook_event_to_webhook', 'webhooks_events', 'webhook_id', 'webhooks', 'id', 'CASCADE', 'CASCADE'); + + $webhooks = $this->db->createCommand('SELECT id, `events` FROM webhooks')->queryAll(); + foreach ($webhooks as $webhook) { + if (empty($webhook['events'])) { + continue; + } + + $events = json_decode($webhook['events'], true); + if (empty($events)) { + continue; + } + + $this->batchInsert( + 'webhooks_events', + ['webhook_id', 'event_type'], + array_map(fn($event) => [$webhook['id'], $event], $events), + ); + } + + $this->dropColumn('webhooks', 'events'); + } + +} diff --git a/console/models/WebHookForm.php b/console/models/WebHookForm.php index bc84757..0d971b2 100644 --- a/console/models/WebHookForm.php +++ b/console/models/WebHookForm.php @@ -4,9 +4,7 @@ declare(strict_types=1); namespace console\models; use common\models\WebHook; -use common\models\WebHookEvent; use Webmozart\Assert\Assert; -use Yii; use yii\base\Model; class WebHookForm extends Model { @@ -22,6 +20,9 @@ class WebHookForm extends Model { public function __construct(WebHook $webHook, array $config = []) { parent::__construct($config); $this->webHook = $webHook; + $this->url = $webHook->url; + $this->secret = $webHook->secret; + $this->events = (array)$webHook->events; } public function rules(): array { @@ -38,22 +39,12 @@ class WebHookForm extends Model { return false; } - $transaction = Yii::$app->db->beginTransaction(); - $webHook = $this->webHook; $webHook->url = $this->url; $webHook->secret = $this->secret; + $webHook->events = array_values($this->events); // Drop the keys order Assert::true($webHook->save(), 'Cannot save webhook.'); - foreach ($this->events as $event) { - $eventModel = new WebHookEvent(); - $eventModel->webhook_id = $webHook->id; - $eventModel->event_type = $event; - Assert::true($eventModel->save(), 'Cannot save webhook event.'); - } - - $transaction->commit(); - return true; }