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

Upgrade to Symfony 5 #9

Open
wants to merge 30 commits into
base: master
Choose a base branch
from
Open

Upgrade to Symfony 5 #9

wants to merge 30 commits into from

Conversation

Richtermeister
Copy link
Owner

This includes auto-wiring, annotation-routing..

@Dragony
Copy link

Dragony commented Mar 11, 2021

Hey man, great work on this package. I just finished installing this branch on a fresh symfony 5 installation. It was a pain without documentation, but that's more my fault than yours ;-)

There is one main issue I stumbled upon: In CodeCloud\Bundle\ShopifyBundle\Service\JwtResolver:32 you reference a library that is not declared as a composer dependency. I resolved it by simply running composer require firebase/php-jwt

Additionally I needed to install guzzle (composer require guzzlehttp/guzzle) in order for the API to work.

Aside from that I'm going to paste the configuration files I changed to make this work:

base config packages/code_cloud_shopify.yaml:

code_cloud_shopify:
  oauth:
    api_key: "%shopify_api_key%" #key
    shared_secret: #secret
    scope: "write_customers"
    redirect_route: admin
  webhooks:
    - orders/create
    - customers/update

add into routes.yaml

code_cloud_shopify:
  resource: "@CodeCloudShopifyBundle/Resources/config/routing.yml"

security.yaml:

security:
    # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
    providers:
        users_in_memory: { memory: null }
        codecloud_shopify:
            id: CodeCloud\Bundle\ShopifyBundle\Security\ShopifyAdminUserProvider

    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
#        main:
#            anonymous: true
#            lazy: true
#            provider: users_in_memory
#
#            # activate different ways to authenticate
#            # https://symfony.com/doc/current/security.html#firewalls-authentication
#
#            # https://symfony.com/doc/current/security/impersonating_user.html
#            # switch_user: true
        admin:
            pattern: ^/admin
            provider: codecloud_shopify
            guard:
                authenticators:
                    - CodeCloud\Bundle\ShopifyBundle\Security\SessionAuthenticator
    # Easy way to control access for large sections of your site
    # Note: Only the *first* access control that matches will be used
    access_control:
        # - { path: ^/admin, roles: ROLE_ADMIN }
        # - { path: ^/profile, roles: ROLE_USER }

I added an alias for my StoreManager in services.yaml:

    CodeCloud\Bundle\ShopifyBundle\Model\ShopifyStoreManagerInterface:
        alias: App\Domain\Shopify\StoreManager

Add into twig.yaml:

twig:
    globals:
        shopify_api_key: "%shopify_api_key%"

This last one is required for the jwt.html.twig.

Now I'm getting this:
image

Success! Thanks again

@Dragony
Copy link

Dragony commented Mar 12, 2021

For anyone reading this issue, here is a reference implementation of StoreManager and a ShopifyStore Entity. You'll need to add the methods findOneByStoreName and findOneBySessionId to your store repository.

StoreManager:

<?php

namespace App\Domain\Shopify;

use App\Entity\ShopifyStore;
use App\Repository\ShopifyStoreRepository;
use CodeCloud\Bundle\ShopifyBundle\Model\Session;
use CodeCloud\Bundle\ShopifyBundle\Model\ShopifyStoreManagerInterface;
use Doctrine\ORM\EntityManagerInterface;

class StoreManager implements ShopifyStoreManagerInterface
{
    /**
     * @var ShopifyStoreRepository
     */
    private ShopifyStoreRepository $shopifyStoreRepository;
    /**
     * @var EntityManagerInterface
     */
    private EntityManagerInterface $entityManager;

    public function __construct(ShopifyStoreRepository $shopifyStoreRepository, EntityManagerInterface $entityManager)
    {
        $this->shopifyStoreRepository = $shopifyStoreRepository;
        $this->entityManager = $entityManager;
    }

    public function getAccessToken($storeName): string
    {
        $store = $this->shopifyStoreRepository->findOneByStoreName($storeName);

        if (null === $store) {
            throw new \Exception('trying to get access token for unknown store');
        }

        return $store->getAccessToken();
    }

    public function storeExists($storeName): bool
    {
        return $this->shopifyStoreRepository->findOneByStoreName($storeName) !== null;
    }

    public function preAuthenticateStore($storeName, $nonce)
    {

    }

    public function authenticateStore($storeName, $accessToken, $nonce)
    {
        $store = $this->shopifyStoreRepository->findOneByStoreName($storeName);

        if (null === $store) {
            $store = new ShopifyStore();
            $store->setStoreName($storeName);
            $this->entityManager->persist($store);
        }

        $store->setAccessToken($accessToken);
        $this->entityManager->flush();
    }

    public function authenticateSession(Session $session)
    {
        $store = $this->shopifyStoreRepository->findOneByStoreName($session->storeName);

        if (null === $store) {
            throw new \Exception('tried to authenticate unknown store');
        }

        $store->setSessionId($session->sessionId);

        $this->entityManager->flush();
    }

    public function findStoreNameBySession(string $sessionId): ?string
    {
        $store = $this->shopifyStoreRepository->findOneBySessionId($sessionId);

        if (null === $store) {
            return null;
        }

        return $store->getStoreName();
    }
}

ShopifyStore:

<?php

namespace App\Entity;

use App\Repository\ShopifyStoreRepository;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass=ShopifyStoreRepository::class)
 */
class ShopifyStore
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $storeName;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $accessToken;

    /**
     * @ORM\Column(type="text", nullable=true)
     */
    private $sessionId;

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getStoreName(): ?string
    {
        return $this->storeName;
    }

    public function setStoreName(string $storeName): self
    {
        $this->storeName = $storeName;

        return $this;
    }

    public function getAccessToken(): ?string
    {
        return $this->accessToken;
    }

    public function setAccessToken(string $accessToken): self
    {
        $this->accessToken = $accessToken;

        return $this;
    }

    public function getSessionId(): ?string
    {
        return $this->sessionId;
    }

    public function setSessionId(?string $sessionId): self
    {
        $this->sessionId = $sessionId;

        return $this;
    }
}

@Richtermeister
Copy link
Owner Author

Hey @Dragony,

This is awesome, thank you for taking a crack at installing this fresh. You nailed it!
I'll button things up this weekend and get your feedback into the docs! 👍

@jmunozco
Copy link

Hi, great job @Dragony.
Just one thing.. what is the right url for the auth and callback in the Shopify side? I mean... in the app settings inside Shopify. Please, if anyone of you could help me. Thanks!

@Dragony
Copy link

Dragony commented Jun 10, 2021

@jmunozco Here is the config on my Shopify App:

image

Be careful though, Shopify changed their policy to not support third party cookies anymore for embedded apps. This extension uses cookies to save the logged in state. Additionally cookies are used for CSRF protection in forms (by symfony). Both of these things will not work in an embedded app. If your app is not embedded you shouldn't have any issues though.

@jmunozco
Copy link

Thanks @Dragony !
Just one thing... what is the service I should inject into the Controller when we talk about this line: $api = $this->get('')->getForStore("name-of-store"); ?
Please, a little bit of help to finish my integration @Dragony @Richtermeister @codecloud

@Dragony
Copy link

Dragony commented Jun 11, 2021

@jmunozco With this extension the store name is the username of the user that is logged in. It all boils down to this:


use CodeCloud\Bundle\ShopifyBundle\Api\ShopifyApiFactory;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

/**
 * @Route("/admin")
 */
class AdminController extends AbstractController
{
    /**
     * @Route("/", name="admin")
     */
    public function index(Request $request, ShopifyApiFactory $shopifyApiFactory): Response
    {
        $api = $shopifyApiFactory->getForStore($this->getUser()->getUsername());
        // ...
    }
}

I use this class to get the Shopify Store. It also takes care of some edge cases:

<?php

namespace App\Domain\Provider;

use App\Entity\ShopifyStore;
use App\Repository\ShopifyStoreRepository;
use Symfony\Component\Security\Core\Security;

class ShopifyStoreProvider
{
    private ?ShopifyStore $shopifyStore;
    /**
     * @var Security
     */
    private Security $security;
    /**
     * @var ShopifyStoreRepository
     */
    private ShopifyStoreRepository $shopifyStoreRepository;

    public function __construct(Security $security, ShopifyStoreRepository $shopifyStoreRepository)
    {
        $this->security = $security;
        $this->shopifyStoreRepository = $shopifyStoreRepository;
    }

    public function getCurrentShopifyStore(): ?ShopifyStore
    {
        // Already set by setter method or previous call
        if (isset($this->shopifyStore)) {
            return $this->shopifyStore;
        }

        if (php_sapi_name() === "cli") {
            // Just set one, since we can't guess on the cli
            return $this->shopifyStore = $this->shopifyStoreRepository->findOneBy([]);
        }

        // If store is not set manually, fallback to username
        $user = $this->security->getUser();

        if (null === $user) {
            throw new \Exception('unable to find a relevant store');
        }

        $store = $this->setShopifyStoreByName($this->security->getUser()->getUsername());

        // Do nothing if we can't find a store

        return $this->shopifyStore = $store;
    }

    public function setShopifyStoreByName(string $storeName)
    {
        $store = $this->shopifyStoreRepository->findOneByStoreName($storeName);

        return $this->shopifyStore = $store;
    }

    /**
     * @param ShopifyStore $shopifyStore
     */
    public function setShopifyStore(ShopifyStore $shopifyStore): void
    {
        $this->shopifyStore = $shopifyStore;
    }

    public function hasStore(): bool
    {
        return isset($this->shopifyStore);
    }
}

@jmunozco
Copy link

jmunozco commented Jun 11, 2021

Is there a way to call specific API version? I got this
Client error: `GET https://olive-aove.myshopify.com/admin/customers.json?page=1&limit=250` resulted in a `400 Bad Request` response: {"errors":{"page":"page cannot be passed. See https:\/\/help.shopify.com\/api\/guides\/paginated-rest-results for more i (truncated...)
Thanks, your help is being crucial for me!

@Dragony
Copy link

Dragony commented Jun 11, 2021

You can set the API version in the config:

parameters:
  shopify_api_key: "%env(APP_API_KEY)%"
  shopify_shared_secret: "%env(APP_API_SECRET)%"

code_cloud_shopify:
  oauth:
    api_key: "%shopify_api_key%"
    shared_secret: "%shopify_shared_secret%"
    # https://shopify.dev/docs/admin-api/access-scopes
    #scope: "read_product_listings,read_content,write_script_tags,read_locations,write_products,read_orders,write_inventory,write_shipping"
    scope: "write_script_tags,read_locations"
    redirect_route: admin
  api_version: 2021-01 # <--------
  webhooks:
    - app/uninstalled
    - subscription_billing_attempts/failure
    - subscription_billing_attempts/success

@Richtermeister
Copy link
Owner Author

@Dragony @jmunozco Sorry for the delay, I need to wrap this PR up. FYI, this PR already works around the 3rd-party cookie issue by forcing the session id into the url. All you need to do is include a specific parameter in your admin url, like so:

@Route("/admin/{shopify_session_id}")

This listener ensures that the parameter stays in the url: https://github.com/Richtermeister/symfony-shopify-bundle/blob/68b63072195b87e2476bb6b112012a094b4707a5/src/EventListener/SessionRoutingListener.php

and this session authenticator makes sure to retrieve the right user against it:
https://github.com/Richtermeister/symfony-shopify-bundle/blob/68b63072195b87e2476bb6b112012a094b4707a5/src/Security/SessionAuthenticator.php

I'll add a more comprehensive readme asap.

@jmunozco
Copy link

jmunozco commented Jul 1, 2021

One more thing... after installing the app in a store (in Shopify), it redirects to redirect_route but it loads this route in a frame inside Shopify. Is there a way to redirect to a route out of Shopify? Thanks mate!

@Dragony
Copy link

Dragony commented Jul 1, 2021

You can use FrameBusterRedirectResponse to redirect the browser away from the iframe. Also I think you can configure your app to be external, rather than integrated into the admin. I can't recall where though, sorry.

@Braywolf11
Copy link

I have to integrate symfony 6 with shopify and the documentation of the shopify page is very scarce and I would like to use this bundle but I would like a clearer example or a project where I can guide me to integrate it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants