プロジェクト

全般

プロフィール

Bug(バグ) #1594

Yuya Watanabe さんがほぼ13年前に更新

h3. Overview (現象) 

 デイリーニュースに1つ以上ガジェットが登録されている場合、 
 デイリーニュース配信タスクを実行するとエラーが表示され、デイリーニュースが1通も送信されない。 

 h5. 実行するタスク 

 <pre> 
 $./symfony openpne:send-daily-news 
 </pre> 

 h5. 表示されるエラー 

 <pre> 
 Call to undefined method myUser::getMember. 
 </pre> 

 h3. 再現バージョン 

 h5. OpenPNE 

 * OpenPNE 3.7.0-dev 
 * OpenPNE 3.6beta12 

 h5. php 

 * PHP 5.2.13 
 * PHP 5.3.3 
 * PHP 5.3.5 

 h3. Causes (原因) 


 h3. Way to fix (修正内容) 


 h3. 報告元 

 http://sns.openpne.jp/diary/25133 より転載 

 <pre> 
 デイリーニュースが送れない、です。 
 PHP5.3.3の所為??それとも、3.7.0-devだから?? 

 【環境】 
 Powered by OpenPNE 3.7.0-dev 
 # ./symfony plugin:list 
 Installed plugins: 
 symfony 1.4.6-stable 
 openpne 3.7.0dev-beta 
 opAuthMailAddressPlugin 1.3.1-devel 
 opAuthMobileUIDPlugin 1.3.0-devel 
 opAuthOpenIDPlugin 1.3.0-beta 
 opCommunityTopicPlugin 1.0.0.2-stable 
 opWebAPIPlugin 0.4.0-beta 
 opDiaryPlugin 1.3.1-beta 
 opBlogPlugin 1.0.1-stable 
 opOpenSocialPlugin 1.2.0.1-stable 
 opAshiatoPlugin 0.9.1-stable 
 opMessagePlugin 0.9.1-beta 
 opAlbumPlugin 0.9.4-beta 
 opIntroFriendPlugin 0.9.0.1-beta 
 opFavoritePlugin 1.0.0.3-beta 
 opRankingPlugin 1.0.0-beta 


 FreeBSD 7.2-RELEASE-p8 
 Apache/2.2.16 (FreeBSD) 
 PHP 5.3.3 with Suhosin-Patch Zend Engine v2.3.0 
 mysql 5.1.36 

 【現象】 
 コマンドラインで 
 #./symfony openpne:send-daily-news 
 を実行すると 

 Call to undefined method myUser::getMember. 

 というエラーがでて、デイリーニュースが送れない。 
 </pre> 

 h3. 原因 

 下記コマンドを実行した時にデイリーニュース用のガジェットが表示可能かのロジックが正しくない. 
 <pre> 
   $ ./symfony openpne:send-daily-news 
 </pre> 

 lib/task/openpneSendDailyNewsTask.class.php の下記部分が実行され,61 行目が実行される. 
 <pre> 
  27     protected function execute($arguments = array(), $options = array()) 
  28     { 
 ... 
  56         $filteredGadgets = array(); 
  57         if ($gadgets) 
  58         { 
  59           foreach ($gadgets as $gadget) 
  60           { 
  61             if ($gadget->isEnabled()) 
  62             { 
  63               $filteredGadgets[] = array( 
  64                 'component' => array('module' => $gadget->getComponentModule(), 'action' => $gadget->getComponentAction()), 
  65                 'gadget' => $gadget, 
  66                 'member' => $member, 
  67               ); 
  68             }  
  69           }  
  70         }  
 </pre> 

 ここで isEnabled() を見てみると,sfContext で得られる getUser() で使えるかどうかを決定している.しかし,タスクで実行しているためここで得られる User は実際にメールを送信したい Member を含む User ではなくタスクを実行した時の User (ここでは apps/api/lib/myUser.class.php )である.エラー自体はここで User から getMember() を呼び出すことができないという問題であるが,エラーが発生していなくても正しく動作しないものと思われる. 

 lib/model/doctrine/Gadget.class.php  
 <pre> 
  66     public function isEnabled() 
  67     { 
  68       $list = $this->getGadgetConfigList(); 
  69       if (empty($list[$this->name])) 
  70       { 
  71         return false; 
  72       } 
  73  
  74       $controller = sfContext::getInstance()->getController(); 
  75       if (!$controller->componentExists($this->getComponentModule(), $this->getComponentAction())) 
  76       { 
  77         return false; 
  78       } 
  79  
  80       $member = sfContext::getInstance()->getUser()->getMember(); 
  81       $isEnabled = $this->isAllowed($member, 'view'); 
  82  
  83       return $isEnabled; 
  84     } 
 </pre> 

 h3. 修正案 

 表示可能かどうかを見たいメンバを Gadget の isEnable() メソッドの引数に与えるようにする. 
 <pre> 
 diff --git a/lib/model/doctrine/Gadget.class.php b/lib/model/doctrine/Gadget.class.php 
 index 4cb5053..80ab1cd 100644 
 --- a/lib/model/doctrine/Gadget.class.php 
 +++ b/lib/model/doctrine/Gadget.class.php 
 @@ -63,7 +63,7 @@ class Gadget extends BaseGadget implements opAccessControlRecordInterface 
      return $list[$this->name]['component'][1]; 
    } 
 
 -    public function isEnabled() 
 +    public function isEnabled($member = null) 
    { 
      $list = $this->getGadgetConfigList(); 
      if (empty($list[$this->name])) 
 @@ -77,7 +77,10 @@ class Gadget extends BaseGadget implements opAccessControlRecordInterface 
        return false; 
      } 
 
 -      $member = sfContext::getInstance()->getUser()->getMember(); 
 +      if (is_null($member)) 
 +      { 
 +        $member = sfContext::getInstance()->getUser()->getMember(); 
 +      } 
      $isEnabled = $this->isAllowed($member, 'view'); 
 
      return $isEnabled; 
 diff --git a/lib/task/openpneSendDailyNewsTask.class.php b/lib/task/openpneSendDailyNewsTask.class.php 
 index f70f082..3a7d9a9 100644 
 --- a/lib/task/openpneSendDailyNewsTask.class.php 
 +++ b/lib/task/openpneSendDailyNewsTask.class.php 
 @@ -58,7 +58,7 @@ EOF; 
        { 
          foreach ($gadgets as $gadget) 
          { 
 -            if ($gadget->isEnabled()) 
 +            if ($gadget->isEnabled($member)) 
            { 
              $filteredGadgets[] = array( 
                'component' => array('module' => $gadget->getComponentModule(), 'action' => $gadget->getComponentAction()), 
 </pre> 

 h2. 問題2 

 # 携帯メールアドレスのみを持つメンバを追加する 
 # 携帯デイリーニュースにガジェットを追加する 
 # 「symfony openpne:send-daily-news」を実行する 
 ** 下記エラーが発生する 
 <pre> 
 PHP Fatal error:    Cannot redeclare class defaultComponents in /home/hoge/sns/36.example.com/apps/mobile_frontend/modules/default/actions/components.class.php on line 50 
 </pre> 


 h3. 原因 

 pc_frontend $gadget->isEnabled() の中の sfContext::getInstance()->getController()->componentExists() でコンポーネントの存在確認される際にロードされるコンポーネントのアプリケーションが実際に送信する際のコンテキストとは別の場合があることが原因であると思われる.そのため,最初に pc_frontend で sfContext::createInstance() が呼び出されているので携帯メールアドレス向けにデイリー・ニュースを送信しようとするとエラーが発生する.直前の sfContext::createInstance()mobile_frontend の defaultComponent が両方共ロードされることで redeclare としてエラーが発生する状態でした. 


 h3. 修正方針 componentExists() のインスタンスが一致する場合はエラーが発生しないため,PCメールアドレスに送信するときにエラーが発生しない. 

 同時に同じクラスがロードされることが原因だったため,実行する php のプロセスをそれぞれ別にすることでこの問題を回避する方針を取ります. 

 また,この修正のために php のバイナリを探しだす方法として下記 URL 先のものを参考にしました. 

 http://www.serverphorums.com/read.php?7,415337 具体的には,member_id=1 のメンバが $member->getEmailAddress() でPCメールアドレスを取得でき,member_id=2 のメンバが携帯メールアドレスを取得できるとするとき,下記のような感じで直前に生成されたコンテキストと送信時のコンテキストが一段階ずつずれている. 
 https://github.com/sebastianbergmann/phpunit/issues/432 # 最初にpc_frontendがコンテキストで設定される (openpneSendDailyNewsTask.class.php 31行目) 
 https://github.com/symfony/Process/blob/379b35a41a2749cf7361dda0f03e04410daaca4c/PhpExecutableFinder.php  


 h3. 修正案2 

 コンテキストの変更を 送信時ではなく $gadget->isEnabled() よりも前に行うことで原因で発生するような齟齬が発生しなくなる. ** componentExists時: 未    送信時: 未 
 <pre> # componentExists() が呼び出される (openpneSendDailyNewsTask.class.php 61行目) 
 diff --git a/lib/task/openpneSendDailyNewsTask.class.php b/lib/task/openpneSendDailyNewsTask.class.php ** componentExists時: pc_frontend    送信時: 未 
 index e1c511f..303e9d6 100644 # opBaseMailTask::getContextByEmailAddress() 内で sfContext::createInstance() が呼び出される (openpneSendDailyNewsTask.class.php 72行目) 
 --- a/lib/task/openpneSendDailyNewsTask.class.php ** componentExists時: pc_frontend    送信時: pc_frontend 
 +++ b/lib/task/openpneSendDailyNewsTask.class.php # opMailSend::sendTemplateMail() が呼び出される (openpneSendDailyNewsTask.class.php 80行目) 
 @@ -22,20 +22,58 @@ Call it with: 
 
    [php symfony openpne:send-birthday-mail|INFO] 
  EOF; ** componentExists時: pc_frontend    送信時: pc_frontend -> メールが送信される 
 + # opBaseMailTask::getContextByEmailAddress() 内で sfContext::createInstance() が呼び出される (openpneSendDailyNewsTask.class.php 72行目) 
 +      $this->addOptions( ** componentExists時: pc_frontend    送信時: mobile_frontend 
 +        array( # opMailSend::sendTemplateMail() が呼び出される (openpneSendDailyNewsTask.class.php 80行目) 
 +          new sfCommandOption('app', null, sfCommandOption::PARAMETER_OPTIONAL, 'send to pc or mobile', null), ** componentExists時: pc_frontend    送信時: mobile_frontend -> エラーが発生する 

 lib/task/openpneSendDailyNewsTask.class.php 
 +        ) 
 +      ); 
    } 
 
    protected function execute($arguments = array(), $options = array()) 
    { 
      parent::execute($arguments, $options); 
 
 -      <pre> 
  30  
  31       sfContext::createInstance($this->createConfiguration('pc_frontend', 'prod'), 'pc_frontend'); 
  32 
 +      $expectedOptions = array('pc_frontend', 'mobile_frontend'); 
 + 
 +      if (isset($options['app'])) 
 +      ... 
  60           { 
 +        
  61             if (in_array($options['app'], $expectedOptions)) 
 +        ($gadget->isEnabled($member)) 
  62             { 
 +          $this->sendDailyNews($options['app']); 
 +        } 
 +        else 
 +        { 
 +          throw new Exception('invalid option'); 
 +        } 
 +      } 
 +      else{ 
 +        $php ... 
  71        
  72         $context = $this->findPhpBinary(); 
 +        foreach ($expectedOptions as $app) 
 +        { 
 +          exec($php.' '.sfConfig::get('sf_root_dir').'/symfony openpne:send-daily-news --app='.$app); 
 +        } 
 +      } 
 + $this->getContextByEmailAddress($address); 
  73         $params = array( 
  74           'member'    } => $member, 
  75           'gadgets' => $filteredGadgets, 
  76           'subject' => $context->getI18N()->__('デイリーニュース'), 
  77           'today'     => time(), 
  78         ); 
  79 
  80         opMailSend::sendTemplateMail('dailyNews', $address, opConfig::get('admin_mail_address'), $params, $context); 
 + </pre> 

 lib/task/opBaseSendMailTask.class.php 
 +    private <pre> 
  44     protected function sendDailyNews($app) 
 +    getContextByEmailAddress($address) 
  45     { 
 +      $isAppMobile 
  46       $application = 'mobile_frontend' === $app; 
 +      $dailyNewsName 'pc_frontend'; 
  47       if (opToolkit::isMobileEmailAddress($address)) 
  48       { 
  49         $application = $isAppMobile ?    'mobileDailyNews' : 'dailyNews'; 
 +      'mobile_frontend'; 
  50       } 
  51  
  52       if (!sfContext::hasInstance($application)) 
  53       { 
  54         $context = sfContext::createInstance($this->createConfiguration($app, sfContext::createInstance($this->createConfiguration($application, 'prod'), $app); 
 
 -      $pcGadgets $application); 
  55       } 
  56       else 
  57       { 
  58         $context = Doctrine::getTable('Gadget')->retrieveGadgetsByTypesName('dailyNews'); sfContext::getInstance($application); 
  59       } 
  60  
  61       return $context; 
  62     } 
 -      $mobileGadgets = Doctrine::getTable('Gadget')->retrieveGadgetsByTypesName('mobileDailyNews'); </pre> 


 h3. 修正案2 

 コンテキストの変更を 送信時ではなく $gadget->isEnabled() よりも前に行うことで原因で発生するような齟齬が発生しなくなる. 
 +      $gadgets = Doctrine::getTable('Gadget')->retrieveGadgetsByTypesName($dailyNewsName); <pre> 
 +      $gadgets = $gadgets[$dailyNewsName.'Contents']; 
 
      $targetMembers = Doctrine::getTable('Member')->findAll(); 
      foreach ($targetMembers as $member) 
      { diff --git a/lib/task/openpneSendDailyNewsTask.class.php b/lib/task/openpneSendDailyNewsTask.class.php 
 +        $address = $member->getEmailAddress(); index 3a7d9a9..e1c511f 100644 
 +        if ($isAppMobile !== opToolkit::isMobileEmailAddress($address)) --- a/lib/task/openpneSendDailyNewsTask.class.php 
 +        { +++ b/lib/task/openpneSendDailyNewsTask.class.php 
 +          continue; 
 +        } 
 + 
        $dailyNewsConfig = $member->getConfig('daily_news'); 
        if (null !== $dailyNewsConfig && 0 === (int)$dailyNewsConfig) 
        { 
 @@ -46,13 +84,6 -48,6 +48,7 @@ EOF; 
        { 
          continue; } 
        } 
 -        $address = $member->getEmailAddress(); 
 -        
        $gadgets = $pcGadgets['dailyNewsContents']; 
 - +        $context = $this->getContextByEmailAddress($address); 
 -        
        if (opToolkit::isMobileEmailAddress($address)) 
 -        
        { 
 -          
          $gadgets = $mobileGadgets['mobileDailyNewsContents']; 
 -        } 
 
        $filteredGadgets = array(); 
        if ($gadgets) 
 @@ -91,4 +122,36 -69,7 +70,6 @@ EOF; 
 
      return in_array($day, opConfig::get('daily_news_day')); 
    
          } 
 + 
 +    private function findPhpBinary() 
 +    { 
 +      if (defined('PHP_BINARY') && PHP_BINARY) 
 +      { 
 +        return PHP_BINARY; 
 +      
        } 
 + 
 +      if (false !== strpos(basename($php = $_SERVER['_']), 'php')) 
 +      { 
 + 
 
 -        return $php; 
 +      } 
 + 
 +      // from https://github.com/symfony/Process/blob/379b35a41a2749cf7361dda0f03e04410daaca4c/PhpExecutableFinder.php 
 +      $suffixes $context = DIRECTORY_SEPARATOR == '\\' ? (getenv('PATHEXT') ? explode(PATH_SEPARATOR, getenv('PATHEXT')) : array('.exe', '.bat', '.cmd', '.com')) : array(''); 
 +      foreach ($suffixes as $suffix) 
 +      { 
 +        if (is_executable($php $this->getContextByEmailAddress($address); 
        $params = PHP_BINDIR.DIRECTORY_SEPARATOR.'php'.$suffix)) 
 +        { 
 +          return $php; 
 +        } 
 +      } 
 + 
 +      if ($php = getenv('PHP_PEAR_PHP_BIN')) { 
 +        if (is_executable($php)) { 
 +          return $php; 
 +        } 
 +      } 
 + 
 +      return sfToolkit::getPhpCli(); 
 + array( 
          'member'    } 
 + 
  } => $member, 
          'gadgets' => $filteredGadgets, 
 </pre> 

戻る