Adding a link to a PHP page in Grav Admin UI

I’m trying to add a link on the left hand navbar of Grav Admin that points to a PHP script I wrote that does some server related tasks.

I created a plugin and then followed the “Extending” instructions to create a template to override the UI (I put a custom link in the side navbar via nav-quick-tray.html.twig in my plugin directory, under /admin/themes/grav/templates/partials/).

Now I just want to have that link point to the PHP script that I wrote and run that. It does some server related tasks.

  1. Where do I put my PHP script in the filesystem so Grav can see it and run it when I click on the link?
  2. What would the URL / “route” be for that link? Where do I define that?
  3. Or, is there a better way to accomplish all this?
1 Like

In Grav Admin you can’t point the sidebar straight to a raw PHP file — all requests have to go through Grav’s routing layer.
The usual approach is to add a custom admin route in your plugin:

  1. Put your PHP logic in a plugin method (e.g. onTask.mytool() or a controller-style class inside your plugin folder).

  2. Register an admin page or task route in your plugin’s onPluginsInitialized() — for example:

    if ($this->isAdmin()) {
        $this->enable([
            'onAdminPage' => ['onAdminPage', 0],
        ]);
    }
    
    

    and map /admin/mytool to your handler.

  3. Place any Twig/HTML under plugins/yourplugin/admin/themes/grav/templates/ so the Admin UI renders it.

  4. Point your sidebar link to /admin/mytool.

Don’t drop a loose PHP file under /user or /system — it won’t be routed or protected.
If you only need a backend action with no UI, register a task URL (e.g. /admin/task:mytool) and handle it in onTask.mytool().

See the Grav docs: Extending the Admin plugin — it shows how to add pages and tasks the “Grav way.”

1 Like

Thank you, @Maria1, I will give this a try! I appreciate the details.

I think I need a bit more clarification since I’m so new to Grav.

Does the method name need to be the same as a particular URL? I’m not sure I understand what you mean by the “onTask.mytool()” example you gave.

This I was able to do. But I do not understand how “onAdminPage” maps to a URL…?

Where and how do I do this? What is a “handler” in Grav vernacular?

Does the twig file need be named the same as the method?

If I point the link to this URL, how does it map to the above plugin method and / or mapped handler?

Anyone able to help me with this, please?

I did manage to figure out that the method I created in onPluginsInitialized() needs to be the same name as “onAdminPage” in “Maria1’s” example. So I now have a method called onAdminPage() that runs when I access any admin page. Which is sorta correct, but not entirely, since I only want to run that method when I access a certain URL.

Which brings me to my next point. I also managed to figure out that in the plugin’s yaml file I’m supposed to define a “route:” and that can have a URI path, which I did. But it shows a 404.

Please help.

Check how Login plugin deals with URLs like /register and others. Might give you some hints

Give me hints, yes, answers, no. That Login plugin has a of code and conditions.
I’m simply trying to add a link… :laughing:
Any chance someone could just help me with specific instructions, please?
I’ve almost got it working, I think, but I just need a little more help.

Again, I’d just like to add a way to click a link and run a script and display it’s output in my admin plugin page. I’m not looking to write a huge, complex plugin, just a way to pass the link URL request to my plugin code and return the output of a PHP cli script into the plugin page. Thanks again.

The best and most secure way to achieve this is by using your Grav plugin to create a custom Admin route and controller, rather than linking directly to a raw PHP script. This leverages Grav’s built-in security and architecture.

Here is a breakdown of the recommended approach, which answers your questions.


The Better Way: Create a Custom Admin Route and Controller (Recommended)

Directly linking to a standalone PHP file is discouraged in a CMS environment like Grav because it bypasses the system’s authentication and initialization process, posing a security risk.

1. Where to Put Your PHP Script Logic (Controller)

Your server-related PHP tasks should be integrated into your plugin’s main PHP file (your-plugin-name.php) or a dedicated class file within your plugin. You will use Grav’s Event Hooks to execute this logic.

  • Location: user/plugins/your-plugin-name/your-plugin-name.php

2. What Would the URL / “Route” Be? Where to Define That?

You define the route and the logic that handles it within your plugin’s main PHP class.

A. Add the Navigation Link and Define the Route

Use the onAdminMenu() event in your plugin to register your new navigation item. This will automatically add the link to the Admin sidebar.

In user/plugins/your-plugin-name/your-plugin-name.php:

PHP

<?php
namespace Grav\Plugin;

use Grav\Common\Plugin;
use Grav\Common\Uri;

class YourPluginNamePlugin extends Plugin
{
    /**
     * @return array
     */
    public static function getSubscribedEvents()
    {
        return [
            'onAdminMenu' => ['onAdminMenu', 0],
            'onPagesInitialized' => ['onPagesInitialized', 0],
        ];
    }

    public function onAdminMenu()
    {
        // Add a new item to the Admin sidebar navigation
        $this->grav['twig']->plugins_hooked_nav['PLUGIN_YOUR_PLUGIN_NAME.MENU_TITLE'] = [
            'route' => 'server-tasks', // <-- This is your custom route segment
            'icon' => 'fa-tasks', // Use a FontAwesome icon
            'authorize' => 'admin.super', // Restrict access to SuperUsers
            'label' => 'Server Tasks',
        ];
    }

    // ... (Your custom link in the Twig template is now easier)
}

  • Custom Route: The route you’ve defined is server-tasks. The full Admin URL will be /admin/server-tasks.

B. Handle the Route and Execute the Task

Use the onPagesInitialized() event to intercept the request when a user visits your custom Admin URL and execute your PHP task logic.

In user/plugins/your-plugin-name/your-plugin-name.php (inside the YourPluginNamePlugin class):

PHP

    public function onPagesInitialized()
    {
        // Only proceed if we are in the Admin and the custom route is requested
        if ($this->isAdmin()) {
            /** @var Uri $uri */
            $uri = $this->grav['uri'];

            // Check if the Admin path matches your custom route
            if ($uri->path() == '/admin/server-tasks') {

                // Prevent Grav from trying to look up a page based on this URL
                $this->grav['admin']->disablePages();

                // ----------------------------------------------------
                // **EXECUTE YOUR SERVER TASK LOGIC HERE**
                // ----------------------------------------------------
                $output = $this->runServerTask(); 
                
                // You can output the result directly or render a Twig template
                echo "<h1>Server Task Report</h1><pre>" . htmlspecialchars($output) . "</pre>";
                
                // Stop further Grav processing after outputting
                exit();
            }
        }
    }

    protected function runServerTask()
    {
        // Your PHP script logic to perform server tasks goes here.
        // E.g., executing a command-line utility:
        // $result = shell_exec('php /path/to/your/cli/script.php');
        
        return "Server task executed successfully.";
    }

3. Updating the Twig Template (nav-quick-tray.html.twig)

Since you registered the menu item with onAdminMenu(), Grav’s Admin plugin will automatically render it in the sidebar. You typically do not need to manually override the entire nav-quick-tray.html.twig template just to add the link, unless you want to place it in a very specific custom location within that template.

If you must use a link within your custom nav-quick-tray.html.twig partial, you can use the route you defined:

Twig

<a href="{{ url('server-tasks') }}" class="button">
    <i class="fa fa-fw fa-tasks"></i>
    <span>Server Tasks</span>
</a>

Wow, this is fantastic, thank you, @PeterFu!

I really appreciate your friendly tone and clear instruction with this. Some of us are very new to Grav but not new to web development, and in some cases the Grav documentation is just not extensive enough, and I found this to be the case with this situation (I did read it all).

This greatly helps me understand how to accomplish adding custom admin elements.

It works! Thanks very much.

I do have a couple of remaining questions, if you don’t mind.

  1. I get an error that says "Call to undefined method Grav\Plugin\Admin\Admin::disablePages(). I’m on Grav v 1.7.50 and Admin v.1.10.49.1. If I comment that line out, it works fine, but I’m wondering if there’s a way to get it to work with that function call being in place.

  2. If I wanted to retain the admin UI “wrapper” on my plugin page (ie. to get my plugin output to appear in the right hand side of the Admin UI, like the other Admin pages do, and retain the left hand nav, etc), would I need to put some Twig includes into my custom plugin Twig file?
    I still have the following function which references my plugin’s Twig file directory:

public function onAdminTwigTemplatePaths($event): void
{
    $paths = $event['paths'];
    $paths[] = __DIR__ . '/admin/themes/grav/templates';
    $event['paths'] = $paths;
}

But it doesn’t seem to be using the Twig file I put in that directory (named the same name as I’m using as the 'route' in onAdminMenu(), appended with .html.twig - for example, server-tasks.html.twig). I understand it’s not using the Twig file, though, since the code is running within the above functions you provided, executing the script there, and bypassing any Twig files.

Is there a way to use a Twig file to format the output of my command line script? Could I embed the $output variable into it, for example?

I looked and there are a lot of template files in user/plugins/admin/themes/grav/templates, and I’m curious which ones to look at to determine how to do this.

Thank you again.