Рубрики та категорії wordpress, функція wp_list_categories. PHP клас для зручної та безпечної роботи з MySQL Марна category php

У цій статті (рівень веб-майстра - просунутий) йтиметься про, що перетинається за різними ознаками, т.зв. "фасетної" навігації. Для спрощення засвоєння матеріалу рекомендую пробігтися за статтею у Вікіпедії "Фасетна класифікація" та публікації на англійській мові(але з картинками!) "Design better faceted navigation for your websites".

Фасетна навігація з фільтрацією за кольором або ціновим діапазоном може бути корисна для ваших відвідувачів, але часто шкодить у пошуку через те, що створює безліч комбінацій адрес з контентом, що дублюється . Через дублі пошукові системине зможуть швидко сканувати сайт щодо оновлень контенту, що відповідно впливає і на індексацію. Щоб мінімізувати цю проблему та допомогти веб-майстрам зробити фасетну навігацію доброзичливою щодо пошуку, ми б хотіли:

Ідеально для користувачів та пошуку Google

Чіткий шлях до продуктів/сторінок статті:

Подання URL для сторінки категорії:
http://www.example.com/category.php?category=gummy-candies

Подання URL для конкретного продукту:
http://www.example.com/product.php?item=swedish-fish

Небажані дублікати, спричинені фасетною навігацією

Одна й та сама сторінка доступна з різних веб-адрес:

Канонічна сторінка



URL: example.com/product.php? item=swedish-fish

Дубльована сторінка



URL:example.com/product.php? item=swedish-fish&category=gummy-candies&price=5-10


category=gummy-candies&taste=sour&price=5-10

Помилки:

  • Безглуздо для Google, тому що користувачі рідко шукають [мармелад за ціною 9:55 доларів].
  • Безглуздо для пошукових роботів, які виявлять той самий елемент ("фруктовий салат") від батьківських сторінок категорій (або "Жувальний мармелад" або "кислий Жувальний мармелад").
  • Негативний момент для власника сайту, тому запити на індексацію розбавляються численними версіями однієї категорії.
  • Негативний момент для власника сайту, тому що це марне та зайве навантаження в пропускної спроможностісайту
Порожні сторінки:


URL: example.com/category.php? category=gummy-candies&taste=sour&price=over-10

Помилки:

  • Неправильно віддається код для пошукових систем (у такому разі сторінка має віддавати код 404)
  • Порожня сторінка для користувачів


Найгірші рішення (не дружні щодо пошуку) фасетної навігації

Приклад №1: У складі URL застосовуються не стандартні параметри: коми та дужки, замість ключ = значення &:

  • example.com/category? [ category:gummy-candy ][ sort:price-low-to-high ][ sid:789 ]
  • example.com/category?category , gummy-candy , sort , lowtohigh , sid , 789
Як треба:
example.com/category? category=gummy-candy&sort=low-to-high&sid=789

Приклад №2: Використання каталогів або шляхів до файлів, а не параметрів у списках значень, які не змінюють вмісту сторінки:
example.com/c123 /s789/ product?swedish-fish
(де /c123/ категорія, /s789/ ID сесії, що не змінює вміст сторінки)

Хороше рішення:

  • example.com /gummy-candy/ product?item=swedish-fish&sid=789(каталог, /gummy-candy/, змінює вміст сторінки значною мірою)
Найкраще рішення:
  • example.com/product?item=swedish-fish& category=gummy-candy&sid=789 (параметри URL дають більшу гнучкість для пошукових систем, щоб визначити, як ефективно сканувати)
Пошуковим роботам важко диференціювати корисні значення (наприклад, "gummy-candy") від марних (наприклад, "SESSIONID"), коли ці значення розміщуються безпосередньо у дорозі посилання. З іншого боку, параметри URL забезпечують гнучкість для пошукових систем, щоб швидко перевірити та визначити, коли це значення не вимагає доступу скануючого робота (краулера) до всіх всіх варіантів.

Загальні значення, які не змінюють вміст сторінки і повинні бути перераховані як параметри URL, включають:

  • ID сесії
  • Відстеження ідентифікаторів
  • Referrer ідентифікатор
  • Позначки часу
Приклад №3: Перетворення створених користувачами значень (можливо нескінченних) у параметри URL, які доступні для сканування та індексування, але марні для пошуку.
Використання незначних даних, що генеруються користувачами сайту (наприклад, як довгота/широта або "днів тому"), у сканованих та індексованих адресах:
  • example.com/find-a-doctor? radius=15&latitude=40.7565068&longitude=-73.9668408
  • example.com/article?category=health& days-ago=7
Як треба:
  • example.com/find-a-doctor? city=san-francisco&neighborhood=soma
  • example.com/articles?category=health& date=january-10-2014
Замість того, щоб дозволити користувачеві генерувати значення для створення сканованих URL-адрес (що призводить до нескінченних можливостей з дуже невеликою цінністю для відвідувачів), краще публікувати категорію сторінки для найбільш популярних значень, до того ж можна включати додаткову інформацію, щоб сторінка представляла більшу цінність, ніж звичайна пошукова сторінка із результатами. Крім того, можна подумати про розміщення згенерованих користувачем значення в окремому каталозі, а потім через robots.txt заборонити сканування з цього каталогу.
  • example.com /filtering/ find-a-doctor?radius=15&latitude=40.7565068&longitude=-73.9668408
  • example.com /filtering/ articles?category=health&days-ago=7
І в robots.txt:
User-agent: *
Disallow: /filtering/

Приклад №4. Додавання параметрів URL без логіки.

  • example.com /gummy-candy/lollipops/gummy-candy/ gummy-candy/product?swedish-fish
  • example.com/product? cat=gummy-candy&cat=lollipops&cat=gummy-candy&cat=gummy-candy&item=swedish-fish
Хороше рішення:
  • example.com /gummy-candy/ product?item=swedish-fish
Найкраще рішення:
  • example.com/product? item=swedish-fish&category=gummy-candy
Сторонні параметри URL лише збільшують дублювання, в результаті сайт менш ефективно сканується та індексується. Тому необхідно позбавлятися непотрібних параметрів URL і періодично займатися прибиранням сміттєвих посилань перед генерацією нових URL. Якщо багато параметрів необхідні для користувача сеансу, можна приховати інформацію в cookie, а не постійно додавати значення, як cat=gummy-candy&cat=lollipops&cat=gummy-candy& ...

Приклад №5: Пропонувати подальші уточнення (фільтрація), коли є нульові результати

Погано:
Дозволяє користувачам вибрати фільтри, коли існують нульові елементи для уточнення.


Уточнення до сторінки з нульовими результатами (наприклад, price=over-10), що засмучує користувачів та викликає непотрібні запити для пошукових систем.

Як треба:
Створювати посилання лише тоді, коли є елементи вибору користувача. При нульовому результаті посилання позначати "сірою" (тобто недоступною для кліка). Для подальшого поліпшення юзабіліті розглянути питання включення показника кількості доступних елементів поруч із кожним фільтром.


Виведення сторінки з нульовими результатами (наприклад, price=over-10) не допускається, плюс забороняється користувачам робити непотрібні кліки, а пошукова система сканувати цю не корисну сторінку.

Необхідно запобігати появі непотрібних адрес та мінімізувати простір для відвідувача, створюючи URL тільки за наявності продукції. Це допоможе користувачам залишатися зайнятими на вашому сайті (менше кліків по кнопці назад, коли немає жодного товару), зменшить кількість можливих URL, відомих пошуковим системам. Крім того, якщо сторінка не просто "тимчасово немає", а навряд чи коли-небудь міститиме релевантну інформацію, варто розглянути можливість зробити для неї код відповіді 404 . На 404 сторінці ви можете оформити корисне повідомленнядля користувачів з великою кількістю опцій у навігації або вікно пошуку, щоб користувачі могли знайти споріднені продукти.

Для нових сайтів, веб-майстри яких розглядають впровадження фасетної навігації, є кілька варіантів оптимізації сканування (сукупність адрес на вашому сайті, відомих роботу Google) унікальних сторінок контенту та зменшення попадання в індекс пошукової системи сторінок, що дублюються (консолідація сигналів індексації).

Визначте, які параметри URL-адреси потрібні для пошукових систем, щоб сканувати кожну індивідуальну сторінку з контентом (тобто визначити, які параметри необхідні для створення щонайменше одного клік-шляху до кожного пункту). Обов'язкові параметри можуть включати item-id, category-id, page т.д.

Визначте, які параметри будуть корисні для відвідувачів з їхніми запитами, і які, швидше за все, викличуть дублювання при скануванні та індексуванні. У прикладі з кондитерськими товарами (мармелад) параметр URL "taste" може бути цінним для користувачів із запитами на прикладі taste=sour . Тим не менш, логічно порахувати параметр "price", що викликає зайве дублювання category=gummy-candies&taste=sour& price=over-10 . Інші поширені приклади:

  • Цінні параметри для пошукових систем: item-id, category-id, name, brand...
  • Непотрібні параметри: session-id, price-range...
Розглянемо реалізацію однієї з кількох варіантів конфігурації для URL-адрес, які містять непотрібні параметри. Просто переконайтеся, що "непотрібні" параметри URL дійсно не потрібні для сканування пошуковим роботам або знаходження користувача кожного окремого продукту!

Варіант 1: і внутрішні посилання

Позначте всі непотрібні URL-адреси атрибутом . Це зменшить трудовитрати пошукового робота і запобігає зниженню частоти сканування. Глобально керувати скануванням потрібно через robots.txt (Прим. перекладача: див. статтю " ").
Скористайтеся атрибутом rel="canonical", щоб відокремити сторінки для пошукового індексу від непотрібних сторінок (наприклад на сторінці price=5-10 можна прописати атрибут rel="canonical", що вказує на категорію всього кислого мармеладу example.com/category.php?category=gummy-candies&taste=sour& page=all ).

Варіант 2: Robots.txt та Disallow

URL-адреси з непотрібними параметрами включають директорію /filtering/ , яка буде закрита в robots.txt (заборона disallow). Це дасть усім пошуковим системам сканувати тільки "правильне" внутрішньопосилання (вміст) сайту, але блокуватиме разом сканування небажаних URL. Наприклад ( example.com/category.php?category=gummy-candies), якщо цінними параметрами були item, category та taste, і зайвими були ідентифікатор сеансу та price, то URL буде для taste таким:
example.com/category.php?category=gummy-candies& taste=sour, але всі непотрібні параметри, такі як price, URL включить до визначеного каталогу - /filtering/:
example.com /filtering/ category.php?category=gummy-candies&price=5-10,
який потім через robots.txt буде заборонено:
User-agent: *
Disallow: /filtering/

Варіант 3: Роздільні хости

Впевніться, що найкращі рішення, перераховані вище (наприклад, для непотрібних адрес) ще застосовуються. Інакше пошукові системи вже сформували велику масу посилань в індексі. Таким чином, ваша робота буде спрямована на зниження подальшого зростання непотрібних сторінок, переглянутих за допомогою робота Google та консолідацію сигналів індексації.

Використовуйте параметри зі стандартним кодуванням та форматом ключ = значення (key = value).

Переконайтеся, що значення, які не змінюють вміст сторінки, такі як ідентифікатори сеансів, реалізовані як ключ=значення, а не каталогів.

Не дозволяйте клацати та не генеруйте URL-адреси, коли не існує елементів для фільтру.

Додайте логіку до відображення параметрів URL: видаліть непотрібні параметри, а не додавайте постійно значення (наприклад, уникайте такої генерації посилання: example.com/product?cat=gummy-candy&cat=lollipops &cat=gummy-candy&item=swedish-fish).

Зберігайте цінні параметри в URL, перерахувавши їх у першу (оскільки URL видно в результатах пошуку) черга і менш доречні параметри в останню (наприклад, ідентифікатор сесії).
Уникайте такої структури посилань: example.com/category.php? session-id=123&tracking-id=456&category=gummy-candies&taste=sour
Налаштуйте параметри URL в Інструментах для веб-майстрів, якщо ви маєте чітке уявлення про роботу посилань на вашому сайті.

Переконайтеся, що під час використання JavaScript для динамічного керування контентом (sort/filter/hide) без оновлення URL, є реальні веб-адреси на вашому сайті, що мають цінність у пошуку, наприклад, це основні категорії та сторінки продуктів, які доступні для сканування та індексування . Намагайтеся не використовувати тільки домашню сторінку(тобто один URL) для всього вашого сайту, а через JavaScript динамічно змінювати контент навігацією - це, на жаль, видасть у пошуку користувачам лише одну URL. Крім того, перевірте, щоб продуктивність не вплинула на роботу динамічної фільтрації у гірший бік, оскільки завадить користувачеві працювати з сайтом.

Поліпшіть індексацію різних сторінок одного контенту за допомогою атрибута rel="canonical" на привілейовану версію сторінки. Атрибут rel="canonical" може бути використаний всередині одного та кількох доменів.

Оптимізуйте індексацію контенту, розбитого на сторінки "паджинації" (наприклад, page=1 та page=2 з категорії "gummy candies") за допомогою (або):

  • Додайте атрибут rel="canonical" у серію сторінок із зазначенням канонічної категорії з параметром “view-all” (наприклад, page=1, page=2, та page=3 з категорії "gummy candies" з with rel=”canonical” на category=gummy-candies&page=all), переконавшись, що сторінка необхідна користувачам та завантажується швидко.
  • Використовуйте розмітку розбиття на сторінках rel="next" та rel="prev" , щоб вказати на зв'язок між окремими сторінками (див. статтю "Paginaton with rel="next" and rel="prev" ") .
Увімкніть лише канонічні посилання у файли Sitemap.

Для кожного посту та запису wordpressКористувач може задавати одну або кілька рубрик (категорій). Ця можливість дозволяє згрупувати близькі за змістом записи та надати можливість відвідувачам читати та переглядати лише ті рубрики, які їм подобаються. Наприклад, коли я створював свій основний блог Tod's Blog, то збирався писати про всі нюанси інтернету - починаючи з дизайну і до програмування. Припустимо, людина прийшла з пошуковика на статтю про wordpress і захотіла б почитати про систему ще більше - їй довелося б ритися в архівах, повторно використовувати пошук або переглядати всі пости поспіль. Зрозуміло, цього можна було уникнути, зайшовши в спеціальну категорію під назвою wordpress. Або, наприклад, для тих, хто захоплюється лише дизайном, могла бути цікавою рубрика для блогу.

Якщо уважно подивіться на шапку блогу, можете побачити свого роду меню, де рубрики wordpress виступають у ролі розділів проекту. Як на мене, це досить зручний та наочний спосіб поділу тематики записів.

У самому центрі сторінки ви побачите форму, щоб додати нову категорію. Тут потрібно вказати її назву (ім'я), ярлик (частина посилання url для чпу), батьківську категорію (якщо така є), а також можна задати короткий опис. Батьківська категорій дозволяє створювати в Wordpress розділи з декількома рівнями вкладеності – наприклад, для категорії «водрпрес» на якомусь ІТ-блозі можна додати ті ж шаблони, плагіни тощо.

Справа на сторінці Рубрики відображаються всі категорії wordpress, з можливістю редагування або видалення. Щоб зробити дії, достатньо підвести курсор мишки на ім'я тієї чи іншої категорії, після чого побачите невелике спливаюче меню.

Під час редагування ви побачите в одному з інформаційних блоків той, де можна буде вибрати одну або кілька категорій статті. Просто поставте галочки напроти потрібних імен.

Тут же можна додавати нові рубрики – натиснувши на відповідне посилання. Єдиний недолік цього механізму в тому, що при створенні можна вказати лише ім'я та батьківську категорію, тоді як завдання поля ярлик доведеться переходити до розділу «Рубрики» та редагувати інформацію там.

Крім того, редагувати категорії для постів у блозі можна через їх список у меню Записи – Змінити. Там при наведенні на ту чи іншу публікацію ви побачите посилання. Швидке редагування». Натискаємо по ній і бачимо форму для редагування:

Тут можна змінити і категорії, і теги та всю додаткову інформацію за статтею. Річ дуже зручна + працює без перезавантаження сторінки.

Функція wp_list_categories для категорії Wordpress

За традицією розглядаю як питання роботи з тими чи іншими елементами системи, а й наводжу спеціальні функції шаблонів. Так само як я розповідав про . Отже, для виведення списку категорій із посиланнями на них використовується wp_list_categories. Вона має цілу низку аргументів:

  • show_option_all – відображає посилання на всі категорії, якщо стилем відображення вибрав список.
  • orderby – сортування для категорій за ID, ім'ям (name), ярликом (slug), кількістю постів (count).
  • order – порядок сортування (ASC – по збільшенню, DESC – по зменшенню).
  • show_last_updated – показувати дату останнього оновлення.
  • style – стиль оформлення: список (list), поділ через
    (None).
  • show_count – відображати кількість постів у категорії.
  • hide_empty – приховувати порожні рубрики, де записів немає.
  • use_desc_for_title – використовувати опис для атрибуту title у посиланні.
  • child_of – виведення лише категорій для заданої батьківської рубрики.
  • feed – відображення посилання на фід для категорій.
  • feed_type – тип фіда.
  • feed_image – зображення для rss значок.
  • exclude – виключення категорій зі списку, при цьому параметр child_of автоматично вимикається.
  • exclude_tree - виключення цілої гілки рубрик.
  • include – зворотний параметр, який включає лише зазначені категорії Wordpress до списку.
  • hierarchical – параметр для відображення підкатегорій.
  • title_li - заголовок списку рубрик.
  • number – кількість категорій для відображення (якщо їх забагато).
  • echo – виводить рубрики, що за промовчанням дорівнює True.
  • depth – вказує кількість рівнів для підкатегорій висновку.

Насамкінець наведу ряд прикладів використання wp_list_categories. По-перше, варіант із шапки цього блогу.

"hide_empty=1&exclude=1&title_li=&orderby=count&order=desc&use_desc_for_title=0") ; ?>

Тут задано відображення прихованих категорій, виняток зі списку рубрики з, порожній рядок для заголовка блоку, сортування за кількістю статей та зменшення (тобто найбільше статей у мене в розділі ). Останній аргумент не підставляє опис категорії в title для посилання.

Ну і ще кілька простих ситуацій. Використання виключень та включень категорій.

Якщо є що доповнити про рубрики та категорії wordpress, пишемо в коментарях.

Update:Також вам може знадобитися невеликий хак щоб. У wordpress за замовчуванням визначається текст title щось на зразок «переглянути всі записи в рубриці ….», можна натомість просто залишити назву рубрики — читаємо статтю за посиланням вище.

Повертає масив об'єктів, що містять інформацію про категорії.

Параметри цієї функції дуже схожі на параметри функції, що передаються wp_list_categories()можуть бути передані як у вигляді масиву, так і у вигляді рядка запиту: type=post&order=DESC .

✈ 1 раз = 0.005625с = дуже повільно| 50000 разів = 11.98с = повільно| PHP 7.1.11, WP 4.9.5

Використання

$categories = get_categories($args);

Шаблон використання

$categories = get_categories(array("taxonomy" => "category", "type" => "post", "child_of" => 0, "parent" => "", "orderby" => "name", " order" => "ASC", "hide_empty" => 1, "hierarchical" => 1, "exclude" => "", "include" => "", "number" => 0, "pad_counts" => false, // повний списокпараметрів дивіться в описі функції http://wp-kama.ru/function/get_terms)); if($categories)( foreach($categories as $cat)( // Дані в об'єкті $cat // $cat->term_id // $cat->name (Рубрика 1) // $cat->slug (rubrika- 1) // $cat->term_group (0) // $cat->term_taxonomy_id (4) // $cat->taxonomy (category) // $cat->description (Текст опису) // $cat->parent (0) // $cat->count (14) // $cat->object_id (2743) // $cat->cat_ID (4) // $cat->category_count (14) // $cat->category_description (Текст опису) // $cat->cat_name (Рубрика 1) // $cat->category_nicename (rubrika-1) // $cat->category_parent (0) ) ) taxonomy (Рядок)Назву таксономії, яку потрібно обробляти. Додано з версії 3.0.
Типово: "category" type (Рядок)
  • post - категорії для постів (за замовчуванням);
  • link – розділи посилань.
    Типово: "post"
child_of (Рядок)Отримати дочірні категорії (включаючи всі рівні вкладеності) зазначеної категорії. У параметрі вказується ID батьківської категорії (категорія, вкладені категорії якої потрібно показати). parent (число)Отримує категорії батьківська категорія яких дорівнює вказаному у параметрі ID. На відміну від child_of в тому, що буде показано один рівень вкладеності.
За замовчуванням: "" orderby (Рядок)

Сортування даних за певними критеріями. Наприклад, за кількістю постів у кожній категорії або за назвою категорій. Доступні такі критерії:

  • ID – сортування за ID;
  • name – сортування за назвою (за замовчуванням);
  • slug - сортування по алт. імені (slug);
  • count – за кількістю записів у категорії;
  • term_group – за групою.

Типово: "name"

Order (Рядок)

Напрямок сортування, зазначеного у параметрі "orderby":

  • ASC - по порядку, від меншого до більшого (1, 2, 3; a, b, c);
  • DESC - в зворотному порядку, від більшого до меншого (3, 2, 1; c, b, a).

Типово: "ASC"

Hide_empty (логічний)

Отримувати чи ні порожні категорії (які не мають записів):

  • 1 (true) - не одержувати порожні,
  • 0 (false) – отримувати порожні.

Типово: true

Hierarchical (логічний)Якщо параметр встановлено в true, то результат будуть включені порожні дочірні категорії, дочірні категорії яких мають записи (непусті).
Типово: true exclude (рядок/масив)Виключити будь-які категорії зі списку. Потрібно вказувати ID категорій через кому або в масиві. Якщо цей параметр вказано, параметр child_of буде скасовано.
За замовчуванням: "" include (рядок/масив)Вивести списком лише зазначені категорії. Вказувати потрібно ID категорій через кому або в масиві.
За замовчуванням: "" number (число)Ліміт. Кількість категорій, які будуть отримані. За замовчуванням без обмежень будуть отримані всі категорії. pad_counts (логічний)Якщо передати true, то число, яке показує кількість записів у батьківських категоріях, буде сумою своїх записів та записів з дочірніх категорій.
Типово: false

Приклади

#1 Випадаючий список

Для того, щоб створити список з категорій ми можемо скористатися іншою спеціальною для цієї мети, функцією wp_dropdown_categories() :

Wp_dropdown_categories(array("hide_empty" => 0, "name" => "category_parent", "orderby" => "name", "selected" => $category->parent, "hierarchical" => true, "show_option_none" => __("None")));

однак з таким підходом ми втратимо певну гнучкість у налаштуванні списку, тому що ми отримай вже повністю сформований список.

Тому, в деяких випадках буде логічніше створити список, що випадає, за допомогою функції get_categories(). Ось приклад (передбачається, що нам потрібно вивести підкатегорії (дочірні) категорії 10):

#2 Список категорій та їх опис

Цей приклад покаже нам як можна вивести списком посилання на категорії, де відразу після кожного посилання йтиме опис категорії (вказується при створенні/редагуванні категорії):

"name", "order" => "ASC")); foreach($categories as $category)( echo "

Category: term_id). "" title="" . sprintf(__("View all posts in %s"), $category->name) . "" " . ">" . $category->name."

"; echo"

Description:". $category->description . "

"; echo"

Post Count: ". $category->count . "

"; } ?>

нотатки

  • Дивіться: get_terms() Type of arguments that can be changed.

список змін

З версії 2.1.0 Введено.

Код get categories : wp-includes/category.php WP 5.3.2

"category"); $args = wp_parse_args($args, $defaults); /** * Filters taxonomy використовувалися для редагування термінів, коли calling get_categories(). * * @since 2.7.0 * * @param string $taxonomy Taxonomy to retrieve terms from. * @param array $args An array of arguments. See get_terms(). */ $args["taxonomy"] = apply_filters("get_categories_taxonomy", $args["taxonomy"], $args); // Back compat if (isset($args["type"]) && "link" == $args["type"]) ( _deprecated_argument(__FUNCTION__, "3.0.0", sprintf(/* translators: 1: " type => link", 2: "taxonomy => link_category" */ __("%1$s is deprecated. Use %2$s instead."), " type => link", "taxonomy => link_category")); $args["taxonomy"] = "link_category"; ) $categories = get_terms($args); if (is_wp_error($categories)) ( $categories = array(); ) $categories; foreach (array_keys($categories) as $k) ( _make_cat_compat($categories[ $k ]); ) ) return $categories;

Я взявся за написання класу, що реалізує викладені у ній ідеї.
А точніше, оскільки ключовий функціонал уже використовувався в рамках робітничого фремворку, я зайнявся виділенням його до самостійного класу. Користуючись нагодою, хочу подякувати учасникам PHPClub-у за допомогу у виправленні кількох критичних помилок та корисні зауваження. Нижче я спробую описати основні особливості, але спочатку невеликий

дисклеймер

Є кілька способів роботи з SQL – можна використовувати квері-білдер, можна ORM, можна працювати з чистим SQL. Я обрав останній варіант, бо мені він ближчий. Я зовсім не вважаю перші два погані. Просто особисто мені завжди було тісно у їх межах. Але я в жодному разі не стверджую, що мій варіант кращий. Це ще один варіант. Який можна використовувати, у тому числі, і при написанні ORM-а. У будь-якому випадку, я вважаю, що наявність безпечного способу працювати з чистим SQL не може завдати жодної шкоди. Але при цьому, можливо, допоможе останнім прихильникам використання mysql_*, що залишилися, в коді програми, відмовитися, нарешті, від цієї порочної практики.


Двома словами, клас будується навколо набору функцій-хелперів, що дозволяють виконувати більшість операцій з БД в один рядок, забезпечуючи при цьому (на відміну від стандартних API) повнузахист від SQL ін'єкцій, реалізований за допомогою розширеного набору плейсхолдерів, що захищають будь-які типи даних, які можуть потрапляти до запиту.
В основу класу покладено три базові принципи:
  1. 100% захист від SQL ін'єкцій
  2. При цьому захист дуже зручний у застосуванні, що робить код коротшим, а не довшим
  3. Універсальність, портабельність та простота освоєння
Зупинюся докладніше на кожному з пунктів.

Безпека

забезпечується тими самими двома правилами, які я сформулював у статті:
  1. Будь-які- без виключень! - динамічні елементи потрапляють у запит тількичерез плейсхолдери.
  2. Все, що не виходить підставити через плейсхолдери - проганяє спочатку через білий список.
На жаль, стандартні бібліотеки не надають повного захисту від ін'єкцій, захищаючи за допомогою попередніх статей лише рядки та числа.
Тому, щоб зробити захист повним, довелося відмовитися від свідомо обмеженої концепції prepared statements на користь ширшого поняття – плейсхолдерів. Причому плейсхолдерів типизованих (ця річ усім нам відома за сімейством функцій printf(): %d - плейсхолдер, який підказує парсеру, як обробляти значення, що встановлюється, в даному випадку - як ціле число). Нововведення виявилося настільки вдалим, що зараз вирішило безліч проблем і значно спростило код. Докладніше про типизовані плейсхолдери я напишу нижче.
Підтримка фільтрації за білими списками забезпечується двома функціями, дещо притягнутими за вуха, але, тим не менш, необхідними.

Зручність та стислість коду програми

Тут мені також здорово допомогли типізовані плейсхолдери, які дозволили зробити виклики функцій однорядковими, передаючи відразу запит і дані для нього. Плюс набір хелперів, що нагадують такі в PEAR::DB - функцій, що відразу повертають результат потрібного типу. Всі хелпери організовані за однією і тією ж схемою: у функцію передається один обов'язковий параметр - запит з плейсхолдерами, і скільки завгодно опціональних параметрів, кількість і порядок яких повинні співпадати з кількістю та порядком розташування плейсхолдерів у запиті. У функцій сімейства Ind використовується ще один обов'язковий параметр - ім'я поля, яким здійснюється індексація повертається масиву.
Виходячи зі свого досвіду, я прийшов до наступного набору значень, що повертаються (і, як наслідок - хелперів):
  • query() – повертає mysqli resource. Може використовуватися традиційно з fetch() і т.д.
  • getOne() - повертає скаляр, перший елемент першого рядка результату
  • getRow() - одномірний масив, перший рядок результату
  • getCol() - одновимірний масив скалярів - колонку таблиці
  • getAll() - двомірний масив, індексований числами по порядку
  • getInd() - двомірний масив, індексований значеннями поля, вказаного першим параметром
  • getIndCol() - масив скалярів, індексований полем першого параметра. Незамінно складання словників виду key => value
У результаті більшість звернень до БД зводиться одно-двох малих конструкцій (замість 5-10 при традиційному підході):
$data = $db->getAll("SELECT * FROM ?n WHERE mod=?s LIMIT ?i",$table,$mod,$limit);
У цьому коді є лише необхідні та значущі елементи, але немає нічого зайвого та повторюваного. Всі тельбухи акуратно заховані всередину класу: хелпер getAll() дозволяє отримати відразу потрібний результат без написання циклів у коді програми, а типизовані плейсхолдери дозволяють безпечнододавати в запит динамічні елементи будь-якихтипів без прописування прив'язок (bind_param) вручну. Extra DRY код! У випадках використання плейсхолдерів?a і?u різниця в кількості коду стає ще більшою:
$data = $db->getAll("SELECT * FROM table WHERE category IN(?a)",$ids);

Універсальність та простота освоєння

стоять на трьох китах:
  1. ДужеНевеликий API - півдюжини плейсхолдерів і стільки ж хелперів.
  2. Ми працюємо зі старим добрим SQL, який не треба наново вчити.
  3. На погляд непомітна, але неймовірно корисна функція parse(), яка спочатку призначалася лише налагодження, але у результаті зросла до ключового елемента під час упорядкування складних запитів.
У результаті всі складносурядні запити збираються по-старому - наприклад в циклі - але при цьому при дотриманні всіх правил безпеки!
Наведу невеликий приклад (приклади складніше можна знайти в документації на посилання внизу статті):
Досить частий випадок, коли нам треба додати у запит умову за наявності змінної

$sqlpart=""; if (!empty($var)) ( $sqlpart = $db->parse(" AND field = ?s", $var); ) $data = $db->getAll("SELECT * FROM table WHERE a=? i ?p", $id, $sqlpart);
Тут важливо відзначити кілька моментів.
По-перше, оскільки ми не пов'язані рідним API, ніхто не забороняє нам пропарсити не весь запит повністю, а тільки його частину. Це виявляється супер-зручно для запитів, що збираються відповідно до будь-якої логіки: ми паримо тільки частину запиту, а потім вона підставляється в основний запит через спеціальний «холостий» плейсхолдер, щоб уникнути повторного парсингу (і дотримуватися правила «будь-які елементи підставляються тільки через плейсхолдер»).
Але, на жаль, це слабке місце всього класу. На відміну від решти плейсхолдерів (які, навіть будучи використані невірно, ніколи не призведуть до ін'єкції) некоректне використання плейсхолдера?p може до неї привести.
Проте захист від дурня сильно ускладнив би клас, але при цьому ніяк не захистив би від тупої вставки змінної в рядок запиту. Тому я вирішив залишити як є. Але якщо ви знаєте спосіб, як без надто великого оверинжинірингу вирішити цю проблему - я був би вдячний за ідеї.

Тим не менш, у результаті ми отримали потужний і легкий генератор запитів, який з лишком виправдовує цей невеликий недолік.
Потужний тому, що ми не обмежені синтаксисом квері-білдера, «SQL, написаним на PHP» - ми пишемо чистий SQL.
Легкий тому, що весь API складання запитів складається з півдюжини плейсхолдерів та функції parse()
Ось мій улюблений приклад – вставка з використанням функцій Mysql
$data = array("field"=>$value,"field2"=>$value); $sql = "INSERT INTO table SET ts=unix_timestamp(), ip=inet_aton(?s),?u"; $db->query($sql, $ip, $data);
З одного боку, ми зберігаємо синтаксис SQL, з іншого – робимо його безпечним, а з третього – капітально скорочуємо кількість коду.

Докладніше про типизовані плейсхолдери

Спочатку відповімо на запитання, чому плейсхолдери загалом?
Це, загалом, вже загальне місце, але, повторююсь - будь-які динамічні дані повинні потрапляти у запит лише через плейсхолдериз наступних причин:
  • найголовніше – безпека. Додавши змінну черз плейсхолдер, ми можемо бути впевнені, що вона буде коректно відформатована.
  • локальність форматування. Це не менш важливий момент. По-перше, дані форматуються безпосередньо перед потраплянням у запит, і не зачіпають вихідну змінну, яка потім може бути використана десь. По-друге, дані форматуються рівно там, де потрібно, а не до початку роботи скрипта, як при magic quotes, і не в десяти можливих місцях коду декількома розробниками, кожен з яких може сподіватися на іншого.
Розвиваючи цю концепцію, ми приходимо до думки, що пейсхолдери обов'язково повинні бути типизованими. Але чому?
Тут я хотів би ненадовго зупинитися, і простежити історію розвитку програмістської думки у сфері захисту від ін'єкцій.
Спочатку був хаос - взагалі ніякого захисту, пишаємо все як є.
Далі стало не сильно краще, з парадигмою «іскейпі все, що прийшло в скрипт від користувача» і кульмінацією у вигляді magic quotes.
Далі найкращі уми дійшли того, що правильно говорити не про екранування, а про форматування. Оскільки форматування не завжди зводиться до одного іскейпінг. Так у PDO з'явився метод quote(), який робив закінчене форматування рядка - не тільки екранував у ньому спецсимволи, а й укладав її в лапки, не сподіваючись на програміста. В результаті, навіть якщо програміст використовував цю функцію не на місці (наприклад, для числа), то ін'єкція все одно не проходила (а у випадку з голим екрануванням через mysql_real_escape_string вона легко проходить, якщо ми помістили в запит число, не укладаючи його в лапки ). Будучи використаною для форматування ідентифікатора, ця функція призводила до помилки на етапі розробки, що підказувало автору коду про те, що він трішечки неправий.
На жаль, на цьому автори PDO і зупинилися, оскільки в головах розробників досі міцно сидить думка про те, що форматувати у запиті треба лише рядки. Але насправді в запиті набагато більше елементів різних типів. І для кожного потрібен свій тип форматування!Тобто єдиний метод quote() нас ніяк не влаштує - потрібно багато різних quotes. Причому не як виняток, «нате вам quoteName()», а як одна з головних концепцій: кожному типу - свій формат. Ну а разів типів форматування виявляється багато – тип треба якось вказувати. І типизований плейсхолдер для цього підходить найкраще.

Крім того, типизований плейсхолдер - це дуже зручно!
По-перше, тому що стає непотрібним спеціальний оператор для прив'язки значення до плейсхолдера (але при цьому зберігається можливість вказати тип значення, що передається!)
По-друге, раз ми винайшли типизований плейсхолдер - ми можемо наліпити цих плейсхолдерів величезна кількість, на вирішення безлічі рутинних завдань зі складання SQL запитів.
Насамперед зробимо плейсхолдер для ідентифікаторів - нам його відчайдушно не вистачає в реальному, а не уявному авторами стандартних API, житті. Як тільки девелопер стикається з необхідністю динамічно додати в запит ім'я поля - кожен починає перекручуватися по-своєму, хто в ліс, хто дрова. Тут все уніфіковано з іншими елементами запиту, і додавання ідентифікатора стає не складніше додавання рядка. Але при цьому ідентифікатор форматується не як рядок, а відповідно до своїх власних правил - полягає у зворотні лапки, а всередині ці лапки екрануються подвоєнням.
Дальше більше. Наступний біль голови будь-якого розробника, коли-небудь намагався використовувати стандартні prepared statements у реальному житті - оператор IN(). Вуаль, у нас є плейсхолдер і для цієї операції! Підстановка масиву стає не складнішою за будь-які інші елементи, плюс вона уніфікованаз ними - ніяких окремих функцій, змінюється лише літера в плейсхолдері.
Так само робимо і плейсхолдер для SET. Не втримаюся і продемонструю, наскільки простим стає код для такого запиту, як INSERT… ON DUPLICATE:
$data = array("offers_in" => $in, "offers_out" => $out); $sql = "INSERT INTO stats SET pid=?i,dt=CURDATE(),?u ON DUPLICATE KEY UPDATE ?u"; $db->query($sql,$pid,$data,$data);
На даний момент класом підтримується 6 типів плейсхолдерів.

  • ?s («string») - рядки (а також DATE, FLOAT та DECIMAL).
  • ?i («integer») - цілі числа.
  • ?n («name») - імена полів та таблиць
  • ?p («parsed») - для вставки вже оброблених частин запиту
  • ?a («array») - набір значень для IN (рядок виду "a","b","c")
  • ?u («update») - набір значень для SET (рядок виду `field`="value", `field`="value")
Що цілком достатньо для моїх завдань, але цей набір завжди можна розширити будь-якими іншими плейсхолдерами, наприклад для дробових чисел. Робити окремий плейсхолдер для NULL я не бачу сенсу – його можна завжди вписати прямо у запит.
Автоматичну трансляцію PHP-шного NULL до SQL-івського NULL я вирішив не робити. Можливо, це трохи ускладнить код (у тих окремих випадках, коли це потрібно), зате зменшить його неоднозначність.

До речі, як багато хто міг помітити, цей клас багато в чому нагадує бібліотеку DbSimple Дмитра Котерова. Але я маю принципові розбіжності з деякими ідеями, закладеними в неї.
По-перше, я противник будь-якої магії, коли одна й та сама функція може повертати різний результат залежно від типу переданих даних. Це, можливо, трохи спрощує написання, але при цьому жахливо ускладнює супровід та налагодження коду. Тому в моєму класі вся магія зведена до мінімуму, а всі операції та типи даних завжди прописуються явно.
По-друге, у DbSimple трохи, як на мене, переускладнений синтаксис. З одного боку, фігурні дужки – геніальна ідея. З іншого боку - а навіщо, якщо в нашому розпорядженні вся міць PHP? Тому я вирішив піти іншим шляхом і вагомо «внутрішньою» - свідомо обмеженою - логіки ввів «зовнішню», обмежену лише синтаксисом РНР. Головне, щоб будь-які динамічні елементи потрапляли у запит лише через плейсхолдери, а решта залежить лише від фантазії розробника (та функції parse()).

Код класу доступний на Гітхабі, github.com/colshrapnel/safemysql/blob/master/safemysql.class.php
Cheat sheet з основними командами та прикладами: phpfaq.ru/misc/safemysql_cheatsheet_ru.pdf
Хороше уявлення про можливості можна отримати на сторінці прикладів документації (на жаль, не закінченої), phpfaq.ru/safemysql
Там же є відповіді на питання, що часто ставляться, такі як «чому ти не використовуєш рідні prepared statements?» та ін.
Тим не менш, буду радий відповісти на будь-які питання в коментарях, а також покращити за вашими зауваженнями як сам клас, так і цю статтю.