Add flex-object to existing flex-dir if not exists (from plugin php)

Hey, i’ve been trying ChatGPT to help me create a custom plugin for a simple like-button in GRAV. I want to store the data in a flex-directory called “likes”, there is one test-entry in it already. Properties are:
published (copied from the contacts demo), masked_ip, datetime, page, hash.
where hash is $hash = md5($ip . $page_route);

I’m having problems creating a new flex-object from the plugin and checking if the hash already exists. I want to prevent visitors from liking multiple times. I know this is not a guarantee, but i don’t want to use cookies or sessions.

  //Define the Flex Objects directory
            $flexdir = $this->grav['locator']->findResource('user://data/flex-objects/likes.json');
            if (!$flexdir) {
                throw new \RuntimeException('Likes flex-directory not found');
            }

            if ($flexdir) {
                $this->grav['log']->info('dir exists');
                $this->grav['log']->info('page: '.$page_route);
                $object = new FlexObject($flexdir,[
                    'published' => 1,
                    'masked_ip' => $ip_masked,
                    'datetime' => $timestamp,
                    'page' => $page_route
                ]);

                // Save the object to the Flex Directory
                $flexdir->save('likes/' . $hash, $object);
            } 

The logfile says:
grav.CRITICAL: Grav\Framework\Flex\FlexObject::__construct(): Argument #1 ($elements) must be of type array, string given, called in … line 131

This line is: 'page' => $page_route

Any ideas? I tried YAML instead of flex objects before, but didn’t work. Not sure if YAML is good for storing arrays of the same structure.

Thanks in advance.

1 Like

@assbach,

grav.CRITICAL: Grav\Framework\Flex\FlexObject::__construct(): Argument #1 ($elements) must be of type array, string given, called in …

The error is about the first parameter of the FlexObject constructor, which must be an array.

ResourceLocator::findResource() returns a string|false

i still don’t get it. $flexdir seems to be okay, i thought, cause the Exception is not thrown.
What array is it looking for? I cannot find any example code unfortunately.

@assbach,

The flex-object plugin provides example ‘Contacts’ which uses contacts.json

See

1 Like

The last link was the info and example i needed, thanks a bunch! Saving works now!
Now trying to check if the entry already exists.

@assbach, When creating an object using the FlexDirectory::createObject(data, key, ...), you can pass in a key as second param. This could be a hash of an IP address.

Using FlexDirectory::getObject(key) you can pass in a hash of the IP address of a new entry and test if the return value is null or not.

This way, you are not storing the IP address itself. And a hash cannot be converted back to the original IP address, which eliminates any privacy concern.

Of course a hash does have a slim chance of generating the same hash code based on different IP addresses.

Thank you, i tried using the hash as the key, but it didn’t work yet. Will try again.

Last thing that looked good was doing it this way:

$collection = $flex->getCollection('likes');
if ($collection) {
    $this->grav['log']->info('Collection exists');

    $collectionFiltered = $collection->filterBy(['hash' => $hash]);

    if ($collectionFiltered) {
        $this->grav['log']->info('$collectionFiltered exists');
    } else {

        // create new object here 
    }
 
}

What do you think?

And yeah, i know it’s not a perfect solution but a reasonable compromise. That is why i said “I know this is not a guarantee, but i don’t want to use cookies or sessions.”

Thank you very much for your help again!

@assbach, I didn’t have a look at the implementation of a FlexDirectory::getObject(key) and FlexCollection::filterBy(), but my gut feel says that FlexDirectory::getObject(key) is an index lookup while Collection::filterBy() is requires a loop through the entire collection.

Mhm, since i also want to delete an object when someone who liked the page (clicked the button) comes back later and unlikes, i think i need the key. My code just won’t use the hash as a key, what am i doing wrong again? Like you said i used $hash as the second parameter. No errors, but the key is a totally different hash. Is it something in the setup/YAML i need to fix? Its a copy from contacts.

$ip = $_SERVER['REMOTE_ADDR'];
$ip_masked = md5($ip);
$timestamp = date("Y-m-d H:i:s");
$url = $page_route;
$hash = md5($ip . $url);

$flex = $this->grav['flex'];
$flexDir = $flex->getDirectory('likes');

$newObject = $flexDir->createObject(
      [
      'masked_ip' => $ip_masked,
      'datetime' => $timestamp,
      'page' => $page_route,
      'action' => $action
      ],$hash
  );

  $newObject->save();

@assbach, I’ve been trying a few things and I give up…

I expected the $key value in FlexDirectory::createObject($data, $key, ...) to be used as the key of the object entry. It doesn’t seems to be like that and right now I have no clue what it does…

I also expected $key in FlexDirectory::getObject($key) to refer to the key used in createObject(). It seems it doesn’t…

FlexCollection::search() and FlexCollection::filter() do seem to work OK.

As said in another thread, I’m not a Flex user and it feels too complex to my liking.

How many entries do you think your “likes” is going to contain? If not too big, using a plain json file might suffice. Flex is used when using thousands of entries and performance using a json file is not good enough.

Thank you very much for testing and your support. Actually, i feel a bit relieved that i am not the only one who struggles with that.
There will not be many likes so performance will not be an issue i think. I first tried using a simple YAML file to store the data, but it didn’t work as expected (data overwritten instead of appended) and i was unsure if YAML is the right format for this job. a simple JSON file in /user/data/ would be perfect for me. Actually flex is saving a json-file as well…
Next thing would be setting up a mysql database, but i haven’t worked with the plugin yet.

@assbach, When using a yaml/json file, you’ll need to

  • load the file,
  • parse it into an array,
  • insert/append/remove item
  • convert array to yaml/json
  • save and override file.

Grav has a File class with which you can open/save a file and a Yaml class to parse/dump the content of/for the file.

I think i got it working with a YAML file now, thanks for the hints.
The only thing i need to achieve is, to assign a CSS-class to the “heart-icon” on page-load, when the visitor comes back and has previously liked the page, meaning the $hash exists in the YAML file.

@assbach, By the way, you’re not the first doing this. Have a look at the Comments plugin to get some inspiration. It may give you some suggestions for performance and passing data to Twig.

The only thing i need to achieve is, to assign a CSS-class to the “heart-icon” on page-load, when the visitor comes back and has previously liked the page, meaning the $hash exists in the YAML file.

The result of your processing of the “likes” file can be passed into Twig where a class can be assigned to the generated HTML.

$this->grav['twig']->twig_vars['enable_comments_plugin'] = $enabled;
$this->grav['twig']->twig_vars['comments'] = $comments;

To improve performance you can use Grav’s cache mechanism. Have look at the code of the Comments plugin#L349-L365:

  • Check if file is in cache
  • If so,
    • read it from cache
  • if not,
    • read the file
    • parse the json/yaml
    • write content to cache

Also, you could split up the “likes” in separate files named after the path of the page being requested.

Going for caching and performace maybel later.
I’m still having a couple problems:

  • how do i get the slug/route of the actual page the likebutton is on? let’s say “/test”. I tried these:
 $route = $page->route(); 
 $route = $page->slug();
 $route = $uri->getCurrentRoute();

but that is “/likebutton” (through ajax) when the button is clicked.
I need $route = "/test"; in any case

  • load plugin only if likebutton is on page (it’s included via twig manually atm, blog-page-template later), atm it’s loaded on every page
  • trying to render the button via plugin didn’t work, the html got shown on the page instead of being rendered (like escaped or something)

@assbach, This post seems to be drifting away from the initial question, which will lead to buried questions/answers, which:

  • are hard to find for other users.
  • cannot be marked as being ‘solved’.

I think your last question deserves a new post with its own suitable title.

As a kind request for the new post: A bit more clarity would be appreciated…

1 Like

You’re right regarding the topic and drifting off, i was hoping to make things clear and post the whole solution for others, but it has nothing to do with flex anymore, im sorry.