Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add variant support to PropertyAdd plugin #252

Open
wants to merge 9 commits into
base: master
Choose a base branch
from

Conversation

iammart
Copy link

@iammart iammart commented Sep 28, 2021

Following on from the discussion on the below thread, I have updated the plugin to query on product code(s) not product ID.

This enables product attributes to be stored on order products that have types of selection or bundle.

https://aimeos.org/help/viewtopic.php?p=15713#p15713

@coveralls
Copy link

coveralls commented Sep 28, 2021

Coverage Status

Coverage decreased (-2.3%) to 85.886% when pulling a6f0848 on iammart:master into f04beb8 on aimeos:master.

@aimeos
Copy link
Owner

aimeos commented Sep 28, 2021

Using codes only leads to the situation that it now works for variant articles but not for selection products any more. You need to fetch all products by their ID or their code.

@iammart
Copy link
Author

iammart commented Sep 28, 2021

If I update the existing expression to following:

$expr = [
    $search->combine('||', [
        $search->compare('==', 'product.id', array_unique($productIds)),
        $search->compare('==', 'product.code', array_unique($productCodes)),
    ]),
    $search->getConditions()
];
$search->setConditions($search->combine('&&', $expr));

And the getProductItems signature to accept an additional property $productCodes, I am now returned two products in the list.

The addAttributes filters the product from the array list using the productId stored on the order. Do we need to check here on the product type and switch between code and ID in order to get the correct product reference to grab the properties we are attaching to the order product?

Or, would you just filter the list of products by code first, followed by id if null is returned?

@aimeos
Copy link
Owner

aimeos commented Sep 28, 2021

It's expected that it returns two products for ordered variant articles (the article and the selection product) and we want to get properties from both if they exists.

You can shorten the code a bit:

$search->add( $search->or( [
    $search->is( 'product.id', '==', array_unique( $productIds ) ),
    $search->is( 'product.code', '==', array_unique( $productCodes ) ),
] );
$manager->search( $search );

@iammart
Copy link
Author

iammart commented Sep 28, 2021

That makes sense now, I was ignoring the possibility the article could still have properties that might need to be stored against the order product. This now introduces more questions :) Which properties should take priority?

For eg. an article and selection will have the same property options. If one of these is a radio group, either yes/no then there is always a value. This would be stored as two json objects, and result in two comma separated values when printed out in the back office under the order.

I'll make the changes for now and we can review.

@aimeos
Copy link
Owner

aimeos commented Sep 28, 2021

If both, selection products and variant articles has the same property type assigned, then article properties should have priority - but I wouldn't care much about that scenario yet

@@ -121,18 +121,19 @@ public function update( \Aimeos\MW\Observer\Publisher\Iface $order, string $acti
if( !is_array( $value ) )
{
\Aimeos\MW\Common\Base::checkClass( \Aimeos\MShop\Order\Item\Base\Product\Iface::class, $value );
return $this->addAttributes( $value, $this->getProductItems( [$value->getProductId()] ), $types );
return $this->addAttributes( $value, $this->getProductItems( [$value->getProductId()], [$value->getProductCode()]), $types );
}

$list = [];
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be better to use $ids and $codes instead of a two-dimensional array

Comment on lines 129 to 134
foreach( $value as $orderProduct )
{
\Aimeos\MW\Common\Base::checkClass( \Aimeos\MShop\Order\Item\Base\Product\Iface::class, $orderProduct );
$list[] = $orderProduct->getProductId();
}
\Aimeos\MW\Common\Base::checkClass(\Aimeos\MShop\Order\Item\Base\Product\Iface::class, $orderProduct);
$list['code'][] = $orderProduct->getProductCode();
$list['id'][] = $orderProduct->getProductId();
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can shorten this to:

$ids = map( $value )->getProductId();
$codes = map( $value )->getProductCode();

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we still need to keep the checks for orderProduct(s)? ie. can do you want me to also remove the loop?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't think they are really necessary

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can shorten this to:

$ids = map( $value )->getProductId();
$codes = map( $value )->getProductCode();

This is sorcery...it's pretty cool to be able to call a method on an array of objects directly from the list.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jQuery style ;-)

Comment on lines 200 to 208
protected function getProductItems( array $productCodes ) : \Aimeos\Map
{
$manager = \Aimeos\MShop::create( $this->getContext(), 'product' );
$search = $manager->filter( true );
$expr = [
$search->compare( '==', 'product.id', array_unique( $productIds ) ),
$search->compare( '==', 'product.code', array_unique( $productCodes ) ),
$search->getConditions(),
];
$search->setConditions( $search->and( $expr ) );
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't use both, IDs and codes

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My bad, I never copied my changes over, im copying my edits from editor to the browser.

Im trying to find more info on how I can run the test suite using phing, I just setup the environment so it has some data to work against.

Comment on lines 161 to 168
if($products->count() > 1) {
$variant = $products->find(
function ( \Aimeos\MShop\Product\Item\Iface $item ) use ( $orderProduct ) : bool {
return $item->getCode() === $orderProduct->getProductCode();
}
);
}

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's more efficient if you use $map = $products->col( null, 'product.code' ); first and then access the products by code

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! Im used to using laravel collections and find myself digging through your docs for functions that are similar that I can use.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Map methods are very close to Laravel, only if standard PHP uses another name (array_column() instead of Laravel's pluck()), then the Map methods follow the PHP naming.

iammart and others added 3 commits September 28, 2021 17:55
Im commiting blind in the browser again, if there is anything I miss im really going to need to spend some time to understand and setup phing.
Comment on lines 127 to 128
$ids = map( $value )->getProductId()->toArray();
$codes = map( $value )->getProductCode()->toArray();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use ->unique() already here and pass the Map objects if you change the method signature of getProductsItems() to iterable instead of array

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did do that initially, but my mistake was type hinting as Aimeos\Map, which would have failed for the call we make on the addAttributes method outside of the loop.

$search->add( $search->or( [
$search->is( 'product.id', '==', array_unique( $productIds ) ),
$search->is( 'product.code', '==', array_unique( $productCodes ) ),
] ) );

return $manager->search( $search, ['product/property'] );
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could save a lot of $products->col( null, 'product.code' ) mappings in addAttributes() if we already return the merged properties already here. For that, we would need an array of product.code/product.id pairs, search for both like now and return a product.id/propertiy items map. What do you think?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method should probably change to reflect what it returns then.

getProductItems should probably become getProductProperties.

I guess we would also want to filter the attributes by the types configured here also, which means passing the types to method.

Do you not think passing an array of code/ids into getProductProperties is less explicit than the current approach, as the filter will need to extract the keys and values for the relevant columns to filter on?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can also pass IDs and codes separately and return a map of product.code as keys and property items as values.
Your are right, we should rename the method, getProperties() is shorter and would fit too

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to return the map keyed as you described?

The changes ive made locally within the now labelled getProperties function, are to accept the types.

I map over the types and return a new map that includes the properties.

It returns something like the following:

Aimeos\Map {#1780
  #list: array:3 [
    "package-weight" => Aimeos\Map {#1778
      #list: array:1 [
        182 => "0.4"
      ]
    }
    "package-net-weight" => Aimeos\Map {#1777
      #list: array:1 [
        190 => "0.4"
      ]
    }
    "package-dangerous" => Aimeos\Map {#1772
      #list: array:2 [
        191 => "0"
        192 => "0"
      ]
    }
  ]
}

This is then passed to addAttributes replacing the existing $products parameter, and I iterate over this and attach to the $orderProduct.

The tests are still passing for me with these changes - but could be a false positive due to coverage. I can publish my changes If you like?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that should work

Comment on lines 152 to 153
if ( ($attrItem = $orderProduct->getAttributeItem($type, 'product/property')) === null ) {
$attrItem = $this->orderAttrManager->create();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can shorten this to:

$attrItem = $orderProduct->getAttributeItem( $type, 'product/property' ) ?: $this->orderAttrManager->create();

foreach( $types as $type )
{
$list = $product->getProperties( $type );
$properties->each(function ( $attributes, $type ) use ( &$orderProduct ) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is $type comming from? & isn't necessary for objects

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$type is the key in the properties map, it's the type the user configures in the admin for the plugin.

An example of the properties passed into addAttributes is below.

Array
(
    [package-width] => Aimeos\Map Object
        (
            [list:protected] => Array
                (
                    [7] => 15.0
                )

            [sep:protected] => /
        )

)

Comment on lines 182 to 188
$items = $manager->search( $search, ['product/property'] );

return map( $types )->map( function ( $type ) use ( $items ) {
if( !( $properties = $items->getProperties($type)->collapse() )->empty() ) {
return [ $type => $properties ];
}
} )->filter()->collapse(1);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be sufficient to get a product code index and the properties as list of items:

return $manager->search( $search, ['product/property'] )->col( null, 'product.code' )->getProperties( $types );

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getProperties requires a string not an array, does it accept comma separated values? If so we would need to implode $types.

@aimeos
Copy link
Owner

aimeos commented Oct 1, 2021

$manager->search( $search, ['product/property'] )
->col( null, 'product.code' )
->map( function( $product ) use( $types ) {
foreach ( $types as $type ) {
return $product->getProperties( $type );
}
});

That should work.

@iammart
Copy link
Author

iammart commented Oct 7, 2021

$manager->search( $search, ['product/property'] )
->col( null, 'product.code' )
->map( function( $product ) use( $types ) {
foreach ( $types as $type ) {
return $product->getProperties( $type );
}
});

That should work.

I thought so too initially, but after I thought about it - it couldn't be :)

We're mapping over the products, and returning properties by type, but this will return early if there is more than a single type as we are not returning a collection of properties by the types given.

For eg. the below would only return a map of properties belonging to the type 'one'

$types = ['one', 'two'];

$properties = $manager->search( $search, ['product/property'] )
    ->col( null, 'product.code' )
    ->map( function( $product ) use( $types ) {
        foreach ( $types as $type ) {
            return $product->getProperties( $type );
        }
    });

I will come back to this when I have some more time, for me what I originally wrote before the refactoring was working and has enabled me to continue.

@aimeos
Copy link
Owner

aimeos commented Oct 13, 2022

@iammart Any news on this topic?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants