URL Query Parameters

Hi.

The book (PHP & MySQL: Novice to Ninja 6) creates a website with only a few pages each of which tends to display some/all rows in a table in the database, such as all the jokes in the jokes table.

I have a different requirement, not covered by the book.

I have database tables in which each row is a page in itself. For example, I have a table “services” which contains close to a hundred rows, each of which contains all the information about a single service a company offers.

I have set up my navigation to loop through the services table and display a list of links, one for each page. Each link containing the row id. I have created a servicesController which has, an action called display which accepts a query parameter, uses a findById DatabaseTable method to get THAT service and display it.

All this works wonderfully.

My question is this:
I was looking at a specific service page yesterday. I got to the page by clicking a link in my navigation and it produced the following url in the browser’s bar: www.domain.com/service/display?id=1. I copied the url from the address bar and sent it to a friend so he could take a look at the page. I hadn’t anticipated his reply but once I had it it made perfect sense.

He said he had clicked the link in his email and was taken to www.domain.com. The site’s homepage.
And of course, I realised there is code in the Framework which grabs the url UP TO THE QUESTION MARK. My links work internally because I am passing the id to a function (no urls invloved). This is how routing works in the framework: no query parameters.

Argh!

  1. How can I achieve the ability to send a link to a specific page to someone else?
  2. Am I handling my requirement (to be able to display a single table row as a page rather than display the whole table as a page), correctly within THIS framework?

Thanks, Mike

It sounds like either your sending email client/server or the receiving email client/server didn’t carry the full url. Does the person you sent this to SEE the full url either as text (which would indicate the email is not being treated as html and a click on it should not do anything) or as a link (which would indicate the email is being treated as html) and for a link, what do they see when they hover over it with the mouse cursor?

1 Like

mabismad,

Thank you for your reply, but no, the full url is being emailed and received. The problem is that the framework I’m using grabs the url up to the question mark (i.e. no query parameters) so not sure how to handle a display by id situation.

Code you are citing that’s getting the part of the url up to the ? is (probably) just determining which control logic to use.

That the other person ended up back at the homepage is probably because the value on the end of the url contains some additional character(s), such as a period that got typed as part of the value in the email, and the code didn’t find any matching data to display.

It would take seeing all the offending code that would be needed to reproduce this problem, in order to help.

1 Like

Thanks again for your reply.

Here is the code that gets the route

$route = ltrim(strtok($_SERVER['REQUEST_URI'], '?'), '/');

You are right in that this code DOES determine which logic to use but, as you can see, it works with the request url up to the question mark only.

That line of code is (should be) executed the same for both you and the other person. If a URL with ?id=1 on the end of it works for you in your browser, but not for someone else, the source of the problem isn’t what that line of code is doing.

1 Like

That is correct, the part before the question mark determines what controller needs be used. The rest of the URL (i.e. the query string) then is used inside that controller to determine what to load. The query string cannot be used in routing in this framework.

Like @mabismad said - if a URL works on your machine it should work on somebody else’s machine as well.

One thing to think about, is to change the way URLs are structures, because /service/display?id=1 doesn’t really tell me much. Something like /service/html-coding for example would be a lot nicer as a URL.

The framework laid out in PHP Novice to Ninja doesn’t support such URLs, but it should be possible to add them - or maybe swap out the router completely for something more powerful.

1 Like

rpkamp,

Thank you for getting back.

You are both correct. I feel a little embarrassed now. I looked at this briefly last night and it didn’t seem to work, as my friend had said, but this morning it does. And now I feel like an idiot!

Apologies to you both for that. Not sure what happened there: I’ve marked you both with likes.

This is intriguing me though. I agree, your url is a lot nicer:

I don’t want to swap out the router yet, maybe later but I’m still learning to use what I’ve got and don’t want to bite off more than I can chew.

Could you give me some detail about

This is a big issue with me and something my customer would appreciate. All these ID’s in query strings is ugly and, as you say uninformative. How would this be done? I’ve never done it before so if you could dumb it down a little…

This is what I’m doing after I’m sent to my $controller->$method by my routes code:

public function display()
    {
        if (isset($_GET['service_id'])) {
            $service = $this->servicesTable->findById($_GET['service_id']);
            $service_group = $this->serviceGroupsTable->findById($service->service_group_id);
        }

        return [
            'template' => 'displayservice.html.php',
            'title' => $service->page_title,
            'variables' => [
                'service_text' => $service->description,
                'service_title' => $service->page_title,
                'service_group_title' => $service_group->page_title
            ],
        ];
    }

It grabs some data then identifies the correct template which is a single template handling all services by id.
Is this how you would have handled the issue of having a one page to one table row match: one service is one row in my database table services.

I feel the tidy url and the method of handling this issue are intrinsically linked.

Mike

As a baseline, yes.

One thing I note is that there is only happy path in your code. It assumes that everything being asked is always there.

  • What if $_GET['service_id'] is not set?
  • What if there is no service with ID $_GET['service_id']? Or what if was there but has since been deleted?
  • What if there is no service_group with ID $service->service_group_id?

Probably in most (or all?) of these case you should present the user with some sort of 404 page not found page - possibly offering suggestions of similar services.

Anyway… :slight_smile:

What you’d need to do to use a bit nicer URLs is to use a concept most people call slugs - it’s basically a short string that is used in a URL for the purpose of identifying a page. So you’d use it in the URL instead of the ID. The ID still stays though, because you need a way to uniquely identify all records in a way that never changes - a slug might change. So in my previous example /service/html-coding, html-coding is the slug.

So as a first step I would recommend:

  1. Add a slug to the services table, set a value for each row
  2. Change the URL to /service/display?slug={slug} (instead of ?id={id})

Once you have that working we can go into the next step of how to modify the router to work with this.

1 Like

You can just use a text field from the database as an identifier rather than an ID, that way you have complete control over what is in the URL. Keep in mind you’ll need to ensure the value is unique for each record.

1 Like

rpkamp,

Hello my friend; thank you for getting back to me.

Absolutely. I’m trying to keep the code minimal for now

  • until I have grasped the logic
  • and I happen to KNOW that it IS set
    but, yes, production wise, this is not good enough.

OK, I get you with your slugs. I have implemented that, using a simplified version of the page title as a slug. I have also changed some of the in site links to use the new format.

My links now look like this:
www.domain.devel/service/display?slug=mes-systems

I have two issues with this:

  1. It’s not as nice as your url (/service/html-coding). I realise, currently, I need “display” which is the name of the method in my controller that handles the display of ALL services… (unless you have some magic to come…)
  2. I wouldn’t want the word “slug” in the url with all of it’s negative connotations.

But, hey. I’m with you so far. Wondering how we “still have the id” now it’s not in the url and, if we don’t, what about Tom’s comment regarding the slug needing to be unique?

Tom,

Thank you for chipping in with this. I suppose I could make that column a Primary Key which would stop any duplicates…

As I said in the other thread, you’ll need some additional logic if you want to remove the query string entirely.

Instead of using $_GET, split $_SERVER['REQUEST_URI'] on / and decide what to do with each part so you split /foo/bar/baz into an array with 3 entries, then add some logic to determine what to do with the URL. A simple approach is that [0] is the controller name, [1] is the function name in the controller, and the rest of the entries in the array are arguments passed to the function.

1 Like

You already have a primary key, ID. And that should remain. And since a table can have only one primary key adding a primary key is not possible. You can however add a UNIQUE key, which also prevents duplicates the same way the primary key does.

I know, but we’re doing this step-by-step :slight_smile:
(on purpose - to prevent conflation of different concepts into a giant bull of mud)

Now, we can go one of two ways:

  1. We can either make the router more generic so that it can handle the pretty routes. This would be quite involved, and in the end you’d be better off swapping out the router completely for something else that’s tried and tested.
  2. We can make the URLs pretty through web server configuration. Do you know which web server you’re running? It’s probably either Apache or NGiNX

Option 1 is hard but gives full control on the PHP side while option 2 is easier but puts the control in the server instead of in PHP.

My recommendation would be to go for option 2 now and then once you’ve more got the hang of how this routing works swap out the router in your application to go from option 2 to option 1.

But it’s up to you :slight_smile:

TomB

So www.domain.com/service/display/3 would give me server and display and 3. I could use the 3 to work with the id of the row in the services table. If I used urls like www.domain.com/service/display/serviceName I could, as rpkamp says use a UNIQUE key on the serviceName field and as you said earlier use this field instead of ID to get the row.

So our router would use [0] and [1] for $controller->$method and pass [2] in $variables to the controller?

rpkamp

I very much appreciate your step by step approach. It is all a bit like juggling with 10 balls at the moment as I fully understand each ball and I can put it down and pick it up as needed instead of keeping them all in mind… a giant ball of mud. You get me. Cheers for that.

TomB (also immensely helpful) is chipping in as you can see and it seems like you’re both on the same lines with your option one. I have some experience of your option two (if we going down the url rewriting road here - and it’s Apache that I use) but I’d rather keep control within PHP.

If I’m understanding Tom (see my reply to his last post) is it that hard? Let’s bite that bullet anyway.

Thanks again for your step by step approach. Let’s take another step. My gut is telling me not to swap out the router as I do understand it. Can we work with it?

Mike

something like this would do what you want:

$parts = explode('/', $_SERVER['REQUEST_URI']);

$controller = array_unshift($parts);
$method = array_unshift($parts);

$page = $controllers[$controller]->$method(...$parts);

You’ll need some error checking and an array of all the possible controllers (or if you want to get more advanced, a Dependency Injection Container to create the controller)

1 Like

TomB,

Thanks for the reply.

Right… none of that looks familiar to me. I’ll get into the PHP docs and try to understand what you’re doing here.

While I’m doing that legwork… regarding the controllers… I don’t want to get more advanced :slight_smile: . Can you explain why I would need the array of all possible controllers and where in the framework I would put that to help me understand how to implement that, please?

Thanks, Tom.
Mike

Let me just post a suggestion out of left field here. Feel free to ignore. The Ninja book is a good introduction to assorted php concepts but I don’t think it was intended as a basis for a production ready framework. Parsing URLs and such suggests you are getting just a bit too far down into the weeds and perhaps reinventing a bit too many wheels.

This might be the time to consider switching to a real framework such as Symfony. With Symfony, a bit of code like:

class ServiceController extends AbstractController {
    /** @Route("/service/display/{$id}")
    public function display($id) {
        // And do what you need to do to retrieve and display

The frameworks takes care of all the boring routing details and handles the many many special cases you might encounter. /service/display/1 ends up calling the above method with $id being ready to use. Other frameworks offer similar functionality.

I know you have expressed concerned over giving up the time you have already sunk into learning the Ninja way. But that is really just a fact of life in programming.

Regardless, this article might help explain a bit about the http request/response cycle and perhaps help you around your current roadblock.

3 Likes

Well, let’s take Tom’s solution line by line:

$parts = explode('/', $_SERVER['REQUEST_URI']);

(which for consitency sake I’d change to $parts = explode('/', strtok($_SERVER['REQUEST_URI'], '?')); but that’s details)

What this does it takes the URL and creates an array from all the parts. So let’s say we have a URL services/display/serviceName that will be converted to an array ['services', 'display', 'serviceName'].

$controller = array_unshift($parts);

This takes the first element of the array, assign it to $controller and then remove the element from the array. So to continue our example $controller would be services and $parts is now ['display', 'serviceName'].

$method = array_unshift($parts);

Same as above really, so $method is display and then $parts is just ['serviceName'].

$page = $controllers[$controller]->$method(...$parts);

Okay, let’s break that down; it assumes we have an array of controllers, indexed by name. So for our example it would look at $controllers['services'], so we’d need to define that somewhere inside the router. Like:

$controllers = [
    'services' => new ServicesController(),
];

So now $controllers['services'] just gives us that ServicesController. Now, the next part ->$method( calls a method on that controller, in our example this would be display.

Then lastly it will call the method with the argument serviceName (... means convert an array to separate parameters).

So basically what would be called at the end of all this is (new ServiceController())->display('serviceName');

And then inside the ServiceController’s display method you can use serviceName to find the correct record in the database, and from that point it’s pretty much the same as you had with an ID.

While this approach works it’s quite rigid. For example, if you wanted services/serviceName for example, that’s not possible, there has be a method name there. This why my recommendation remains that maybe not now, but then at least some time in the future, to swap out the router for something else.

That would be awesome, but let’s try and walk first before we go running :wink:

1 Like