Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Replace rest with graphql for theme level support #360

Open
wants to merge 5 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 [];
}
}
175 changes: 124 additions & 51 deletions src/Actions/VerifyThemeSupport.php
Original file line number Diff line number Diff line change
@@ -1,79 +1,152 @@
<?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;

/**
* Activates a plan for a shop.
*/
class VerifyThemeSupport
final class VerifyThemeSupport
{
/**
* Querier for shops.
*
* @var IShopQuery
*/
protected $shopQuery;
private const ASSET_FILE_NAMES = ['templates/product.json', 'templates/collection.json', 'templates/index.json'];

/**
* Theme helper.
*
* @var ThemeHelper
*/
protected $themeHelper;
private const MAIN_ROLE = 'main';

private string $cacheInterval;

private int $cacheDuration;

/**
* Setup.
*
* @param IShopQuery $shopQuery The querier for shops.
* @param ThemeHelper $themeHelper Theme helper.
*
* @return void
*/
public function __construct(
IShopQuery $shopQuery,
ThemeHelper $themeHelper
private IShopQuery $shopQuery,
private FetchMainTheme $fetchMainTheme,
private FetchThemeAssets $fetchThemeAssets,
) {
$this->shopQuery = $shopQuery;
$this->themeHelper = $themeHelper;
$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
{
$shop = $this->shopQuery->getById($shopId);

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

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);

$hasTemplates = count($assets) > 0;
$allTemplatesHasRightType = count($assets) === count($sectionsWithAppBlock);
$hasTemplatesCountWithRightType = count($sectionsWithAppBlock) > 0;

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

return ThemeSupportLevel::UNSUPPORTED;
}

/**
* Execution.
* @template T
* @template Z
*
* @param ShopId $shopId The shop ID.
*
* @return int
* @param Z $mainTheme
* @param T $assets
* @return T
*/
public function __invoke(ShopId $shopId): int
private function mainSections(ShopModel $shop, array $mainTheme, array $assets): array
{
$this->themeHelper->extractStoreMainTheme($shopId);
$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);
}

if ($this->themeHelper->themeIsReady()) {
$templateJSONFiles = $this->themeHelper->templateJSONFiles();
$templateMainSections = $this->themeHelper->mainSections($templateJSONFiles);
$sectionsWithAppBlock = $this->themeHelper->sectionsWithAppBlock($templateMainSections);
$assetContent = json_decode($content, true);

$hasTemplates = count($templateJSONFiles) > 0;
$allTemplatesHasRightType = count($templateJSONFiles) === count($sectionsWithAppBlock);
$templatesСountWithRightType = count($sectionsWithAppBlock);

switch (true) {
case $hasTemplates && $allTemplatesHasRightType:
return ThemeSupportLevel::FULL;
$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);

case $templatesСountWithRightType:
return ThemeSupportLevel::PARTIAL;
if ($mainAsset) {
return 'sections/' . end($mainAsset)['type'] . '.liquid';
}
}, $assets)
);

default:
return ThemeSupportLevel::UNSUPPORTED;
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 ThemeSupportLevel::UNSUPPORTED;
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