I've been working on this almost all day. Every time I try to call another module I either get a nasty deprecation message when I use the static call code I found in the docs or I get an error telling me that I'm missing a __construct() parameter when I call my class. I've also gotten whacky error messages telling me that the view "presenter/myview" couldn't be found when I pass in "myview" to the presenter.
So, how do you properly call another module's presenter compiled view when using themes and modules? The only way I've managed to do it is to have the template in APPPATH/views and that won't work as it needs to use the specified theme's version of the template.
I'd like to know how I can call anything from any other module in the future as well. I've tried statically calling my presenter and controller directly as stated in the docs and I get a message telling me that the static method is deprecated. I've tried calling the method directly as an object and I get __construct() errors for \View and \Presenter. I've tried using the theme's presenter() method and view() method and it can't find the file unless it's in APPPATH/views. I've tried using \Request::forge('PATH HERE', false)->execute() and I get a 404 error. (I read the message about the false parameter and it didn't fix it. It doesn't show the routed 404, but the default one instead.
I'm going nuts trying to figure this out. Please help. Also, before you ask, yes, I've loaded the module right before I tried to make a call to it.
Packages contain classes, and those could, like the one's in the Fuel core, be called statically.
Modules are front-end componente, containing controllers, which must be called through a Request. Obviously, you can add other (helper) classes to a module too, and those from a technical point of view can be called statically without problems. Although it is frowned apon, since it creates tight coupling between the modules.
The other problem you have is what is called "Request context". When you call a class in a module from app, the request context is still global. You'll using a PHP static call, so there is no way the framework could know it all of a sudden needs to use different views or theme folders to find files.
The request context is set by Request:forge().
The correct way to call a controller action is:
try { // use the false parameter if there is a route defined that would block access to this URI $result = \Request::forge('some/other/controller/action')->execute()->response()->body(); } catch (\HttpNotFoundException $e) { // url called does not exist or is not routable }
then $result returns whatever the controller action in the module returns.
This also tells you it will not work with template or theme controllers, since those return pages, while this kind of requests are usually used for widgets, returning parts of a page.
Thanks for that. What I'm trying to do is have the menu compiled in one place and have modules access the menu partial. The menu must use themes. Wouldn't a module be the appropriate place to place the menu partial that all modules use?
FWIW, I've decided to abandon the use of modules since it appears that they aren't able to be used as described above no matter how hard I try. As FuelPHP version 2 removes the module functionality, abandoning the concept is probably in my best interest anyway to make future upgrades that much easier. Even though someone could route to a page component via the URL, I doubt it will matter, especially if it's programmed with that knowledge in mind to prevent security holes. I just wish I could use modules and call custom functions instead of using action_ calls.
All our apps work this way, they have a single controller in the app (called loader) which is routed to on all requests. It loads a widget list from the database for the requested URL, and uses HMVC calls to collect all widgets which are then send to the theme template (a widget is just a wrapper class for a View or Presenter with some custom additional methods and data).
What doesn't work is: - call cross-module (or app to module) the PHP way, like $result = \Module\Some_Controller::some_action(); - do an HMVC call using Request but don't use the called action in isolation, but attempt to do Theme stuff in there
Nice. That did the trick. It took me all day to hammer out the specifics, but now it's finally working and in a way that's perfectly manageable. For anyone else who comes across this thread, below are a few tips for you. ORM really helps out to make pulling widget data easy.
1.) I forgot that there's a method called \Request::is_hmvc() that will tell your program if you called the controller via the URL or through \Request::forge(). This is how you can make it so that your module doesn't show in the URL. If someone calls it directly, just write:
throw new \HttpNotFoundException();
That will force the system to rended your routed _404_ error page which you must make use your loader to call it via HMVC. (In other words, you won't have it call the FuelPHP 404 error page when the system realizes that it will create an endless loop.)
2.) FuelPHP has TONS of concepts and how you handle those concepts really matters in the long run in what's compatible with what and how. As Harro was discussing, widgets are just template partials and in turn modules may be used as widgets. (I took that route.)
My original setup was trying to use template partials everywhere and that's what was throwing off my design. Only use template partials in your loader file. Use get_template()->set_safe() to set your frame template's partials to the concatenated output of all of your widgets.
Your module controllers need to return the Response object for the view, or use a presenter to return the view and then have the controller call it. (Obvious from reading the docs) The kicker comes in that you do not set theme partials or the sections for whole pages in a module. Modules should only return a portion of the page, such as a parsed menu template. FuelPHP modules in this case (maybe in every case) cannot be used as sections of your website. For instance you cannot have an account area module and a sales website module.
3.) If you're confused about the routing portion, try the entries below. (I've changed the names to be more generic.)
'_root_' => 'loader/index', // The default route '_404_' => 'loader/index/error/404', // The main 404 route '(:any)' => 'loader/index/$1', // Any other pages
The last entry allows you to use Apache ErrorDocument calls to set where your errors should wind up. You'll of course need to validate that in your APPPATH's loader controller's action_index(). To get the router translated path, use \Request::active()->method_params for the array of parameters. That omits loader/index from the \Uri::string(). It also allows for you to have a multi-paged site. ;)
4.) If your using one error page template and you just make it dynamic based on the HTTP status code, you have it a bit easier. If you're using PHP 5.4+, http_response_code() allows you to grab the Apache set status code easily and you can use it in your loader controller's before() to set stuff. If you have a separate error page template for each error, keep in mind that you can use variables in your widget list in the database. Just use a string that won't ever be valid for a page URI and replace that with the status code. You can then load one row from the DB for all of your errors and use it for all of your error pages.
5.) As you need to run the same query every time you load every single page, consider using Redis or another caching solution (See the Cache section of the manual) to speed up your page loads. Cache every page's widgets into a nice array and never let it expire in a production environment. While you're developing your software, you shut off the caching. Use \Fuel::$env to check that, of course. Also, consider building a script to pre-cache all of your never-changing DB tables when in a production environment. (The Migration tool could be an excellent option for this.)
6) design your applications API carefully. Separate interactive controllers from widget controllers from REST or SOAP API controllers. If makes it easier to use base controllers, and not have to deal with stuff like is_hmvc() or is_ajax() in every action. Version your API's, you can you introduce a new version without your endpoints breaking because of your changes.
7) Namespace. Everything. So change the default "Controller_" prefix in the config to "Controller\', and use \Controller\Some\Name or \Module\Controller\Some\Name. Same for models and presenters.
8) Design for re-use.Use modules for re-usable application components, use Packages for re-usable framework extensions.Never thightly couple your re-usable components.
9) Stay true to the MVC principles. Even if you use the ORM, and it has something called a "model" that maps to a table record, it does NOT replace the "model" mentioned in MVC. The model should still be used to abstract your business logic away from your controller, the ORM model is just a way of representation of your records, and an ActiveRecord implementation of getting those.
How do you properly separate controller logic in FuelPHP as described in number 6? I have a hunch that it ties in with number 9 somehow. With modules having to have an action_ method in order to call them through \Request::forge() and the URI relying on the same method while people can enter whatever they'd like, I'd have to wonder how one might pull this off without the is_ methods.
By not mixing these functions in the same controller, but keep them separated.
You can use some clever generic routes to make stuff appear together while is isn't in real life, for example
// maps /api/v1/user/get/1 to /user/api/v1/get/1 'api/(:version)/(:module)/(:all)' => '$2/api/$1/$3',
where 'user' is a module containing a REST API controller. Depending on the size of your API, this could either be \User\Controller\V1 with action_get() or \User\Controller\V1\Get with a router() that deals with any parameters.
We use the same trick in the frontend, the application is constructed from modules, every module has an admin controller. But instead of using '/user/admin', '/product/admin' etc, we expose '/admin/user' and '/admin/product' to the enduser, which is more consistent.
Yesterday I changed the line for the routing and sent everything to the loader using (:any) and that stopped it from routing anything that isn't in the DB. (See my routes above.) As for your idea on how to separate the logic, that sounds like a smart way of doing things. I'll use that when needed. Thank you.