ZF2 - Injecting pages to navigation before controller is called - zend-framework2

I'm creating a dynamic Application in which the content is added through a CMS. Inside the CMS, I'm setting a db entry which states what module to use for each content page.
NodeId,
ParentNodeId,
Name_de,
Name_en,
ModuleName,
foreignkey_ContentLinks,
in this table entries look as follows:
6,
1,
Veranstaltung-21-02-2013,
Event-21-02-2013,
Events,
682
The entire tree should end up in my navigation (and perfectly also in my routing). I do not want to add it in some controller, because my Application consists of a whole bunch of Modules and I want to access that Info across all my Modules.
I already tried injecting it in the global.php, but to no avail because I can't my db adapter or any other important classes at that stage.
Any ideas or links to best practices?

The navigation containers are composed by factory classes. The easiest approach is to write your own factory and have the getPages() method fetch pages from a database instead of from config. If you extend from the AbstractNavigationFactory you only need to write a couple of methods.
<?php
namespace Application\Navigation\Service;
use Zend\Navigation\Service\AbstractNavigationFactory;
use Zend\ServiceManager\ServiceLocatorInterface;
class CmsNavigationFactory extends AbstractNavigationFactory
{
/**
* #param ServiceLocatorInterface $serviceLocator
* #return array
* #throws \Zend\Navigation\Exception\InvalidArgumentException
*/
protected function getPages(ServiceLocatorInterface $serviceLocator)
{
if (null === $this->pages) {
$application = $serviceLocator->get('Application');
$routeMatch = $application->getMvcEvent()->getRouteMatch();
$router = $application->getMvcEvent()->getRouter();
// get your pages from wherever...
$pages = $this->getPagesFromDB();
$this->pages = $this->injectComponents($pages, $routeMatch, $router);
}
return $this->pages;
}
public function getName()
{
// this isn't used if fetching from db, it's just here to keep the abstract factory happy
return 'cms';
}
}
Add the factory to the service manager, just like you would for other containers
'service_manager' => array(
'factories' => array(
'CmsNavigation' => 'Application\Navigation\Service\CmsNavigationFactory',
),
),
And use it with the navigation view helpers in the same way
<?php echo $this->navigation()->menu('CmsNavigation'); ?>

Responding to your comment on #Crisp's answer, and for future googlers, I'll explain how to do something similar for routing.
Typically you would want to create a custom router that can match URLs to the pages in your database, similarly to the standard Segment router. To do this, you will have to implement the Zend\Mvc\Router\RouteInterface interface. For example:
namespace Application\Router;
use Zend\Mvc\Router\RouteInterface;
use Application\Model\CMSTable;
class CmsRoute implements RouteInterface, ServiceLocatorAwareInterface
{
protected $table;
// Service locator injection code
public function getCmsTable()
{
// Retrieve the table from the service manager
}
public function match(Request $request)
{
// Match the request on some route field, etc.
}
public function assemble(array $params = array(), array $options = array())
{
// Assemble a URL based on the given parameters (e.g. page ID).
}
public static function factory($options = array())
{
// Construct a new route, based on the options.
}
}
You could then register this route as an invokable for the RoutePluginManager in your module configuration:
'route_manager' => array(
'invokables' => array(
'Cms' => 'Application\Router\CmsRoute'
),
),
Then, you can create a new route (just as you would for any other route) with type Cms. The route plugin manager will create your route instance, and since CmsRoute implements ServiceLocatorAwareInterface, the plugin manager will inject itself in the route. In turn, the plugin manager has the main service manager set, so that you can get the database table from there!
Of course you can match on page ID, but if you have a hierarchical structure, it's nicer to reflect that in your URLs. I would therefore recommend adding a route field to the database schema and match on that, beginning with the tree root and working down.

Related

Zend Framework 2: Ho to set Cookie in Module.php to access from other Modules

I have created a Project to display adverts. Each Advert has a Location.
Currently a list if adverts are displayed. I would now like a list of locations in my layout.phtml and after a click on the location the adverts should be filtered.
To achieve this I have created a new Module called Geolocation. I have then created two new view helpers; one to show all the Locations and the other is to display the name of the chosen Location, which is stored in a Cookie.
When you click on a location in the list you access a AJAX request is made to the Geolocation Controller. The controller calls a method in a service to store the location in the cookie.
I am now changing my SQL Queries and Repositories in my Advert Module to accept location if it is set:
public function countAdvertsByCategory($location=false)
Normally, I would add $location = $_COOKIE['ChosenCounty'] in my advert controller, but I am sure there is a better way.
I would have thought that I can maybe add this in the module.php from the Geolocation Module. If that Module has included the variable, $location will be set with the cookie value and otherwise it will just be ignored.
Is that the right way or what is the best practice? And how would I do that?
UPDATE
I have now changed my Factory:
namespace Application\Navigation;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
class MyNavigationFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $serviceLocator)
{
// previous without Geolocation
$navigation = new MyNavigation();
return $navigation->createService($serviceLocator);
$location = $serviceLocator->get('Geolocation\Service\Geolocation');
$navigation = new MyNavigation($location);
return $navigation->createService($serviceLocator);
}
BUT, if I would now remove my Geolocation Module, than the Factory in my Application Module to create my Navigation would fail, meaning my Factory is now dependent of this new Module which I did not want. How could I avoid this?
You could add the cookie value as a 'service' to the service manager. Whenever you need the $location you would then just retrieve it from the service manager.
Create a factory that accesses the required cookie variable.
namespace GeoLocation\Service;
use Zend\ServiceManager\ServiceLocatorInterface;
use Zend\ServiceManager\FactoryInterface;
class GeoLocationFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $serviceLocator)
{
$request = $serviceLocator->get('Request');
$cookies = $request->getHeaders()->get('cookie');
return isset($cookies->location) ? $cookies->location : false;
}
}
Then register it with the service manager in module.config.php.
'service_manager' => [
'factories' => [
'GeoLocation\Service\GeoLocation' => 'GeoLocation\Service\GeoLocationFactory',
],
],
Then you can update your AdvertService to require the value
class AdvertService
{
protected $location;
public function __construct($location)
{
$this->location = $location;
}
public function getAdvertsByCategory()
{
return $this->repository->countAdvertsByCategory($this->location);
}
}
You can then create a new AdvertServiceFactory that will fetch and inject the service into the AdvertService::__construct using
$serviceManager->get('GeoLocation\Service\GeoLocation');

zf2 how to use 3rd party module in my own specific module (based on route)?

I'm developping a website by ZendFramework 2. I have 2 modules: module for administration called Administration(route defined like www.mysite.com/admin/...) et module public site called Application(route defined like www.mysite.com/...) I distinguish the 2 modules by the route.
I don't know how to distinguish the two modules based on route.
To make it clear, I have 2 questions for example:
I use Zfcuser for the login system for the module Administration et in Administration/Module.php I added the following code to the purpose that if one user doesn't have identity the layout will change to the login form.
namespace Administration;
use Zend\Mvc\ModuleRouteListener;
use Zend\Mvc\MvcEvent;
class Module {
public function onBootstrap(MvcEvent $e) {
$eventManager = $e->getApplication ()->getEventManager ();
$moduleRouteListener = new ModuleRouteListener ();
$moduleRouteListener->attach ( $eventManager );
$eventManager->attach('dispatch', array($this, 'checkLoginChangeLayout'));
}
public function checkLoginChangeLayout(MvcEvent $e) {
if (! $e->getApplication ()->getServiceManager ()->get ( 'zfcuser_auth_service' )->hasIdentity ()) {
$controller = $e->getTarget ();
$controller->layout ( 'layout/authentication.phtml' );
}
}
public function getConfig() {
return include __DIR__ . '/config/module.config.php';
}
public function getAutoloaderConfig() {
return array (
'Zend\Loader\StandardAutoloader' => array (
'namespaces' => array (
__NAMESPACE__ => __DIR__ . '/src/' . __NAMESPACE__
)
)
);
}
}
But all the 2 modules are affected by the function checkLoginChangeLayout(). I want to use the module ZfcUser just in the module Administration but not the module Application.
Can I do something about the Module Manager or Event Manager to solve the problem?
I've found a 3rd party module called BjyAuthorize which is used for ACL by "guard". When I active the module in application.config.php , all my 2 modules are controlled by it. But I just want to use the 3rd party module in the module Administration but not the other modules.
your first approach is fail
because (as you found) there is another module for doing such a thing called BJYAUTHORIZE
it has a config for allowing different type of users access which controller/action/module/route/...
plz review it's documentation for more information and also there is a blog post for How to join ZFCUser and BJY together .
http://samminds.com/2013/03/zfcuser-bjyauthorize-and-doctrine-working-together/

ZF2: Using JSON-RPC without MVC?

It is stated in the ZF2 documentation, as well as by Matthew Weier O'Phinney's blog, that:
Many developers want to stick this in their MVC application directly,
in order to have pretty URLs. However, the framework team typically
recommends against this. When serving APIs, you want responses to
return as quickly as possible, and as the servers basically
encapsulate the Front Controller and MVC patterns in their design,
there's no good reason to duplicate processes and add processing
overhead.
It is recommended that you put the server endpoints in the public directory structure. For example, you might have /public/some-api.php that instantiates and runs the Zend RPC Server. But I have already created this dope module in which I have a bunch of classes and a config file that lays out the dependency injection, factories, etc for creating the classes.
Soo... how do I leverage that code in my RPC server, without putting it into a MVC controller?
Thanks!
Adam
Here is how I did it. I have this broken out into a few files, but you can put all this in your public root directory, something like rpc-service.php:
use Zend\ServiceManager\ServiceManager,
Zend\Mvc\Service\ServiceManagerConfig;
class Bootstrap {
/** #var ServiceManager */
private static $serviceManager;
private static function _go() {
chdir(dirname(__DIR__));
require __DIR__ . '/../init_autoloader.php';
$config = include __DIR__ . '/../config/application.config.php';
$serviceManager = new ServiceManager(new ServiceManagerConfig());
$serviceManager->setService('ApplicationConfig', $config);
$serviceManager->get('ModuleManager')->loadModules();
self::$serviceManager = $serviceManager;
}
/**
* #return ServiceManager
*/
public static function getServiceManager() {
if (!self::$serviceManager)
self:: _go();
return self::$serviceManager;
}
}
$sm = Bootstrap::getServiceManager();
use Zend\Json\Server\Server,
Zend\Json\Server\Smd,
$jsonRpc = new Server();
$jsonRpc->setClass($sm->get('Some\Class'));
$jsonRpc->getRequest()->setVersion(Server::VERSION_2);
if ($_SERVER['REQUEST_METHOD'] == 'GET') {
echo $jsonRpc->getServiceMap()->setEnvelope(Smd::ENV_JSONRPC_2);
}
else {
$jsonRpc->handle();
}
As you can see, I'm using the Service Manager! Yay. All is right in the world.

Autofac Dependencies Per Area

I'm creating a new MVC4 site using Autoface that has a public consumer site as well as an admin area for managing the consumer facing site. The admin site will be located in a different area be using the same services as the consumer facing site, but will not having some of the custom branding features.
I've followed the advice given elsewhere of having a ViewDataFactory which provides a set of shared data for the view to use. My goal is to provide a different ViewDataFactory depending on what Area you are in.
So for example, here is the Service that implements IViewDataFactory
builder.RegisterType<SelfServiceViewDataFactory>().As<IViewDataFactory>();
This gives me one ViewFactory which is injected into all my controllers. However what I'm trying to acheive is something like this (not functional code):
builder.RegisterType<ViewDataFactory>().As<IViewDataFactory>().ForType(ControllerBase1);
builder.RegisterType<DifferentViewDataFactory>().As<IViewDataFactory>().ForType(ControllerBase2);
Where the controller type or the MVC area would determine which service is resolved.
EDIT
To clarify my post has two questions:
Is there a way in Autofac to say "only for classes of type X, a service of type Y will be provided by instance Z" ?
Is there a way to change the Autofac behavior based on the Area the component is being used in?
From everything I've been reading the answer to #1 seems to be "no" unless you have a parameter to use to check which component to supply. I know Ninject can supply a dependency based on namespace so other frameworks seems to handle this case. Seems the solution is to either supply a parameter or have two different services defined.
I haven't really seen much discussion of Autofac and MVC areas so I'm guessing #2 is also not possible without a custom solution. Thanks!
Using named services is probably your best option. So you'd do something like:
builder
.RegisterType<ViewDataFactory>()
.Named<IViewDataFactory>("Area1");
builder
.RegisterType<DifferentViewDataFactory>()
.As<IViewDataFactory>("Area2");
And then if you want to avoid having to then manually register your controllers. You could use this code that I just cobbled together and haven't tested:
Put this attribute somewhere globally accessible:
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)]
public class ServiceNamedAttribute : Attribute
{
private readonly string _key;
public ServiceNamedAttribute(string key)
{
_key = key;
}
public string Key { get { return _key; } }
}
Add this module to your Autofac config:
public class ServiceNamedModule : Module
{
protected override void AttachToComponentRegistration(
IComponentRegistry registry, IComponentRegistration registration)
{
registration.Preparing +=
(sender, args) =>
{
if (!(args.Component.Activator is ReflectionActivator))
return;
var namedParameter = new ResolvedParameter(
(p, c) => GetCustomAttribute<ServiceNamedAttribute>(p) != null,
(p, c) => c.ResolveNamed(GetCustomAttribute<ServiceNamedAttribute>(p).Name, p.ParameterType));
args.Parameters = args.Parameters.Union(new[] { namedParameter });
};
}
private static T GetCustomAttribute<T>(ParameterInfo parameter) where T : Attribute
{
return parameter.GetCustomAttributes(typeof(T), false).Cast<T>().SingleOrDefault();
}
}
And then you can still auto-register your controllers by decorating the constructor like so:
public class Controller1
{
public Controller1(ServiceNamed["Area1"] IViewDataFactory factory)
{ ... }
}

sfContext::getInstance()->getUser() not working in createQuery from validator

Now: resolved - no reproducible anymore
For some specific application security, I have the following createQuery function on a table, ie you can only access the table record if you have the "Administrator" credential or if you are the user that is stored in the MembershipDelegate relation.
class OrganisationTable extends Doctrine_Table
function createQuery($alias = ''){
if (!$alias){
$alias = 'o';
}
$query = parent::createQuery($alias);
try {
$user = sfContext::getInstance()->getUser();
}catch(Exception $e){
if ($e->getMessage() == 'The "default" context does not exist.'){
return $query;
}else{
throw $e;
}
}
if ($user->hasCredential('Administrator')){
//all good
}else{
$userId = $user->getAttribute('userId');
print "<!--testaa ".print_r($user->getCredentials(),1).'-->';
$query->
leftJoin("$alias.MembershipDelegate mdelsec")->
addWhere ("mdelsec.joomla_user_id=$userId");
}
return $query;
}
This seems to work fine at all levels, however there is a choice validator for which the $user object seems to come back empty
/**
* Person form base class.
*
*/
...
abstract class BasePersonForm extends BaseFormDoctrine
{
public function setup()
{
$this->setWidgets(array(
...
'organisation_id' => new sfWidgetFormDoctrineChoice(array('model' => $this->getRelatedModelName('Organisation'), 'add_empty' => true)),
class PersonForm extends BasePersonForm{
public function configure(){
$this->widgetSchema['organisation_id']->setOption('renderer_class', 'sfWidgetFormDoctrineJQueryAutocompleter');
$this->widgetSchema['organisation_id']->setOption('renderer_options', array(
'model' => 'Organisation',
'url' => NZGBCTools::makeUriJoomlaCompatible(
sfContext::getInstance()->getController()->genUrl('organisation/jsonList'))
));
$this->validatorSchema['organisation_id']->setOption('required',false);
is there any other way to get the user object in the model?
This approach to row level security may not be MVC by-the-book, but is IMO safer and superior than implementing the same security concepts in the actions:
It can be used with out of the box admin-generator modules
It is much harder to forget to implement it somewhere
It may at times not require any credentials, only Super Admin access
I'm not aware of another way to get the session user in the models (I don't think there is one), but if you're able to, you ought to pass the user object down to the models.
I think you are missing something here, you have mixed the MVC layers quite a lot. My suggestion is to make the Model independent from the Controller (delegate to the Controller the Context-specific situations), and validation should be done by the Controller also ( there should be an action that receives the request , binds the form and validates it. There is where you have to definy anithing thats context speficif). Here is what i would do:
The OrganisationTable class should have 2 methods: createQueryForAdministrator , and createQueryForMembershipDelegate . Each one does the things it should do, and now you should change the Controller (the action that handles it) and do something like:
public function executeAction(sfWebRequest $request)
{
if ($this->getUser()->hasCredential('administrator'))
{
//call the administrator method
} else {
//call the other method
}
}
The action that instances the Form should also check the user credentials and do something like:
If ($user->hasCredential("administrator"))
{
sfContext::getInstance()->getConfiguration()->loadHelper("Url");
$form->getValidator("organization_id")->setOption("url",url_for("#action"));
[..]
} else {
[..]
}
Check the url helper reference, you can do thinks like loading helpers on actions etc and you could also create your own helpers too. Hope this helps!
EDITED: Check this post about sfContext class and alternatives
It now appears that this question was a fluke as I can't reproduce the problem after fixing other issues around the user authentication.

Resources