#1
I have created a project-based task manager module. Anyone who has followed along with the task manager tutorial videos will not be surprised by the overall structure. However, the module is based on created projects, and a submodule deals with tasks linked to the selected project.

It's working great. I thought I'd share it and I'd love feedback on better solutions. In the future I think I will keep the submodule separate for a project I will work on in the future. But I wanted to try it this way for this simple experiment.

https://github.com/PartialShawn/projects-manager-trongate



The major difference is that projects are slug based: they use a friendly URL ID (not currently validated as being safe URLs and just user entered). Minor differences are I use 'edit' rather than 'create' (or 'update') and list rather than manage or index in the url/function names/view names, as you can see above. Hence URLs like:



The tasks submodule, again, looks unsurprising, except the need to get the slug from the URL. For example, for task with ID 10, which happens to be part of project zeta, works as follows:



In many of these views I don't actually need the slug, as the ID numbers are unique. But they allow me to have the cancel button etc. work without querying the projects table.

So, here is how I did that, and I'd like to hear any comments about doing things a different way. Especially if it's a better, more efficient way to use slugs (friendly URL IDs).

First, I put the tasks module as a submodule because I intend to have the modules be separate in a project I will be working on, and wanted to make sure I got a taste for how it would work this way. The web app I will make will may be designed with a projects module, and then when working on the data for the primary module (not tasks, something else) it will require to work with the projects. Just not as a submodule. This may change.

So I get the slug data (and the slug can be edited, hence `$current_slug`) and use that rather than ID when querying the database.

https://github.com/PartialShawn/projects-manager-trongate/blob/main/Projects.php




This required changes to `get_data_from_db()` to query by slug rather than ID:

https://github.com/PartialShawn/projects-manager-trongate/blob/main/Projects_model.php



Now, for tasks I just use the ID, but I keep the slug in the URL to give a hint about what task is being worked on, and to make it easy to go to the project's tasks list on completion or cancellation. But when inserting a record, I do need an extra line to make sure I get the `project_id` which is added to the tasks table:

https://github.com/PartialShawn/projects-manager-trongate/blob/main/tasks/Tasks_model.php



Trivial example here, but needed for my larger future project I will use Trongate for. On the other hand, fetching tasks did require a touch more work, doing a left join with projects to query projects (since I don't have the `project_id` from the URL).



It occurs to me now that if I wanted to do anything different, I could just append the project ID number to the slug. Example: zeta becomes `zeta-13` to reduce the small load on the join. Creating a SQL view would also increase the speed of the application, I assume. Still, these queries are not very intensive.

But in my upcoming app, different users will have different access to each project. So in that case I would have to query the project table and make sure the user has rights to it. I'm not sure the best way to do that and keep the data--user a session or global variable in `make_sure_allowed()`. And that will need to know both the project and the operation on the project (view yes, delete no, etc).

If anyone has any comments, please send them along. I plan to do things like add custom routes and maybe use a language string resource file (PHP array?).
#2

The thread creator has indicated that this post solved the problem or provided the best insight.

Members who post winning responses earn points for our leaderboard.
Points mean prizes!
G'day mate,

Hope all is well. Nice module idea, and all works well from a git clone. The module structure is clean and easy to follow.
Keeping `tasks` as a submodule is perfectly reasonable for experimentation. However, for long-term scalability. Instead of tightly coupling via URL segments, consider passing project_id internally after resolving the slug once.

Add an index for your slugs, as lookups will degrade over time

Instead of joining every time in fetch_tasks():
1. Resolve slug → project_id in controller.
2. Pass project_id to model.
3. Query tasks using WHERE project_id = ?.
This removes the join entirely.
Example pattern:That is faster, cleaner, and easier to secure later.

(zeta-13) - Appending an `id` to slug works and reduces lookup cost, but:
• It exposes internal IDs.
• It adds parsing complexity.
• It becomes redundant if the slug is indexed.
I would not recommend this unless SEO is critical.

Slug Validation (Important) - “not currently validated as being safe URLs and just user entered”

This must be addressed - At a minimum you can start via our string helper: url_title()
• Enforce lowercase
• Replace spaces with hyphens
• Remove non URL-safe characters
• Enforce uniqueness

Docs - https://trongate.io/documentation-ref/information/helpers/string-helpers/url_title

A permissions hierarchy for users needs to be sorted - Don't do it via SESSIONS or rely on a slug alone. The new TGv2 members module will be a good start here.

By default, a new Trongate app (v1 or v2) has security relaxed - hence you experiancing no effect with Edit `config.php` and set `ENV` to anything other than `dev` and you will see the administrator login - default user `admin`, pw `admin`.

You mentioned, “Creating a SQL view would increase speed”

A view does not improve performance. It is just stored SQL. Performance comes from:
• Proper indexing
• Avoiding unnecessary joins
• Querying by integer keys
So removing the join is more impactful than creating a view.

Avoid die(); in models, as they should not terminate execution - throw an exception or return 'false'. Let the controller decide the logic.

Overall, very solid work for a learning module.

Cheers.
#3
Thank DaFa, I love the critique. I was coming here to add a post discussing custom routes, which might relate to what you were saying. Anyway, I wanted human-readable URLs so that people working on a project (or the public interested in a public) can easily see which project and data-item (in this learning project, task ID) is being linked to at any point. (Not relevant to this learning TG app, but for the one I hope to make this year. So:



Technically, confirm_delete could use the real URL, but I was on a roll. The custom_routing.php is pretty straightforward:



One thing that messed me up a few times was generating links with trailing slashes or adding a trailing slash to a custom route would prevent a match. An easy mistake to make.

Now, DaFa, when discussing appending the ID to the slug, you said "I would not recommend this unless SEO is critical." I was wondering if you have any similar concerns with this, here. Or is this fine.

Having done this, I realize in my real project I'd rather the project slug be segment 1 and just avoid use of 'project' at all. But I have other things to deal with first.

Anyway, I appreciate what you wrote DaFa and plan to go back over it, soon. Any other comments from anyone else would also be apprecaited.
#4
I don’t think you’ll get a better response than Simon’s there, and I’d encourage you to award him the win for this thread.

For what it’s worth, I spent about five minutes reading through your code on my phone last night. At the time, I wasn’t able to respond or test anything properly, but I can offer a little feedback:

First of all, I think it’s probably the most beautiful PHP code I’ve ever read. Everything feels organised and tidy. I know I’m slightly biased, since it’s very close to what I covered in some recent YouTube tutorials, but even so, I was genuinely blown away by how clear and easy it was to follow. Seeing the model file used correctly is great too. I’m really glad v2 offers that as an option. Well done!

With regard to the URL slugs and whatnot, that’s a bit too deep in the weeds for me at the moment, so I can’t really comment.

Simon’s point about avoiding table joins is very interesting. I’ve never seen that technique or theory formally published in books or tutorials, but I have tried it in the wild. It works. He's right. However, if your database isn’t massive and you’re comfortable using joins, I think it’s perfectly reasonable to stick with what feels right for you.

Regarding the auth side of things, I received an email this week from someone who was struggling to understand how “auth and auth” works in Trongate v2. That suggests to me that either the documentation isn’t clear enough or it needs corrections. I’m planning to revisit that chapter to make sure everything is as clear as possible.

In the meantime, if you’re happy to explain exactly what you’re trying to build from an authentication perspective, I may be able to share some example code or even put together a YouTube tutorial.

Hope that helps.

DC
#5
Good question.

No, I don’t have the same concerns with your custom routing approach. What you’re doing there is perfectly fine. Appending an ID to the slug changes the identity model of the resource. You end up leaking the primary key and introducing parsing logic purely for performance reasons.

Your custom routes are presentation only. They make the URLs clearer without changing how records are actually identified in the database. A URL like project/gamma/task/14 is clean and readable. Internally, you can still resolve the slug once, convert it to a project_id, and then work purely with integers from that point forward. That separation between presentation and persistence is the important part.

On DC’s point about avoiding joins, what I was suggesting is less about dogma and more about efficiency at scale. Joins are absolutely fine and are a normal part of relational databases. On a small or medium dataset, you will not notice any issue. The idea of resolving the slug once and then querying by project_id simply removes unnecessary work from repeated queries. It becomes more noticeable as data grows or when permissions checks get layered in. It is not that joins are “bad”, just that if you already have the foreign key, there is no need to ask the database to rediscover it every time.

Your routing approach doesn’t conflict with that at all. You can keep expressive, human-readable URLs and still keep your internal queries lean. The two concerns are separate.

The only practical things to stay mindful of are route ordering and consistency with trailing slashes. If you eventually drop the project prefix and let slugs live in segment one, you will need a reserved word strategy so they don’t clash with real module names.

Overall, you’re on solid ground. Clean URLs, resolve once, work internally with IDs, and only optimise further if the data size or complexity actually demands it.
#6
Separate queries are good for another reason: when adding HTML titles I realize I should have the project name. Even if I added it to the join, that wouldn't work when there are no tasks at all! So for me that will be the better option.

Thanks for all the opinions.