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

[4.2] Getting unknown property: craft\commerce\elements\db\VariantQuery::hasSales #471

Closed
Nellyaa opened this issue Feb 7, 2023 · 8 comments

Comments

@Nellyaa
Copy link

Nellyaa commented Feb 7, 2023

I'm currently playing with integrating Blitz into a Craft Commerce install and stumbled upon this.
We have a page that lists all variants/products on sale, we utilize something akin to this query in Twig:

{{ craft.variants.hasSales(true).one().title }}

which works fine without Blitz enabled. But when I enable Blitz caching for this page I get an unknown property exception. Since it works when Blitz is disabled, I assume the problem lies with the Blitz plugin?

Unknown Property – [yii\base\UnknownPropertyException](https://www.yiiframework.com/doc-2.0/yii-base-unknownpropertyexception.html)
Getting unknown property: craft\commerce\elements\db\VariantQuery::hasSales

Reproduce:

  1. Install Craft Commerce
  2. Create a product and a sale and make product on sale
  3. Make an example twig template that displays variants on sale, e.g. {{ craft.variants.hasSales(true).one().title }}
  4. Enable Blitz caching of this page
  5. Get exception

Using:
Craft Pro 4.3.7.1
Commerce 4.2.5.1
Blitz 4.2.3

@bencroker
Copy link
Collaborator

Can you please post the full error stack trace? It'll appear with devMode enabled, otherwise you should find it in the logs.

@Nellyaa
Copy link
Author

Nellyaa commented Feb 7, 2023

Absolutely, sorry!


Unknown Property – [yii\base\UnknownPropertyException](https://www.yiiframework.com/doc-2.0/yii-base-unknownpropertyexception.html)
Getting unknown property: craft\commerce\elements\db\VariantQuery::hasSales
1. in /var/www/vendor/yiisoft/yii2/base/Component.phpat line 154
145146147148149150151152153154155156157158159160161162163            if ($behavior->canGetProperty($name)) {
                return $behavior->$name;
            }
        }
 
        if (method_exists($this, 'set' . $name)) {
            throw new InvalidCallException('Getting write-only property: ' . get_class($this) . '::' . $name);
        }
 
        throw new UnknownPropertyException('Getting unknown property: ' . get_class($this) . '::' . $name);
    }
 
    /**
     * Sets the value of a component property.
     *
     * This method will check in the following order and act accordingly:
     *
     *  - a property defined by a setter: set the property value
     *  - an event in the format of "on xyz": attach the handler to the event "xyz"
2. in /var/www/vendor/putyourlightson/craft-blitz/src/helpers/ElementQueryHelper.php at line 32– [yii\base\Component::__get](https://www.yiiframework.com/doc-2.0/yii-base-component.html#__get()-detail)('hasSales')
26272829303132333435363738    {
        $params = [];
 
        $defaultParams = self::getDefaultElementQueryParams($elementQuery->elementType);
 
        foreach ($defaultParams as $key => $default) {
            $value = $elementQuery->{$key};
 
            if ($value !== $default) {
                $params[$key] = $value;
            }
        }
 
3. in /var/www/vendor/putyourlightson/craft-blitz/src/services/GenerateCacheService.php at line 189– putyourlightson\blitz\helpers\ElementQueryHelper::getUniqueElementQueryParams(craft\commerce\elements\db\VariantQuery)
183184185186187188189190191192193194195 
    /**
     * Saves an element query.
     */
    public function saveElementQuery(ElementQuery $elementQuery): void
    {
        $params = json_encode(ElementQueryHelper::getUniqueElementQueryParams($elementQuery));
 
        // Create a unique index from the element type and parameters for quicker indexing and less storage
        $index = sprintf('%u', crc32($elementQuery->elementType . $params));
 
        // Require a mutex for the element query index to avoid doing the same operation multiple times
        $mutex = Craft::$app->getMutex();
4. in /var/www/vendor/putyourlightson/craft-blitz/src/services/GenerateCacheService.php at line 181– putyourlightson\blitz\services\GenerateCacheService::saveElementQuery(craft\commerce\elements\db\VariantQuery)
175176177178179180181182183184185186187 
        // Don't proceed if this is a relation query
        if (ElementQueryHelper::isRelationQuery($elementQuery)) {
            return;
        }
 
        $this->saveElementQuery($elementQuery);
    }
 
    /**
     * Saves an element query.
     */
    public function saveElementQuery(ElementQuery $elementQuery): void
5. in /var/www/vendor/putyourlightson/craft-blitz/src/services/GenerateCacheService.php at line 115– putyourlightson\blitz\services\GenerateCacheService::addElementQuery(craft\commerce\elements\db\VariantQuery)
109110111112113114115116117118119120121        // Register element query prepare event
        Event::on(ElementQuery::class, ElementQuery::EVENT_BEFORE_PREPARE,
            function(CancelableEvent $event) {
                if (Craft::$app->getResponse()->getIsOk()) {
                    /** @var ElementQuery $elementQuery */
                    $elementQuery = $event->sender;
                    $this->addElementQuery($elementQuery);
                }
            }
        );
    }
 
    /**
6. putyourlightson\blitz\services\GenerateCacheService::putyourlightson\blitz\services\{closure}(craft\events\CancelableEvent)
7. in /var/www/vendor/yiisoft/yii2/base/Event.php at line 312– call_user_func(Closure, craft\events\CancelableEvent)
8. in /var/www/vendor/yiisoft/yii2/base/Component.php at line 642– [yii\base\Event::trigger](https://www.yiiframework.com/doc-2.0/yii-base-event.html#trigger()-detail)('craft\elements\db\ElementQuery', 'beforePrepare', craft\events\CancelableEvent)
9. in /var/www/vendor/craftcms/cms/src/elements/db/ElementQuery.php at line 1897– [yii\base\Component::trigger](https://www.yiiframework.com/doc-2.0/yii-base-component.html#trigger()-detail)('beforePrepare', craft\events\CancelableEvent)
1891189218931894189518961897189818991900190119021903     * @see prepare()
     * @see afterPrepare()
     */
    protected function beforePrepare(): bool
    {
        $event = new CancelableEvent();
        $this->trigger(self::EVENT_BEFORE_PREPARE, $event);
 
        return $event->isValid;
    }
 
    /**
     * This method is called at the end of preparing an element query for the query builder.
10. in /var/www/vendor/craftcms/commerce/src/elements/db/VariantQuery.php at line 889– craft\elements\db\ElementQuery::beforePrepare()
883884885886887888889890891892893894895                $this->subQuery->andWhere(['not', $hasSalesCondition]);
            }
        }
 
        $this->_applyHasProductParam();
 
        return parent::beforePrepare();
    }
 
    /**
     * Normalizes the productId param to an array of IDs or null
     */
    private function _normalizeProductId(): void
11. in /var/www/vendor/craftcms/cms/src/elements/db/ElementQuery.php at line 1294– craft\commerce\elements\db\VariantQuery::beforePrepare()
1288128912901291129212931294129512961297129812991300            ->innerJoin(['elements_sites' => Table::ELEMENTS_SITES], '[[elements_sites.id]] = [[subquery.elementsSitesId]]');
 
        // Keep track of whether an element table is joined into the query
        $this->_joinedElementTable = false;
 
        // Give other classes a chance to make changes up front
        if (!$this->beforePrepare()) {
            throw new QueryAbortedException();
        }
 
        $this->subQuery
            ->addSelect([
                'elementsId' => 'elements.id',
12. in /var/www/vendor/yiisoft/yii2/db/QueryBuilder.php at line 227– craft\elements\db\ElementQuery::prepare(craft\db\mysql\QueryBuilder)
13. in /var/www/vendor/yiisoft/yii2/db/Query.php at line 157– [yii\db\QueryBuilder::build](https://www.yiiframework.com/doc-2.0/yii-db-querybuilder.html#build()-detail)(craft\commerce\elements\db\VariantQuery)
14. in /var/www/vendor/yiisoft/yii2/db/Query.php at line 320– [yii\db\Query::createCommand](https://www.yiiframework.com/doc-2.0/yii-db-query.html#createCommand()-detail)(craft\db\Connection)
15. in /var/www/vendor/craftcms/cms/src/db/Query.php at line 309– [yii\db\Query::column](https://www.yiiframework.com/doc-2.0/yii-db-query.html#column()-detail)(null)
303304305306307308309310311312313314315    /**
     * @inheritdoc
     */
    public function column($db = null): array
    {
        try {
            return parent::column($db);
        } catch (QueryAbortedException) {
            return [];
        }
    }
 
    /**
16. in /var/www/vendor/craftcms/cms/src/elements/db/ElementQuery.php at line 1532– craft\db\Query::column(null)
1526152715281529153015311532153315341535153615371538            $this->from = ['elements' => Table::ELEMENTS];
            $result = parent::column($db);
            $this->from = null;
            return $result;
        }
 
        return parent::column($db);
    }
 
    /**
     * @inheritdoc
     */
    public function exists($db = null): bool
17. in /var/www/vendor/craftcms/cms/src/elements/db/ElementQuery.php at line 1564– craft\elements\db\ElementQuery::column(null)
1558155915601561156215631564156515661567156815691570     * @inheritdoc
     */
    public function ids(?Connection $db = null): array
    {
        $select = $this->select;
        $this->select = ['elements.id' => 'elements.id'];
        $result = $this->column($db);
        $this->select($select);
 
        return $result;
    }
 
    /**
18. in /var/www/vendor/craftcms/commerce/src/elements/db/VariantQuery.php at line 682– craft\elements\db\ElementQuery::ids()
676677678679680681682683684685686687688                $query->$attribute = $this->$attribute;
            }
 
            $query->andWhere(['commerce_products.promotable' => true]);
            unset($query->hasSales);
            $query->limit = null;
            $variantIds = $query->ids();
 
            $productIds = Product::find()
                ->andWhere(['promotable' => true])
                ->limit(null)
                ->ids();
 
19. in /var/www/vendor/craftcms/cms/src/elements/db/ElementQuery.php at line 1294– craft\commerce\elements\db\VariantQuery::beforePrepare()
1288128912901291129212931294129512961297129812991300            ->innerJoin(['elements_sites' => Table::ELEMENTS_SITES], '[[elements_sites.id]] = [[subquery.elementsSitesId]]');
 
        // Keep track of whether an element table is joined into the query
        $this->_joinedElementTable = false;
 
        // Give other classes a chance to make changes up front
        if (!$this->beforePrepare()) {
            throw new QueryAbortedException();
        }
 
        $this->subQuery
            ->addSelect([
                'elementsId' => 'elements.id',
20. in /var/www/vendor/yiisoft/yii2/db/QueryBuilder.php at line 227– craft\elements\db\ElementQuery::prepare(craft\db\mysql\QueryBuilder)
21. in /var/www/vendor/yiisoft/yii2/db/Query.php at line 157– [yii\db\QueryBuilder::build](https://www.yiiframework.com/doc-2.0/yii-db-querybuilder.html#build()-detail)(craft\commerce\elements\db\VariantQuery)
22. in /var/www/vendor/yiisoft/yii2/db/Query.php at line 320– [yii\db\Query::createCommand](https://www.yiiframework.com/doc-2.0/yii-db-query.html#createCommand()-detail)(craft\db\Connection)
23. in /var/www/vendor/craftcms/cms/src/db/Query.php at line 309– [yii\db\Query::column](https://www.yiiframework.com/doc-2.0/yii-db-query.html#column()-detail)(null)
303304305306307308309310311312313314315    /**
     * @inheritdoc
     */
    public function column($db = null): array
    {
        try {
            return parent::column($db);
        } catch (QueryAbortedException) {
            return [];
        }
    }
 
    /**
24. in /var/www/vendor/craftcms/cms/src/elements/db/ElementQuery.php at line 1532– craft\db\Query::column(null)
1526152715281529153015311532153315341535153615371538            $this->from = ['elements' => Table::ELEMENTS];
            $result = parent::column($db);
            $this->from = null;
            return $result;
        }
 
        return parent::column($db);
    }
 
    /**
     * @inheritdoc
     */
    public function exists($db = null): bool
25. in /var/www/vendor/craftcms/commerce/src/elements/db/ProductQuery.php at line 923– craft\elements\db\ElementQuery::column()
917918919920921922923924925926927928929        } else {
            return;
        }
 
        $variantQuery->limit = null;
        $variantQuery->select('commerce_variants.productId');
        $productIds = $variantQuery->asArray()->column();
 
        // Remove any blank product IDs (if any)
        $productIds = array_filter($productIds);
 
        $this->subQuery->andWhere(['commerce_products.id' => array_values($productIds)]);
    }
26. in /var/www/vendor/craftcms/commerce/src/elements/db/ProductQuery.php at line 813– craft\commerce\elements\db\ProductQuery::_applyHasVariantParam()
807808809810811812813814815816817818819        }
 
        if (isset($this->defaultSku)) {
            $this->subQuery->andWhere(Db::parseParam('commerce_products.defaultSku', $this->defaultSku));
        }
 
        $this->_applyHasVariantParam();
        $this->_applyEditableParam();
        $this->_applyRefParam();
 
        return parent::beforePrepare();
    }
 
27. in /var/www/vendor/craftcms/cms/src/elements/db/ElementQuery.php at line 1294– craft\commerce\elements\db\ProductQuery::beforePrepare()
1288128912901291129212931294129512961297129812991300            ->innerJoin(['elements_sites' => Table::ELEMENTS_SITES], '[[elements_sites.id]] = [[subquery.elementsSitesId]]');
 
        // Keep track of whether an element table is joined into the query
        $this->_joinedElementTable = false;
 
        // Give other classes a chance to make changes up front
        if (!$this->beforePrepare()) {
            throw new QueryAbortedException();
        }
 
        $this->subQuery
            ->addSelect([
                'elementsId' => 'elements.id',
28. in /var/www/vendor/yiisoft/yii2/db/QueryBuilder.php at line 227– craft\elements\db\ElementQuery::prepare(craft\db\mysql\QueryBuilder)
29. in /var/www/vendor/yiisoft/yii2/db/Query.php at line 157– [yii\db\QueryBuilder::build](https://www.yiiframework.com/doc-2.0/yii-db-querybuilder.html#build()-detail)(craft\commerce\elements\db\ProductQuery)
30. in /var/www/vendor/yiisoft/yii2/db/Query.php at line 249– [yii\db\Query::createCommand](https://www.yiiframework.com/doc-2.0/yii-db-query.html#createCommand()-detail)(craft\db\Connection)
31. in /var/www/vendor/craftcms/cms/src/db/Query.php at line 248– [yii\db\Query::all](https://www.yiiframework.com/doc-2.0/yii-db-query.html#all()-detail)(null)
242243244245246247248249250251252253254    /**
     * @inheritdoc
     */
    public function all($db = null): array
    {
        try {
            return parent::all($db);
        } catch (QueryAbortedException) {
            return [];
        }
    }
 
    /**
32. in /var/www/vendor/craftcms/cms/src/elements/db/ElementQuery.php at line 1492– craft\db\Query::all(null)
1486148714881489149014911492149314941495149614971498            if ($this->with) {
                Craft::$app->getElements()->eagerLoadElements($this->elementType, $cachedResult, $this->with);
            }
            return $cachedResult;
        }
 
        return parent::all($db);
    }
 
    /**
     * @inheritdoc
     * @return ElementInterface|array|null
     */
33. in /var/www/vendor/craftcms/cms/src/db/Query.php at line 264– craft\elements\db\ElementQuery::all(null)
258259260261262263264265266267268269270     * If this parameter is not given, the `db` application component will be used.
     * @return Collection A collection of the resulting elements.
     * @since 4.0.0
     */
    public function collect(?YiiConnection $db = null): Collection
    {
        return ElementCollection::make($this->all($db));
    }
 
    /**
     * @inheritdoc
     */
    public function one($db = null): mixed
34. in /var/www/vendor/twig/twig/src/Extension/CoreExtension.php at line 1607– craft\db\Query::collect()
35. in /var/www/vendor/craftcms/cms/src/helpers/Template.php at line 110– twig_get_attribute(craft\web\twig\Environment, Twig\Source, craft\commerce\elements\db\ProductQuery, 'collect', ...)
104105106107108109110111112113114115116            if (is_object($value) && get_class($value) === Markup::class) {
                $arguments[$key] = (string)$value;
            }
        }
 
        try {
            return twig_get_attribute($env, $source, $object, $item, $arguments, $type, $isDefinedTest, $ignoreStrictCheck);
        } catch (UnknownMethodException $e) {
            // Copy twig_get_attribute()'s BadMethodCallException handling
            if ($ignoreStrictCheck || !$env->isStrictVariables()) {
                return null;
            }
            throw new RuntimeError($e->getMessage(), -1, $source);
36. in /var/www/templates/_views/listing.twig at line 17– craft\helpers\Template::attribute(craft\web\twig\Environment, Twig\Source, craft\commerce\elements\db\ProductQuery, 'collect', ...)
--------------------
38. in /var/www/vendor/twig/twig/src/Template.php at line 367– [Twig\Template::displayWithErrorHandling](http://twig.sensiolabs.org/api/2.x/Twig/Template.html#method_displayWithErrorHandling)(['category' => craft\elements\Category, 'variables' => ['category' => craft\elements\Category], 'craft' => craft\web\twig\variables\CraftVariable, 'currentSite' => craft\models\Site, ...], ['content' => [__TwigTemplate_def8a3dc189f07dd23d9d3bbc23b620e, 'block_content']])
39. in /var/www/vendor/twig/twig/src/Template.php at line 379– [Twig\Template::display](http://twig.sensiolabs.org/api/2.x/Twig/Template.html#method_display)(['category' => craft\elements\Category, 'variables' => ['category' => craft\elements\Category]])
40. in /var/www/vendor/twig/twig/src/TemplateWrapper.php at line 40– [Twig\Template::render](http://twig.sensiolabs.org/api/2.x/Twig/Template.html#method_render)(['category' => craft\elements\Category, 'variables' => ['category' => craft\elements\Category]], [])
41. in /var/www/vendor/twig/twig/src/Environment.php at line 277– [Twig\TemplateWrapper::render](http://twig.sensiolabs.org/api/2.x/Twig/TemplateWrapper.html#method_render)(['category' => craft\elements\Category, 'variables' => ['category' => craft\elements\Category]])
42. in /var/www/vendor/craftcms/cms/src/web/View.php at line 451– [Twig\Environment::render](http://twig.sensiolabs.org/api/2.x/Twig/Environment.html#method_render)('_views/listing', ['category' => craft\elements\Category, 'variables' => ['category' => craft\elements\Category]])
445446447448449450451452453454455456457 
        // Render and return
        $renderingTemplate = $this->_renderingTemplate;
        $this->_renderingTemplate = $template;
 
        try {
            $output = $this->getTwig()->render($template, $variables);
        } finally {
            $this->_renderingTemplate = $renderingTemplate;
            $this->setTemplateMode($oldTemplateMode);
        }
 
        $this->afterRenderTemplate($template, $variables, $templateMode, $output);
43. in /var/www/vendor/craftcms/cms/src/web/View.php at line 504– craft\web\View::renderTemplate('_views/listing', ['category' => craft\elements\Category, 'variables' => ['category' => craft\elements\Category]])
498499500501502503504505506507508509510 
        $isRenderingPageTemplate = $this->_isRenderingPageTemplate;
        $this->_isRenderingPageTemplate = true;
 
        try {
            $this->beginPage();
            echo $this->renderTemplate($template, $variables);
            $this->endPage();
        } finally {
            $this->_isRenderingPageTemplate = $isRenderingPageTemplate;
            $this->setTemplateMode($oldTemplateMode);
            $output = ob_get_clean();
        }
44. in /var/www/vendor/craftcms/cms/src/web/TemplateResponseFormatter.php at line 56– craft\web\View::renderPageTemplate('_views/listing', ['category' => craft\elements\Category, 'variables' => ['category' => craft\elements\Category]], 'site')
50515253545556575859606162        ) {
            $view->registerAssetBundle(ContentWindowAsset::class);
        }
 
        // Render and return the template
        try {
            $response->content = $view->renderPageTemplate($behavior->template, $behavior->variables, $behavior->templateMode);
        } catch (Throwable $e) {
            if (!$e->getPrevious() instanceof ExitException) {
                // Bail on the template response
                $response->format = Response::FORMAT_HTML;
                throw $e;
            }
45. in /var/www/vendor/yiisoft/yii2/web/Response.php at line 1098– craft\web\TemplateResponseFormatter::format(craft\web\Response)
46. in /var/www/vendor/craftcms/cms/src/web/Response.php at line 286– [yii\web\Response::prepare](https://www.yiiframework.com/doc-2.0/yii-web-response.html#prepare()-detail)()
280281282283284285286287288289290291292 
    /**
     * @inheritdoc
     */
    protected function prepare(): void
    {
        parent::prepare();
        $this->_isPrepared = true;
    }
 
    /**
     * Clear the output buffer to prevent corrupt downloads.
     *
47. in /var/www/vendor/yiisoft/yii2/web/Response.php at line 339– craft\web\Response::prepare()
48. in /var/www/vendor/yiisoft/yii2/base/Application.php at line 390– [yii\web\Response::send](https://www.yiiframework.com/doc-2.0/yii-web-response.html#send()-detail)()
49. in /var/www/web/index.php at line 12– [yii\base\Application::run](https://www.yiiframework.com/doc-2.0/yii-base-application.html#run()-detail)()
6789101112// Load shared bootstrap
require dirname(__DIR__) . '/bootstrap.php';
 
// Load and run Craft
/** @var craft\web\Application $app */
$app = require CRAFT_VENDOR_PATH . '/craftcms/cms/bootstrap/web.php';
$app->run();
$_COOKIE = [
    XXXXXXXXXXXXXXXX
];

$_SESSION = [
    'a50e111d5f13061b3bc1c20c2a9c215d__flash' => [
        'cp-notification-notice' => -1,
        'cp-notice' => -1,
    ],
    'a50e111d5f13061b3bc1c20c2a9c215d__auth_access' => [
        'saveAssets:10',
    ],
    '08b50206495ab19261c35e647aeb027a__token' => 'Is6q26T3sJbZh9K96b637iN7kwuPlWWyYhebaZJIP_WwNqx6B4md3osPayPdAjscN5fb6EdDPTx9tl67coGPbL9az7CAcOoMPpMk',
    '08b50206495ab19261c35e647aeb027a__id' => 1,
    '__authKey' => '["Is6q26T3sJbZh9K96b637iN7kwuPlWWyYhebaZJIP_WwNqx6B4md3osPayPdAjscN5fb6EdDPTx9tl67coGPbL9az7CAcOoMPpMk",null,"b9cbd8dc13f19f9e7eb854f472bfa274"]',
    'cp-notification-notice' => [
        'Plugin settings saved.',
        [
            'icon' => 'info',
            'iconLabel' => 'Notice',
        ],
    ],
    'cp-notice' => 'Plugin settings saved.',
];

@bencroker
Copy link
Collaborator

bencroker commented Feb 7, 2023

That's strange. The VariantQuery class definitely has a hasSales property.
Can you check that this exists in your local copy?
https://github.com/craftcms/commerce/blob/52f542e4e51faa4820036f98945d6e8ade580bf6/src/elements/db/VariantQuery.php#L63-L66

@Nellyaa
Copy link
Author

Nellyaa commented Feb 7, 2023

Yeah, it does exist.
The page works fine with Blitz disabled too. That wouldn't be the case if it wouldn't exist, I guess. :)

@bencroker
Copy link
Collaborator

Ok thanks, I'll do some testing and let you know what I find.

@bencroker
Copy link
Collaborator

So it turns out that Commerce unsets the hasSales property at https://github.com/craftcms/commerce/blob/52f542e4e51faa4820036f98945d6e8ade580bf6/src/elements/db/VariantQuery.php#L680

I've added a workaround for this in 10f06d0, for the next release.

@bencroker
Copy link
Collaborator

Just released in version 4.3.0.

@Nellyaa
Copy link
Author

Nellyaa commented Feb 7, 2023

Perfect, thank you for the blitz-like fix! Works like a charm now. :)

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

No branches or pull requests

2 participants