public function modify($subtotal, $forcecalculation = false)
    {
        $order = $this->Order();
        $value = ($order->IsCart() || $forcecalculation) ? $this->value($subtotal) : $this->Amount;
        switch ($this->Type) {
            case 'Chargable':
                $subtotal += $value;
                break;
            case 'Deductable':
                $subtotal -= $value;
                break;
            case 'Ignored':
                break;
        }
        $value = round($value, Order::config()->rounding_precision);
        $this->Amount = $value;
        return $subtotal;
    }

public function modify ( $ subtotal , $ forcecalculation = false ) { $ order = $ this -> Order ( ) ; $ value = ( $ order -> IsCart ( ) || $ forcecalculation ) ? $ this -> value ( $ subtotal ) : $ this -> Amount ; switch ( $ this -> Type ) { case 'Chargable' : $ subtotal += $ value ; break ; case 'Deductable' : $ subtotal -= $ value ; break ; case 'Ignored' : break ; } $ value = round ( $ value , Order :: config ( ) -> rounding_precision ) ; $ this -> Amount = $ value ; return $ subtotal ; }

Modifies the incoming value by adding,
subtracting or ignoring the value this modifier calculates.

Sets $this->Amount to the calculated value;

@param $subtotal - running total to be modified
@param $forcecalculation - force calculating the value, if order isn't in cart

@return $subtotal - updated subtotal

Modifies the incoming value by adding subtracting or ignoring the value this modifier calculates .


public static function setupSteps($steps = null)
    {
        if (!is_array($steps)) {
            //default steps
            $steps =[
                'membership'      => Membership::class,
                'contactdetails'  => ContactDetails::class,
                'shippingaddress' => Address::class,
                'billingaddress'  => Address::class,
                'paymentmethod'   => PaymentMethod::class,
                'summary'         => Summary::class,
            ];
        }

        CheckoutPage::config()->steps = $steps;

        if (!CheckoutPage::config()->first_step) {
            reset($steps);
            CheckoutPage::config()->first_step = key($steps);
        }
        //initiate extensions
        CheckoutPageController::add_extension(SteppedCheckoutExtension::class);
        foreach ($steps as $action => $classname) {
            CheckoutPageController::add_extension($classname);
        }
    }

public
static
function
setupSteps
(
$
steps
=
)
{
if
(
!
is_array
(
$
steps
)
)
{
//default steps
$
steps
=
[
'membership'
=>
Membership
::
class
,
'contactdetails'
=>
ContactDetails
::
class
,
'shippingaddress'
=>
Address
::
class
,
'billingaddress'
=>
Address
::
class
,
'paymentmethod'
=>
PaymentMethod
::
class
,
'summary'
=>
Summary
::
class
,
]
;
}
CheckoutPage
::
config
(
)
->
steps
=
$
steps
;
if
(
!
CheckoutPage
::
config
(
)
->
first_step
)
{
reset
(
$
steps
)
;
CheckoutPage
::
config
(
)
->
first_step
=
key
(
$
steps
)
;
}
//initiate extensions
CheckoutPageController
::
add_extension
(
SteppedCheckoutExtension
::
class
)
;
foreach
(
$
steps
as
$
action
=>
$
classname
)
{
CheckoutPageController
::
add_extension
(
$
classname
)
;
}
}

Set up CheckoutPage_Controller decorators for managing steps


public function onAfterInit()
    {
        $action = $this->owner->getRequest()->param('Action');
        $steps = $this->getSteps();
        if (!ShoppingCart::curr() && !empty($action) && isset($steps[$action])) {
            Controller::curr()->redirect($this->owner->Link());
            return;
        }
    }

public function onAfterInit ( ) { $ action = $ this -> owner -> getRequest ( ) -> param ( 'Action' ) ; $ steps = $ this -> getSteps ( ) ; if ( ! ShoppingCart :: curr ( ) && ! empty ( $ action ) && isset ( $ steps [ $ action ] ) ) { Controller :: curr ( ) -> redirect ( $ this -> owner -> Link ( ) ) ; return ; } }

Redirect back to start of checkout if no cart started


public function IsCurrentStep($name)
    {
        if ($this->owner->getAction() === $name) {
            return true;
        } elseif (!$this->owner->getAction() || $this->owner->getAction() === 'index') {
            return $this->actionPos($name) === 0;
        }
        return false;
    }

public function IsCurrentStep ( $ name ) { if ( $ this -> owner -> getAction ( ) === $ name ) { return true ; } elseif ( ! $ this -> owner -> getAction ( ) || $ this -> owner -> getAction ( ) === 'index' ) { return $ this -> actionPos ( $ name ) === 0 ; } return false ; }

Check if passed action is the same as the current step


private function actionPos($incoming)
    {
        $count = 0;
        foreach ($this->getSteps() as $action => $step) {
            if ($action == $incoming) {
                return $count;
            }
            $count++;
        }
    }

private function actionPos ( $ incoming ) { $ count = 0 ; foreach ( $ this -> getSteps ( ) as $ action => $ step ) { if ( $ action == $ incoming ) { return $ count ; } $ count ++ ; } }


public static function find_link($urlSegment = false, $action = false, $id = false)
    {
        $base = CartPageController::config()->url_segment;
        if ($page = self::get()->first()) {
            $base = $page->Link();
        }
        return Controller::join_links($base, $action, $id);
    }

public static function find_link ( $ urlSegment = false , $ action = false , $ id = false ) { $ base = CartPageController :: config ( ) -> url_segment ; if ( $ page = self :: get ( ) -> first ( ) ) { $ base = $ page -> Link ( ) ; } return Controller :: join_links ( $ base , $ action , $ id ) ; }

Returns the link to the checkout page on this site

@param boolean $urlSegment If set to TRUE, only returns the URLSegment field

@return string Link to checkout page


public function Products($recursive = true)
    {
        $products = $this->ProductsShowable($recursive);

        //sort the products
        $products = $this->getSorter()->sortList($products);

        //paginate the products, if necessary
        $pagelength = ProductCategory::config()->page_length;
        if ($pagelength > 0) {
            $products = PaginatedList::create($products, $this->request);
            $products->setPageLength($pagelength);
            $products->TotalCount = $products->getTotalItems();
        }

        return $products;
    }

public
function
Products
(
$
recursive
=
true
)
{
$
products
=
$
this
->
ProductsShowable
(
$
recursive
)
;
//sort the products
$
products
=
$
this
->
getSorter
(
)
->
sortList
(
$
products
)
;
//paginate the products, if necessary
$
pagelength
=
ProductCategory
::
config
(
)
->
page_length
;
if
(
$
pagelength
>
0
)
{
$
products
=
PaginatedList
::
create
(
$
products
,
$
this
->
request
)
;
$
products
->
setPageLength
(
$
pagelength
)
;
$
products
->
TotalCount
=
$
products
->
getTotalItems
(
)
;
}
return
$
products
;
}


public function TrimCents()
    {
        $val = $this->value;

        if (floor($val) == $val) {
            return floor($val);
        }

        return $val;
    }

public function TrimCents ( ) { $ val = $ this -> value ; if ( floor ( $ val ) == $ val ) { return floor ( $ val ) ; } return $ val ; }

If no cents on the price, trim those off.

@return string

If no cents on the price trim those off .


public function getCMSFields()
    {
        $self = $this;

        $this->beforeUpdateCMSFields(
            function (FieldList $fields) use ($self) {
                $fields->fieldByName('Root.Main.Title')
                    ->setTitle(_t(__CLASS__ . '.PageTitle', 'Product Title'));

                $fields->addFieldsToTab('Root.Main', [
                    TextField::create('InternalItemID', _t(__CLASS__ . '.InternalItemID', 'Product Code/SKU'), '', 30),
                    DropdownField::create('ParentID', _t(__CLASS__ . '.Category', 'Category'), $self->getCategoryOptions())
                        ->setDescription(_t(__CLASS__ . '.CategoryDescription', 'This is the parent page or default category.')),
                    ListboxField::create(
                        'ProductCategories',
                        _t(__CLASS__ . '.AdditionalCategories', 'Additional Categories'),
                        $self->getCategoryOptionsNoParent()
                    ),
                    TextField::create('Model', _t(__CLASS__ . '.Model', 'Model'), '', 30),
                    CheckboxField::create('Featured', _t(__CLASS__ . '.Featured', 'Featured Product')),
                    CheckboxField::create('AllowPurchase', _t(__CLASS__ . '.AllowPurchase', 'Allow product to be purchased'), 1),
                ], 'Content');

                $fields->addFieldsToTab(
                    'Root.Pricing',
                    [
                        TextField::create('BasePrice', $this->fieldLabel('BasePrice'))
                        ->setDescription(_t(__CLASS__ . '.PriceDesc', 'Base price to sell this product at.'))
                        ->setMaxLength(12),
                    ]
                );

                $fieldSubstitutes = [
                    'LengthUnit' => $self::config()->length_unit
                ];

                $fields->addFieldsToTab(
                    'Root.Shipping',
                    [
                    TextField::create(
                        'Weight',
                        _t(
                            __CLASS__ . '.WeightWithUnit',
                            'Weight ({WeightUnit})',
                            '',
                            [
                            'WeightUnit' => self::config()->weight_unit
                            ]
                        ),
                        '',
                        12
                    ),
                    TextField::create(
                        'Height',
                        _t(__CLASS__ . '.HeightWithUnit', 'Height ({LengthUnit})', '', $fieldSubstitutes),
                        '',
                        12
                    ),
                    TextField::create(
                        'Width',
                        _t(__CLASS__ . '.WidthWithUnit', 'Width ({LengthUnit})', '', $fieldSubstitutes),
                        '',
                        12
                    ),
                    TextField::create(
                        'Depth',
                        _t(__CLASS__ . '.DepthWithUnit', 'Depth ({LengthUnit})', '', $fieldSubstitutes),
                        '',
                        12
                    ),
                    ]
                );

                if (!$fields->dataFieldByName('Image')) {
                    $fields->addFieldToTab(
                        'Root.Images',
                        UploadField::create('Image', _t(__CLASS__ . '.Image', 'Product Image'))
                    );
                }
            }
        );

        return parent::getCMSFields();
    }

Add product fields to CMS

@return FieldList updated field list


private function getCategoryOptions()
    {
        $categories = ProductCategory::get()->map('ID', 'NestedTitle')->toArray();
        $categories = [
            0 => _t('SilverStripe\CMS\Model\SiteTree.PARENTTYPE_ROOT', 'Top-level page'),
        ] + $categories;
        if ($this->ParentID && !($this->Parent() instanceof ProductCategory)) {
            $categories = [
                $this->ParentID => $this->Parent()->Title . ' (' . $this->Parent()->i18n_singular_name() . ')',
            ] + $categories;
        }

        return $categories;
    }

Helper function for generating list of categories to select from.

@return array categories

Helper function for generating list of categories to select from .


private function getCategoryOptionsNoParent()
    {
        $ancestors = $this->getAncestors()->column('ID');
        $categories = ProductCategory::get();
        if (!empty($ancestors)) {
            $categories = $categories->exclude('ID', $ancestors);
        }
        return $categories->map('ID', 'NestedTitle')->toArray();
    }

private function getCategoryOptionsNoParent ( ) { $ ancestors = $ this -> getAncestors ( ) -> column ( 'ID' ) ; $ categories = ProductCategory :: get ( ) ; if ( ! empty ( $ ancestors ) ) { $ categories = $ categories -> exclude ( 'ID' , $ ancestors ) ; } return $ categories -> map ( 'ID' , 'NestedTitle' ) -> toArray ( ) ; }

Helper function for generating a list of additional categories excluding the main parent.

@return array categories

Helper function for generating a list of additional categories excluding the main parent .


public function getCategoryIDs()
    {
        $ids = array();
        //ancestors
        foreach ($this->getAncestors() as $ancestor) {
            $ids[$ancestor->ID] = $ancestor->ID;
        }
        //additional categories
        $ids += $this->ProductCategories()->getIDList();

        return $ids;
    }

public
function
getCategoryIDs
(
)
{
$
ids
=
array
(
)
;
//ancestors
foreach
(
$
this
->
getAncestors
(
)
as
$
ancestor
)
{
$
ids
[
$
ancestor
->
ID
]
=
$
ancestor
->
ID
;
}
//additional categories
$
ids
+=
$
this
->
ProductCategories
(
)
->
getIDList
(
)
;
return
$
ids
;
}

Get ids of all categories that this product appears in.

@return array ids list

Get ids of all categories that this product appears in .


public function sellingPrice()
    {
        $price = $this->BasePrice;
        //TODO: this is not ideal, because prices manipulations will not happen in a known order
        $this->extend('updateSellingPrice', $price);
        //prevent negative values
        $price = $price < 0 ? 0 : $price;

        // NOTE: Ideally, this would be dependent on the locale but as of
        // now the Silverstripe Currency field type has 2 hardcoded all over
        // the place. In the mean time there is an issue where the displayed
        // unit price can not exactly equal the multiplied price on an order
        // (i.e. if the calculated price is 3.145 it will display as 3.15.
        // so if I put 10 of them in my cart I will expect the price to be
        // 31.50 not 31.45).
        return round($price, Order::config()->rounding_precision);
    }

The raw retail price the visitor will get when they
add to cart. Can include discounts or markups on the base price.

The raw retail price the visitor will get when they add to cart . Can include discounts or markups on the base price .


public function Image()
    {
        $image = $this->getComponent('Image');
        $this->extend('updateImage', $image);

        if ($image && $image->exists()) {
            return $image;
        }
        $image = SiteConfig::current_site_config()->DefaultProductImage();
        if ($image && $image->exists()) {
            return $image;
        }
        return null;
    }

public function Image ( ) { $ image = $ this -> getComponent ( 'Image' ) ; $ this -> extend ( 'updateImage' , $ image ) ; if ( $ image && $ image -> exists ( ) ) { return $ image ; } $ image = SiteConfig :: current_site_config ( ) -> DefaultProductImage ( ) ; if ( $ image && $ image -> exists ( ) ) { return $ image ; } return null ; }

If the product does not have an image, and a default image
is defined in SiteConfig, return that instead.

@return Image
@throws \Exception

If the product does not have an image and a default image is defined in SiteConfig return that instead .


public function submitpayment($data, $form)
    {
        $data = $form->getData();

        $cancelUrl = $this->getFailureLink() ? $this->getFailureLink() : $this->controller->Link();

        $order = $this->config->getOrder();

        // final recalculation, before making payment
        $order->calculate();

        // handle cases where order total is 0. Note that the order will appear
        // as "paid", but without a Payment record attached.
        if ($order->GrandTotal() == 0 && Order::config()->allow_zero_order_total) {
            if (!$this->orderProcessor->placeOrder()) {
                $form->sessionMessage($this->orderProcessor->getError());
                return $this->controller->redirectBack();
            }
            return $this->controller->redirect($this->getSuccessLink());
        }

        // try to place order before payment, if configured
        if (Order::config()->place_before_payment) {
            if (!$this->orderProcessor->placeOrder()) {
                $form->sessionMessage($this->orderProcessor->getError());
                return $this->controller->redirectBack();
            }
            $cancelUrl = $this->orderProcessor->getReturnUrl();
        }

        // if we got here from checkoutSubmit and there's a namespaced Component that provides payment data,
        // we need to strip the inputs down to only the checkout component.
        $components = $this->config->getComponents();
        if ($components->first() instanceof CheckoutComponentNamespaced) {
            foreach ($components as $component) {
                if ($component->Proxy()->providesPaymentData()) {
                    $data = array_merge($data, $component->unnamespaceData($data));
                }
            }
        }

        $gateway = Checkout::get($order)->getSelectedPaymentMethod(false);
        $fieldFactory = new GatewayFieldsFactory($gateway);

        // This is where the payment is actually attempted
        $paymentResponse = $this->orderProcessor->makePayment(
            $gateway,
            $fieldFactory->normalizeFormData($data),
            $this->getSuccessLink(),
            $cancelUrl
        );

        $response = null;
        if ($this->controller->hasMethod('processPaymentResponse')) {
            $response = $this->controller->processPaymentResponse($paymentResponse, $form);
        } elseif ($paymentResponse && !$paymentResponse->isError()) {
            $response = $paymentResponse->redirectOrRespond();
        } else {
            $form->sessionMessage($this->orderProcessor->getError(), 'bad');
            $response = $this->controller->redirectBack();
        }

        return $response;
    }

Behaviour can be overwritten by creating a processPaymentResponse method
on the controller owning this form. It takes a Symfony\Component\HttpFoundation\Response argument,
and expects an SS_HTTPResponse in return.

Behaviour can be overwritten by creating a processPaymentResponse method on the controller owning this form . It takes a Symfony \ Component \ HttpFoundation \ Response argument and expects an SS_HTTPResponse in return .


public function dopayment($data, $form)
    {
        if (self::config()->allow_paying
            && $this->order
            && $this->order->canPay()
        ) {
            // Save payment data from form and process payment
            $data = $form->getData();
            $gateway = (!empty($data['PaymentMethod'])) ? $data['PaymentMethod'] : null;

            if (!GatewayInfo::isManual($gateway)) {
                $processor = OrderProcessor::create($this->order);
                $fieldFactory = new GatewayFieldsFactory(null);
                $response = $processor->makePayment(
                    $gateway,
                    $fieldFactory->normalizeFormData($data),
                    $processor->getReturnUrl()
                );
                if ($response && !$response->isError()) {
                    return $response->redirectOrRespond();
                } else {
                    $form->sessionMessage($processor->getError(), 'bad');
                }
            } else {
                $form->sessionMessage(_t(__CLASS__ . '.ManualNotAllowed', 'Manual payment not allowed'), 'bad');
            }

            return $this->controller->redirectBack();
        }
        $form->sessionMessage(
            _t(__CLASS__ . '.CouldNotProcessPayment', 'Payment could not be processed.'),
            'bad'
        );
        return $this->controller->redirectBack();
    }

Make payment for a place order, where payment had previously failed.

@param array $data
@param Form  $form

@return HTTPResponse

Make payment for a place order where payment had previously failed .


public function docancel($data, $form)
    {
        if (self::config()->allow_cancelling
            && $this->order->canCancel()
        ) {
            $this->order->Status = 'MemberCancelled';
            $this->order->write();

            if (self::config()->email_notification) {
                OrderEmailNotifier::create($this->order)->sendCancelNotification();
            }

            $this->controller->sessionMessage(
                _t(__CLASS__ . '.OrderCancelled', 'Order sucessfully cancelled'),
                'warning'
            );
            if (Security::getCurrentUser() && $link = $this->order->Link()) {
                $this->controller->redirect($link);
            } else {
                $this->controller->redirectBack();
            }
        }
    }

public
function
docancel
(
$
data
,
$
form
)
{
if
(
self
::
config
(
)
->
allow_cancelling
&&
$
this
->
order
->
canCancel
(
)
)
{
$
this
->
order
->
Status
=
'MemberCancelled'
;
$
this
->
order
->
write
(
)
;
if
(
self
::
config
(
)
->
email_notification
)
{
OrderEmailNotifier
::
create
(
$
this
->
order
)
->
sendCancelNotification
(
)
;
}
$
this
->
controller
->
sessionMessage
(
_t
(
__CLASS__
.
'.OrderCancelled'
,
'Order sucessfully cancelled'
)
,
'warning'
)
;
if
(
Security
::
getCurrentUser
(
)
&&
$
link
=
$
this
->
order
->
Link
(
)
)
{
$
this
->
controller
->
redirect
(
$
link
)
;
}
else
{
$
this
->
controller
->
redirectBack
(
)
;
}
}
}

Form action handler for CancelOrderForm.

Take the order that this was to be change on,
and set the status that was requested from
the form request data.

@param  array $data The form request data submitted
@param  Form  $form The {@link Form} this was submitted on
@throws \SilverStripe\ORM\ValidationException

Form action handler for CancelOrderForm .


protected function getCCFields(array $gateways)
    {
        $fieldFactory = new GatewayFieldsFactory(null, array('Card'));
        $onsiteGateways = array();
        $allRequired = array();
        foreach ($gateways as $gateway => $title) {
            if (!GatewayInfo::isOffsite($gateway)) {
                $required = GatewayInfo::requiredFields($gateway);
                $onsiteGateways[$gateway] = $fieldFactory->getFieldName($required);
                $allRequired += $required;
            }
        }

        $allRequired = array_unique($allRequired);
        $allRequired = $fieldFactory->getFieldName(array_combine($allRequired, $allRequired));

        if (empty($onsiteGateways)) {
            return null;
        }

        $ccFields = $fieldFactory->getCardFields();

        // Remove all the credit card fields that aren't required by any gateway
        foreach ($ccFields->dataFields() as $name => $field) {
            if ($name && !in_array($name, $allRequired)) {
                $ccFields->removeByName($name, true);
            }
        }

        $lookupField = LiteralField::create(
            '_CCLookupField',
            sprintf(
                '',
                json_encode($onsiteGateways, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP)
            )
        );

        $ccFields->push($lookupField);

        return CompositeField::create($ccFields)->setTag('fieldset')->addExtraClass('credit-card');
    }

Get credit card fields for the given gateways

@param  array $gateways
@return CompositeField|null


public static function add_session_order(Order $order)
    {
        $history = self::get_session_order_ids();
        if (!is_array($history)) {
            $history = array();
        }
        $history[$order->ID] = $order->ID;
        ShopTools::getSession()->set(self::$sessname, $history);
    }

public static function add_session_order ( Order $ order ) { $ history = self :: get_session_order_ids ( ) ; if ( ! is_array ( $ history ) ) { $ history = array ( ) ; } $ history [ $ order -> ID ] = $ order -> ID ; ShopTools :: getSession ( ) -> set ( self :: $ sessname , $ history ) ; }

Add an order to the session-stored history of orders.

Add an order to the session - stored history of orders .


public static function get_session_order_ids()
    {
        $history = ShopTools::getSession()->get(self::$sessname);
        if (!is_array($history)) {
            $history = null;
        }
        return $history;
    }

public static function get_session_order_ids ( ) { $ history = ShopTools :: getSession ( ) -> get ( self :: $ sessname ) ; if ( ! is_array ( $ history ) ) { $ history = null ; } return $ history ; }

Get historical orders for current session.

Get historical orders for current session .


public function orderfromid()
    {
        $request = $this->owner->getRequest();
        $id = (int)$request->param('ID');
        if (!$id) {
            $id = (int)$request->postVar('OrderID');
        }

        return $this->allorders()->byID($id);
    }

public function orderfromid ( ) { $ request = $ this -> owner -> getRequest ( ) ; $ id = ( int ) $ request -> param ( 'ID' ) ; if ( ! $ id ) { $ id = ( int ) $ request -> postVar ( 'OrderID' ) ; } return $ this -> allorders ( ) -> byID ( $ id ) ; }

Get the order via url 'ID' or form submission 'OrderID'.
It will check for permission based on session stored ids or member id.

@return Order order

Get the order via url ID or form submission OrderID . It will check for permission based on session stored ids or member id .


public function ActionsForm()
    {
        if ($order = $this->orderfromid()) {
            $form = OrderActionsForm::create($this->owner, 'ActionsForm', $order);
            $form->extend('updateActionsForm', $order);
            if (!$form->Actions()->exists()) {
                return null;
            }

            return $form;
        }
        return null;
    }

public function ActionsForm ( ) { if ( $ order = $ this -> orderfromid ( ) ) { $ form = OrderActionsForm :: create ( $ this -> owner , 'ActionsForm' , $ order ) ; $ form -> extend ( 'updateActionsForm' , $ order ) ; if ( ! $ form -> Actions ( ) -> exists ( ) ) { return null ; } return $ form ; } return null ; }

Build a form for cancelling, or retrying payment for a placed order.

@return Form

Build a form for cancelling or retrying payment for a placed order .


public function create($data)
    {
        $result = ValidationResult::create();
        if (!Checkout::member_creation_enabled()) {
            $result->addError(
                _t('SilverShop\Checkout\Checkout.MembershipIsNotAllowed', 'Creating new memberships is not allowed')
            );
            throw new ValidationException($result);
        }
        $idfield = Config::inst()->get(Member::class, 'unique_identifier_field');
        if (!isset($data[$idfield]) || empty($data[$idfield])) {
            $result->addError(
                _t(
                    'SilverShop\Checkout\Checkout.IdFieldNotFound',
                    'Required field not found: {IdentifierField}',
                    'Identifier is the field that holds the unique user-identifier, commonly this is \'Email\'',
                    ['IdentifierField' => $idfield]
                )
            );
            throw new ValidationException($result);
        }
        if (!isset($data['Password']) || empty($data['Password'])) {
            $result->addError(_t('SilverShop\Checkout\Checkout.PasswordRequired', 'A password is required'));
            throw new ValidationException($result);
        }
        $idval = $data[$idfield];
        if ($member = MemberExtension::get_by_identifier($idval)) {
            // get localized field labels
            $fieldLabels = $member->fieldLabels(false);
            // if a localized value exists, use this for our error-message
            $fieldLabel = isset($fieldLabels[$idfield]) ? $fieldLabels[$idfield] : $idfield;

            $result->addError(
                _t(
                    'SilverShop\Checkout\Checkout.MemberExists',
                    'A member already exists with the {Field} {Identifier}',
                    '',
                    ['Field' => $fieldLabel, 'Identifier' => $idval]
                )
            );
            throw new ValidationException($result);
        }

        /** @var Member $member */
        $member = Member::create()->update($data);
        // 3.2 changed validate to protected which made this fall through the DataExtension and error out
        $validation = $member->doValidate();
        if (!$validation->isValid()) {
            //TODO need to handle i18n here?
            foreach ($validation->getMessages() as $message) {
                $result->addError($message['message']);
            }
        }
        if (!$result->isValid()) {
            throw new ValidationException($result);
        }

        return $member;
    }

Create member account from data array.
Data must contain unique identifier.

@throws ValidationException

@param $data - map of member data

@return Member - new member (not saved to db)

Create member account from data array . Data must contain unique identifier .


public static function get_by_identifier($idvalue)
    {
        return Member::get()->filter(
            Member::config()->unique_identifier_field,
            $idvalue
        )->first();
    }

public static function get_by_identifier ( $ idvalue ) { return Member :: get ( ) -> filter ( Member :: config ( ) -> unique_identifier_field , $ idvalue ) -> first ( ) ; }

Get member by unique field.

@return Member|null


public function afterMemberLoggedIn()
    {
        if (Member::config()->login_joins_cart && $order = ShoppingCart::singleton()->current()) {
            $order->MemberID = $this->owner->ID;
            $order->write();
        }
    }

public function afterMemberLoggedIn ( ) { if ( Member :: config ( ) -> login_joins_cart && $ order = ShoppingCart :: singleton ( ) -> current ( ) ) { $ order -> MemberID = $ this -> owner -> ID ; $ order -> write ( ) ; } }

Link the current order to the current member on login,
if there is one, and if configuration is set to do so.

Link the current order to the current member on login if there is one and if configuration is set to do so .


public function getPastOrders()
    {
        return Order::get()
            ->filter('MemberID', $this->owner->ID)
            ->filter('Status:not', Order::config()->hidden_status);
    }

public function getPastOrders ( ) { return Order :: get ( ) -> filter ( 'MemberID' , $ this -> owner -> ID ) -> filter ( 'Status:not' , Order :: config ( ) -> hidden_status ) ; }

Get the past orders for this member

@return DataList list of orders


public function AJAXLinkHiddenField()
    {
        if ($quantitylink = $this->item->setquantityLink()) {
            return HiddenField::create(
                $this->MainID() . '_Quantity_SetQuantityLink'
            )->setValue($quantitylink)->addExtraClass('ajaxQuantityField_qtylink');
        }
    }

public function AJAXLinkHiddenField ( ) { if ( $ quantitylink = $ this -> item -> setquantityLink ( ) ) { return HiddenField :: create ( $ this -> MainID ( ) . '_Quantity_SetQuantityLink' ) -> setValue ( $ quantitylink ) -> addExtraClass ( 'ajaxQuantityField_qtylink' ) ; } }

Used for storing the quantity update link for ajax use.

Used for storing the quantity update link for ajax use .


public function getExistingAddressFields()
    {
        $member = Security::getCurrentUser();
        if ($member && $member->AddressBook()->exists()) {
            $addressoptions = $member->AddressBook()->sort('Created', 'DESC')->map('ID', 'toString')->toArray();
            $addressoptions['newaddress'] = _t('SilverShop\Model\Address.CreateNewAddress', 'Create new address');
            $fieldtype = count($addressoptions) > 3 ? DropdownField::class : OptionsetField::class;

            $label = _t("SilverShop\Model\Address.Existing{$this->addresstype}Address", "Existing {$this->addresstype} Address");

            return FieldList::create(
                $fieldtype::create(
                    $this->addresstype . 'AddressID',
                    $label,
                    $addressoptions,
                    $member->{'Default' . $this->addresstype . 'AddressID'}
                )->addExtraClass('existingValues')
            );
        }

        return null;
    }

Allow choosing from an existing address

@return FieldList|null fields for


public function getCMSFields()
    {
        $fields = FieldList::create(TabSet::create('Root', Tab::create('Main')));
        $fs = '
';
        $fe = '
';
        $parts = array(
            DropdownField::create('Status', $this->fieldLabel('Status'), self::get_order_status_options()),
            LiteralField::create('Customer', $fs . $this->renderWith('SilverShop\Admin\OrderAdmin_Customer') . $fe),
            LiteralField::create('Addresses', $fs . $this->renderWith('SilverShop\Admin\OrderAdmin_Addresses') . $fe),
            LiteralField::create('Content', $fs . $this->renderWith('SilverShop\Admin\OrderAdmin_Content') . $fe),
        );
        if ($this->Notes) {
            $parts[] = LiteralField::create('Notes', $fs . $this->renderWith('SilverShop\Admin\OrderAdmin_Notes') . $fe);
        }
        $fields->addFieldsToTab('Root.Main', $parts);
        $this->extend('updateCMSFields', $fields);
        if ($payments = $fields->fieldByName('Root.Payments.Payments')) {
            $fields->removeByName('Payments');
            $fields->insertAfter('Content', $payments);
            $payments->addExtraClass('order-payments');
        }

        return $fields;
    }

public
function
getCMSFields
(
)
{
$
fields
=
FieldList
::
create
(
TabSet
::
create
(
'Root'
,
Tab
::
create
(
'Main'
)
)
)
;
$
fs
=
'
'
;
$
fe
=
'
'
;
$
parts
=
array
(
DropdownField
::
create
(
'Status'
,
$
this
->
fieldLabel
(
'Status'
)
,
self
::
get_order_status_options
(
)
)
,
LiteralField
::
create
(
'Customer'
,
$
fs
.
$
this
->
renderWith
(
'SilverShop\Admin\OrderAdmin_Customer'
)
.
$
fe
)
,
LiteralField
::
create
(
'Addresses'
,
$
fs
.
$
this
->
renderWith
(
'SilverShop\Admin\OrderAdmin_Addresses'
)
.
$
fe
)
,
LiteralField
::
create
(
'Content'
,
$
fs
.
$
this
->
renderWith
(
'SilverShop\Admin\OrderAdmin_Content'
)
.
$
fe
)
,
)
;
if
(
$
this
->
Notes
)
{
$
parts
[
]
=
LiteralField
::
create
(
'Notes'
,
$
fs
.
$
this
->
renderWith
(
'SilverShop\Admin\OrderAdmin_Notes'
)
.
$
fe
)
;
}
$
fields
->
addFieldsToTab
(
'Root.Main'
,
$
parts
)
;
$
this
->
extend
(
'updateCMSFields'
,
$
fields
)
;
if
(
$
payments
=
$
fields
->
fieldByName
(
'Root.Payments.Payments'
)
)
{
$
fields
->
removeByName
(
'Payments'
)
;
$
fields
->
insertAfter
(
'Content'
,
$
payments
)
;
$
payments
->
addExtraClass
(
'order-payments'
)
;
}
return
$
fields
;
}

Create CMS fields for cms viewing and editing orders


public function getDefaultSearchContext()
    {
        $context = parent::getDefaultSearchContext();
        $fields = $context->getFields();

        $validStates = self::config()->placed_status;
        $statusOptions = array_filter(self::get_order_status_options(), function ($k) use ($validStates) {
            return in_array($k, $validStates);
        }, ARRAY_FILTER_USE_KEY);

        $fields->push(
            // TODO: Allow filtering by multiple statuses
            DropdownField::create('Status', $this->fieldLabel('Status'))
                ->setSource($statusOptions)
                ->setHasEmptyDefault(true)
        );

        // add date range filtering
        $fields->insertBefore(
            'Status',
            DateField::create('DateFrom', _t(__CLASS__ . '.DateFrom', 'Date from'))
        );

        $fields->insertBefore(
            'Status',
            DateField::create('DateTo', _t(__CLASS__ . '.DateTo', 'Date to'))
        );

        // get the array, to maniplulate name, and fullname seperately
        $filters = $context->getFilters();
        $filters['DateFrom'] = GreaterThanFilter::create('Placed');
        $filters['DateTo'] = LessThanFilter::create('Placed');

        // filter customer need to use a bunch of different sources
        $filters['Name'] = MultiFieldPartialMatchFilter::create(
            'FirstName',
            false,
            ['SplitWords'],
            [
                'Surname',
                'Member.FirstName',
                'Member.Surname',
                'BillingAddress.FirstName',
                'BillingAddress.Surname',
                'ShippingAddress.FirstName',
                'ShippingAddress.Surname',
            ]
        );

        $context->setFilters($filters);

        $this->extend('updateDefaultSearchContext', $context);
        return $context;
    }

Adjust scafolded search context

@return SearchContext the updated search context


public function getComponents($componentName, $id = null)
    {
        $components = parent::getComponents($componentName, $id);
        if ($componentName === 'Items' && get_class($components) !== UnsavedRelationList::class) {
            $query = $components->dataQuery();
            $components = OrderItemList::create(OrderItem::class, 'OrderID');
            $components->setDataQuery($query);
            $components = $components->forForeignID($this->ID);
        }
        return $components;
    }

public function getComponents ( $ componentName , $ id = null ) { $ components = parent :: getComponents ( $ componentName , $ id ) ; if ( $ componentName === 'Items' && get_class ( $ components ) !== UnsavedRelationList :: class ) { $ query = $ components -> dataQuery ( ) ; $ components = OrderItemList :: create ( OrderItem :: class , 'OrderID' ) ; $ components -> setDataQuery ( $ query ) ; $ components = $ components -> forForeignID ( $ this -> ID ) ; } return $ components ; }

Hack for swapping out relation list with OrderItemList

@inheritdoc


public function calculate()
    {
        if (!$this->IsCart()) {
            return $this->Total;
        }
        $calculator = OrderTotalCalculator::create($this);
        return $this->Total = $calculator->calculate();
    }

public function calculate ( ) { if ( ! $ this -> IsCart ( ) ) { return $ this -> Total ; } $ calculator = OrderTotalCalculator :: create ( $ this ) ; return $ this -> Total = $ calculator -> calculate ( ) ; }

Calculate the total

@return float the final total


public function getModifier($className, $forcecreate = false)
    {
        $calculator = OrderTotalCalculator::create($this);
        return $calculator->getModifier($className, $forcecreate);
    }

public function getModifier ( $ className , $ forcecreate = false ) { $ calculator = OrderTotalCalculator :: create ( $ this ) ; return $ calculator -> getModifier ( $ className , $ forcecreate ) ; }

This is needed to maintain backwards compatiability with
some subsystems using modifiers. eg discounts

This is needed to maintain backwards compatiability with some subsystems using modifiers . eg discounts


public function TotalOutstanding($includeAuthorized = true)
    {
        return round(
            $this->GrandTotal() - ($includeAuthorized ? $this->TotalPaidOrAuthorized() : $this->TotalPaid()),
            self::config()->rounding_precision
        );
    }

public function TotalOutstanding ( $ includeAuthorized = true ) { return round ( $ this -> GrandTotal ( ) - ( $ includeAuthorized ? $ this -> TotalPaidOrAuthorized ( ) : $ this -> TotalPaid ( ) ) , self :: config ( ) -> rounding_precision ) ; }

Calculate how much is left to be paid on the order.
Enforces rounding precision.

Payments that have been authorized via a non-manual gateway should count towards the total paid amount.
However, it's possible to exclude these by setting the $includeAuthorized parameter to false, which is
useful to determine the status of the Order. Order status should only change to 'Paid' when all
payments are 'Captured'.

@param  bool $includeAuthorized whether or not to include authorized payments (excluding manual payments)
@return float

Calculate how much is left to be paid on the order . Enforces rounding precision .


public function Link()
    {
        if (Security::getCurrentUser()) {
            $link = Controller::join_links(AccountPage::find_link(), 'order', $this->ID);
        }

        $link = CheckoutPage::find_link(false, 'order', $this->ID);

        $this->extend('updateLink', $link);

        return $link;
    }

public function Link ( ) { if ( Security :: getCurrentUser ( ) ) { $ link = Controller :: join_links ( AccountPage :: find_link ( ) , 'order' , $ this -> ID ) ; } $ link = CheckoutPage :: find_link ( false , 'order' , $ this -> ID ) ; $ this -> extend ( 'updateLink' , $ link ) ; return $ link ; }

Get the link for finishing order processing.

Get the link for finishing order processing .


public function canPay($member = null)
    {
        $extended = $this->extendedCan(__FUNCTION__, $member);
        if ($extended !== null) {
            return $extended;
        }

        if (!in_array($this->Status, self::config()->payable_status)) {
            return false;
        }
        if ($this->TotalOutstanding(true) > 0 && empty($this->Paid)) {
            return true;
        }
        return false;
    }

public function canPay ( $ member = null ) { $ extended = $ this -> extendedCan ( __FUNCTION__ , $ member ) ; if ( $ extended !== null ) { return $ extended ; } if ( ! in_array ( $ this -> Status , self :: config ( ) -> payable_status ) ) { return false ; } if ( $ this -> TotalOutstanding ( true ) > 0 && empty ( $ this -> Paid ) ) { return true ; } return false ; }

Check if an order can be paid for.

@return boolean


public function canDelete($member = null)
    {
        $extended = $this->extendedCan(__FUNCTION__, $member);
        if ($extended !== null) {
            return $extended;
        }

        return false;
    }

public function canDelete ( $ member = null ) { $ extended = $ this -> extendedCan ( __FUNCTION__ , $ member ) ; if ( $ extended !== null ) { return $ extended ; } return false ; }

Prevent deleting orders.

@return boolean


public function canView($member = null)
    {
        $extended = $this->extendedCan(__FUNCTION__, $member);
        if ($extended !== null) {
            return $extended;
        }

        return true;
    }

public function canView ( $ member = null ) { $ extended = $ this -> extendedCan ( __FUNCTION__ , $ member ) ; if ( $ extended !== null ) { return $ extended ; } return true ; }

Check if an order can be viewed.

@return boolean


public function getName()
    {
        $firstname = $this->FirstName ? $this->FirstName : $this->Member()->FirstName;
        $surname = $this->FirstName ? $this->Surname : $this->Member()->Surname;
        return implode(' ', array_filter(array($firstname, $surname)));
    }

public
function
getName
(
)
{
$
firstname
=
$
this
->
FirstName
$
this
->
FirstName
:
$
this
->
Member
(
)
->
FirstName
;
$
surname
=
$
this
->
FirstName
$
this
->
Surname
:
$
this
->
Member
(
)
->
Surname
;
return
implode
(
' '
,
array_filter
(
array
(
$
firstname
,
$
surname
)
)
)
;
}


public function getBillingAddress()
    {
        if (!$this->SeparateBillingAddress && $this->ShippingAddressID === $this->BillingAddressID) {
            return $this->getShippingAddress();
        } else {
            return $this->getAddress('Billing');
        }
    }

public function getBillingAddress ( ) { if ( ! $ this -> SeparateBillingAddress && $ this -> ShippingAddressID === $ this -> BillingAddressID ) { return $ this -> getShippingAddress ( ) ; } else { return $ this -> getAddress ( 'Billing' ) ; } }

Get billing address, if marked to use seperate address, otherwise use shipping address,
or the member default billing address.

Get billing address if marked to use seperate address otherwise use shipping address or the member default billing address .


public function generateReference()
    {
        $reference = str_pad($this->ID, self::$reference_id_padding, '0', STR_PAD_LEFT);

        $this->extend('generateReference', $reference);

        $candidate = $reference;
        //prevent generating references that are the same
        $count = 0;
        while (Order::get()->filter('Reference', $candidate)->count() > 0) {
            $count++;
            $candidate = $reference . '' . $count;
        }
        $this->Reference = $candidate;
    }

Create a unique reference identifier string for this order.

Create a unique reference identifier string for this order .


protected function onBeforeWrite()
    {
        parent::onBeforeWrite();
        if (!$this->getField('Reference') && in_array($this->Status, self::$placed_status)) {
            $this->generateReference();
        }

        // perform status transition
        if ($this->isInDB() && $this->isChanged('Status')) {
            $this->statusTransition(
                empty($this->original['Status']) ? 'Cart' : $this->original['Status'],
                $this->Status
            );
        }

        // While the order is unfinished/cart, always store the current locale with the order.
        // We do this everytime an order is saved, because the user might change locale (language-switch).
        if ($this->Status == 'Cart') {
            $this->Locale = ShopTools::get_current_locale();
        }
    }


protected function onBeforeDelete()
    {
        foreach ($this->Items() as $item) {
            $item->delete();
        }

        foreach ($this->Modifiers() as $modifier) {
            $modifier->delete();
        }

        foreach ($this->OrderStatusLogs() as $logEntry) {
            $logEntry->delete();
        }

        // just remove the payment relations…
        // that way payment objects still persist (they might be relevant for book-keeping?)
        $this->Payments()->removeAll();

        parent::onBeforeDelete();
    }

delete attributes, statuslogs, and payments

delete attributes statuslogs and payments


public function provideI18nEntities()
    {
        $entities = parent::provideI18nEntities();

        // collect all the payment status values
        foreach ($this->dbObject('Status')->enumValues() as $value) {
            $key = strtoupper($value);
            $entities[__CLASS__ . ".STATUS_$key"] = array(
                $value,
                "Translation of the order status '$value'",
            );
        }

        return $entities;
    }

Provide i18n entities for the order class

@return array


public function Field($properties = array())
    {
        $editables = $this->editableItems();
        $customcartdata = array(
            'Items' => $editables,
        );
        // NOTE: this was originally incorrect - passing just $editables and $customcartdata
        // which broke modules like Display_Logic.
        $this->extend('onBeforeRender', $this, $editables, $customcartdata);

        return SSViewer::execute_template(
            $this->template,
            $this->cart->customise($customcartdata),
            array('Editable' => true)
        );
    }

Render the cart with editable item fields.

@param array $properties

Render the cart with editable item fields .


protected function editableItems()
    {
        $editables = ArrayList::create();
        foreach ($this->items as $item) {
            $buyable = $item->Buyable();
            if (!$buyable) {
                continue;
            }
            // If the buyable is a variation, use the belonging product instead for variation-form generation
            if ($buyable instanceof Variation) {
                $buyable = $buyable->Product();
            }
            $name = $this->name . "[$item->ID]";
            $quantity = TextField::create(
                $name . '[Quantity]',
                'Quantity',
                $item->Quantity
            )
                ->addExtraClass('numeric')
                ->setAttribute('type', 'number')
                ->setAttribute('min', '0');

            $variationfield = false;
            if ($buyable->hasMany('Variations')) {
                $variations = $buyable->Variations();
                if ($variations->exists()) {
                    $variationfield = DropdownField::create(
                        $name . '[ProductVariationID]',
                        _t('SilverShop\Model\Variation\Variation.SINGULARNAME', 'Variation'),
                        $variations->map('ID', 'Title'),
                        $item->ProductVariationID
                    );
                }
            }
            $remove = CheckboxField::create($name . '[Remove]', _t('SilverShop\Generic.Remove', 'Remove'));
            $editables->push(
                $item->customise(
                    array(
                        'QuantityField' => $quantity,
                        'VariationField' => $variationfield,
                        'RemoveField' => $remove,
                    )
                )
            );
        }

        if (is_callable($this->editableItemsCallback)) {
            $callback = $this->editableItemsCallback;
            $editables = $callback($editables);
        }

        return $editables;
    }

Add quantity, variation and remove fields to the
item set.

@param SS_List $items

Add quantity variation and remove fields to the item set .


public static function find_link($urlSegment = false)
    {
        $page = self::get_if_account_page_exists();
        return ($urlSegment) ? $page->URLSegment : $page->Link();
    }

public static function find_link ( $ urlSegment = false ) { $ page = self :: get_if_account_page_exists ( ) ; return ( $ urlSegment ) ? $ page -> URLSegment : $ page -> Link ( ) ; }

Returns the link or the URLSegment to the account page on this site

@param boolean $urlSegment Return the URLSegment only

@return mixed


public static function get_order_link($orderID, $urlSegment = false)
    {
        $page = self::get_if_account_page_exists();
        return ($urlSegment ? $page->URLSegment . '/' : $page->Link()) . 'order/' . $orderID;
    }

public static function get_order_link ( $ orderID , $ urlSegment = false ) { $ page = self :: get_if_account_page_exists ( ) ; return ( $ urlSegment ? $ page -> URLSegment . '/' : $ page -> Link ( ) ) . 'order/' . $ orderID ; }

Return a link to view the order on the account page.

@param int|string $orderID    ID of the order
@param boolean    $urlSegment Return the URLSegment only

@return string

Return a link to view the order on the account page .


public function browse( $_, $assoc_args ) {
		$this->set_composer_auth_env_var();
		if ( empty( $assoc_args['format'] ) || 'table' === $assoc_args['format'] ) {
			WP_CLI::line( WP_CLI::colorize( '%CAlthough the package index will remain in place for backward compatibility reasons, it has been deprecated and will not be updated further. Please refer to https://github.com/wp-cli/ideas/issues/51 to read about its potential replacement.%n' ) );
		}
		$this->show_packages( 'browse', $this->get_community_packages(), $assoc_args );
	}

Browses WP-CLI packages available for installation.

Lists packages available for installation from the [Package Index](http://wp-cli.org/package-index/).
Although the package index will remain in place for backward compatibility reasons, it has been
deprecated and will not be updated further. Please refer to https://github.com/wp-cli/ideas/issues/51
to read about its potential replacement.

## OPTIONS

[--fields=<fields>]
: Limit the output to specific fields. Defaults to all fields.

[--format=<format>]
: Render output in a particular format.
---
default: table
options:
- table
- csv
- ids
- json
- yaml
---

## AVAILABLE FIELDS

These fields will be displayed by default for each package:

* name
* description
* authors
* version

There are no optionally available fields.

## EXAMPLES

$ wp package browse --format=yaml
---
10up/mu-migration:
name: 10up/mu-migration
description: A set of WP-CLI commands to support the migration of single WordPress instances to multisite
authors: Nícholas André
version: dev-master, dev-develop
aaemnnosttv/wp-cli-dotenv-command:
name: aaemnnosttv/wp-cli-dotenv-command
description: Dotenv commands for WP-CLI
authors: Evan Mattson
version: v0.1, v0.1-beta.1, v0.2, dev-master, dev-dev, dev-develop, dev-tests/behat
aaemnnosttv/wp-cli-http-command:
name: aaemnnosttv/wp-cli-http-command
description: WP-CLI command for using the WordPress HTTP API
authors: Evan Mattson
version: dev-master

Browses WP - CLI packages available for installation .


public function list_( $args, $assoc_args ) {
		$this->set_composer_auth_env_var();
		$this->show_packages( 'list', $this->get_installed_packages(), $assoc_args );
	}

public function list_ ( $ args , $ assoc_args ) { $ this -> set_composer_auth_env_var ( ) ; $ this -> show_packages ( 'list' , $ this -> get_installed_packages ( ) , $ assoc_args ) ; }

Lists installed WP-CLI packages.

## OPTIONS

[--fields=<fields>]
: Limit the output to specific fields. Defaults to all fields.

[--format=<format>]
: Render output in a particular format.
---
default: table
options:
- table
- csv
- ids
- json
- yaml
---

## AVAILABLE FIELDS

These fields will be displayed by default for each package:

* name
* authors
* version
* update
* update_version

These fields are optionally available:

* description

## EXAMPLES

$ wp package list
+-----------------------+------------------------------------------+---------+------------+
| name                  | description                              | authors | version    |
+-----------------------+------------------------------------------+---------+------------+
| wp-cli/server-command | Start a development server for WordPress |         | dev-master |
+-----------------------+------------------------------------------+---------+------------+

@subcommand list


public function path( $args ) {
		$packages_dir = WP_CLI::get_runner()->get_packages_dir_path();
		if ( ! empty( $args ) ) {
			$packages_dir .= 'vendor/' . $args[0];
			if ( ! is_dir( $packages_dir ) ) {
				WP_CLI::error( 'Invalid package name.' );
			}
		}
		WP_CLI::line( $packages_dir );
	}

public
function
path
(
$
args
)
{
$
packages_dir
=
WP_CLI
::
get_runner
(
)
->
get_packages_dir_path
(
)
;
if
(
!
empty
(
$
args
)
)
{
$
packages_dir
.=
'vendor/'
.
$
args
[
0
]
;
if
(
!
is_dir
(
$
packages_dir
)
)
{
WP_CLI
::
error
(
'Invalid package name.'
)
;
}
}
WP_CLI
::
line
(
$
packages_dir
)
;
}

Gets the path to an installed WP-CLI package, or the package directory.

If you want to contribute to a package, this is a great way to jump to it.

## OPTIONS

[<name>]
: Name of the package to get the directory for.

## EXAMPLES

# Get package path
$ wp package path
/home/person/.wp-cli/packages/

# Change directory to package path
$ cd $(wp package path) && pwd
/home/vagrant/.wp-cli/packages

Gets the path to an installed WP - CLI package or the package directory .


public function update() {
		$this->set_composer_auth_env_var();
		$composer = $this->get_composer();

		// Set up the EventSubscriber
		$event_subscriber = new PackageManagerEventSubscriber();
		$composer->getEventDispatcher()->addSubscriber( $event_subscriber );

		// Set up the installer
		$install = Installer::create( new ComposerIO(), $composer );
		$install->setUpdate( true ); // Installer class will only override composer.lock with this flag
		$install->setPreferSource( true ); // Use VCS when VCS for easier contributions.
		WP_CLI::log( 'Using Composer to update packages...' );
		WP_CLI::log( '---' );
		$res = false;
		try {
			$res = $install->run();
		} catch ( Exception $e ) {
			WP_CLI::warning( $e->getMessage() );
		}
		WP_CLI::log( '---' );

		if ( 0 === $res ) {
			WP_CLI::success( 'Packages updated.' );
		} else {
			$res_msg = $res ? " (Composer return code {$res})" : ''; // $res may be null apparently.
			WP_CLI::error( "Failed to update packages{$res_msg}." );
		}
	}

Updates all installed WP-CLI packages to their latest version.

## EXAMPLES

$ wp package update
Using Composer to update packages...
---
Loading composer repositories with package information
Updating dependencies
Resolving dependencies through SAT
Dependency resolution completed in 0.074 seconds
Analyzed 1062 packages to resolve dependencies
Analyzed 22383 rules to resolve dependencies
Writing lock file
Generating autoload files
---
Success: Packages updated.

Updates all installed WP - CLI packages to their latest version .


public function uninstall( $args ) {
		list( $package_name ) = $args;

		$this->set_composer_auth_env_var();
		$package = $this->get_installed_package_by_name( $package_name );
		if ( false === $package ) {
			WP_CLI::error( 'Package not installed.' );
		}
		$package_name = $package->getPrettyName(); // Make sure package name is what's in composer.json.

		// Read the WP-CLI packages composer.json and do some initial error checking.
		list( $json_path, $composer_backup, $composer_backup_decoded ) = $this->get_composer_json_path_backup_decoded();

		// Revert on shutdown if `$revert` is true (set to false on success).
		$revert = true;
		$this->register_revert_shutdown_function( $json_path, $composer_backup, $revert );

		// Remove the 'require' from composer.json.
		WP_CLI::log( sprintf( 'Removing require statement from %s', $json_path ) );
		$manipulator = new JsonManipulator( $composer_backup );
		$manipulator->removeSubNode( 'require', $package_name, true /*caseInsensitive*/ );

		// Remove the 'repository' details from composer.json.
		WP_CLI::log( sprintf( 'Removing repository details from %s', $json_path ) );
		$manipulator->removeSubNode( 'repositories', $package_name, true /*caseInsensitive*/ );

		file_put_contents( $json_path, $manipulator->getContents() );
		$composer = $this->get_composer();

		// Set up the installer.
		$install = Installer::create( new NullIO(), $composer );
		$install->setUpdate( true ); // Installer class will only override composer.lock with this flag
		$install->setPreferSource( true ); // Use VCS when VCS for easier contributions.

		WP_CLI::log( 'Removing package directories and regenerating autoloader...' );
		$res = false;
		try {
			$res = $install->run();
		} catch ( Exception $e ) {
			WP_CLI::warning( $e->getMessage() );
		}

		if ( 0 === $res ) {
			$revert = false;
			WP_CLI::success( 'Uninstalled package.' );
		} else {
			$res_msg = $res ? " (Composer return code {$res})" : ''; // $res may be null apparently.
			WP_CLI::error( "Package removal failed{$res_msg}." );
		}
	}

Uninstalls a WP-CLI package.

## OPTIONS

<name>
: Name of the package to uninstall.

## EXAMPLES

$ wp package uninstall wp-cli/server-command
Removing require statement from /home/person/.wp-cli/packages/composer.json
Deleting package directory /home/person/.wp-cli/packages/vendor/wp-cli/server-command
Regenerating Composer autoload.
Success: Uninstalled package.


private function get_composer() {
		$this->avoid_composer_ca_bundle();
		try {
			$composer_path = $this->get_composer_json_path();

			// Composer's auto-load generating code makes some assumptions about where
			// the 'vendor-dir' is, and where Composer is running from.
			// Best to just pretend we're installing a package from ~/.wp-cli or similar
			chdir( pathinfo( $composer_path, PATHINFO_DIRNAME ) );

			// Prevent DateTime error/warning when no timezone set.
			// Note: The package is loaded before WordPress load, For environments that don't have set time in php.ini.
			// phpcs:ignore WordPress.WP.TimezoneChange.timezone_change_date_default_timezone_set,WordPress.PHP.NoSilencedErrors.Discouraged
			date_default_timezone_set( @date_default_timezone_get() );

			$composer = Factory::create( new NullIO(), $composer_path );
		} catch ( Exception $e ) {
			WP_CLI::error( sprintf( 'Failed to get composer instance: %s', $e->getMessage() ) );
		}
		return $composer;
	}


private function get_community_packages() {
		static $community_packages;

		if ( null === $community_packages ) {
			$this->avoid_composer_ca_bundle();
			try {
				$community_packages = $this->package_index()->getPackages();
			} catch ( Exception $e ) {
				WP_CLI::error( $e->getMessage() );
			}
		}

		return $community_packages;
	}

private function get_community_packages ( ) { static $ community_packages ; if ( null === $ community_packages ) { $ this -> avoid_composer_ca_bundle ( ) ; try { $ community_packages = $ this -> package_index ( ) -> getPackages ( ) ; } catch ( Exception $ e ) { WP_CLI :: error ( $ e -> getMessage ( ) ) ; } } return $ community_packages ; }

Gets all of the community packages.

@return array


private function package_index() {
		static $package_index;

		if ( ! $package_index ) {
			$config_args = [
				'config' => [
					'secure-http' => true,
					'home'        => dirname( $this->get_composer_json_path() ),
				],
			];
			$config      = new Config();
			$config->merge( $config_args );
			$config->setConfigSource( new JsonConfigSource( $this->get_composer_json() ) );

			try {
				$package_index = new ComposerRepository( [ 'url' => self::PACKAGE_INDEX_URL ], new NullIO(), $config );
			} catch ( Exception $e ) {
				WP_CLI::error( $e->getMessage() );
			}
		}

		return $package_index;
	}

private function package_index ( ) { static $ package_index ; if ( ! $ package_index ) { $ config_args = [ 'config' => [ 'secure-http' => true , 'home' => dirname ( $ this -> get_composer_json_path ( ) ) , ] , ] ; $ config = new Config ( ) ; $ config -> merge ( $ config_args ) ; $ config -> setConfigSource ( new JsonConfigSource ( $ this -> get_composer_json ( ) ) ) ; try { $ package_index = new ComposerRepository ( [ 'url' => self :: PACKAGE_INDEX_URL ] , new NullIO ( ) , $ config ) ; } catch ( Exception $ e ) { WP_CLI :: error ( $ e -> getMessage ( ) ) ; } } return $ package_index ; }

Gets the package index instance

We need to construct the instance manually, because there's no way to select
a particular instance using $composer->getRepositoryManager()

@return ComposerRepository


private function show_packages( $context, $packages, $assoc_args ) {
		if ( 'list' === $context ) {
			$default_fields = [
				'name',
				'authors',
				'version',
				'update',
				'update_version',
			];
		} elseif ( 'browse' === $context ) {
			$default_fields = [
				'name',
				'description',
				'authors',
				'version',
			];
		}
		$defaults   = [
			'fields' => implode( ',', $default_fields ),
			'format' => 'table',
		];
		$assoc_args = array_merge( $defaults, $assoc_args );

		$composer = $this->get_composer();
		$list     = [];
		foreach ( $packages as $package ) {
			$name = $package->getPrettyName();
			if ( isset( $list[ $name ] ) ) {
				$list[ $name ]['version'][] = $package->getPrettyVersion();
			} else {
				$package_output                = [];
				$package_output['name']        = $package->getPrettyName();
				$package_output['description'] = $package->getDescription();
				$package_output['authors']     = implode( ', ', array_column( (array) $package->getAuthors(), 'name' ) );
				$package_output['version']     = [ $package->getPrettyVersion() ];
				$update                        = 'none';
				$update_version                = '';
				if ( 'list' === $context ) {
					try {
						$latest = $this->find_latest_package( $package, $composer, null );
						if ( $latest && $latest->getFullPrettyVersion() !== $package->getFullPrettyVersion() ) {
							$update         = 'available';
							$update_version = $latest->getPrettyVersion();
						}
					} catch ( Exception $e ) {
						WP_CLI::warning( $e->getMessage() );
						$update         = 'error';
						$update_version = $update;
					}
				}
				$package_output['update']         = $update;
				$package_output['update_version'] = $update_version;
				$package_output['pretty_name']    = $package->getPrettyName(); // Deprecated but kept for BC with package-command 1.0.8.
				$list[ $package_output['name'] ]  = $package_output;
			}
		}

		$list = array_map(
			function( $package ) {
				$package['version'] = implode( ', ', $package['version'] );
				return $package;
			},
			$list
		);

		ksort( $list );
		if ( 'ids' === $assoc_args['format'] ) {
			$list = array_keys( $list );
		}
		Utils\format_items( $assoc_args['format'], $list, $assoc_args['fields'] );
	}

Displays a set of packages

@param string $context
@param array
@param array


private function get_package_by_shortened_identifier( $package_name ) {
		// Check the package index first, so we don't break existing behavior.
		$lc_package_name = strtolower( $package_name ); // For BC check.
		foreach ( $this->get_community_packages() as $package ) {
			if ( $package_name === $package->getPrettyName() ) {
				return $package;
			}
			// For BC allow getting by lowercase name.
			if ( $lc_package_name === $package->getName() ) {
				return $package;
			}
		}

		// Check if the package exists on Packagist.
		$url      = "https://repo.packagist.org/p/{$package_name}.json";
		$response = Utils\http_request( 'GET', $url );
		if ( 20 === (int) substr( $response->status_code, 0, 2 ) ) {
			return $package_name;
		}

		// Fall back to GitHub URL if we had no match yet.
		$url          = "https://github.com/{$package_name}.git";
		$github_token = getenv( 'GITHUB_TOKEN' ); // Use GITHUB_TOKEN if available to avoid authorization failures or rate-limiting.
		$headers      = $github_token ? [ 'Authorization' => 'token ' . $github_token ] : [];
		$response     = Utils\http_request( 'GET', $url, null /*data*/, $headers );
		if ( 20 === (int) substr( $response->status_code, 0, 2 ) ) {
			return $url;
		}

		return false;
	}

Gets a package by its shortened identifier.

A shortened identifier has the form `<vendor>/<package>`.

This method first checks the deprecated package index, for BC reasons,
and then falls back to the corresponding GitHub URL.

Gets a package by its shortened identifier .


private function get_installed_packages() {
		$composer = $this->get_composer();

		$repo                   = $composer->getRepositoryManager()->getLocalRepository();
		$existing               = json_decode( file_get_contents( $this->get_composer_json_path() ), true );
		$installed_package_keys = ! empty( $existing['require'] ) ? array_keys( $existing['require'] ) : [];
		if ( empty( $installed_package_keys ) ) {
			return [];
		}
		// For use by legacy incorrect name check.
		$lc_installed_package_keys = array_map( 'strtolower', $installed_package_keys );
		$installed_packages        = [];
		foreach ( $repo->getCanonicalPackages() as $package ) {
			$idx = array_search( $package->getName(), $lc_installed_package_keys, true );
			// Use pretty name as it's case sensitive and what's in composer.json (or at least should be).
			if ( in_array( $package->getPrettyName(), $installed_package_keys, true ) ) {
				$installed_packages[] = $package;
			} elseif ( false !== $idx ) { // Legacy incorrect name check.
				WP_CLI::warning( sprintf( "Found package '%s' misnamed '%s' in '%s'.", $package->getPrettyName(), $installed_package_keys[ $idx ], $this->get_composer_json_path() ) );
				$installed_packages[] = $package;
			}
		}
		return $installed_packages;
	}


private function get_installed_package_by_name( $package_name ) {
		foreach ( $this->get_installed_packages() as $package ) {
			if ( $package_name === $package->getPrettyName() ) {
				return $package;
			}
			// Also check non-pretty (lowercase) name in case of legacy incorrect name.
			if ( $package_name === $package->getName() ) {
				return $package;
			}
		}
		return false;
	}


private static function get_package_name_and_version_from_dir_package( $dir_package ) {
		$composer_file = $dir_package . '/composer.json';
		if ( ! file_exists( $composer_file ) ) {
			WP_CLI::error( sprintf( "Invalid package: composer.json file '%s' not found.", $composer_file ) );
		}
		$composer_data = json_decode( file_get_contents( $composer_file ), true );
		if ( null === $composer_data ) {
			WP_CLI::error( sprintf( "Invalid package: failed to parse composer.json file '%s' as json.", $composer_file ) );
		}
		if ( empty( $composer_data['name'] ) ) {
			WP_CLI::error( sprintf( "Invalid package: no name in composer.json file '%s'.", $composer_file ) );
		}
		$package_name = $composer_data['name'];
		$version      = 'dev-master';
		if ( ! empty( $composer_data['version'] ) ) {
			$version = $composer_data['version'];
		}
		return [ $package_name, $version ];
	}

Gets the name of the package from the composer.json in a directory path

@param string $dir_package
@return array Two-element array containing package name and version.

Gets the name of the package from the composer . json in a directory path


private function get_composer_json_path() {
		static $composer_path;

		if ( null === $composer_path || getenv( 'WP_CLI_TEST_PACKAGE_GET_COMPOSER_JSON_PATH' ) ) {

			if ( getenv( 'WP_CLI_PACKAGES_DIR' ) ) {
				$composer_path = Utils\trailingslashit( getenv( 'WP_CLI_PACKAGES_DIR' ) ) . 'composer.json';
			} else {
				$composer_path = Utils\trailingslashit( Utils\get_home_dir() ) . '.wp-cli/packages/composer.json';
			}

			// `composer.json` and its directory might need to be created
			if ( ! file_exists( $composer_path ) ) {
				$composer_path = $this->create_default_composer_json( $composer_path );
			} else {
				$composer_path = realpath( $composer_path );
				if ( false === $composer_path ) {
					$error = error_get_last();
					WP_CLI::error( sprintf( "Composer path '%s' for packages/composer.json not found: %s", $composer_path, $error['message'] ) );
				}
			}
		}

		return $composer_path;
	}

Gets the absolute path to the WP-CLI packages composer.json.

Gets the absolute path to the WP - CLI packages composer . json .


private function create_default_composer_json( $composer_path ) {

		$composer_dir = pathinfo( $composer_path, PATHINFO_DIRNAME );
		if ( ! is_dir( $composer_dir ) ) {
			if ( ! @mkdir( $composer_dir, 0777, true ) ) { // @codingStandardsIgnoreLine
				$error = error_get_last();
				WP_CLI::error( sprintf( "Composer directory '%s' for packages couldn't be created: %s", $composer_dir, $error['message'] ) );
			}
		}

		$composer_dir = realpath( $composer_dir );
		if ( false === $composer_dir ) {
			$error = error_get_last();
			WP_CLI::error( sprintf( "Composer directory '%s' for packages not found: %s", $composer_dir, $error['message'] ) );
		}

		$composer_path = Utils\trailingslashit( $composer_dir ) . Utils\basename( $composer_path );

		$json_file = new JsonFile( $composer_path );

		$repositories = (object) [
			'wp-cli' => (object) $this->composer_type_package,
		];

		$options = [
			'name'              => 'wp-cli/wp-cli',
			'description'       => 'Installed community packages used by WP-CLI',
			'version'           => self::get_wp_cli_version_composer(),
			'authors'           => [ (object) $this->author_data ],
			'homepage'          => self::PACKAGE_INDEX_URL,
			'require'           => new stdClass(),
			'require-dev'       => new stdClass(),
			'minimum-stability' => 'dev',
			'prefer-stable'     => true,
			'license'           => 'MIT',
			'repositories'      => $repositories,
		];

		try {
			$json_file->write( $options );
		} catch ( Exception $e ) {
			WP_CLI::error( $e->getMessage() );
		}

		return $composer_path;
	}

Creates a default WP-CLI packages composer.json.

@param string $composer_path Where the composer.json should be created
@return string Returns the absolute path of the newly created default WP-CLI packages composer.json.

Creates a default WP - CLI packages composer . json .


private function get_raw_git_version( $version ) {
		if ( '' === $version ) {
			return 'master';
		}

		// If Composer hash given then just use whatever's after it.
		$hash_pos = strpos( $version, '#' );
		if ( false !== $hash_pos ) {
			return substr( $version, $hash_pos + 1 );
		}

		// Strip any Composer 'dev-' prefix.
		if ( 0 === strncmp( $version, 'dev-', 4 ) ) {
			$version = substr( $version, 4 );
		}

		// Ignore/strip any relative suffixes.
		return str_replace( [ '^', '~' ], '', $version );
	}

Get the version to use for raw github request. Very basic.

@string $version Package version.
@string Version to use for github request.

Get the version to use for raw github request . Very basic .


private function guess_version_constraint_from_tag( $tag ) {
		$matches = [];
		if ( 1 !== preg_match( '/(?:version|v)\s*((?:[0-9]+\.?)+)(?:-.*)/i', $tag, $matches ) ) {
			return $tag;
		}

		$constraint = "^{$matches[1]}";
		WP_CLI::debug( "Guessing version constraint to use: {$constraint}", 'packages' );

		return $constraint;
	}

Guesses the version constraint from a release tag.

@param string $tag Release tag to guess the version constraint from.

@return string Version constraint.

Guesses the version constraint from a release tag .


private function get_composer_json_path_backup_decoded() {
		$composer_json_obj = $this->get_composer_json();
		$json_path         = $composer_json_obj->getPath();
		$composer_backup   = file_get_contents( $json_path );
		if ( false === $composer_backup ) {
			$error = error_get_last();
			WP_CLI::error( sprintf( "Failed to read '%s': %s", $json_path, $error['message'] ) );
		}
		try {
			$composer_backup_decoded = $composer_json_obj->read();
		} catch ( Exception $e ) {
			WP_CLI::error( sprintf( "Failed to parse '%s' as json: %s", $json_path, $e->getMessage() ) );
		}

		return [ $json_path, $composer_backup, $composer_backup_decoded ];
	}

private
function
get_composer_json_path_backup_decoded
(
)
{
$
composer_json_obj
=
$
this
->
get_composer_json
(
)
;
$
json_path
=
$
composer_json_obj
->
getPath
(
)
;
$
composer_backup
=
file_get_contents
(
$
json_path
)
;
if
(
false
===
$
composer_backup
)
{
$
error
=
error_get_last
(
)
;
WP_CLI
::
error
(
sprintf
(
"Failed to read '%s': %s"
,
$
json_path
,
$
error
[
'message'
]
)
)
;
}
try
{
$
composer_backup_decoded
=
$
composer_json_obj
->
read
(
)
;
}
catch
(
Exception
$
e
)
{
WP_CLI
::
error
(
sprintf
(
"Failed to parse '%s' as json: %s"
,
$
json_path
,
$
e
->
getMessage
(
)
)
)
;
}
return
[
$
json_path
,
$
composer_backup
,
$
composer_backup_decoded
]
;
}

Reads the WP-CLI packages composer.json, checking validity and returning array containing its path, contents, and decoded contents.

@return array Indexed array containing the path, the contents, and the decoded contents of the WP-CLI packages composer.json.

Reads the WP - CLI packages composer . json checking validity and returning array containing its path contents and decoded contents .


protected function setObject(DataObject $object, $name = 'Object')
    {
        $this->{$name . 'ID'} = $object->ID;
        $this->{$name . 'Type'} = $object->ClassName;
    }

protected function setObject ( DataObject $ object , $ name = 'Object' ) { $ this -> { $ name . 'ID' } = $ object -> ID ; $ this -> { $ name . 'Type' } = $ object -> ClassName ; }

Sets a data object for persisting by adding its id and type to the serialised vars

@param DataObject $object
@param string $name A name to give it, if you want to store more than one


private function loadCustomConfig()
    {
        $custom = $this->getCustomConfig();

        if (!is_array($custom)) {
            return;
        }

        foreach ($custom as $class => $settings) {
            foreach ($settings as $setting => $value) {
                Config::modify()->set($class, $setting, $value);
            }
        }
    }

private function loadCustomConfig ( ) { $ custom = $ this -> getCustomConfig ( ) ; if ( ! is_array ( $ custom ) ) { return ; } foreach ( $ custom as $ class => $ settings ) { foreach ( $ settings as $ setting => $ value ) { Config :: modify ( ) -> set ( $ class , $ setting , $ value ) ; } } }

Sets custom configuration settings from the job data.

Sets custom configuration settings from the job data .


public function run($request)
    {
        $confirm = $request->getVar('confirm');

        $jobs = DataObject::get(QueuedJobDescriptor::class);

        if (!$confirm) {
            echo "Really delete " . $jobs->count() . " jobs? Please add ?confirm=1 to the URL to confirm.";
            return;
        }

        echo "Deleting " . $jobs->count() . " jobs...<br>\n";
        $jobs->removeAll();
        echo "Done.";
    }

Run the task
@param HTTPRequest $request


public function process()
    {
        // construct limit statement if query_limit is valid int value
        $limit = '';
        $query_limit = $this->config()->get('query_limit');
        if (is_numeric($query_limit) && $query_limit >= 0) {
            $limit = ' LIMIT ' . ((int)$query_limit);
        }

        $statusList = implode('\', \'', $this->config()->get('cleanup_statuses'));
        switch ($this->config()->get('cleanup_method')) {
            // If Age, we need to get jobs that are at least n days old
            case "age":
                $cutOff = date(
                    "Y-m-d H:i:s",
                    strtotime(DBDatetime::now() .
                        " - " .
                        $this->config()->cleanup_value .
                        " days")
                );
                $stale = DB::query(
                    'SELECT "ID"
					FROM "QueuedJobDescriptor"
					WHERE "JobStatus"
					IN (\'' . $statusList . '\')
					AND "LastEdited" < \'' . $cutOff . '\'' . $limit
                );
                $staleJobs = $stale->column("ID");
                break;
            // If Number, we need to save n records, then delete from the rest
            case "number":
                $fresh = DB::query(
                    'SELECT "ID"
					FROM "QueuedJobDescriptor"
					ORDER BY "LastEdited"
					ASC LIMIT ' . $this->config()->cleanup_value
                );
                $freshJobIDs = implode('\', \'', $fresh->column("ID"));

                $stale = DB::query(
                    'SELECT "ID"
					FROM "QueuedJobDescriptor"
					WHERE "ID"
					NOT IN (\'' . $freshJobIDs . '\')
					AND "JobStatus"
					IN (\'' . $statusList . '\')' . $limit
                );
                $staleJobs = $stale->column("ID");
                break;
            default:
                $this->addMessage("Incorrect configuration values set. Cleanup ignored");
                $this->isComplete = true;
                return;
        }
        if (empty($staleJobs)) {
            $this->addMessage("No jobs to clean up.");
            $this->isComplete = true;
            $this->reenqueue();
            return;
        }
        $numJobs = count($staleJobs);
        $staleJobs = implode('\', \'', $staleJobs);
        DB::query('DELETE FROM "QueuedJobDescriptor"
			WHERE "ID"
			IN (\'' . $staleJobs . '\')');
        $this->addMessage($numJobs . " jobs cleaned up.");
        // let's make sure there is a cleanupJob in the queue
        $this->reenqueue();
        $this->isComplete = true;
    }

Clear out stale jobs based on the cleanup values


public function queueJob(QueuedJob $job, $startAfter = null, $userId = null, $queueName = null)
    {
        $signature = $job->getSignature();

        // see if we already have this job in a queue
        $filter = [
            'Signature' => $signature,
            'JobStatus' => [
                QueuedJob::STATUS_NEW,
                QueuedJob::STATUS_INIT,
            ],
        ];

        $existing = QueuedJobDescriptor::get()
            ->filter($filter)
            ->first();

        if ($existing && $existing->ID) {
            return $existing->ID;
        }

        $jobDescriptor = new QueuedJobDescriptor();
        $jobDescriptor->JobTitle = $job->getTitle();
        $jobDescriptor->JobType = $queueName ? $queueName : $job->getJobType();
        $jobDescriptor->Signature = $signature;
        $jobDescriptor->Implementation = get_class($job);
        $jobDescriptor->StartAfter = $startAfter;

        $runAsID = 0;
        if ($userId) {
            $runAsID = $userId;
        } elseif (Security::getCurrentUser() && Security::getCurrentUser()->exists()) {
            $runAsID = Security::getCurrentUser()->ID;
        }
        $jobDescriptor->RunAsID = $runAsID;

        // copy data
        $this->copyJobToDescriptor($job, $jobDescriptor);

        $jobDescriptor->write();

        $this->startJob($jobDescriptor, $startAfter);

        return $jobDescriptor->ID;
    }

Adds a job to the queue to be started

Relevant data about the job will be persisted using a QueuedJobDescriptor

@param QueuedJob $job
The job to start.
@param $startAfter
The date (in Y-m-d H:i:s format) to start execution after
@param int $userId
The ID of a user to execute the job as. Defaults to the current user

@return int


protected function copyJobToDescriptor($job, $jobDescriptor)
    {
        $data = $job->getJobData();

        $jobDescriptor->TotalSteps = $data->totalSteps;
        $jobDescriptor->StepsProcessed = $data->currentStep;
        if ($data->isComplete) {
            $jobDescriptor->JobStatus = QueuedJob::STATUS_COMPLETE;
            $jobDescriptor->JobFinished = DBDatetime::now()->Rfc2822();
        }

        $jobDescriptor->SavedJobData = serialize($data->jobData);
        $jobDescriptor->SavedJobMessages = serialize($data->messages);
    }

protected function copyJobToDescriptor ( $ job , $ jobDescriptor ) { $ data = $ job -> getJobData ( ) ; $ jobDescriptor -> TotalSteps = $ data -> totalSteps ; $ jobDescriptor -> StepsProcessed = $ data -> currentStep ; if ( $ data -> isComplete ) { $ jobDescriptor -> JobStatus = QueuedJob :: STATUS_COMPLETE ; $ jobDescriptor -> JobFinished = DBDatetime :: now ( ) -> Rfc2822 ( ) ; } $ jobDescriptor -> SavedJobData = serialize ( $ data -> jobData ) ; $ jobDescriptor -> SavedJobMessages = serialize ( $ data -> messages ) ; }

Copies data from a job into a descriptor for persisting

@param QueuedJob $job
@param QueuedJobDescriptor $jobDescriptor


public function getNextPendingJob($type = null)
    {
        // Filter jobs by type
        $type = $type ?: QueuedJob::QUEUED;
        $list = QueuedJobDescriptor::get()
            ->filter('JobType', $type)
            ->sort('ID', 'ASC');

        // see if there's any blocked jobs that need to be resumed
        /** @var QueuedJobDescriptor $waitingJob */
        $waitingJob = $list
            ->filter('JobStatus', QueuedJob::STATUS_WAIT)
            ->first();
        if ($waitingJob) {
            return $waitingJob;
        }

        // If there's an existing job either running or pending, the lets just return false to indicate
        // that we're still executing
        $runningJob = $list
            ->filter('JobStatus', [QueuedJob::STATUS_INIT, QueuedJob::STATUS_RUN])
            ->first();
        if ($runningJob) {
            return false;
        }

        // Otherwise, lets find any 'new' jobs that are waiting to execute
        /** @var QueuedJobDescriptor $newJob */
        $newJob = $list
            ->filter('JobStatus', QueuedJob::STATUS_NEW)
            ->where(sprintf(
                '"StartAfter" < \'%s\' OR "StartAfter" IS NULL',
                DBDatetime::now()->getValue()
            ))
            ->first();

        return $newJob;
    }

Check the current job queues and see if any of the jobs currently in there should be started. If so,
return the next job that should be executed

@param string $type Job type

@return QueuedJobDescriptor|false

Check the current job queues and see if any of the jobs currently in there should be started . If so return the next job that should be executed


public function checkJobHealth($queue = null)
    {
        $queue = $queue ?: QueuedJob::QUEUED;
        // Select all jobs currently marked as running
        $runningJobs = QueuedJobDescriptor::get()
            ->filter([
                'JobStatus' => [
                    QueuedJob::STATUS_RUN,
                    QueuedJob::STATUS_INIT,
                ],
                'JobType' => $queue,
            ]);

        // If no steps have been processed since the last run, consider it a broken job
        // Only check jobs that have been viewed before. LastProcessedCount defaults to -1 on new jobs.
        $stalledJobs = $runningJobs
            ->filter('LastProcessedCount:GreaterThanOrEqual', 0)
            ->where('"StepsProcessed" = "LastProcessedCount"');
        foreach ($stalledJobs as $stalledJob) {
            $this->restartStalledJob($stalledJob);
        }

        // now, find those that need to be marked before the next check
        // foreach job, mark it as having been incremented
        foreach ($runningJobs as $job) {
            $job->LastProcessedCount = $job->StepsProcessed;
            $job->write();
        }

        // finally, find the list of broken jobs and send an email if there's some found
        $brokenJobs = QueuedJobDescriptor::get()->filter('JobStatus', QueuedJob::STATUS_BROKEN);
        if ($brokenJobs && $brokenJobs->count()) {
            $this->getLogger()->error(
                print_r(
                    [
                        'errno' => 0,
                        'errstr' => 'Broken jobs were found in the job queue',
                        'errfile' => __FILE__,
                        'errline' => __LINE__,
                        'errcontext' => [],
                    ],
                    true
                ),
                [
                    'file' => __FILE__,
                    'line' => __LINE__,
                ]
            );
        }

        return $stalledJobs->count();
    }

Runs an explicit check on all currently running jobs to make sure their "processed" count is incrementing
between each run. If it's not, then we need to flag it as paused due to an error.

This typically happens when a PHP fatal error is thrown, which can't be picked up by the error
handler or exception checker; in this case, we detect these stalled jobs later and fix (try) to
fix them

@param int $queue The queue to check against

Runs an explicit check on all currently running jobs to make sure their processed count is incrementing between each run . If it s not then we need to flag it as paused due to an error .


public function checkDefaultJobs($queue = null)
    {
        $queue = $queue ?: QueuedJob::QUEUED;
        if (count($this->defaultJobs)) {
            $activeJobs = QueuedJobDescriptor::get()->filter(
                'JobStatus',
                [
                    QueuedJob::STATUS_NEW,
                    QueuedJob::STATUS_INIT,
                    QueuedJob::STATUS_RUN,
                    QueuedJob::STATUS_WAIT,
                    QueuedJob::STATUS_PAUSED,
                ]
            );
            foreach ($this->defaultJobs as $title => $jobConfig) {
                if (!isset($jobConfig['filter']) || !isset($jobConfig['type'])) {
                    $this->getLogger()->error(
                        "Default Job config: $title incorrectly set up. Please check the readme for examples",
                        [
                            'file' => __FILE__,
                            'line' => __LINE__,
                        ]
                    );
                    continue;
                }
                $job = $activeJobs->filter(array_merge(
                    ['Implementation' => $jobConfig['type']],
                    $jobConfig['filter']
                ));
                if (!$job->count()) {
                    $this->getLogger()->error(
                        "Default Job config: $title was missing from Queue",
                        [
                            'file' => __FILE__,
                            'line' => __LINE__,
                        ]
                    );
                    Email::create()
                        ->setTo(
                            isset($jobConfig['email'])
                                ? $jobConfig['email']
                                : Config::inst()->get(Email::class, 'queued_job_admin_email')
                        )
                        ->setFrom(Config::inst()->get(Email::class, 'admin_email'))
                        ->setSubject('Default Job "' . $title . '" missing')
                        ->setData($jobConfig)
                        ->addData('Title', $title)
                        ->addData('Site', Director::absoluteBaseURL())
                        ->setHTMLTemplate('QueuedJobsDefaultJob')
                        ->send();
                    if (isset($jobConfig['recreate']) && $jobConfig['recreate']) {
                        if (!array_key_exists('construct', $jobConfig)
                            || !isset($jobConfig['startDateFormat'])
                            || !isset($jobConfig['startTimeString'])
                        ) {
                            $this->getLogger()->error(
                                "Default Job config: $title incorrectly set up. Please check the readme for examples",
                                [
                                    'file' => __FILE__,
                                    'line' => __LINE__,
                                ]
                            );
                            continue;
                        }
                        QueuedJobService::singleton()->queueJob(
                            Injector::inst()->createWithArgs($jobConfig['type'], $jobConfig['construct']),
                            date($jobConfig['startDateFormat'], strtotime($jobConfig['startTimeString']))
                        );
                        $this->getLogger()->info(
                            "Default Job config: $title has been re-added to the Queue",
                            [
                                'file' => __FILE__,
                                'line' => __LINE__,
                            ]
                        );
                    }
                }
            }
        }
    }

Checks through ll the scheduled jobs that are expected to exist


protected function restartStalledJob($stalledJob)
    {
        if ($stalledJob->ResumeCounts < static::config()->get('stall_threshold')) {
            $stalledJob->restart();
            $logLevel = 'warning';
            $message = _t(
                __CLASS__ . '.STALLED_JOB_RESTART_MSG',
                'A job named {name} (#{id}) appears to have stalled. It will be stopped and restarted, please '
                . 'login to make sure it has continued',
                ['name' => $stalledJob->JobTitle, 'id' => $stalledJob->ID]
            );
        } else {
            $stalledJob->pause();
            $logLevel = 'error';
            $message = _t(
                __CLASS__ . '.STALLED_JOB_MSG',
                'A job named {name} (#{id}) appears to have stalled. It has been paused, please login to check it',
                ['name' => $stalledJob->JobTitle, 'id' => $stalledJob->ID]
            );
        }

        $this->getLogger()->log(
            $logLevel,
            $message,
            [
                'file' => __FILE__,
                'line' => __LINE__,
            ]
        );
        $from = Config::inst()->get(Email::class, 'admin_email');
        $to = Config::inst()->get(Email::class, 'queued_job_admin_email');
        $subject = _t(__CLASS__ . '.STALLED_JOB', 'Stalled job');

        if ($to) {
            $mail = Email::create($from, $to, $subject)
                ->setData([
                    'JobID' => $stalledJob->ID,
                    'Message' => $message,
                    'Site' => Director::absoluteBaseURL(),
                ])
                ->setHTMLTemplate('QueuedJobsStalledJob');
            $mail->send();
        }
    }

Attempt to restart a stalled job

@param QueuedJobDescriptor $stalledJob

@return bool True if the job was successfully restarted


protected function initialiseJob(QueuedJobDescriptor $jobDescriptor)
    {
        // create the job class
        $impl = $jobDescriptor->Implementation;
        /** @var QueuedJob $job */
        $job = Injector::inst()->create($impl);
        /* @var $job QueuedJob */
        if (!$job) {
            throw new Exception("Implementation $impl no longer exists");
        }

        $jobDescriptor->JobStatus = QueuedJob::STATUS_INIT;
        $jobDescriptor->write();

        // make sure the data is there
        $this->copyDescriptorToJob($jobDescriptor, $job);

        // see if it needs 'setup' or 'restart' called
        if ($jobDescriptor->StepsProcessed <= 0) {
            $job->setup();
        } else {
            $job->prepareForRestart();
        }

        // make sure the descriptor is up to date with anything changed
        $this->copyJobToDescriptor($job, $jobDescriptor);
        $jobDescriptor->write();

        return $job;
    }

Prepares the given jobDescriptor for execution. Returns the job that
will actually be run in a state ready for executing.

Note that this is called each time a job is picked up to be executed from the cron
job - meaning that jobs that are paused and restarted will have 'setup()' called on them again,
so your job MUST detect that and act accordingly.

@param QueuedJobDescriptor $jobDescriptor
The Job descriptor of a job to prepare for execution

@return QueuedJob|boolean
@throws Exception

Prepares the given jobDescriptor for execution . Returns the job that will actually be run in a state ready for executing .


protected function hasPassedTimeLimit()
    {
        // Ensure a limit exists
        $limit = static::config()->get('time_limit');
        if (!$limit) {
            return false;
        }

        // Ensure started date is set
        $this->markStarted();

        // Check duration
        $now = DBDatetime::now()->getTimestamp();
        return $now > $this->startedAt + $limit;
    }

protected
function
hasPassedTimeLimit
(
)
{
// Ensure a limit exists
$
limit
=
static
::
config
(
)
->
get
(
'time_limit'
)
;
if
(
!
$
limit
)
{
return
false
;
}
// Ensure started date is set
$
this
->
markStarted
(
)
;
// Check duration
$
now
=
DBDatetime
::
now
(
)
->
getTimestamp
(
)
;
return
$
now
>
$
this
->
startedAt
+
$
limit
;
}

Is execution time too long?

@return bool True if the script has passed the configured time_limit


protected function isMemoryTooHigh()
    {
        $used = $this->getMemoryUsage();
        $limit = $this->getMemoryLimit();
        return $limit && ($used > $limit);
    }

protected function isMemoryTooHigh ( ) { $ used = $ this -> getMemoryUsage ( ) ; $ limit = $ this -> getMemoryLimit ( ) ; return $ limit && ( $ used > $ limit ) ; }


protected function parseMemory($memString)
    {
        switch (strtolower(substr($memString, -1))) {
            case "b":
                return round(substr($memString, 0, -1));
            case "k":
                return round(substr($memString, 0, -1) * 1024);
            case "m":
                return round(substr($memString, 0, -1) * 1024 * 1024);
            case "g":
                return round(substr($memString, 0, -1) * 1024 * 1024 * 1024);
            default:
                return round($memString);
        }
    }

protected function parseMemory ( $ memString ) { switch ( strtolower ( substr ( $ memString , - 1 ) ) ) { case "b" : return round ( substr ( $ memString , 0 , - 1 ) ) ; case "k" : return round ( substr ( $ memString , 0 , - 1 ) * 1024 ) ; case "m" : return round ( substr ( $ memString , 0 , - 1 ) * 1024 * 1024 ) ; case "g" : return round ( substr ( $ memString , 0 , - 1 ) * 1024 * 1024 * 1024 ) ; default : return round ( $ memString ) ; } }

Convert memory limit string to bytes.
Based on implementation in install.php5

@param string $memString

@return float

Convert memory limit string to bytes . Based on implementation in install . php5


public function getJobListFilter($type = null, $includeUpUntil = 0)
    {
        $util = singleton(QJUtils::class);

        $filter = ['JobStatus <>' => QueuedJob::STATUS_COMPLETE];
        if ($includeUpUntil) {
            $filter['JobFinished > '] = DBDatetime::create()->setValue(
                DBDatetime::now()->getTimestamp() - $includeUpUntil
            )->Rfc2822();
        }

        $filter = $util->dbQuote($filter, ' OR ');

        if ($type) {
            $filter = $util->dbQuote(['JobType =' => (string)$type]) . ' AND (' . $filter . ')';
        }

        return $filter;
    }

public
function
getJobListFilter
(
$
type
=
,
$
includeUpUntil
=
0
)
{
$
util
=
singleton
(
QJUtils
::
class
)
;
$
filter
=
[
'JobStatus <>'
=>
QueuedJob
::
STATUS_COMPLETE
]
;
if
(
$
includeUpUntil
)
{
$
filter
[
'JobFinished > '
]
=
DBDatetime
::
create
(
)
->
setValue
(
DBDatetime
::
now
(
)
->
getTimestamp
(
)
$
includeUpUntil
)
->
Rfc2822
(
)
;
}
$
filter
=
$
util
->
dbQuote
(
$
filter
,
' OR '
)
;
if
(
$
type
)
{
$
filter
=
$
util
->
dbQuote
(
[
'JobType ='
=>
(
string
)
$
type
]
)
.
' AND ('
.
$
filter
.
')'
;
}
return
$
filter
;
}

Return the SQL filter used to get the job list - this is used by the UI for displaying the job list...

@param string $type
if we're after a particular job list
@param int $includeUpUntil
The number of seconds to include jobs that have just finished, allowing a job list to be built that
includes recently finished jobs

@return string

Return the SQL filter used to get the job list - this is used by the UI for displaying the job list ...


public function runQueue($queue)
    {
        if (!self::config()->get('disable_health_check')) {
            $this->checkJobHealth($queue);
        }
        $this->checkdefaultJobs($queue);
        $this->queueRunner->runQueue($queue);
    }

public function runQueue ( $ queue ) { if ( ! self :: config ( ) -> get ( 'disable_health_check' ) ) { $ this -> checkJobHealth ( $ queue ) ; } $ this -> checkdefaultJobs ( $ queue ) ; $ this -> queueRunner -> runQueue ( $ queue ) ; }

Process the job queue with the current queue runner

@param string $queue


public function processJobQueue($name)
    {
        // Start timer to measure lifetime
        $this->markStarted();

        // Begin main loop
        do {
            if (class_exists(Subsite::class)) {
                // clear subsite back to default to prevent any subsite changes from leaking to
                // subsequent actions
                Subsite::changeSubsite(0);
            }

            $job = $this->getNextPendingJob($name);
            if ($job) {
                $success = $this->runJob($job->ID);
                if (!$success) {
                    // make sure job is null so it doesn't continue the current
                    // processing loop. Next queue executor can pick up where
                    // things left off
                    $job = null;
                }
            }
        } while ($job);
    }

Process all jobs from a given queue

@param string $name The job queue to completely process