diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1fdc056 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,13 @@ +name: CI + +on: + pull_request: null + +jobs: + Silverstripe: + name: 'Silverstripe (bundle)' + uses: nswdpc/ci-files/.github/workflows/silverstripe_5_81.yml@1.1.0 + PHPStan: + name: 'PHPStan (analyse)' + uses: nswdpc/ci-files/.github/workflows/phpstan.silverstripe.yml@1.1.0 + needs: Silverstripe diff --git a/.gitignore b/.gitignore index 7dba068..62e4929 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,6 @@ /vendor/ .DS_Store /.php-cs-fixer.cache -/codeception.yml -/tests/codeception/tests/_output/ +/public/ +/composer.lock +node_modules diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php deleted file mode 100644 index f9b7107..0000000 --- a/.php-cs-fixer.dist.php +++ /dev/null @@ -1,21 +0,0 @@ -in(__DIR__); - -$config = new PhpCsFixer\Config(); -return $config->setRules([ - '@PSR2' => true, - 'array_indentation' => true, - 'array_syntax' => ['syntax' => 'short'], - 'blank_line_after_namespace' => true, - 'blank_line_after_opening_tag' => true, - 'full_opening_tag' => true, - 'no_closing_tag' => true, - ]) - ->setIndent(" ") - ->setFinder($finder); diff --git a/_config/routes.yml b/_config/routes.yml index 58ac1dc..6a32c8d 100644 --- a/_config/routes.yml +++ b/_config/routes.yml @@ -3,7 +3,7 @@ Name: silverstripe-chimple-routes After: - '#coreroutes' --- -Silverstripe\Control\Director: +SilverStripe\Control\Director: rules: 'mc-subscribe/v1': 'NSWDPC\Chimple\Controllers\ChimpleController' --- diff --git a/composer.json b/composer.json index d732728..4251bfc 100644 --- a/composer.json +++ b/composer.json @@ -31,21 +31,27 @@ "client/static" ] }, - "support": { - "key" : "value" - }, + "repositories": [ + { + "type": "git", + "url": "https://github.com/nswdpc/ci-files.git" + } + ], "require": { "dnadesign/silverstripe-elemental": "^5", "silverstripe/framework" : "^5", "silverstripe/siteconfig" : "^5", + "silverstripe/asset-admin": "^2", "symbiote/silverstripe-queuedjobs": "^5", "drewm/mailchimp-api" : "^2.5", "symbiote/silverstripe-multivaluefield" : "^6" }, "require-dev": { + "cambis/silverstripe-rector": "^0.5.1", "phpunit/phpunit": "^9.5", - "friendsofphp/php-cs-fixer": "^3", - "vlucas/phpdotenv": "^5" + "syntro/silverstripe-phpstan": "^5", + "vlucas/phpdotenv": "^5", + "nswdpc/ci-files": "dev-main" }, "autoload": { "psr-4": { @@ -60,5 +66,15 @@ "NSWDPC\\Chimple\\Traits\\": "src/Traits/", "NSWDPC\\Chimple\\Tests\\": "tests/" } + }, + "prefer-stable": true, + "minimum-stability": "dev", + "config": { + "allow-plugins": { + "composer/installers": true, + "silverstripe/recipe-plugin": true, + "silverstripe/vendor-plugin": true, + "phpstan/extension-installer": true + } } } diff --git a/src/Controllers/ChimpleController.php b/src/Controllers/ChimpleController.php index ae59087..820fc1e 100644 --- a/src/Controllers/ChimpleController.php +++ b/src/Controllers/ChimpleController.php @@ -31,21 +31,11 @@ */ class ChimpleController extends PageController { + private static string $url_segment = 'mc-subscribe/v1'; - /** - * @var string - */ - private static $url_segment = 'mc-subscribe/v1'; - - /** - * @var bool - */ - private static $hide_generic_form = true; + private static bool $hide_generic_form = true; - /** - * @var array - */ - private static $allowed_actions = [ + private static array $allowed_actions = [ 'SubscribeForm', 'XhrSubscribeForm' ]; @@ -67,17 +57,11 @@ public function index() public function pageTitle($complete = null) { - switch($complete) { - case 'y': - return _t(__CLASS__. '.DEFAULT_TITLE_SUCCESSFUL', 'Thanks for subscribing'); - break; - case 'n': - return _t(__CLASS__. '.DEFAULT_TITLE_NOT_SUCCESSFUL', 'Sorry, there was an error'); - break; - default: - return _t(__CLASS__. '.DEFAULT_TITLE', 'Subscribe'); - break; - } + return match ($complete) { + 'y' => _t(self::class. '.DEFAULT_TITLE_SUCCESSFUL', 'Thanks for subscribing'), + 'n' => _t(self::class. '.DEFAULT_TITLE_NOT_SUCCESSFUL', 'Sorry, there was an error'), + default => _t(self::class. '.DEFAULT_TITLE', 'Subscribe'), + }; } public function Link($action = null) @@ -104,7 +88,7 @@ public function HideGenericChimpleForm() * This returns false to avoid the form being included in generic $Form templates/layouts * Use $ChimpleSubscribeForm('some-code') in templates instead */ - public function Form() + public function Form(): bool { return false; } @@ -112,7 +96,8 @@ public function Form() /** * Set a suffix form the form name */ - public function setFormNameSuffix(string $suffix = '') : self { + public function setFormNameSuffix(string $suffix = ''): self + { $suffix = trim($suffix); $this->formNameSuffix = $suffix; return $this; @@ -121,32 +106,23 @@ public function setFormNameSuffix(string $suffix = '') : self { /** * Return a suffix to use with the form name */ - public function getFormNameSuffix() : string { - if($this->formNameSuffix) { - $suffix = "_{$this->formNameSuffix}"; - } else { - $suffix = ""; - } - return $suffix; + public function getFormNameSuffix(): string + { + return $this->formNameSuffix ? "_{$this->formNameSuffix}" : ""; } /** * Get a subscription form based on parameters */ - public function getSubscriptionForm($useXhr = false) : ?SubscribeForm { - if($useXhr) { - $form = $this->XhrSubscribeForm(); - } else { - $form = $this->SubscribeForm(); - } - return $form; + public function getSubscriptionForm($useXhr = false): SubscribeForm|XhrSubscribeForm|null + { + return $useXhr ? $this->XhrSubscribeForm() : $this->SubscribeForm(); } /** * Return a subscription form if it is enabled - * @link MailchimpConfig::SubscribeForm */ - public function XhrSubscribeForm() : XhrSubscribeForm + public function XhrSubscribeForm(): ?XhrSubscribeForm { $enabled = MailchimpConfig::isEnabled(); @@ -163,7 +139,7 @@ public function XhrSubscribeForm() : XhrSubscribeForm ); $form = $this->configureForm($form); - $form->setFormAction( $this->Link('XhrSubscribeForm') ); + $form->setFormAction($this->Link('XhrSubscribeForm')); // Set a validation response callback handling for XHR form submissions $form->setValidationResponseCallback($this->getCallbackForXhrValidation()); @@ -171,14 +147,14 @@ public function XhrSubscribeForm() : XhrSubscribeForm // this form doesn't need to retain state $form->clearMessage(); - return $form; + // phpstan return type checking requirement + return $form instanceof XhrSubscribeForm ? $form : null; } /** * Return a subscription form if it is enabled - * @link MailchimpConfig::SubscribeForm */ - public function SubscribeForm() + public function SubscribeForm(): ?SubscribeForm { $enabled = MailchimpConfig::isEnabled(); @@ -195,7 +171,7 @@ public function SubscribeForm() ); $form = $this->configureForm($form); - $form->setFormAction( $this->Link('SubscribeForm') ); + $form->setFormAction($this->Link('SubscribeForm')); // Handle error validation with custom callback $form->setValidationResponseCallback($this->getCallbackForValidation($form)); @@ -206,7 +182,8 @@ public function SubscribeForm() /** * Apply common configuration to a subscription form */ - protected function configureForm(SubscribeForm $form) : SubscribeForm { + protected function configureForm(SubscribeForm $form): SubscribeForm + { // Form JS, incl XHR handling Requirements::javascript( 'nswdpc/silverstripe-chimple:client/static/js/chimple.js' @@ -228,39 +205,39 @@ protected function configureForm(SubscribeForm $form) : SubscribeForm { /** * Get fields for the form */ - protected function getFields() : FieldList { - $fields = FieldList::create( - $name = TextField::create('Name', _t(__CLASS__. '.NAME', 'Name')) - ->setAttribute('placeholder', _t(__CLASS__. '.YOUR_NAME', 'Your name')) - ->setAttribute('title', _t(__CLASS__. '.NAME', 'Name')) + protected function getFields(): FieldList + { + return FieldList::create( + $name = TextField::create('Name', _t(self::class. '.NAME', 'Name')) + ->setAttribute('placeholder', _t(self::class. '.YOUR_NAME', 'Your name')) + ->setAttribute('title', _t(self::class. '.NAME', 'Name')) ->setAttribute('required', 'required'), - $email = EmailField::create('Email', _t(__CLASS__. '.EMAIL', 'Email')) - ->setAttribute('placeholder', _t(__CLASS__. '.EMAIL_ADDRESS', 'Email address')) - ->setAttribute('title', _t(__CLASS__. '.EMAIL', 'Email')) + $email = EmailField::create('Email', _t(self::class. '.EMAIL', 'Email')) + ->setAttribute('placeholder', _t(self::class. '.EMAIL_ADDRESS', 'Email address')) + ->setAttribute('title', _t(self::class. '.EMAIL', 'Email')) ->setAttribute('required', 'required') ); - return $fields; } /** * Get actions for the form */ - protected function getActions() : FieldList { - $actions = FieldList::create( + protected function getActions(): FieldList + { + return FieldList::create( FormAction::create( 'subscribe', - _t(__CLASS__ . '.SUBSCRIBE', 'Subscribe') + _t(self::class . '.SUBSCRIBE', 'Subscribe') )->setUseButtonTag(true) ->addExtraClass('signup') ); - return $actions; } /** * Return the default validator for the form. - * @returns Validator|null */ - protected function getValidator() : ?Validator { + protected function getValidator(): ?Validator + { return RequiredFields::create(['Name','Email']); } @@ -268,13 +245,13 @@ protected function getValidator() : ?Validator { * Returns the validation callback upon errors * A response is returned only upon errors in XHR submissions * See FormRequestHandler::getValidationErrorResponse(); - * @return callable */ - protected function getCallbackForXhrValidation() : callable { - return function(ValidationResult $result) { + protected function getCallbackForXhrValidation(): callable + { + return function (ValidationResult $result): \SilverStripe\Control\HTTPResponse { // Fail, using the first message returned from the validation result $messages = $result->getMessages(); - $message = (!empty($messages[0]['message']) ? $messages[0]['message'] : ''); + $message = (empty($messages[0]['message']) ? '' : $messages[0]['message']); return $this->xhrError(400, $message); }; } @@ -282,8 +259,9 @@ protected function getCallbackForXhrValidation() : callable { /** * Callback validator for SubscribeForm, avoid redirectBack() */ - protected function getCallbackForValidation(SubscribeForm $form) : callable { - return function(ValidationResult $result) use ($form) { + protected function getCallbackForValidation(SubscribeForm $form): callable + { + return function (ValidationResult $result) use ($form): \SilverStripe\Control\HTTPResponse { // Prior to redirection, persist this result in session to re-display on redirect $form->setSessionValidationResult($result); $form->setSessionData($form->getData()); @@ -299,28 +277,32 @@ protected function getCallbackForValidation(SubscribeForm $form) : callable { /** * Handle errors, based on the request type */ - private function handleError($code, $error_message, Form $form = null) { + private function handleError($code, $error_message, Form $form = null): ?\SilverStripe\Control\HTTPResponse + { if($this->request->isAjax()) { return $this->xhrError($code, $error_message); - } else if($form) { + } elseif($form instanceof \SilverStripe\Forms\Form) { // set session error on the form $form->sessionError($error_message, ValidationResult::TYPE_ERROR); } - return; + + return null; } /** * Handle successful submissions, based on the request type */ - private function handleSuccess($code, $message, Form $form = null) { + private function handleSuccess(int $code, Form $form = null): ?\SilverStripe\Control\HTTPResponse + { $success_message = Config::inst()->get(MailchimpConfig::class, 'success_message'); if($this->request->isAjax()) { - return $this->xhrSuccess($code, $message, $success_message); - } else if($form) { + return $this->xhrSuccess($code, $success_message); + } elseif($form instanceof \SilverStripe\Forms\Form) { // set session message on the form $form->sessionMessage($success_message, ValidationResult::TYPE_GOOD); } - return; + + return null; } /** @@ -332,55 +314,50 @@ public function subscribe($data = [], Form $form = null) try { $response = null; - $code = "";// MailchimpConfig.Code + $code = strip_tags(trim($data['code'] ?? ''));// MailchimpConfig.Code $list_id = ""; + $mc_config = null; + $error_message = ""; + $email = $data['Email'] ?? ''; - if(!$form) { + if(!$form instanceof \SilverStripe\Forms\Form) { throw new RequestException("Forbidden", 403); } - $mc_config = null; - - if(empty($data['code'])) { + if($code === "") { // fail with error $error_message = _t( - __CLASS__ . '.NO_CODE', + self::class . '.NO_CODE', "No code was provided" ); $error_code = 400;// default to invalid data - } else { - - $code = strip_tags(trim($data['code'] ?: '')); $error_message = ""; $error_code = 400;// default to invalid data - $mc_config = null; - } $enabled = MailchimpConfig::isEnabled(); if(!$enabled) { $error_message = _t( - __CLASS__ . '.SUBSCRIPTIONS_NOT_AVAILABLE', + self::class . '.SUBSCRIPTIONS_NOT_AVAILABLE', "Subscriptions are not available at the moment" ); } // proceed with Email checking... if (!$error_message) { - if (empty($data['Email'])) { + if ($email === '') { // fail with error $error_message = _t( - __CLASS__ . '.NO_EMAIL_ADDRESS', + self::class . '.NO_EMAIL_ADDRESS', "No e-mail address was provided" ); - } - if (!Email::is_valid_address($data['Email'])) { + } elseif (!Email::is_valid_address($email)) { $error_message = _t( - __CLASS__ . '.INVALID_EMAIL_ADDRESS', + self::class . '.INVALID_EMAIL_ADDRESS', "Please provide a valid e-mail address, '{email}' is not valid", [ - 'email' => htmlspecialchars($data['Email']) + 'email' => htmlspecialchars((string) $data['Email']) ] ); } @@ -388,26 +365,27 @@ public function subscribe($data = [], Form $form = null) if (!$error_message) { // check code provided - if (!$code) { + if ($code === '' || $code === '0') { $error_message = _t( - __CLASS__ . ".GENERIC_ERROR_1", + self::class . ".GENERIC_ERROR_1", "Sorry, the sign-up could not be completed" ); } else { $mc_config = MailchimpConfig::getConfig('', '', $code); - if (empty($mc_config->ID)) { + if ($mc_config instanceof \NSWDPC\Chimple\Models\MailchimpConfig) { + $list_id = $mc_config->getMailchimpListId(); + } else { $error_message = _t( - __CLASS__ . ".GENERIC_ERROR_2", + self::class . ".GENERIC_ERROR_2", "Sorry, the sign-up could not be completed" ); } - $list_id = $mc_config->getMailchimpListId(); } } - if (!$list_id) { + if ($list_id === null || $list_id === '' || $list_id === '0') { $error_message = _t( - __CLASS__ . ".GENERIC_ERROR_3", + self::class . ".GENERIC_ERROR_3", "Sorry, the sign-up could not be completed" ); } @@ -426,20 +404,20 @@ public function subscribe($data = [], Form $form = null) ]) // for the Email or the MD5 of it ->filterAny([ - 'Email' => $data['Email'],// match on email address provided - 'SubscribedId' => MailchimpSubscriber::getMailchimpSubscribedId($data['Email'])// OR may not have the email anymore + 'Email' => $email,// match on email address provided + 'SubscribedId' => MailchimpSubscriber::getMailchimpSubscribedId($email)// OR may not have the email anymore ])->first(); if (empty($sub->ID)) { $sub = MailchimpSubscriber::create(); - $sub->Name = $data['Name']; - $sub->Email = $data['Email']; + $sub->Name = $data['Name'] ?? ''; + $sub->Email = $email; $sub->MailchimpListId = $list_id;//list they are subscribing to $sub->Status = MailchimpSubscriber::CHIMPLE_STATUS_NEW; - $sub->Tags = $mc_config->Tags; + $sub->Tags = $mc_config instanceof \NSWDPC\Chimple\Models\MailchimpConfig ? $mc_config->Tags : null; $sub_id = $sub->write(); if (!$sub_id) { - throw new RequestException("502", "Bad Gateway"); + throw new RequestException("Bad Gateway", 502); } } @@ -450,8 +428,8 @@ public function subscribe($data = [], Form $form = null) } // handle a successful subscription - $response = $this->handleSuccess(200, "OK", $form); - if($response && ($response instanceof HTTPResponse)) { + $response = $this->handleSuccess(200, $form); + if($response instanceof \SilverStripe\Control\HTTPResponse && ($response instanceof HTTPResponse)) { // handle responses for e.g XHR return $response; } else { @@ -466,7 +444,7 @@ public function subscribe($data = [], Form $form = null) } catch (RequestException $e) { $error_message = $e->getMessage(); $error_code = $e->getCode(); - } catch (\Exception $e) { + } catch (\Exception) { // general exceptin $error_message = Config::inst()->get(MailchimpConfig::class, 'error_message'); $error_code = 500; @@ -474,7 +452,7 @@ public function subscribe($data = [], Form $form = null) // Handle subscribe attempt failures $response = $this->handleError($error_code, $error_message, $form); - if($response && ($response instanceof HTTPResponse)) { + if($response instanceof \SilverStripe\Control\HTTPResponse && ($response instanceof HTTPResponse)) { // handle XHR error responses return $response; } else { @@ -490,26 +468,26 @@ public function subscribe($data = [], Form $form = null) /** * Return error response for XHR - * @return HTTPResponse */ - private function xhrError($code = 500, $message = "", $description = "") { - $response = new HTTPResponse(); + private function xhrError($code = 500, $message = ""): HTTPResponse + { + $response = \SilverStripe\Control\HTTPResponse::create(); $response->setStatusCode($code); $response->addHeader('Content-Type', 'application/json'); - $response->addHeader('X-Submission-OK', 0); + $response->addHeader('X-Submission-OK', '0'); $response->addHeader('X-Submission-Description', $message); return $response; } /** * Return success response for XHR - * @return HTTPResponse */ - private function xhrSuccess($code = 200, $message = "", $description = "") { - $response = new HTTPResponse(); + private function xhrSuccess(int $code = 200, $description = ""): HTTPResponse + { + $response = \SilverStripe\Control\HTTPResponse::create(); $response->setStatusCode($code); $response->addHeader('Content-Type', 'application/json'); - $response->addHeader('X-Submission-OK', 1); + $response->addHeader('X-Submission-OK', '1'); $response->addHeader('X-Submission-Description', $description); return $response; } diff --git a/src/Controllers/MailchimpAdmin.php b/src/Controllers/MailchimpAdmin.php index 8d40dc6..3f65815 100644 --- a/src/Controllers/MailchimpAdmin.php +++ b/src/Controllers/MailchimpAdmin.php @@ -13,12 +13,12 @@ */ class MailchimpAdmin extends ModelAdmin { - private static $managed_models = [ + private static array $managed_models = [ MailchimpSubscriber::class, MailchimpConfig::class ]; - private static $menu_title = 'Mailchimp'; + private static string $menu_title = 'Mailchimp'; - private static $url_segment = 'mailchimp'; + private static string $url_segment = 'mailchimp'; } diff --git a/src/Exceptions/RequestException.php b/src/Exceptions/RequestException.php index 8c43bff..7df4327 100644 --- a/src/Exceptions/RequestException.php +++ b/src/Exceptions/RequestException.php @@ -6,4 +6,6 @@ * An exception thrown when an incoming subscription request is determined to be * invalid */ -class RequestException extends \Exception {} +class RequestException extends \Exception +{ +} diff --git a/src/Extensions/DisableSecurityTokenExtension.php b/src/Extensions/DisableSecurityTokenExtension.php index 06919c0..eb4bc68 100644 --- a/src/Extensions/DisableSecurityTokenExtension.php +++ b/src/Extensions/DisableSecurityTokenExtension.php @@ -2,7 +2,7 @@ namespace NSWDPC\Chimple\Extensions; -use Silverstripe\Core\Extension; +use SilverStripe\Core\Extension; /** * This extension can be applied at the project level in situations where @@ -12,7 +12,8 @@ */ class DisableSecurityTokenExtension extends Extension { - public function updateChimpleSubscribeForm() { - $this->owner->disableSecurityToken(); + public function updateChimpleSubscribeForm() + { + $this->getOwner()->disableSecurityToken(); } } diff --git a/src/Extensions/PageExtension.php b/src/Extensions/PageExtension.php index 40f5463..2d83c05 100644 --- a/src/Extensions/PageExtension.php +++ b/src/Extensions/PageExtension.php @@ -2,6 +2,7 @@ namespace NSWDPC\Chimple\Extensions; +use NSWDPC\Chimple\Forms\SubscribeForm; use NSWDPC\Chimple\Models\MailchimpConfig; use SilverStripe\SiteConfig\SiteConfig; use SilverStripe\Core\Config\Config; @@ -10,30 +11,30 @@ class PageExtension extends Extension { - /** * Returns a form for the configuration marked 'IsGlobal' - * @return Form|null */ - public function ChimpleGlobalSubscribeForm() { + public function ChimpleGlobalSubscribeForm(): ?SubscribeForm + { $config = MailchimpConfig::getGlobalConfig(); if ($config) { return $config->SubscribeForm(); } + return null; } /** * Returns a form based on a config code - * @return Form|null * @param string $config_code a MailchimpConfig.Code value (not an audience ID) */ - public function ChimpleSubscribeForm($config_code) + public function ChimpleSubscribeForm(string $config_code): ?SubscribeForm { $config = MailchimpConfig::getConfig('', '', $config_code); - if($config) { + if($config instanceof \NSWDPC\Chimple\Models\MailchimpConfig) { return $config->SubscribeForm(); } + return null; } } diff --git a/src/Extensions/SiteConfigExtension.php b/src/Extensions/SiteConfigExtension.php index e9d1459..64b9d3b 100644 --- a/src/Extensions/SiteConfigExtension.php +++ b/src/Extensions/SiteConfigExtension.php @@ -4,21 +4,22 @@ use NSWDPC\Chimple\Models\MailchimpConfig; -use Silverstripe\ORM\DataExtension; +use SilverStripe\ORM\DataExtension; use SilverStripe\Forms\CheckboxField; -use Silverstripe\Forms\FieldList; -use Silverstripe\Forms\DropdownField; +use SilverStripe\Forms\FieldList; +use SilverStripe\Forms\DropdownField; class SiteConfigExtension extends DataExtension { - private static $db = [ + private static array $db = [ 'MailchimpEnabled' => 'Boolean' ]; - private static $has_one = [ + private static array $has_one = [ 'MailchimpConfig' => MailchimpConfig::class // global element for configuration ]; + #[\Override] public function updateCmsFields(FieldList $fields) { $fields->addFieldsToTab( @@ -26,25 +27,24 @@ public function updateCmsFields(FieldList $fields) [ CheckboxField::create( 'MailchimpEnabled', - _t(__CLASS__. '.MAILCHIMP_ENABLED', 'Mailchimp subscriptions enabled') + _t(self::class. '.MAILCHIMP_ENABLED', 'Mailchimp subscriptions enabled') ), DropdownField::create( 'MailchimpConfigID', - _t(__CLASS__. '.DEFAULT_MAILCHIMP_CONFIG', 'Default Mailchimp configuration'), + _t(self::class. '.DEFAULT_MAILCHIMP_CONFIG', 'Default Mailchimp configuration'), MailchimpConfig::get()->map('ID', 'TitleCode') )->setEmptyString('') ] ); } + #[\Override] public function onAfterWrite() { parent::onAfterWrite(); - if($this->owner->MailchimpConfigID) { - if($config = MailchimpConfig::get()->byId($this->owner->MailchimpConfigID)) { - $config->IsGlobal = 1; - $config->write(); - } + if ($this->getOwner()->MailchimpConfigID && ($config = MailchimpConfig::get()->byId($this->getOwner()->MailchimpConfigID))) { + $config->IsGlobal = 1; + $config->write(); } } diff --git a/src/Forms/SubscribeForm.php b/src/Forms/SubscribeForm.php index 48c64b6..777fa21 100644 --- a/src/Forms/SubscribeForm.php +++ b/src/Forms/SubscribeForm.php @@ -10,8 +10,8 @@ * Subscription form subclass of {@link SilverStripe\Forms\Form} * Allows overrides of default form behaviour */ -class SubscribeForm extends Form { - +class SubscribeForm extends Form +{ use SubscriptionForm; } diff --git a/src/Forms/XhrSubscribeForm.php b/src/Forms/XhrSubscribeForm.php index d7e196a..aabfdbe 100644 --- a/src/Forms/XhrSubscribeForm.php +++ b/src/Forms/XhrSubscribeForm.php @@ -13,13 +13,12 @@ * Subscription form subclass to handle submissions via XHR * Allows overrides of default form behaviour */ -class XhrSubscribeForm extends SubscribeForm { - +class XhrSubscribeForm extends SubscribeForm +{ /** * Set to true if forms of this class will appear on a publicly cacheable page - * @var bool */ - private static $disable_security_token = false; + private static bool $disable_security_token = false; public function __construct( RequestHandler $controller = null, @@ -37,6 +36,7 @@ public function __construct( /** * @inheritdoc */ + #[\Override] protected function canBeCached() { $token = $this->getSecurityToken(); @@ -51,6 +51,7 @@ protected function canBeCached() * @inheritdoc * Add attributes */ + #[\Override] protected function getDefaultAttributes(): array { $attributes = parent::getDefaultAttributes(); diff --git a/src/Jobs/MailchimpCleanupJob.php b/src/Jobs/MailchimpCleanupJob.php index 576d3a1..610a606 100644 --- a/src/Jobs/MailchimpCleanupJob.php +++ b/src/Jobs/MailchimpCleanupJob.php @@ -19,46 +19,52 @@ class MailchimpCleanupJob extends AbstractQueuedJob implements QueuedJob { use Configurable; - private static $run_in_minutes = 30; + private static int $run_in_minutes = 30; public function __construct($minutes_ago = 30, $limit = 0, $report_only = 0) { + $minutes_ago = (int)$minutes_ago; + $limit = (int)$limit; + $report_only = (int)$report_only; $this->report_only = $report_only; - $this->minutes_ago = $minutes_ago; - $this->limit = $limit; + + $this->minutes_ago = $minutes_ago > 0 ? $minutes_ago : 30; + $this->limit = max($limit, 0); } public function getTitle() { - return sprintf( - _t( - __CLASS__ . '.TITLE', - "Mailchimp cleanup job - minutes=%s limit=%s report_only=%s" - ), - $this->minutes_ago, - $this->limit, - $this->report_only + return _t( + self::class . '.TITLE', + "Mailchimp cleanup job - minutes={minutes} limit={limit} report_only={report_only}", + [ + 'minutes' => $this->minutes_ago, + 'limit' => $this->limit, + 'report_only' => $this->report_only + ] ); } + #[\Override] public function getJobType() { return QueuedJob::QUEUED; } + #[\Override] public function setup() { $this->totalSteps = 1; } + #[\Override] public function process() { $this->processSubscriptions(); $this->isComplete = true; - return; } - private function processSubscriptions() + private function processSubscriptions(): bool { $prune_datetime = new DateTime(); $minutes = $this->minutes_ago; @@ -87,18 +93,20 @@ private function processSubscriptions() $subscriber->delete(); $success_deletes++; } + $this->addMessage("Deleted {$success_deletes} subscribers with status " . MailchimpSubscriber::CHIMPLE_STATUS_SUCCESS); } - - $this->currentStep = $this->totalSteps = $success_deletes; + $this->currentStep = $success_deletes; + $this->totalSteps = $success_deletes; // remove stale failed subscriptions older than 7 days $fail_datetime = new DateTime(); $fail_datetime->modify('-7 days'); + $failed = MailchimpSubscriber::get()->filter([ - 'Status' => MailchimpSubscriber::CHIMPLE_STATUS_FAIL, - 'Created:LessThan' => $fail_datetime->format('Y-m-d H:i:s') + 'Status' => MailchimpSubscriber::CHIMPLE_STATUS_FAIL, + 'Created:LessThan' => $fail_datetime->format('Y-m-d H:i:s') ]); if ($this->report_only) { @@ -109,11 +117,12 @@ private function processSubscriptions() $failed_subscriber->delete(); $failed_deletes++; } + $this->addMessage("Deleted {$failed_deletes} subscribers with status " . MailchimpSubscriber::CHIMPLE_STATUS_FAIL); } - - $this->currentStep = $this->totalSteps = ($success_deletes + $failed_deletes); + $this->currentStep = $success_deletes + $failed_deletes; + $this->totalSteps = $success_deletes + $failed_deletes; return true; } @@ -121,16 +130,18 @@ private function processSubscriptions() /** * Get next configured run time */ - private function getNextRunMinutes() + private function getNextRunMinutes(): int { $minutes = (int)$this->config()->get('run_in_minutes'); if ($minutes <= 2) { // min every 2 minutes $minutes = 2; } + return $minutes; } + #[\Override] public function afterComplete() { $run_datetime = new DateTime(); diff --git a/src/Jobs/MailchimpSubscribeJob.php b/src/Jobs/MailchimpSubscribeJob.php index 5c2a6a7..ceed02a 100644 --- a/src/Jobs/MailchimpSubscribeJob.php +++ b/src/Jobs/MailchimpSubscribeJob.php @@ -14,46 +14,54 @@ class MailchimpSubscribeJob extends AbstractQueuedJob implements QueuedJob { use Configurable; - private static $run_in_seconds = 60; - private static $default_limit = 100; + private static int $run_in_seconds = 60; + + private static int $default_limit = 100; public function __construct($limit = 100, $report_only = 0) { + $limit = (int)$limit; + $report_only = (int)$report_only; $this->report_only = $report_only; - $this->limit = (int)$limit; + + $this->limit = max($limit, 0); } public function getTitle() { - $title = _t( - __CLASS__ . '.TITLE', - "Batch subscribe emails to Mailchimp" + return _t( + self::class . '.TITLE', + "Batch subscribe emails to Mailchimp (limit: {limit}, report only: {report_only})", + [ + 'limit' => $this->limit, + 'report_only' => $this->report_only + ] ); - $title .= " (limit:{$this->limit})"; - $title .= ($this->report_only ? " - report only" : ""); - return $title; } + #[\Override] public function getJobType() { return QueuedJob::QUEUED; } + #[\Override] public function setup() { $this->totalSteps = 1; } - private function getTotalResults($results) + private function getTotalResults(array $results): int|float { $total = 0; - foreach ($results as $key => $count) { + foreach ($results as $count) { $total += $count; } + return $total; } - private function getTotalNonFailedResults($results) + private function getTotalNonFailedResults(array $results): int|float { $copy = $results; unset($copy[ MailchimpSubscriber::CHIMPLE_STATUS_FAIL]); @@ -63,29 +71,35 @@ private function getTotalNonFailedResults($results) /** * Process the job */ + #[\Override] public function process() { $results = MailchimpSubscriber::batch_subscribe($this->limit, $this->report_only); if ($this->report_only) { $this->addMessage("Report only: " . json_encode($results)); - $this->totalSteps = $this->currentStep = $this->getTotalResults($results); + $this->totalSteps = $this->getTotalResults($results); + $this->currentStep = $this->totalSteps; } elseif (is_array($results)) { foreach ($results as $status => $count) { $message = $status . ":" . $count; $this->addMessage($message); } + $this->totalSteps = $this->getTotalResults($results);// total including failed $this->currentStep = $this->getTotalNonFailedResults($results); } else { $this->addMessage("Failed completely - check logs!"); - $this->totalSteps = $this->currentStep = 0; + $this->totalSteps = 0; + $this->currentStep = 0; } + $this->isComplete = true; } /** * Recreate the MailchimpSubscribeJob job for the next run */ + #[\Override] public function afterComplete() { $run_datetime = new DateTime(); @@ -94,6 +108,7 @@ public function afterComplete() // min every 30s $seconds = 30; } + $run_datetime->modify("+{$seconds} seconds"); singleton(QueuedJobService::class)->queueJob( new MailchimpSubscribeJob($this->limit, $this->report_only), diff --git a/src/Models/Elements/ElementChimpleSubscribe.php b/src/Models/Elements/ElementChimpleSubscribe.php index 0c8c4e3..10744b5 100644 --- a/src/Models/Elements/ElementChimpleSubscribe.php +++ b/src/Models/Elements/ElementChimpleSubscribe.php @@ -3,6 +3,7 @@ namespace NSWDPC\Chimple\Models\Elements; use DNADesign\Elemental\Models\BaseElement; +use NSWDPC\Chimple\Forms\SubscribeForm; use NSWDPC\Chimple\Models\MailchimpConfig; use SilverStripe\Admin\LeftAndMain; use SilverStripe\Assets\Image; @@ -23,46 +24,48 @@ */ class ElementChimpleSubscribe extends BaseElement { + private static string $table_name = 'ElementChimpleSubscribe'; - private static $table_name = 'ElementChimpleSubscribe'; + private static string $singular_name = 'Mailchimp subscribe'; - private static $singular_name = 'Mailchimp subscribe'; - private static $plural_name = 'Mailchimp subscribe'; + private static string $plural_name = 'Mailchimp subscribe'; - private static $icon = 'font-icon-up-circled'; + private static string $icon = 'font-icon-up-circled'; - private static $db = [ + private static array $db = [ 'UseXHR' => 'Boolean',// whether to submit without redirect 'BeforeFormContent' => 'HTMLText', 'AfterFormContent' => 'HTMLText', 'ImageAlignment' => 'Varchar(32)', ]; - private static $defaults = [ + private static array $defaults = [ 'UseXHR' => 1 ]; /** * Has_one relationship - * @var array */ - private static $has_one = [ + private static array $has_one = [ 'MailchimpConfig' => MailchimpConfig::class, 'Image' => Image::class ]; - private static $owns = [ + private static array $owns = [ 'Image' ]; + #[\Override] public function getType() { - return _t(__CLASS__ . '.BlockType', 'Mailchimp Subscribe'); + return _t(self::class . '.BlockType', 'Mailchimp Subscribe'); } - private static $title = 'Mailchimp subscribe'; - private static $description = 'Provide a mailchimp subscription form'; + private static string $title = 'Mailchimp subscribe'; + private static string $description = 'Provide a mailchimp subscription form'; + + #[\Override] public function getCMSFields() { $fields = parent::getCMSFields(); @@ -74,7 +77,7 @@ public function getCMSFields() LiteralField::create( 'NotEnabled', '
' ) ); @@ -85,42 +88,43 @@ public function getCMSFields() ]); $fields->addFieldsToTab( - 'Root.Main', [ + 'Root.Main', + [ DropdownField::create( 'MailchimpConfigID', _t( - __CLASS__ . '.SELECT_CONFIGURATION', + self::class . '.SELECT_CONFIGURATION', 'Select the list configuration to use for this subscription form' ), - MailchimpConfig::get()->sort('Title ASC')->map('ID','TitleWithDetails') + MailchimpConfig::get()->sort('Title ASC')->map('ID', 'TitleWithDetails') )->setEmptyString(''), CheckboxField::create( 'UseXHR', _t( - __CLASS__ . '.USE_XHR', + self::class . '.USE_XHR', 'Submit without redirecting' ), ), HTMLEditorField::create( 'BeforeFormContent', _t( - __CLASS__ . '.BEFORE_CONTENT', + self::class . '.BEFORE_CONTENT', 'Content to show before form' ) )->setRows(6), HTMLEditorField::create( 'AfterFormContent', _t( - __CLASS__ . '.AFTER_CONTENT', + self::class . '.AFTER_CONTENT', 'Content to show after form' ) )->setRows(6), UploadField::create( 'Image', - _t(__CLASS__ . '.IMAGE', 'Image') + _t(self::class . '.IMAGE', 'Image') )->setTitle( _t( - __CLASS__ . '.ADD_IMAGE_TO_CONTENT_BLOCK', + self::class . '.ADD_IMAGE_TO_CONTENT_BLOCK', 'Add an image' ) )->setFolderName('blocks/content/' . $this->owner->ID) @@ -128,14 +132,14 @@ public function getCMSFields() ->setIsMultiUpload(false), DropdownField::create( 'ImageAlignment', - _t(__CLASS__ . '.IMAGE_ALIGNMENT', 'Image alignment'), + _t(self::class . '.IMAGE_ALIGNMENT', 'Image alignment'), [ - 'left' => _t(__CLASS__ . '.LEFT', 'Left'), - 'right' => _t(__CLASS__ . '.RIGHT', 'Right') + 'left' => _t(self::class . '.LEFT', 'Left'), + 'right' => _t(self::class . '.RIGHT', 'Right') ] )->setEmptyString('Choose an option') ->setDescription( - _t(__CLASS__ . '.IMAGE_ALIGNMENT_DESCRIPTION', 'Use of this option is dependent on the theme') + _t(self::class . '.IMAGE_ALIGNMENT_DESCRIPTION', 'Use of this option is dependent on the theme') ) ] ); @@ -146,9 +150,9 @@ public function getCMSFields() /** * Provide $SubscribeForm for template * When called in the context of the administration area, return null - * @return Form|null */ - public function getSubscribeForm() { + public function getSubscribeForm(): ?SubscribeForm + { if(Controller::curr() instanceof LeftAndMain) { return null; @@ -156,9 +160,10 @@ public function getSubscribeForm() { if($config = $this->MailchimpConfig()) { // render the form with this element's XHR setting overriding the config being used - $form = $config->SubscribeForm( $this->UseXHR == 1 ); + $form = $config->SubscribeForm($this->UseXHR == 1); return $form; } + return null; } } diff --git a/src/Models/MailchimpConfig.php b/src/Models/MailchimpConfig.php index dbb2e2f..e5f6900 100644 --- a/src/Models/MailchimpConfig.php +++ b/src/Models/MailchimpConfig.php @@ -3,6 +3,7 @@ namespace NSWDPC\Chimple\Models; use NSWDPC\Chimple\Controllers\ChimpleController; +use NSWDPC\Chimple\Forms\SubscribeForm; use NSWDPC\Chimple\Services\Logger; use SilverStripe\Core\Convert; use SilverStripe\Core\Config\Config; @@ -13,7 +14,7 @@ use SilverStripe\Forms\TextField; use SilverStripe\Forms\CheckboxField; use SilverStripe\Forms\HTMLEditor\HTMLEditorField; -use Silverstripe\ORM\DataObject; +use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DB; use SilverStripe\ORM\FieldType\DBHTMLText; use SilverStripe\Security\PermissionProvider; @@ -30,26 +31,29 @@ */ class MailchimpConfig extends DataObject implements TemplateGlobalProvider, PermissionProvider { + private static string $list_id = ""; - private static $list_id = "";// default list (audience) ID - private static $api_key = "";// API key provided by Mailchimp + // default list (audience) ID + private static string $api_key = "";// API key provided by Mailchimp - private static $success_message = "Thank you for subscribing. You will receive an email to confirm your subscription shortly."; - private static $error_message = "Sorry, we could not subscribe that email address at the current time. Please try again later."; + private static string $success_message = "Thank you for subscribing. You will receive an email to confirm your subscription shortly."; - private static $table_name = 'ChimpleConfig'; + private static string $error_message = "Sorry, we could not subscribe that email address at the current time. Please try again later."; - private static $singular_name = 'Mailchimp Configuration'; - private static $plural_name = 'Mailchimp Configurations'; + private static string $table_name = 'ChimpleConfig'; - private static $title = "Mailchimp Subscriber Form"; - private static $description = "Configuration for a Mailchimp subscribe form"; + private static string $singular_name = 'Mailchimp Configuration'; + + private static string $plural_name = 'Mailchimp Configurations'; + + private static string $title = "Mailchimp Subscriber Form"; + + private static string $description = "Configuration for a Mailchimp subscribe form"; /** * Database fields - * @var array */ - private static $db = [ + private static array $db = [ 'Title' => 'Varchar(255)', 'Code' => 'Varchar(255)',// auto created, used to identify config 'IsGlobal' => 'Boolean', @@ -70,9 +74,8 @@ class MailchimpConfig extends DataObject implements TemplateGlobalProvider, Perm /** * Defines summary fields commonly used in table columns * as a quick overview of the data for this dataobject - * @var array */ - private static $summary_fields = [ + private static array $summary_fields = [ 'Title' => 'Title', 'Code' => 'Code', 'IsGlobal.Nice' => 'Default', @@ -81,16 +84,15 @@ class MailchimpConfig extends DataObject implements TemplateGlobalProvider, Perm 'UseXHR.Nice' => 'Submit w/o redirect' ]; - private static $indexes = [ + private static array $indexes = [ 'MailchimpListId' => true, 'Code' => ['type' => 'unique'] ]; /** * Add default values to database - * @var array */ - private static $defaults = [ + private static array $defaults = [ 'UpdateExisting' => 1,// @deprecated 'SendWelcome' => 0,// @deprecated 'ReplaceInterests' => 0,// @deprecated @@ -99,11 +101,13 @@ class MailchimpConfig extends DataObject implements TemplateGlobalProvider, Perm 'UseXHR' => 1 ]; - public function TitleCode() { + public function TitleCode(): string + { return "{$this->Title} ({$this->Code})"; } - public static function isEnabled() { + public static function isEnabled(): bool + { $site_config = SiteConfig::current_site_config(); return $site_config->MailchimpEnabled == 1; } @@ -120,35 +124,38 @@ public static function getApiKey() /** * Returns the data centre (dc) component based on the API key e.g us2 - * @return string */ - public static function getDataCentre() : string { - $dc = ''; + public static function getDataCentre(): string + { $key = self::getApiKey(); $parts = []; if($key) { - $parts = explode("-", $key); + $parts = explode("-", (string) $key); } - return !empty($parts[1]) ? $parts[1] : ''; + + return empty($parts[1]) ? '' : $parts[1]; } - public function TitleWithCode() { + public function TitleWithCode(): string + { return $this->Title . " - (code {$this->Code})"; } - public function TitleWithDetails() { + public function TitleWithDetails(): string + { $title = $this->Title; $list_id = $this->getMailchimpListId(); - $title .= " (list {$list_id})"; - return $title; + return $title . " (list {$list_id})"; } + #[\Override] public function onBeforeWrite() { parent::onBeforeWrite(); if (!$this->Code) { $this->Code = bin2hex(random_bytes(16)); } + $this->Code = Convert::raw2url($this->Code); if($this->IsGlobal == 1) { @@ -165,44 +172,50 @@ public function onBeforeWrite() /** * Return the current global config */ - public static function getGlobalConfig() { + public static function getGlobalConfig() + { return MailchimpConfig::get()->filter(['IsGlobal' => 1])->first(); } /** * If not configured in the database, return the value in yml */ - public function getMailchimpListId() + public function getMailchimpListId(): ?string { $list_id = $this->getField('MailchimpListId'); if (!$list_id) { $list_id = self::getDefaultMailchimpListId(); } + return $list_id; } - public function HasMailchimpListId() + public function HasMailchimpListId(): bool { return $this->getMailchimpListId() != ''; } - public static function getConfig($id = '', $list_id = '', $code = '') + public static function getConfig($id = '', $list_id = '', $code = ''): ?self { if ($id) { return MailchimpConfig::get()->byId($id); } + if ($list_id) { return MailchimpConfig::get()->filter('MailchimpListId', $list_id)->first(); } + if ($code) { return MailchimpConfig::get()->filter('Code', $code)->first(); } - return false; + + return null; } /** * @inheritdoc */ + #[\Override] public function getCMSFields() { $fields = parent::getCMSFields(); @@ -223,7 +236,7 @@ public function getCMSFields() 'NoApiKey', ' ' @@ -237,7 +250,7 @@ public function getCMSFields() TextField::create( 'ArchiveLink', _t( - __CLASS__ . '.ARCHIVE_URL', + self::class . '.ARCHIVE_URL', 'Newsletter archive URL' ) ) @@ -247,14 +260,14 @@ public function getCMSFields() $list_id = $this->getField('MailchimpListId'); $fields->dataFieldByName('MailchimpListId') ->setDescription( - !$list_id ? - sprintf( + $list_id ? + "" : sprintf( _t( - __CLASS__ . '.NO_LIST_ID', + self::class . '.NO_LIST_ID', "No list Id is set, the default list id '%s' is being used." ), $default_list_id - ) : "" + ) ); // this is set from SiteConfig @@ -265,9 +278,9 @@ public function getCMSFields() 'IsGlobalBanner', ' ' ), 'Title' @@ -279,7 +292,7 @@ public function getCMSFields() MultiValueTextField::create( 'Tags', _t( - __CLASS__ . '.TAGS_FOR_SUBSCRIPTIONS', + self::class . '.TAGS_FOR_SUBSCRIPTIONS', 'Tags assigned to subscribers' ) ) @@ -290,7 +303,7 @@ public function getCMSFields() CheckboxField::create( 'UseXHR', _t( - __CLASS__ . '.USE_XHR', + self::class . '.USE_XHR', 'Submit without redirecting' ) ), @@ -298,26 +311,28 @@ public function getCMSFields() ); $fields->addFieldsToTab( - 'Root.Main', [ - HTMLEditorField::create( - 'BeforeFormContent', - _t( - __CLASS__ . '.BEFORE_CONTENT', - 'Content to show before form' - ) - )->setRows(6), - HTMLEditorField::create( - 'AfterFormContent', - _t( - __CLASS__ . '.AFTER_CONTENT', - 'Content to show after form' - ) - )->setRows(6) - ]); + 'Root.Main', + [ + HTMLEditorField::create( + 'BeforeFormContent', + _t( + self::class . '.BEFORE_CONTENT', + 'Content to show before form' + ) + )->setRows(6), + HTMLEditorField::create( + 'AfterFormContent', + _t( + self::class . '.AFTER_CONTENT', + 'Content to show after form' + ) + )->setRows(6) + ] + ); if($heading = $fields->dataFieldByName('Heading')) { $heading->setDescription(_t( - __CLASS__ . '.HEADING_DESCRIPTON', + self::class . '.HEADING_DESCRIPTON', 'Displayed above the form' )); } @@ -328,9 +343,8 @@ public function getCMSFields() /** * Return signup link - * @return string */ - public function MailchimpLink() + public function MailchimpLink(): string { return singleton(ChimpleController::class)->Link(); } @@ -338,13 +352,14 @@ public function MailchimpLink() /** * Ensure the subscription for the global footer is added */ + #[\Override] public function requireDefaultRecords() { $config = MailchimpConfig::get()->filter(['IsGlobal' => 1])->first(); if (empty($config->ID)) { $config = MailchimpConfig::create([ - 'Title' => _t(__CLASS__ . '.DEFAULT_CONFIG_TITLE', 'Default Configuration'), - 'Heading' => _t(__CLASS__ . '.DEFAULT_CONFIG_HEADER', 'Subscribe'), + 'Title' => _t(self::class . '.DEFAULT_CONFIG_TITLE', 'Default Configuration'), + 'Heading' => _t(self::class . '.DEFAULT_CONFIG_HEADER', 'Subscribe'), 'IsGlobal' => 1, 'MailchimpListId' => null ]); @@ -353,6 +368,7 @@ public function requireDefaultRecords() } else { $config_id = $config->ID; } + if ($config_id) { $site_config = SiteConfig::current_site_config(); if (!empty($site_config->ID) && empty($site_config->MailchimpConfigID)) { @@ -366,9 +382,8 @@ public function requireDefaultRecords() /** * Use the form provided by the controller * @param bool $force_xhr whether to submit in place via XHR or not, the default is to let the config decide - * @return Form */ - public function SubscribeForm($force_xhr = null) + public function SubscribeForm($force_xhr = null): ?SubscribeForm { // No form available if not enabled $enabled = self::isEnabled(); @@ -396,6 +411,7 @@ public function SubscribeForm($force_xhr = null) if ($this->Heading) { $form->setLegend($this->Heading); } + $form->addExtraClass('form-subscribe'); return $form; } @@ -405,39 +421,43 @@ public function SubscribeForm($force_xhr = null) /** * Return alerts for the form - * @return string */ - public function Alerts() + public function Alerts(): string { return '{tags}