From 54a17e7748ca77eb65b4f59c333d3dbae3093729 Mon Sep 17 00:00:00 2001 From: August Miller Date: Fri, 21 Jul 2023 15:08:09 -0700 Subject: [PATCH 1/7] Gateway generator --- src/Plugin.php | 17 ++ src/generators/Gateway.php | 471 +++++++++++++++++++++++++++++++++++++ 2 files changed, 488 insertions(+) create mode 100644 src/generators/Gateway.php diff --git a/src/Plugin.php b/src/Plugin.php index b2e0ee0e92..7b1fd45b8a 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -32,6 +32,7 @@ use craft\commerce\fieldlayoutelements\VariantTitleField; use craft\commerce\fields\Products as ProductsField; use craft\commerce\fields\Variants as VariantsField; +use craft\commerce\generators\Gateway as GatewayGenerator; use craft\commerce\gql\interfaces\elements\Product as GqlProductInterface; use craft\commerce\gql\interfaces\elements\Variant as GqlVariantInterface; use craft\commerce\gql\queries\Product as GqlProductQueries; @@ -114,6 +115,7 @@ use craft\events\RegisterGqlTypesEvent; use craft\events\RegisterUserPermissionsEvent; use craft\fixfks\controllers\RestoreController; +use craft\generator\Command; use craft\gql\ElementQueryConditionBuilder; use craft\helpers\Console; use craft\helpers\Db; @@ -254,6 +256,7 @@ public function init(): void $this->_registerCacheTypes(); $this->_registerGarbageCollection(); $this->_registerDebugPanels(); + $this->_registerGenerators(); if ($request->getIsConsoleRequest()) { $this->_defineResaveCommand(); @@ -1029,4 +1032,18 @@ private function _registerTemplateHooks(): void Craft::$app->getView()->hook('cp.users.edit.content', [$this->getCustomers(), 'addEditUserCommerceTabContent']); } } + + /** + * Registers custom generators for Commerce components. + * + * @since 4.3 + */ + private function _registerGenerators(): void + { + if (class_exists(Command::class)) { + Event::on(Command::class, Command::EVENT_REGISTER_GENERATOR_TYPES, function (RegisterComponentTypesEvent $event) { + $event->types[] = GatewayGenerator::class; + }); + } + } } diff --git a/src/generators/Gateway.php b/src/generators/Gateway.php new file mode 100644 index 0000000000..3bc4a29bca --- /dev/null +++ b/src/generators/Gateway.php @@ -0,0 +1,471 @@ +className = $this->classNamePrompt('Gateway name:', [ + 'required' => true, + ]); + + $this->displayName = Inflector::camel2words($this->className); + + $this->supportsSubscriptions = $this->command->confirm('Will your gateway support subscriptions?', true); + + $this->gatewayNamespace = $this->namespacePrompt('Gateway namespace:', [ + 'default' => "$this->baseNamespace\\gateways", + ]); + + $this->paymentFormNamespace = $this->namespacePrompt('Payment form namespace:', [ + 'default' => "$this->baseNamespace\\models\\payments", + ]); + + $this->responseNamespace = $this->namespacePrompt('Request/response namespace:', [ + 'default' => "$this->baseNamespace\\models\\responses", + ]); + + $this->writeGatewayClass(); + $this->writePaymentFormClass(); + $this->writeResponseClass(); + + if ( + $this->isForModule() && + !$this->addRegistrationEventHandlerCode( + Gateways::class, + 'EVENT_REGISTER_GATEWAY_TYPES', + "$this->gatewayNamespace\\$this->className", + $fallbackExample, + ) + ) { + $moduleFile = $this->moduleFile(); + $this->command->note(<<gatewayNamespace)) + ->addUse(Craft::class) + ->addUse(User::class) + ->addUse(BaseGateway::class) + ->addUse(NotImplementedException::class) + ->addUse(Order::class) + ->addUse(BasePaymentForm::class) + ->addUse(RequestResponseInterface::class) + ->addUse(PaymentSource::class) + ->addUse(Transaction::class) + ->addUse(WebResponse::class, 'WebResponse') + ->addUse("$this->paymentFormNamespace\\{$this->className}PaymentForm") + ->addUse("$this->responseNamespace\\{$this->className}Response"); + + $methods = [ + 'displayName' => sprintf('return %s;', $this->messagePhp($this->displayName)), + 'getPaymentFormHtml' => <<getView(); + +// If you are implementing this in a module, you will need to register a template root: +return \$view->renderTemplate('{$this->module->id}/forms/payment', [ + 'gateway' => \$this, +]); +PHP, + 'authorize' => <<className}Response(); +PHP, + 'capture' => <<className}Response(); +PHP, + 'completeAuthorize' => <<className}Response(); +PHP, + 'completePurchase' => <<className}Response(); +PHP, + 'createPaymentSource' => <<messagePhp('This gateway does not support saved payment sources.')}); +PHP, + 'deletePaymentSource' => <<messagePhp('This gateway does not support saved payment sources.')}); +PHP, + 'getPaymentFormModel' => <<className}PaymentForm(); +PHP, + 'purchase' => <<className}Response(); +PHP, + 'refund' => <<className}Response(); +PHP, + 'supportsAuthorize' => << << << << << << << << << << << <<getRequest()->getRawBody(); +\$data = craft\helpers\Json::decodeIfJson(\$rawData); + +\$response = Craft::\$app->getResponse(); +\$response->format = WebResponse::FORMAT_RAW; + +// Responses are only seen by the machine that sent the webhook: +\$response->data = 'Thanks, robot!'; + +// If an exception is thrown while processing a webhook, Craft will +// automatically send an HTTP response code >= 400! + +return \$response; +PHP, + 'getTransactionHashFromWebhook' => <<getRequest()->getRawBody(); +\$data = Json::decodeIfJson(\$rawData); +PHP, + ]; + + // Additional methods must be implemented to satisfy SubscriptionGatewayInterface: + if ($this->supportsSubscriptions) { + $methods = array_merge($methods, [ + 'cancelSubscription' => <<className}SubscriptionResponse(); +PHP, + 'getNextPaymentAmount' => << << << << << <<className}SubscriptionResponse(); + +// ...set some properties on the response object... + +return \$response; +PHP, + 'reactivateSubscription' => <<className}SubscriptionResponse(); +PHP, + 'switchSubscriptionPlan' => <<className}SubscriptionResponse(); +PHP, + 'supportsReactivation' => << << << << <<getView(); + +return \$view->renderTemplate('{$this->module->id}/forms/resolve-billing-issue', [ + 'gateway' => \$this, + 'subscription' => \$subscription, +]); +PHP, + ]); + + // Additional `use` statements are required: + $namespace + ->addUse(SubscriptionGateway::class) + ->addUse(Subscription::class) + ->addUse(SubscriptionForm::class) + ->addUse(SwitchPlansForm::class) + ->addUse(CancelSubscriptionForm::class) + ->addUse(SubscriptionResponseInterface::class) + ->addUse(Plan::class) + ->addUse("{$this->responseNamespace}\\{$this->className}SubscriptionResponse"); + } + + $class = $this->createClass($this->className, $this->supportsSubscriptions ? SubscriptionGateway::class : BaseGateway::class, [ + self::CLASS_METHODS => $methods, + ]); + $namespace->add($class); + + $class->setComment(<<displayName gateway + +You may instead extend {@see craft\commerce\base\SubscriptionGateway} if your gateway should support subscriptions! Additional methods must be implemented for +MD); + $this->writePhpClass($namespace); + $this->command->success("**Gateway created!**"); + + // Payment form templates: + $paymentFormTemplate = << +TWIG; + + $this->command->writeToFile("{$this->basePath}/templates/forms/payment.twig", $paymentFormTemplate); + + $this->command->success("**Created payment templates!**"); + + if ($this->supportsSubscriptions) { + $cancelSubscriptionForm = <<command->writeToFile("{$this->basePath}/templates/forms/subscription-cancel.twig", $cancelSubscriptionForm); + + $cancelSubscriptionForm = <<command->writeToFile("{$this->basePath}/templates/forms/subscription-cancel.twig", $cancelSubscriptionForm); + + $this->command->success("**Created subscription templates!**"); + } + } + + /** + * Generates a PaymentForm class. + */ + private function writePaymentFormClass(): void + { + $namespace = (new PhpNamespace($this->paymentFormNamespace)) + ->addUse(Craft::class) + ->addUse(BasePaymentForm::class); + + $class = $this->createClass("{$this->className}PaymentForm", BasePaymentForm::class, [ + self::CLASS_METHODS => [ + 'populateFromPaymentSource' => <<myGatewayPaymentMethodId = \$paymentSource->token; +// \$customer = Commerce::getInstance()->getCustomers()->getCustomer(\$paymentSource->gatewayId, \$paymentSource->getCustomer()); +// \$this->myGatewayCustomerId = \$customer->reference; +PHP, + 'defineRules' => <<add($class); + + $class->setComment(<<displayName payment form + +You may instead extend {@see craft\commerce\models\payments\CreditCardPaymentForm}, if your gateway uses a standard tokenized payment flow! +COMMENT); + + $this->writePhpClass($namespace); + $this->command->success("**Payment form created!**"); + } + + private function writeResponseClass(): void + { + $responseNamespace = (new PhpNamespace($this->responseNamespace)) + ->addUse(Craft::class) + ->addUse(RequestResponseInterface::class); + + $paymentResponseClass = $this->createClass("{$this->className}Response", null, [ + self::CLASS_IMPLEMENTS => [ + RequestResponseInterface::class, + ], + self::CLASS_METHODS => [ + 'getData' => << << << << << << <<getData()['complete_payment_url']; +PHP, + 'getTransactionReference' => <<getData()['...']; +PHP, + 'getCode' => << <<isSuccessful() ? {$this->messagePhp('Payment complete.')} : {$this->messagePhp('Payment failed.')}; +PHP, + 'redirect' => <<setComment("$this->displayName payment request response container"); + $responseNamespace->add($paymentResponseClass); + + $this->writePhpClass($responseNamespace); + $this->command->success("**Request/response class for payments created!**"); + + if ($this->supportsSubscriptions) { + $subscriptionResponseNamespace = (new PhpNamespace($this->responseNamespace)) + ->addUse(Craft::class) + ->addUse(SubscriptionResponseInterface::class) + ->addUse(DateTimeHelper::class); + + $subscriptionResponseClass = $this->createClass("{$this->className}SubscriptionResponse", null, [ + self::CLASS_IMPLEMENTS => [ + SubscriptionResponseInterface::class, + ], + self::CLASS_METHODS => [ + 'getReference' => <<getData()['...'] ?? ''; +PHP, + 'getTrialDays' => << <<getData(); +return DateTimeHelper::toDateTime(\$this->getData()['next_payment_date']); +PHP, + 'isCanceled' => << << <<setComment(<<className subscription response container + +You will instantiate and populate this class with data retrieved from your gateway (whatever its source of truth may be). Add a public property and/or a constructor to memoize that data! +COMMENT); + + $subscriptionResponseNamespace->add($subscriptionResponseClass); + $this->writePhpClass($subscriptionResponseNamespace); + $this->command->success("**Request/response class for subscriptions created!**"); + } + + } +} From 61d4c22ba5c2e62f40686776f86240cad2079a73 Mon Sep 17 00:00:00 2001 From: August Miller Date: Wed, 26 Jul 2023 16:09:00 -0700 Subject: [PATCH 2/7] Subscriptions support for gateway generator --- src/generators/Gateway.php | 157 +++++++++++++++++++++++++++++-------- 1 file changed, 124 insertions(+), 33 deletions(-) diff --git a/src/generators/Gateway.php b/src/generators/Gateway.php index 3bc4a29bca..2a4fd368b7 100644 --- a/src/generators/Gateway.php +++ b/src/generators/Gateway.php @@ -23,6 +23,7 @@ use craft\generator\BaseGenerator; use craft\helpers\DateTimeHelper; use craft\web\Response as WebResponse; +use craft\web\View; use yii\helpers\Inflector; /** @@ -62,6 +63,8 @@ public function run(): bool $this->writeGatewayClass(); $this->writePaymentFormClass(); $this->writeResponseClass(); + $this->writePlanClass(); // Returns immediately if `supportsSubscriptions` is false! + $this->writeTemplates(); if ( $this->isForModule() && @@ -101,6 +104,7 @@ private function writeGatewayClass(): void ->addUse(PaymentSource::class) ->addUse(Transaction::class) ->addUse(WebResponse::class, 'WebResponse') + ->addUse(View::class) ->addUse("$this->paymentFormNamespace\\{$this->className}PaymentForm") ->addUse("$this->responseNamespace\\{$this->className}Response"); @@ -110,10 +114,12 @@ private function writeGatewayClass(): void // Return a string or render a template (and don’t forget to register any relevant asset bundles): \$view = Craft::\$app->getView(); -// If you are implementing this in a module, you will need to register a template root: -return \$view->renderTemplate('{$this->module->id}/forms/payment', [ +\$params = array_merge([ 'gateway' => \$this, -]); +], \$params); + +// If you are implementing this in a module, you will need to register a template root: +return \$view->renderTemplate('{$this->module->id}/forms/payment', \$params, View::TEMPLATE_MODE_CP); PHP, 'authorize' => <<className}SubscriptionResponse(); PHP, 'getNextPaymentAmount' => << << << << << <<getView()->renderTemplate('{$this->module->id}/forms/subscription-switch'); +PHP, + 'getPlanSettingsHtml' => << \$this, + 'plans' => [ + // The format of this array depends on how you render the form/input(s). + ['label' => 'Plan A', 'value' => 'plan-a'], + ['label' => 'Plan B', 'value' => 'plan-b'], + ], +], \$params); + +return Craft::\$app->getView()->renderTemplate('{$this->module->id}/settings/plan', \$params); +PHP, + 'getPlanModel' => <<className}Plan(); +PHP, + 'getSubscriptionFormModel' => << << << \$this, 'subscription' => \$subscription, ]); +PHP, + 'getSwitchPlansFormHtml' => <<getView()->renderTemplate('{$this->module->id}/forms/subscription-switch'); PHP, ]); @@ -278,7 +318,8 @@ private function writeGatewayClass(): void ->addUse(CancelSubscriptionForm::class) ->addUse(SubscriptionResponseInterface::class) ->addUse(Plan::class) - ->addUse("{$this->responseNamespace}\\{$this->className}SubscriptionResponse"); + ->addUse("{$this->responseNamespace}\\{$this->className}SubscriptionResponse") + ->addUse("{$this->baseNamespace}\\models\\Plan", "{$this->className}Plan"); } $class = $this->createClass($this->className, $this->supportsSubscriptions ? SubscriptionGateway::class : BaseGateway::class, [ @@ -288,35 +329,9 @@ private function writeGatewayClass(): void $class->setComment(<<displayName gateway - -You may instead extend {@see craft\commerce\base\SubscriptionGateway} if your gateway should support subscriptions! Additional methods must be implemented for MD); $this->writePhpClass($namespace); $this->command->success("**Gateway created!**"); - - // Payment form templates: - $paymentFormTemplate = << -TWIG; - - $this->command->writeToFile("{$this->basePath}/templates/forms/payment.twig", $paymentFormTemplate); - - $this->command->success("**Created payment templates!**"); - - if ($this->supportsSubscriptions) { - $cancelSubscriptionForm = <<command->writeToFile("{$this->basePath}/templates/forms/subscription-cancel.twig", $cancelSubscriptionForm); - - $cancelSubscriptionForm = <<command->writeToFile("{$this->basePath}/templates/forms/subscription-cancel.twig", $cancelSubscriptionForm); - - $this->command->success("**Created subscription templates!**"); - } } /** @@ -431,7 +446,7 @@ private function writeResponseClass(): void self::CLASS_METHODS => [ 'getReference' => <<getData()['...'] ?? ''; +return \$this->getData()['...'] ?? 'plan-identifier'; PHP, 'getTrialDays' => << << <<writePhpClass($subscriptionResponseNamespace); $this->command->success("**Request/response class for subscriptions created!**"); } + } + private function writePlanClass(): void + { + if (!$this->supportsSubscriptions) { + // Nothing to do! + return; + } + + $modelsNamespace = (new PhpNamespace("{$this->baseNamespace}\\models")) + ->addUse(Plan::class, 'BasePlan'); + + // This uses a string for the parent class because they would end up being named the same thing. We’ve defined the alias, above! + $planClass = $this->createClass('Plan', Plan::class, [ + self::CLASS_METHODS => [ + 'canSwitchFrom' => <<setComment('Subscription Plan class'); + + $modelsNamespace->add($planClass); + $this->writePhpClass($modelsNamespace); + $this->command->success("**Plan classes created!**"); + } + + private function writeTemplates(): void + { + // Plan settings: + $planSettingsTemplate = <<messageTwig('Reference')}, + name: 'reference', + value: plan ? plan.reference : null, + options: plans, + errors: plan ? plan.getErrors('reference') : null, +}) }} +TWIG; + + $this->command->writeToFile("{$this->basePath}/templates/settings/plan.twig", $planSettingsTemplate); + + $this->command->success("**Created plan settings template!**"); + + // Payment form templates: + $paymentFormTemplate = << +TWIG; + + $this->command->writeToFile("{$this->basePath}/templates/forms/payment.twig", $paymentFormTemplate); + + $this->command->success("**Created payment template!**"); + + if ($this->supportsSubscriptions) { + $cancelSubscriptionForm = <<command->writeToFile("{$this->basePath}/templates/forms/subscription-cancel.twig", $cancelSubscriptionForm); + + $switchSubscriptionsForm = <<command->writeToFile("{$this->basePath}/templates/forms/subscription-switch.twig", $switchSubscriptionsForm); + + $this->command->success("**Created subscription template!**"); + } } } From c337cc0c70edec8d1c25de5d94ec47c9874c7026 Mon Sep 17 00:00:00 2001 From: August Miller Date: Wed, 26 Jul 2023 17:04:25 -0700 Subject: [PATCH 3/7] Adjuster generator --- src/Plugin.php | 2 + src/generators/Adjuster.php | 103 ++++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 src/generators/Adjuster.php diff --git a/src/Plugin.php b/src/Plugin.php index 7b1fd45b8a..ba5cec9e44 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -32,6 +32,7 @@ use craft\commerce\fieldlayoutelements\VariantTitleField; use craft\commerce\fields\Products as ProductsField; use craft\commerce\fields\Variants as VariantsField; +use craft\commerce\generators\Adjuster; use craft\commerce\generators\Gateway as GatewayGenerator; use craft\commerce\gql\interfaces\elements\Product as GqlProductInterface; use craft\commerce\gql\interfaces\elements\Variant as GqlVariantInterface; @@ -1042,6 +1043,7 @@ private function _registerGenerators(): void { if (class_exists(Command::class)) { Event::on(Command::class, Command::EVENT_REGISTER_GENERATOR_TYPES, function (RegisterComponentTypesEvent $event) { + $event->types[] = Adjuster::class; $event->types[] = GatewayGenerator::class; }); } diff --git a/src/generators/Adjuster.php b/src/generators/Adjuster.php new file mode 100644 index 0000000000..34dff91010 --- /dev/null +++ b/src/generators/Adjuster.php @@ -0,0 +1,103 @@ +className = $this->classNamePrompt('Adjuster name:', [ + 'required' => true, + ]); + + $this->namespace = $this->namespacePrompt('Adjuster namespace:', [ + 'default' => "$this->baseNamespace\\adjusters", + ]); + + $this->displayName = Inflector::camel2words($this->className); + + $namespace = (new PhpNamespace($this->namespace)) + ->addUse(Craft::class) + ->addUse(Component::class) + ->addUse(AdjusterInterface::class) + ->addUse(OrderAdjustment::class) + ->addUse(Currency::class); + + $class = $this->createClass($this->className, Component::class, [ + self::CLASS_IMPLEMENTS => [ + AdjusterInterface::class, + ], + self::CLASS_METHODS => [ + 'adjust' => <<getLineItems() as \$lineItem) { + if (\$lineItem->qty >= 10) { + \$adjustment = new OrderAdjustment(); + \$adjustment->setLineItem(\$lineItem); // Optional! + \$adjustment->setOrder(\$order); + \$adjustment->type = 'discount'; + \$adjustment->amount = Currency::round(\$lineItem->qty * \$lineItem->salePrice * -0.1); + \$adjustment->name = 'Bulk Discount'; + \$adjustment->description = '10% off when you buy 10 or more!'; + + \$adjustments[] = \$adjustment; + } +} + +return \$adjustments; +PHP, + ], + ]); + $namespace->add($class); + + $class->setComment(<<displayName adjuster + +Grants 10% off when ordering in bulk. +MD); + + $this->writePhpClass($namespace); + + if ( + $this->isForModule() && + !$this->addRegistrationEventHandlerCode( + OrderAdjustments::class, + 'EVENT_REGISTER_ORDER_ADJUSTERS', + "$this->namespace\\$this->className", + $fallbackExample, + ) + ) { + $moduleFile = $this->moduleFile(); + $this->command->note(<<command->success("**Adjuster created!**"); + return true; + } +} From be3b0eecc7b80a1bfa232881ee80f4f0b3d1fb61 Mon Sep 17 00:00:00 2001 From: August Miller Date: Thu, 27 Jul 2023 12:48:46 -0700 Subject: [PATCH 4/7] Adjuster constant --- src/generators/Adjuster.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/generators/Adjuster.php b/src/generators/Adjuster.php index 34dff91010..a3cc1392b9 100644 --- a/src/generators/Adjuster.php +++ b/src/generators/Adjuster.php @@ -10,7 +10,7 @@ use craft\commerce\models\OrderAdjustment; use craft\commerce\services\OrderAdjustments; use craft\generator\BaseGenerator; -use Illuminate\Support\Collection; +use Nette\PhpGenerator\Constant; use yii\helpers\Inflector; /** @@ -68,6 +68,12 @@ public function run(): bool PHP, ], ]); + + $typeConstant = new Constant('ADJUSTMENT_TYPE'); + $typeConstant->setValue('discount'); + $typeConstant->setComment('Must be one of `discount`, `shipping`, or `tax`.'); + + $class->addMember($typeConstant); $namespace->add($class); $class->setComment(<< Date: Thu, 27 Jul 2023 17:02:37 -0700 Subject: [PATCH 5/7] Shipping method generator --- src/Plugin.php | 2 + src/generators/ShippingMethod.php | 147 ++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 src/generators/ShippingMethod.php diff --git a/src/Plugin.php b/src/Plugin.php index ba5cec9e44..8096dcde1c 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -34,6 +34,7 @@ use craft\commerce\fields\Variants as VariantsField; use craft\commerce\generators\Adjuster; use craft\commerce\generators\Gateway as GatewayGenerator; +use craft\commerce\generators\ShippingMethod; use craft\commerce\gql\interfaces\elements\Product as GqlProductInterface; use craft\commerce\gql\interfaces\elements\Variant as GqlVariantInterface; use craft\commerce\gql\queries\Product as GqlProductQueries; @@ -1045,6 +1046,7 @@ private function _registerGenerators(): void Event::on(Command::class, Command::EVENT_REGISTER_GENERATOR_TYPES, function (RegisterComponentTypesEvent $event) { $event->types[] = Adjuster::class; $event->types[] = GatewayGenerator::class; + $event->types[] = ShippingMethod::class; }); } } diff --git a/src/generators/ShippingMethod.php b/src/generators/ShippingMethod.php new file mode 100644 index 0000000000..707c107b30 --- /dev/null +++ b/src/generators/ShippingMethod.php @@ -0,0 +1,147 @@ +className = $this->classNamePrompt('Shipping method name:', [ + 'required' => true, + ]); + + $this->namespace = $this->namespacePrompt('Shipping method namespace:', [ + 'default' => "$this->baseNamespace\\models", + ]); + + $this->displayName = Inflector::camel2words($this->className); + + $id = Inflector::camel2id($this->className); + + $methodNamespace = (new PhpNamespace($this->namespace)) + ->addUse(Craft::class) + ->addUse(Order::class) + ->addUse(Model::class) + ->addUse(ShippingMethodInterface::class) + ->addUse(ShippingRuleInterface::class) + ->addUse(ShippingRule::class); + + $methodClass = $this->createClass($this->className, Model::class, [ + self::CLASS_IMPLEMENTS => [ + ShippingMethodInterface::class, + ], + self::CLASS_METHODS => [ + 'getType' => sprintf('return \'%s\';', $id), + 'getId' => 'return null;', + 'getName' => sprintf('return \'%s\';', $this->displayName), + 'getHandle' => << "return '';", + 'getShippingRules' => << 'Example {$this->displayName} Rule', + 'description' => 'This will always match.', + 'baseRate' => 10.0, + ]), +]; +PHP, + 'getIsEnabled' => 'return true;', + 'matchOrder' => << <<getShippingRules() as \$rule) { + /** @var ShippingRuleInterface \$rule */ + if (\$rule->matchOrder(\$order)) { + return \$rule; + } +} + +return null; +PHP, + 'getPriceForOrder' => <<getMatchingShippingRule(\$order); +// Calculate the total shipping value for the order based on the matched rule’s rates. +// See (or extend) `craft\commerce\base\ShippingMethod` for an example implementation that examines each shippable LineItem! +return \$rule->getBaseRate(); +PHP, + ], + ]); + $methodNamespace->add($methodClass); + + $methodClass->setComment(<<displayName shipping method + +This class may be instantiated any number of times when registering shipping methods. Each instance should represent a discrete method—but the source of truth about those methods can be anything. +MD); + + $this->writePhpClass($methodNamespace); + $this->command->success("**Shipping method created!**"); + + // Finish up, output instructions: + $moduleFile = $this->moduleFile(); + $this->command->note(<<namespace}\\{$this->className}; + +Event::on( + ShippingMethods::class, + ShippingMethods::EVENT_REGISTER_AVAILABLE_SHIPPING_METHODS, + function(RegisterAvailableShippingMethodsEvent \$event) { + \$event->shippingMethods[] = new {$this->className}(); + } +); +``` + +You may register multiple shipping methods at once, by replacing the body of the event handler with this: + +``` +foreach (YourPlugin::getInstance()->getShipping()->getShippingMethods() as \$method) { + \$event->shippingMethods[] = \$method; +} +``` + +The service and method we’re invoking here are your responsibility to implement. + +Commerce will narrow the returned list to only those whose `matchOrder()` method returns `true`. +MD); + + $this->command->note(<<command->warning(<< Date: Thu, 27 Jul 2023 17:16:42 -0700 Subject: [PATCH 6/7] ECS --- src/Plugin.php | 2 +- src/generators/Adjuster.php | 2 +- src/generators/Gateway.php | 2 +- src/generators/ShippingMethod.php | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Plugin.php b/src/Plugin.php index 8096dcde1c..da22d94a36 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -1043,7 +1043,7 @@ private function _registerTemplateHooks(): void private function _registerGenerators(): void { if (class_exists(Command::class)) { - Event::on(Command::class, Command::EVENT_REGISTER_GENERATOR_TYPES, function (RegisterComponentTypesEvent $event) { + Event::on(Command::class, Command::EVENT_REGISTER_GENERATOR_TYPES, function(RegisterComponentTypesEvent $event) { $event->types[] = Adjuster::class; $event->types[] = GatewayGenerator::class; $event->types[] = ShippingMethod::class; diff --git a/src/generators/Adjuster.php b/src/generators/Adjuster.php index a3cc1392b9..621f1b212e 100644 --- a/src/generators/Adjuster.php +++ b/src/generators/Adjuster.php @@ -3,7 +3,6 @@ namespace craft\commerce\generators; use Craft; -use Nette\PhpGenerator\PhpNamespace; use craft\base\Component; use craft\commerce\base\AdjusterInterface; use craft\commerce\helpers\Currency; @@ -11,6 +10,7 @@ use craft\commerce\services\OrderAdjustments; use craft\generator\BaseGenerator; use Nette\PhpGenerator\Constant; +use Nette\PhpGenerator\PhpNamespace; use yii\helpers\Inflector; /** diff --git a/src/generators/Gateway.php b/src/generators/Gateway.php index 2a4fd368b7..f4ecc9cc1a 100644 --- a/src/generators/Gateway.php +++ b/src/generators/Gateway.php @@ -3,7 +3,6 @@ namespace craft\commerce\generators; use Craft; -use Nette\PhpGenerator\PhpNamespace; use craft\commerce\base\Gateway as BaseGateway; use craft\commerce\base\Plan; use craft\commerce\base\RequestResponseInterface; @@ -24,6 +23,7 @@ use craft\helpers\DateTimeHelper; use craft\web\Response as WebResponse; use craft\web\View; +use Nette\PhpGenerator\PhpNamespace; use yii\helpers\Inflector; /** diff --git a/src/generators/ShippingMethod.php b/src/generators/ShippingMethod.php index 707c107b30..af4da0d19b 100644 --- a/src/generators/ShippingMethod.php +++ b/src/generators/ShippingMethod.php @@ -4,12 +4,12 @@ use Craft; use craft\commerce\base\Model; -use Nette\PhpGenerator\PhpNamespace; use craft\commerce\base\ShippingMethodInterface; use craft\commerce\base\ShippingRuleInterface; use craft\commerce\elements\Order; use craft\commerce\models\ShippingRule; use craft\generator\BaseGenerator; +use Nette\PhpGenerator\PhpNamespace; use yii\helpers\Inflector; /** From 73ef7d438ebf10fc13321309d02e7d0f12b3796f Mon Sep 17 00:00:00 2001 From: August Miller Date: Thu, 27 Jul 2023 17:29:15 -0700 Subject: [PATCH 7/7] Dev dependencies --- composer.json | 1 + composer.lock | 217 +++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 217 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index a8991b6eb8..ac8cb7bc12 100644 --- a/composer.json +++ b/composer.json @@ -36,6 +36,7 @@ "codeception/module-yii2": "^1.0.0", "craftcms/ckeditor": "^3.0", "craftcms/ecs": "dev-main", + "craftcms/generator": "^1.5", "craftcms/phpstan": "dev-main", "craftcms/rector": "dev-main", "craftcms/redactor": "*", diff --git a/composer.lock b/composer.lock index ded2d73679..d5e962f399 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "1ca19d91c0d39483a103ed4da20fa2cf", + "content-hash": "38bc045fe255183c3f3575b6c6488569", "packages": [ { "name": "cebe/markdown", @@ -7791,6 +7791,65 @@ }, "time": "2022-06-30T16:27:12+00:00" }, + { + "name": "craftcms/generator", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/craftcms/generator.git", + "reference": "e8f5970fc294da2431933f5d13095911ae99117a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/craftcms/generator/zipball/e8f5970fc294da2431933f5d13095911ae99117a", + "reference": "e8f5970fc294da2431933f5d13095911ae99117a", + "shasum": "" + }, + "require": { + "craftcms/cms": "^4.4.11", + "nette/php-generator": "^4.0", + "nikic/php-parser": "^4.15", + "php": "^8.0.2" + }, + "require-dev": { + "craftcms/ecs": "dev-main", + "craftcms/phpstan": "dev-main", + "pestphp/pest": "^1.22" + }, + "type": "yii2-extension", + "extra": { + "bootstrap": "craft\\generator\\Extension" + }, + "autoload": { + "psr-4": { + "craft\\generator\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "mit" + ], + "authors": [ + { + "name": "Pixel & Tonic", + "homepage": "https://pixelandtonic.com/" + } + ], + "description": "Craft CMS component generator", + "homepage": "https://craftcms.com", + "keywords": [ + "cms", + "craftcms", + "yii2" + ], + "support": { + "email": "support@craftcms.com", + "issues": "https://github.com/craftcms/generator/issues?state=open", + "rss": "https://github.com/craftcms/generator/releases.atom", + "source": "https://github.com/craftcms/generator" + }, + "time": "2023-05-15T17:18:45+00:00" + }, { "name": "craftcms/html-field", "version": "2.0.7", @@ -8309,6 +8368,162 @@ ], "time": "2023-03-08T13:26:56+00:00" }, + { + "name": "nette/php-generator", + "version": "v4.0.7", + "source": { + "type": "git", + "url": "https://github.com/nette/php-generator.git", + "reference": "de1843fbb692125e307937c85d43937d0dc0c1d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/php-generator/zipball/de1843fbb692125e307937c85d43937d0dc0c1d4", + "reference": "de1843fbb692125e307937c85d43937d0dc0c1d4", + "shasum": "" + }, + "require": { + "nette/utils": "^3.2.9 || ^4.0", + "php": ">=8.0 <8.3" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "dev-master", + "nette/tester": "^2.4", + "nikic/php-parser": "^4.15", + "phpstan/phpstan": "^1.0", + "tracy/tracy": "^2.8" + }, + "suggest": { + "nikic/php-parser": "to use ClassType::from(withBodies: true) & ClassType::fromCode()" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🐘 Nette PHP Generator: generates neat PHP code for you. Supports new PHP 8.2 features.", + "homepage": "https://nette.org", + "keywords": [ + "code", + "nette", + "php", + "scaffolding" + ], + "support": { + "issues": "https://github.com/nette/php-generator/issues", + "source": "https://github.com/nette/php-generator/tree/v4.0.7" + }, + "time": "2023-04-26T15:09:53+00:00" + }, + { + "name": "nette/utils", + "version": "v4.0.0", + "source": { + "type": "git", + "url": "https://github.com/nette/utils.git", + "reference": "cacdbf5a91a657ede665c541eda28941d4b09c1e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/utils/zipball/cacdbf5a91a657ede665c541eda28941d4b09c1e", + "reference": "cacdbf5a91a657ede665c541eda28941d4b09c1e", + "shasum": "" + }, + "require": { + "php": ">=8.0 <8.3" + }, + "conflict": { + "nette/finder": "<3", + "nette/schema": "<1.2.2" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "dev-master", + "nette/tester": "^2.4", + "phpstan/phpstan": "^1.0", + "tracy/tracy": "^2.9" + }, + "suggest": { + "ext-gd": "to use Image", + "ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()", + "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", + "ext-json": "to use Nette\\Utils\\Json", + "ext-mbstring": "to use Strings::lower() etc...", + "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()", + "ext-xml": "to use Strings::length() etc. when mbstring is not available" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", + "homepage": "https://nette.org", + "keywords": [ + "array", + "core", + "datetime", + "images", + "json", + "nette", + "paginator", + "password", + "slugify", + "string", + "unicode", + "utf-8", + "utility", + "validation" + ], + "support": { + "issues": "https://github.com/nette/utils/issues", + "source": "https://github.com/nette/utils/tree/v4.0.0" + }, + "time": "2023-02-02T10:41:53+00:00" + }, { "name": "nikic/php-parser", "version": "v4.15.5",