Skip to content

Replace rest with graphql for theme level support #360

New issue

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

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

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions src/Actions/FetchMainTheme.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace Osiset\ShopifyApp\Actions;

use Illuminate\Support\Facades\Log;
use Osiset\ShopifyApp\Contracts\ShopModel;

final class FetchMainTheme
{
/**
* @return array{id: ?string, name: ?string}
*/
public function handle(ShopModel $shop): array
{
$response = $shop->api()->graph('{
themes(first: 1, roles: MAIN) {
nodes {
id
name
}
}
}');

if (blank(data_get($response['body']->toArray(), 'data.themes.userErrors'))) {
return data_get($response['body']->toArray(), 'data.themes.nodes.0', []);
}

Log::error('Fetching main theme error: '.json_encode(data_get($response['body']->toArray(), 'data.themes.userErrors')));

return [];
}
}
49 changes: 49 additions & 0 deletions src/Actions/FetchThemeAssets.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

namespace Osiset\ShopifyApp\Actions;

use Illuminate\Support\Facades\Log;
use Osiset\ShopifyApp\Contracts\ShopModel;

final class FetchThemeAssets
{
/**
* @param array<int, array{filename: string, content: string}> $filenames
*/
public function handle(ShopModel $shop, string $mainThemeId, array $filenames): array
{
$response = $shop->api()->graph('query ($id: ID!, $filenames: [String!]) {
theme(id: $id) {
id
name
role
files(filenames: $filenames) {
nodes {
filename
body {
... on OnlineStoreThemeFileBodyText {
content
}
}
}
}
}
}', [
'id' => $mainThemeId,
'filenames' => $filenames,
]);

if (blank(data_get($response['body']->toArray(), 'data.theme.userErrors'))) {
return array_map(fn (array $data) => [
'filename' => $data['filename'],
'content' => $data['body']['content'] ?? '',
], data_get($response['body']->toArray(), 'data.theme.files.nodes'));
}

Log::error('Fetching settings data error: '.json_encode(data_get($response['body']->toArray(), 'data.theme.userErrors')));

return [];
}
}
151 changes: 130 additions & 21 deletions src/Actions/VerifyThemeSupport.php
Original file line number Diff line number Diff line change
@@ -1,45 +1,154 @@
<?php

declare(strict_types=1);

namespace Osiset\ShopifyApp\Actions;

use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
use Osiset\ShopifyApp\Contracts\Queries\Shop as IShopQuery;
use Osiset\ShopifyApp\Contracts\ShopModel;
use Osiset\ShopifyApp\Objects\Enums\ThemeSupportLevel;
use Osiset\ShopifyApp\Objects\Values\ShopId;
use Osiset\ShopifyApp\Services\ThemeHelper;
use Osiset\ShopifyApp\Util;

class VerifyThemeSupport
final class VerifyThemeSupport
{
private const ASSET_FILE_NAMES = ['templates/product.json', 'templates/collection.json', 'templates/index.json'];

private const MAIN_ROLE = 'main';

private string $cacheInterval;

private int $cacheDuration;

public function __construct(
protected IShopQuery $shopQuery,
protected ThemeHelper $themeHelper
private IShopQuery $shopQuery,
private FetchMainTheme $fetchMainTheme,
private FetchThemeAssets $fetchThemeAssets,
) {
$this->cacheInterval = (string) Str::of(Util::getShopifyConfig('theme_support.cache_interval'))
->plural()
->ucfirst()
->start('add');

$this->cacheDuration = (int) Util::getShopifyConfig('theme_support.cache_duration');
}

public function __invoke(ShopId $shopId): int
{
$this->themeHelper->extractStoreMainTheme($shopId);
$shop = $this->shopQuery->getById($shopId);

if ($this->themeHelper->themeIsReady()) {
$templateJSONFiles = $this->themeHelper->templateJSONFiles();
$templateMainSections = $this->themeHelper->mainSections($templateJSONFiles);
$sectionsWithAppBlock = $this->themeHelper->sectionsWithAppBlock($templateMainSections);
/** @var array{id: string, name: string} */
$mainTheme = Cache::remember(
"mainTheme.{$shop->getId()->toNative()}",
now()->{$this->cacheInterval}($this->cacheDuration),
fn () => $this->fetchMainTheme->handle($shop)
);

$hasTemplates = count($templateJSONFiles) > 0;
$allTemplatesHasRightType = count($templateJSONFiles) === count($sectionsWithAppBlock);
$templatesСountWithRightType = count($sectionsWithAppBlock);
if (isset($mainTheme['id'])) {
/** @var array<int, array{filename: string, content: string}> */
$assets = Cache::remember(
"assets.{$mainTheme['id']}.{$shop->getId()->toNative()}",
now()->{$this->cacheInterval}($this->cacheDuration),
fn () => $this->fetchThemeAssets->handle(
shop: $shop,
mainThemeId: $mainTheme['id'],
filenames: self::ASSET_FILE_NAMES
)
);
$templateMainSections = $this->mainSections(
shop: $shop,
mainTheme: $mainTheme,
assets: $assets
);
$sectionsWithAppBlock = $this->sectionsWithAppBlock($templateMainSections);

switch (true) {
case $hasTemplates && $allTemplatesHasRightType:
return ThemeSupportLevel::FULL;
$hasTemplates = count($assets) > 0;
$allTemplatesHasRightType = count($assets) === count($sectionsWithAppBlock);
$hasTemplatesCountWithRightType = count($sectionsWithAppBlock) > 0;

case $templatesСountWithRightType:
return ThemeSupportLevel::PARTIAL;

default:
return ThemeSupportLevel::UNSUPPORTED;
}
return match (true) {
$hasTemplates && $allTemplatesHasRightType => ThemeSupportLevel::FULL,
$hasTemplatesCountWithRightType => ThemeSupportLevel::PARTIAL,
default => ThemeSupportLevel::UNSUPPORTED
};
}

return ThemeSupportLevel::UNSUPPORTED;
}

/**
* @template T
* @template Z
*
* @param Z $mainTheme
* @param T $assets
*
* @return T
*/
private function mainSections(ShopModel $shop, array $mainTheme, array $assets): array
{
$filenamesForMainSections = array_filter(
array_map(function ($asset) {
$content = $asset['content'];

if (! $this->json_validate($content)) {
$content = preg_replace("#(/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+/)|([\s\t]//.*)|(^//.*)#", '', $content);
}

$assetContent = json_decode($content, true);


$mainAsset = array_filter($assetContent['sections'], function ($value, $key) {
return $key == self::MAIN_ROLE || str_starts_with($value['type'], self::MAIN_ROLE);
}, ARRAY_FILTER_USE_BOTH);

if ($mainAsset) {
return 'sections/'.end($mainAsset)['type'].'.liquid';
}
}, $assets)
);

return Cache::remember(
"mainSections.{$mainTheme['id']}.".sha1(implode('|', $filenamesForMainSections)),
now()->{$this->cacheInterval}($this->cacheDuration),
fn () => $this->fetchThemeAssets->handle(
shop: $shop,
mainThemeId: $mainTheme['id'],
filenames: [...$filenamesForMainSections]
)
);
}

/**
* @template T
*
* @param T $templateMainSections
*
* @return T
*/
private function sectionsWithAppBlock(array $templateMainSections): array
{
return array_filter(array_map(function ($file) {
$acceptsAppBlock = false;

preg_match('/\{\%-?\s+schema\s+-?\%\}([\s\S]*?)\{\%-?\s+endschema\s+-?\%\}/m', $file['content'], $matches);
$schema = json_decode($matches[1] ?? '{}', true);

if ($schema && isset($schema['blocks'])) {
$acceptsAppBlock = in_array('@app', array_column($schema['blocks'], 'type'));
}

return $acceptsAppBlock ? $file : null;
}, $templateMainSections));
}


private function json_validate(string $string): bool
{
json_decode($string);

return json_last_error() === JSON_ERROR_NONE;
}
}
96 changes: 0 additions & 96 deletions src/Objects/Values/MainTheme.php

This file was deleted.

Loading
Loading