# Building Links and Routes

https://dev.wisecp.com/es/building-links-and-routes

Addresses are generated from a route key, never written out, because every route is translated and the panel directory is not fixed.

## Overview

A route key is a name; the address it produces depends on the language and on the installation. A literal path works in one language and answers 404 in the others. A panel link that hardcodes the admin directory breaks on any installation that renamed it.

Four static methods cover every case, all of them in `coremio/classes/LinkGenerator.php`.

## Reference

```php
public static function admin($route = '', $params = [], $lang = '', $wqs = []): string|bool;
public static function client($route = '', $params = [], $lang = '');
public static function wQS(null|string|bool $url, string|array $params = []): string;
public static function convert_to_link(string $arg): string;
```

- **admin()**: A panel address. The only one with a fourth argument. That makes it the only one that can append a query string on its own.
- **client()**: A website or client-area address. Three arguments, no query string; wrap the result in `wQS()` when you need one.
- **wQS()**: Appends a query string to an address that already exists, choosing between `?` and `&` itself. The second argument must be an array in practice.
- **convert_to_link()**: Resolves a stored reference such as `pages/12` into a full address. This is what makes a menu entry or an editor link portable. Results are memoized per request and language.

### The Four Arguments

| Position | Argument | What it must be |
| --- | --- | --- |
| 1 | `$route` | A key from the route table, not a path. An unknown key is not rejected, it is pasted into the address as is. |
| 2 | `$params` | Positional path segments. Each value fills the next `(?)` in the key's pattern, in order; leftovers are dropped silently. A non-array value is wrapped into a one element list. |
| 3 | `$lang` | `''` uses the language being served. A code such as `tr` forces that language, and the literal `none` produces an address with no language segment at all. |
| 4 | `$wqs` | Query string as an associative array, applied only when it is non-empty. Present on `admin()` only. |

### A Route Key Carries Its Segment Count

The router fills one `(?)` per parameter and stops. A key whose pattern has no placeholder ignores every parameter you pass. That is why the list and the detail address are two different keys rather than one key with arguments.

```php
// coremio/locale/en/admin-routes.php - key => [address pattern, controller route]
return ['admin-routes' => [
    'services'   => ['services',         'services'],
    'services-1' => ['services/(?)',     'services/(1)'],
    'services-2' => ['services/(?)/(?)', 'services/(1)/(2)'],
]];

// LinkGenerator::admin("services",   ["detail", 5])  ->  /services        both values dropped
// LinkGenerator::admin("services-1", ["detail"])     ->  /services/detail
// LinkGenerator::admin("services-2", ["detail", 5])  ->  /services/detail/5
```

- **locale/{lang}/admin-routes.php**: Panel keys. One file per language, so the same key produces a different path in each.
- **locale/{lang}/website-routes.php**: Website and client-area keys, same shape.

### What convert_to_link Accepts

| Form | Example | Resolves to |
| --- | --- | --- |
| Site page key | `home`, `contact`, `basket` | That website route in the active language. |
| Client-area key | `ca-invoices`, `ca-domains`, `ca-tickets` | That client-area route. |
| Record reference | `pages/12`, `products/48`, `kbase-page/7` | The record's detail address, looked up from its translated slug. |
| Category reference | `category/25` | The catalogue, article, reference or knowledge base listing, chosen from the category's own type. |
| Product group | `product-group/hosting` | That group's catalogue page. |
| Anything else | `whatever` | The string `#`, so a broken reference appears as a dead link instead of an error. |

## Example

```php
// A list page: no parameters, no query string.
$list = LinkGenerator::admin("services");

// A detail page. The record id travels as a query string, so the fourth
// argument is used and the third stays empty.
$detail = LinkGenerator::admin("services-1", ["detail"], '', ['id' => $serviceId]);

// The same page opened on a tab.
$tab = LinkGenerator::admin("services-1", ["detail"], '', ['id' => $serviceId, 'tab' => "invoices"]);

// A website address forced into one language, for an alternate-language tag.
$signIn = LinkGenerator::client("sign-in", [], "tr");

// client() has no query string argument, so wrap it.
$paged = LinkGenerator::wQS(LinkGenerator::client("services"), ['page' => 2]);

// A stored reference from a menu row, resolved at render time.
$href = LinkGenerator::convert_to_link($row["page"]);       // "pages/12" -> /about-us
```

```php
// The second element of the entry is the controller route the address maps back to,
// which is why the key, not the path, is the thing code is allowed to know:
//
//   'services-1' => ['services/(?)', 'services/(1)']
//                     ^ written by       ^ read by the router, which dispatches
//                       LinkGenerator       services::page_detail()

class Services extends Controllers
{
    public function page_detail(): void
    {
        $id = (int) Filter::init("GET/id", "rnumbers");
        $this->addData("back", LinkGenerator::admin("services"));
    }
}
```

## Pitfalls

> **The query string is the fourth argument, the third is the language**
> 
> Writing `LinkGenerator::admin("services-1", ["detail"], ['id' => 5])` puts the array where the language code belongs. Nothing is thrown. You get an `Array to string conversion` warning and the address `/Array/{admin}/services/detail`. The path segments in it still resolve, and the id you meant to send is nowhere. Pass `''` for the language and keep the query string in position four.

> **An unknown key becomes the address**
> 
> The router returns the key itself when it is not in the table. A typo then produces a plausible looking address that answers 404, not an error at the point of the mistake. The same happens when a key exists but has fewer placeholders than the parameters you passed. The extra values vanish there.

> **wQS takes an array, whatever the type says**
> 
> The declared type of the second argument allows a string. The value is still handed to `http_build_query()`, which rejects anything that is not an array or object with a TypeError. Never concatenate a question mark by hand either: the generated address may already carry parameters, and you would produce a second one.

## Related Articles

- [Controllers and Routing](https://dev.wisecp.com/en/controllers-and-routing)
- [Translations and Language Files](https://dev.wisecp.com/en/translations-and-language-files)
- [Menus and Navigation](https://dev.wisecp.com/en/menus-and-navigation)
- [Utility Helpers](https://dev.wisecp.com/en/utility-helpers)
