diff --git a/.github/workflows/phpbench.yml b/.github/workflows/phpbench.yml index 78344a7..d51f5dc 100644 --- a/.github/workflows/phpbench.yml +++ b/.github/workflows/phpbench.yml @@ -14,11 +14,12 @@ permissions: env: REQUIRED_PHP_EXTENSIONS: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo - # TemplateCacheBench::benchVarExporter segfaults on CI for PHP 8.4/8.5. Exclude it on + # TemplateCacheBench's var-export subjects segfault on CI for PHP 8.4/8.5. Exclude them on # every version, not just those two: a benchmark set that varies per PHP version makes # the per-version comparison summaries incomparable with each other. Local runs are - # unfiltered, so the benchmark still exists — it just does not gate PRs. - PHPBENCH_FILTER: '^(?!.*TemplateCacheBench::benchVarExporter$).*' + # unfiltered, so the benchmarks still exist — they just do not gate PRs. + PHPBENCH_FILTER: '^(?!.*TemplateCacheBench::bench(Build|LoadAndRender)VarExporter$).*' + PHPBENCH_MAX_REG: '5' jobs: benchmark: @@ -78,6 +79,7 @@ jobs: set -euo pipefail vendor/bin/phpbench run \ + --group=default \ --filter="$PHPBENCH_FILTER" \ --progress=none \ --warmup=1 \ @@ -91,6 +93,7 @@ jobs: set -euo pipefail vendor/bin/phpbench run \ + --group=default \ --filter="$PHPBENCH_FILTER" \ --progress=none \ --warmup=1 \ @@ -171,5 +174,5 @@ jobs: } - name: Enforce comparison failures - if: always() && steps.compare.outputs.exit_code != '0' && steps.compare.outputs.exit_code != '1' + if: always() && steps.compare.outputs.exit_code != '0' run: exit 1 diff --git a/composer.json b/composer.json index 90ca7c8..d139568 100644 --- a/composer.json +++ b/composer.json @@ -30,8 +30,7 @@ "spatie/invade": "^2.0", "spatie/ray": "^1.28", "symfony/console": "^7.0 || ^8.0", - "symfony/var-exporter": "^7.0 || ^8.0", - "symfony/yaml": "^7.0 || ^8.0" + "symfony/var-exporter": "^7.0 || ^8.0" }, "autoload": { "psr-4": { @@ -51,7 +50,9 @@ "pint", "phpstan analyse" ], - "benchmark": "phpbench run --report=aggregate", + "benchmark": "phpbench run --group=default --warmup=1 --retry-threshold=5 --report=aggregate", + "benchmark:cache": "phpbench run --group=cache --warmup=1 --retry-threshold=5 --report=aggregate", + "benchmark:operations": "phpbench run --group=operations --warmup=1 --retry-threshold=5 --report=aggregate", "profile": "phpbench xdebug:profile" }, "config": { diff --git a/performance/CompiledThemeTestTemplate.php b/performance/CompiledThemeTestTemplate.php deleted file mode 100644 index 65c6d6a..0000000 --- a/performance/CompiledThemeTestTemplate.php +++ /dev/null @@ -1,57 +0,0 @@ -templateName); - } - - public function render(array $assigns = []): void - { - $content = $this->template->render($this->buildContext($assigns)); - - if ($this->layout) { - $this->layout->render($this->buildContext([ - ...$assigns, - 'content_for_layout' => $content, - ])); - } - } - - public function stream(array $assigns = []): void - { - $content = $this->template->stream($this->buildContext($assigns)); - - if ($this->layout) { - $content = $this->layout->stream($this->buildContext([ - ...$assigns, - 'content_for_layout' => $content, - ])); - } - - while ($content->valid()) { - $content->next(); - } - } - - protected function buildContext(array $assigns = []): RenderContext - { - return $this->environment->newRenderContext( - staticData: $assigns, - ); - } -} diff --git a/performance/ProfileReport.php b/performance/ProfileReport.php new file mode 100644 index 0000000..ef3bbdc --- /dev/null +++ b/performance/ProfileReport.php @@ -0,0 +1,55 @@ +> + * } + */ + public static function fromProfiler(Profiler $profiler): array + { + return [ + 'schema_version' => 1, + 'generated_at' => date(DATE_ATOM), + 'php_version' => PHP_VERSION, + 'profiles' => array_map(self::profile(...), $profiler->getProfiles()), + ]; + } + + /** + * @return array{ + * type: string, + * name: string, + * duration: float, + * self_duration: float, + * memory_usage: int, + * peak_memory_usage: int, + * children: array> + * } + */ + private static function profile(Profile $profile): array + { + return [ + 'type' => $profile->type->value, + 'name' => $profile->name, + 'duration' => $profile->getDuration(), + 'self_duration' => $profile->getSelfDuration(), + 'memory_usage' => $profile->getMemoryUsage(), + 'peak_memory_usage' => $profile->getPeakMemoryUsage(), + 'children' => array_map(self::profile(...), $profile->getChildren()), + ]; + } +} diff --git a/performance/README.md b/performance/README.md new file mode 100644 index 0000000..93a0c27 --- /dev/null +++ b/performance/README.md @@ -0,0 +1,109 @@ +# Benchmark suite + +```bash +composer benchmark # default group: the storefront theme +composer benchmark:cache # cache group: template-cache backends +composer benchmark:operations # operations group: individual operations +php performance/profile-theme.php --output=profile.json +``` + +## What each group is for + +The three groups have different jobs, and conflating them is how a benchmark suite +stops being useful. + +**`default`** (`ThemeBench`) renders the storefront theme — 29 templates across +four pages. It answers *"did rendering get slower"* and nothing more. It cannot +tell you *what* got slower, because a regression in any one tag is averaged +across everything else. Don't expect it to localize. + +**`cache`** (`TemplateCacheBench`) measures compilation and fresh-environment +loading for every supported template-cache backend. + +**`operations`** (`OperationBench`) measures single operations on tiny templates. +This is where per-feature sensitivity lives, and where a benchmark is allowed to +be unrealistic: an artificial template that does one thing 64 times is a better +instrument than a realistic page. + +The split is what lets the theme be realistic. Whenever realism and measurement +sensitivity conflict inside the theme, realism wins — sensitivity is not the +theme's job. + +Pull-request comparisons enforce a worst-subject throughput regression threshold +of 5%. The comparator still labels changes inside its 2% noise band as neutral, +and marks RSD above 5% as high variance for review. + +## The storefront fixture + +`performance/themes/storefront/` is a deliberately plausible storefront: real +`` metadata, a main nav, breadcrumbs, a product grid, a filter sidebar, a +variant picker, a specs table, a multi-column footer. The nav is deliberately +flat — multi-level menus were ruled out as breadth the fixture does not need, and +depth comes from the product graph instead. It uses only tags a real theme +would use (`render`, `for`/`else`, `if`, `unless`, `case`, `capture`, `cycle`, +`assign`, `break`, `continue`, `{% liquid %}`). + +Constraints that are not obvious from reading the code: + +- **`Database` assigns, it does not compute.** No scans, no reductions, no + sorting. Nothing is memoized either, so the page-local object graph is rebuilt + on every render *inside the measured region* — any computation added here moves + the theme numbers for reasons that have nothing to do with the library. + Derived values go into the literal data or onto a drop method, where they are + measured as template work. `StorefrontTheme::renderData()` builds page and + layout data once, sharing only the `shop` drop where both contexts need it. +- **Fixed dataset: 24 products.** A deliberate page size, not an accident. +- **Fresh drops per render.** `#[Cache]` therefore starts cold on every render, + and no state is shared between revolutions. Memoized instances would measure a + warm cache 95% of the time and leak `ContextAware` state across revs. +- **Two render contexts per page.** The page renders into one, the layout into + another, both from a single fixture setup. Consequence: nothing under `layout/` + may read a variable a template assigned — it would render empty. +- **No missing lookups.** Every field the theme reads exists, so empty output in + a benchmark is a bug rather than an expected state. +- **Template sources are read in `setUp`,** never inside a subject. Reading 29 + files per revolution measured the filesystem, not the tokenizer. + +### Drop resolution strategies + +`Drop::__get` resolves names through four branches of very different cost, so the +fixture assigns them on purpose rather than by accident — while keeping each one +somewhere a real storefront drop would genuinely use it: + +| Strategy | Cost | Where the fixture uses it | +| --- | --- | --- | +| Public typed property | Cheapest — first lookup loop | Stored fields: `title`, `handle`, `price_cents` | +| Invokable method | Misses the property loop first | Derived values: `on_sale`, `saving_cents`, `url` | +| `#[Cache]`d method | Method cost, once per instance | `in_stock_variant_count` — walks every variant; read twice per instance on the product page, once on a card | +| `liquidMethodMissing` | Most expensive; a **miss** throws and catches up to three exceptions | `MetafieldsDrop` only, where keys are genuinely arbitrary | + +## Verifying the fixture + +Benchmarks run with the library defaults, because that is what an application +looks like. `tests/Integration/Performance/StorefrontThemeTest.php` instead +renders every page with `strictVariables`, `strictFilters` and `rethrowErrors` +all on, so a missing variable, a missing filter or a swallowed render error fails +the suite instead of quietly rendering as empty output. + +There are deliberately **no snapshot assertions**. The theme is expected to keep +growing, and a snapshot over a fast-changing fixture gets regenerated on autopilot +until it asserts nothing. Strict mode cannot be silenced that way. + +## Deferred + +Known gaps, in rough priority order: + +- **Additional per-tag `operations` subjects.** The suite isolates property, + method, `liquidMethodMissing` hit/miss, filters and list-size scaling, but it + does not yet isolate `case`, `capture`, `cycle` or `render` depth. +- **Full-theme size scaling.** The operations group exposes 4 / 24 / 96-product + loop scaling, while the storefront theme deliberately keeps a fixed 24-product + dataset. A second full-theme size profile would be useful only if a suspected + regression needs that wider lens. +- **Coverage-only tags.** `tablerow`, `increment`, `decrement`, `ifchanged`, + `raw` and `doc` are unbenchmarked. Real themes barely use them, so they belong + in `operations` rather than in the theme. +- **`TemplateCacheBench` shape.** Six subjects are driven by six near-identical + `setUp*` wrappers around a string `match`; `ParamProviders` could reduce that + repetition. Each benchmark setup now receives a unique temporary cache path, + so concurrent runs do not share cache files. diff --git a/performance/Shopify/CommentFormTag.php b/performance/Shopify/CommentFormTag.php deleted file mode 100644 index acd0048..0000000 --- a/performance/Shopify/CommentFormTag.php +++ /dev/null @@ -1,69 +0,0 @@ -body !== null); - $this->body = $context->body; - - $variableName = $context->params->expression(); - $this->variableName = match (true) { - $variableName instanceof VariableLookup, is_string($variableName) => (string) $variableName, - default => throw new SyntaxException('Invalid variable name'), - }; - - $this->attributes = []; - - $context->params->assertEnd(); - - return $this; - } - - public function render(RenderContext $context): string - { - $article = $context->get($this->variableName); - assert(is_array($article)); - - $context->stack(function (RenderContext $context) { - $context->set('form', [ - 'posted_successfully?' => $context->getRegister('posted_successfully'), - 'errors' => $context->get('comment.errors'), - 'author' => $context->get('comment.author'), - 'email' => $context->get('comment.email'), - 'body' => $context->get('comment.body'), - ]); - }); - - return $this->wrapInForm($article, $this->body->render($context)); - } - - protected function wrapInForm(array $article, string $input): string - { - return << - $input - - HTML; - } -} diff --git a/performance/Shopify/CustomFilters.php b/performance/Shopify/CustomFilters.php deleted file mode 100644 index f35c944..0000000 --- a/performance/Shopify/CustomFilters.php +++ /dev/null @@ -1,228 +0,0 @@ -toArray(); - } - - if (is_array($value) && array_key_exists('collections', $value)) { - $value = [...$value]; - unset($value['collections']); - } - - return json_encode($value, flags: JSON_THROW_ON_ERROR); - } - - public function moneyWithCurrency(int|float|null $money): string - { - if ($money === null) { - return ''; - } - - return sprintf('$ %.2f USD', $money / 100); - } - - public function money(int|float|null $money): string - { - if ($money === null) { - return ''; - } - - return sprintf('$ %.2f', $money / 100); - } - - public function currency(): string - { - return 'USD'; - } - - public function weight(int|float $grams): string - { - return sprintf('%.2f', $grams / 1000); - } - - public function weightWithUnit(int|float $grams): string - { - return $this->weight($grams).' kg'; - } - - public function linkToTag(string|int|float $label, string $tag): string - { - return sprintf( - '%s', - $tag, - $this->context->get('handle'), - $tag, - $label - ); - } - - public function highlightActiveTag(string|int|float $tag, string $cssClass = 'active'): string - { - $currentTags = $this->context->get('current_tags') ?? []; - assert(is_array($currentTags)); - - if (in_array($tag, $currentTags)) { - return sprintf('%s', $cssClass, $tag); - } - - return (string) $tag; - } - - public function linkToAddTag(string|int|float $label, string $tag): string - { - $currentTags = $this->context->get('current_tags'); - $currentTags = is_array($currentTags) ? $currentTags : []; - $currentTags = array_values(array_filter($currentTags, is_string(...))); - $tags = array_unique([...$currentTags, $tag]); - - return sprintf( - '%s', - $tag, - $this->context->get('handle'), - implode('+', $tags), - $label - ); - } - - public function linkToRemoveTag(string|int|float $label, string $tag): string - { - $currentTags = $this->context->get('current_tags'); - $currentTags = is_array($currentTags) ? $currentTags : []; - $currentTags = array_values(array_filter($currentTags, is_string(...))); - $tags = array_values(array_filter($currentTags, fn (string $currentTag) => $currentTag !== $tag)); - - return sprintf( - '%s', - $tag, - $this->context->get('handle'), - implode('+', $tags), - $label - ); - } - - public function assetUrl(string $input): string - { - return "/files/1/[shop_id]/[shop_id]/assets/$input"; - } - - public function globalAssetUrl(string $input): string - { - return "/global/$input"; - } - - public function shopifyAssetUrl(string $input): string - { - return "/shopify/$input"; - } - - public function scriptTag(string $url): string - { - return sprintf('', $url); - } - - public function stylesheetTag(string $url, string $media = 'all'): string - { - return sprintf('', $url, $media); - } - - public function linkTo(string $link, string $url, string $title = ''): string - { - return sprintf('%s', $url, $title, $link); - } - - public function imgTag(string $url, string $alt = ''): string - { - return sprintf('%s', $url, $alt); - } - - public function linkToVendor(?string $vendor): string - { - if ($vendor !== null) { - return $this->linkTo($this->urlForVendor($vendor), $vendor); - } - - return 'Unknown vendor'; - } - - public function linkToType(?string $type): string - { - if ($type !== null) { - return $this->linkTo($this->urlForType($type), $type); - } - - return 'Unknown vendor'; - } - - public function urlForVendor(string $vendor): string - { - return '/collections/'.$this->toHandle($vendor); - } - - public function urlForType(string $type): string - { - return '/collections/'.$this->toHandle($type); - } - - public function productImgUrl(string $url, string $style = 'small'): string - { - if (preg_match('/\Aproducts\/([\w\-_]+)\.(\w{2,4})/', $url, $matches) === 0) { - throw new InvalidArgumentException('filter "size" can only be called on product images'); - } - - $filename = $matches[1] ?? ''; - $extension = $matches[2] ?? ''; - - return match ($style) { - 'original' => '/files/shops/random_number/'.$url, - 'grande', 'large', 'medium', 'compact', 'small', 'thumb', 'icon' => sprintf('/files/shops/random_number/products/%s_%s.%s', $filename, $style, $extension), - default => throw new InvalidArgumentException('valid parameters for filter "size" are: original, grande, large, medium, compact, small, thumb and icon '), - }; - } - - public function defaultPagination(array $paginate): string - { - $html = []; - - if (isset($paginate['previous'])) { - $html[] = ''.$this->linkTo($paginate['previous']['title'], $paginate['previous']['url']).''; - } - - foreach ($paginate['parts'] as $part) { - $html[] = match (true) { - $part['is_link'] => ''.$this->linkTo($part['title'], $part['url']).'', - (int) $part['title'] == (int) $paginate['current_page'] => ''.$part['title'].'', - default => ''.$part['title'].'', - }; - } - - if (isset($paginate['next'])) { - $html[] = ''.$this->linkTo($paginate['next']['title'], $paginate['next']['url']).''; - } - - return implode(' ', $html); - } - - public function pluralize(int|float $input, string $singular, string $plural): string - { - return $input === 1 ? $singular : $plural; - } - - protected function toHandle(string $input): string - { - $result = $input; - $result = strtolower($result); - $result = str_replace(['\'', '"', '()', '[]'], '', $result); - $result = preg_replace('/\W+/', '-', $result) ?? $result; - - return trim($result, '-'); - } -} diff --git a/performance/Shopify/Database.php b/performance/Shopify/Database.php deleted file mode 100644 index 635fa1b..0000000 --- a/performance/Shopify/Database.php +++ /dev/null @@ -1,125 +0,0 @@ - Arr::first( - is_array($collection['products'] ?? null) ? $collection['products'] : [], - fn (array $p) => $p['id'] === $product['id'] - ) !== null - ); - $product['collections'] = array_values($collections); - - return $product; - }, - $products - ); - - $tables = []; - foreach ($database as $key => $values) { - if (! is_array($values)) { - continue; - } - - $tables[$key] = array_reduce($values, function (array $acc, mixed $item) { - if (! is_array($item)) { - return $acc; - } - - if (isset($item['handle']) && is_string($item['handle'])) { - $acc[$item['handle']] = $item; - } else { - $acc[] = $item; - } - - return $acc; - }, []); - } - - $tables['collection'] = Arr::first($collections); - $tables['product'] = Arr::first($database['products']); - $tables['blog'] = Arr::first($blogs); - $articles = $tables['blog']['articles'] ?? []; - assert(is_array($articles)); - $tables['article'] = Arr::first($articles); - - $tables['cart'] = [ - 'total_price' => array_reduce($lineItems, function (int $total, mixed $item): int { - if (! is_array($item)) { - return $total; - } - - $linePrice = $item['line_price'] ?? 0; - $quantity = $item['quantity'] ?? 0; - if (! is_int($linePrice) && ! is_float($linePrice)) { - $linePrice = 0; - } - if (! is_int($quantity) && ! is_float($quantity)) { - $quantity = 0; - } - - return (int) ($total + $linePrice * $quantity); - }, 0), - 'item_count' => array_reduce($lineItems, function (int $total, mixed $item): int { - if (! is_array($item)) { - return $total; - } - - $quantity = $item['quantity'] ?? 0; - if (! is_int($quantity) && ! is_float($quantity)) { - $quantity = 0; - } - - return (int) ($total + $quantity); - }, 0), - 'items' => $lineItems, - ]; - - return static::$tables = array_map( - static::toLiquidValue(...), - $tables, - ); - } - - protected static function toLiquidValue(mixed $value): mixed - { - if (! is_array($value)) { - return $value; - } - - $values = array_map( - static::toLiquidValue(...), - $value, - ); - - if (array_is_list($value)) { - return $values; - } - - return new DatabaseDrop($values); - } -} diff --git a/performance/Shopify/DatabaseDrop.php b/performance/Shopify/DatabaseDrop.php deleted file mode 100644 index 7654fc2..0000000 --- a/performance/Shopify/DatabaseDrop.php +++ /dev/null @@ -1,41 +0,0 @@ - $data - */ - public function __construct( - private readonly array $data, - ) {} - - public function liquidMethodMissing(string $name): mixed - { - if (array_key_exists($name, $this->data)) { - return $this->data[$name]; - } - - throw new UndefinedDropMethodException($name); - } - - /** - * @return array - */ - public function toArray(): array - { - return $this->data; - } - - /** - * @return array - */ - public function jsonSerialize(): array - { - return $this->data; - } -} diff --git a/performance/Shopify/PaginateTag.php b/performance/Shopify/PaginateTag.php deleted file mode 100644 index 7b00d65..0000000 --- a/performance/Shopify/PaginateTag.php +++ /dev/null @@ -1,124 +0,0 @@ -body !== null); - $this->body = $context->body; - - $collectionName = $context->params->expression(); - $this->collectionName = match (true) { - $collectionName instanceof VariableLookup, is_string($collectionName) => (string) $collectionName, - default => throw new SyntaxException('Invalid collection name'), - }; - - if ($context->params->idOrFalse('by')) { - $this->pageSize = (int) $context->params->consume(TokenType::Number)->data; - } - - if (! $context->params->isEnd()) { - dd('paginate', $context->params->current()); - } - - $context->params->assertEnd(); - - return $this; - } - - public function render(RenderContext $context): string - { - return $context->stack(function (RenderContext $context) { - $currentPage = $context->get('current_page'); - $currentPage = is_int($currentPage) ? $currentPage : 1; - - $collection = $context->get($this->collectionName); - $collection = match (true) { - $collection instanceof Range => $collection->toArray(), - $collection instanceof \Iterator => iterator_to_array($collection), - default => $collection, - }; - if (! is_array($collection)) { - throw new InvalidArgumentException(sprintf('Cannot paginate array %s. Not found.', $this->collectionName)); - } - - $collectionSize = count($collection); - $pageCount = ceil($collectionSize / $this->pageSize) + 1; - - $pagination = [ - 'page_size' => $this->pageSize, - 'current_page' => 5, - 'current_offset' => $this->pageSize * 5, - 'items' => $collectionSize, - 'pages' => $pageCount - 1, - 'previous' => $currentPage > 1 ? $this->link('« Previous', $currentPage - 1) : null, - 'next' => $currentPage < $pageCount - 1 ? $this->link('Next »', $currentPage + 1) : null, - 'parts' => [], - ]; - - if ($pageCount > 2) { - foreach (range(1, (int) $pageCount - 1) as $page) { - $pagination['parts'][] = match (true) { - $currentPage === $page => $this->noLink((string) $page), - $page === 1 => $this->link((string) $page, $page), - $page === $pageCount - 1 => $this->link((string) $page, $page), - $page <= ($currentPage - (int) $this->attributes['window_size']) => $this->link('...', $page), - $page >= ($currentPage + (int) $this->attributes['window_size']) => $this->link('...', $page), - default => $this->link((string) $page, $page), - }; - } - } - - $context->set('paginate', $pagination); - - return $this->body->render($context); - }); - } - - protected function noLink(string $title): array - { - return [ - 'title' => $title, - 'is_link' => false, - ]; - } - - protected function link(string $title, int $page): array - { - return [ - 'title' => $title, - 'url' => $this->currentUrl().'?page='.$page, - 'is_link' => true, - ]; - } - - protected function currentUrl(): string - { - return '/collections/frontpage'; - } -} diff --git a/performance/Shopify/vision.database.yml b/performance/Shopify/vision.database.yml deleted file mode 100644 index 9f38c30..0000000 --- a/performance/Shopify/vision.database.yml +++ /dev/null @@ -1,945 +0,0 @@ -# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- -# Variants -# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- - -product_variants: - - &product-1-var-1 - id: 1 - title: 151cm / Normal - price: 19900 - weight: 1000 - compare_at_price: 49900 - available: true - inventory_quantity: 5 - option1: 151cm - option2: Normal - option3: - - &product-1-var-2 - id: 2 - title: 155cm / Normal - price: 31900 - weight: 1000 - compare_at_price: 50900 - available: true - inventory_quantity: 2 - option1: 155cm - option2: Normal - option3: - - &product-2-var-1 - id: 3 - title: 162cm - price: 29900 - weight: 1000 - compare_at_price: 52900 - available: true - inventory_quantity: 3 - option1: 162cm - option2: - option3: - - &product-3-var-1 - id: 4 - title: 159cm - price: 19900 - weight: 1000 - compare_at_price: - available: true - inventory_quantity: 4 - option1: 159cm - option2: - option3: - - &product-4-var-1 - id: 5 - title: 159cm - price: 19900 - weight: 1000 - compare_at_price: 32900 - available: true - inventory_quantity: 6 - option1: 159cm - option2: - option3: - - &product-1-var-3 - id: 6 - title: 158cm / Wide - price: 23900 - weight: 1000 - compare_at_price: 99900 - available: false - inventory_quantity: 0 - option1: 158cm - option2: Wide - option3: - - &product-3-var-2 - id: 7 - title: 162cm - price: 19900 - weight: 1000 - compare_at_price: - available: false - inventory_quantity: 0 - option1: 162cm - option2: - option3: - - &product-3-var-3 - id: 8 - title: 165cm - price: 22900 - weight: 1000 - compare_at_price: - available: true - inventory_quantity: 4 - option1: 165cm - option2: - option3: - - &product-5-var-1 - id: 9 - title: black / 42 - price: 11900 - weight: 500 - compare_at_price: 22900 - available: true - inventory_quantity: 1 - option1: black - option2: 42 - option3: - - &product-5-var-2 - id: 10 - title: beige / 42 - price: 11900 - weight: 500 - compare_at_price: 22900 - available: true - inventory_quantity: 3 - option1: beige - option2: 42 - option3: - - &product-5-var-3 - id: 11 - title: white / 42 - price: 13900 - weight: 500 - compare_at_price: 24900 - available: true - inventory_quantity: 1 - option1: white - option2: 42 - option3: - - &product-5-var-4 - id: 12 - title: black / 44 - price: 11900 - weight: 500 - compare_at_price: 22900 - available: true - inventory_quantity: 2 - option1: black - option2: 44 - option3: - - &product-5-var-5 - id: 13 - title: beige / 44 - price: 11900 - weight: 500 - compare_at_price: 22900 - available: false - inventory_quantity: 0 - option1: beige - option2: 44 - option3: - - &product-5-var-6 - id: 14 - title: white / 44 - price: 13900 - weight: 500 - compare_at_price: 24900 - available: false - inventory_quantity: 0 - option1: white - option2: 44 - option3: - - &product-6-var-1 - id: 15 - title: red - price: 2179500 - weight: 200000 - compare_at_price: - available: true - inventory_quantity: 0 - option1: red - option2: - option3: - - &product-7-var-1 - id: 16 - title: black / small - price: 1900 - weight: 200 - compare_at_price: - available: true - inventory_quantity: 20 - option1: black - option2: small - option3: - - &product-7-var-2 - id: 17 - title: black / medium - price: 1900 - weight: 200 - compare_at_price: - available: false - inventory_quantity: 0 - option1: black - option2: medium - option3: - - &product-7-var-3 - id: 18 - title: black / large - price: 1900 - weight: 200 - compare_at_price: - available: true - inventory_quantity: 10 - option1: black - option2: large - option3: - - &product-7-var-4 - id: 19 - title: black / extra large - price: 1900 - weight: 200 - compare_at_price: - available: false - inventory_quantity: 0 - option1: black - option2: extra large - option3: - - &product-8-var-1 - id: 20 - title: brown / small - price: 5900 - weight: 400 - compare_at_price: 6900 - available: true - inventory_quantity: 5 - option1: brown - option2: small - option3: - - &product-8-var-2 - id: 21 - title: brown / medium - price: 5900 - weight: 400 - compare_at_price: 6900 - available: false - inventory_quantity: 0 - option1: brown - option2: medium - option3: - - &product-8-var-3 - id: 22 - title: brown / large - price: 5900 - weight: 400 - compare_at_price: 6900 - available: true - inventory_quantity: 10 - option1: brown - option2: large - option3: - - &product-8-var-4 - id: 23 - title: black / small - price: 5900 - weight: 400 - compare_at_price: 6900 - available: true - inventory_quantity: 10 - option1: black - option2: small - option3: - - &product-8-var-5 - id: 24 - title: black / medium - price: 5900 - weight: 400 - compare_at_price: 6900 - available: true - inventory_quantity: 10 - option1: black - option2: medium - option3: - - &product-8-var-6 - id: 25 - title: black / large - price: 5900 - weight: 400 - compare_at_price: 6900 - available: false - inventory_quantity: 0 - option1: black - option2: large - option3: - - &product-9-var-1 - id: 26 - title: Body Only - price: 499995 - weight: 2000 - compare_at_price: - available: true - inventory_quantity: 3 - option1: Body Only - option2: - option3: - - &product-9-var-2 - id: 27 - title: Kit with 18-55mm VR lens - price: 523995 - weight: 2000 - compare_at_price: - available: true - inventory_quantity: 2 - option1: Kit with 18-55mm VR lens - option2: - option3: - - &product-9-var-3 - id: 28 - title: Kit with 18-200 VR lens - price: 552500 - weight: 2000 - compare_at_price: - available: true - inventory_quantity: 3 - option1: Kit with 18-200 VR lens - option2: - option3: - -# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- -# Products -# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- - -products: - - &product-1 - id: 1 - title: Arbor Draft - handle: arbor-draft - type: Snowboards - vendor: Arbor - price: 23900 - price_max: 31900 - price_min: 23900 - price_varies: true - available: true - tags: - - season2005 - - pro - - intermediate - - wooden - - freestyle - options: - - Length - - Style - compare_at_price: 49900 - compare_at_price_max: 50900 - compare_at_price_min: 49900 - compare_at_price_varies: true - url: /products/arbor-draft - featured_image: products/arbor_draft.jpg - images: - - products/arbor_draft.jpg - description: - The Arbor Draft snowboard wouldn't exist if Polynesians hadn't figured out how to surf hundreds of years ago. But the Draft does exist, and it's here to bring your urban and park riding to a new level. The board's freaky Tiki design pays homage to culture that inspired snowboarding. It's designed to spin with ease, land smoothly, lock hook-free onto rails, and take the abuse of a pavement pounding or twelve. The Draft will pop off kickers with authority and carve solidly across the pipe. The Draft features targeted Koa wood die cuts inlayed into the deck that enhance the flex pattern. Now bow down to riding's ancestors. - variants: - - *product-1-var-1 - - *product-1-var-2 - - *product-1-var-3 - - &product-2 - id: 2 - title: Arbor Element - handle: arbor-element - type: Snowboards - vendor: Arbor - price: 29900 - price_max: 29900 - price_min: 29900 - price_varies: false - available: true - tags: - - season2005 - - pro - - wooden - - freestyle - options: - - Length - compare_at_price: 52900 - compare_at_price_max: 52900 - compare_at_price_min: 52900 - compare_at_price_varies: false - url: /products/arbor-element - featured_image: products/element58.jpg - images: - - products/element58.jpg - description: - The Element is a technically advanced all-mountain board for riders who readily transition from one terrain, snow condition, or riding style to another. Its balanced design provides the versatility needed for the true ride-it-all experience. The Element is exceedingly lively, freely initiates, and holds a tight edge at speed. Its structural real-wood topsheet is made with book-matched Koa. - variants: - - *product-2-var-1 - - - &product-3 - id: 3 - title: Comic ~ Pastel - handle: comic-pastel - type: Snowboards - vendor: Technine - price: 19900 - price_max: 22900 - price_min: 19900 - tags: - - season2006 - - beginner - - intermediate - - freestyle - - purple - options: - - Length - price_varies: true - available: true - compare_at_price: - compare_at_price_max: 0 - compare_at_price_min: 0 - compare_at_price_varies: false - url: /products/comic-pastel - featured_image: products/technine1.jpg - images: - - products/technine1.jpg - - products/technine2.jpg - - products/technine_detail.jpg - description: - 2005 Technine Comic Series Description The Comic series was developed to be the ultimate progressive freestyle board in the Technine line. Dependable edge control and a perfect flex pattern for jumping in the park or out of bounds. Landins and progression will come easy with this board and it will help your riding progress to the next level. Street rails, park jibs, backcountry booters and park jumps, this board will do it all. - variants: - - *product-3-var-1 - - *product-3-var-2 - - *product-3-var-3 - - - &product-4 - id: 4 - title: Comic ~ Orange - handle: comic-orange - type: Snowboards - vendor: Technine - price: 19900 - price_max: 19900 - price_min: 19900 - price_varies: false - available: true - tags: - - season2006 - - beginner - - intermediate - - freestyle - - orange - options: - - Length - compare_at_price: 32900 - compare_at_price_max: 32900 - compare_at_price_min: 32900 - compare_at_price_varies: false - url: /products/comic-orange - featured_image: products/technine3.jpg - images: - - products/technine3.jpg - - products/technine4.jpg - description: - 2005 Technine Comic Series Description The Comic series was developed to be the ultimate progressive freestyle board in the Technine line. Dependable edge control and a perfect flex pattern for jumping in the park or out of bounds. Landins and progression will come easy with this board and it will help your riding progress to the next level. Street rails, park jibs, backcountry booters and park jumps, this board will do it all. - variants: - - *product-4-var-1 - - - &product-5 - id: 5 - title: Burton Boots - handle: burton-boots - type: Boots - vendor: Burton - price: 11900 - price_max: 11900 - price_min: 11900 - price_varies: false - available: true - tags: - - season2006 - - beginner - - intermediate - - boots - options: - - Color - - Shoe Size - compare_at_price: 22900 - compare_at_price_max: 22900 - compare_at_price_min: 22900 - compare_at_price_varies: false - url: /products/burton-boots - featured_image: products/burton.jpg - images: - - products/burton.jpg - description: - The Burton boots are particularly well on snowboards. The very best thing about them is that the according picture is cubic. This makes testing in a Vision testing environment very easy. - variants: - - *product-5-var-1 - - *product-5-var-2 - - *product-5-var-3 - - *product-5-var-4 - - *product-5-var-5 - - *product-5-var-6 - - - &product-6 - id: 6 - title: Superbike 1198 S - handle: superbike - type: Superbike - vendor: Ducati - price: 2179500 - price_max: 2179500 - price_min: 2179500 - price_varies: false - available: true - tags: - - ducati - - superbike - - bike - - street - - racing - - performance - options: - - Color - compare_at_price: - compare_at_price_max: 0 - compare_at_price_min: 0 - compare_at_price_varies: false - url: /products/superbike - featured_image: products/ducati.jpg - images: - - products/ducati.jpg - description: -

‘S’ PERFORMANCE

-

Producing 170hp (125kW) and with a dry weight of just 169kg (372.6lb), the new 1198 S now incorporates more World Superbike technology than ever before by taking the 1198 motor and adding top-of-the-range suspension, lightweight chassis components and a true racing-style traction control system designed for road use.

-

The high performance, fully adjustable 43mm Öhlins forks, which sport low friction titanium nitride-treated fork sliders, respond effortlessly to every imperfection in the tarmac. Beyond their advanced engineering solutions, one of the most important characteristics of Öhlins forks is their ability to communicate the condition and quality of the tyre-to-road contact patch, a feature that puts every rider in superior control. The suspension set-up at the rear is complemented with a fully adjustable Öhlins rear shock equipped with a ride enhancing top-out spring and mounted to a single-sided swingarm for outstanding drive and traction. The front-to-rear Öhlins package is completed with a control-enhancing adjustable steering damper.

- variants: - - *product-6-var-1 - - - &product-7 - id: 7 - title: Shopify Shirt - handle: shopify-shirt - type: Shirt - vendor: Shopify - price: 1900 - price_max: 1900 - price_min: 1900 - price_varies: false - available: true - tags: - - shopify - - shirt - - apparel - - tshirt - - clothing - options: - - Color - - Size - compare_at_price: - compare_at_price_max: 0 - compare_at_price_min: 0 - compare_at_price_varies: false - url: /products/shopify-shirt - featured_image: products/shopify_shirt.png - images: - - products/shopify_shirt.png - description: -

High Quality Shopify Shirt. Wear your e-commerce solution with pride and attract attention anywhere you go.

-

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

- variants: - - *product-7-var-1 - - *product-7-var-2 - - *product-7-var-3 - - *product-7-var-4 - - - &product-8 - id: 8 - title: Hooded Sweater - handle: hooded-sweater - type: Sweater - vendor: Stormtech - price: 5900 - price_max: 5900 - price_min: 5900 - price_varies: false - available: true - tags: - - sweater - - hooded - - apparel - - clothing - options: - - Color - - Size - compare_at_price: 6900 - compare_at_price_max: 6900 - compare_at_price_min: 6900 - compare_at_price_varies: false - url: /products/hooded-sweater - featured_image: products/hooded-sweater.jpg - images: - - products/hooded-sweater.jpg - - products/hooded-sweater-b.jpg - description: -

Extra comfortable zip up sweater. Durable quality, ideal for any outdoor activities.

-

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

- variants: - - *product-8-var-1 - - *product-8-var-2 - - *product-8-var-3 - - *product-8-var-4 - - *product-8-var-5 - - *product-8-var-6 - - - &product-9 - id: 9 - title: D3 Digital SLR Camera - handle: d3 - type: SLR - vendor: Nikon - price: 499995 - price_max: 552500 - price_min: 499995 - price_varies: true - available: true - tags: - - camera - - slr - - nikon - - professional - options: - - Bundle - compare_at_price: - compare_at_price_max: 0 - compare_at_price_min: 0 - compare_at_price_varies: false - url: /products/d3 - featured_image: products/d3.jpg - images: - - products/d3.jpg - - products/d3_2.jpg - - products/d3_3.jpg - description: -

Flagship pro D-SLR with a 12.1-MP FX-format CMOS sensor, blazing 9 fps shooting at full FX resolution and low-noise performance up to 6400 ISO.

-

Nikon's original 12.1-megapixel FX-format (23.9 x 36mm) CMOS sensor: Couple Nikon's exclusive digital image processing system with the 12.1-megapixel FX-format and you'll get breathtakingly rich images while also reducing noise to unprecedented levels with even higher ISOs.

-

Continuous shooting at up to 9 frames per second: At full FX resolution and up to 11fps in the DX crop mode, the D3 offers uncompromised shooting speeds for fast-action and sports photography.

- variants: - - *product-9-var-1 - - *product-9-var-2 - - *product-9-var-3 - - -# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- -# Line Items -# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- - -line_items: - - &line_item-1 - id: 1 - title: 'Arbor Draft' - subtitle: '151cm' - price: 29900 - line_price: 29900 - quantity: 1 - variant: *product-1-var-1 - product: *product-1 - - - &line_item-2 - id: 2 - title: 'Comic ~ Orange' - subtitle: '159cm' - price: 19900 - line_price: 39800 - quantity: 2 - variant: *product-4-var-1 - product: *product-4 - -# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- -# Link Lists -# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- -links: - - &link-1 - id: 1 - title: Our Sale - url: /collections/sale - - &link-2 - id: 2 - title: Arbor Stuff - url: /collections/arbor - - &link-3 - id: 3 - title: All our Snowboards - url: /collections/snowboards - - &link-4 - id: 4 - title: Powered by Shopify - url: 'http://shopify.com' - - &link-5 - id: 5 - title: About Us - url: /pages/about-us - - &link-6 - id: 6 - title: Policies - url: /pages/shipping - - &link-7 - id: 7 - title: Contact Us - url: /pages/contact - - &link-8 - id: 8 - title: Our blog - url: /blogs/bigcheese-blog - - &link-9 - id: 9 - title: New Boots - url: /products/burton-boots - - &link-10 - id: 10 - title: Paginated Sale - url: /collections/paginated-sale - - &link-11 - id: 11 - title: Our Paginated blog - url: /blogs/paginated-blog - - &link-12 - id: 12 - title: Catalog - url: /collections/all - - - -# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- -# Link Lists -# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- - -link_lists: - - &link-list-1 - id: 1 - title: 'Main Menu' - handle: 'main-menu' - links: - - *link-12 - - *link-5 - - *link-7 - - *link-8 - - &link-list-2 - id: 1 - title: 'Footer Menu' - handle: 'footer' - links: - - *link-5 - - *link-6 - -# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- -# Collections -# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- - -collections: - - &collection-1 - id: 1 - title: Frontpage - handle: frontpage - url: /collections/frontpage - products: - - *product-7 - - *product-8 - - *product-9 - - - &collection-2 - id: 2 - title: Arbor - handle: arbor - url: /collections/arbor - products: - - *product-1 - - *product-2 - - - &collection-3 - id: 3 - title: Snowboards - handle: snowboards - url: /collections/snowboards - description: -

This is a description for my Snowboards collection.

- products: - - *product-1 - - *product-2 - - *product-3 - - *product-4 - - - &collection-4 - id: 4 - title: Items On Sale - handle: sale - url: /collections/sale - products: - - *product-1 - - - &collection-5 - id: 5 - title: Paginated Sale - handle: 'paginated-sale' - url: '/collections/paginated-sale' - products: - - *product-1 - - *product-2 - - *product-3 - - *product-4 - products_count: 210 - - - &collection-6 - id: 6 - title: All products - handle: 'all' - url: '/collections/all' - products: - - *product-7 - - *product-8 - - *product-9 - - *product-6 - - *product-1 - - *product-2 - - *product-3 - - *product-4 - - *product-5 - - -# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- -# Pages and Blogs -# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- -pages: - - &page-2 - id: 1 - title: Contact Us - handle: contact - url: /pages/contact - author: Tobi - content: - "

You can contact us via phone under (555) 567-2222.

-

Our retail store is located at Rue d'Avignon 32, Avignon (Provence).

-

Opening Hours:
Monday through Friday: 9am - 6pm
Saturday: 10am - 3pm
Sunday: closed

" - created_at: 2005-04-04 12:00 - - - &page-3 - id: 2 - title: About Us - handle: about-us - url: /pages/about-us - author: Tobi - content: - "

Our company was founded in 1894 and we are since operating out of Avignon from the beautiful Provence.

-

We offer the highest quality products and are proud to serve our customers to their heart's content.

" - created_at: 2005-04-04 12:00 - - - &page-4 - id: 3 - title: Shopping Cart - handle: shopping-cart - url: /pages/shopping-cart - author: Tobi - content: "
  • Your order is safe with us. Our checkout uses industry standard security to protect your information.
  • Your order will be billed immediately upon checkout.
  • ALL SALES ARE FINAL: Defective or damaged product will be exchanged
  • All orders are processed expediently: usually in under 24 hours.
" - created_at: 2005-04-04 12:00 - - - &page-5 - id: 4 - title: Shipping and Handling - handle: shipping - url: /pages/shipping - author: Tobi - content:

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

- created_at: 2005-04-04 12:00 - - - &page-6 - id: 5 - title: Frontpage - handle: frontpage - url: /pages/frontpage - author: Tobi - content:

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

- created_at: 2005-04-04 12:00 - -blogs: - - id: 1 - handle: news - title: News - url: /blogs/news - articles: - - id: 3 - title: 'Welcome to the new Foo Shop' - author: Daniel - content:

Welcome to your Shopify store! The jaded Pixel crew is really glad you decided to take Shopify for a spin.

To help you get you started with Shopify, here are a couple of tips regarding what you see on this page.

The text you see here is an article. To edit this article, create new articles or create new pages you can go to the Blogs & Pages tab of the administration menu.

The Shopify t-shirt above is a product and selling products is what Shopify is all about. To edit this product, or create new products you can go to the Products Tab in of the administration menu.

While you're looking around be sure to check out the Collections and Navigations tabs and soon you will be well on your way to populating your site.

And of course don't forget to browse the theme gallery to pick a new look for your shop!

Shopify is in beta
If you would like to make comments or suggestions please visit us in the Shopify Forums or drop us an email.

- created_at: 2005-04-04 16:00 - - id: 4 - title: 'Breaking News: Restock on all sales products' - author: Tobi - content: Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. - created_at: 2005-04-04 12:00 - articles_count: 2 - - - id: 2 - handle: bigcheese-blog - title: Bigcheese blog - url: /blogs/bigcheese-blog - articles: - - id: 1 - title: 'One thing you probably did not know yet...' - author: Justin - content: Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. - created_at: 2005-04-04 16:00 - comments: - - - id: 1 - author: John Smith - email: john@smith.com - content: Wow...great article man. - status: published - created_at: 2009-01-01 12:00 - updated_at: 2009-02-01 12:00 - url: "" - - - id: 2 - author: John Jones - email: john@jones.com - content: I really enjoyed this article. And I love your shop! It's awesome. Shopify rocks! - status: published - created_at: 2009-03-01 12:00 - updated_at: 2009-02-01 12:00 - url: "http://somesite.com/" - - id: 2 - title: Fascinating - author: Tobi - content: Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. - created_at: 2005-04-06 12:00 - comments: - articles_count: 2 - comments_enabled?: true - comment_post_url: "" - comments_count: 2 - moderated?: true - - - id: 3 - handle: paginated-blog - title: Paginated blog - url: /blogs/paginated-blog - articles: - - id: 6 - title: 'One thing you probably did not know yet...' - author: Justin - content: Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. - created_at: 2005-04-04 16:00 - - - id: 7 - title: Fascinating - author: Tobi - content: Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. - created_at: 2005-04-06 12:00 - articles_count: 200 diff --git a/performance/Support/Database.php b/performance/Support/Database.php new file mode 100644 index 0000000..a06a14c --- /dev/null +++ b/performance/Support/Database.php @@ -0,0 +1,514 @@ +, + * variants: non-empty-list, + * images: non-empty-list, + * metafields: array + * } + * + * Two rules govern this class, and both exist to keep the benchmark measuring + * the library rather than the fixture. See performance/README.md. + * + * 1. It assigns, it does not compute. No scans, no reductions, no sorting. Every + * derived value is either stored literally here or exposed as a drop method, + * where it becomes a measured part of template rendering. + * 2. Nothing is memoized. Each call builds fresh drops, so #[Cache] starts cold + * on every render and no state is shared between benchmark revolutions. + */ +final class Database +{ + public const PRODUCTS_PER_PAGE = 24; + + /** + * Titles shared by an entity and by pageTitle(), held once so the two cannot + * drift apart. + */ + private const COLLECTION_TITLE = 'Summer Essentials'; + + private const PAGE_TITLE = 'Our story'; + + private const FEATURED_PRODUCT_INDEX = 4; + + /** + * The rows live behind a method rather than a constant on purpose: PHPStan + * treats a literal array this large as an `oversized-array` and collapses the + * per-key types into one union, whereas it trusts a declared return type. + * + * @return non-empty-list + */ + private static function productRows(): array + { + return [ + [ + 'handle' => 'linen-shirt', 'title' => 'Linen Shirt', 'vendor' => 'ACME Apparel', 'type' => 'Shirts', + 'description' => 'A relaxed shirt in washed European linen, cut for long warm afternoons.', + 'price' => 3950, 'compare_at' => 5900, 'badges' => ['new', 'linen'], + 'variants' => [['linen-shirt-s', 'Small', 'S', 3950, true], ['linen-shirt-m', 'Medium', 'M', 3950, true], ['linen-shirt-l', 'Large', 'L', 4150, false]], + 'images' => [['/cdn/storefront/linen-shirt-front.jpg', 'Linen Shirt, front', 1200, 1500], ['/cdn/storefront/linen-shirt-detail.jpg', 'Linen Shirt, cuff detail', 1200, 1500]], + 'metafields' => ['care' => 'Machine wash cold, line dry', 'material' => '100% European linen'], + ], + [ + 'handle' => 'canvas-tote', 'title' => 'Canvas Tote', 'vendor' => 'Field Goods', 'type' => 'Bags', + 'description' => 'A wide-mouthed tote in heavy cotton canvas that softens with every trip.', + 'price' => 2400, 'compare_at' => 3200, 'badges' => ['travel', 'sale'], + 'variants' => [['canvas-tote-natural', 'Natural', 'Natural', 2400, true], ['canvas-tote-slate', 'Slate', 'Slate', 2400, true], ['canvas-tote-olive', 'Olive', 'Olive', 2600, true]], + 'images' => [['/cdn/storefront/canvas-tote-front.jpg', 'Canvas Tote, front', 1200, 1200], ['/cdn/storefront/canvas-tote-inside.jpg', 'Canvas Tote, interior', 1200, 1200]], + 'metafields' => ['care' => 'Spot clean', 'material' => '18oz cotton canvas'], + ], + [ + 'handle' => 'sun-hat', 'title' => 'Sun Hat', 'vendor' => 'Coastline', 'type' => 'Hats', + 'description' => 'A packable wide-brim hat woven from paper straw, with a grosgrain band.', + 'price' => 1875, 'compare_at' => 2500, 'badges' => ['summer', 'sale'], + 'variants' => [['sun-hat-s', 'Small', 'S', 1875, true], ['sun-hat-m', 'Medium', 'M', 1875, false], ['sun-hat-l', 'Large', 'L', 1875, true]], + 'images' => [['/cdn/storefront/sun-hat-front.jpg', 'Sun Hat, front', 1000, 1000], ['/cdn/storefront/sun-hat-brim.jpg', 'Sun Hat, brim', 1000, 1000]], + 'metafields' => ['care' => 'Reshape when damp', 'material' => 'Paper straw, cotton band'], + ], + [ + 'handle' => 'ceramic-mug', 'title' => 'Ceramic Mug', 'vendor' => 'Studio Form', 'type' => 'Kitchen', + 'description' => 'A thrown stoneware mug with a matte glaze and a handle that fits four fingers.', + 'price' => 2200, 'compare_at' => 2200, 'badges' => ['kitchen', 'new'], + 'variants' => [['ceramic-mug-bone', 'Bone', 'Bone', 2200, true], ['ceramic-mug-ash', 'Ash', 'Ash', 2200, true], ['ceramic-mug-clay', 'Clay', 'Clay', 2400, false]], + 'images' => [['/cdn/storefront/ceramic-mug-front.jpg', 'Ceramic Mug, front', 1100, 1100], ['/cdn/storefront/ceramic-mug-stack.jpg', 'Ceramic Mug, stacked', 1100, 1100]], + 'metafields' => ['care' => 'Dishwasher safe', 'material' => 'Glazed stoneware'], + ], + [ + 'handle' => 'weekend-bag', 'title' => 'Weekend Bag', 'vendor' => 'Field Goods', 'type' => 'Bags', + 'description' => 'Two nights of luggage with a leather base, brass hardware and a shoulder strap.', + 'price' => 8900, 'compare_at' => 10900, 'badges' => ['travel', 'limited'], + 'variants' => [['weekend-bag-tan', 'Tan', 'Tan', 8900, true], ['weekend-bag-black', 'Black', 'Black', 8900, true], ['weekend-bag-navy', 'Navy', 'Navy', 9400, true]], + 'images' => [['/cdn/storefront/weekend-bag-front.jpg', 'Weekend Bag, front', 1400, 1000], ['/cdn/storefront/weekend-bag-strap.jpg', 'Weekend Bag, strap', 1400, 1000]], + 'metafields' => ['care' => 'Condition leather twice a year', 'material' => 'Waxed canvas, bridle leather'], + ], + [ + 'handle' => 'cotton-throw', 'title' => 'Cotton Throw', 'vendor' => 'North Loom', 'type' => 'Home', + 'description' => 'A loose-woven throw with hand-knotted fringe, warm enough for a cool evening.', + 'price' => 7600, 'compare_at' => 7600, 'badges' => ['home', 'soft'], + 'variants' => [['cotton-throw-oat', 'Oat', 'Oat', 7600, true], ['cotton-throw-indigo', 'Indigo', 'Indigo', 7600, false], ['cotton-throw-rust', 'Rust', 'Rust', 7900, true]], + 'images' => [['/cdn/storefront/cotton-throw-folded.jpg', 'Cotton Throw, folded', 1300, 1000], ['/cdn/storefront/cotton-throw-weave.jpg', 'Cotton Throw, weave', 1300, 1000]], + 'metafields' => ['care' => 'Wash warm, tumble low', 'material' => 'Organic cotton'], + ], + [ + 'handle' => 'leather-wallet', 'title' => 'Leather Wallet', 'vendor' => 'Atelier No. 8', 'type' => 'Accessories', + 'description' => 'Four card slots and a note pocket, cut from a single piece of vegetable-tanned hide.', + 'price' => 5200, 'compare_at' => 6500, 'badges' => ['gift', 'sale'], + 'variants' => [['leather-wallet-chestnut', 'Chestnut', 'Chestnut', 5200, true], ['leather-wallet-black', 'Black', 'Black', 5200, true], ['leather-wallet-cognac', 'Cognac', 'Cognac', 5500, true]], + 'images' => [['/cdn/storefront/leather-wallet-front.jpg', 'Leather Wallet, front', 1000, 800], ['/cdn/storefront/leather-wallet-open.jpg', 'Leather Wallet, open', 1000, 800]], + 'metafields' => ['care' => 'Keep dry', 'material' => 'Vegetable-tanned leather'], + ], + [ + 'handle' => 'travel-bottle', 'title' => 'Travel Bottle', 'vendor' => 'Coastline', 'type' => 'Kitchen', + 'description' => 'A vacuum bottle that keeps coffee hot to the end of a long train ride.', + 'price' => 2800, 'compare_at' => 2800, 'badges' => ['travel', 'summer'], + 'variants' => [['travel-bottle-steel', 'Steel', 'Steel', 2800, true], ['travel-bottle-sand', 'Sand', 'Sand', 2800, true], ['travel-bottle-forest', 'Forest', 'Forest', 3000, false]], + 'images' => [['/cdn/storefront/travel-bottle-front.jpg', 'Travel Bottle, front', 900, 1200], ['/cdn/storefront/travel-bottle-cap.jpg', 'Travel Bottle, cap', 900, 1200]], + 'metafields' => ['care' => 'Hand wash the lid', 'material' => '18/8 stainless steel'], + ], + [ + 'handle' => 'desk-lamp', 'title' => 'Desk Lamp', 'vendor' => 'Studio Form', 'type' => 'Lighting', + 'description' => 'A counterweighted task lamp with a warm dimmable bulb and a fabric cord.', + 'price' => 6800, 'compare_at' => 8100, 'badges' => ['home', 'new'], + 'variants' => [['desk-lamp-brass', 'Brass', 'Brass', 6800, true], ['desk-lamp-black', 'Black', 'Black', 6800, true], ['desk-lamp-white', 'White', 'White', 6800, false]], + 'images' => [['/cdn/storefront/desk-lamp-lit.jpg', 'Desk Lamp, lit', 1200, 1400], ['/cdn/storefront/desk-lamp-arm.jpg', 'Desk Lamp, arm', 1200, 1400]], + 'metafields' => ['care' => 'Wipe with a dry cloth', 'material' => 'Steel, brass, linen cord'], + ], + [ + 'handle' => 'wool-socks', 'title' => 'Wool Socks', 'vendor' => 'North Loom', 'type' => 'Socks', + 'description' => 'Ribbed merino socks with a reinforced heel, warm without the itch.', + 'price' => 1600, 'compare_at' => 1600, 'badges' => ['warm', 'gift'], + 'variants' => [['wool-socks-s', 'Small', 'S', 1600, true], ['wool-socks-m', 'Medium', 'M', 1600, true], ['wool-socks-l', 'Large', 'L', 1600, true]], + 'images' => [['/cdn/storefront/wool-socks-pair.jpg', 'Wool Socks, pair', 1000, 1000], ['/cdn/storefront/wool-socks-rib.jpg', 'Wool Socks, rib', 1000, 1000]], + 'metafields' => ['care' => 'Wash cool, dry flat', 'material' => 'Merino wool blend'], + ], + [ + 'handle' => 'market-basket', 'title' => 'Market Basket', 'vendor' => 'Field Goods', 'type' => 'Home', + 'description' => 'A woven basket with leather handles, sized for a week of vegetables.', + 'price' => 4300, 'compare_at' => 5000, 'badges' => ['home', 'limited'], + 'variants' => [['market-basket-small', 'Small', 'Small', 4300, true], ['market-basket-medium', 'Medium', 'Medium', 4700, true], ['market-basket-large', 'Large', 'Large', 5200, false]], + 'images' => [['/cdn/storefront/market-basket-front.jpg', 'Market Basket, front', 1200, 1200], ['/cdn/storefront/market-basket-handles.jpg', 'Market Basket, handles', 1200, 1200]], + 'metafields' => ['care' => 'Keep out of direct sun', 'material' => 'Seagrass, leather'], + ], + [ + 'handle' => 'notebook-set', 'title' => 'Notebook Set', 'vendor' => 'Paper Mill', 'type' => 'Desk', + 'description' => 'Three saddle-stitched notebooks on paper that takes fountain ink without bleeding.', + 'price' => 1900, 'compare_at' => 1900, 'badges' => ['desk', 'new'], + 'variants' => [['notebook-set-ruled', 'Ruled', 'Ruled', 1900, true], ['notebook-set-dotted', 'Dotted', 'Dotted', 1900, true], ['notebook-set-plain', 'Plain', 'Plain', 1900, true]], + 'images' => [['/cdn/storefront/notebook-set-stack.jpg', 'Notebook Set, stack', 1100, 900], ['/cdn/storefront/notebook-set-open.jpg', 'Notebook Set, open', 1100, 900]], + 'metafields' => ['care' => 'Keep away from damp', 'material' => '100gsm uncoated paper'], + ], + [ + 'handle' => 'silk-scarf', 'title' => 'Silk Scarf', 'vendor' => 'Atelier No. 8', 'type' => 'Accessories', + 'description' => 'A hand-rolled square in heavy silk twill, printed with a coastal map motif.', + 'price' => 7400, 'compare_at' => 8900, 'badges' => ['gift', 'sale'], + 'variants' => [['silk-scarf-tide', 'Tide', 'Tide', 7400, true], ['silk-scarf-dune', 'Dune', 'Dune', 7400, false], ['silk-scarf-harbour', 'Harbour', 'Harbour', 7800, true]], + 'images' => [['/cdn/storefront/silk-scarf-flat.jpg', 'Silk Scarf, flat', 1200, 1200], ['/cdn/storefront/silk-scarf-tied.jpg', 'Silk Scarf, tied', 1200, 1200]], + 'metafields' => ['care' => 'Dry clean only', 'material' => '100% silk twill'], + ], + [ + 'handle' => 'beach-towel', 'title' => 'Beach Towel', 'vendor' => 'Coastline', 'type' => 'Home', + 'description' => 'A flat-woven towel that dries quickly and packs down to nothing.', + 'price' => 3600, 'compare_at' => 3600, 'badges' => ['summer', 'travel'], + 'variants' => [['beach-towel-stripe', 'Stripe', 'Stripe', 3600, true], ['beach-towel-solid', 'Solid', 'Solid', 3600, true], ['beach-towel-check', 'Check', 'Check', 3800, true]], + 'images' => [['/cdn/storefront/beach-towel-folded.jpg', 'Beach Towel, folded', 1300, 900], ['/cdn/storefront/beach-towel-edge.jpg', 'Beach Towel, edge', 1300, 900]], + 'metafields' => ['care' => 'Wash before first use', 'material' => 'Turkish cotton'], + ], + [ + 'handle' => 'glass-vase', 'title' => 'Glass Vase', 'vendor' => 'Studio Form', 'type' => 'Home', + 'description' => 'A hand-blown vase with a heavy base and a neck sized for a single stem.', + 'price' => 4600, 'compare_at' => 5200, 'badges' => ['home', 'limited'], + 'variants' => [['glass-vase-clear', 'Clear', 'Clear', 4600, true], ['glass-vase-smoke', 'Smoke', 'Smoke', 4900, true], ['glass-vase-green', 'Green', 'Green', 4900, false]], + 'images' => [['/cdn/storefront/glass-vase-front.jpg', 'Glass Vase, front', 1000, 1300], ['/cdn/storefront/glass-vase-base.jpg', 'Glass Vase, base', 1000, 1300]], + 'metafields' => ['care' => 'Hand wash', 'material' => 'Hand-blown glass'], + ], + [ + 'handle' => 'knit-cap', 'title' => 'Knit Cap', 'vendor' => 'North Loom', 'type' => 'Hats', + 'description' => 'A close-fitting lambswool cap with a turned brim that sits below the ears.', + 'price' => 3100, 'compare_at' => 3100, 'badges' => ['warm', 'new'], + 'variants' => [['knit-cap-charcoal', 'Charcoal', 'Charcoal', 3100, true], ['knit-cap-oat', 'Oat', 'Oat', 3100, true], ['knit-cap-moss', 'Moss', 'Moss', 3100, false]], + 'images' => [['/cdn/storefront/knit-cap-front.jpg', 'Knit Cap, front', 1000, 1000], ['/cdn/storefront/knit-cap-brim.jpg', 'Knit Cap, brim', 1000, 1000]], + 'metafields' => ['care' => 'Hand wash cool', 'material' => 'Lambswool'], + ], + [ + 'handle' => 'key-organizer', 'title' => 'Key Organizer', 'vendor' => 'Atelier No. 8', 'type' => 'Accessories', + 'description' => 'Holds six keys flat and silent, on a leather strap with a brass rivet.', + 'price' => 4100, 'compare_at' => 4800, 'badges' => ['gift', 'travel'], + 'variants' => [['key-organizer-brass', 'Brass', 'Brass', 4100, true], ['key-organizer-steel', 'Steel', 'Steel', 4100, true], ['key-organizer-black', 'Black', 'Black', 4300, true]], + 'images' => [['/cdn/storefront/key-organizer-front.jpg', 'Key Organizer, front', 900, 900], ['/cdn/storefront/key-organizer-open.jpg', 'Key Organizer, open', 900, 900]], + 'metafields' => ['care' => 'Tighten the rivet yearly', 'material' => 'Brass, bridle leather'], + ], + [ + 'handle' => 'picnic-blanket', 'title' => 'Picnic Blanket', 'vendor' => 'Field Goods', 'type' => 'Home', + 'description' => 'Wool on top, waxed cotton beneath, with straps for carrying it over a shoulder.', + 'price' => 9200, 'compare_at' => 11200, 'badges' => ['summer', 'home'], + 'variants' => [['picnic-blanket-check', 'Check', 'Check', 9200, true], ['picnic-blanket-stripe', 'Stripe', 'Stripe', 9200, false], ['picnic-blanket-plain', 'Plain', 'Plain', 9600, true]], + 'images' => [['/cdn/storefront/picnic-blanket-rolled.jpg', 'Picnic Blanket, rolled', 1300, 1000], ['/cdn/storefront/picnic-blanket-spread.jpg', 'Picnic Blanket, spread', 1300, 1000]], + 'metafields' => ['care' => 'Brush clean, air dry', 'material' => 'Wool, waxed cotton'], + ], + [ + 'handle' => 'tea-canister', 'title' => 'Tea Canister', 'vendor' => 'Paper Mill', 'type' => 'Kitchen', + 'description' => 'An airtight tin with a double lid that keeps loose leaf dry for months.', + 'price' => 2700, 'compare_at' => 2700, 'badges' => ['kitchen', 'desk'], + 'variants' => [['tea-canister-small', 'Small', 'Small', 2700, true], ['tea-canister-large', 'Large', 'Large', 3200, true], ['tea-canister-set', 'Set of two', 'Set', 5600, true]], + 'images' => [['/cdn/storefront/tea-canister-front.jpg', 'Tea Canister, front', 1000, 1200], ['/cdn/storefront/tea-canister-lid.jpg', 'Tea Canister, lid', 1000, 1200]], + 'metafields' => ['care' => 'Wipe dry before refilling', 'material' => 'Tinplate steel'], + ], + [ + 'handle' => 'camp-lantern', 'title' => 'Camp Lantern', 'vendor' => 'Coastline', 'type' => 'Lighting', + 'description' => 'A rechargeable lantern with a warm low setting that runs for forty hours.', + 'price' => 5400, 'compare_at' => 6200, 'badges' => ['travel', 'limited'], + 'variants' => [['camp-lantern-sand', 'Sand', 'Sand', 5400, true], ['camp-lantern-slate', 'Slate', 'Slate', 5400, true], ['camp-lantern-red', 'Red', 'Red', 5600, false]], + 'images' => [['/cdn/storefront/camp-lantern-lit.jpg', 'Camp Lantern, lit', 1000, 1200], ['/cdn/storefront/camp-lantern-handle.jpg', 'Camp Lantern, handle', 1000, 1200]], + 'metafields' => ['care' => 'Charge every six months', 'material' => 'Aluminium, silicone'], + ], + [ + 'handle' => 'table-clock', 'title' => 'Table Clock', 'vendor' => 'Studio Form', 'type' => 'Home', + 'description' => 'A silent-sweep clock with a brushed face and a folded steel stand.', + 'price' => 5700, 'compare_at' => 5700, 'badges' => ['home', 'desk'], + 'variants' => [['table-clock-steel', 'Steel', 'Steel', 5700, true], ['table-clock-black', 'Black', 'Black', 5700, true], ['table-clock-brass', 'Brass', 'Brass', 6100, true]], + 'images' => [['/cdn/storefront/table-clock-front.jpg', 'Table Clock, front', 1100, 1100], ['/cdn/storefront/table-clock-side.jpg', 'Table Clock, side', 1100, 1100]], + 'metafields' => ['care' => 'One AA battery, not included', 'material' => 'Brushed steel'], + ], + [ + 'handle' => 'cashmere-wrap', 'title' => 'Cashmere Wrap', 'vendor' => 'North Loom', 'type' => 'Accessories', + 'description' => 'A generous wrap in two-ply cashmere, light enough to fold into a bag.', + 'price' => 12800, 'compare_at' => 14900, 'badges' => ['warm', 'gift'], + 'variants' => [['cashmere-wrap-pearl', 'Pearl', 'Pearl', 12800, true], ['cashmere-wrap-slate', 'Slate', 'Slate', 12800, false], ['cashmere-wrap-camel', 'Camel', 'Camel', 13400, true]], + 'images' => [['/cdn/storefront/cashmere-wrap-folded.jpg', 'Cashmere Wrap, folded', 1200, 1000], ['/cdn/storefront/cashmere-wrap-worn.jpg', 'Cashmere Wrap, worn', 1200, 1000]], + 'metafields' => ['care' => 'Hand wash, dry flat', 'material' => 'Two-ply cashmere'], + ], + [ + 'handle' => 'card-holder', 'title' => 'Card Holder', 'vendor' => 'Atelier No. 8', 'type' => 'Accessories', + 'description' => 'A slim sleeve for four cards and a folded note, no stitching on the spine.', + 'price' => 3300, 'compare_at' => 3300, 'badges' => ['gift', 'new'], + 'variants' => [['card-holder-black', 'Black', 'Black', 3300, true], ['card-holder-tan', 'Tan', 'Tan', 3300, true], ['card-holder-olive', 'Olive', 'Olive', 3500, true]], + 'images' => [['/cdn/storefront/card-holder-front.jpg', 'Card Holder, front', 900, 700], ['/cdn/storefront/card-holder-side.jpg', 'Card Holder, side', 900, 700]], + 'metafields' => ['care' => 'Keep dry', 'material' => 'Vegetable-tanned leather'], + ], + [ + 'handle' => 'sketchbook', 'title' => 'Sketchbook', 'vendor' => 'Paper Mill', 'type' => 'Desk', + 'description' => 'A lay-flat sketchbook with heavy sized paper that takes a wash without buckling.', + 'price' => 2500, 'compare_at' => 2500, 'badges' => ['desk', 'travel'], + 'variants' => [['sketchbook-a5', 'A5', 'A5', 2500, true], ['sketchbook-a4', 'A4', 'A4', 3400, true], ['sketchbook-square', 'Square', 'Square', 2900, false]], + 'images' => [['/cdn/storefront/sketchbook-cover.jpg', 'Sketchbook, cover', 1000, 1300], ['/cdn/storefront/sketchbook-open.jpg', 'Sketchbook, open', 1000, 1300]], + 'metafields' => ['care' => 'Store flat', 'material' => '200gsm sized paper'], + ], + ]; + } + + /** + * @var list + */ + private const MAIN_MENU = [ + ['title' => 'New arrivals', 'url' => '/collections/new'], + ['title' => 'Summer', 'url' => '/collections/summer-essentials'], + ['title' => 'Travel', 'url' => '/collections/travel'], + ['title' => 'Home', 'url' => '/collections/home'], + ['title' => 'Journal', 'url' => '/blogs/journal'], + ]; + + /** + * @var list + */ + private const FOOTER_MENU = [ + ['title' => 'Shipping', 'url' => '/pages/shipping'], + ['title' => 'Returns', 'url' => '/pages/returns'], + ['title' => 'Sizing', 'url' => '/pages/sizing'], + ['title' => 'Contact', 'url' => '/pages/contact'], + ['title' => 'Stockists', 'url' => '/pages/stockists'], + ]; + + /** + * @var list, image: array{string, string, int, int}}> + */ + private const ARTICLES = [ + [ + 'handle' => 'packing-light', 'title' => 'Packing light for a weekend away', + 'excerpt' => 'Three small choices that make a two-night trip feel lighter than it is.', + 'author' => 'Ada Fenn', 'published_at' => '2026-05-14', 'reading_minutes' => 4, + 'tags' => ['travel', 'guides'], + 'image' => ['/cdn/storefront/journal-packing.jpg', 'A packed weekend bag', 1400, 900], + ], + [ + 'handle' => 'choosing-a-daily-carry', 'title' => 'How to choose a daily carry', + 'excerpt' => 'A practical guide to materials, proportions and the weight you stop noticing.', + 'author' => 'Ruben Oyelaran', 'published_at' => '2026-04-28', 'reading_minutes' => 6, + 'tags' => ['bags', 'guides'], + 'image' => ['/cdn/storefront/journal-carry.jpg', 'A tote on a bench', 1400, 900], + ], + [ + 'handle' => 'objects-that-earn-their-place', 'title' => 'The objects that earn their place', + 'excerpt' => 'On buying less, using more, and the quiet test a good object has to pass.', + 'author' => 'Mira Delacroix', 'published_at' => '2026-04-02', 'reading_minutes' => 5, + 'tags' => ['essays'], + 'image' => ['/cdn/storefront/journal-objects.jpg', 'A shelf of everyday objects', 1400, 900], + ], + ]; + + /** + * The for each page type. Fixture data, so it lives here rather than + * in StorefrontTheme, which owns environment wiring only. + */ + public static function pageTitle(string $template): string + { + return match ($template) { + 'index' => 'New arrivals', + // Read from the row rather than the drop: product() would build the + // whole product graph just to reach a string. + 'product' => self::productRows()[self::FEATURED_PRODUCT_INDEX]['title'], + 'page' => self::PAGE_TITLE, + default => self::COLLECTION_TITLE, + }; + } + + /** + * Page-local fixture data. The shared layout data is built separately so a + * page render only constructs the drops its own template can use. + * + * @return array<string, mixed> + */ + public static function pageData(string $template, ShopDrop $shop): array + { + return match ($template) { + 'index' => [ + 'shop' => $shop, + 'collection' => self::collection(), + 'articles' => self::articles(), + ], + 'collection' => [ + 'collection' => self::collection(), + 'products_per_page' => self::PRODUCTS_PER_PAGE, + ], + 'product' => [ + 'collection' => self::collection(), + 'product' => self::product(), + ], + 'page' => [ + 'shop' => $shop, + 'page' => self::page(), + ], + default => throw new \InvalidArgumentException("Unknown storefront page template [$template]."), + }; + } + + public static function shop(): ShopDrop + { + return new ShopDrop( + name: 'Northstar Goods', + domain: 'northstar-goods.example', + currency: 'EUR', + description: 'Useful objects for slower days, closer places and longer weekends.', + ); + } + + /** + * Totals are stored, not summed from line items — see the class docblock. + */ + public static function cart(): CartDrop + { + return new CartDrop(itemCount: 3, totalPrice: 12650); + } + + /** + * @return list<LinkDrop> + */ + public static function mainMenu(): array + { + return self::links(self::MAIN_MENU); + } + + /** + * @return list<LinkDrop> + */ + public static function footerMenu(): array + { + return self::links(self::FOOTER_MENU); + } + + /** + * @return non-empty-list<ProductDrop> + */ + public static function products(): array + { + return array_map(self::productFromRow(...), self::productRows()); + } + + /** + * @param ProductRow $row + */ + private static function productFromRow(array $row): ProductDrop + { + return new ProductDrop( + handle: $row['handle'], + title: $row['title'], + vendor: $row['vendor'], + type: $row['type'], + description: $row['description'], + priceCents: $row['price'], + compareAtPriceCents: $row['compare_at'], + badges: $row['badges'], + images: array_map(self::image(...), $row['images']), + variants: array_map(self::variant(...), $row['variants']), + metafields: new MetafieldsDrop($row['metafields']), + ); + } + + /** + * @param array{string, string, string, int, bool} $variant + */ + private static function variant(array $variant): VariantDrop + { + return new VariantDrop( + id: $variant[0], + title: $variant[1], + option: $variant[2], + priceCents: $variant[3], + available: $variant[4], + ); + } + + public static function collection(): CollectionDrop + { + return new CollectionDrop( + handle: 'summer-essentials', + title: self::COLLECTION_TITLE, + label: 'summer', + description: 'Everyday pieces for long weekends, slow mornings and bright afternoons.', + tags: ['New arrivals', 'Travel ready', 'Summer layers', 'Gifts under 100', 'Last few'], + products: self::products(), + image: self::image(['/cdn/storefront/collection-summer.jpg', 'Summer Essentials', 1600, 900]), + ); + } + + /** + * The product page subject. The row index is hard-wired rather than searched, + * and only that row is built — returning products()[4] would construct all 24 + * products, and that cost lands inside the measured region. + */ + public static function product(): ProductDrop + { + return self::productFromRow(self::productRows()[self::FEATURED_PRODUCT_INDEX]); + } + + public static function page(): PageDrop + { + return new PageDrop( + handle: 'our-story', + title: self::PAGE_TITLE, + content: 'Northstar Goods began with a single canvas bag and a stubborn belief that most things are made too quickly.', + sections: [ + ['title' => 'How we choose makers', 'body' => 'We visit every workshop before we place an order, and we place small orders first.'], + ['title' => 'What we will not do', 'body' => 'No seasonal churn, no synthetic linings, and no product we would not use ourselves.'], + ['title' => 'Repairs', 'body' => 'Send anything back within five years and we will repair it or replace it.'], + ], + ); + } + + /** + * @return list<ArticleDrop> + */ + public static function articles(): array + { + $articles = []; + + foreach (self::ARTICLES as $article) { + $articles[] = new ArticleDrop( + handle: $article['handle'], + title: $article['title'], + excerpt: $article['excerpt'], + author: $article['author'], + publishedAt: $article['published_at'], + readingMinutes: $article['reading_minutes'], + tags: $article['tags'], + image: self::image($article['image']), + ); + } + + return $articles; + } + + /** + * @param list<array{title: string, url: string}> $links + * @return list<LinkDrop> + */ + private static function links(array $links): array + { + $drops = []; + + foreach ($links as $link) { + $drops[] = new LinkDrop(title: $link['title'], url: $link['url']); + } + + return $drops; + } + + /** + * @param array{string, string, int, int} $image + */ + private static function image(array $image): ImageDrop + { + return new ImageDrop(src: $image[0], alt: $image[1], width: $image[2], height: $image[3]); + } +} diff --git a/performance/Support/Drops/ArticleDrop.php b/performance/Support/Drops/ArticleDrop.php new file mode 100644 index 0000000..620f02f --- /dev/null +++ b/performance/Support/Drops/ArticleDrop.php @@ -0,0 +1,27 @@ +<?php + +namespace Keepsuit\Liquid\Performance\Support\Drops; + +use Keepsuit\Liquid\Drop; + +final class ArticleDrop extends Drop +{ + /** + * @param list<string> $tags + */ + public function __construct( + public readonly string $handle, + public readonly string $title, + public readonly string $excerpt, + public readonly string $author, + public readonly string $publishedAt, + public readonly int $readingMinutes, + public readonly array $tags, + public readonly ImageDrop $image, + ) {} + + public function url(): string + { + return '/blogs/journal/'.$this->handle; + } +} diff --git a/performance/Support/Drops/CartDrop.php b/performance/Support/Drops/CartDrop.php new file mode 100644 index 0000000..36a5cdd --- /dev/null +++ b/performance/Support/Drops/CartDrop.php @@ -0,0 +1,24 @@ +<?php + +namespace Keepsuit\Liquid\Performance\Support\Drops; + +use Keepsuit\Liquid\Drop; + +/** + * Scalars only. The cart earns a drop because the layout header reads it on every + * page, but nothing renders line items, so there is no LineItemDrop. + * + * Totals are stored, not summed: Database assigns, it does not compute. + */ +final class CartDrop extends Drop +{ + public function __construct( + public readonly int $itemCount, + public readonly int $totalPrice, + ) {} + + public function isEmpty(): bool + { + return $this->itemCount === 0; + } +} diff --git a/performance/Support/Drops/CollectionDrop.php b/performance/Support/Drops/CollectionDrop.php new file mode 100644 index 0000000..2b5dfe4 --- /dev/null +++ b/performance/Support/Drops/CollectionDrop.php @@ -0,0 +1,32 @@ +<?php + +namespace Keepsuit\Liquid\Performance\Support\Drops; + +use Keepsuit\Liquid\Drop; + +final class CollectionDrop extends Drop +{ + /** + * @param list<string> $tags + * @param list<ProductDrop> $products + */ + public function __construct( + public readonly string $handle, + public readonly string $title, + public readonly string $label, + public readonly string $description, + public readonly array $tags, + public readonly array $products, + public readonly ImageDrop $image, + ) {} + + public function url(): string + { + return '/collections/'.$this->handle; + } + + public function productsCount(): int + { + return count($this->products); + } +} diff --git a/performance/Support/Drops/ImageDrop.php b/performance/Support/Drops/ImageDrop.php new file mode 100644 index 0000000..ebbe953 --- /dev/null +++ b/performance/Support/Drops/ImageDrop.php @@ -0,0 +1,24 @@ +<?php + +namespace Keepsuit\Liquid\Performance\Support\Drops; + +use Keepsuit\Liquid\Drop; + +/** + * Stored fields are public typed properties, which resolve through the cheapest + * branch of Drop::__get. See performance/README.md for the strategy matrix. + */ +final class ImageDrop extends Drop +{ + public function __construct( + public readonly string $src, + public readonly string $alt, + public readonly int $width, + public readonly int $height, + ) {} + + public function aspectRatio(): string + { + return $this->width.'/'.$this->height; + } +} diff --git a/performance/Support/Drops/LinkDrop.php b/performance/Support/Drops/LinkDrop.php new file mode 100644 index 0000000..04f0015 --- /dev/null +++ b/performance/Support/Drops/LinkDrop.php @@ -0,0 +1,13 @@ +<?php + +namespace Keepsuit\Liquid\Performance\Support\Drops; + +use Keepsuit\Liquid\Drop; + +final class LinkDrop extends Drop +{ + public function __construct( + public readonly string $title, + public readonly string $url, + ) {} +} diff --git a/performance/Support/Drops/MetafieldsDrop.php b/performance/Support/Drops/MetafieldsDrop.php new file mode 100644 index 0000000..711f011 --- /dev/null +++ b/performance/Support/Drops/MetafieldsDrop.php @@ -0,0 +1,40 @@ +<?php + +namespace Keepsuit\Liquid\Performance\Support\Drops; + +use Keepsuit\Liquid\Attributes\DropDynamicProperties; +use Keepsuit\Liquid\Drop; +use Keepsuit\Liquid\Exceptions\UndefinedDropMethodException; + +/** + * The only drop in the fixture on the liquidMethodMissing path, and the only one + * with a real reason to be there: metafield keys are arbitrary strings, so they + * cannot be declared as properties up front. + * + * Templates must only read keys that exist. A miss costs up to three thrown and + * caught exceptions inside Drop::__get, and under strictVariables it is an error + * the fixture test will fail on. + */ +final class MetafieldsDrop extends Drop +{ + /** + * @param array<string, string> $fields + */ + public function __construct( + private readonly array $fields, + ) {} + + /** + * Declared so toArray() can see the dynamic keys, as both liquidMethodMissing + * implementations in src/Drops do. It does not affect Drop::__get resolution. + */ + #[DropDynamicProperties(['care', 'material'])] + protected function liquidMethodMissing(string $name): mixed + { + if (array_key_exists($name, $this->fields)) { + return $this->fields[$name]; + } + + throw new UndefinedDropMethodException($name); + } +} diff --git a/performance/Support/Drops/PageDrop.php b/performance/Support/Drops/PageDrop.php new file mode 100644 index 0000000..0742ed7 --- /dev/null +++ b/performance/Support/Drops/PageDrop.php @@ -0,0 +1,23 @@ +<?php + +namespace Keepsuit\Liquid\Performance\Support\Drops; + +use Keepsuit\Liquid\Drop; + +final class PageDrop extends Drop +{ + /** + * @param list<array{title: string, body: string}> $sections + */ + public function __construct( + public readonly string $handle, + public readonly string $title, + public readonly string $content, + public readonly array $sections, + ) {} + + public function url(): string + { + return '/pages/'.$this->handle; + } +} diff --git a/performance/Support/Drops/ProductDrop.php b/performance/Support/Drops/ProductDrop.php new file mode 100644 index 0000000..2c1813d --- /dev/null +++ b/performance/Support/Drops/ProductDrop.php @@ -0,0 +1,70 @@ +<?php + +namespace Keepsuit\Liquid\Performance\Support\Drops; + +use Keepsuit\Liquid\Attributes\Cache; +use Keepsuit\Liquid\Drop; + +final class ProductDrop extends Drop +{ + /** + * @param list<string> $badges + * @param non-empty-list<ImageDrop> $images + * @param non-empty-list<VariantDrop> $variants + */ + public function __construct( + public readonly string $handle, + public readonly string $title, + public readonly string $vendor, + public readonly string $type, + public readonly string $description, + public readonly int $priceCents, + public readonly int $compareAtPriceCents, + public readonly array $badges, + public readonly array $images, + public readonly array $variants, + public readonly MetafieldsDrop $metafields, + ) {} + + public function url(): string + { + return '/products/'.$this->handle; + } + + public function featuredImage(): ImageDrop + { + return $this->images[0]; + } + + public function onSale(): bool + { + return $this->compareAtPriceCents > $this->priceCents; + } + + public function savingCents(): int + { + return $this->compareAtPriceCents - $this->priceCents; + } + + /** + * Walks every variant instead of returning a stored field, which is why it is + * a method rather than a property. + * + * The cache earns its keep on the product page, where variant_picker and specs + * both read it for the same instance. On a card it is read once, so the cache + * is populated and never hit — that asymmetry is realistic and deliberate. + */ + #[Cache] + public function inStockVariantCount(): int + { + $count = 0; + + foreach ($this->variants as $variant) { + if ($variant->available) { + $count++; + } + } + + return $count; + } +} diff --git a/performance/Support/Drops/ShopDrop.php b/performance/Support/Drops/ShopDrop.php new file mode 100644 index 0000000..5c5106f --- /dev/null +++ b/performance/Support/Drops/ShopDrop.php @@ -0,0 +1,15 @@ +<?php + +namespace Keepsuit\Liquid\Performance\Support\Drops; + +use Keepsuit\Liquid\Drop; + +final class ShopDrop extends Drop +{ + public function __construct( + public readonly string $name, + public readonly string $domain, + public readonly string $currency, + public readonly string $description, + ) {} +} diff --git a/performance/Support/Drops/VariantDrop.php b/performance/Support/Drops/VariantDrop.php new file mode 100644 index 0000000..f030c3e --- /dev/null +++ b/performance/Support/Drops/VariantDrop.php @@ -0,0 +1,16 @@ +<?php + +namespace Keepsuit\Liquid\Performance\Support\Drops; + +use Keepsuit\Liquid\Drop; + +final class VariantDrop extends Drop +{ + public function __construct( + public readonly string $id, + public readonly string $title, + public readonly string $option, + public readonly int $priceCents, + public readonly bool $available, + ) {} +} diff --git a/performance/Support/StorefrontFilters.php b/performance/Support/StorefrontFilters.php new file mode 100644 index 0000000..e5ac727 --- /dev/null +++ b/performance/Support/StorefrontFilters.php @@ -0,0 +1,23 @@ +<?php + +namespace Keepsuit\Liquid\Performance\Support; + +use Keepsuit\Liquid\Filters\FiltersProvider; + +/** + * Fixture-local filters. Named as a real theme would name them — neither `money` + * nor `pluralize` collides with a standard filter, and the templates are supposed + * to read like real Liquid. + */ +final class StorefrontFilters extends FiltersProvider +{ + public function money(int|float $cents): string + { + return '€'.number_format($cents / 100, 2, '.', ''); + } + + public function pluralize(int $count, string $singular, string $plural): string + { + return $count === 1 ? $singular : $plural; + } +} diff --git a/performance/Support/StorefrontTheme.php b/performance/Support/StorefrontTheme.php new file mode 100644 index 0000000..1ccaa55 --- /dev/null +++ b/performance/Support/StorefrontTheme.php @@ -0,0 +1,204 @@ +<?php + +namespace Keepsuit\Liquid\Performance\Support; + +use Keepsuit\Liquid\Environment; +use Keepsuit\Liquid\EnvironmentFactory; +use Keepsuit\Liquid\FileSystems\LocalFileSystem; +use Keepsuit\Liquid\Render\RenderContext; + +/** + * Entry point for the storefront benchmark theme. + * + * Benchmarks build the environment with the library defaults; the fixture test + * layers strictVariables, strictFilters and rethrowErrors on top of + * environmentFactory(), so a missing variable, a missing filter or a swallowed + * render error fails the suite instead of rendering as empty output. + */ +final class StorefrontTheme +{ + /** @var list<string>|null */ + private static ?array $templateNames = null; + + /** @var list<string>|null */ + private static ?array $pageTemplateNames = null; + + private static ?string $layoutTemplateName = null; + + public static function themePath(): string + { + return dirname(__DIR__).'/themes/storefront'; + } + + public static function environmentFactory(): EnvironmentFactory + { + return EnvironmentFactory::new() + ->setFilesystem(new LocalFileSystem(self::themePath())) + ->registerFilters(StorefrontFilters::class); + } + + public static function environment(): Environment + { + return self::environmentFactory()->build(); + } + + /** + * Every template the theme owns: the layout, the page templates and every + * snippet. Parsing benchmarks walk this list. + * + * @return list<string> + */ + public static function templateNames(): array + { + return self::$templateNames ??= self::discoverTemplateNames(); + } + + /** + * @return list<string> + */ + public static function pageTemplateNames(): array + { + return self::$pageTemplateNames ??= array_values(array_filter( + self::templateNames(), + static fn (string $templateName): bool => str_starts_with($templateName, 'templates.'), + )); + } + + public static function layoutTemplateName(): string + { + if (self::$layoutTemplateName !== null) { + return self::$layoutTemplateName; + } + + $layouts = array_values(array_filter( + self::templateNames(), + static fn (string $templateName): bool => str_starts_with($templateName, 'layout.'), + )); + + if (count($layouts) !== 1) { + throw new \RuntimeException('The storefront theme must contain exactly one layout template.'); + } + + return self::$layoutTemplateName = $layouts[0]; + } + + public static function templatePath(string $templateName): string + { + return self::themePath().'/'.str_replace('.', '/', $templateName).'.liquid'; + } + + public static function templateSource(string $templateName): string + { + $source = file_get_contents(self::templatePath($templateName)); + + if ($source === false) { + throw new \RuntimeException("Could not read fixture template [$templateName]."); + } + + return $source; + } + + /** + * @return array{page: array<string, mixed>, layout: array<string, mixed>} + */ + public static function renderData(string $pageTemplateName): array + { + $template = str_replace('templates.', '', $pageTemplateName); + $shop = Database::shop(); + + return [ + 'page' => Database::pageData($template, $shop), + 'layout' => [ + 'shop' => $shop, + 'cart' => Database::cart(), + 'linklists' => [ + 'main_menu' => Database::mainMenu(), + 'footer' => Database::footerMenu(), + ], + 'template' => $template, + 'page_title' => Database::pageTitle($template), + ], + ]; + } + + /** + * @param array<string, mixed> $data + */ + private static function newRenderContext( + Environment $environment, + array $data, + ): RenderContext { + return $environment->newRenderContext(staticData: $data); + } + + /** + * @param array<string, mixed> $data + */ + private static function newLayoutRenderContext( + Environment $environment, + mixed $content, + array $data, + ): RenderContext { + return $environment->newRenderContext(staticData: [ + ...$data, + 'content_for_layout' => $content, + ]); + } + + /** + * The page renders into its own context, then the layout renders into a second + * one. That means the layout cannot see variables the page assigned — nothing + * under layout/ may read template-assigned state. + */ + public static function renderPage(Environment $environment, string $pageTemplateName): string + { + $data = self::renderData($pageTemplateName); + $content = $environment->parseTemplate($pageTemplateName) + ->render(self::newRenderContext($environment, $data['page'])); + + return $environment->parseTemplate(self::layoutTemplateName()) + ->render(self::newLayoutRenderContext($environment, $content, $data['layout'])); + } + + /** + * @return \Generator<string> + */ + public static function streamPage(Environment $environment, string $pageTemplateName): \Generator + { + $data = self::renderData($pageTemplateName); + $content = $environment->parseTemplate($pageTemplateName) + ->stream(self::newRenderContext($environment, $data['page'])); + + return $environment->parseTemplate(self::layoutTemplateName()) + ->stream(self::newLayoutRenderContext($environment, $content, $data['layout'])); + } + + /** + * @return list<string> + */ + private static function discoverTemplateNames(): array + { + $templateNames = []; + $files = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator( + self::themePath(), + \FilesystemIterator::SKIP_DOTS, + )); + + foreach ($files as $file) { + if (! $file instanceof \SplFileInfo || ! $file->isFile() || $file->getExtension() !== 'liquid') { + continue; + } + + $relativePath = substr($file->getPathname(), strlen(self::themePath()) + 1, -7); + $templateNames[] = str_replace(DIRECTORY_SEPARATOR, '.', $relativePath); + } + + sort($templateNames); + + if ($templateNames === []) { + throw new \RuntimeException('The storefront theme contains no Liquid templates.'); + } + + return $templateNames; + } +} diff --git a/performance/ThemeRunner.php b/performance/ThemeRunner.php deleted file mode 100644 index 3895b4a..0000000 --- a/performance/ThemeRunner.php +++ /dev/null @@ -1,110 +0,0 @@ -<?php - -namespace Keepsuit\Liquid\Performance; - -use Keepsuit\Liquid\Environment; -use Keepsuit\Liquid\Performance\Shopify\Database; -use Keepsuit\Liquid\Support\Arr; - -class ThemeRunner -{ - /** - * @var array<ThemeTestTemplate> - */ - protected array $tests; - - /** - * @var array<CompiledThemeTestTemplate> - */ - protected array $compiledTemplates; - - public function __construct( - protected Environment $environment - ) { - $files = glob(__DIR__.'/tests/**/*.liquid'); - - if ($files === false) { - throw new \RuntimeException('Could not find any tests'); - } - - $this->tests = Arr::compact(Arr::map($files, function (string $path) { - if (basename($path) === 'theme.liquid') { - return null; - } - - $themePath = dirname($path).'/theme.liquid'; - - return new ThemeTestTemplate( - environment: $this->environment, - templateName: $path, - liquid: file_get_contents($path) ?: '', - layoutLiquid: file_exists($themePath) ? (file_get_contents($themePath) ?: '') : null, - ); - })); - - $this->compileAllTests(); - } - - public function tokenize(): void - { - foreach ($this->tests as $test) { - $this->environment->newParseContext()->tokenize($test->liquid); - if ($test->layoutLiquid !== null) { - $this->environment->newParseContext()->tokenize($test->layoutLiquid); - } - } - } - - public function compile(): void - { - foreach ($this->tests as $test) { - $this->environment->parseString($test->liquid); - if ($test->layoutLiquid !== null) { - $this->environment->parseString($test->layoutLiquid); - } - } - } - - public function render(): void - { - $database = [...Database::tables()]; - - foreach ($this->compiledTemplates as $compiled) { - $compiled->render($database); - } - } - - public function stream(): void - { - $database = [...Database::tables()]; - - foreach ($this->compiledTemplates as $compiled) { - $compiled->stream($database); - } - } - - public function run(): void - { - $database = [...Database::tables()]; - - foreach ($this->tests as $test) { - $compiled = $test->compile(); - $compiled->render($database); - } - } - - public function runStreaming(): void - { - $database = [...Database::tables()]; - - foreach ($this->tests as $test) { - $compiled = $test->compile(); - $compiled->stream($database); - } - } - - protected function compileAllTests(): void - { - $this->compiledTemplates = Arr::map($this->tests, fn (ThemeTestTemplate $template) => $template->compile()); - } -} diff --git a/performance/ThemeTestTemplate.php b/performance/ThemeTestTemplate.php deleted file mode 100644 index fcd9772..0000000 --- a/performance/ThemeTestTemplate.php +++ /dev/null @@ -1,33 +0,0 @@ -<?php - -namespace Keepsuit\Liquid\Performance; - -use Keepsuit\Liquid\Environment; - -class ThemeTestTemplate -{ - public function __construct( - protected Environment $environment, - public string $templateName, - public string $liquid, - public ?string $layoutLiquid, - ) {} - - public function pageTemplate(): string - { - return basename($this->templateName); - } - - public function compile(): CompiledThemeTestTemplate - { - $template = $this->environment->parseString($this->liquid); - $layout = $this->layoutLiquid !== null ? $this->environment->parseString($this->layoutLiquid) : null; - - return new CompiledThemeTestTemplate( - environment: $this->environment, - templateName: $this->templateName, - template: $template, - layout: $layout, - ); - } -} diff --git a/performance/benchmarks/LiquidBench.php b/performance/benchmarks/LiquidBench.php deleted file mode 100644 index 53719ce..0000000 --- a/performance/benchmarks/LiquidBench.php +++ /dev/null @@ -1,70 +0,0 @@ -<?php - -namespace Keepsuit\Liquid\Performance\benchmarks; - -use Keepsuit\Liquid\EnvironmentFactory; -use Keepsuit\Liquid\Performance\Shopify\CommentFormTag; -use Keepsuit\Liquid\Performance\Shopify\CustomFilters; -use Keepsuit\Liquid\Performance\Shopify\PaginateTag; -use Keepsuit\Liquid\Performance\ThemeRunner; -use PhpBench\Attributes\BeforeMethods; -use PhpBench\Attributes\Iterations; -use PhpBench\Attributes\OutputMode; -use PhpBench\Attributes\OutputTimeUnit; -use PhpBench\Attributes\Revs; - -#[Iterations(10)] -#[Revs(10)] -#[OutputMode('throughput')] -#[OutputTimeUnit('seconds', precision: 3)] -#[BeforeMethods('setUp')] -class LiquidBench -{ - protected ThemeRunner $themeRunner; - - public function setUp(): void - { - $this->themeRunner = $this->getThemeRunner(); - } - - public function benchTokenize(): void - { - $this->themeRunner->tokenize(); - } - - public function benchParsing(): void - { - $this->themeRunner->compile(); - } - - public function benchRender(): void - { - $this->themeRunner->render(); - } - - public function benchStream(): void - { - $this->themeRunner->stream(); - } - - public function benchParsingAndRendering(): void - { - $this->themeRunner->run(); - } - - public function benchParsingAndStreaming(): void - { - $this->themeRunner->runStreaming(); - } - - protected function getThemeRunner(): ThemeRunner - { - $environment = EnvironmentFactory::new() - ->registerTag(CommentFormTag::class) - ->registerTag(PaginateTag::class) - ->registerFilters(CustomFilters::class) - ->build(); - - return new ThemeRunner($environment); - } -} diff --git a/performance/benchmarks/OperationBench.php b/performance/benchmarks/OperationBench.php new file mode 100644 index 0000000..2c89c44 --- /dev/null +++ b/performance/benchmarks/OperationBench.php @@ -0,0 +1,191 @@ +<?php + +namespace Keepsuit\Liquid\Performance\benchmarks; + +use Keepsuit\Liquid\Environment; +use Keepsuit\Liquid\EnvironmentFactory; +use Keepsuit\Liquid\Performance\Support\Database; +use Keepsuit\Liquid\Performance\Support\Drops\ProductDrop; +use Keepsuit\Liquid\Render\RenderContext; +use Keepsuit\Liquid\Template; +use PhpBench\Attributes\BeforeMethods; +use PhpBench\Attributes\Groups; +use PhpBench\Attributes\Iterations; +use PhpBench\Attributes\OutputMode; +use PhpBench\Attributes\OutputTimeUnit; +use PhpBench\Attributes\Revs; + +#[Groups(['operations'])] +#[Iterations(10)] +#[Revs(20)] +#[OutputMode('throughput')] +#[OutputTimeUnit('seconds', precision: 3)] +#[BeforeMethods('setUp')] +class OperationBench +{ + private Environment $environment; + + private Template $scalarTemplate; + + private Template $nestedTemplate; + + private Template $filterWithoutArgumentsTemplate; + + private Template $filterWithArgumentsTemplate; + + private Template $dropMethodTemplate; + + private Template $dropMethodMissingHitTemplate; + + private Template $dropMethodMissingMissTemplate; + + private Template $productListTemplate; + + /** + * Built in setUp, not in the subject: these two subjects measure how + * Drop::__get resolves a property, and constructing a ProductDrop (variants, + * images, metafields) inside the subject would swamp that with fixture cost. + * Sharing one instance across revolutions is safe here only because the + * template reads a public property and never a #[Cache]d method. + */ + private ProductDrop $productDrop; + + /** @var ProductDrop[] */ + protected array $productList; + + public function setUp(): void + { + $this->environment = EnvironmentFactory::new()->build(); + $this->scalarTemplate = $this->environment->parseString(str_repeat('{{ value }}', 64)); + $this->nestedTemplate = $this->environment->parseString(str_repeat('{{ product.title }}', 64)); + $this->filterWithoutArgumentsTemplate = $this->environment->parseString(str_repeat('{{ value | upcase | escape }}', 32)); + $this->filterWithArgumentsTemplate = $this->environment->parseString(str_repeat('{{ value | append: suffix | replace: from, to }}', 32)); + $this->dropMethodTemplate = $this->environment->parseString(str_repeat('{{ product.url }}', 64)); + $this->dropMethodMissingHitTemplate = $this->environment->parseString(str_repeat('{{ product.metafields.material }}', 64)); + $this->dropMethodMissingMissTemplate = $this->environment->parseString(str_repeat('{{ product.metafields.unknown }}', 64)); + $this->productListTemplate = $this->environment->parseString('{% for product in products %}{{ product.title }}{% endfor %}'); + $this->productDrop = Database::product(); + $this->productList = Database::products(); + } + + public function benchScalarRender(): void + { + $this->scalarTemplate->render($this->environment->newRenderContext( + staticData: ['value' => 'value'], + )); + } + + public function benchScalarStream(): void + { + $this->drain($this->scalarTemplate->stream($this->environment->newRenderContext( + staticData: ['value' => 'value'], + ))); + } + + public function benchNestedArrayRender(): void + { + $this->nestedTemplate->render($this->environment->newRenderContext( + staticData: ['product' => ['title' => 'Product title']], + )); + } + + public function benchNestedArrayStream(): void + { + $this->drain($this->nestedTemplate->stream($this->environment->newRenderContext( + staticData: ['product' => ['title' => 'Product title']], + ))); + } + + public function benchDropRender(): void + { + $this->nestedTemplate->render($this->environment->newRenderContext( + staticData: ['product' => $this->productDrop], + )); + } + + public function benchDropStream(): void + { + $this->drain($this->nestedTemplate->stream($this->environment->newRenderContext( + staticData: ['product' => $this->productDrop], + ))); + } + + public function benchDropMethodRender(): void + { + $this->dropMethodTemplate->render($this->productContext()); + } + + public function benchDropMethodStream(): void + { + $this->drain($this->dropMethodTemplate->stream($this->productContext())); + } + + public function benchDropMethodMissingHitRender(): void + { + $this->dropMethodMissingHitTemplate->render($this->productContext()); + } + + public function benchDropMethodMissingHitStream(): void + { + $this->drain($this->dropMethodMissingHitTemplate->stream($this->productContext())); + } + + public function benchDropMethodMissingMissRender(): void + { + $this->dropMethodMissingMissTemplate->render($this->productContext()); + } + + public function benchDropMethodMissingMissStream(): void + { + $this->drain($this->dropMethodMissingMissTemplate->stream($this->productContext())); + } + + public function benchProductListRender(): void + { + $this->productListTemplate->render($this->environment->newRenderContext( + staticData: ['products' => $this->productList], + )); + } + + public function benchProductListStream(): void + { + $this->drain($this->productListTemplate->stream($this->environment->newRenderContext( + staticData: ['products' => $this->productList], + ))); + } + + public function benchFilterWithoutArguments(): void + { + $this->filterWithoutArgumentsTemplate->render($this->filterContext()); + } + + public function benchFilterWithArguments(): void + { + $this->filterWithArgumentsTemplate->render($this->filterContext()); + } + + /** + * @param \Generator<string> $stream + */ + private function drain(\Generator $stream): void + { + while ($stream->valid()) { + $stream->next(); + } + } + + private function filterContext(): RenderContext + { + return $this->environment->newRenderContext(staticData: [ + 'value' => 'example', + 'suffix' => '-suffix', + 'from' => 'example', + 'to' => 'value', + ]); + } + + private function productContext(): RenderContext + { + return $this->environment->newRenderContext(staticData: ['product' => $this->productDrop]); + } +} diff --git a/performance/benchmarks/RenderBench.php b/performance/benchmarks/RenderBench.php deleted file mode 100644 index edc819e..0000000 --- a/performance/benchmarks/RenderBench.php +++ /dev/null @@ -1,86 +0,0 @@ -<?php - -namespace Keepsuit\Liquid\Performance\benchmarks; - -use Keepsuit\Liquid\Environment; -use Keepsuit\Liquid\EnvironmentFactory; -use Keepsuit\Liquid\Performance\Shopify\DatabaseDrop; -use Keepsuit\Liquid\Template; -use PhpBench\Attributes\BeforeMethods; -use PhpBench\Attributes\Iterations; -use PhpBench\Attributes\OutputMode; -use PhpBench\Attributes\OutputTimeUnit; -use PhpBench\Attributes\Revs; - -#[Iterations(10)] -#[Revs(20)] -#[OutputMode('throughput')] -#[OutputTimeUnit('seconds', precision: 3)] -#[BeforeMethods('setUp')] -class RenderBench -{ - private Environment $environment; - - private Template $scalarTemplate; - - private Template $nestedTemplate; - - public function setUp(): void - { - $this->environment = EnvironmentFactory::new()->build(); - $this->scalarTemplate = $this->environment->parseString(str_repeat('{{ value }}', 64)); - $this->nestedTemplate = $this->environment->parseString(str_repeat('{{ product.title }}', 64)); - } - - public function benchScalarRender(): void - { - $this->scalarTemplate->render($this->environment->newRenderContext( - staticData: ['value' => 'value'], - )); - } - - public function benchScalarStream(): void - { - $this->drain($this->scalarTemplate->stream($this->environment->newRenderContext( - staticData: ['value' => 'value'], - ))); - } - - public function benchNestedArrayRender(): void - { - $this->nestedTemplate->render($this->environment->newRenderContext( - staticData: ['product' => ['title' => 'Product title']], - )); - } - - public function benchNestedArrayStream(): void - { - $this->drain($this->nestedTemplate->stream($this->environment->newRenderContext( - staticData: ['product' => ['title' => 'Product title']], - ))); - } - - public function benchDropRender(): void - { - $this->nestedTemplate->render($this->environment->newRenderContext( - staticData: ['product' => new DatabaseDrop(['title' => 'Product title'])], - )); - } - - public function benchDropStream(): void - { - $this->drain($this->nestedTemplate->stream($this->environment->newRenderContext( - staticData: ['product' => new DatabaseDrop(['title' => 'Product title'])], - ))); - } - - /** - * @param \Generator<string> $stream - */ - private function drain(\Generator $stream): void - { - while ($stream->valid()) { - $stream->next(); - } - } -} diff --git a/performance/benchmarks/TemplateCacheBench.php b/performance/benchmarks/TemplateCacheBench.php index fc2e348..0d7c3c4 100644 --- a/performance/benchmarks/TemplateCacheBench.php +++ b/performance/benchmarks/TemplateCacheBench.php @@ -2,125 +2,166 @@ namespace Keepsuit\Liquid\Performance\benchmarks; +use Keepsuit\Liquid\Contracts\LiquidTemplatesCache; use Keepsuit\Liquid\Environment; -use Keepsuit\Liquid\EnvironmentFactory; -use Keepsuit\Liquid\FileSystems\LocalFileSystem; -use Keepsuit\Liquid\Performance\Shopify\CommentFormTag; -use Keepsuit\Liquid\Performance\Shopify\CustomFilters; -use Keepsuit\Liquid\Performance\Shopify\Database; -use Keepsuit\Liquid\Performance\Shopify\PaginateTag; -use Keepsuit\Liquid\Support\Arr; +use Keepsuit\Liquid\Performance\Support\StorefrontTheme; use Keepsuit\Liquid\TemplatesCache\MemoryTemplatesCache; use Keepsuit\Liquid\TemplatesCache\SerializeTemplatesCache; use Keepsuit\Liquid\TemplatesCache\VarExportTemplatesCache; use PhpBench\Attributes\AfterMethods; use PhpBench\Attributes\BeforeMethods; +use PhpBench\Attributes\Groups; use PhpBench\Attributes\Iterations; use PhpBench\Attributes\OutputMode; use PhpBench\Attributes\OutputTimeUnit; use PhpBench\Attributes\Revs; -use PhpBench\Attributes\Warmup; +#[Groups(['cache'])] #[Iterations(10)] -#[Revs(10)] -#[Warmup(1)] +#[Revs(20)] #[OutputMode('throughput')] #[OutputTimeUnit('seconds', precision: 3)] #[AfterMethods('clearCache')] class TemplateCacheBench { - protected Environment $environment; + private const CACHE_DIRECTORY = 'keepsuit-liquid-phpbench'; - protected array $templates; + private Environment $environment; - #[BeforeMethods('setupInMemory')] - public function benchInMemory(): void + private LiquidTemplatesCache $cache; + + /** @var list<string> */ + private array $templateNames; + + /** @var list<string> */ + private array $pageTemplateNames; + + private string $cacheDirectory; + + #[BeforeMethods('setUpInMemoryBuild')] + public function benchBuildInMemory(): void { - $this->renderTemplates(); + $this->buildStaticTheme(); } - public function setupInMemory(): void + #[BeforeMethods('setUpInMemoryCachedRender')] + public function benchLoadAndRenderInMemory(): void { - $this->environment = $this->environmentBuilder() - ->setTemplatesCache(new MemoryTemplatesCache) - ->build(); + $this->renderCachedTheme(); + } - $this->loadTemplates(); + #[BeforeMethods('setUpSerializeBuild')] + public function benchBuildSerialize(): void + { + $this->buildStaticTheme(); } - #[BeforeMethods('setupVarExporter')] - public function benchVarExporter(): void + #[BeforeMethods('setUpSerializeCachedRender')] + public function benchLoadAndRenderSerialize(): void { - $this->renderTemplates(); + $this->renderCachedTheme(); } - public function setupVarExporter(): void + #[BeforeMethods('setUpVarExporterBuild')] + public function benchBuildVarExporter(): void { - $this->environment = $this->environmentBuilder() - ->setTemplatesCache(new VarExportTemplatesCache(__DIR__.'/cache/var_export', keepInMemory: false)) - ->build(); + $this->buildStaticTheme(); + } - $this->loadTemplates(); + #[BeforeMethods('setUpVarExporterCachedRender')] + public function benchLoadAndRenderVarExporter(): void + { + $this->renderCachedTheme(); } - #[BeforeMethods('setupSerialize')] - public function benchSerialize(): void + public function setUpInMemoryBuild(): void { - $this->renderTemplates(); + $this->setUpBuild('memory'); } - public function setupSerialize(): void + public function setUpInMemoryCachedRender(): void { - $this->environment = $this->environmentBuilder() - ->setTemplatesCache(new SerializeTemplatesCache(__DIR__.'/cache/serialize', keepInMemory: false)) - ->build(); + $this->setUpCachedRender('memory'); + } - $this->loadTemplates(); + public function setUpSerializeBuild(): void + { + $this->setUpBuild('serialize'); } - protected function loadTemplates(): void + public function setUpSerializeCachedRender(): void { - $baseDir = __DIR__.'/../tests'; - $files = glob($baseDir.'/**/*.liquid'); + $this->setUpCachedRender('serialize'); + } - if ($files === false) { - throw new \RuntimeException('Could not find any tests'); - } + public function setUpVarExporterBuild(): void + { + $this->setUpBuild('var-exporter'); + } - $this->templates = Arr::map($files, function (string $path) use ($baseDir) { - // relative path to the base directory - $name = str_replace($baseDir.'/', '', $path); - // remove .liquid extension - $name = substr($name, 0, -7); + public function setUpVarExporterCachedRender(): void + { + $this->setUpCachedRender('var-exporter'); + } - // replace / with . - return str_replace('/', '.', $name); - }); + public function clearCache(): void + { + $this->cache->clear(); + } + + private function setUpBuild(string $backend): void + { + $this->templateNames = StorefrontTheme::templateNames(); + $this->pageTemplateNames = StorefrontTheme::pageTemplateNames(); + $this->cacheDirectory = sys_get_temp_dir().'/'.self::CACHE_DIRECTORY.'-'.bin2hex(random_bytes(8)); + $this->cache = $this->newCache($backend); + $this->cache->clear(); + $this->environment = StorefrontTheme::environmentFactory()->setTemplatesCache($this->cache)->build(); + } + + private function setUpCachedRender(string $backend): void + { + $this->setUpBuild($backend); + $this->compileStaticTheme($this->environment); - foreach ($this->templates as $template) { - $this->environment->parseTemplate($template); + $this->cache = $backend === 'memory' + ? $this->cache + : $this->newCache($backend); + $this->environment = StorefrontTheme::environmentFactory()->setTemplatesCache($this->cache)->build(); + } + + private function compileStaticTheme(Environment $environment): void + { + foreach ($this->templateNames as $templateName) { + $environment->parseTemplate($templateName); } } - protected function environmentBuilder(): EnvironmentFactory + private function buildStaticTheme(): void { - return EnvironmentFactory::new() - ->setFilesystem(new LocalFileSystem(__DIR__.'/../tests')) - ->registerTag(CommentFormTag::class) - ->registerTag(PaginateTag::class) - ->registerFilters(CustomFilters::class); + $this->cache->clear(); + $this->compileStaticTheme($this->environment); } - protected function renderTemplates(): void + private function renderCachedTheme(): void { - foreach ($this->templates as $template) { - $this->environment->parseTemplate($template) - ->render($this->environment->newRenderContext(staticData: [...Database::tables()])); + foreach ($this->pageTemplateNames as $templateName) { + StorefrontTheme::renderPage($this->environment, $templateName); } } - public function clearCache(): void + private function newCache(string $backend): LiquidTemplatesCache + { + return match ($backend) { + 'memory' => new MemoryTemplatesCache, + 'serialize' => new SerializeTemplatesCache($this->cachePath('serialize'), keepInMemory: false), + 'var-exporter' => new VarExportTemplatesCache($this->cachePath('var-exporter'), keepInMemory: false), + default => throw new \InvalidArgumentException("Unknown templates cache backend [$backend]."), + }; + } + + private function cachePath(string $backend): string { - $this->environment->templatesCache->clear(); + return $this->cacheDirectory.'/'.$backend; } } diff --git a/performance/benchmarks/ThemeBench.php b/performance/benchmarks/ThemeBench.php new file mode 100644 index 0000000..a327ab8 --- /dev/null +++ b/performance/benchmarks/ThemeBench.php @@ -0,0 +1,87 @@ +<?php + +namespace Keepsuit\Liquid\Performance\benchmarks; + +use Keepsuit\Liquid\Environment; +use Keepsuit\Liquid\Performance\Support\StorefrontTheme; +use PhpBench\Attributes\BeforeMethods; +use PhpBench\Attributes\Groups; +use PhpBench\Attributes\Iterations; +use PhpBench\Attributes\OutputMode; +use PhpBench\Attributes\OutputTimeUnit; +use PhpBench\Attributes\Revs; + +/** + * Whole-pipeline canary for the storefront theme. + * + * This class answers "did rendering get slower", not "what got slower": it + * averages 29 templates across four pages, so it cannot localize a regression. + * Per-feature sensitivity belongs in the operations group. + * + * ponytail: the operations group has not caught up yet, so nothing in the suite + * isolates a single tag, the drop miss path, or superlinear growth. The deferred + * list is in performance/README.md. + */ +#[Groups(['default'])] +#[Iterations(10)] +#[Revs(20)] +#[OutputMode('throughput')] +#[OutputTimeUnit('seconds', precision: 3)] +#[BeforeMethods('setUp')] +class ThemeBench +{ + private Environment $environment; + + /** + * Sources are read up front: reading them inside a benchmark would measure + * the filesystem instead of the tokenizer and the parser. + * + * @var array<string, string> + */ + private array $sources; + + /** @var list<string> */ + private array $pageTemplateNames; + + public function setUp(): void + { + $this->environment = StorefrontTheme::environment(); + $this->sources = []; + + foreach (StorefrontTheme::templateNames() as $name) { + $this->environment->parseTemplate($name); + $this->sources[$name] = StorefrontTheme::templateSource($name); + } + + $this->pageTemplateNames = StorefrontTheme::pageTemplateNames(); + } + + public function benchTokenize(): void + { + foreach ($this->sources as $source) { + $this->environment->newParseContext()->tokenize($source); + } + } + + public function benchParse(): void + { + foreach ($this->sources as $name => $source) { + $this->environment->parseString($source, $name); + } + } + + public function benchRender(): void + { + foreach ($this->pageTemplateNames as $pageTemplateName) { + StorefrontTheme::renderPage($this->environment, $pageTemplateName); + } + } + + public function benchStream(): void + { + foreach ($this->pageTemplateNames as $pageTemplateName) { + foreach (StorefrontTheme::streamPage($this->environment, $pageTemplateName) as $chunk) { + } + } + } +} diff --git a/performance/profile-theme.php b/performance/profile-theme.php new file mode 100644 index 0000000..2afcaa7 --- /dev/null +++ b/performance/profile-theme.php @@ -0,0 +1,60 @@ +<?php + +use Keepsuit\Liquid\Extensions\ProfilerExtension; +use Keepsuit\Liquid\Performance\ProfileReport; +use Keepsuit\Liquid\Performance\Support\StorefrontTheme; +use Keepsuit\Liquid\Profiler\Profiler; + +require dirname(__DIR__).'/vendor/autoload.php'; + +$outputPath = null; +/** @var list<string> $arguments */ +$arguments = $_SERVER['argv'] ?? []; +for ($argumentIndex = 1; $argumentIndex < count($arguments); $argumentIndex++) { + $argument = $arguments[$argumentIndex]; + + if ($argument === '--help') { + fwrite(STDOUT, "Usage: php performance/profile-theme.php [--output=profile.json]\n"); + exit(0); + } + + if ($argument === '--output') { + $outputPath = $arguments[++$argumentIndex] ?? null; + } elseif (str_starts_with($argument, '--output=')) { + $outputPath = substr($argument, strlen('--output=')); + } else { + fwrite(STDERR, "Unknown argument: {$argument}\n"); + exit(1); + } + + if (! is_string($outputPath) || $outputPath === '') { + fwrite(STDERR, "The --output option requires a path.\n"); + exit(1); + } +} + +$environment = StorefrontTheme::environment(); +$environment->addExtension(new ProfilerExtension( + profiler: $profiler = new Profiler, + tags: true, + variables: true, +)); + +foreach (StorefrontTheme::pageTemplateNames() as $templateName) { + StorefrontTheme::renderPage($environment, $templateName); +} + +$report = ProfileReport::fromProfiler($profiler); +$json = json_encode($report, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR).PHP_EOL; + +if ($outputPath === null) { + fwrite(STDOUT, $json); + exit(0); +} + +if (file_put_contents($outputPath, $json) === false) { + fwrite(STDERR, "Could not write profile report to {$outputPath}.\n"); + exit(1); +} + +fwrite(STDOUT, "Profile report written to {$outputPath}\n"); diff --git a/performance/tests/dropify/article.liquid b/performance/tests/dropify/article.liquid deleted file mode 100644 index dca6caa..0000000 --- a/performance/tests/dropify/article.liquid +++ /dev/null @@ -1,74 +0,0 @@ -<div class="article"> - <h2 class="article-title">{{ article.title }}</h2> - <p class="article-details">posted <span class="article-time">{{ article.created_at | date: "%Y %h" }}</span> by <span class="article-author">{{ article.author }}</span></p> - - <div class="article-body textile"> - {{ article.content }} - </div> - -</div> - -<!-- Comments --> -{% if blog.comments_enabled? %} -<div id="comments"> - <h3>Comments</h3> - - <!-- List all comments --> - <ul id="comment-list"> - {% for comment in article.comments %} - <li> - <div class="comment-details"> - <span class="comment-author">{{ comment.author }}</span> said on <span class="comment-date">{{ comment.created_at | date: "%B %d, %Y" }}</span>: - </div> - - <div class="comment"> - {{ comment.content }} - </div> - </li> - {% endfor %} - </ul> - - <!-- Comment Form --> - <div id="comment-form"> - {% form article %} - <h3>Leave a comment</h3> - - <!-- Check if a comment has been submitted in the last request, and if yes display an appropriate message --> - {% if form.posted_successfully? %} - {% if blog.moderated? %} - <div class="notice"> - Successfully posted your comment.<br /> - It will have to be approved by the blog owner first before showing up. - </div> - {% else %} - <div class="notice">Successfully posted your comment.</div> - {% endif %} - {% endif %} - - {% if form.errors %} - <div class="notice error">Not all the fields have been filled out correctly!</div> - {% endif %} - - <dl> - <dt class="{% if form.errors contains 'author' %}error{% endif %}"><label for="comment_author">Your name</label></dt> - <dd><input type="text" id="comment_author" name="comment[author]" size="40" value="{{form.author}}" class="{% if form.errors contains 'author' %}input-error{% endif %}" /></dd> - - <dt class="{% if form.errors contains 'email' %}error{% endif %}"><label for="comment_email">Your email</label></dt> - <dd><input type="text" id="comment_email" name="comment[email]" size="40" value="{{form.email}}" class="{% if form.errors contains 'email' %}input-error{% endif %}" /></dd> - - <dt class="{% if form.errors contains 'body' %}error{% endif %}"><label for="comment_body">Your comment</label></dt> - <dd><textarea id="comment_body" name="comment[body]" cols="40" rows="5" class="{% if form.errors contains 'body' %}input-error{% endif %}">{{form.body}}</textarea></dd> - </dl> - - {% if blog.moderated? %} - <p class="hint">comments have to be approved before showing up</p> - {% endif %} - - <input type="submit" value="Post comment" id="comment-submit" /> - {% endform %} - </div> - <!-- END Comment Form --> - -</div> -{% endif %} -<!-- END Comments --> diff --git a/performance/tests/dropify/blog.liquid b/performance/tests/dropify/blog.liquid deleted file mode 100644 index 5a800c4..0000000 --- a/performance/tests/dropify/blog.liquid +++ /dev/null @@ -1,33 +0,0 @@ -<div id="page"> - <h2>{{page.title}}</h2> - - {% paginate blog.articles by 20 %} - - {% for article in blog.articles %} - - <div class="article"> - <div class="headline"> - <h3 class="title"> - <a href="{{article.url}}">{{ article.title }}</a> - </h3> - <h4 class="date">Posted on {{ article.created_at | date: "%B %d, '%y" }} by {{ article.author }}.</h4> - </div> - - <div class="article-body textile"> - {{ article.content | strip_html | truncate: 250 }} - </div> - - {% if blog.comments_enabled? %} - <p style="text-align: right"><a href="{{article.url}}#comments">{{ article.comments_count }} comments</a></p> - {% endif %} - </div> - - {% endfor %} - - <div id="pagination"> - {{ paginate | default_pagination }} - </div> - - {% endpaginate %} - -</div> diff --git a/performance/tests/dropify/cart.liquid b/performance/tests/dropify/cart.liquid deleted file mode 100644 index 8fe26ab..0000000 --- a/performance/tests/dropify/cart.liquid +++ /dev/null @@ -1,66 +0,0 @@ -<script type="text/javascript"> - function remove_item(id) { - document.getElementById('updates_'+id).value = 0; - document.getElementById('cartform').submit(); - } -</script> - -<div> - - {% if cart.item_count == 0 %} - <h4>Your shopping cart is looking rather empty...</h4> - {% else %} - <form action="/cart" method="post" id="cartform"> - - <div id="cart"> - - <h3>You have {{ cart.item_count }} {{ cart.item_count | pluralize: 'product', 'products' }} in here!</h3> - - <ul id="line-items"> - {% for item in cart.items %} - <li id="item-{{item.id}}" class="clearfix"> - <div class="thumb"> - <div class="prodimage"> - <a href="{{item.product.url}}" title="View {{item.title}} Page"><img src="{{item.product.featured_image | product_img_url: 'thumb' }}" alt="{{item.title | escape }}" /></a> - </div></div> - <h3 style="padding-right: 150px"> - <a href="{{item.product.url}}" title="View {{item.title | escape }} Page"> - {{ item.title }} - {% if item.variant.available == true %} - ({{item.variant.title}}) - {% endif %} - </a> - </h3> - <small class="itemcost">Costs {{ item.price | money }} each, <span class="money">{{item.line_price | money }}</span> total.</small> - <p class="right"> - <label for="updates">How many? </label> - <input type="text" size="4" name="updates[{{item.variant.id}}]" id="updates_{{item.variant.id}}" value="{{item.quantity}}" onfocus="this.select();"/><br /> - <a href="#" onclick="remove_item({{item.variant.id}}); return false;" class="remove"><img style="padding:15px 0 0 0;margin:0;" src="{{ 'delete.gif' | asset_url }}" /></a> - </p> - </li> - {% endfor %} - <li id="total"> - <input type="image" id="update-cart" name="update" value="Update My Cart" src="{{ 'update.gif' | asset_url }}" /> - Subtotal: - <span class="money">{{ cart.total_price | money_with_currency }}</span> - </li> - </ul> - - </div> - - <div class="info"> - <input type="image" value="Checkout!" name="checkout" src="{{ 'checkout.gif' | asset_url }}" /> - </div> - - {% if additional_checkout_buttons %} - <div class="additional-checkout-buttons"> - <p>- or -</p> - {{ content_for_additional_checkout_buttons }} - </div> - {% endif %} - - </form> - - {% endif %} - -</div> diff --git a/performance/tests/dropify/collection.liquid b/performance/tests/dropify/collection.liquid deleted file mode 100644 index ad1427c..0000000 --- a/performance/tests/dropify/collection.liquid +++ /dev/null @@ -1,22 +0,0 @@ -{% paginate collection.products by 20 %} - -<ul id="product-collection"> - {% for product in collection.products %} - <li class="singleproduct clearfix"> - <div class="small"> - <div class="prodimage"><a href="{{product.url}}"><img src="{{ product.featured_image | product_img_url: 'small' }}" /></a></div> - </div> - <div class="description"> - <h3><a href="{{product.url}}">{{product.title}}</a></h3> - <p>{{ product.description | strip_html | truncatewords: 35 }}</p> - <p class="money">{{ product.price_min | money }}{% if product.price_varies %} - {{ product.price_max | money }}{% endif %}</p> - </div> - </li> - {% endfor %} -</ul> - -<div id="pagination"> - {{ paginate | default_pagination }} -</div> - -{% endpaginate %} diff --git a/performance/tests/dropify/index.liquid b/performance/tests/dropify/index.liquid deleted file mode 100644 index f1ea137..0000000 --- a/performance/tests/dropify/index.liquid +++ /dev/null @@ -1,47 +0,0 @@ -<div id="frontproducts"><div id="frontproducts-top"><div id="frontproducts-bottom"> -<h2 style="display: none;">Featured Items</h2> -{% for product in collections.frontpage.products limit:1 offset:0 %} - <div class="productmain"> - <a href="{{ product.url }}"><img src="{{ product.featured_image | product_img_url: 'small' }}" alt="{{ product.title | escape }}" /></a> - <h3><a href="{{ product.url }}">{{ product.title }}</a></h3> - <div class="description">{{ product.description | strip_html | truncatewords: 18 }}</div> - <p class="money">{{ product.price_min | money }}</p> - </div> -{% endfor %} -{% for product in collections.frontpage.products offset:1 %} - <div class="product"> - <a href="{{ product.url }}"><img src="{{ product.featured_image | product_img_url: 'thumb' }}" alt="{{ product.title | escape }}" /></a> - <h3><a href="{{ product.url }}">{{ product.title }}</a></h3> - <p class="money">{{ product.price_min | money }}</p> - </div> -{% endfor %} -</div></div></div> - -<div id="mainarticle"> - {% assign article = pages.frontpage %} - - {% if article.content != "" %} - <h2>{{ article.title }}</h2> - <div class="article-body textile"> - {{ article.content }} - </div> - {% else %} - <div class="article-body textile"> - In <em>Admin > Blogs & Pages</em>, create a page with the handle <strong><code>frontpage</code></strong> and it will show up here.<br /> - {{ "Learn more about handles" | link_to: "http://wiki.shopify.com/Handle" }} - </div> - {% endif %} - -</div> -<br style="clear: both;" /> -<div id="articles"> - {% for article in blogs.news.articles offset:1 %} - <div class="article"> - <h2>{{ article.title }}</h2> - <div class="article-body textile"> - {{ article.content }} - </div> - </div> - {% endfor %} -</div> - diff --git a/performance/tests/dropify/page.liquid b/performance/tests/dropify/page.liquid deleted file mode 100644 index 2ccbc63..0000000 --- a/performance/tests/dropify/page.liquid +++ /dev/null @@ -1,8 +0,0 @@ -<div id="page"> - <h2>{{page.title}}</h2> - - <div class="article textile"> - {{page.content}} - </div> - -</div> diff --git a/performance/tests/dropify/product.liquid b/performance/tests/dropify/product.liquid deleted file mode 100644 index 8870389..0000000 --- a/performance/tests/dropify/product.liquid +++ /dev/null @@ -1,68 +0,0 @@ -<div id="productpage"> - - <div id="productimages"><div id="productimages-top"><div id="productimages-bottom"> - {% for image in product.images %} - {% if forloop.first %} - <a href="{{ image | product_img_url: 'large' }}" class="productimage" rel="lightbox"> - <img src="{{ image | product_img_url: 'medium'}}" alt="{{product.title | escape }}" /> - </a> - {% else %} - <a href="{{ image | product_img_url: 'large' }}" class="productimage-small" rel="lightbox"> - <img src="{{ image | product_img_url: 'small'}}" alt="{{product.title | escape }}" /> - </a> - {% endif %} - {% endfor %} - </div></div></div> - - <h2>{{ product.title }}</h2> - - <ul id="details" class="hlist"> - <li>Vendor: {{ product.vendor | link_to_vendor }}</li> - <li>Type: {{ product.type | link_to_type }}</li> - </ul> - - <small>{{ product.price_min | money }}{% if product.price_varies %} - {{ product.price_max | money }}{% endif %}</small> - - <div id="variant-add"> - <form action="/cart/add" method="post"> - - <select id="variant-select" name="id" class="product-info-options"> - {% for variant in product.variants %} - <option value="{{ variant.id }}">{{ variant.title }} - {{ variant.price | money }}</option> - {% endfor %} - </select> - - <div id="price-field" class="price"></div> - - <div style="text-align:center;"><input type="image" name="add" value="Add to Cart" id="add" src="{{ 'addtocart.gif' | asset_url }}" /></div> - </form> - </div> - - <div class="description textile"> - {{ product.description }} - </div> -</div> - -<script type="text/javascript"> -<!-- - // prototype callback for multi variants dropdown selector - var selectCallback = function(variant, selector) { - if (variant && variant.available == true) { - // selected a valid variant - $('add').removeClassName('disabled'); // remove unavailable class from add-to-cart button - $('add').disabled = false; // reenable add-to-cart button - $('price-field').innerHTML = Shopify.formatMoney(variant.price, "{{shop.money_with_currency_format}}"); // update price field - } else { - // variant doesn't exist - $('add').addClassName('disabled'); // set add-to-cart button to unavailable class - $('add').disabled = true; // disable add-to-cart button - $('price-field').innerHTML = (variant) ? "Sold Out" : "Unavailable"; // update price-field message - } - }; - - // initialize multi selector for product - Event.observe(document, 'dom:loaded', function() { - new Shopify.OptionSelectors("variant-select", { product: {{ product | json }}, onVariantSelected: selectCallback }); - }); ---> -</script> diff --git a/performance/tests/dropify/theme.liquid b/performance/tests/dropify/theme.liquid deleted file mode 100644 index aca9206..0000000 --- a/performance/tests/dropify/theme.liquid +++ /dev/null @@ -1,105 +0,0 @@ -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" - "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> - -<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> -<head> - <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> - <title>{{shop.name}} - {{page_title}} - - {{ 'textile.css' | global_asset_url | stylesheet_tag }} - {{ 'lightbox/v204/lightbox.css' | global_asset_url | stylesheet_tag }} - - {{ 'prototype/1.6/prototype.js' | global_asset_url | script_tag }} - {{ 'scriptaculous/1.8.2/scriptaculous.js' | global_asset_url | script_tag }} - {{ 'lightbox/v204/lightbox.js' | global_asset_url | script_tag }} - {{ 'option_selection.js' | shopify_asset_url | script_tag }} - - {{ 'layout.css' | asset_url | stylesheet_tag }} - {{ 'shop.js' | asset_url | script_tag }} - - {{ content_for_header }} - - - - -

Skip to navigation.

- - {% if cart.item_count > 0 %} - - {% endif %} - -
- -
-
- -
-
- {{ content_for_layout }} -
-
- -
-
- - - - {% if tags %} - - {% endif %} - - - -
- -

- - - -
-
- -
- - - - - diff --git a/performance/tests/ripen/article.liquid b/performance/tests/ripen/article.liquid deleted file mode 100644 index e1b7e71..0000000 --- a/performance/tests/ripen/article.liquid +++ /dev/null @@ -1,74 +0,0 @@ -
-

{{ article.title }}

-

posted {{ article.created_at | date: "%Y %h" }} by

- -
- {{ article.content }} -
- -
- - -{% if blog.comments_enabled? %} -
-

Comments

- - -
    - {% for comment in article.comments %} -
  • -
    - {{ comment.content }} -
    - -
    - Posted by {{ comment.author }} on {{ comment.created_at | date: "%B %d, %Y" }} -
    -
  • - {% endfor %} -
- - -
- {% form article %} -

Leave a comment

- - - {% if form.posted_successfully? %} - {% if blog.moderated? %} -
- Successfully posted your comment.
- It will have to be approved by the blog owner first before showing up. -
- {% else %} -
Successfully posted your comment.
- {% endif %} - {% endif %} - - {% if form.errors %} -
Not all the fields have been filled out correctly!
- {% endif %} - -
-
-
- -
-
- -
-
-
- - {% if blog.moderated? %} -

comments have to be approved before showing up

- {% endif %} - - - {% endform %} -
- - -
-{% endif %} - diff --git a/performance/tests/ripen/blog.liquid b/performance/tests/ripen/blog.liquid deleted file mode 100644 index 91b71ba..0000000 --- a/performance/tests/ripen/blog.liquid +++ /dev/null @@ -1,13 +0,0 @@ -
-

{{page.title}}

- {% for article in blog.articles %} -

- {{ article.created_at | date: '%d %b' }} - {{ article.title }} -

- {{ article.content }} - {% if blog.comments_enabled? %} -

{{ article.comments_count }} comments

- {% endif %} - {% endfor %} -
diff --git a/performance/tests/ripen/cart.liquid b/performance/tests/ripen/cart.liquid deleted file mode 100644 index db7a221..0000000 --- a/performance/tests/ripen/cart.liquid +++ /dev/null @@ -1,54 +0,0 @@ - - -
- - {% if cart.item_count == 0 %} -

Your shopping cart is empty...

-

Continue shopping

- {% else %} - -

- - - - - - - - - - - {% for item in cart.items %} - - - - - - - - - {% endfor %} -
ProductQtyPriceTotalRemove
{{ item.product.featured_image | product_img_url: 'thumb' | img_tag }}{{ item.title }}{{ item.price | money }}{{item.line_price | money }}Remove
-

-

- Subtotal: {{cart.total_price | money_with_currency }} -

-

- - {% if additional_checkout_buttons %} -
-

- or -

- {{ content_for_additional_checkout_buttons }} -
- {% endif %} - -
- - {% endif %} - -
diff --git a/performance/tests/ripen/collection.liquid b/performance/tests/ripen/collection.liquid deleted file mode 100644 index 31151f3..0000000 --- a/performance/tests/ripen/collection.liquid +++ /dev/null @@ -1,29 +0,0 @@ -
- -{% if collection.description %} -
{{ collection.description }}
-{% endif %} - -{% paginate collection.products by 20 %} - -
    - {% for product in collection.products %} -
  • -
    -
    {{ product.title | escape }}
    -
    -
    -

    {{product.title}}

    -

    {{ product.description | strip_html | truncatewords: 35 }}

    -

    {{ product.price_min | money }}{% if product.price_varies %} - {{ product.price_max | money }}{% endif %}

    -
    -
  • - {% endfor %} -
- - - -{% endpaginate %} -
diff --git a/performance/tests/ripen/index.liquid b/performance/tests/ripen/index.liquid deleted file mode 100644 index 0e8ad56..0000000 --- a/performance/tests/ripen/index.liquid +++ /dev/null @@ -1,32 +0,0 @@ -
-

Featured products...

- - -
- {% assign article = pages.frontpage %} - {% if article.content != "" %} -

{{ article.title }}

- {{ article.content }} - {% else %} - In Admin > Blogs & Pages, create a page with the handle frontpage and it will show up here.
- {{ "Learn more about handles" | link_to: "http://wiki.shopify.com/Handle" }} - {% endif %} -
-
diff --git a/performance/tests/ripen/page.liquid b/performance/tests/ripen/page.liquid deleted file mode 100644 index b3e9702..0000000 --- a/performance/tests/ripen/page.liquid +++ /dev/null @@ -1,4 +0,0 @@ -
-

{{page.title}}

- {{ page.content }} -
diff --git a/performance/tests/ripen/product.liquid b/performance/tests/ripen/product.liquid deleted file mode 100644 index 9a97045..0000000 --- a/performance/tests/ripen/product.liquid +++ /dev/null @@ -1,75 +0,0 @@ -
-

{{ product.title }}

-
-
- {% for image in product.images %} - {% if forloop.first %} - - {{product.title | escape }} - - {% else %} - - {{product.title | escape }} - - {% endif %} - {% endfor %} -
- -
    -
  • Vendor: {{ product.vendor | link_to_vendor }}
  • -
  • Type: {{ product.type | link_to_type }}
  • -
- - {{ product.price_min | money }}{% if product.price_varies %} - {{ product.price_max | money }}{% endif %} - -
- {% if product.available %} - -
- - - -
- -
-
- {% else %} - Sold Out! - {% endif %} -
- -
- {{ product.description }} -
-
-
- - - - diff --git a/performance/tests/ripen/theme.liquid b/performance/tests/ripen/theme.liquid deleted file mode 100644 index 72c2992..0000000 --- a/performance/tests/ripen/theme.liquid +++ /dev/null @@ -1,85 +0,0 @@ - - - - {{shop.name}} - {{page_title}} - - - {{ 'main.css' | asset_url | stylesheet_tag }} - {{ 'shop.js' | asset_url | script_tag }} - - {{ 'mootools.js' | asset_url | script_tag }} - {{ 'slimbox.js' | asset_url | script_tag }} - {{ 'option_selection.js' | shopify_asset_url | script_tag }} - {{ 'slimbox.css' | asset_url | stylesheet_tag }} - - {{ content_for_header }} - - - -

Skip to navigation.

-
-
- -
- {{ content_for_layout }} -
-
- {% if template != 'cart' %} -
-
-
Shopping Cart
-
- {% if cart.item_count != 0 %} - {{ cart.item_count }} {{ cart.item_count | pluralize: 'item', 'items' }} in your cart - {% else %} - Your cart is empty - {% endif %} -
-
-
- {% endif %} - - -
- -
-
-
- - - - - diff --git a/performance/tests/tribble/404.liquid b/performance/tests/tribble/404.liquid deleted file mode 100644 index acf184c..0000000 --- a/performance/tests/tribble/404.liquid +++ /dev/null @@ -1,56 +0,0 @@ -
- -
-
-

Oh no!

-
- Seems like you are looking for something that just isn't here. Try heading back to our main page. Or you can checkout some of our featured products below. -
-
-
- - -

Featured Products

-
    - - {% for product in collections.frontpage.products %} -
  • -
    -
    -
    -
    -

    {{product.title}}

    -

    {{ product.description | truncatewords: 15 }}

    -
    - {{ product.title | escape }} -
    - -
    - - -

    - View Details - - - {% if product.compare_at_price %} - {% if product.price_min != product.compare_at_price %} - {{product.compare_at_price | money}} - - {% endif %} - {% endif %} - - {{product.price_min | money}} - - -

    -
    -
    -
    -
  • - {% endfor %} - -
-
- - - - diff --git a/performance/tests/tribble/article.liquid b/performance/tests/tribble/article.liquid deleted file mode 100644 index cb2453a..0000000 --- a/performance/tests/tribble/article.liquid +++ /dev/null @@ -1,98 +0,0 @@ - -
-
- -
-

{{article.title}}

-
-
{{ article.created_at | date: "%b %d" }}
- {{ article.content }} -
- - -{% if blog.comments_enabled? %} -
-

Comments

- - -
    - {% for comment in article.comments %} -
  • -
    - {{ comment.content }} -
    - -
    - Posted by {{ comment.author }} on {{ comment.created_at | date: "%B %d, %Y" }} -
    -
  • - {% endfor %} -
- - -
- {% form article %} -

Leave a comment

- - - {% if form.posted_successfully? %} - {% if blog.moderated? %} -
- Successfully posted your comment.
- It will have to be approved by the blog owner first before showing up. -
- {% else %} -
Successfully posted your comment.
- {% endif %} - {% endif %} - - {% if form.errors %} -
Not all the fields have been filled out correctly!
- {% endif %} - -
-
-
- -
-
- -
-
-
- - {% if blog.moderated? %} -

comments have to be approved before showing up

- {% endif %} - - - {% endform %} -
- - -
-{% endif %} - - - -
-
- -
-

Why Shop With Us?

-
    -
  • -

    24 Hours

    -

    We're always here to help.

    -
  • -
  • -

    No Spam

    -

    We'll never share your info.

    -
  • -
  • -

    Secure Servers

    -

    Checkout is 256bit encrypted.

    -
  • -
-
-
diff --git a/performance/tests/tribble/blog.liquid b/performance/tests/tribble/blog.liquid deleted file mode 100644 index ba9f52a..0000000 --- a/performance/tests/tribble/blog.liquid +++ /dev/null @@ -1,41 +0,0 @@ -
-
-

Post from our blog...

- {% paginate blog.articles by 20 %} - {% for article in blog.articles %} - -
-

{{ article.title }}

-
-
{{ article.created_at | date: "%b %d" }}
- {{ article.content }} -
-
- - {% endfor %} - -
- {{ paginate | default_pagination }} -
- - {% endpaginate %} -
- -
-

Why Shop With Us?

-
    -
  • -

    24 Hours

    -

    We're always here to help.

    -
  • -
  • -

    No Spam

    -

    We'll never share your info.

    -
  • -
  • -

    Secure Servers

    -

    Checkout is 256bit encrypted.

    -
  • -
-
-
diff --git a/performance/tests/tribble/cart.liquid b/performance/tests/tribble/cart.liquid deleted file mode 100644 index bdc5e83..0000000 --- a/performance/tests/tribble/cart.liquid +++ /dev/null @@ -1,134 +0,0 @@ - - -
. - {% if cart.item_count == 0 %} -

Your cart is currently empty.

- {% else %} - -

Your Cart ({{ cart.item_count }} {{ cart.item_count | pluralize: 'item', 'items' }}, {{cart.total_price | money_with_currency }} total)

- -
- -
- - - - - - - - - - {% for item in cart.items %} - - - - {% endfor %} -
- - - - - - - - -
{{ item.product.featured_image | product_img_url: 'thumb' | img_tag }}

{{ item.title }}

{{item.line_price | money }}Remove
-
- -
- -
- {{ pages.shopping-cart.content }} -
- -

- Order Total: {{cart.total_price | money_with_currency }} -

- -

- -

- - {% if additional_checkout_buttons %} -
-

- or -

- {{ content_for_additional_checkout_buttons }} -
- {% endif %} - -
- -
- -
- - {% endif %} - - - -

Other Products You Might Enjoy

-
    - - {% for product in collections.frontpage.products limit:2 %} -
  • -
    -
    -
    -
    -

    {{product.title}}

    -

    {{ product.description | truncatewords: 15 }}

    -
    - {{ product.title | escape }} -
    - -
    - - -

    - View Details - - - {% if product.compare_at_price %} - {% if product.price_min != product.compare_at_price %} - {{product.compare_at_price | money}} - - {% endif %} - {% endif %} - - {{product.price_min | money}} - - -

    -
    -
    -
    -
  • - {% endfor %} - -
- -
-

Why Shop With Us?

-
    -
  • -

    24 Hours

    -

    We're always here to help.

    -
  • -
  • -

    No Spam

    -

    We'll never share your info.

    -
  • -
  • -

    Secure Servers

    -

    Checkout is 256bit encrypted.

    -
  • -
-
- -
- diff --git a/performance/tests/tribble/collection.liquid b/performance/tests/tribble/collection.liquid deleted file mode 100644 index fd2fd01..0000000 --- a/performance/tests/tribble/collection.liquid +++ /dev/null @@ -1,70 +0,0 @@ -
-

{{ collection.title }}

- {% if collection.description.size > 0 %} -
{{ collection.description }}
- {% endif %} - - {% paginate collection.products by 8 %} - -
    - {% for product in collection.products %} -
  • -
    -
    -
    -
    -

    {{product.title}}

    -

    {{ product.description | truncatewords: 15 }}

    -
    - {{ product.title | escape }} -
    - -
    - - -

    - View Details - - - {% if product.compare_at_price %} - {% if product.price_min != product.compare_at_price %} - {{product.compare_at_price | money}} - - {% endif %} - {% endif %} - - {{product.price_min | money}} - - -

    -
    -
    -
    -
  • - {% endfor %} -
- -
- {{ paginate | default_pagination }} -
- - -
-

Why Shop With Us?

-
    -
  • -

    24 Hours

    -

    We're always here to help.

    -
  • -
  • -

    No Spam

    -

    We'll never share your info.

    -
  • -
  • -

    Secure Servers

    -

    Checkout is 256bit encrypted.

    -
  • -
-
-
- -{% endpaginate %} diff --git a/performance/tests/tribble/index.liquid b/performance/tests/tribble/index.liquid deleted file mode 100644 index 551cf7c..0000000 --- a/performance/tests/tribble/index.liquid +++ /dev/null @@ -1,94 +0,0 @@ -
-
-

Three Great Reasons You Should Shop With Us...

-
    -
  • -

    Free Shipping

    -

    On all orders over $25

    -
  • -
  • -

    Top Quality

    -

    Hand made in our shop

    -
  • -
  • -

    100% Guarantee

    -

    Any time, any reason

    -
  • -
-
-
- -
- -
{{pages.alert.content}}
- -
    - - {% for product in collections.frontpage.products %} -
  • -
    -
    -
    -
    -

    {{product.title}}

    - {{ product.description | truncatewords: 15 }}

    -
    - {{ product.title | escape }} -
    - -
    - - -

    - View Details - - - {% if product.compare_at_price %} - {% if product.price_min != product.compare_at_price %} - {{product.compare_at_price | money}} - - {% endif %} - {% endif %} - - {{product.price_min | money}} - - -

    -
    -
    -
    -
  • - {% endfor %} - -
- -
-
-

Why Shop With Us?

-
    -
  • -

    24 Hours

    -

    We're always here to help.

    -
  • -
  • -

    No Spam

    -

    We'll never share your info.

    -
  • -
  • -

    Save Energy

    -

    We're green, all the way.

    -
  • -
  • -

    Secure Servers

    -

    Checkout is 256bits encrypted.

    -
  • -
-
- -
-

Our Company

- {{pages.about-us.content | truncatewords: 49}} read more

-
-
- -
- diff --git a/performance/tests/tribble/page.liquid b/performance/tests/tribble/page.liquid deleted file mode 100644 index fa01097..0000000 --- a/performance/tests/tribble/page.liquid +++ /dev/null @@ -1,56 +0,0 @@ -
- -
-
-

{{page.title}}

-
- {{page.content}} -
-
-
- - -

Featured Products

-
    - - {% for product in collections.frontpage.products %} -
  • -
    -
    -
    -
    -

    {{product.title}}

    -

    {{ product.description | truncatewords: 15 }}

    -
    - {{ product.title | escape }} -
    - -
    - - -

    - View Details - - - {% if product.compare_at_price %} - {% if product.price_min != product.compare_at_price %} - {{product.compare_at_price | money}} - - {% endif %} - {% endif %} - - {{product.price_min | money}} - - -

    -
    -
    -
    -
  • - {% endfor %} - -
-
- - - - diff --git a/performance/tests/tribble/product.liquid b/performance/tests/tribble/product.liquid deleted file mode 100644 index f0a1bf6..0000000 --- a/performance/tests/tribble/product.liquid +++ /dev/null @@ -1,116 +0,0 @@ -
-

{{ collection.title }} {{ product.title }}

- - -

Product Tags: - {% for tag in product.tags %} - {{ tag }} | - {% endfor %} -

- -
-
-

{{ product.title }}

-
-

{{ product.description }}

-
- - {% if product.available %} -
- -

Product Options:

- - - -
- -
- -
- -
- {% else %} -

Sold out!

-

Sorry, we're all out of this product. Check back often and order when it returns

- {% endif %} -
- -
- {% for image in product.images %} - - {% if forloop.first %} -
- {{product.title | escape }} -
- {% else %} - {% endif %} - {% endfor %} - -
    - {% for image in product.images %} - {% if forloop.first %} - {% else %} - -
  • - - {{product.title | escape }} - -
  • - {% endif %} - {% endfor %} -
-
-
- - - -
-

Why Shop With Us?

-
    -
  • -

    24 Hours

    -

    We're always here to help.

    -
  • -
  • -

    No Spam

    -

    We'll never share your info.

    -
  • -
  • -

    Secure Servers

    -

    Checkout is 256bit encrypted.

    -
  • -
-
- -
- - - - - diff --git a/performance/tests/tribble/search.liquid b/performance/tests/tribble/search.liquid deleted file mode 100644 index f509c74..0000000 --- a/performance/tests/tribble/search.liquid +++ /dev/null @@ -1,51 +0,0 @@ - - - - -
-

Search Results

- {% if search.performed %} - - {% paginate search.results by 10 %} - - {% if search.results == empty %} -
Your search for "{{search.terms | escape}}" did not yield any results
- {% else %} - - -
    - {% for item in search.results %} -
  • -

    {{ item.title | link_to: item.url }}

    -

    {{ item.content | strip_html | truncatewords: 65 | highlight: search.terms }} ... view this item

    -
  • - {% endfor %} -
- {% endif %} - -
- {{ paginate | default_pagination }} -
- - -
-

Why Shop With Us?

-
    -
  • -

    24 Hours

    -

    We're always here to help.

    -
  • -
  • -

    No Spam

    -

    We'll never share your info.

    -
  • -
  • -

    Secure Servers

    -

    Checkout is 256bit encrypted.

    -
  • -
-
-
- -{% endpaginate %} -{% endif %} diff --git a/performance/tests/tribble/theme.liquid b/performance/tests/tribble/theme.liquid deleted file mode 100644 index c353c86..0000000 --- a/performance/tests/tribble/theme.liquid +++ /dev/null @@ -1,90 +0,0 @@ - - - - {{shop.name}} - {{page_title}} - - - {{ 'reset.css' | asset_url | stylesheet_tag }} - {{ 'style.css' | asset_url | stylesheet_tag }} - - {{ 'lightbox.css' | asset_url | stylesheet_tag }} - {{ 'prototype/1.6/prototype.js' | global_asset_url | script_tag }} - {{ 'scriptaculous/1.8.2/scriptaculous.js' | global_asset_url | script_tag }} - {{ 'lightbox.js' | asset_url | script_tag }} - {{ 'option_selection.js' | shopify_asset_url | script_tag }} - - {{ content_for_header }} - - - -
- -
-
-

Shopping Cart

-

- {% if cart.item_count == 0 %} - Your cart is currently empty - {% else %} - {{ cart.item_count }} {{ cart.item_count | pluralize: 'item', 'items' }} - Total: {{cart.total_price | money_with_currency }} - View Cart - {% endif %} -

-
- -
-

{{shop.name}}

-

Tribble: A Shopify Theme

- -
-
- - - - {{ content_for_layout }} - - - -
- - - diff --git a/performance/tests/vogue/article.liquid b/performance/tests/vogue/article.liquid deleted file mode 100644 index a7196fd..0000000 --- a/performance/tests/vogue/article.liquid +++ /dev/null @@ -1,66 +0,0 @@ -
-

{{ article.title }}

-

posted {{ article.created_at | date: "%Y %h" }} by {{ article.author }}

-
- {{ article.content }} -
-
- -{% if blog.comments_enabled? %} -
-

Comments

- - -
    - {% for comment in article.comments %} -
  • -
    - {{ comment.content }} -
    - -
    - Posted by {{ comment.author }} on {{ comment.created_at | date: "%B %d, %Y" }} -
    -
  • - {% endfor %} -
- - - {% form article %} -

Leave a comment

- - - {% if form.posted_successfully? %} - {% if blog.moderated? %} -
- Successfully posted your comment.
- It will have to be approved by the blog owner first before showing up. -
- {% else %} -
Successfully posted your comment.
- {% endif %} - {% endif %} - - {% if form.errors %} -
Not all the fields have been filled out correctly!
- {% endif %} - -
-
-
- -
-
- -
-
-
- - {% if blog.moderated? %} -

comments have to be approved before showing up

- {% endif %} - - - {% endform %} -
-{% endif %} diff --git a/performance/tests/vogue/blog.liquid b/performance/tests/vogue/blog.liquid deleted file mode 100644 index 8ce5583..0000000 --- a/performance/tests/vogue/blog.liquid +++ /dev/null @@ -1,32 +0,0 @@ -
-

{{page.title}}

- -{% paginate blog.articles by 20 %} - - {% for article in blog.articles %} -
-

- {{ article.title }} -

- -

- {% if blog.comments_enabled? %} - {{ article.comments_count }} comments - — - {% endif %} - posted {{ article.created_at | date: "%Y %h" }} by {{ article.author }}

-
- {{ article.content }} -
-
- {% endfor %} - - - -{% endpaginate %} - - -
-
diff --git a/performance/tests/vogue/cart.liquid b/performance/tests/vogue/cart.liquid deleted file mode 100644 index b997bd8..0000000 --- a/performance/tests/vogue/cart.liquid +++ /dev/null @@ -1,58 +0,0 @@ -

Shopping Cart

-{% if cart.item_count == 0 %} -

Your shopping basket is empty. Perhaps a featured item below is of interest...

- - {% tablerow product in collections.frontpage.products cols: 3 limit: 12 %} - - - {% endtablerow %} - -{% else %} - -
- - - - - - - - {% for item in cart.items %} - - - - - - - {% endfor %} -
Item DescriptionPriceQtyDeleteTotal
-
- {{ item.title | escape }} -
-
-

{{ item.title }}

- {{ item.product.description | strip_html | truncate: 120 }} -
-
{{ item.price | money }}{% if item.variant.compare_at_price > item.price %}
{{ item.variant.compare_at_price | money }}{% endif %}
Remove{{ item.line_price | money }}
-
-

Subtotal {{ cart.total_price | money }}

- - - {% if additional_checkout_buttons %} -
-

- or -

- {{ content_for_additional_checkout_buttons }} -
- {% endif %} -
-
{% endif %} diff --git a/performance/tests/vogue/collection.liquid b/performance/tests/vogue/collection.liquid deleted file mode 100644 index e1fa4c0..0000000 --- a/performance/tests/vogue/collection.liquid +++ /dev/null @@ -1,19 +0,0 @@ -{% paginate collection.products by 12 %}{% if collection.products.size == 0 %} - No products found in this collection.{% else %} -

{{ collection.title }}

- {{ collection.description }} - - {% tablerow product in collection.products cols: 3 %} - - - {% endtablerow %} - {% if paginate.pages > 1 %} -
- {{ paginate | default_pagination }} -
{% endif %}{% endif %} -{% endpaginate %} diff --git a/performance/tests/vogue/index.liquid b/performance/tests/vogue/index.liquid deleted file mode 100644 index 1cfc291..0000000 --- a/performance/tests/vogue/index.liquid +++ /dev/null @@ -1,22 +0,0 @@ -
- {% assign article = pages.frontpage %} - {% if article.content != "" %} -

{{ article.title }}

- {{ article.content }} - {% else %} - In Admin > Blogs & Pages, create a page with the handle frontpage and it will show up here.
- {{ "Learn more about handles" | link_to: "http://wiki.shopify.com/Handle" }} - {% endif %} -
- - - {% tablerow product in collections.frontpage.products cols: 3 limit: 12 %} - - - {% endtablerow %} - diff --git a/performance/tests/vogue/page.liquid b/performance/tests/vogue/page.liquid deleted file mode 100644 index 1ea6661..0000000 --- a/performance/tests/vogue/page.liquid +++ /dev/null @@ -1,3 +0,0 @@ -

{{ page.title }}

- {{ page.content }} - diff --git a/performance/tests/vogue/product.liquid b/performance/tests/vogue/product.liquid deleted file mode 100644 index a9a0ddc..0000000 --- a/performance/tests/vogue/product.liquid +++ /dev/null @@ -1,62 +0,0 @@ -
- {% for image in product.images %}{% if forloop.first %}
- {{ product.title | escape }} -
{% else %} -
- {{ product.title | escape }} -
{% endif %}{% endfor %} -
-
-

{{ product.title }}

- {{ product.description }} - - {% if product.available %} -
- -
-
- - -
- - -
- {% else %} -

This product is temporarily unavailable

- {% endif %} - -
- Continue Shopping
- Browse more {{ product.type | link_to_type }} or additional {{ product.vendor | link_to_vendor }} products. -
-
- - - - diff --git a/performance/tests/vogue/theme.liquid b/performance/tests/vogue/theme.liquid deleted file mode 100644 index 914c03f..0000000 --- a/performance/tests/vogue/theme.liquid +++ /dev/null @@ -1,122 +0,0 @@ - - - - -{{ shop.name }} — {{ page_title }} - - -{{ 'stylesheet.css' | asset_url | stylesheet_tag }} - - - -{{ 'mootools.js' | global_asset_url | script_tag }} -{{ 'slimbox.js' | global_asset_url | script_tag }} -{{ 'option_selection.js' | shopify_asset_url | script_tag }} - -{{ content_for_header }} - - - - - - - - -
-
-
{% if template == "search" %} -

Search Results

{% endif %} - {{ content_for_layout }} -
{% if template != "cart" %}{% if template != "product" %} - -
- {% if template == "index" %} - {% if blogs.news.articles.size > 1 %} - Subscribe -

More news

- - {% endif %} - {% endif %} - - {% if template == "collection" %} -

Collection Tags

-
{% if collection.tags.size == 0 %} - No tags found.{% else %} - {% for tag in collection.tags %}{% if current_tags contains tag %} {{ tag | highlight_active_tag | link_to_remove_tag: tag }}{% else %} {{ tag | highlight_active_tag | link_to_add_tag: tag }}{% endif %}{% unless forloop.last %}, {% endunless %}{% endfor %}{% endif %} -
- {% endif %} - -

Navigation

- - - {% if template != "page" %} -

Featured Products

- - {% endif %} -
{% endif %}{% endif %} -
-
- - - - - diff --git a/performance/themes/storefront/layout/theme.liquid b/performance/themes/storefront/layout/theme.liquid new file mode 100644 index 0000000..3b47f5f --- /dev/null +++ b/performance/themes/storefront/layout/theme.liquid @@ -0,0 +1,14 @@ + + + + {%- render "snippets.shared.meta_tags", shop: shop, page_title: page_title, template: template -%} + + + + {% render "snippets.shared.site_header", shop: shop, cart: cart, links: linklists.main_menu %} +
+ {{ content_for_layout }} +
+ {% render "snippets.shared.site_footer", shop: shop, links: linklists.footer %} + + diff --git a/performance/themes/storefront/snippets/collection/filters.liquid b/performance/themes/storefront/snippets/collection/filters.liquid new file mode 100644 index 0000000..661cebf --- /dev/null +++ b/performance/themes/storefront/snippets/collection/filters.liquid @@ -0,0 +1,16 @@ +{%- assign vendors = collection.products | map: 'vendor' | uniq | sort -%} + diff --git a/performance/themes/storefront/snippets/collection/grid.liquid b/performance/themes/storefront/snippets/collection/grid.liquid new file mode 100644 index 0000000..d0f167e --- /dev/null +++ b/performance/themes/storefront/snippets/collection/grid.liquid @@ -0,0 +1,9 @@ +
+ {%- for product in collection.products %} +
+ {%- render "snippets.product.card", product: product, position: forloop.index, label: collection.label -%} +
+ {%- else %} +

Nothing in this collection yet.

+ {%- endfor %} +
diff --git a/performance/themes/storefront/snippets/collection/header.liquid b/performance/themes/storefront/snippets/collection/header.liquid new file mode 100644 index 0000000..84dce94 --- /dev/null +++ b/performance/themes/storefront/snippets/collection/header.liquid @@ -0,0 +1,9 @@ +
+

{{ collection.label | upcase }}

+

{{ collection.title }}

+

{{ collection.description }}

+
+ {{ collection.image.alt | escape }} +
+ {%- render "snippets.shared.button", label: "Shop all", url: collection.url, icon: "arrow", tone: "primary" -%} +
diff --git a/performance/themes/storefront/snippets/collection/toolbar.liquid b/performance/themes/storefront/snippets/collection/toolbar.liquid new file mode 100644 index 0000000..1c1a516 --- /dev/null +++ b/performance/themes/storefront/snippets/collection/toolbar.liquid @@ -0,0 +1,16 @@ +{%- assign sort_options = 'featured,created-descending,price-ascending' | split: ',' -%} +
+

{{ visible_count }} {{ visible_count | pluralize: 'product', 'products' }}, up to {{ products_per_page }} per page

+
    + {%- for option in sort_options %} + {%- case option %} + {%- when 'featured' %} +
  • Featured
  • + {%- when 'created-descending' %} +
  • Newest first
  • + {%- else %} +
  • Price, low to high
  • + {%- endcase %} + {%- endfor %} +
+
diff --git a/performance/themes/storefront/snippets/index/featured_products.liquid b/performance/themes/storefront/snippets/index/featured_products.liquid new file mode 100644 index 0000000..860ef49 --- /dev/null +++ b/performance/themes/storefront/snippets/index/featured_products.liquid @@ -0,0 +1,11 @@ + diff --git a/performance/themes/storefront/snippets/index/hero.liquid b/performance/themes/storefront/snippets/index/hero.liquid new file mode 100644 index 0000000..6317ba5 --- /dev/null +++ b/performance/themes/storefront/snippets/index/hero.liquid @@ -0,0 +1,11 @@ +
+
+

{{ collection.label | upcase }}

+

{{ collection.title }}

+

{{ shop.description }}

+ {%- render "snippets.shared.button", label: "Explore the collection", url: collection.url, icon: "arrow", tone: "primary" -%} +
+
+ {{ collection.image.alt | escape }} +
+
diff --git a/performance/themes/storefront/snippets/index/journal.liquid b/performance/themes/storefront/snippets/index/journal.liquid new file mode 100644 index 0000000..6fbc31b --- /dev/null +++ b/performance/themes/storefront/snippets/index/journal.liquid @@ -0,0 +1,22 @@ +
+
+

From the journal

+

Better everyday rituals

+
+
+ {%- for article in articles %} +
+
{{ article.image.alt | escape }}
+

{{ article.title }}

+

{{ article.author }} · {{ article.published_at }} · {{ article.reading_minutes }} min read

+

{{ article.excerpt }}

+
    + {%- for tag in article.tags %} +
  • {{ tag | upcase }}
  • + {%- endfor %} +
+ {%- render "snippets.shared.button", label: "Read article", url: article.url, icon: "arrow", tone: "text" -%} +
+ {%- endfor %} +
+
diff --git a/performance/themes/storefront/snippets/page/content.liquid b/performance/themes/storefront/snippets/page/content.liquid new file mode 100644 index 0000000..935e18d --- /dev/null +++ b/performance/themes/storefront/snippets/page/content.liquid @@ -0,0 +1,10 @@ +
+

{{ page.content }}

+ {%- for section in page.sections %} +
+

{{ section.title }}

+

{{ section.body }}

+
+ {%- endfor %} + {%- render "snippets.shared.button", label: "Contact us", url: "/pages/contact", icon: "arrow", tone: "secondary" -%} +
diff --git a/performance/themes/storefront/snippets/page/header.liquid b/performance/themes/storefront/snippets/page/header.liquid new file mode 100644 index 0000000..f6d0bd1 --- /dev/null +++ b/performance/themes/storefront/snippets/page/header.liquid @@ -0,0 +1,4 @@ + diff --git a/performance/themes/storefront/snippets/product/badges.liquid b/performance/themes/storefront/snippets/product/badges.liquid new file mode 100644 index 0000000..cd24d53 --- /dev/null +++ b/performance/themes/storefront/snippets/product/badges.liquid @@ -0,0 +1,6 @@ +
    +
  • {{ label }}
  • + {%- for badge in product.badges %} +
  • {{ badge | upcase }}
  • + {%- endfor %} +
diff --git a/performance/themes/storefront/snippets/product/card.liquid b/performance/themes/storefront/snippets/product/card.liquid new file mode 100644 index 0000000..7b621ea --- /dev/null +++ b/performance/themes/storefront/snippets/product/card.liquid @@ -0,0 +1,12 @@ +
+ + {{ product.featured_image.alt | escape }} + +
+

{{ product.vendor | upcase }}

+

{{ product.title }}

+ {%- render "snippets.product.pricing", product: product -%} + {%- render "snippets.product.badges", product: product, label: label -%} +

{{ product.in_stock_variant_count }} {{ product.in_stock_variant_count | pluralize: 'option', 'options' }} in stock

+
+
diff --git a/performance/themes/storefront/snippets/product/detail.liquid b/performance/themes/storefront/snippets/product/detail.liquid new file mode 100644 index 0000000..a2216fb --- /dev/null +++ b/performance/themes/storefront/snippets/product/detail.liquid @@ -0,0 +1,13 @@ +
+ {%- render "snippets.product.gallery", product: product -%} +
+

{{ product.vendor | upcase }}

+

{{ product.title }}

+

{{ product.description }}

+ {%- render "snippets.product.pricing", product: product -%} + {%- render "snippets.product.variant_picker", product: product -%} + {%- render "snippets.shared.button", label: "Add to bag", url: "/cart/add", icon: "bag", tone: "primary" -%} + {%- render "snippets.product.badges", product: product, label: product.type -%} + {%- render "snippets.product.specs", product: product -%} +
+
diff --git a/performance/themes/storefront/snippets/product/gallery.liquid b/performance/themes/storefront/snippets/product/gallery.liquid new file mode 100644 index 0000000..095f6bf --- /dev/null +++ b/performance/themes/storefront/snippets/product/gallery.liquid @@ -0,0 +1,8 @@ + diff --git a/performance/themes/storefront/snippets/product/pricing.liquid b/performance/themes/storefront/snippets/product/pricing.liquid new file mode 100644 index 0000000..552f47b --- /dev/null +++ b/performance/themes/storefront/snippets/product/pricing.liquid @@ -0,0 +1,7 @@ +

+ {%- render "snippets.shared.price", cents: product.price_cents -%} + {%- if product.on_sale %} + {% render "snippets.shared.price", cents: product.compare_at_price_cents %} + Save {{ product.saving_cents | money }} + {%- endif %} +

diff --git a/performance/themes/storefront/snippets/product/specs.liquid b/performance/themes/storefront/snippets/product/specs.liquid new file mode 100644 index 0000000..8941559 --- /dev/null +++ b/performance/themes/storefront/snippets/product/specs.liquid @@ -0,0 +1,10 @@ + + + + + + + + + +
Details
Vendor{{ product.vendor }}
Type{{ product.type }}
Material{{ product.metafields.material }}
Care{{ product.metafields.care }}
Availability{{ product.in_stock_variant_count }} of {{ product.variants | size }} in stock
diff --git a/performance/themes/storefront/snippets/product/variant_picker.liquid b/performance/themes/storefront/snippets/product/variant_picker.liquid new file mode 100644 index 0000000..01fb532 --- /dev/null +++ b/performance/themes/storefront/snippets/product/variant_picker.liquid @@ -0,0 +1,21 @@ +{%- liquid + assign default_variant = null + for variant in product.variants + if variant.available + assign default_variant = variant + break + endif + endfor +-%} +
+ Choose an option + {%- for variant in product.variants %} + {%- unless variant.available %}{% continue %}{% endunless %} + + {%- endfor %} +

{{ product.in_stock_variant_count }} of {{ product.variants | size }} {{ product.variants | size | pluralize: 'option', 'options' }} available, shipping from {{ default_variant.option }}

+
diff --git a/performance/themes/storefront/snippets/shared/breadcrumbs.liquid b/performance/themes/storefront/snippets/shared/breadcrumbs.liquid new file mode 100644 index 0000000..a991911 --- /dev/null +++ b/performance/themes/storefront/snippets/shared/breadcrumbs.liquid @@ -0,0 +1,7 @@ + diff --git a/performance/themes/storefront/snippets/shared/button.liquid b/performance/themes/storefront/snippets/shared/button.liquid new file mode 100644 index 0000000..6edef67 --- /dev/null +++ b/performance/themes/storefront/snippets/shared/button.liquid @@ -0,0 +1,2 @@ +{%- capture button_class %}button button--{{ tone }}{% endcapture -%} +{% render "snippets.shared.icon", name: icon %}{{ label }} diff --git a/performance/themes/storefront/snippets/shared/icon.liquid b/performance/themes/storefront/snippets/shared/icon.liquid new file mode 100644 index 0000000..657be11 --- /dev/null +++ b/performance/themes/storefront/snippets/shared/icon.liquid @@ -0,0 +1,7 @@ +{%- case name -%} +{%- when 'arrow' -%} +{%- when 'bag' -%} +{%- when 'filter' -%} +{%- when 'star' -%} +{%- else -%} +{%- endcase -%} diff --git a/performance/themes/storefront/snippets/shared/meta_tags.liquid b/performance/themes/storefront/snippets/shared/meta_tags.liquid new file mode 100644 index 0000000..89a2636 --- /dev/null +++ b/performance/themes/storefront/snippets/shared/meta_tags.liquid @@ -0,0 +1,23 @@ +{%- capture meta_title %}{{ page_title | escape }} — {{ shop.name | escape }}{% endcapture -%} +{%- liquid + case template + when 'index' + assign meta_description = shop.description + when 'collection' + assign meta_description = 'Shop the summer collection at ' | append: shop.name + when 'product' + assign meta_description = 'Product details and sizing at ' | append: shop.name + else + assign meta_description = shop.description + endcase + assign canonical = 'https://' | append: shop.domain +-%} + + +{{ meta_title }} + + + + + + diff --git a/performance/themes/storefront/snippets/shared/newsletter.liquid b/performance/themes/storefront/snippets/shared/newsletter.liquid new file mode 100644 index 0000000..39b5469 --- /dev/null +++ b/performance/themes/storefront/snippets/shared/newsletter.liquid @@ -0,0 +1,9 @@ + diff --git a/performance/themes/storefront/snippets/shared/price.liquid b/performance/themes/storefront/snippets/shared/price.liquid new file mode 100644 index 0000000..b85b61a --- /dev/null +++ b/performance/themes/storefront/snippets/shared/price.liquid @@ -0,0 +1 @@ +{{ cents | money }} diff --git a/performance/themes/storefront/snippets/shared/site_footer.liquid b/performance/themes/storefront/snippets/shared/site_footer.liquid new file mode 100644 index 0000000..bb6a612 --- /dev/null +++ b/performance/themes/storefront/snippets/shared/site_footer.liquid @@ -0,0 +1,20 @@ +
+ + +
diff --git a/performance/themes/storefront/snippets/shared/site_header.liquid b/performance/themes/storefront/snippets/shared/site_header.liquid new file mode 100644 index 0000000..efee37f --- /dev/null +++ b/performance/themes/storefront/snippets/shared/site_header.liquid @@ -0,0 +1,18 @@ + diff --git a/performance/themes/storefront/templates/collection.liquid b/performance/themes/storefront/templates/collection.liquid new file mode 100644 index 0000000..c165a37 --- /dev/null +++ b/performance/themes/storefront/templates/collection.liquid @@ -0,0 +1,13 @@ +{%- liquid + assign visible_count = collection.products_count + assign heading = collection.title +-%} +
+ {%- render "snippets.shared.breadcrumbs", title: heading, parent_title: "Collections", parent_url: "/collections" -%} + {%- render "snippets.collection.header", collection: collection -%} + {%- render "snippets.collection.toolbar", collection: collection, visible_count: visible_count, products_per_page: products_per_page -%} +
+ {%- render "snippets.collection.filters", collection: collection -%} + {%- render "snippets.collection.grid", collection: collection -%} +
+
diff --git a/performance/themes/storefront/templates/index.liquid b/performance/themes/storefront/templates/index.liquid new file mode 100644 index 0000000..cb07479 --- /dev/null +++ b/performance/themes/storefront/templates/index.liquid @@ -0,0 +1,5 @@ +
+ {%- render "snippets.index.hero", collection: collection, shop: shop -%} + {%- render "snippets.index.featured_products", collection: collection -%} + {%- render "snippets.index.journal", articles: articles -%} +
diff --git a/performance/themes/storefront/templates/page.liquid b/performance/themes/storefront/templates/page.liquid new file mode 100644 index 0000000..fcb2444 --- /dev/null +++ b/performance/themes/storefront/templates/page.liquid @@ -0,0 +1,5 @@ +
+ {%- render "snippets.shared.breadcrumbs", title: page.title, parent_title: "About", parent_url: "/pages" -%} + {%- render "snippets.page.header", page: page, shop: shop -%} + {%- render "snippets.page.content", page: page -%} +
diff --git a/performance/themes/storefront/templates/product.liquid b/performance/themes/storefront/templates/product.liquid new file mode 100644 index 0000000..6588f51 --- /dev/null +++ b/performance/themes/storefront/templates/product.liquid @@ -0,0 +1,13 @@ +
+ {%- render "snippets.shared.breadcrumbs", title: product.title, parent_title: product.type, parent_url: "/collections/all" -%} + {%- render "snippets.product.detail", product: product -%} + +
diff --git a/phpbench.json b/phpbench.json index 753df5e..78c320c 100644 --- a/phpbench.json +++ b/phpbench.json @@ -2,6 +2,7 @@ "$schema": "./vendor/phpbench/phpbench/phpbench.schema.json", "runner.bootstrap": "vendor/autoload.php", "runner.path": "performance/benchmarks", + "runner.retry_threshold": 5, "runner.php_config": { "opcache.enable": 1, "opcache.enable_cli": 1, diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index fe7c22a..65e93da 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,25 +1,5 @@ parameters: ignoreErrors: - - - message: "#^Parameter \\#3 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#" - count: 3 - path: performance/Shopify/CustomFilters.php - - - - message: "#^Cannot access offset 'articles' on mixed\\.$#" - count: 1 - path: performance/Shopify/Database.php - - - - message: "#^Parameter \\#1 \\$callback of function array_map expects \\(callable\\(mixed\\)\\: mixed\\)\\|null, Closure\\(array\\)\\: non\\-empty\\-array given\\.$#" - count: 1 - path: performance/Shopify/Database.php - - - - message: "#^Parameter \\#2 \\$callback of function array_filter expects \\(callable\\(mixed\\)\\: bool\\)\\|null, Closure\\(array\\)\\: bool given\\.$#" - count: 1 - path: performance/Shopify/Database.php - - message: "#^Parameter \\#3 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#" count: 1 diff --git a/tests/Integration/Performance/StorefrontThemeTest.php b/tests/Integration/Performance/StorefrontThemeTest.php new file mode 100644 index 0000000..b82a88c --- /dev/null +++ b/tests/Integration/Performance/StorefrontThemeTest.php @@ -0,0 +1,161 @@ +setStrictVariables(true) + ->setStrictFilters(true) + ->setRethrowErrors(true) + ->build(); +} + +test('every discovered template exists and every template on disk is discovered', function () { + $discovered = StorefrontTheme::templateNames(); + + expect($discovered)->toHaveCount(29); + + foreach ($discovered as $templateName) { + expect(StorefrontTheme::templatePath($templateName))->toBeReadableFile(); + } + + $onDisk = []; + $files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(StorefrontTheme::themePath())); + foreach ($files as $file) { + if (! $file instanceof SplFileInfo) { + continue; + } + + if ($file->isFile() && $file->getExtension() === 'liquid') { + $relative = substr($file->getPathname(), strlen(StorefrontTheme::themePath()) + 1, -7); + $onDisk[] = str_replace(DIRECTORY_SEPARATOR, '.', $relative); + } + } + + sort($onDisk); + $expected = $discovered; + sort($expected); + + // An undiscovered template is invisible to the parse benchmarks, so it would be + // silently excluded from every measurement. + expect($onDisk)->toBe($expected); +}); + +test('every page renders through the layout with no missing variables or filters', function () { + $environment = storefrontStrictEnvironment(); + + foreach ([ + 'templates.index' => 'Better everyday rituals', + 'templates.collection' => 'Summer Essentials', + 'templates.product' => 'Weekend Bag', + 'templates.page' => 'Our story', + ] as $templateName => $expectedContent) { + $rendered = StorefrontTheme::renderPage($environment, $templateName); + + expect($rendered) + ->toContain('