Completed
Push — master ( 137f58...aece78 )
by Aimeos
02:30
created

Standard::editProduct()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 31
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 31
rs 8.5806
cc 4
eloc 18
nc 4
nop 4
1
<?php
2
3
/**
4
 * @license LGPLv3, http://opensource.org/licenses/LGPL-3.0
5
 * @copyright Metaways Infosystems GmbH, 2012
6
 * @copyright Aimeos (aimeos.org), 2015-2016
7
 * @package Controller
8
 * @subpackage Frontend
9
 */
10
11
12
namespace Aimeos\Controller\Frontend\Basket;
13
14
15
/**
16
 * Default implementation of the basket frontend controller.
17
 *
18
 * @package Controller
19
 * @subpackage Frontend
20
 */
21
class Standard extends Base implements Iface, \Aimeos\Controller\Frontend\Common\Iface
22
{
23
	private $basket;
24
	private $domainManager;
25
26
27
	/**
28
	 * Initializes the frontend controller.
29
	 *
30
	 * @param \Aimeos\MShop\Context\Item\Iface $context Object storing the required instances for manaing databases
31
	 *  connections, logger, session, etc.
32
	 */
33
	public function __construct( \Aimeos\MShop\Context\Item\Iface $context )
34
	{
35
		parent::__construct( $context );
36
37
		$this->domainManager = \Aimeos\MShop\Factory::createManager( $context, 'order/base' );
38
		$this->basket = $this->domainManager->getSession();
39
40
		$this->checkLocale();
41
	}
42
43
44
	/**
45
	 * Empties the basket and removing all products, addresses, services, etc.
46
	 */
47
	public function clear()
48
	{
49
		$this->basket = $this->domainManager->createItem();
50
		$this->domainManager->setSession( $this->basket );
51
	}
52
53
54
	/**
55
	 * Returns the basket object.
56
	 *
57
	 * @return \Aimeos\MShop\Order\Item\Base\Iface Basket holding products, addresses and delivery/payment options
58
	 */
59
	public function get()
60
	{
61
		return $this->basket;
62
	}
63
64
65
	/**
66
	 * Adds a categorized product to the basket of the user stored in the session.
67
	 *
68
	 * @param string $prodid ID of the base product to add
69
	 * @param integer $quantity Amount of products that should by added
70
	 * @param array $options Possible options are: 'stock'=>true|false and 'variant'=>true|false
71
	 * 	The 'stock'=>false option allows adding products without being in stock.
72
	 * 	The 'variant'=>false option allows adding the selection product to the basket
73
	 * 	instead of the specific sub-product if the variant-building attribute IDs
74
	 * 	doesn't match a specific sub-product or if the attribute IDs are missing.
75
	 * @param array $variantAttributeIds List of variant-building attribute IDs that identify a specific product
76
	 * 	in a selection products
77
	 * @param array $configAttributeIds  List of attribute IDs that doesn't identify a specific product in a
78
	 * 	selection of products but are stored together with the product (e.g. for configurable products)
79
	 * @param array $hiddenAttributeIds List of attribute IDs that should be stored along with the product in the order
80
	 * @param array $customAttributeValues Associative list of attribute IDs and arbitrary values that should be stored
81
	 * 	along with the product in the order
82
	 * @param string $warehouse Unique code of the warehouse to deliver the products from
83
	 * @throws \Aimeos\Controller\Frontend\Basket\Exception If the product isn't available
84
	 */
85
	public function addProduct( $prodid, $quantity = 1, array $options = array(), array $variantAttributeIds = array(),
86
		array $configAttributeIds = array(), array $hiddenAttributeIds = array(), array $customAttributeValues = array(),
87
		$warehouse = 'default' )
88
	{
89
		$context = $this->getContext();
90
91
		$productItem = $this->getDomainItem( 'product', 'product.id', $prodid, array( 'media', 'supplier', 'price', 'product', 'text' ) );
92
93
		$orderBaseProductItem = \Aimeos\MShop\Factory::createManager( $context, 'order/base/product' )->createItem();
94
		$orderBaseProductItem->copyFrom( $productItem );
95
		$orderBaseProductItem->setQuantity( $quantity );
96
		$orderBaseProductItem->setWarehouseCode( $warehouse );
97
98
		$attr = array();
99
		$prices = $productItem->getRefItems( 'price', 'default', 'default' );
100
101
		switch( $productItem->getType() )
102
		{
103
			case 'select':
104
				$attr = $this->getVariantDetails( $orderBaseProductItem, $productItem, $prices, $variantAttributeIds, $options );
105
				break;
106
			case 'bundle':
107
				$this->addBundleProducts( $orderBaseProductItem, $productItem, $variantAttributeIds, $warehouse );
108
				break;
109
		}
110
111
		$priceManager = \Aimeos\MShop\Factory::createManager( $context, 'price' );
112
		$price = $priceManager->getLowestPrice( $prices, $quantity );
113
114
		$attr = array_merge( $attr, $this->createOrderProductAttributes( $price, $prodid, $quantity, $configAttributeIds, 'config' ) );
115
		$attr = array_merge( $attr, $this->createOrderProductAttributes( $price, $prodid, $quantity, $hiddenAttributeIds, 'hidden' ) );
116
		$attr = array_merge( $attr, $this->createOrderProductAttributes( $price, $prodid, $quantity, array_keys( $customAttributeValues ), 'custom', $customAttributeValues ) );
117
118
		// remove product rebate of original price in favor to rebates granted for the order
119
		$price->setRebate( '0.00' );
120
121
		$orderBaseProductItem->setPrice( $price );
122
		$orderBaseProductItem->setAttributes( $attr );
123
124
		$this->basket->addProduct( $orderBaseProductItem );
1 ignored issue
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Aimeos\MShop\Common\Item\Iface as the method addProduct() does only exist in the following implementations of said interface: Aimeos\MShop\Order\Item\Base\Base, Aimeos\MShop\Order\Item\Base\Standard.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
125
		$this->domainManager->setSession( $this->basket );
126
	}
127
128
129
	/**
130
	 * Deletes a product item from the basket.
131
	 *
132
	 * @param integer $position Position number (key) of the order product item
133
	 */
134
	public function deleteProduct( $position )
135
	{
136
		$product = $this->basket->getProduct( $position );
1 ignored issue
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Aimeos\MShop\Common\Item\Iface as the method getProduct() does only exist in the following implementations of said interface: Aimeos\MShop\Order\Item\Base\Base, Aimeos\MShop\Order\Item\Base\Standard.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
137
138
		if( $product->getFlags() === \Aimeos\MShop\Order\Item\Base\Product\Base::FLAG_IMMUTABLE )
139
		{
140
			$msg = sprintf( 'Basket item at position "%1$d" cannot be deleted manually', $position );
141
			throw new \Aimeos\Controller\Frontend\Basket\Exception( $msg );
142
		}
143
144
		$this->basket->deleteProduct( $position );
1 ignored issue
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Aimeos\MShop\Common\Item\Iface as the method deleteProduct() does only exist in the following implementations of said interface: Aimeos\MShop\Order\Item\Base\Base, Aimeos\MShop\Order\Item\Base\Standard.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
145
		$this->domainManager->setSession( $this->basket );
146
	}
147
148
149
	/**
150
	 * Edits the quantity of a product item in the basket.
151
	 *
152
	 * @param integer $position Position number (key) of the order product item
153
	 * @param integer $quantity New quantiy of the product item
154
	 * @param array $options Possible options are: 'stock'=>true|false
155
	 * 	The 'stock'=>false option allows adding products without being in stock.
156
	 * @param string[] $configAttributeCodes Codes of the product config attributes that should be REMOVED
157
	 */
158
	public function editProduct( $position, $quantity, array $options = array(),
159
		array $configAttributeCodes = array() )
160
	{
161
		$product = $this->basket->getProduct( $position );
1 ignored issue
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Aimeos\MShop\Common\Item\Iface as the method getProduct() does only exist in the following implementations of said interface: Aimeos\MShop\Order\Item\Base\Base, Aimeos\MShop\Order\Item\Base\Standard.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
162
		$product->setQuantity( $quantity ); // Enforce check immediately
163
164
		if( $product->getFlags() & \Aimeos\MShop\Order\Item\Base\Product\Base::FLAG_IMMUTABLE )
165
		{
166
			$msg = sprintf( 'Basket item at position "%1$d" cannot be changed', $position );
167
			throw new \Aimeos\Controller\Frontend\Basket\Exception( $msg );
168
		}
169
170
		$attributes = $product->getAttributes();
171
		foreach( $attributes as $key => $attribute )
172
		{
173
			if( in_array( $attribute->getCode(), $configAttributeCodes ) ) {
174
				unset( $attributes[$key] );
175
			}
176
		}
177
		$product->setAttributes( $attributes );
178
179
		$productItem = $this->getDomainItem( 'product', 'product.code', $product->getProductCode(), array( 'price', 'text' ) );
180
		$prices = $productItem->getRefItems( 'price', 'default' );
181
182
		$product->setPrice( $this->calcPrice( $product, $prices, $quantity ) );
183
184
		$this->basket->deleteProduct( $position );
1 ignored issue
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Aimeos\MShop\Common\Item\Iface as the method deleteProduct() does only exist in the following implementations of said interface: Aimeos\MShop\Order\Item\Base\Base, Aimeos\MShop\Order\Item\Base\Standard.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
185
		$this->basket->addProduct( $product, $position );
1 ignored issue
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Aimeos\MShop\Common\Item\Iface as the method addProduct() does only exist in the following implementations of said interface: Aimeos\MShop\Order\Item\Base\Base, Aimeos\MShop\Order\Item\Base\Standard.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
186
187
		$this->domainManager->setSession( $this->basket );
188
	}
189
190
191
	/**
192
	 * Adds the given coupon code and updates the basket.
193
	 *
194
	 * @param string $code Coupon code entered by the user
195
	 * @throws \Aimeos\Controller\Frontend\Basket\Exception if the coupon code is invalid or not allowed
196
	 */
197
	public function addCoupon( $code )
198
	{
199
		$manager = \Aimeos\MShop\Factory::createManager( $this->getContext(), 'coupon' );
200
		$codeManager = \Aimeos\MShop\Factory::createManager( $this->getContext(), 'coupon/code' );
201
202
203
		$search = $codeManager->createSearch( true );
204
		$expr = array(
205
			$search->compare( '==', 'coupon.code.code', $code ),
206
			$search->getConditions(),
207
		);
208
		$search->setConditions( $search->combine( '&&', $expr ) );
209
		$search->setSlice( 0, 1 );
210
211
		$result = $codeManager->searchItems( $search );
212
213
		if( ( $codeItem = reset( $result ) ) === false ) {
214
			throw new \Aimeos\Controller\Frontend\Basket\Exception( sprintf( 'Coupon code "%1$s" is invalid or not available any more', $code ) );
215
		}
216
217
218
		$search = $manager->createSearch( true );
219
		$expr = array(
220
			$search->compare( '==', 'coupon.id', $codeItem->getParentId() ),
221
			$search->getConditions(),
222
		);
223
		$search->setConditions( $search->combine( '&&', $expr ) );
224
		$search->setSlice( 0, 1 );
225
226
		$result = $manager->searchItems( $search );
227
228
		if( ( $item = reset( $result ) ) === false ) {
229
			throw new \Aimeos\Controller\Frontend\Basket\Exception( sprintf( 'Coupon for code "%1$s" is not available any more', $code ) );
230
		}
231
232
233
		$provider = $manager->getProvider( $item, $code );
234
235
		if( $provider->isAvailable( $this->basket ) !== true ) {
236
			throw new \Aimeos\Controller\Frontend\Basket\Exception( sprintf( 'Requirements for coupon code "%1$s" aren\'t met', $code ) );
237
		}
238
239
		$provider->addCoupon( $this->basket );
240
		$this->domainManager->setSession( $this->basket );
241
	}
242
243
244
	/**
245
	 * Removes the given coupon code and its effects from the basket.
246
	 *
247
	 * @param string $code Coupon code entered by the user
248
	 * @throws \Aimeos\Controller\Frontend\Basket\Exception if the coupon code is invalid
249
	 */
250
	public function deleteCoupon( $code )
251
	{
252
		$manager = \Aimeos\MShop\Factory::createManager( $this->getContext(), 'coupon' );
253
254
		$search = $manager->createSearch();
255
		$search->setConditions( $search->compare( '==', 'coupon.code.code', $code ) );
256
		$search->setSlice( 0, 1 );
257
258
		$result = $manager->searchItems( $search );
259
260
		if( ( $item = reset( $result ) ) === false ) {
261
			throw new \Aimeos\Controller\Frontend\Basket\Exception( sprintf( 'Coupon code "%1$s" is invalid', $code ) );
262
		}
263
264
		$manager->getProvider( $item, $code )->deleteCoupon( $this->basket );
265
		$this->domainManager->setSession( $this->basket );
266
	}
267
268
269
	/**
270
	 * Sets the address of the customer in the basket.
271
	 *
272
	 * @param string $type Address type constant from \Aimeos\MShop\Order\Item\Base\Address\Base
273
	 * @param \Aimeos\MShop\Common\Item\Address\Iface|array|null $value Address object or array with key/value pairs of address or null to remove address from basket
274
	 * @throws \Aimeos\Controller\Frontend\Basket\Exception If the billing or delivery address is not of any required type of
275
	 * 	if one of the keys is invalid when using an array with key/value pairs
276
	 */
277
	public function setAddress( $type, $value )
278
	{
279
		$address = \Aimeos\MShop\Factory::createManager( $this->getContext(), 'order/base/address' )->createItem();
280
		$address->setType( $type );
281
282
		if( $value instanceof \Aimeos\MShop\Common\Item\Address\Iface )
283
		{
284
			$address->copyFrom( $value );
285
			$this->basket->setAddress( $address, $type );
1 ignored issue
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Aimeos\MShop\Common\Item\Iface as the method setAddress() does only exist in the following implementations of said interface: Aimeos\MShop\Order\Item\Base\Base, Aimeos\MShop\Order\Item\Base\Standard.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
286
		}
287
		else if( is_array( $value ) )
288
		{
289
			$this->setAddressFromArray( $address, $value );
290
			$this->basket->setAddress( $address, $type );
1 ignored issue
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Aimeos\MShop\Common\Item\Iface as the method setAddress() does only exist in the following implementations of said interface: Aimeos\MShop\Order\Item\Base\Base, Aimeos\MShop\Order\Item\Base\Standard.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
291
		}
292
		else if( $value === null )
293
		{
294
			$this->basket->deleteAddress( $type );
1 ignored issue
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Aimeos\MShop\Common\Item\Iface as the method deleteAddress() does only exist in the following implementations of said interface: Aimeos\MShop\Order\Item\Base\Base, Aimeos\MShop\Order\Item\Base\Standard.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
295
		}
296
		else
297
		{
298
			throw new \Aimeos\Controller\Frontend\Basket\Exception( sprintf( 'Invalid value for address type "%1$s"', $type ) );
299
		}
300
301
		$this->domainManager->setSession( $this->basket );
302
	}
303
304
305
	/**
306
	 * Sets the delivery/payment service item based on the service ID.
307
	 *
308
	 * @param string $type Service type code like 'payment' or 'delivery'
309
	 * @param string $id Unique ID of the service item
310
	 * @param array $attributes Associative list of key/value pairs containing the attributes selected or
311
	 * 	entered by the customer when choosing one of the delivery or payment options
312
	 * @throws \Aimeos\Controller\Frontend\Basket\Exception If there is no price to the service item attached
313
	 */
314
	public function setService( $type, $id, array $attributes = array() )
315
	{
316
		$context = $this->getContext();
317
318
		$serviceManager = \Aimeos\MShop\Factory::createManager( $context, 'service' );
319
		$serviceItem = $this->getDomainItem( 'service', 'service.id', $id, array( 'media', 'price', 'text' ) );
320
321
		$provider = $serviceManager->getProvider( $serviceItem );
322
		$result = $provider->checkConfigFE( $attributes );
323
		$unknown = array_diff_key( $attributes, $result );
324
325
		if( count( $unknown ) > 0 )
326
		{
327
			$msg = sprintf( 'Unknown attributes "%1$s"', implode( '","', array_keys( $unknown ) ) );
328
			throw new \Aimeos\Controller\Frontend\Basket\Exception( $msg );
329
		}
330
331
		foreach( $result as $key => $value )
332
		{
333
			if( $value !== null ) {
334
				throw new \Aimeos\Controller\Frontend\Basket\Exception( $value );
335
			}
336
		}
337
338
		$orderBaseServiceManager = \Aimeos\MShop\Factory::createManager( $context, 'order/base/service' );
339
		$orderServiceItem = $orderBaseServiceManager->createItem();
340
		$orderServiceItem->copyFrom( $serviceItem );
341
342
		$price = $provider->calcPrice( $this->basket );
343
		// remove service rebate of original price
344
		$price->setRebate( '0.00' );
345
		$orderServiceItem->setPrice( $price );
346
347
		$provider->setConfigFE( $orderServiceItem, $attributes );
348
349
		$this->basket->setService( $orderServiceItem, $type );
1 ignored issue
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Aimeos\MShop\Common\Item\Iface as the method setService() does only exist in the following implementations of said interface: Aimeos\MShop\Order\Item\Base\Base, Aimeos\MShop\Order\Item\Base\Standard.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
350
		$this->domainManager->setSession( $this->basket );
351
	}
352
353
354
	/**
355
	 * Adds the bundled products to the order product item.
356
	 *
357
	 * @param \Aimeos\MShop\Order\Item\Base\Product\Iface $orderBaseProductItem Order product item
358
	 * @param \Aimeos\MShop\Product\Item\Iface $productItem Bundle product item
359
	 * @param array $variantAttributeIds List of product variant attribute IDs
360
	 * @param string $warehouse
361
	 */
362
	protected function addBundleProducts( \Aimeos\MShop\Order\Item\Base\Product\Iface $orderBaseProductItem,
363
		\Aimeos\MShop\Product\Item\Iface $productItem, array $variantAttributeIds, $warehouse )
364
	{
365
		$quantity = $orderBaseProductItem->getQuantity();
366
		$products = $subProductIds = $orderProducts = array();
367
		$orderProductManager = \Aimeos\MShop\Factory::createManager( $this->getContext(), 'order/base/product' );
368
369
		foreach( $productItem->getRefItems( 'product', null, 'default' ) as $item ) {
370
			$subProductIds[] = $item->getId();
371
		}
372
373
		if( count( $subProductIds ) > 0 )
374
		{
375
			$productManager = \Aimeos\MShop\Factory::createManager( $this->getContext(), 'product' );
376
377
			$search = $productManager->createSearch( true );
378
			$expr = array(
379
				$search->compare( '==', 'product.id', $subProductIds ),
380
				$search->getConditions(),
381
			);
382
			$search->setConditions( $search->combine( '&&', $expr ) );
383
384
			$products = $productManager->searchItems( $search, array( 'attribute', 'media', 'price', 'text' ) );
385
		}
386
387
		foreach( $products as $product )
388
		{
389
			$prices = $product->getRefItems( 'price', 'default', 'default' );
390
391
			$orderProduct = $orderProductManager->createItem();
392
			$orderProduct->copyFrom( $product );
393
			$orderProduct->setWarehouseCode( $warehouse );
394
			$orderProduct->setPrice( $this->calcPrice( $orderProduct, $prices, $quantity ) );
395
396
			$orderProducts[] = $orderProduct;
397
		}
398
399
		$orderBaseProductItem->setProducts( $orderProducts );
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Aimeos\MShop\Order\Item\Base\Product\Iface as the method setProducts() does only exist in the following implementations of said interface: Aimeos\MShop\Order\Item\Base\Product\Standard.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
400
	}
401
402
403
	/**
404
	 * Checks for a locale mismatch and migrates the products to the new basket if necessary.
405
	 */
406
	protected function checkLocale()
407
	{
408
		$errors = array();
409
		$context = $this->getContext();
410
		$session = $context->getSession();
411
		$locale = $this->basket->getLocale();
1 ignored issue
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Aimeos\MShop\Common\Item\Iface as the method getLocale() does only exist in the following implementations of said interface: Aimeos\MShop\Order\Item\Base\Base, Aimeos\MShop\Order\Item\Base\Standard.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
412
413
		$localeStr = $session->get( 'aimeos/basket/locale' );
414
		$localeKey = $locale->getSite()->getCode() . '|' . $locale->getLanguageId() . '|' . $locale->getCurrencyId();
415
416
		if( $localeStr !== null && $localeStr !== $localeKey )
417
		{
418
			$locParts = explode( '|', $localeStr );
419
			$locSite = ( isset( $locParts[0] ) ? $locParts[0] : '' );
420
			$locLanguage = ( isset( $locParts[1] ) ? $locParts[1] : '' );
421
			$locCurrency = ( isset( $locParts[2] ) ? $locParts[2] : '' );
422
423
			$localeManager = \Aimeos\MShop\Factory::createManager( $context, 'locale' );
424
			$locale = $localeManager->bootstrap( $locSite, $locLanguage, $locCurrency, false );
425
426
			$context = clone $context;
427
			$context->setLocale( $locale );
428
429
			$manager = \Aimeos\MShop\Order\Manager\Factory::createManager( $context )->getSubManager( 'base' );
430
			$basket = $manager->getSession();
431
432
			$this->copyAddresses( $basket, $errors, $localeKey );
433
			$this->copyServices( $basket, $errors );
434
			$this->copyProducts( $basket, $errors, $localeKey );
435
			$this->copyCoupons( $basket, $errors, $localeKey );
436
437
			$manager->setSession( $basket );
438
		}
439
440
		$session->set( 'aimeos/basket/locale', $localeKey );
441
	}
442
443
444
	/**
445
	 * Migrates the addresses from the old basket to the current one.
446
	 *
447
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $basket Basket object
448
	 * @param array $errors Associative list of previous errors
449
	 * @param string $localeKey Unique identifier of the site, language and currency
450
	 * @return array Associative list of errors occured
451
	 */
452
	protected function copyAddresses( \Aimeos\MShop\Order\Item\Base\Iface $basket, array $errors, $localeKey )
453
	{
454
		foreach( $basket->getAddresses() as $type => $item )
455
		{
456
			try
457
			{
458
				$this->setAddress( $type, $item->toArray() );
459
				$basket->deleteAddress( $type );
460
			}
461
			catch( \Exception $e )
462
			{
463
				$logger = $this->getContext()->getLogger();
464
				$str = 'Error migrating address with type "%1$s" in basket to locale "%2$s": %3$s';
465
				$logger->log( sprintf( $str, $type, $localeKey, $e->getMessage() ), \Aimeos\MW\Logger\Base::INFO );
466
				$errors['address'][$type] = $e->getMessage();
467
			}
468
		}
469
470
		return $errors;
471
	}
472
473
474
	/**
475
	 * Migrates the coupons from the old basket to the current one.
476
	 *
477
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $basket Basket object
478
	 * @param array $errors Associative list of previous errors
479
	 * @param string $localeKey Unique identifier of the site, language and currency
480
	 * @return array Associative list of errors occured
481
	 */
482
	protected function copyCoupons( \Aimeos\MShop\Order\Item\Base\Iface $basket, array $errors, $localeKey )
483
	{
484
		foreach( $basket->getCoupons() as $code => $list )
485
		{
486
			try
487
			{
488
				$this->addCoupon( $code );
489
				$basket->deleteCoupon( $code, true );
490
			}
491
			catch( \Exception $e )
492
			{
493
				$logger = $this->getContext()->getLogger();
494
				$str = 'Error migrating coupon with code "%1$s" in basket to locale "%2$s": %3$s';
495
				$logger->log( sprintf( $str, $code, $localeKey, $e->getMessage() ), \Aimeos\MW\Logger\Base::INFO );
496
				$errors['coupon'][$code] = $e->getMessage();
497
			}
498
		}
499
500
		return $errors;
501
	}
502
503
504
	/**
505
	 * Migrates the products from the old basket to the current one.
506
	 *
507
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $basket Basket object
508
	 * @param array $errors Associative list of previous errors
509
	 * @param string $localeKey Unique identifier of the site, language and currency
510
	 * @return array Associative list of errors occured
511
	 */
512
	protected function copyProducts( \Aimeos\MShop\Order\Item\Base\Iface $basket, array $errors, $localeKey )
513
	{
514
		foreach( $basket->getProducts() as $pos => $product )
515
		{
516
			if( $product->getFlags() & \Aimeos\MShop\Order\Item\Base\Product\Base::FLAG_IMMUTABLE ) {
517
				continue;
518
			}
519
520
			try
521
			{
522
				$attrIds = array();
523
524
				foreach( $product->getAttributes() as $attrItem ) {
525
					$attrIds[$attrItem->getType()][] = $attrItem->getAttributeId();
526
				}
527
528
				$this->addProduct(
529
					$product->getProductId(),
530
					$product->getQuantity(),
531
					array(),
532
					$this->getValue( $attrIds, 'variant', array() ),
533
					$this->getValue( $attrIds, 'config', array() ),
534
					$this->getValue( $attrIds, 'hidden', array() ),
535
					$this->getValue( $attrIds, 'custom', array() ),
536
					$product->getWarehouseCode()
537
				);
538
539
				$basket->deleteProduct( $pos );
540
			}
541
			catch( \Exception $e )
542
			{
543
				$code = $product->getProductCode();
544
				$logger = $this->getContext()->getLogger();
545
				$str = 'Error migrating product with code "%1$s" in basket to locale "%2$s": %3$s';
546
				$logger->log( sprintf( $str, $code, $localeKey, $e->getMessage() ), \Aimeos\MW\Logger\Base::INFO );
547
				$errors['product'][$pos] = $e->getMessage();
548
			}
549
		}
550
551
		return $errors;
552
	}
553
554
555
	/**
556
	 * Migrates the services from the old basket to the current one.
557
	 *
558
	 * @param \Aimeos\MShop\Order\Item\Base\Iface $basket Basket object
559
	 * @param array $errors Associative list of previous errors
560
	 * @return array Associative list of errors occured
561
	 */
562
	protected function copyServices( \Aimeos\MShop\Order\Item\Base\Iface $basket, array $errors )
563
	{
564
		foreach( $basket->getServices() as $type => $item )
565
		{
566
			try
567
			{
568
				$attributes = array();
569
570
				foreach( $item->getAttributes() as $attrItem ) {
571
					$attributes[$attrItem->getCode()] = $attrItem->getValue();
572
				}
573
574
				$this->setService( $type, $item->getServiceId(), $attributes );
575
				$basket->deleteService( $type );
576
			}
577
			catch( \Exception $e ) {; } // Don't notify the user as appropriate services can be added automatically
578
		}
579
580
		return $errors;
581
	}
582
583
584
	/**
585
	 * Fills the order address object with the values from the array.
586
	 *
587
	 * @param \Aimeos\MShop\Order\Item\Base\Address\Iface $address Address item to store the values into
588
	 * @param array $map Associative array of key/value pairs. The keys must be the same as when calling toArray() from
589
	 * 	an address item.
590
	 * @throws \Aimeos\Controller\Frontend\Basket\Exception
591
	 */
592
	protected function setAddressFromArray( \Aimeos\MShop\Order\Item\Base\Address\Iface $address, array $map )
593
	{
594
		foreach( $map as $key => $value ) {
595
			$map[$key] = strip_tags( $value ); // prevent XSS
596
		}
597
598
		$errors = $address->fromArray( $map );
599
600
		if( count( $errors ) > 0 )
601
		{
602
			$msg = sprintf( 'Invalid address properties, please check your input' );
603
			throw new \Aimeos\Controller\Frontend\Basket\Exception( $msg, 0, null, $errors );
604
		}
605
	}
606
607
608
	/**
609
	 * Returns the variant attributes and updates the price list if necessary.
610
	 *
611
	 * @param \Aimeos\MShop\Order\Item\Base\Product\Iface $orderBaseProductItem Order product item
612
	 * @param \Aimeos\MShop\Product\Item\Iface &$productItem Product item which is replaced if necessary
613
	 * @param array &$prices List of product prices that will be updated if necessary
614
	 * @param array $variantAttributeIds List of product variant attribute IDs
615
	 * @param array $options Associative list of options
616
	 * @return \Aimeos\MShop\Order\Item\Base\Product\Attribute\Iface[] List of order product attributes
617
	 * @throws \Aimeos\Controller\Frontend\Basket\Exception If no product variant is found
618
	 */
619
	protected function getVariantDetails( \Aimeos\MShop\Order\Item\Base\Product\Iface $orderBaseProductItem,
620
		\Aimeos\MShop\Product\Item\Iface &$productItem, array &$prices, array $variantAttributeIds, array $options )
621
	{
622
		$attr = array();
623
		$productItems = $this->getProductVariants( $productItem, $variantAttributeIds );
624
625
		if( count( $productItems ) > 1 )
626
		{
627
			$msg = sprintf( 'No unique article found for selected attributes and product ID "%1$s"', $productItem->getId() );
628
			throw new \Aimeos\Controller\Frontend\Basket\Exception( $msg );
629
		}
630
		else if( ( $result = reset( $productItems ) ) !== false ) // count == 1
631
		{
632
			$productItem = $result;
633
			$orderBaseProductItem->setProductCode( $productItem->getCode() );
634
635
			$subprices = $productItem->getRefItems( 'price', 'default', 'default' );
636
637
			if( count( $subprices ) > 0 ) {
638
				$prices = $subprices;
639
			}
640
641
			$orderProductAttrManager = \Aimeos\MShop\Factory::createManager( $this->getContext(), 'order/base/product/attribute' );
642
			$variantAttributes = $productItem->getRefItems( 'attribute', null, 'variant' );
643
644
			foreach( $this->getAttributes( array_keys( $variantAttributes ), array( 'text' ) ) as $attrItem )
645
			{
646
				$orderAttributeItem = $orderProductAttrManager->createItem();
647
				$orderAttributeItem->copyFrom( $attrItem );
648
				$orderAttributeItem->setType( 'variant' );
649
650
				$attr[] = $orderAttributeItem;
651
			}
652
		}
653
		else if( !isset( $options['variant'] ) || $options['variant'] != false ) // count == 0
654
		{
655
			$msg = sprintf( 'No article found for selected attributes and product ID "%1$s"', $productItem->getId() );
656
			throw new \Aimeos\Controller\Frontend\Basket\Exception( $msg );
657
		}
658
659
		return $attr;
660
	}
661
}
662