0

Apologies for the long post, feel like I have gone down a rabbit hole here and struggling to find a solution which works.

I'm building a Symfony (2.6) application which I'd like to support multiple stores each with unique products for example using subdomains.

http://stores.com                     - Primary store landing page
http://stores.com/first-product       - Product only available on store.com
http://nicks.stores.com               - Nicks store landing page
http://nicks.stores.com/first-product - Different product from the above example, only available on nicks.stores.com

Handling subdomains

I successfully have store landing pages working using host matching on the default Symfony router (http://symfony.com/doc/current/components/routing/hostname_pattern.html).

With an event listener which picks up the store subdomain and adds it into a service (idea taken from http://knpuniversity.com/screencast/question-answer-day/symfony2-dynamic-subdomains).

Store service (note I use JMSDiExtraBundle to define services using annotations):

/**
 * @Service("store_service")
 */
class StoreService
{
    protected $storeCurrent;

    public function getCurrentStore()
    {
        return $this->storeCurrent;
    }

    public function setCurrentStore(Store $store)
    {
        $this->storeCurrent = $store;
        return $this;
    }
}

Event listener for extracting subdomain from request and adding into the service if it exists

/**
 * @Service
 */
class CurrentStoreListener
{
    /**
     * @var EntityManager
     */
    protected $em;

    /**
     * @var StoreService
     */
    protected $storeService;

    /**
     * @DI\InjectParams({
     *      "em" = @DI\Inject("doctrine.orm.entity_manager"),
     *      "storeService" = @DI\Inject("store_service")
     * })
     */
    public function __construct(EntityManager $em, StoreService $storeService)
    {
        $this->em = $em;
        $this->storeService = $storeService;
    }

    /**
     * @Observe("kernel.request", priority=31)
     */
    public function onKernelRequest(GetResponseEvent $event)
    {
        $store = $this->em->getRepository('StoreBundle:Store')
            ->findByDomainNotDeleted($event->getRequest()->getHost());

        if (!$store) {
            throw new NotFoundHttpException('No such store exists');
        }

        $this->storeService->setCurrentStore($store);
    }
}

Example controller action with route using the SensioFrameworkExtraBundle:

class StoreController extends Controller
{
    /**
     * @DI\Inject("doctrine.orm.entity_manager")
     * @var EntityManager
     */
    private $em;

    /**
     * @DI\Inject("store_service")
     * @var StoreService
     */
    protected $storeService;

    /**
     * @Route("/", name="store_index")
     * @Template
     */
    public function indexAction()
    {
        $store = $this->storeService->getCurrentStore();
        $products = $this->em->getRepository('StoreBundle:Product')->findAllForStoreInOrder($store);

        return [
            'store' => $store,
            'products' => $products
        ];
    }
}

This all works perfectly. :)

Handling dynamic product routes for stores

This is where I'm starting to have issues because the event listener (as shown above) is not being called early enough so it's available for the dynamic router below.

This dynamic router worked just fine when the application was a single store, the below has been built using the CMF Dynamic router (http://symfony.com/doc/current/cmf/book/routing.html)

# app/config/config.yml

cmf_routing:
  dynamic:
    enabled:                      true
    route_provider_service_id:    store.product_router
  chain:
    routers_by_id:
      router.default:             32
      cmf_routing.dynamic_router: 30
/**
 * @Service("store.product_router")
 */
class ProductRouter implements RouteProviderInterface
{
    /**
     * @var EntityManager
     */
    protected $em;

    /**
     * @var StoreService
     */
    protected $storeService;

    /**
     * @DI\InjectParams({
     *      "em" = @DI\Inject("doctrine.orm.entity_manager"),
     *      "storeService" = @DI\Inject("store_service")
     * })
     */
    public function __construct(EntityManager $em, StoreService $storeService)
    {
        $this->em = $em;
        $this->storeService = $storeService;
    }

    /**
     * @param Request $request
     * @return RouteCollection
     */
    public function getRouteCollectionForRequest(Request $request)
    {
        $collection = new RouteCollection();

        $product = $this->em->getRepository('StoreBundle:Product')
            ->findOneBySlugForStore(substr($request->getRequestUri(), 1), $this->storeService->getCurrentStore());

        if (empty($product)) {
            return $collection;
        }

        $route = new Route(
            '/' . $product->getSlug(),
            [
                '_controller' => 'StoreBundle:Product:view',
                'slug' => $product->getSlug()
            ]
        );

        $collection->add($product->getSlug(), $route);

        return $collection;
    }

    public function getRouteByName($name, $params = [])
    {
    }

    public function getRoutesByNames($names)
    {
    }
}

I have tried to rearrange the priorities of the services, to me the following priorities should work:

32 - Default router

31 - Current store listener

30 - Dynamic router

Any suggestions on how I can get access to the current store inside my dynamic router?

Understand I could hack this and just extract the host request and pass that into the repository on the dynamic router but thats an extra table join I'd rather not do seeing as I should already have the store entity available.

Nick
  • 1,219
  • 1
  • 13
  • 24
  • I haven't used cmf so far, but can you explain, why a default routing setup doesn't work for you? You need your router to have a `Store` object available to determine the controller and action? Why don't you let the request invoke your controller's action and check the correctness there? Your route would be `/{slug}` and the name e.g. `store_product`. Don't make your router dynamic, but generic. – Aitch Apr 19 '15 at 21:05
  • What would be the problem of triggering the Current store listener before even the default router? You seem to only depend on the Request, which already exists at that point. I think what is confusing you is that the priorities of the routers are only within the chain router and not the same as the priorities of the event listeners. the chainrouter itself is executed when the routing event is triggered, and gives each router a turn in the priority order. your current store listener happens before or after, but not in the middle of the chain. – dbu Apr 22 '15 at 07:08

0 Answers0