putyourlightson / craft-blitz

Intelligent static page caching for creating lightning-fast sites with Craft CMS.
https://putyourlightson.com/plugins/blitz
Other
149 stars 36 forks source link

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

Closed Nellyaa closed 1 year ago

Nellyaa commented 1 year ago

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 commented 1 year ago

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

Nellyaa commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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

bencroker commented 1 year ago

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 https://github.com/putyourlightson/craft-blitz/commit/10f06d0827e01d2389135af6447a3b34fcdfd851, for the next release.

bencroker commented 1 year ago

Just released in version 4.3.0.

Nellyaa commented 1 year ago

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