[SOLVED] How to generate fake url /item-XXX or item/XXX for the same page?

Hello,

I trying to create a tiny real estate website with Grav and external API for content and I wish possibility to generate each ads with a dynamic url like www.domain.com/ads/id or …/ads-id.html, etc…

And intercept ID then push API request with this ID for retrieve my datas about this ads.

My question is simple, how can I do for to be dynamic urls and retrieve value in my php file in my theme ?

Thx

ps : sorry for my english, I’m French :slight_smile:

@nicolasG Grav’s routing capabilities are quite flexibel and powerful. Have a look at the documentation for Routing.

For instance you can set multiple aliases in the frontmatter of a page. For example the page mydomain/ads/maison-a-rouen with the following frontmatter…

routes:
    aliases:
        - /api/1
        - /api/maison/1

… can be accessed using the urls mydomain/ads/maison-a-rouen, mydomain/api/1 and mydomain/api/maison/1. These aliases can be set using the Admin tool, or via any editor in the page’s *.md file.

In your plugin you can test for the requested url and if the url matches, it can send back the required data.

These aliases can also be generated when saving/updating a page using Admin, for instance if you want to assign unique ids to the ads. When adding a onAdminSave method in your plugin, your could do sometime like:

public function onPluginsInitialized()
{
    // Don't proceed if we are not in the admin plugin
    if (!$this->isAdmin()) {
        return;
    }

    // Only proceed if page is child of "ads"
    $paths = $this->grav['uri']->paths();
    if (count($paths) <= 3 || $paths[2] !== 'ads') {
        return;
    }

    // Enable the main event we are interested in
    $this->enable([
        'onAdminSave' => ['onAdminSave', 0]
    ]);
}

public function onAdminSave(Event $e)
{
    $page = $page = $e['object'];
    $header = $page->header();

    $id = $this->myUniqueIdGenerator();
    $header->routes['aliases'] = [
        "/api/$id",
        "/api/maison/$id",
    ];
}

Please consider the above code as a rough sketch. Adapt code for your specific needs.

Thanks you for your reply !

I was inspired by you and I did this (it works) , what do you think about it ? I think it is a little dirty no ?

<?php
namespace Grav\Theme;

use Grav\Common\Theme;

class Realestate extends Theme
{
    public static function getSubscribedEvents()
    {
        return [
            'onPagesInitialized' => ['onPagesInitialized', 0]            
        ];
    }
	
    public function onPagesInitialized()
    {
		if ($this->isAdmin()) {
            $this->active = false;
            return;
		}
		
		require_once __DIR__ . '/api_annonces.php';
		
		/** @var Page $page */
        $page = $this->grav['page'];
        /** @var Twig $twig */
        $twig = $this->grav['twig'];
        /** @var Data $config */
        $config = $this->mergeConfig($page, true);
		
		
		
		$paths = $this->grav['uri']->paths();
		    		
		if (count($paths) == 1 && preg_match('@annonce-([0-9])+@i',$paths[0])) { //annonce-123.html			
			
			$this->grav['twig']->twig_vars['annonce'] = $this->getAnnonce($paths[0]);
		}
		elseif(count($paths) == 0){ // homepage
			
			$this->grav['twig']->twig_vars['latestannonces'] = $this->getLastAnnonces($page);
		} 
		else {
			return '';	
		}
	}
	
	private function getLastAnnonces($page){
		
		$api = new fetchAnnonces();  		
		    		
		return $api->getResults([...]);
	}
	
	private function getAnnonce($page){
		
		$api = new fetchAnnonces();
		
		$res = explode('-',$page); //annonce-123145
		$annonce_id = $res[1];	// 123145	
		
		return $api->getResultsAnnonce($annonce_id);
	}
}

site.yaml

routes:
/annonce-[0-9]+\.html: '/annonce'

Ah, I see I have misunderstood your requirements…

The approach you have taken seems to fit your needs pretty well. I see some area’s of improvement in coding though:

  • The regex used to get the ID will bite you…
    ([0-9])+ will only return the last digit of the ID, while ([0-9]+) will return all digits.

  • The code should not be placed inside the theme, but inside a plugin.
    Themes are responsible for the views. A plugin is the controller between model and view. The plugin “feeds” the view with data from the model (your ‘api_annonces’) by assigning Twig variables.

  • My personal preference is to use clean urls without noice - i.e., without extensions.
    This is also how Grav generates urls for pages. Having said that, if your client prefers extensions…

  • I would refactor the code to adopt the more commonly seen structure of plugins.
    It is like with coding guidelines, you can do whatever you like, but adopting the style of peers makes life easier.

    The code could then look like:

    public static function getSubscribedEvents()
    {
         return [
             'onPluginsInitialized' => ['onPluginsInitialized', 0]
         ];
    }
    
    public function onPluginsInitialized()
    {
       // Don't proceed if in Admin
    
       // Don't proceed if not homepage and not '/annonce-[0-9]+
       // You might assign instance variables $annonceId and $isHome while you are testing for it anyway
    
       // Enable the main event(s) you are interested in
       $this->enable([
           'onTwigSiteVariables' => ['onTwigSiteVariables', 0]
       ]);
    }
    
    public function onTwigSiteVariables()
    {
       // Require/instantiate your model 'api_annonces'
       // Assign data to the Twig variables based on $isHome or $annonceId.
    }
    
    

If you would like to compare with my efforts, I would be happy to share.

1 Like

Many thanks for your feedback and reply ! It Precious.

I listened your advices and I did that

`<?php

namespace Grav\Plugin;

use Grav\Common\Plugin;
use Grav\Common\Uri;
use RocketTheme\Toolbox\Event\Event;

require('api_annonces.php');

/**
 * Class AnnoncesPlugin
 * @package Grav\Plugin
 */
class AnnoncesPlugin extends Plugin
{
	protected $lastAnnonces;
	protected $annonce;
	protected $agence;
	
	protected $page;
	protected $twig;
	protected $config;
	protected $uri;	
	
	public static function getSubscribedEvents()
	{ 
		 return [
			 'onPluginsInitialized' => ['onPluginsInitialized', 0]
		 ];
	}

	public function onPluginsInitialized()
	{
	   // Don't proceed if in Admin
	   if ($this->isAdmin()) {
			$this->active = false;
            return;
		}
	   
		/** @var Uri $uri */
		$this->uri = $this->grav['uri'];		
		/** @var Page $page */
		$this->page = $this->grav['page'];
		/** @var Twig $twig */
		$this->twig = $this->grav['twig'];
		/** @var Data $config */
		$this->config = $this->mergeConfig($this->page, true);
		/*
		echo '<pre>';
		var_dump($this->uri->path());
		echo '</pre>';
		die;*/
		
		if ($this->uri->path() == '/') { // Homepage
			
			$api = new API_Annonces();
			
			$this->lastAnnonces = $api->getResults(
							$this->page->header()->annonces['lastannonces']['nb'],
							$this->page->header()->annonces['lastannonces']['orderby'],
							$this->page->header()->annonces['lastannonces']['order'],
							$this->page->header()->annonces['lastannonces']['insee'],
							$this->page->header()->annonces['lastannonces']['agence']
							);
			
			$this->enable([
				'onTwigPageVariables' => ['onTwigPageVariables', 0]
			]);			
		}
		elseif(preg_match('@annonce-([0-9]+)@i', $this->uri->path() )) { //annonce-xxx
		
		
			$api = new API_Annonces();
			
			$this->annonce = $api->getAnnonce( $this->uri->path() );
			$this->agence = $api->getAgence( $telhab = '0763436145' );
			
			$this->enable([
				'onTwigPageVariables' => ['onTwigPageVariables', 0]
			]);
			
		} 
	}

	public function onTwigPageVariables()
	{	
        //    		die('test');
	   // Assign data to the Twig variables based on $isHome or $annonceId.
	   $twig = $this->grav['twig'];	   
	   
       $twig->twig_vars['lastAnnonces'] = $this->lastAnnonces;
       $twig->twig_vars['annonce'] 		= $this->annonce;
       $twig->twig_vars['agence'] 		= $this->agence;
	   
	}
}`

It’s better ?

However, I have yet some problems…

1/ Unable to import my “Model” with Use (namespace), already “Not found class”. So i did a dirty require…
2/ Currently, If I’m in homepage, I’ve “Page not found Exception”
However if I “die()” just before

$this->enable([
				'onTwigPageVariables' => ['onTwigPageVariables', 0]
			]);	

My “die()” works. And if put my “die()” in onTwigPageVariables(), the 404 message is here. why ? IDK…
3/ If I go to an ads (annonce-xxx.html), I’ve " Fatal error : Allowed memory size of 134217728 bytes exhausted (tried to allocate 130968 bytes) in /home/sites/www-horscms/grav-multisite/system/src/Grav/Common/Page/Pages.php on line 502" why ? IDK…

Thanks you by advance…

Update : Same if comment onTwigPageVariables, the problem stay same.
If I change by everything like _onPluginsInitialized instead of onPluginsInitialized, the page load normaly…
Where is the ** problem :slight_smile: ? Something in my plugin generate this 404, but what…

Edit : The problem come from $this->page = $this->grav[‘page’];, if I comment, the page works.

Have a look at this sample which is only part of the solution…

Of course you need to adjust to your own needs and add extra required checks…

<?php
namespace Grav\Plugin;

require_once __DIR__ . '/classes/autoload.php';

use Grav\Common\Plugin;
use RocketTheme\Toolbox\Event\Event;
use Grav\Plugin\Annonces\ApiAnnonces;

class AnnoncesPlugin extends Plugin
{
    protected $annonceId = null;
    protected $isHome = false;

    public static function getSubscribedEvents()
    {
        return [
            'onPluginsInitialized' => ['onPluginsInitialized', 0]
        ];
    }

    public function onPluginsInitialized()
    {
        // Don't proceed if in Admin
        if ($this->isAdmin()) {
            return;
        }

        $matches = [];
        $uri = $this->grav['uri'];

		if($uri->path() === '/') { // homepage
			$this->isHome = true;
        } else if (preg_match('/annonce-([0-9]+)/i', $uri->path(), $matches)) { //annonce-123.html
            $this->annonceId = $matches[1];
		} else {
			return;
        }

        // Enable the main event(s) you are interested in
        $this->enable([
            'onTwigSiteVariables' => ['onTwigSiteVariables', 0]
        ]);
    }

    public function onTwigSiteVariables()
    {
        $api = new ApiAnnonces();
        $page = $this->grav['page'];

        if ($this->isHome) {
            $lastAnnonces = $page->header()->annonces['lastannonces'];
            $results = $api->getResults(
                $lastAnnonces['nb'],
                $lastAnnonces['orderby'],
                $lastAnnonces['order'],
                $lastAnnonces['insee'],
                $lastAnnonces['agence']
            );
            $this->grav['twig']->twig_vars['lastAnnonces'] = $results;
        } else {
            $annonce = $api->getAnnonces( $this->grav['uri']->path() );
            $agence =  $api->getAgence( $this->annonceId );

            $this->grav['twig']->twig_vars['annonce'] = $annonce;
            $this->grav['twig']->twig_vars['agence'] = $agence;
        }
    }
}

Note:

  • Class ‘ApiAnnonces’ only contains the methods and returns dummy data. It is stored in a separate folder called ‘classes’. The folder also contains the file ‘autoload.php’ which allows for autoloading classes.

On a guick glance…
Have a look at the Lifecycle of Grav. You can see that event ‘onPluginInitialized’ comes before ‘onPagesInitialized’, hence $this->grav[‘page’] does not return a valid page.

Re pamtbauu,

All is OK now, thank you so much ! It’s more clear for me.

Sorry for my ignorance, I come from Laravel, and I am not used by hooks and lifecycle of grav.
Little by little, I will improve my knowledge of grav.

My final file (stay just autoload).

<?php

namespace Grav\Plugin;

use Grav\Common\Plugin;
use Grav\Common\Uri;
use RocketTheme\Toolbox\Event\Event;

require('api_annonces.php');

/**
 * Class AnnoncesPlugin
 * @package Grav\Plugin
 */
class AnnoncesPlugin extends Plugin
{
	protected $annonceId = null;
    protected $isHome = false;
	
	public static function getSubscribedEvents()
	{ 
		 return [
			 'onPluginsInitialized' => ['onPluginsInitialized', 0]
		 ];
	}

	public function onPluginsInitialized()
	{
	   // Don't proceed if in Admin
	   if ($this->isAdmin()) {
			$this->active = false;
            return;
		}
	   
		$matches = [];
        $uri = $this->grav['uri'];
				
		if ($uri->path() === '/') { // Homepage		
			$this->isHome = true;			
		}
		elseif(preg_match('/annonce-([0-9]+)/i', $uri->path(), $matches )) { //annonce-xxx
			$this->annonceId = $matches[1];	
			//var_dump($this->annonceId);
		}
		else {
			return;
		}
		
		// Enable the main event(s) you are interested in
        $this->enable([
            'onTwigSiteVariables' => ['onTwigSiteVariables', 0]
        ]);
	}

	public function onTwigSiteVariables()
	{			
	   // Assign data to the Twig variables based on $isHome or $annonceId.  	   	   
	    $api = new API_Annonces();
        $page = $this->grav['page'];
			
        if ($this->isHome) {					
			
            $lastAnnonces = $page->header()->annonces['lastannonces'];
			
			/*echo '<pre>';
			var_dump($lastAnnonces);
			echo '</pre>';
			die;*/
			
            $results = $api->getResults(
                $lastAnnonces['nb'],
                $lastAnnonces['orderby'],
                $lastAnnonces['order'],
                $lastAnnonces['insee'],
                $lastAnnonces['agence'],
                $lastAnnonces['dep']
            );
			
            $this->grav['twig']->twig_vars['latestannonces'] = $results;
			
        } else {
			$parts = explode('-',$this->grav['uri']->path());
			
            $annonce = $api->getResultsAnnonce(  $parts[1]);		
            $this->grav['twig']->twig_vars['annonce'] = $annonce;            
        }
	   
	}
}

Nice!

May I add just one last nitpick?

This line is not really necessary. You can reuse $this->annonceId, which has been assigned in ‘onPluginsInitialized’ using the preg_match statement. ‘preg_match’ assignes the resulting capturing groups to parameter $matches.

elseif(preg_match('/annonce-([0-9]+)/i', $uri->path(), $matches )) { //annonce-xxx
   $this->annonceId = $matches[1];

One last tiny bit of code:

if ($this->topic === 'solved') {
   $this->markTopicSolved();
} else {
   $this->chewMoreDocs();
}

Exact ! it will be done :slight_smile:

Done !

thank you very much for your precious help !