2009/07/19

symfony routing cache and memory error

I was struggling with this trouble for almost 2 months!!!! My current project uses symfony as always, but I had a big problem with memory issue.

I am engaging this project for almost a year. This application was first created with symfony1.1, but now it is compatible with 1.2. Currently it uses many only 1.2 features such as routing, cache, and etc. I was very satisfied with symfony1.2, except this memory problem.

There were many out of memory error lines in my httpd error log.

  1. PHP Fatal error:  Allowed memory size of 100663296 bytes exhausted (tried to allocate 5926568 bytes) in /xxxxx/cache/frontend/prod/config/config_core_compile.yml.php on line 3366

Because of this error, I increased memory_limit to 96M in php.ini. However, it was not enough. Plus, because my server was a bit old and has only 512M, it always uses swap, and gets really bad sometimes. I was so frustrated with this problem, and googled this a lot. It is still a guess, and I don't know the solution I did was the exact one. BUT, It is working very good so far.

After I googled this issue, I found a couple similar topics in symfony forum as follows:

The first one was the problem with symfony1.2.4, and it said routing cache files size gets huge and takes too much memory to serialize it. At first, I did not know this post relates with my problem, because I was too lazy to look inside of line 3366 in config_core_compile.yml.php. After checking line 3366, I found it was the same problem. Too much memory consumption exceeded allowed memory size. One of this fix is using SQLite to handle this cache, but in my understanding, symfony1.2.7 provides different way to solve this problem, which you can find symfony 1.2.7 - more power.

  1. all:
  2.   routing:
  3.     class: sfPatternRouting
  4.     param:
  5.       generate_shortest_url:            true
  6.       extra_parameters_as_query_string: true
  7.       lazy_routes_deserialize:          true
  8.       lookup_cache_dedicated_keys:      true

I checked how this fix affects. This fix generate many routing cache files in cache/app_name/prod/config/routing/. The file size was not huge, but number of files is huge, as the symfony blog says. I might need to use different storage for this cache handling, but now the memory error is disappeared. I am very happy with it. :)

Man, i should have noticed symfony1.2.7 fix. :(

BUT, it did not fix, but it is working now, The answer is "Do not use routing cache. Set sfNoCache instead."

2009/04/08

Don’t want .html file extention for my link!

To make the symfony1.1 base application compatible with symfony1.2 is not very difficult. Because symfony1.2 considers backward compatibility. If you look inside of symfony, you will find many strange implements, such as link_to1, link_to2. In this case, link_to1 is for symfony1.1 base application, and link_to2 is for symfony1.2.

It made us easy to move on to symfony1.2. And I also upgrade the application I developed with symfony1.1 to symfony1.2. One of the new symfony1.2 feature is routing. Yeah! When I did Jobeet tutorial, it took a bit time to understand the new routing framework. I understand it after I implemented askeet with symfony1.2. :) When I developed askeet with symfony1.2, I used pure symfony1.2, so the generated codes are symfony1.2 base, and did not have any problems. However, an application I'm currently working on was developed with symfony1.1 first. It works with symfony1.2 now , but routing ruling is still syfmony1.1 base, and there are other symfony1.1 base code as well. :(

One of the routing features which added in symfony1.2 is sfPropelRouteCollection. Before the emergence of the useful class, I had to write many CRUD and more rule in routing.yml, but sfPropelRouteCollection helped me a lot to implement model base module. I was trying my symfony1.1 base application to upgrading routing rule for symfony1.2. After adding sfPropelRouteCollection in routing.yml, it seemed it is working great, and result of app:routes command is just what I was expected. :) However, urls displayed converted with link_to or url_for are not what I wanted. :( These helper functions add ".html" for urls. I don't want them! :( I struggled to removing this for a bit. Well, It was easy, but it took me two hours to find this fix. When I found the solution, I was so happy that I could not write post related with my problems.

The problem is generate_shortest_url and extra_parameters_as_query_string values in factories.yml. Well, if you used symfony1.2 to generate application using generate:app, you do not have this problem, because symfony1.2 generate factories.yml with this values. However my application was generated with symfony1.1 almost half year ago, and generate_shortest_url and extra_parameters_as_query_string was not in factories.yml.

As you find in symfony1.2 base application's factories.yml, you need following values in your symfony1.1 base factories.yml.

  1. all:
  2.   routing:
  3.     class: sfPatternRouting
  4.     param:
  5.       generate_shortest_url: true
  6.       extra_parameters_as_query_strings: true

O.K, that's it! :) It is very simple, if you know the reason.

2009/04/05

Playing with sfForm with Askeet Part 2

It's another post related with sfForm. After using symfony1.2 several months, I found mastering sfForm is crucial for symfony developers. :) I'm still learning it, too. :)

In this post, I will explain QuesionForm, QuestionTagForm. Maybe I should also explain BackendQuestionForm, because QuestionForm uses tag form with auto complete, and BackendQuestionForm does not, and you might want to choose whether you use it or not. Well, I do! ;)

O.K, let's clarify the specification of posting question and tagging. In askeet, you can only post a question from question posting page. Yeah, as it is. Look at the image below. This is the question posting form.

However, tagging is different. You can set tags when you post a question as an author of the question. Plus, if you are authenticated user, you can set/add tag for the questions no matter who post the question. So, there are two ways for tagging. The one you see above, which is tagging with posting question, and the form you found in sidebar when you logined askeet and go to the question detail page as a below image:

To explain how to do it, I use following codes:

  • lib/form/QuestionForm.class.php
  • lib/form/QuestionTagForm.class.php
  • lib/form/BackendQuestionForm.class.php
  • lib/form/sfWidgetFormProtoculousAutocompleter.class.php
  • apps/frontend/modules/question/actions/actions.class.php(executeNew, executeCreate)
  • apps/frontend//modules/question/templates/newSuccess.php
  • apps/frontend/modules/tag/actions/actions.class.php(executeCreate, executeAutocomplete)
  • apps/frontend/modules/tag/components/components.class.php
  • apps/frontend/modules/tag/templates/_add.php
  • apps/frontend/modules/tag/templates/autocompleteSuccess.php

O.K, let's start with the posting question form.

lib/form/QuestionForm.class.php

  1. class QuestionForm extends BaseQuestionForm
  2. {
  3.   protected $questionTagForm = null;
  4.   public function configure()
  5.   {
  6.     unset(
  7.       $this['created_at'],
  8.       $this['updated_at'],
  9.       $this['interested_users'],
  10.       $this['reports'],
  11.       $this['user_id'],
  12.       $this['html_body'],
  13.       $this['stripped_title'],
  14.       $this['report_question_list'],
  15.       $this['question_tag_list'],
  16.       $this['interest_list']
  17.     );
  18.  
  19.     $this->widgetSchema['title'] = new sfWidgetFormInput( array('label' => 'question:')
  20.     );
  21.     $this->validatorSchema['title'] = new sfValidatorString(
  22.       array(),
  23.       array('required' => 'You must give a title to your question')
  24.     );
  25.  
  26.     $this->widgetSchema['body'] = new sfWidgetFormTextarea(
  27.       array('label' => 'describe it:'),
  28.       array('cols' => 40, 'rows' => 10)
  29.     );
  30.     $this->validatorSchema['body'] = new sfValidatorString(
  31.       array('min_length' => 10),
  32.       array(
  33.         'required' => 'You must provide a brief context for your question',
  34.         'min_length' => 'Please, give some more details'
  35.       )
  36.     );
  37.  
  38.     $this->setQuestionTagForm();
  39.     $this->mergeForm($this->questionTagForm);
  40.   }
  41.  
  42.   public function updateObject($values = null)
  43.   {
  44.     $object = parent::updateObject($values);
  45.     if (is_null($object->getUserId())) {
  46.       $userId = sfContext::getInstance()->getUser()->getSubscriberId();
  47.       $object->setUserId($userId);
  48.     }
  49.     $interest = new Interest();
  50.     $interest->setUserId($userId);
  51.     $object->addInterest($interest);
  52.  
  53.     return $object;
  54.   }
  55.  
  56.   public function doSave($con = null)
  57.   {
  58.     parent::doSave($con);
  59.  
  60.     $this->taintedValues['question_id'] = $this->object->getId();
  61.     $this->questionTagForm->bind($this->taintedValues);
  62.     $this->questionTagForm->doSave($con);
  63.   }
  64.  
  65.   protected function setQuestionTagForm()
  66.   {
  67.     $this->questionTagForm = new QuestionTagForm(
  68.       null,
  69.       array('url' => sfContext::getInstance()->getController()->genUrl('tag_autocomplete'))
  70.     );
  71.   }
  72. }

Check out doSave method. It is a bit tricky, because we need to save posted tags as well. When tagging, you are not going to set only one tag. You might want to tag more than one phrases. Also, using mergeForm does not call doSave for the merged form, so you have to override doSave method here. I need to bind the question_id for QuestionTagForm

In the end of configure method, I called setQuestionTagForm and mergeForm, because I will override setQuestionTagForm in BackendQuestionForm, not to use auto completing feature. See the BackendQuestionForm now.

lib/form/BackendQuestionForm.class.php

  1. class BackendQuestionForm extends QuestionForm
  2. {
  3.   public function configure()
  4.   {
  5.     parent::configure();
  6.     $this->widgetSchema['user_id'] = new sfWidgetFormInputHidden();
  7.     $this->validatorSchema['user_id'] = new sfValidatorPropelChoice(array(
  8.       'model' => 'User', 'column' => 'id', 'required' => false
  9.     ));
  10.   }
  11.  
  12.   protected function setQuestionTagForm()
  13.   {
  14.     $this->questionTagForm = new QuestionTagForm(null);
  15.   }
  16.  
  17.   public function updateDefaultsFromObject()
  18.   {
  19.     parent::updateDefaultsFromObject();
  20.     $tag = implode(" ", $this->object->getTags());
  21.     $values['tag'] = $tag;
  22.     if ($this->isNew) {
  23.       $this->setDefaults(array_merge($values, $this->getDefaults()));
  24.     } else {
  25.       $this->setDefaults(array_merge($this->getDefaults(), $values));
  26.     }
  27.   }
  28. }

As I saild above, I overrided the setQuestionTagFrom for without auto completing function.

O.K, now it's about time for tagging.

lib/form/QuestionTagForm.class.php

  1. class QuestionTagForm extends BaseQuestionTagForm
  2. {
  3.   public function configure()
  4.   {
  5.     unset(
  6.       $this['user_id'],
  7.       $this['normalized_tag'],
  8.       $this['created_at']
  9.     );
  10.  
  11.     $this->widgetSchema['question_id'] = new sfWidgetFormInputHidden();
  12.     $this->validatorSchema['question_id'] = new sfValidatorPropelChoice(array('model' => 'Question', 'column' => 'id', 'required' => false));
  13.  
  14.     $url = $this->getOption('url');
  15.     if (!empty($url)) {
  16.       $this->widgetSchema['tag'] = new sfWidgetFormProtoculousAutocompleter(array(
  17.         'label' => 'tags:',
  18.         'url' => $url,
  19.         'use_style' => true
  20.       ));
  21.     } else {
  22.       $this->widgetSchema['tag'] = new sfWidgetFormInput(array(
  23.         'label' => 'tags:'
  24.       ));
  25.     }
  26.     $this->widgetSchema->setNameFormat('question[%s]');
  27.   }
  28.  
  29.   public function doSave($con = null)
  30.   {
  31.     $tags = Tag::splitPhrase($this->taintedValues['tag'] . (sfConfig::get('app_permanent_tag') ? ' '.sfConfig::get('app_permanent_tag') : '' ));
  32.     $question = QuestionPeer::retrieveByPk($this->taintedValues['question_id']);
  33.  
  34.     foreach ($tags as $tag) {
  35.       try {
  36.         $questionTag = new QuestionTag();
  37.         $questionTag->setQuestionId($question->getId());
  38.         $questionTag->setUserId($question->getUserId());
  39.         $questionTag->setTag($tag);
  40.         $questionTag->save($con);
  41.       } catch (PropelException $e) {
  42.         // do nothing
  43.       }
  44.     }
  45.     // hmm, i don't like this... :(
  46.     $this->object = $question;
  47.   }
  48. }

To create auto complete widget, I made sfWidgetFormProtoculousAutocompleter. There was JavaScript helper for auto complete. However, I do not want to use it, because I prefer to use sfForm. :) Also symfony1.2 integrated some useful function with jQuery. :) jQuery is a wonderful library, but I hate use two different JavaScript library in one application. If I use jQuery, I only use jQuery, and askeet has a lot of prototype base code, so I created autocompletor widget for sfProtoculousPlugin. :) Well, it was easy anyway.
So, if there was url option when intiating QuestionTagForm, use sfWidgetFormProtoculousAutocompleter. Without option, use another sfWigetFormInput.

Actually I do not like my implementation of doSave. As I saild above, you need to set more than one tag same time when posting. So, I need to save all the tags. Because saving more than one tag, i don't know what to return. I just return the question object here. :(

Good thing about this QuestionTagForm, you can use this both mergeForm and only itself. As I mentioned above, there are two ways to tag, and both are using this class.

lib/form/sfWidgetFormProtoculousAutocompleter.class.php

  1. class sfWidgetFormProtoculousAutocompleter extends sfWidgetFormInput
  2. {
  3.   protected function configure($options = array(), $attributes = array())
  4.   {
  5.     $this->addRequiredOption('url');
  6.     $this->addOption('config', '{ }');
  7.     $this->addOption('use_style');
  8.  
  9.     parent::configure($options, $attributes);
  10.   }
  11.  
  12.   public function render($name, $value = null, $attributes = array(), $errors = array())
  13.   {
  14.     $response = sfContext::getInstance()->getResponse();
  15.     $response->addJavascript(sfConfig::get('sf_prototype_web_dir').'/js/prototype');
  16.     $response->addJavascript(sfConfig::get('sf_prototype_web_dir').'/js/effects');
  17.     $response->addJavascript(sfConfig::get('sf_prototype_web_dir').'/js/controls');
  18.  
  19.     if ($this->getOption('use_style')) {
  20.       $response->addStylesheet(sfConfig::get('sf_prototype_web_dir').'/css/input_auto_complete_tag');
  21.     }
  22.     return parent::render($name, '', $attributes, $errors) .
  23.       content_tag('div' , '', array('id' => $this->generateId($name) . '_autocomplete', 'class' => 'auto_complete')) .
  24.       sprintf('<script type="text/javascript">new Ajax.Autocompleter("%s", "%s", "%s", %s);</script>',
  25.         $this->generateId($name),
  26.         $this->generateId($name . '_autocomplete'),
  27.         $this->getOption('url'),
  28.         $this->getOption('config')
  29.       );
  30.   }
  31. }

I was a bit lazy, so above code is not well written. :( That's all for form code, and let's see the action and templates from here.

/apps/frontend/modules/questions/actions/actions.class.php

  1. // snip
  2.  public function executeNew(sfWebRequest $request)
  3.   {
  4.     $this->form = new QuestionForm();
  5.   }
  6.   public function executeCreate(sfWebRequest $request)
  7.   {
  8.     $this->form = new QuestionForm();
  9.     $this->form->bind($request->getParameter($this->form->getName()));
  10.     if ($this->form->isValid()) {
  11.       $this->form->save();
  12.       $this->redirect($this->generateUrl('question_show', $this->form->getObject()));
  13.     }
  14.     $this->setTemplate('new');
  15.   }
  16. // snip

executeNew and executeCreate is very simple, and probably, you don't need any instruction with this. Good thing about mergeForm is you don't need to know you are using tag form in action class, because we have already merge it in QuestionForm class. :) You just render the quetion form, and bind and save it.

apps/frontend/modules/question/template/newSuccess.php

  1. <h1><?php echo __('ask a question') ?></h1>
  2.  
  3. <div class="in_form">
  4.   <p>
  5.   <?php echo __('Have you looked for similar questions? Check if a related question already exists: The more interesting a question is, the more people will be willing to answer it.') ?>
  6.   </p>
  7.   <p>
  8.   <?php echo __('Be as accurate as you can when giving a title to your question. Keep it short and put the details in the question body.') ?>
  9.   </p>
  10. </div>
  11.  
  12. <form action="<?php echo url_for('question') ?>" method="post" class="form">
  13.   <fieldset>
  14.     <?php echo $form['title']->renderError() ?>
  15.     <?php echo $form['title']->renderLabel() ?>
  16.     <?php echo $form['title'] ?>
  17.     <br class="clearleft" />
  18.  
  19.     <?php echo $form['body']->renderError() ?>
  20.     <?php echo $form['body']->renderLabel() ?>
  21.     <?php echo $form['body'] ?>
  22.     <br class="clearleft" />
  23.     <?php echo include_partial('content/markdown_help') ?>
  24.  
  25.     <?php echo $form['tag']->renderError() ?>
  26.     <?php echo $form['tag']->renderLabel() ?>
  27.     <?php echo $form['tag'] ?>
  28.     <br class="clearleft" />
  29.     <div class="small in_form"><?php echo __('example: askeet "how to"') ?></div>
  30.  
  31.   </fieldset>
  32.  
  33.   <div class="right">
  34.     <input type="submit" value="<?php echo __('ask it') ?>" />
  35.   </div>
  36. </form>

View file is also simple. Because we merge the QuestionTagForm, you can use $form['tag'], here! O.K, now you are able to post a question with tagging with auto complete feature!

O.K, it is about time to write about tag form that you find in sidebar. We will use the same QuestionTagForm here. :) To add tag, you have to be authenticated and go to the question/show page. In the component slot, _question.php, you include tag/add component. This component displays the tagging form.

/apps/frontend/modules/tag/actions/components.class.php

  1. class tagComponents extends sfComponents
  2. {
  3.   public function executeAdd(sfWebRequest $request)
  4.   {
  5.     $questionTag = new QuestionTag();
  6.     $questionTag->setQuestionId($this->question_id);
  7.     $this->form = new QuestionTagForm($questionTag, array('url' => $this->generateUrl('tag_autocomplete')));
  8.   }                                                                             
  9. }

In the action, executeAdd, we set QuestionTag object with question_id. Plus auto_complete url is also set when initialize form class. As you can see the template file, this form is ajax form.

/app/frontend/modules/tag/templates/_add.php

  1. <?php if ($sf_user->isAuthenticated()): ?>
  2. <div><?php echo ('Add your own:') ?>
  3. <?php echo form_remote_tag(array(                                             
  4.   'url'    => 'tag_create',
  5.   'update' => 'question_tags',
  6.   'complete' => '$("ask_question_tag_tag").value=""'
  7. )) ?>
  8. <?php echo $form['question_id'] ?>
  9. <?php echo $form['tag'] ?>
  10. <input type="submit" value ="<?php echo __('tag') ?>" />
  11. </form>
  12. </div>
  13. <?php endif; ?>

This form request action goes tag_create rule, which is tag/executeCreate action.

apps/frontend/modules/actions/tag/actions/actions.class.php

  1. // snip
  2.  public function executeCreate(sfWebRequest $request)
  3.   {
  4.     $form = new QuestionTagForm();
  5.     $form->bind($request->getParameter($form->getName()));
  6.     if ($form->isValid()) {
  7.       // xxx this form->save returns qustion object
  8.       $this->question = $form->save();
  9.     } else {
  10.       $this->question = $form->getObject()->getQuestion();
  11.     }
  12.     $this->tags = $this->question->getTags();
  13.   }
  14. // snip

As I mentioned above QuestionTagForm's doSave method returns not QuestionTag object, but Question Object, which is not really nice I think. :( Anyway, let's render the tags value to the view file and return value for ajax request. In createSuccess.php, it is same as tag/question_tags partial. What you need to do here is just updating the question tags list which has just tagged by you.

apps/frontend/modules/actions/tag/actions/actions.class.php

  1. // snip
  2.  public function executeAutocomplete(sfWebRequest $request)
  3.   {
  4.     $this->tags = array();
  5.     $form = new QuestionTagForm();
  6.     $form->bind($request->getParameter($form->getName()));
  7.     if ($form->isValid()) {
  8.       $values= $form->getTaintedValues();
  9.  
  10.       $this->tags = QuestionTagPeer::getTagsForUserLike(
  11.         $this->getUser()->getSubscriberId(),
  12.         $values['tag'],
  13.         10
  14.       );
  15.     }
  16.   }
  17. // snip

2009/03/29

Playing with sfForm with Askeet Part 1

It took time to write a new post. :( My current project had a tight schedule, and it is very important for me to work on this project. Sorry. ;)

So, in this post, I will explain some tips for sfForm using the code I implemented in Askeet. I will explain these two cases for form handling:

  • user login / registration switching
  • question posting with tags and tags posting(next post)

To explain the code, I will use following files.

  • lib/form/UserLoginForm.class.php
  • lib/form/UserMixForm.class.php
  • apps/frontend/modules/user/actions/actions.class.php(executeNew, executeLogin, executeCreate
  • apps/frontend/templates/layout.php
  • apps/frontend/modules/user/actions/components.class.php
  • apps/frontend/modules/user/templates/login.php
  • apps/frontend/modules/user/templates/newSuccess.php

Before I explain the code I need to clarify the specification of user login and registration process in Askeet. There are two ways to login Askeet:

  • For unauthorized users, when some actions require authentication, such as posting questions and answers, voting for interest, moderating and etc, the login form dynamically appears on the page. Like following image.
  • Following signin/register link lead you the page you can login and register. When login failed by both ways this page is used as well. Like following image.

O.K, let's start from the first one. Because I'm using symfony1.2, I should use sfForm instead writing every input tags or tag_helper in the view template. I didn't know how to implement such form, but I got a good idea from Exgear's tutorial, and I followed the instruction.
Here, I wrote some code in UserLoginForm.class.php, layout.php, components.class.php, and its template, _login.php. The code are follows:

UserLoginForm.class.php

  1. class UserLoginForm extends BaseUserForm
  2. {
  3.   public function configure()
  4.   {
  5.     unset(
  6.       $this['id'],
  7.       $this['created_at'],
  8.       $this['first_name'],
  9.       $this['last_name'],
  10.       $this['email'],
  11.       $this['sha1_password'],
  12.       $this['salt'],
  13.       $this['ask_interest_list'],
  14.       $this['ask_relevancy_list']
  15.     );
  16.     $this->validatorSchema['nickname'] = new sfValidatorString(
  17.       array('min_length' => 5),
  18.       array('required' => 'your nickname is required', 'min_length' => 'nickname must be 5 or more characters')
  19.     );
  20.  
  21.     $this->widgetSchema['password'] = new sfWidgetFormInputPassword();
  22.     $this->validatorSchema['password'] = new sfValidatorString(
  23.       array(),
  24.       array('required' => 'your password is required')
  25.     );
  26.     $this->widgetSchema['referer'] = new sfWidgetFormInputHidden();
  27.     $this->validatorSchema['referer'] = new sfValidatorPass();
  28.     $request = sfContext::getInstance()->getRequest();
  29.     if ($request->isMethod('get')) {
  30.       $this->setDefault('referer', $request->getReferer());
  31.     } else {
  32.       $this->setDefault('referer', $request->getParameter($this->getName().'[referer]'));
  33.     }
  34.  
  35.     // check valid user
  36.     $this->validatorSchema->setPostValidator(new sfValidatorCallback(array(
  37.       'callback' => array($this, 'validUser'),
  38.     )));
  39.  
  40.     $this->validatorSchema->setOption('allow_extra_fields', true);
  41.   }
  42.  
  43.   public function validUser($validator, $values)
  44.   {
  45.     if (empty($values['nickname']) or empty($values['password'])) {
  46.       return;
  47.     }
  48.     $nickname = $values['nickname'];
  49.     $c = new Criteria();
  50.     $c->add(UserPeer::NICKNAME, $nickname);
  51.     $user = UserPeer::doSelectOne($c);
  52.  
  53.     if (is_null($user) || sha1($user->getSalt().$values['password']) != $user->getSha1Password()) {
  54.       throw new sfValidatorError($validator, 'no user available');
  55.     }
  56.     $this->object = $user;
  57.  
  58.     return $values;
  59.   }
  60. }

Well, this code is straightforward and not many tricks.If I have to say something, I would say using sfValidatorCallback to authorizing user request as post validator. Actually this idea is also from exgear's site. They are really good. :)
BTW, some might think why I put allow_extra_fields there? Well, I will explain this later. As it is, extra fields are expecting for UserLoginForm.

So, create apps/frontend/modules/user/actions/components.class.php, and render the form object of the class.

components.class.php

  1. class userComponents extends sfComponents                                       
  2. {
  3.   public function executeLogin(sfWebRequest $request)
  4.   {
  5.     $this->form = new UserLoginForm();
  6.   }
  7. }

Simple! Huh! :)

Now its view file, apps/frontend/modules/users/templates/_login.php

_login.php

  1. <form action="<?php echo url_for('login') ?>" method="POST" id="loginform">
  2. <?php echo $form['nickname']->renderLabel() ?> <?php echo $form['nickname'] ?>
  3. <?php echo $form['password']->renderLabel() ?> <?php echo $form['password'] ?>
  4. <?php echo $form['referer'] ?>
  5. <input type='submit' value="<?php echo __('login') ?>">
  6. <?php echo link_to_function(__('cancel'), visual_effect('blind_up', 'login', array('duration' => 0.5))) ?>
  7. </form>

As I am a programmer, I do not like to write every echo $form['blabla'] in view files, but I was too lazy to think other way right now. :( I might try better ways later if I had any chances. :) Anyway, this is very easy, so no explanation here either. Blinding up animation is there, because it was found in the Askeet tutorial, and I liked the cool animation. :) I did not write any error message in this template, you will see why later.

Now you need to include this components in layout.php

layout.php

  1. // snip
  2.       <div id="login" style="display: none">
  3.         <h2><?php echo __('please sign-in first') ?></h2>
  4.         <?php include_component('user', 'login') ?>
  5.       </div>
  6. // snip

Now you can see the form container is invisible, and use link_to_function to will change the status to visible. When actions need user authentication, echo the next helper function, and the login form will appear!

  1. link_to_function('some action label', visual_effect('blind_down', 'login', array('duration' => 0.5)));

O.K, that's one way to login for user. It was longer than I thought. :) We didn't reach the tricky tips yet. Now you might think "what about login error page? How do users know if he or she typed right nickname and password? ", Well, those error messages are displayed signin /register page. So, when user login failed, you have different login page, which I will explain from now.

For easy user registration and login, many web applications use the same form to login or registering. Askeet also use this strategy. The default page of signin/register page only displays nickname and password fields, and a checkbox. Yes, this checkbox having label as "click here to create a new account", switches this form action to be login or registering. Look at the image again.

If you check the checkbox, two more fields will appear in the page like following image:

O.K, finally! I had struggled a bit to implementing this with sfForm. Askeet tutorial was easy, because the tutorial write the input helper directly. I'm using sfForm framework, and I want to avoid this. To implement this form, I created UserMixForm.class.php for this page. UserLoginForm.class.php was for only login, but this form class is for registration mainly. Maybe naming is not good, though :(
Anyway, I copy all the source of UserMixForm.class.php here:

  1. class UserMixForm extends BaseUserForm
  2. {
  3.   public function configure()
  4.   {
  5.     unset(
  6.       $this['id'],
  7.       $this['created_at'],
  8.       $this['first_name'],
  9.       $this['last_name'],
  10.       $this['sha1_password'],
  11.       $this['salt'],
  12.       $this['ask_interest_list'],
  13.       $this['ask_relevancy_list']
  14.     );
  15.  
  16.     $this->widgetSchema['nickname'] = new sfWidgetFormInput(array('label' => 'Nickname:'));
  17.     $this->validatorSchema['nickname'] = new sfValidatorString(
  18.       array('min_length' => 5),
  19.       array('required' => 'your nickname is required', 'min_length' => 'nickname must be 5 or more characters')
  20.     );
  21.  
  22.     $this->widgetSchema['password'] = new sfWidgetFormInputPassword(array('label' => 'Password:'));
  23.     $this->validatorSchema['password'] = new sfValidatorString(
  24.       array(),
  25.       array('required' => 'your password is required')
  26.     );
  27.     $controller = sfContext::getInstance()->getController();
  28.     $this->widgetSchema['new'] = new sfWidgetFormInputCheckbox(array(
  29.       'label' => 'click here to create a new account'
  30.       ),
  31.       array('style' => 'display: inline; float: none',
  32.         'onclick' => "
  33.       if (Element.visible('new_account')) {
  34.         Effect.BlindUp('new_account');
  35.         $('login_form').action = '" . $controller->genUrl('login') . "';
  36.        } else {
  37.         Effect.BlindDown('new_account');
  38.         $('login_form').action = '" . $controller->genUrl('user_create') . "';
  39.        }
  40.     "));
  41.     $this->validatorSchema['new'] = new sfValidatorPass();
  42.  
  43.     $this->widgetSchema['password_biz'] = new sfWidgetFormInputPassword(
  44.       array('label' => 'confirm your password')
  45.         );
  46.     $this->validatorSchema['password_biz'] = new sfValidatorString(
  47.       array(),
  48.       array('required' => 'password confirmation is required')
  49.     );
  50.  
  51.     $this->widgetSchema['email'] = new sfWidgetFormInput(
  52.       array('label' => 'your email')
  53.     );
  54.     $this->validatorSchema['email'] = new sfValidatorEmail(
  55.       array('min_length' => 5),
  56.       array(
  57.         'required' => 'your email is required',
  58.         'invalid' => 'email address is invalid'
  59.       )
  60.     );
  61.  
  62.     $this->validatorSchema->setPostValidator(new sfValidatorAnd(array(
  63.       new sfValidatorSchemaCompare(
  64.         'password', '==', 'password_biz', array(),
  65.         array('invalid' => 'password and confirmed password does not match')
  66.       ),
  67.       new sfValidatorPropelUnique(
  68.         array('model' => 'User', 'column' => array('nickname')),
  69.         array('invalid' => 'the nickname is already taken.')
  70.       ),
  71.       new sfValidatorPropelUnique(
  72.         array('model' => 'User', 'column' => array('email')),
  73.         array('invalid' => 'the email address is areadly registered.')
  74.       )
  75.     )));
  76.  
  77.     $this->widgetSchema['referer'] = new sfWidgetFormInputHidden();
  78.     $this->validatorSchema['referer'] = new sfValidatorPass();
  79.     $request = sfContext::getInstance()->getRequest();
  80.     if ($request->isMethod('get')) {
  81.       $this->setDefaults(array('referer' => $request->getReferer()));
  82.     }
  83.   }
  84.  
  85.   public function updateObject($values = null)
  86.   {
  87.     $object = parent::updateObject($values);
  88.     $password = $this->taintedValues['password'];
  89.     $object->setPassword($password);
  90.     return $object;
  91.   }
  92.  
  93.   public function mergeStatus($loginform)
  94.   {
  95.     if (isset($loginform)) {
  96.       $nickname = $loginform->taintedValues['nickname'];
  97.       $referer = $loginform->taintedValues['referer'];
  98.       $this->errorSchema = $loginform->getErrorSchema();
  99.       $this->setDefault('nickname', $nickname);
  100.       $this->setDefault('referer'$referer);
  101.     }
  102.   }
  103. }

Wow, this is long, but the longest method, configure, does not need any explanation I think. The method, updateObject call setPassword, because there is no password field for user model. There are sha1_password and sha1 fields. So, setPassword of user model set both fields. Actually it is same as Askeet tutorial, so I skip this, too. I just need to update the field, so I overrided the method.

What about mergeStatus method? Well, this is something new. Hehehe. :) Because this is my original idea. I did not refer any site to implement this, and I believe this is a tricky part. As I mentioned above, UserMixForm is actually for user registration. However, this form also handles user login and displaying login error messages as well. To succeed the login failure messages and input values, this method was needed. You will see the reason soon.

So, the signin/register page is assigned to executeNew method in actions. executeCreate is for binding, validating, and saving the user registration form. Finally executeLogin binding, validating login authentication, and if the validation failed it will render the error messages as well. O.K, let's see the code :)

  1. // snip
  2.   public function executeNew(sfWebRequest $request)
  3.   {
  4.     if ($this->getUser()->isAuthenticated()) {
  5.       $this->redirect('homepage');
  6.     }
  7.     $this->form = new UserMixForm();
  8.   }
  9.  
  10.   public function executeCreate(sfWebRequest $request)
  11.   {
  12.     $this->form = new UserMixForm();
  13.     $this->form->bind($request->getParameter($this->form->getName()));
  14.     if ($this->form->isValid()) {
  15.       $this->form->save();
  16.       $this->forward('user', 'login');
  17.     }
  18.     // use user registration form in login template
  19.     $this->getRequest()->setAttribute('newaccount', true);
  20.     $this->setTemplate('new');
  21.   }
  22.   public function executeLogin(sfWebRequest $request)
  23.   {
  24.     $loginform = new UserLoginForm();
  25.     $loginform->bind($request->getParameter($loginform->getName()));
  26.     if ($loginform->isValid()) {
  27.       $this->getUser()->signIn($loginform->getObject());
  28.       $this->redirect($request->getParameter($loginform->getName() .'[referer]', 'homepage'));
  29.     }
  30.     $this->form = new UserMixForm();
  31.     $this->form->mergeStatus($loginform);
  32.     $this->setTemplate('new');
  33.   }
  34.  
  35. // snip

The method, executeNew does not need any explanation. It just renders the UserMixForm object. I don't want authorized user to access this page, so let them forward to homepage route. O.K. the method, executeCreate is also straightforward. It is just another form transaction (bind, validate, save). In case the validation failed, set newaccount attribute for checkbox to be checked, and use the same template as executeNew.

O.K, finally! We are in tricky part, executeLogin. Because UserMixForm and UserLoginForm are children of BaseUserForm class, both share same fields, such as nickname, password. So, UserLoginForm require nickname, password, referer, and the for send the parameters. UserMixForm also send nickname, password, referer(and other fields, too). Well, yes! UserMixForm has other fields. That's why allow_extra_fields is on in UserLoginForm. If the user authentication failed, this action uses newTemplate which means using UserMixForm. You need to render the form value, and you need to set the error messages and input values from the last request to the form as well. To do this, you have to pass them from userLoginForm to UserMixForm. Here is mergeStatus method. I don't know if this name is proper, though. :(

Oops, I forgot to mention newSuccess.php.

newSuccess.php

  1. <h1><?php echo __('sign in / register') ?></h1>
  2.  
  3. <div class="in_form">
  4. <p><?php echo __('Registration is free and required only to create a new question or rate an answer.') ?></p>
  5. </div>
  6. <?php echo $form->renderGlobalErrors() ?>
  7. <form action="<?php echo $sf_request->getAttribute('newaccount', false) ? url_for('user_create') : url_for('login') ?>" method="POST" id="login_form" class="form">
  8.   <fieldset>
  9.     <?php echo $form['nickname']->renderLabel() ?>
  10.     <?php echo $form['nickname']->renderError() ?><?php echo $form['nickname'] ?>
  11.     <br class="clearleft"/>
  12.     <?php echo $form['password']->renderLabel() ?><?php echo $form['password']->renderError() ?>
  13.     <?php echo $form['password'] ?>&nbsp;<?php echo link_to(__('forgot your password?'), 'user_require_password') ?>
  14.     <br class="clearleft"/>
  15.     <div class="in_form">
  16.       <?php echo $form['new'] ?>
  17.       &nbsp;<?php echo $form['new']->renderLabelName() ?>
  18.     </div>
  19.     <br class="clearleft"/>
  20.     <div id="new_account"<?php echo $sf_request->getAttribute('newaccount', false) ? '' : ' style="display: none"' ?>>
  21.       <?php echo $form['password_biz']->renderLabel() ?>
  22.       <?php echo $form['password_biz']->renderError() ?>
  23.       <?php echo $form['password_biz'] ?>
  24.       <br class="clearleft"/>
  25.  
  26.       <?php echo $form['email']->renderLabel() ?>
  27.       <?php echo $form['email']->renderError() ?>
  28.       <?php echo $form['email'] ?>
  29.       <br class="clearleft"/>
  30.  
  31.       <div class="small in_form"><?php echo __('askeet will never disclose this address to a third party') ?></div>
  32.     </div>
  33.   </fieldset>
  34.   <?php echo $form['referer'] ?>
  35.   <div class="right">
  36.   <input type='submit' value="<?php echo __('sign in') ?>">
  37.   </div>
  38. </form>

It is another usual form. No explanation.

Because there are difficulties to explain in English (Probably it would be hard to write this topic in Japanese anyway), I may fix some words or explanation I posted today.

Anway, that's it. Wow, this is a long post, I have to split the tips to two posts. Hmm. Next is Question and QuestionTag form handling. Have fun! :) :) :) :)

2009/03/10

How about sfAskeetPlugin!

I was talking with akky a few minutes ago, and he had a great idea!

It was posting askeet as symfony plugin like a sfJobeetPlugin!

I'm going to work on this for my free time, so it might take a little time. Please wait!

BTW, I replaced the tar ball, because I found a bug. Answering by anonymous user broke the table constraint, so I fixed some code.

I need more unit / functional testing. :)

2009/03/09

Askeet with symfony1.2

O.K, this is my first post in English. I will try my best. :)

After symfony1.2 launched, Jobeet was introduced to the world for users to learn through developing it. The tutorial is very effective for symfony users. I also did the tutorial and learned a lot of symfony1.2 new feature.

Back to 2005, when symfony emerged, there was an application like Jobeet for symfony learners. The application was Askeet! I have used symfony since symfony0.6, and learned a lot from the Askeet tutorial that time. However, the tutorial had some small mistakes, and the source code you can get from svn repository and tar ball was a bit buggy. :( I struggled a lot to complete the tutorial. Maybe Fabien and Francois did it on purpose to motivate us to investigate inside of symfony further. Hehe. ;) Currently nobody care the Askeet tutorial, because there is Jobeet, and Askeet tutorial is obsolete, since symfony is now 1.2!

Is Askeet just a tutorial? Even Askeet is an open source application, I have not encountered any web sites integrate Askeet. Why? I have some reasons in my mind. First, as I wrote on above, it is a bit buggy, such as same duplicate question title cause error, user_interest rule and related actions are never used, and etc. Second, even there was a cache chapter in the tutorial, more caching is needed for real world applications. Third, symfony is growing rapidly, which is good, but applications written by old version of symfony are hard to be taken care. Forth, there is no active source code repository for the application. Of course there is a repository for tutorial, but not for the application.

I was waiting for that somebody develops Askeet with symfony1.2, and give us its tutorial. Actually there were a couple tries in Japan, and I was excited about their work to finish.... BUT, after symfony team introduced Jobeet, everybody went to Jobeet, and the tries are gone to somewhere. :(

So, I did it! :)

Well, since I am too lazy, I'm not going to write tutorials for beginners, but I will give you some tips I found from the experience that I was developing Askeet with symfony1.2. I'm still working on caching strategy for this application, so any help is welcome. Actually my friend, akky, is helping for i18n now.

O.K, for today, I give you the current source code. License? Same as symfony! I'm using symfony1.2.4(current version:3/10/09). You can learn a lot of symfony features from the code I believe. Why I open the source code? Well, there are some reasons. First I love symfony and would like to contribute to symfony community, and second, askeet was an open source application, and why not to. Third, to show how much I can do. People will know my current skill, so that I might get job requests in the future. ;) Finally, even I open the code, I know I am the one who know the code most. :)

You can download the source code from here:Askeet with symfony1.2

O.K. In a few weeks, I will probably post following topics which I found from developing Askeet with symfony1.2:

  • Playing with sfForm
  • Building complex Criteria
  • Cross filtering between different models in Admin Generator
  • Command task from Web

In English version, it works good. However my language, Japanese, has some problems because of full text search and multibyte issues. We are working on it now, and probably, I will post the fixed one later. Well, I need a svn repository, where to use?

2009/02/22

mergeFormかembedFormか

フォームオブジェクトを使う際にはどちらを使うのがいいのかな、と私も瞬間悩んだけど、sfForm.class.phpやsfPropelForm.class.phpを読んでみると、結構動作が違うね。embedFormForEachはembedFormをかぶせただけなので、それほど違いはないとは思う。未だにバグがあると私は思っているので、使う予定はないけども。

mergeFormの場合

  • 単純にフォームをくっつけたいときに使う
  • 具体的には、FormオブジェクトのDefault値とwidgetSchema、widgetValidatorのみmergeをしている
  • mergeをしているという名前のごとく、widgetのfield値が同じであった場合には、mergeされてしまうので、複数追加することができない
  • mergeされたFormオブジェクトの更新系のメソッド(updateObjectとかdoSave)が呼ばれることはないので、手元のフォームで呼んであげないといけないupdateDefaultFromObjectは、呼ばれる。コンストラクタ内なので

embedFormの場合

  • フォームを入れ子にしたいときに使う
  • embedされたFormオブジェクトは、sfFormクラスのインスタンスオブジェクト$embedFormsに格納される
  • 新しくfieldを指定して、その中にembedするので、embedFormの中のwidgetのfield値が同じであっても構わない(新しいfieldはユニークでなければいけないが)
  • embedされたFormオブジェクトのupdateObject, doSaveが呼ばれる

パッと見た感じ、似たようなもんかなー、と思っていたら、データの保存とかが結構違うのでmergeされたFormクラスのupdateObjectなどを独自で書いている際には、明示してあげないと呼ばれないので、ちょっとビックリする。呼ばれることを前提として書いていたので。。。

あと、sfGuardPluginのsfGuardUserAdminForm.class.phpには、mergeFormが出てくるけど、このupdateObjectってトランザクションを持たずにsaveメソッドを呼んでいるような気がするのだが。。。

というわけでちょっとした自分用のメモ。

2009/02/12

symfony1.2 embedFormでファイルアップロード

symfonyでFormを使うときって、mergeFormやらembedFormって使っていますか?

私は、最近までは別々のFormクラスを作って、アクションクラスで使うFormクラスをtemplateにrenderしていました。はい。力業力業。よくないねぇ。

最近は、その辺を激しくリファクタリングして、前から気になっていたmergeFormやembedForm使ってみました。で、いろいろ使ってみると、これいいじゃない?というわけで、そのことをブログに書いてみることにします。

環境は、1.2.5-dev。まぁ、1.2.4でも動きます。embedFormForEachというのもあって、複数のフォームを使いたいときは、そちらを使う方がスマートのような感じがしますが、バグがあるせいか、どうもうまくいかないので、embedFormをループで回して使ってみることにしました。今回は、複数個アップロードするということで、mergeFormは扱わずに説明します。まぁ、いつか説明してもいいけど、こちらはすでにいくつもサンプルがあるので、そちらを勝手に調べてくださいな。私は、embedFormで画像のアップロードを扱うことにします。

超簡単なサンプルということで、スキーマはこんな感じでどうかしら?

  1. propel:
  2.   posts:
  3.     _attributes: { phpName: Post }
  4.     id:
  5.     title:
  6.       type: varchar(128)
  7.       required: true
  8.     description:
  9.       type: longvarchar
  10.       required: true
  11.     created_at:
  12.     updated_at:
  13.   pictures:
  14.     _attributes: { phpName: Picture }
  15.     id:
  16.     post_id:
  17.       type: integer
  18.       required: true
  19.       foreignTable: posts
  20.       foreignReference: id
  21.       onDelete: cascade
  22.       onUpdate: cascade
  23.     filename:
  24.       type: varchar(255)
  25.       required: true
  26.     caption:
  27.       type: varchar(128)
  28.       required: true

相変わらずpropel使っています。説明はいらないと思うけども、一応。

postsというテーブルと、picturesというテーブルを用意して、picturesにはpost_idというカラムを持たせて、postsテーブルにリレーションをさせます。まぁ、そんだけ。つまり、postsとpicturesは、1-N関係なのですね。

databases.ymlなどの設定をしておいて、propel:build-allをするとformクラスやmodelクラスの雛形を作成してくれますね。モジュールも手で作るのが面倒なのと今回の説明には大事なところではないので、propel:generate-module post Postで作っておきます。post/templates/_form.phpは、現在のPostFormオブジェクトによって、作成されるので、echo $formだけで超シンプルにしておきます。つまり、以下のようにしておいてください。

  1. <?php if (!$form->getObject()->isNew()): ?>                                     
  2. <input type="hidden" name="sf_method" value="put" />                           
  3. <?php endif; ?>                                                                 
  4.   <table>                                                                       
  5.     <tfoot>                                                                     
  6.       <tr>                                                                     
  7.         <td colspan="2">                                                       
  8.           <?php echo $form->renderHiddenFields() ?>                             
  9.           &nbsp;<a href="<?php echo url_for('post/index') ?>">Cancel</a>       
  10.           <?php if (!$form->getObject()->isNew()): ?>                           
  11.             &nbsp;<?php echo link_to('Delete', 'post/delete?id='.$form->getObject()->getId(), array('method' => 'delete', 'confirm' => 'Are you sure?')) ?>     
  12.           <?php endif; ?>                                                       
  13.           <input type="submit" value="Save" />                                 
  14.         </td>                                                                   
  15.       </tr>                                                                     
  16.     </tfoot>                                                                   
  17.     <tbody>                                                                     
  18.       <?php echo $form->renderGlobalErrors() ?>                                 
  19.       <?php echo $form ?>                                                       
  20.     </tbody>                                                                   
  21.   </table>                                                                     
  22. </form>

では、まずPostフォームを用意します。propel:build-allをするとbuild-formもついでにしてくれるので、lib/form/PostForm.class.phpができていると思います。これをちょいといじってみます。

  1. public function configure() {
  2.     unset($this['created_at'], $this['updated_at']);
  3.     $this->setWidgets(array(
  4.       'id' => new sfWidgetFormInputHidden(),
  5.       'title' => new sfWidgetFormInput(array(
  6.         'label' => 'タイトル',
  7.       )),
  8.       'description' => new sfWidgetFormTextarea(array(
  9.         'label' => '説明',
  10.       )),
  11.     ));
  12.  
  13.     $this->setValidators(array(
  14.       'id' => new sfValidatorPropelChoice(
  15.         array('model' => 'Post', 'column' => 'id', 'required' => false)
  16.       ),
  17.       'title' => new sfValidatorString(
  18.         array('max_length' => 128, 'min_length' => 3),
  19.         array(
  20.           'max_length' => 'タイトルは128文字以内でお願いします。',
  21.           'min_length' => 'タイトルは3文字以上でお願いします。'
  22.         )
  23.       ),
  24.  
  25.       'description' => new sfValidatorString(
  26.         array('max_length' => 2048, 'min_length' => 3),
  27.         array(
  28.           'max_length' => '説明本文は2000文字以内でお願いします。',
  29.           'min_length' => '説明本文は3文字以上でお願いします。'
  30.         )
  31.       ),
  32.     ));
  33.     $this->widgetSchema->setNameFormat('post[%s]');
  34.   }
  35. }

左の画像のようになりますね。まぁ、こんなもんでしょう。titleとdescriptionだけを入力する単純なフォームですね。バリデーションは適当に書いてみました。ごくごく初歩的なsfPropelFormの使い方だと思います。説明はいらないですね。

では、PictureFormも書いてみます。

  1. class PictureForm extends BasePictureForm
  2. {
  3.   public function configure()
  4.   {
  5.     unset($this['post_id']);
  6.  
  7.     $captions = array('A', 'B', 'C', 'D');
  8.     $this->setWidgets(array(
  9.       'id' => new sfWidgetFormInputHidden(),
  10.       'filename' => new sfWidgetFormInputFileEditable(array(
  11.         'label' => false,
  12.         'delete_label' => 'ファイルを削除する',
  13.         'file_src' => '/uploads/' . $this->getObject()->getFilename(),
  14.         'is_image' => true,
  15.         'edit_mode' => !$this->isNew(),
  16.         'template' => '<div>%file%<br />%input%<br />%delete% %delete_label%</div>',
  17.       )),
  18.       'caption' => new sfWidgetFormSelect(
  19.         array(
  20.           'label' => '説明',
  21.           'choices' => array_combine($captions, $captions)
  22.         )
  23.       )
  24.     ));
  25.  
  26.     $this->setValidators(array(
  27.       'id' => new sfValidatorPropelChoice(array(
  28.         'model' => 'Picture', 'column' => 'id', 'required' => false
  29.       )),
  30.       'filename' => new sfValidatorFile(
  31.         array('required' => false, 'path' => sfConfig::get('sf_upload_dir')),
  32.         array(
  33.           'max_size' => 'ファイルサイズが大きすぎます。',
  34.           'mime_types' => '投稿できる画像フォーマットではありません。',
  35.           'partial' => 'ファイルアップロードに失敗しました。もう一度、投稿してください
  36.           。',
  37.           'no_tmp_dir' => 'システムエラーです。管理者にお伝えください。',
  38.           'cant_write' => 'システムエラーです。管理者にお伝えください。',
  39.           'extension' => 'システムエラーです。管理者にお伝えください。'
  40.         )),
  41.       'caption'    => new sfValidatorString(array(
  42.         'max_length' => 255, 'required' => false
  43.       )),
  44.       'filename_delete' => new sfValidatorPass()
  45.     ));
  46.     $this->widgetSchema->setFormFormatterName('list');
  47.   }
  48. }

これも説明の必要はないかな。symfony1.2からsfWidgetFormInputFileEditableというwidgetが追加されて、これがファイルアップロード関係でいろいろやってくれるのですね。便利になったものです。使い方はこのソースを読んでもいいですし、jobeetのadmin-generatorの章にも書いてありますので、それを参照してください。ここでは、画像だけをアップロードしてもいいのですが、せっかくなので、画像の説明としてcaptionというカラムもpictureテーブルに持たせてみることにします。A,B,C,Dというのはいいアイデアがなかったのとサンプルなので、まぁ、適当に。

これで、二つの独立したテーブルができましたね。しかし、postの投稿フォームにpictureも一緒に投稿させたいじゃないですか。さらに、pictureが複数投稿できたら尚良さそうですよね。ということで、PostFormにPictureFormをembedすることにします。しかも、複数。ここは決め打ちで3つとします。まぁ、サンプルなので。
PostFormのバリデーションの記述の後にでも、次のコードを追記してみましょう。

  1. $pictures = ($this->getObject()->isNew()) ? null: $this->getObject()->getPictures();
  2.     for ($i = 0; $i <3; $i++) {
  3.       $picture = (isset($pictures) and isset($pictures[$i])) ? $pictures[$i] : null;
  4.       $pictureForm = new PictureForm($picture);
  5.       $this->embedForm('picture_' . $i, $pictureForm, '%content%');
  6.       $this->widgetSchema['picture_' . $i]->setLabel('画像');
  7.     }

少し説明しますと、Postが新規投稿であれば、何もしないですが、編集の際には、PictureFormに初期値を設定させて、それをembedFormしていきます。embedFormをする際には、picture_0, picture_1, picture_2というnameを与えて指定してみることにします。また、embedですから入れ子になりますし、ちょっと見た目がダサくなるのでデコレーションは、%content%にしておきます。ついでにlabelも指定しておきます。Formとして表示させるには、これだけ追加するだけで、左の画像のように、title, descriptionだけではなく、3つのPictureFormつまり、filename, captionが3つあるフォームができあがります。

ただ、これだけでは、このpictureがこのpostに関連づけられているかどうかがわからないので、PostFormのupdateObjectでsetPostしてあげます。

  1. public function updateObject($values = null)
  2.   {
  3.     $object = parent::updateObject($values);
  4.     $values = $this->getValues();
  5.  
  6.     foreach ($this->embeddedForms as $i => $picture) {
  7.       if ($picture->getObject()) {
  8.         if ($picture->getObject()->getFilename() == '') {
  9.           unset($this->embeddedForms[$i]);
  10.         } else {
  11.           $picture->getObject()->setPost($object);
  12.         }
  13.       }
  14.     }
  15.   }

ここは少し説明がいりますね。まず、PostのupdateObjectはそのまま親のメソッド呼んで、設定させておきます。embedFormでembedしたFormオブジェクトは、このPostFormクラスのembedFormsフィールドに配列で保持されているので(sfForm.class.php参照)、これをループで回して、どのpostと関連つけているかを指定してあげます。ついでにファイルがアップロードされない場合も保存されてしまいますので、getFilename()が空文字列の場合は、embedFormsからその要素をunsetしておきます。これで、ファイルが入っていたときのみ、保存されることになります。

これで新規投稿に関してはファイルがアップロードがちゃんとできるようになりました。また、編集時には、ちゃんとそのフォームに画像が表示されます。
しかし、このままでは実は編集時に使うfilename_deleteを使うことができないのですね。このままですと、そのpictureオブジェクトを削除せずにpictureオブジェクトのfilenameのカラムを空にしてくれるだけです。まぁ、当たり前と言えば当たり前か。ということで、ファイルを削除できるようにしてみます。

チェックボックスには、filename_deleteという名前を与えているので、フォームから渡ってきた値にfilename_deleteがあった場合には、そのPictureのオブジェクトをインスタンス変数のremovePicturesという配列に格納しておくことにします。そして、doSaveでDBをいじるときに一緒にremovePicturesにあるPictureオブジェクトを削除することにします。ということで、最終的にできあがったPostFormは以下のようになります。

  1. class PostForm extends BasePostForm
  2. {
  3.   private $removePictures = array();
  4.   public function configure() {
  5.     unset($this['created_at'], $this['updated_at']);
  6.  
  7.     $this->setWidgets(array(
  8.       'id' => new sfWidgetFormInputHidden(),
  9.       'title' => new sfWidgetFormInput(array(
  10.         'label' => 'タイトル',
  11.       )),
  12.       'description' => new sfWidgetFormTextarea(array(
  13.         'label' => '説明',
  14.       )),
  15.     ));
  16.  
  17.     $this->setValidators(array(
  18.       'id' => new sfValidatorPropelChoice(
  19.         array('model' => 'Post', 'column' => 'id', 'required' => false)
  20.       ),
  21.       'title' => new sfValidatorString(
  22.         array('max_length' => 128, 'min_length' => 3),
  23.         array(
  24.           'max_length' => 'タイトルは128文字以内でお願いします。',
  25.           'min_length' => 'タイトルは3文字以上でお願いします。'
  26.         )
  27.       ),
  28.  
  29.       'description' => new sfValidatorString(
  30.         array('max_length' => 2048, 'min_length' => 3),
  31.         array(
  32.           'max_length' => '説明本文は2000文字以内でお願いします。',
  33.           'min_length' => '説明本文は3文字以上でお願いします。'
  34.         )
  35.       ),
  36.     ));
  37.  
  38.     $pictures = ($this->getObject()->isNew()) ? null: $this->getObject()->getPictures();
  39.     for ($i = 0; $i <3; $i++) {
  40.       $picture = (isset($pictures) and isset($pictures[$i])) ? $pictures[$i] : null;
  41.       $pictureForm = new PictureForm($picture);
  42.       $this->embedForm('picture_' . $i, $pictureForm, '%content%');
  43.       $this->widgetSchema['picture_' . $i]->setLabel('画像');
  44.     }
  45.  
  46.     $this->widgetSchema->setNameFormat('post[%s]');
  47.   }
  48.  
  49.   public function updateObject($values = null)
  50.   {
  51.     $object = parent::updateObject($values);
  52.     $values = $this->getValues();
  53.  
  54.     foreach ($this->embeddedForms as $i => $picture) {
  55.       if (isset($values[$i]['filename_delete'])) {
  56.         $this->removePictures[] = $picture->getObject();
  57.       } else if ($picture->getObject()->getFilename() == '') {
  58.         unset($this->embeddedForms[$i]);
  59.       } else {
  60.         $picture->getObject()->setPost($object);
  61.       }
  62.     }
  63.   }
  64.  
  65.   public function doSave($con = null)
  66.   {
  67.     parent::doSave($con);
  68.     foreach ($this->removePictures as $p) {
  69.       $p->delete($con);
  70.     }
  71.   }
  72. }

symfony1.2を使いこなせるかどうかのポイントの一つはsfFormがちゃんと使えるかどうかになると思います。今後もsfFormを追いかけてみようと思います。
propel:generate-module post Postで生成されたindexSuccess.phpも少しだけ修正して、ちゃんと一覧画面に表示させるようにてみますか。

  1. <h1>Post List</h1>
  2.  
  3. <table>
  4.   <thead>
  5.     <tr>
  6.       <th>Id</th>
  7.       <th>Title</th>
  8.       <th>Description</th>
  9.       <th>Created at</th>
  10.       <th>Pictures</th>
  11.     </tr>
  12.   </thead>
  13.   <tbody>
  14.     <?php foreach ($post_list as $post): ?>
  15.     <tr>
  16.       <td><a href="<?php echo url_for('post/edit?id='.$post->getId()) ?>"><?php echo $post->getId() ?></a></td>
  17.       <td><?php echo $post->getTitle() ?></td>
  18.       <td><?php echo $post->getDescription() ?></td>
  19.       <td><?php echo $post->getCreatedAt() ?></td>
  20.       <td>
  21.       <?php foreach ($post->getPictures() as $picture): ?>
  22.       <img src="/uploads/<?php echo $picture->getFilename() ?>" alt="<?php echo $picture->getCaption() ?>" width="100" />
  23.       <?php endforeach; ?>
  24.       </td>
  25.     </tr>
  26.     <?php endforeach; ?>
  27.   </tbody>
  28. </table>
  29.  
  30.   <a href="<?php echo url_for('post/new') ?>">New</a>

本当は、編集フォームページにあるDeleteボタンを押すと、ちゃんとデータベースからはPostとそれに関連するPictureが削除されるのですが、実際の画像はunlinkされないので、そこの修正も必要になりますね。編集フォームページで画像だけを削除する際には、sfPropelFormにあるremoveFileが呼ばれますので、unlinkされるのですが、単純にdeleteアクションだけを呼んだ場合には、formクラスとは別のロジックになりますので、ファイルは消えませんので、Pictureモデルのdeleteにでも書いて置かないといけなさそうですね。ちょっとredundantなので、微妙だなぁーと思って今回は、それは載せませんでした。つーか、そもそも削除させないとかw

今回は、サンプルでしたので、書きませんでしたが、画像をconvertしたりする必要があるかもしれないですね。私が作っている本番用では、同時にサムネイルを作ったり、もっとformが複雑だったり、キャプションの与え方が動的だったりします。しかし、embedFormのやり方はこのままですので、これを元にすれば、いろんなところに適応ができそうです。

今回は、symfony1.2を用いて、複数の画像投稿を実現するために、embedFormを用いて実装する方法を説明しました。確かにその複数分ループで回せばいいのですが、embedFormForEachがちゃんと動けば、そちらの方がスマートな気がします。それができるまでは、このembedFormで実現しましょう。

間違いがありましたら、教えてくださいな。

2008/11/29

ハワイ島ヒロでのダイビング

最後にダイビングをしてもう3ヶ月になる。そろそろ禁断症状が出てくるところかな。特に最近は寒いので、海で泳ぎたくてしょうがない。毎日プールで泳いでいるけど、キレイな海で泳ぎたいなー。

そして、ようやく私が以前使っていたカメラのc-5050が13000円くらいで手に入りそう!Chika Watanabeさんは、その一つ前のモデルのc-5040を使っているらしいということをどこかで聞いたような。彼女の写真は相当いいんだよなー。つーか、被写体に恵まれているなー。いいなー。

というわけで、前回ダイビングに行ったヒロでのダイビングの成果でも挙げてみる。ダイブショップには、コナのナイトダイブを勧められたのだけど、なかなかそこまで行く足がなくて、今回は断念した。まぁ、前回の滞在のおかげでいいネットワークができたので、ハワイ島には何度か戻ることにはなりそうなので、その際にマンタと戯れにいくか。

ヒロでのダイビングと言いつつ、実は一日しか潜っていない。つーか、アメリカ値段が高いよ。機材もレンタルだったので、2ダイブで$150くらいかかってしまった。。。しかも、今ほど円高でないときに。さすがに$150だったら、狂ったように潜ることはできないので自重した。まぁ、2ダイブなのにボチボチな写真が撮れたのは収穫としておこう。

ヒロ側は亀さんがいっぱいるので、亀を見たいのであればいいかもしれない。たぶん20匹ほどの亀に会ったような気がする。亀の調査でハワイ大学のヒロ校のマリーンサイエンス学科が研究しているらしい。いくつかの亀には腫瘍があって、その調査をしているみたい。マスターまであるみたいだから、将来そこに入るのもアリかなー、なんて話もしていたり。。。

しかし、ビーチダイブだったので、沖に出るまでが大変だった。沖に出るまで淡水と海水の境のような場所を通っていくのだけど、視界がボヤけるし、波に打たれるし、淡水が冷たいので、結構参ってしまったよ。途中で足がつってしまった。運動不測だなぁ。

というわけで亀さんの写真をいくつか。


確か最初に見た亀さん。死んでいて魚さんが食べているのかと思って、gkbrしちゃったよ。海の中で写真を撮るときは、ホワイトバランスをだいたい砂の色で修正しているんだけど、端が赤みかかってしまった。難しいな。


うーん。画像劣化させすぎかな。。。急上昇中の亀さん。


これは、ちょっと鮮明に撮れた。言い訳をすると、そんなに透明度が高いわけではなかったのでキレイに写真が撮れないのですよ。


透明度が低かったので、鮮明に取れなかったので、結構補正させたらこんな感じ。劣化しなければいい構図だと思うのだが。


亀さんは、あまりに近すぎると怖いので、そこまで寄れないのだけども、こちらのスコーピオンフィッシュは、結構近くまで寄れる被写体だ。いつもダイビングする際には、こういうカモフラしている生物を探すのが好きで、このスコーピオンフィッシュは私が見つけて、他のダイバーに教えてあげた。ちょっと優越感。

こちらのエビさんはいつもいるやつなんだけども、まぁボチボチ寄って撮れたので載せてみた。

一日でこのくらいの成果だったら悪くないんでないかい?あー、でももっといいスポットで潜りたいなー。たぶんタイへの旅行は安くなりそうだからこの際にシミラン、スリンやビルマバンクを狙うのもいいかもしれない。やべっ。ダイビングのことを考えていたら、他のことが手が付かない。早くたくさん潜りたいなー。

2008/11/09

最近の開発環境

去年の今ごろはタイに行って、ダイビングしながら開発をしていたのですが、最近は京都が居心地が良すぎて、おとなしくしています。あー。でも、海に潜りてぇなぁ。

さて、最近は開発で追われていてブログをなかなか書くことができていなかったのですが、私の開発環境でも書いてみることにします。何人もの人がすごいデスクトップをアップしているので、それに比べればイマイチですが、自分では満足している環境です。

とりあえずわかりやすく家でのデスクトップの写真から。そういえば、カメラをハワイで失してしまったので、非常に困っています。オリンパスのc-5050という名機なのですが、ヤフオクでいい値段で落とせないかずっと見張っています。水中写真を撮るためのハウジングはあるけど、中身がないという非常につらい状態です。今回は携帯で写真を撮りました。あと写真には載っていないですが、椅子はこのフィットネスクラブがつくったバランスボール55cmを使っています。


これが家での私のデスクトップです。ちょっとケーブルがイケていないので、なんとかしたいですねぇ。

見ればわかりますが、ノートパソコン二台と外部ディスプレイ一台という構成になっていて三つの画面を使って開発をしています。そこではノートパソコンを両方ノートパソコンスタンドで立てて、キーボード、マウスは外付けにしてsynergyで共有をしています。ちょっとした小技ですね。
MacBook Proは最近モデルチェンジしたみたいですが、その前のモデルのMacBook Proを使用しています。しかし、実はこのMacBook Proは相当スペックがよかったりしています。2.6G Core2Duoで、メモリも4G積んでいたり。。。
もう一台は最近ヤフオクで買ったThinkPad X61です。本当はずっとネットブックが欲しくて、eeepcかInspiron mini 9を買おうと思っていたのですね。最近は英語キーボードじゃないと打ちにくくてしょうがないので、Inspiron mini 9をBTOで英語キーボードにして購入しようと思っていました。普段からHHKP2を使っているため、五段キーボードの方がしっくり来ていたので、Inspiron mini 9の変則キーボードも結構ありかなーなんて思っていましたが、近くのジョーシンで日本語キーボードを触ってみて、キータッチがイマイチだったので、購入を止めました。キータッチはあまり英語キーボードでも日本語キーボードでも変わらないと思います。触ってみた感じではキーボードだけならAspire Oneが一番良かったですね。というか、実際問題、HHKP2を使っているので、キーボードが打ちやすいかどうかなんてあまり関係なかったかもしれません。。。

こうやってキーボードを触ってみてげんなりしていた頃にThinkPad X61をヤフオクで発見したので購入しました。以前、私はThinkpadのT41を使用しており、Thinkpadは結構好きです。今回購入したX61は、最下位モデルの7675-3BJなのですが、それでも1.8G Core2Duoで、メモリも2G積んであるのですね。重さは1.5kg弱でかなり軽く感じます。さらに私の落札したX61は、メモリも増設して2Gになっており、英語キーボードに取り替えてあり、外付けDVD RWも付いてきて7,1000円だったのでですね。ネットブックよりも少し高い気もしましたが、明らかにネットブックよりもスペックがいいため、お得だなーと思ってついついボチッとしてしまいました。たぶん問題があるとすればバッテリーの持ちなんでしょうが、だいたいコンセントがあるところで開発する予定ですので、まぁそこは目を瞑っておきました。

さて、ここまでだったら結構ありがちな構成だと思いますが、私は家に電話線を引いていませんので、イー・モバイルD02HWでネットワークにつないでいます。イー・モバイルに契約していると無料でADSLができるみたいですが、NTTに基本使用料も払いたくありませんので、していません。

というわけで、インターネットはイー・モバイルでMacBook Proにつないで、そのMacBook Proの内蔵AirMacを使ってインターネット共有でルータとしても働いてもらい、ThinkPad X61からもインターネットを使います。いやぁ、世間ではWEPが見破られたとか言っていますが、私は時代遅れの状況でWEPでつないでいます。さらにこのインターネット共有でsynergyも共有してしまいます。


この環境のいいところは、場所を移動しても、二台のパソコンをネットワークもキーボードもマウスもsynergyで共有できることです。この写真は、私が外に持ち出して開発しているところの写真です。3ヶ月前に私もゴキブリ色のBOBLBE-E MEGALOPOLIS を買って、最近は毎日持ち歩いています。MacBook ProThinkPad X61HHKP2Logicool V-150サンワサプライ ノートPCスタンド CR-36二つと全てを持ち歩いているので相当重いのですが、これを背負うとあまり重さを感じなくてすごくいいですね。前は持ち運びのできるディスプレイがあったらいいなぁ、と思っていたのですが、ディスプレイじゃなくてもこれで同じような環境になります。こんな環境ですと見知らぬ人がジロジロと見てきます。まぁ、家じゃないからしょうがないですけどね。基本的に家と違うのはバックアップを取る外付けハードディスクがないことと外部ディスプレイがないことくらいです。あとトイレに行くとき相当大変。やっぱり盗まれたくないので、全部撤収してからトイレに行くことにしています。

あとsynergyでちょっとはまったことは、ポートの開け方がわからなかったことです。しょうがないので、NoobProofというソフトを入れてポートを開けてあげたのですが、今度はDNSやDHCPが閉じてしまって、かなりはまりました。synergyで共有はできるけど、今度はインターネット共有ができない。。。。ってことに。NoobProofをインストールして反映させるとそれまで開いていたいくつかのポートを閉じてしまうんですね。というわけで、NoobProofでポートを明示的に開けてあげました。

今回のポストはいつの間にか、アマゾンへのアフィリエイトブログになっています。まぁ、自分の持っている気に入ったものを勧めているので、こういう広告はありだと思っています。


Apple MacBook Pro 15"/2.5GHz Core 2 Duo/2G/250G/8xSuperDrive DL/Gigabit/BT/DVI MB134J/A
アップル (2008-02-28)
売り上げランキング: 19413
おすすめ度の平均: 5.0

5 期待を裏切りません

私はこのMacBook ProのCPUをさらにカスタマイズして2.6Gにしています。0.1Gにそんなにお金を出す必要性はあったかどうかはわかりません。また、Vmware Fusion 2.0でUbuntuとMicrosoft Windows XP Professionalを入れています。基本的に開発はVMWare Fusion上のUbuntuでしかしません。Microsoft Windows XP Professionalはクライアントのテスト用として入れているのですが、正直なところほとんど使っていなくてもったいないなぁ、と思っています。基本的にMacBook ProではskypeとsafariとiTunesとVMWare Fusionしか走らせません。あとはSynergyでキーボードやマウスのサーバになってもらったりイー・モバイルにつないでルータになってもらうくらいです。正直なところ、私にはマックのインタフェースはちょっと使いにくい。。。あと15インチの方はUSBの口が2個しかないんですね。一つはD02HWにつないでいますし、もう一つはHHKP2につないでいますので、足りないです。。。バス型のUSBハブですと電力が足りないとか言われますし、少々困っています。同時にiPodがつなげないんです。


レノボ・ジャパン ThinkPad X61 (T7300/1024/120/XP/12TFT)T 76754BJ
Lenovo (2007-06-05)
売り上げランキング: 70249

私の持っているモデルは7675-3BJでこの一つ古いものなのですが、やっぱりThinkpadでしょう。安いものはヤフオクで6万円くらいで落札できたりします。私は英語キーボードでかつメモリも増設してあり、外付けDVD RWが付いて7万円くらいで中古を購入しました。ネットブックよりも激しくスペックがいいのが素晴らしいですね。ただ注意したいのは、無線がないモデルもあるということ。無線がないとsynergyで共有もできないし、インターネット共有もできません。私はこのThinkpad X61をデュアルブートにして、ほとんどUbuntuを使っています。Compizを入れれば3Dデスクトップもできてかなりカックイイ!


Vmware Fusion 2.0
Vmware Fusion 2.0
posted with amazlet at 08.11.09
アクト・ツー (2008-10-23)
売り上げランキング: 347

VMWare Fusionはいいのですが、結局仮想マシンなので、Compizなどの視覚効果を出すことができないのが辛いですね。まぁ、おかげでUbuntuが軽かったりしますが。VMWare Fusionを2.0にバージョンアップした際に、プロセッサを2個にしていたら固まる固まる。。。なので、しょうがなく1個にしています。あとExposeにSpacesなどのコントロールを割り当てているのですが、これをVMWare Fusionからも有効にするとAlt+TabもMacの方のコントロールが有効になってしまうのはちょっと嫌です。確か1系だったときにはExposeも使えるしAlt+Tabは、Ubuntu上で切り替わることができたので、少し嫌なバージョンアップでした。


Happy Hacking Keyboard Professional2 墨 PD-KB400B
PFU (2006-03-22)
売り上げランキング: 2354
おすすめ度の平均: 4.5

4 Win△ Mac◎
2 windowsユーザーにとっては使いづらい
5 Proのツールです
4 使用者を選ぶがハマれば完璧。
5 もうやめられない

ずっと前から知ってはいたのですが、ついつい大人買いをしてしまったキーボードです。Thinkpadのキータッチであれば、結構気に入っているのですが、MacBook Proのキータッチはどうも好きになれません。なので、このHHKP2を持ち歩いています。最初は、打ちにくいなぁなんて思っていましたが、慣れてしまえばヤバいです。もうこれ無しではプログラミングもできないんじゃないか、と思うくらい気に入ってしまいました。五段キーボードも気に入っています。ファンクションキーなんてそれほど打つわけでもないですしね。あと、持ち運びができるサイズだというのがうれしいですね。HHKP2は小さいので、脇を締めてタイピングをするようになりました。なんとなくこちらの方が疲れないです。一つだけ注文を付けるならワイアレスだとさらにいいのですが。


Logicool V-150BK レーザーマウス フォア ノートブック ブラック
ロジクール (2006-06-30)
売り上げランキング: 6188
おすすめ度の平均: 3.5

2 ケーブル短いです
5 おすすめのマウスですね

マウスは、これを使用しています。レーザーで小さいのがいいですね。相変わらずUSBでつないでいますが、無線でもいいかも。でも、MacBook Proの15ってUSBの口が2個しかないんですよねぇ。ハブを付けてやるとときどき電力が足りないとかいいますし。。。私はこのマウスをHHKP2のUSBの口で使っていますが、電力は大丈夫です。HHKP2には二つUSBの口があるのですが、そのうち一つをこれに当てて、ときどきもう一つをUSBメモリに使っています。USBメモリによっては、電力が足りないと言われてしまいます。


サンワサプライ ノートPCスタンド CR-36
サンワサプライ
売り上げランキング: 10688
おすすめ度の平均: 5.0

5 効果は絶大

ノートパソコンスタンドは、これを二つ持っています。先日、友人の祝い事があったので、これをプレゼントしたのですが、あまり喜ばれなくてちょっとショックでした。私は結構気に入っていたのに。。。ということは合計で三つも購入していたりするんですね。気に入っているポイントとしては、折り畳んで持ち運びができることですね。しかし、さすがに二つも持ち歩くと結構重いです。また、ケースもマウスパッドになってさらに便利です。ノートパソコンスタンドは、akkyが使っているのを見て激しく欲しくなったので、購入してみたのですが、ノートパソコンでHHKを使う人にとってはかなり便利ですね。


BOBLBE-E MEGALOPOLIS STAR WARS LIMITED EDITION  BLACK (DARTH VADER MODEL)
BOBLBE-E (2007-09-20)
売り上げランキング: 67259

私が持っているのは、ここまで黒光はしませんが、かなり似ています。ヤフオクで15000円くらいで落札しました。手で持ち歩くとかなり重いのですが、背負うと、あら不思議。あまり重さを感じません。この中にMacBook Pro、X61、HHKP2、ノートパソコンスタンド二つ、マウスやアダプタ等を入れています。ちょっと入れすぎかな。。。


ELECOM 低反発発泡ポリウレタンインナーバッグ(参考収容寸法275*40*215mm) ブラック ZSB-IB017BK
エレコム (2006-08-25)
売り上げランキング: 906
おすすめ度の平均: 4.0

4 Think Pad X61で使っています
4 VAIO Type Tにぴったり

MacBook Proは、そのままMEGALOPOLICEに入れていますが、X61はケースに入れています。こっちの方が大事というわけではないのですが、MEGALOPOLICEにはノートパソコンを固定している場所が一つしかないんですよね。まぁ、普通は複数持ち歩かないしw。そちらにMacBook Proを入れて、X61は固定していないところに入れています。なんか心もとないので、ケースが欲しいと思っていました。そしてちょうどピッタシのケースがこれです。たぶん8セルにしたら入らないと思いますが、デフォルトの4セルであれば、ピッタシに入ります。


バッファローコクヨサプライ Arvel USBグースケーブル A-ミニB 15CM ブラック AUGM15BK
バッファローコクヨサプライ (2006-07-28)
売り上げランキング: 3094
おすすめ度の平均: 5.0

5 グースケーブル最高

また、これもakkyが似たようなものを使っていて、私も欲しくなりました。D02HW用のUSBケーブルです。最初から付いてくるUSBケーブルはなんかヘナってなってしまってあまり好きじゃなかったのですが、このUSBケーブルを立てて使っているとD02HWが浮いているようでなかなかどうしていい感じです。


iiyama 20インチワイド液晶ディスプレイ  アスペクト比固定拡大機能搭載 ブラック PLE2002WS-B1
マウスコンピューター (2008-09-03)
売り上げランキング: 16060

家では、外部モニタを使っているのですが、最近外部モニタって安すぎますね。私のは20インチで、たぶんこれの一つ古いやつだと思うのですが、22000円って一昔前では考えられないですね。私が買ったときも25000円くらいで、安すぎだぜ!って喜んでいましたが、今amazonを見てみると26インチが。。。iiyama 26インチワイド液晶ディスプレイ アスペクト比固定拡大機能搭載 ブラック PLE2607WS-B1
41000円かよ。激しく買いたくなってきた。でも、今のがあるから買わないけど。たぶん。


I-O DATA機器 USB 2.0/1.1対応 外付型ハードディスク 500GB HDCN-U500
アイ・オー・データ (2007-12-20)
売り上げランキング: 2
おすすめ度の平均: 4.0

1 2ヶ月でご臨終
5 無難。安い。手軽。とりあえず一台欲しくなる物です♪
5 小さい!コンパクト!
4 安かったので買ってみました。
4 価格相応と割り切った商品

Time Machine用のバックアップハードディスクとして使っています。基本的に私はディスクの容量は少なくていい人なので、500GBもいらねー、と思っていたのですが、まぁ安かったので、これにしました。MacBook Proもディスクがだいぶ余っているし、X61もそう。また、iPod classicも80GBもあるのに実際は5GBくらいしか入っていなくて寂しい限りです。。。うーん。どうやったらディスクの容量をたくさん使うようになれるんだろう。


フィットネスクラブがつくったバランスボール55cm(パールホワイト)
(株)東急スポーツオアシス
売り上げランキング: 5148

椅子はこれです。前はYahoo!ショッピングからしか買えなかったのですが、いつの間にかアマゾンにもありました。安いものがいくつかある中5000円くらいというのはちょっと躊躇しますが、かなり気に入っています。特に下の固定する輪っかのおかげで椅子としてすごく安定していると思います。サイズは悩みました。購入する前にバランスボールで快適パソコンライフを読んでいたので、55cmって小さいかなー、なんて思っていましたが、私はこの55cmでピッタしでした。私は実は安物の65cmも持っているのですが、ちょっと私には大きいかな、と思いました。たぶん机の高さや足の長さに関係するんでしょうね。ちなみに私の身長は171cmなので、まぁ一般的な男性だと思います。アーロンチェアーとかもいいなーなんて思っていましたが、私はこちらの方が好きです。安いし。


こうやって書いてみるとMacBook Proはかなり高いけど、後はそれほどお金をかけなくてもボチボチな環境ができます。まぁ、HHKP2も高いか。普段はお酒を飲むことも少ないですし、このくらいの贅沢はしてもいいかなーなんて思っています。
軽く計算すると
300000 (MacBook Pro)
71000 (Thinkpad X61)
25000 (外部ディスプレイ)
20000 (HHKP2)
15000 (ボブルビー)
10000 (外部ハードディスク)
10000 (VMWare Fusion)
6000 (ノートパソコンスタンドx2)
5000 (バランスボール)
2500 (マウス)
1000 (USBケーブル)
1000 (X61用パソコンケース)
計451500円也
しかし、MacBook Pro以外だと約150000円。思ったより安いね。

次バックパックの旅行に行くときには、こうやって持ち歩くのはしんどいので、たぶんX61だけかMacBook Proだけか、引退させたけど、壊れても盗まれても痛くないT41を持っていくか悩むなぁ。T41も眠らせておくにはもったいないマシンだし。でもいいスペックのマシンを使うとなかなか昔のものに戻れないですね。

Bloglines feedburner