PrestaShop。 階層化ナビゲーションのグリッチについて

PrestaShop Blocklayered



こんにちはHabr! 伝えたいことはごく普通のことだと理解しています。 各オープンソースプログラマーは、1日に最大10個のこのようなケースを抱えています。 しかし、とにかくそれについて書くことにしました。 それは本当に誰かを助けますが、誰かがちょうど彼らの気分を改善するかもしれません、それも良いです。



ちょっとしたリバースエンジニアリング、少し哲学的な考察、そしてもちろんハッピーエンドがあります。 バグの修正にのみ関心がある人-このナンセンスをすべて読むことはできず、記事の最後からハックをすぐにコピーすることはできません。 いずれにせよ、猫へようこそ。



PrestaShopについて



すべては、当局がオンラインストアを作成するタスクを設定したという事実から始まりました。 次の理由により、PrestaShop 1.6が選択されました。





しばらくして、この記事の主題となった疑問が生じました。



グリッチとは何ですか?



商品が既にロードされており、フィルターのセットアップを開始すると、特定の場合にモジュールが正しく動作しないことが判明しました。



各値の反対側にある製品の数がフィルターブロックに表示され(図を参照)、このボックスをオンにするとフィルターされます。 ポイントは、異なるフィルターで異なる値を選択すると、すべての数量が巧妙に再計算され、便利なフィードバックが提供され、商品のさらなる選択を決定するのに役立つということです。



ここで不適切な行動が忍び込んだ。 1つのプロパティをチェックすると、すべてが機能し、新しい数量が表示されます。 2つ以上のプロパティをチェックした場合、残りのフィルターの位置の反対側の数値は、最初のフィルターにリセットされます(位置が選択されていないかのように)。



ここで、PrestaShopには属性の概念と製品プロパティの概念があることに注意してください。 属性は、同じ製品の異なるバージョンの形成に関与する製品の属性です(たとえば、ある特定の靴モデルの靴のサイズ)。 機能は、すべての製品オプションに共通の機能です。 彼らは製品オプションの形成には参加せず、消費者の特性についてユーザーに通知するだけです。



プロパティによってフィルターブロックの位置をマークすると、エラーが表示されます。 他のフィルター(製造元など)では、この効果は発生しません。



主な仮定



次のことが明らかになりました。





インターネットでの検索では何も得られなかったため、選択肢がありました。





私は2番目の決定をしました。 ファイルblocklayered.phpを開いたとき、熱意は弱まりました。 これには3,500行を超えるコードが含まれており、そのうち70%が複数層のSQLクエリでした。 タスクは干し草の山の針のように見え始めました。 最初は怖かったし、PrestaShopのクリエーターについてもひどく考えていた。 しかし、そのようなモジュールの動作の複雑なロジックをどのようにプログラムし始めて、少し落ち着かせるのかを考えました。 タスクは本当に難しく、おそらくコードの複雑さは客観的な理由によって引き起こされます。 しかし、モジュールを使用する場合、すべて同じように、このすべてをより美しくすることが可能であるという考えはありませんでした。



ツールとコツ



問題を解決するには、次のツールを使用します。



WinSCPは、多くの機能を備えた信頼できるFTPクライアントです。 多数のファイルやボリュームでも失敗することはありません。 コマンドラインからを含むすべての機能が利用可能で、スクリプトを作成するときに便利です。



UwAmpは、インストールと構成が簡単なWAMPビルドです。 調査したコードをローカルで実行するために使用します。



Notepad ++は、リバースエンジニアリングに最適なエディターです。 異なるエンコーディングで、異なる行末で動作します。 優れた構文強調表示。 大きなファイルを開く。 ディレクトリ内のファイルを含む文字列を検索します。 非常に確実に機能します。



HeidiSQL -MySQL用のGUI。 無料のグラフィックデータベースツール。 時々バグがありますが、一般的には作業が非常に便利です。 コード分​​析でデータベースの内容を調べるために使用します。



主な手法は、変数をダンプし、関数名とコードの一部をソースコードで検索することです。 Ajaxリクエストを含む興味のあるイベントが発生するため、変数をファイルにダンプします。 これを行うには、必要に応じて次のコードを挿入します。



$f=fopen('headfire.txt','a+'); fwrite($f,$very_important_variable); fwrite($f, PHP_EOL); fclose($f);
      
      





ファイル名のheadfireは私のエイリアスです。コードまたはファイルをマークする必要がある場合に使用します。 コード行を使用する必要があります。 見つけやすく、他のファイルやコード行と混同しにくいことが重要です。



分析の開始



コードの約半分がBackOfficeを担当します。 このコードはすぐに削除され、そこを見ないようにしてください。



問題の宣伝は、ページにフィルターを表示するtplファイルを検索することから始めます。 長い間検索する必要はありませんでした。 tplファイルはモジュールのルートにあり、 blocklayered.tplと呼ばれます 。 調べてみると、その中にはバグのある量の出力のラインがあると確信しています。



 <a href="{$value.link}" data-rel="{$value.rel}">{$value.name|escape:html:'UTF-8'}{if $layered_show_qties}<span> ({$value.nbr})</span>{/if}</a>
      
      





目の隅から、量が$ layered_show_qtiesの条件に従って表示され、量自体に略語nbrが付いていることがわかります。 たぶん便利になるかもしれませんし、そうでないかもしれません。



次のステップは、 blocklayered.tplテンプレートが呼び出される場所を見つけることです。 機能であることが判明



 public function generateFiltersBlock($selected_filters);
      
      





確認するために、2回呼び出されることがわかります。1つは左の列フックから、もう1つはajaxリクエストからです。 本当のようです。 関数自体は小さいですが、テンプレートのデータを準備する関数呼び出しがあります



 public function getFilterBlock($selected_filters = array())
      
      





この関数は、800行を超えます。 SQLクエリの束があります。 ほとんどの場合、フィルター形成のロジック全体がここに集中しています。 モジュールでは5回呼び出されることは注目に値します。 クエリを5回連続で計算するにはコストがかかりすぎるようです。 しかし、あなたは変数に気づく



 static $cache = null;
      
      





そして、あなたはこれが静的変数のキャッシングに関する古き良きトリックであることを理解しています。 また、このコードは悪名高いPHP開発者によって書かれたものであり、この開発者は何も止めないことを理解しています。



および、または聖水



関数がどのように機能するかを何らかの方法で学ぶ必要があります。 グリッチは、フィルターの2番目のチェックマークが点灯した瞬間に発生します。 そして、これにはAjaxリクエストが伴います。 したがって、ファイルへの変数ダンプを使用します。



濃いコーヒーを使用し、ガスストーブの周りでタンバリンと踊り、各フィルターブロックのメインクエリが送信される場所を見つけ、ファイルにこのクエリを表示するデバッグコードを挿入します( $ sql_query変数):



 // headfire debug begin $f=fopen('headfire.txt','a+'); fwrite($f,print_r($sql_query,true)); fwrite($f, PHP_EOL); fclose($f); // headfire debug end $products = false; if (!empty($sql_query['from'])) { $products = Db::getInstance()->executeS($sql_query['select']."\n".$sql_query['from']."\n".$sql_query['join']."\n".$sql_query['where']."\n".$sql_query['group']); }
      
      





注- $ sql_queryは配列です。 これはコードから見ることができるので、 trueフラグを指定してprint_rを使用してダンプを出力します



ファイルへの最初の出力はすぐに問題について叫ぶ:



 Array ( [select] => SELECT p.`id_product`, sa.`quantity`, sa.`out_of_stock` [from] => FROM ps_cat_restriction p [join] => LEFT JOIN `ps_stock_available` sa ON (sa.id_product = p.id_product AND sa.id_product_attribute=0 AND sa.id_shop = 1 AND sa.id_shop_group = 0 ) LEFT JOIN `ps_manufacturer` m ON (m.id_manufacturer = p.id_manufacturer) INNER JOIN `ps_layered_price_index` psi ON (psi.id_product = p.id_product AND psi.id_currency = 1 AND psi.price_min <= 3631136 AND psi.price_max >= 4618 AND psi.id_shop=1) [where] => WHERE 1 AND EXISTS (SELECT * FROM ps_feature_product fp WHERE fp.id_product = p.id_product AND fp.`id_feature_value` = 26634 OR fp.`id_feature_value` = 22096) [group] => [second_query] => )
      
      





[where]条件に注意してください。ANDとORが1行で表示され、ORは同種の条件の間にあり、括弧で強調表示されていません。



 fp.id_product = p.id_product AND fp.`id_feature_value` = 26634 OR fp.`id_feature_value` = 22096
      
      





ANDとORが何らかの条件で組み合わされ、ブラケットが配置されていないことに気付いた賢明なプログラマーは、聖水の後すぐに走り、この感染が広がらないようにモニター、ハードドライブ、およびキーボードを振りかける必要があると確信しています。



真剣に、状況を一見しただけで、問題の性質に基づいても、エラーがここにあることが明らかになります-または括弧で囲むのを忘れました。 この場所を見つけて修正するだけです。 しかし、ここでも少し驚きがあります。



最後の驚き:動的ディスパッチ



誤った条件が形成される場所を見つけようとしています。 このためにトレース出力からの抜粋を使用します。 「fp.`id_feature_value`を検索すると、関数が表示されます。



 private static function getId_featureFilterSubQuery($filter_value, $ignore_join = false)
      
      





これが必要なものです。 括弧なしで条件を形成するコードが表示され、正しいコードに置き換えられます。 途中で、ORがどのように挿入されるかを確認したいと思います-それらは常に挿入され、最後のORは噛み付きます(さらに、野barな方法で)。



  foreach ($filter_value as $filter_val) $query_filters .= 'fp.`id_feature_value` = '.(int)$filter_val.' OR '; $query_filters = rtrim($query_filters, 'OR ').') ';
      
      





これは見苦しいと思います。 したがって、このコードを自分のスタイルで書き直しています。 以下は、ソースおよび修正された機能コードです。



 //modules/blocklayered/block blocklayered.php private static function getId_featureFilterSubQuery($filter_value, $ignore_join = false) { if (empty($filter_value)) return array(); $query_filters = ' AND EXISTS (SELECT * FROM '._DB_PREFIX_.'feature_product fp WHERE fp.id_product = p.id_product AND '; foreach ($filter_value as $filter_val) $query_filters .= 'fp.`id_feature_value` = '.(int)$filter_val.' OR '; $query_filters = rtrim($query_filters, 'OR ').') '; return array('where' => $query_filters); }
      
      





 //modules/blocklayered/block blocklayered.php private static function getId_featureFilterSubQuery($filter_value, $ignore_join = false) { if (empty($filter_value)) return array(); //headfire hack begin $query_filters = ' AND EXISTS (SELECT * FROM '._DB_PREFIX_.'feature_product fp WHERE fp.id_product = p.id_product AND '; $query_filters1 = ''; foreach ($filter_value as $filter_val) { if ($query_filters1) $query_filters1 .= ' OR '; $query_filters1 .= 'fp.`id_feature_value` = '.(int)$filter_val; } $query_filters .= '( '.$query_filters1.' )'.')'; // headfire hack end return array('where' => $query_filters); }
      
      





そして驚きは、この関数がどこでも直接呼び出されないことです。 不器用な名前は、それがプログラムによってどこかに形成され、動的に呼び出されるという悲しい考えにつながります。 ですから、他の同様の場所で正直なケースが作成されています。 そして、ここで動的ディスパッチを使用することにしました。



物語の終わり



グリッチが修正されました。 フィルターは美しく機能し始めました。 おそらく、このエラーはPrestaShopの新しいリリースですでに解決されています。 そうでなくても、あなたが同様の問題を抱えているなら、あなたを助けることができてうれしいです。 そして、まだ-手順が明らかであっても、ブラケットを削らないでください。



All Articles