Tagged “php”

Drupal: Login-Screen instead of 403

Published by cybso on

This is a post from my original site, which was hosted by the former blog service of the University of Osnabrück. I have moved it to the new site for archiving. Pages linked in this article may no longer work today, and the blog comments under the article no longer exist. Opinions expressed in this article reflect the point of view of the time of publication and do not necessarily reflect my opinion today.

If you're running a Drupal-based webpage which is completely restricted to authenticated users, Anonymous would get an "403 Access Denied" on every page he tries. But if you want your visitors to see a friendly login page (instead of just this error message and a small "login block") you might want to put the following code into your sites/default/setting.php:

function custom_url_rewrite($op, $result, $path) {
    global $user;
    if (!$user->uid) {
        return "user/login";
    }
    return $path;
}

You should also check $path if you want to use this on some pages only, e.g. on your frontpage.

UPDATE: This code is erroneous! You should return $result instead of $path, or some other modules like pathauto or path_redirect won't work as expected!

Symfony: Merge embedded Form (Update)

Published by cybso on

This is a post from my original site, which was hosted by the former blog service of the University of Osnabrück. I have moved it to the new site for archiving. Pages linked in this article may no longer work today, and the blog comments under the article no longer exist. Opinions expressed in this article reflect the point of view of the time of publication and do not necessarily reflect my opinion today.

Symfony provides a nice feature called "embedded Forms" ( sfForm::embedForm) to embed subforms into a parent form. This can be used to edit multiple records at the same time. So let's say you have a basic user table called 'sf_guard_user' and a profile table called 'user_profile', then you might follow this guide to merge these forms together: lib/forms/doctrine/sfUserGuardAdminForm.php:

class sfGuardUserAdminForm extends BasesfGuardUserAdminForm
{
  public function configure()
  {
    parent::configure();

    // Embed UserProfileForm into sfGuardUserAdminForm
    $profileForm = new UserProfileForm($this->object->Profile);
    unset($profileForm['id'], $profileForm['sf_guard_user_id']);
    $this->embedForm("profile", $profileForm);
  }
}

Remember to add "profile" to the list of visible columns in apps/backend/modules/sfGuardUser/config/generator.yml as decribed in the linked guide. The result may look like this:

This does what it is expected to do, but it doesn't look very nice. Especially for 1:1 related tables I'm more interested in a solution that looks like this:

You can reach this using sfForm::mergeForm, but sadly the merged model won't get updated and you'll run into problems if the forms are sharing fieldnames. The solution is the following method embedMergeForm which can be defined in BaseFormDoctrine to be avaible in all other forms:

lib/forms/doctrine/BaseFormDoctrine.php:

abstract class BaseFormDoctrine extends sfFormDoctrine
{
  /**
   * Embeds a form like "mergeForm" does, but will still
   * save the input data.
   */
  public function embedMergeForm($name, sfForm $form)
  {
    // This starts like sfForm::embedForm
    $name = (string) $name;
    if (true === $this->isBound() || true === $form->isBound())
    {
      throw new LogicException('A bound form cannot be merged');
    }
    $this->embeddedForms[$name] = $form;

    $form = clone $form;
    unset($form[self::$CSRFFieldName]);

    // But now, copy each widget instead of the while form into the current
    // form. Each widget ist named "formname|fieldname".
    foreach ($form->getWidgetSchema()->getFields() as $field => $widget)
    {
      $widgetName = "$name|$field";
      if (isset($this->widgetSchema[$widgetName]))
      {
        throw new LogicException("The forms cannot be merged. A field name '$widgetName' already exists.");
      }

      $this->widgetSchema[$widgetName] = $widget;                           // Copy widget
      $this->validatorSchema[$widgetName] = $form->validatorSchema[$field]; // Copy schema
      $this->setDefault($widgetName, $form->getDefault($field));            // Copy default value

      if (!$widget->getLabel())
      {
        // Re-create label if not set (otherwise it would be named 'ucfirst($widgetName)')
        $label = $form->getWidgetSchema()->getFormFormatter()->generateLabelName($field);
        $this->getWidgetSchema()->setLabel($widgetName, $label);
      }
    }

    // And this is like in sfForm::embedForm
    $this->resetFormFields();
  }

  /**
   * Override sfFormDoctrine to prepare the
   * values: FORMNAME|FIELDNAME has to be transformed
   * to FORMNAME[FIELDNAME]
   */
  public function updateObject($values = null)
  {
    if (is_null($values))
    {
      $values = $this->values;
      foreach ($this->embeddedForms AS $name => $form)
      {
        foreach ($form AS $field => $f)
        {
          if (isset($values["$name|$field"]))
          {
            // Re-rename the form field and remove
            // the original field
            $values[$name][$field] = $values["$name|$field"];
            unset($values["$name|$field"]);
          }
        }
      }
    }

    // Give the request to the original method
    parent::updateObject($values);
  }
}

This method ensures that each fieldname is unique (named 'FORMNAME|FIELDNAME') and the subform is validated and saved. It is used like embedForm:

lib/forms/doctrine/sfUserGuardAdminForm.php:

class sfGuardUserAdminForm extends BasesfGuardUserAdminForm
{
  public function configure()
  {
    parent::configure();

    // Embed UserProfileForm into sfGuardUserAdminForm
    // without looking like an embedded form
    $profileForm = new UserProfileForm($this->object->Profile);
    unset($profileForm['id'], $profileForm['sf_guard_user_id']);
    $this->embedMergeForm("profile", $profileForm);
  }
}

Feel free to use this method in your own project. Maybe this method get's merged into Symfony some day ;-)

Update

frostpearl reported a problem using embedFormMerge() in conjunction with the autocompleter widget from sfFormExtraPlugin. If you expire these problems try to replace all occurences of $name|$field with $name-$field.

Some kind of "Subcontrollers" with Symfony (Update)

Published by cybso on

This is a post from my original site, which was hosted by the former blog service of the University of Osnabrück. I have moved it to the new site for archiving. Pages linked in this article may no longer work today, and the blog comments under the article no longer exist. Opinions expressed in this article reflect the point of view of the time of publication and do not necessarily reflect my opinion today.

In my current Symfony project I have a model called "Country" and a model called "Region". A Region always belongs to a Country, and this country will never change.

I've used Doctrine's admin generator to create the administration backend:

$ php symfony doctrine:generate-admin --plural="Countries" backend Country
$ php symfony doctrine:generate-admin backend Region

This command produces two modules, "apps/backend/modules/country/" and "apps/backend/modules/region/", and the following entries in apps/backend/config/routing.yml:

region:
  class: sfDoctrineRouteCollection
  options:
    model:               Region
    module:              region
    prefix_path:         region
    column:              id
    with_wildcard_routes: true

country:
  class: sfDoctrineRouteCollection
  options:
    model:               Country
    module:              country
    prefix_path:         country
    column:              id
    with_wildcard_routes: true

The URLs will look like "http://www.example.com/backend.php/country/index" and "http://www.example.com/backend.php/region/index". But what I want is something like "http://www.example.com/backend.php/country/COUNTRY_ID/region/index", so the Region view is always bound to an specific country.

You can reach this with minimal effort. First, you have to modify the routing. Change the entry for region as shown below (changed line is in bold font):

apps/backend/config/routing.yml:

region:
  class: sfDoctrineRouteCollection
  options:
    model:               Region
    module:              region
    prefix_path:         country/:country_id/region
    column:              id
    with_wildcard_routes: true

If you try to open "http://www.example.com/backend.php/country/1/region/index" now (assuming that '1' is a valid country id) you'll get an error like this:

500 | Internal Server Error | InvalidArgumentException
The "/country/:country_id/region/:action/action.:sf_format" route has some missing mandatory parameters (:country_id).This is because the (automatically generated) region view tries to call 'url_for()' for actions like 'filter' or 'add', and there is an parameter called 'country_id' defined which is missing in the argument list. You can solve this problem by overwriting the 'execute()' function in the action class.

apps/backend/modules/region/actions/actions.class.php:

public function execute($sfRequest)
   {
     $this->forward404Unless($country_id = $sfRequest->getUrlParameter(‘country_id’));
     $this->forward404Unless($this->country = Doctrine::getTable(‘Country’)->find($country_id));
     $this->getContext()->getRouting()->setDefaultParameter(‘country_id’, $country_id);
     if ($id = $sfRequest->getUrlParameter(‘id’))
     {
       $this->getContext()->getRouting()->setDefaultParameter(‘id’, $id);
     }
     $result = parent::execute($sfRequest);

    // UPDATE: This is required for the 'new' action
     if (isset($this->form) && $this->form->getObject() && $this->form->getObject()->isNew())
     {
       $this->form->getObject()->country_id = $country_id;
     }
     return $result;
   }

This will set the current country id as default parameter for all calls to methods like 'link_to()' or 'url_for()', and abort if an valid id is missing.

Of course you still have to modify the filters and forms to regard the given country as default, and add some extra actions to the Country view, but the most trickiest part is done. Have a look at this article from Sven to learn how to modify the default filter and read Jobeet Tutorial, Chapter 12 to see how the default actions can be customized.

If you expire some problems with 'new' and 'edit' forms (action="/backend.php/country/region" in <form>-tag), read bug report #6881. Update: This problem can be solved by setting the 'country_id' field of new objects (e.g. by overriding executeNew() and executeCreate()).