Advanced custom fields (ACF) - Pola powtarzalne i stylowanie

Advanced custom fields (ACF) - Pola powtarzalne i stylowanie

Ostatnio zweryfikowano: 13 października 2026
19 min czytania
Case study
Full-stack developer
Wtyczka Advanced Custom Fields (ACF Pro) to standard w profesjonalnym tworzeniu dedykowanych motywów WordPress. Jej najważniejszą funkcją jest bez wątpienia pole powtarzalne (Repeater Field). Umożliwia ono edytorom tworzenie nieograniczonej liczby powtarzalnych elementów: harmonogramów konferencji, cenników, list członków zespołu, kroków procesu zakupowego czy rozbudowanych galerii bez konieczności rejestrowania osobnych typów wpisów (Custom Post Types).

Często jednak pojawia się wyzwanie projektowe: jak w ramach jednej pętli kontrolować wygląd poszczególnych elementów? Designerzy nierzadko projektują układ naprzemienny (tzw. układ zygzakowaty, gdzie wiersze nieparzyste mają tekst po lewej i zdjęcie po prawej, a wiersze parzyste na odwrót), zróżnicowane kolory tła czy wyróżnienie co trzeciej karty ofertowej.

W tym artykule inżynieryjnym dowiesz się, jak prawidłowo obsługiwać pętlę have_rows(), jak wykorzystać operator modulo oraz wbudowane funkcje indeksujące ACF, kiedy przenieść ciężar stylowania do arkuszy CSS za pomocą :nth-child(), a przede wszystkim: jak pole powtarzalne wpływa na bazę danych MySQL i jak uniknąć spadku wydajności przy setkach wierszy.

#Jak działa pole Repeater wewnątrz bazy danych WordPress

Zanim napiszemy pierwszą linijkę kodu w szablonie PHP, musimy zrozumieć architekturę danych ACF. W przeciwieństwie do relacyjnych baz projektowanych od zera, WordPress przechowuje metadane w płaskiej strukturze klucz-wartość w tabeli wp_postmeta.

Gdy dodajesz pole typu Repeater o nazwie sekcje_oferty z dwoma podpólami (tytul oraz opis) i wypełniasz 5 wierszy, ACF tworzy w bazie:

  1. Rekord główny: sekcje_oferty z wartością 5 (liczba wierszy).
  2. Rekordy dla wiersza 0: sekcje_oferty_0_tytul oraz sekcje_oferty_0_opis.
  3. Rekordy dla wiersza 1: sekcje_oferty_1_tytul oraz sekcje_oferty_1_opis.
  4. …oraz ukryte rekordy systemowe definiujące klucze pól (_sekcje_oferty_0_tytul = field_65a12b8).

Dla 10 wierszy i 4 podpól oznacza to wygenerowanie ponad 80 osobnych wpisów w tabeli wp_postmeta. Świadomość tej struktury jest fundamentalna: niewłaściwe odpytywanie repeatera w pętli szablonu może doprowadzić do lawiny zapytań SQL i opóźnień w generowaniu strony (TTFB).

#Podstawowa pętla have_rows i the_row

Oficjalny sposób iteracji po repeaterze opiera się na zestawie funkcji have_rows() i the_row():

<?php if ( have_rows('sekcje_oferty') ) : ?>
    <section class="oferta-repeater-wrapper" aria-label="<?php esc_attr_e('Nasza oferta', 'wppoland'); ?>">
        <?php while ( have_rows('sekcje_oferty') ) : the_row(); 
            // Pobieranie danych bieżącego wiersza
            $tytul = get_sub_field('tytul');
            $tekst = get_sub_field('tekst');
            $grafika = get_sub_field('grafika');
        ?>
            <article class="oferta-karta">
                <?php if ( ! empty( $grafika ) && is_array( $grafika ) ) : ?>
                    <div class="oferta-grafika">
                        <?php echo wp_get_attachment_image( (int) $grafika['id'], 'medium_large' ); ?>
                    </div>
                <?php endif; ?>
                <div class="oferta-tresc">
                    <h3><?php echo esc_html( $tytul ); ?></h3>
                    <div class="opis"><?php echo wp_kses_post( $tekst ); ?></div>
                </div>
            </article>
        <?php endwhile; ?>
    </section>
<?php endif; ?>

Funkcja the_row() przesuwa wewnętrzny wskaźnik tablicy wierszy i ustawia kontekst dla get_sub_field(). Nigdy nie używaj wewnątrz repeatera standardowej funkcji get_field('tytul') zamiast get_sub_field('tytul'), ponieważ spowoduje to błąd lub pobranie niepowiązanej wartości z poziomu wpisu.

#Jak stylować wiersze repeater ACF w PHP z uzyciem get_row_index i modulo

Gdy projekt wymaga, aby co drugi element miał inny kolor tła, odwrócony układ kolumn lub inną klasę CSS, do gry wkracza logika warunkowa.

#Jak działa get_row_index w ACF

Wielu programistów tworzy manualną zmienną pomocniczą $i = 0 przed pętlą. Choć działa to poprawnie, ACF oferuje wbudowaną funkcję pomocniczą get_row_index(). Zwraca ona numer bieżącego wiersza, zaczynając od 1 (wiersz pierwszy = 1, drugi = 2 itd.):

<?php if ( have_rows('bloki_procesu') ) : ?>
    <div class="proces-kontener">
        <?php while ( have_rows('bloki_procesu') ) : the_row(); 
            $indeks = get_row_index();
            // Sprawdzamy parzystość wiersza
            $czy_parzysty = ( $indeks % 2 === 0 );
            $klasa_ukladu = $czy_parzysty ? 'uklad-odwrocony' : 'uklad-standardowy';
            $klasa_tla    = $czy_parzysty ? 'bg-szary' : 'bg-bialy';
        ?>
            <div class="krok-procesu <?php echo esc_attr( $klasa_ukladu . ' ' . $klasa_tla ); ?>">
                <span class="numer-kroku"><?php echo esc_html( sprintf( '%02d', $indeks ) ); ?></span>
                <div class="tresc-kroku">
                    <h3><?php echo esc_html( get_sub_field('naglowek') ); ?></h3>
                    <p><?php echo esc_html( get_sub_field('krotki_opis') ); ?></p>
                </div>
            </div>
        <?php endwhile; ?>
    </div>
<?php endif; ?>

#Jak wyróżnić co trzeci element repeatera

Operator modulo (%) oblicza resztę z dzielenia liczb całkowitych. Daje to ogromne możliwości sterowania układem:

  • Układ naprzemienny (A/B): $indeks % 2 === 0
  • Wyróżnienie co 3. kafelka w siatce (np. karta polecana): $indeks % 3 === 0
  • Wyróżnienie pierwszego i ostatniego wiersza: porównanie z wartością 1 oraz liczbą wszystkich wierszy pobraną przed pętlą.

Oto przykład oznaczania kart polecanych w siatce trójkolumnowej:

$modulo_trzy = $indeks % 3;
if ( $modulo_trzy === 0 ) {
    $klasa_karty = 'karta-polecana karta-trzecia';
} elseif ( $modulo_trzy === 1 ) {
    $klasa_karty = 'karta-standardowa karta-pierwsza';
} else {
    $klasa_karty = 'karta-standardowa karta-druga';
}

#Precyzyjne stylowanie pierwszego i ostatniego elementu

W aplikacjach webowych pierwszy wiersz często pełni funkcję nagłówka sekcji, a ostatni wymaga usunięcia dolnego obramowania lub dodania podsumowania:

<?php 
$surowe_wiersze = get_field('bloki_procesu');
$laczna_liczba  = is_array( $surowe_wiersze ) ? count( $surowe_wiersze ) : 0;

if ( have_rows('bloki_procesu') ) : ?>
    <div class="proces-lista">
        <?php while ( have_rows('bloki_procesu') ) : the_row(); 
            $i = get_row_index();
            $klasy = ['wiersz-procesu'];

            if ( $i === 1 ) {
                $klasy[] = 'wiersz-poczatkowy';
            }
            if ( $i === $laczna_liczba ) {
                $klasy[] = 'wiersz-koncowy';
            }
            if ( $i % 2 === 0 ) {
                $klasy[] = 'wiersz-parzysty';
            } else {
                $klasy[] = 'wiersz-nieparzysty';
            }
        ?>
            <div class="<?php echo esc_attr( implode( ' ', $klasy ) ); ?>">
                <h4><?php echo esc_html( get_sub_field('tytul') ); ?></h4>
            </div>
        <?php endwhile; ?>
    </div>
<?php endif; ?>

#Warunki oparte na zawartości podpól i warianty układu

Stylowanie wierszy w pętli nie musi ograniczać się do pozycji liczbowej. Połączenie indeksu wiersza z wartościami podpól pozwala na dynamiczne przełączanie szablonów i stylów:

<?php if ( have_rows('studia_przypadku') ) : ?>
    <div class="studia-przypadku-kontener">
        <?php while ( have_rows('studia_przypadku') ) : the_row(); 
            $indeks = get_row_index();
            $wariant = get_sub_field('wariant_prezentacji'); // 'pelna-szerokosc', 'kolumny', 'miniatura'
            $kolor_akcentu = get_sub_field('kolor_akcentu');

            $klasy_kontenera = [
                'sekcja-studium',
                'pozycja-' . $indeks,
                'wariant-' . sanitize_html_class( $wariant ?: 'standard' )
            ];

            if ( $indeks % 2 === 0 ) {
                $klasy_kontenera[] = 'kierunek-lustrzany';
            }
        ?>
            <section 
                class="<?php echo esc_attr( implode( ' ', $klasy_kontenera ) ); ?>"
                style="<?php echo $kolor_akcentu ? esc_attr( '--akcent: ' . sanitize_hex_color( $kolor_akcentu ) . ';' ) : ''; ?>"
            >
                <?php 
                if ( $wariant === 'pelna-szerokosc' && $indeks === 1 ) {
                    get_template_part( 'template-parts/studium/hero' );
                } else {
                    get_template_part( 'template-parts/studium/karta' );
                }
                ?>
            </section>
        <?php endwhile; ?>
    </div>
<?php endif; ?>

#Jak stylować repeater ACF samym CSS za pomoca nth-child

Zasada czystego kodu mówi: nie używaj logiki backendowej do zadań, które można zrealizować w czystym arkuszu stylów. Jeśli zmiana w wierszu dotyczy wyłącznie koloru tła, marginesów czy odwrócenia kolejności za pomocą Flexboksa, kod PHP powinien pozostać prosty, a stylowanie należy przekazać do CSS.

Dzięki nowoczesnym pseudoklasom CSS :nth-child() uzyskujemy pożądany układ bez zanieczyszczania szablonu PHP klasami technicznymi:

/* Zmiana tła dla parzystych wierszy (efekt zebry) */
.proces-kontener .krok-procesu:nth-child(even) {
  background-color: #f8fafc;
}

/* Odwrócenie kierunku układu Flexbox w co drugim bloku */
.proces-kontener .krok-procesu {
  display: flex;
  flex-direction: row;
  gap: 2rem;
  align-items: center;
}

.proces-kontener .krok-procesu:nth-child(even) {
  flex-direction: row-reverse;
}

/* Wyróżnienie co trzeciej karty w siatce CSS Grid */
.oferta-repeater-wrapper .oferta-karta:nth-child(3n) {
  border: 2px solid #2563eb;
  transform: scale(1.02);
}

/* Pierwszy i ostatni element listy */
.proces-kontener .krok-procesu:first-child {
  border-top: 3px solid #0f172a;
}

.proces-kontener .krok-procesu:last-child {
  border-bottom: 3px solid #0f172a;
}

#Kiedy PHP jest bezwzględnie konieczne

CSS :nth-child nie wystarczy w następujących scenariuszach:

  1. Różna struktura znaczników HTML: Gdy wiersze nieparzyste wymagają dodatkowego kontenera <div> lub tagu <aside>, którego nie ma w wierszach parzystych.
  2. Ładowanie różnych plików szablonów: Gdy w zależności od iteracji wywołujesz inne funkcje get_template_part('template-parts/karta', 'promowana').
  3. Generowanie danych ustrukturyzowanych Schema.org: Gdy parametr position w JSON-LD musi być wyliczony liczbowo w PHP na serwerze.

#Integracja z ACF Flexible Content dla elastycznych sekcji

Gdy powtarzalne sekcje wymagają całkowicie odmiennych zestawów pól (np. baner wideo, siatka ikon, tabela porównawcza), umieszczanie dziesiątek opcjonalnych podpól w jednym repeaterze prowadzi do chaosu w panelu wpisu. W takich sytuacjach optymalnym rozwiązaniem architektonicznym jest pole typu Flexible Content.

<?php if ( have_rows('moduly_strony') ) : ?>
    <main class="strona-moduly">
        <?php while ( have_rows('moduly_strony') ) : the_row(); 
            $uklad = get_row_layout();
            $numer = get_row_index();
        ?>
            <section class="modul modul-<?php echo esc_attr( $uklad ); ?> modul-indeks-<?php echo (int) $numer; ?>">
                <?php 
                if ( $uklad === 'sekcja_tekst_foto' ) :
                    get_template_part( 'template-parts/bloki/tekst-foto', null, [ 'indeks' => $numer ] );
                elseif ( $uklad === 'siatka_korzysci' ) :
                    get_template_part( 'template-parts/bloki/korzysci', null, [ 'indeks' => $numer ] );
                elseif ( $uklad === 'cytat_klienta' ) :
                    get_template_part( 'template-parts/bloki/cytat', null, [ 'indeks' => $numer ] );
                endif;
                ?>
            </section>
        <?php endwhile; ?>
    </main>
<?php endif; ?>

Dzięki funkcji get_row_layout() kod szablonu pozostaje modułowy, a baza danych nie jest obciążona pustymi wpisami dla niewykorzystanych opcji.

#Nowoczesne bloki Gutenberg z uzyciem ACF Blocks, InnerBlocks i kontekstu blokow

W dobie pełnej edycji witryny (FSE) i edytora blokowego Gutenberg, tradycyjne pola powtarzalne na dole ekranu wpisu ustępują miejsca dedykowanym blokom ACF rejestrowanym przez plik block.json. Wykorzystanie mechanizmów InnerBlocks oraz Block Context pozwala budować zagnieżdżone, wizualne struktury kart z pełną kontrolą edytorską.

#Konfiguracja bloku nadrzędnego i podrzędnego w block.json

Plik block.json dla elementu podrzędnego deklaruje relację rodzica oraz wymagany kontekst:

{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 3,
  "name": "wppoland/karta-etapu",
  "title": "Karta Etapu Procesu",
  "category": "design",
  "parent": [ "wppoland/kontener-etapow" ],
  "usesContext": [ "wppoland/ukladKontenera" ],
  "acf": {
    "mode": "preview",
    "renderTemplate": "render.php"
  }
}

#Szablon renderujący kontener i karty potomne

W szablonie kontenera (kontener-etapow/render.php) wstrzykujemy edytor bloków potomnych:

<div class="kontener-etapow-wrapper">
    <InnerBlocks 
        allowedBlocks="<?php echo esc_attr( wp_json_encode( ['wppoland/karta-etapu'] ) ); ?>"
        template="<?php echo esc_attr( wp_json_encode( [ ['wppoland/karta-etapu'], ['wppoland/karta-etapu'] ] ) ); ?>"
    />
</div>

W pliku potomnym render.php odczytujemy przekazany kontekst układu bez konieczności odpytywania bazy o rodzica:

<?php
$uklad = $context['wppoland/ukladKontenera'] ?? 'siatka';
$naglowek = get_field('naglowek_etapu');
$opis = get_field('opis_etapu');
?>
<div class="karta-etapu-blok uklad-<?php echo esc_attr( $uklad ); ?>">
    <h4><?php echo esc_html( $naglowek ); ?></h4>
    <div><?php echo wp_kses_post( $opis ); ?></div>
</div>

Taka architektura eliminuje ograniczenia monolitycznych repeaterów i pozwala redaktorom przestawiać kolejność kart bezpośrednio w edytorze wizualnym.

#Wydajność: Jak przyspieszyć duże pole Repeater i zapobiec przeciążeniu wp_postmeta

Gdy edytor wprowadzi do repeatera 50 lub 100 wierszy, strona może zwolnić o kilkaset milisekund. Wynika to z faktu, że każda komórka repeatera generuje osobny rekord w wp_postmeta, a wielokrotne wywołania get_sub_field() obciążają procesor i pamięć podręczną.

#1. Pobranie wszystkich danych na raz za pomocą get_field

Zamiast pętli while(have_rows()), która inkrementuje stan wewnętrzny w każdym obrocie, w przypadku bardzo dużych repeaterów szybsze bywa pobranie całej tablicy wielowymiarowej jednym wywołaniem:

$wszystkie_sekcje = get_field('sekcje_oferty');

if ( ! empty( $wszystkie_sekcje ) && is_array( $wszystkie_sekcje ) ) {
    foreach ( $wszystkie_sekcje as $index => $wiersz ) {
        // Dane są już w tablicy PHP, brak wywołań get_sub_field()
        $tytul = $wiersz['tytul'] ?? '';
        $opis  = $wiersz['tekst'] ?? '';
        $numer = $index + 1;
        $klasa = ( $numer % 2 === 0 ) ? 'wiersz-parzysty' : 'wiersz-nieparzysty';

        echo '<div class="' . esc_attr( $klasa ) . '">';
        echo '<h3>' . esc_html( $tytul ) . '</h3>';
        echo '<div>' . wp_kses_post( $opis ) . '</div>';
        echo '</div>';
    }
}

#2. Buforowanie wyrenderowanego HTML z uzyciem Transients API

Dla powtarzalnych bloków, które zmieniają się rzadko (np. cenniki czy sekcje FAQ oparte na repeaterze), najlepszą praktyką inżynieryjną jest buforowanie gotowego kodu HTML w tabeli transientów lub pamięci Redis:

function wppoland_render_sekcje_oferty_cached( int $post_id ): void {
    $cache_key = 'wppoland_repeater_oferta_' . $post_id;
    $html = get_transient( $cache_key );

    if ( false === $html ) {
        ob_start();
        if ( have_rows('sekcje_oferty', $post_id) ) {
            echo '<div class="oferta-grid">';
            while ( have_rows('sekcje_oferty', $post_id) ) {
                the_row();
                $nr = get_row_index();
                $klasa = ( $nr % 2 === 0 ) ? 'karta-parzysta' : 'karta-nieparzysta';
                echo '<div class="' . esc_attr( $klasa ) . '">';
                echo '<h3>' . esc_html( get_sub_field('tytul') ) . '</h3>';
                echo '<div>' . wp_kses_post( get_sub_field('tekst') ) . '</div>';
                echo '</div>';
            }
            echo '</div>';
        }
        $html = ob_get_clean();
        
        // Zapis do pamięci podręcznej na 24 godziny (86400 s)
        set_transient( $cache_key, $html, 86400 );
    }

    echo $html;
}

// Czyszczenie pamięci podręcznej przy zapisie wpisu
add_action('save_post', function (int $post_id): void {
    if ( ! wp_is_post_revision( $post_id ) && ! wp_is_post_autosave( $post_id ) ) {
        delete_transient('wppoland_repeater_oferta_' . $post_id);
    }
});

Dzięki takiemu zabiegowi złożony repeater z 50 wierszami renderuje się w ułamku milisekundy dla każdego kolejnego odwiedzającego.

#Optymalizacja danych repeater w WP REST API i architekturze Headless

Przy tworzeniu nowoczesnych frontonów w technologiach React, Next.js czy Astro, dane repeatera są pobierane przez interfejs WP REST API. Domyślna odpowiedź endpointu zawiera surowe identyfikatory i rozbudowane struktury ACF, które warto zoptymalizować po stronie serwera za pomocą funkcji register_rest_field:

add_action('rest_api_init', function(): void {
    register_rest_field('page', 'bloki_procesu_api', [
        'get_callback' => function( array $post_arr ): array {
            $post_id = $post_arr['id'];
            $wiersze = get_field('bloki_procesu', $post_id);

            if ( empty( $wiersze ) || ! is_array( $wiersze ) ) {
                return [];
            }

            return array_map( function( array $wiersz, int $klucz ): array {
                return [
                    'pozycja'     => $klucz + 1,
                    'czyParzysty' => ( ( $klucz + 1 ) % 2 === 0 ),
                    'naglowek'    => sanitize_text_field( $wiersz['naglowek'] ?? '' ),
                    'opis'        => wp_kses_post( $wiersz['krotki_opis'] ?? '' ),
                ];
            }, $wiersze, array_keys( $wiersze ) );
        },
        'schema' => [
            'description' => 'Zoptymalizowana lista etapów procesu dla aplikacji headless.',
            'type'        => 'array',
        ],
    ]);
});

Taki zabieg zdejmuje z warstwy frontendu konieczność wyliczania parzystości i normalizacji danych.

#Architektura szablonow dla rozbudowanych powtorzen i podzial na komponenty

W miarę jak projekt rośnie, umieszczanie całej logiki HTML bezpośrednio w pliku page.php lub single.php prowadzi do powstania trudnego w utrzymaniu spaghetti code. W profesjonalnych motywach dedykowanych stosujemy wzorzec podziału na małe, reużywalne fragmenty szablonów (template parts) oraz dedykowane klasy pomocnicze.

#Przekazywanie argumentow do get_template_part w WordPress

Od wersji WordPress 5.5 funkcja get_template_part() natywnie obsługuje trzeci parametr $args, co całkowicie eliminuje potrzebę stosowania zmiennych globalnych wewnątrz powtarzalnych pętli ACF:

<?php if ( have_rows('bloki_oferty') ) : ?>
    <div class="oferta-siatka-komponentow">
        <?php while ( have_rows('bloki_oferty') ) : the_row(); 
            $indeks = get_row_index();
            $dane_wiersza = [
                'indeks'       => $indeks,
                'czy_parzysty' => ( $indeks % 2 === 0 ),
                'naglowek'     => get_sub_field('naglowek_karty'),
                'opis'         => get_sub_field('opis_karty'),
                'ikona'        => get_sub_field('ikona_karty'),
            ];

            get_template_part( 'template-parts/komponenty/karta-oferty', null, $dane_wiersza );
        ?>
        <?php endwhile; ?>
    </div>
<?php endif; ?>

Wewnątrz pliku template-parts/komponenty/karta-oferty.php odczytujemy tablicę $args w sposób przewidywalny i bezpieczny:

<?php
/**
 * Komponent pojedynczej karty oferty.
 *
 * @var array $args Dane przekazane z pętli nadrzędnej.
 */
$indeks       = (int) ( $args['indeks'] ?? 1 );
$czy_parzysty = (bool) ( $args['czy_parzysty'] ?? false );
$naglowek     = $args['naglowek'] ?? '';
$opis         = $args['opis'] ?? '';
$ikona        = $args['ikona'] ?? null;

$klasa_karty = $czy_parzysty ? 'karta-oferty styl-parzysty' : 'karta-oferty styl-nieparzysty';
?>
<article class="<?php echo esc_attr( $klasa_karty ); ?>" data-index="<?php echo esc_attr( $indeks ); ?>">
    <?php if ( ! empty( $ikona['id'] ) ) : ?>
        <div class="karta-ikona">
            <?php echo wp_get_attachment_image( (int) $ikona['id'], 'thumbnail' ); ?>
        </div>
    <?php endif; ?>
    <h3 class="karta-naglowek"><?php echo esc_html( $naglowek ); ?></h3>
    <div class="karta-opis"><?php echo wp_kses_post( $opis ); ?></div>
</article>

Dzięki takiemu rozdzieleniu warstw możemy testować wygląd pojedynczej karty w izolacji i łatwo modyfikować reguły formatowania bez ryzyka uszkodzenia nadrzędnej pętli.

#Zaawansowana obsluga zagniezdzonych pol powtarzalnych

Jednym z najczęstszych wyzwań w zaawansowanych motywach WordPress jest sytuacja, w której pole Repeater zawiera wewnątrz kolejne pole typu Repeater (tzw. zagnieżdżony repeater, np. lista działów firmy, a w każdym dziale lista pracowników).

#Bezpieczna iteracja po zagnieżdżeniach

Aby prawidłowo obsłużyć wielopoziomowe struktury, należy precyzyjnie kontrolować zakresy pętli i unikać konfliktów nazw zmiennych:

<?php if ( have_rows('dzialy_firmy') ) : ?>
    <section class="dzialy-firmy-harmonogram">
        <?php while ( have_rows('dzialy_firmy') ) : the_row(); 
            $indeks_dzialu = get_row_index();
            $nazwa_dzialu  = get_sub_field('nazwa_dzialu');
        ?>
            <div class="dzial-blok dzial-<?php echo esc_attr( $indeks_dzialu ); ?>">
                <h2 class="dzial-tytul"><?php echo esc_html( $nazwa_dzialu ); ?></h2>

                <?php if ( have_rows('pracownicy_dzialu') ) : ?>
                    <ul class="pracownicy-lista">
                        <?php while ( have_rows('pracownicy_dzialu') ) : the_row(); 
                            $indeks_pracownika = get_row_index();
                            $imie_nazwisko     = get_sub_field('imie_nazwisko');
                            $stanowisko        = get_sub_field('stanowisko');
                            $klasa_wiersza     = ( $indeks_pracownika % 2 === 0 ) ? 'wiersz-b' : 'wiersz-a';
                        ?>
                            <li class="pracownik-pozycja <?php echo esc_attr( $klasa_wiersza ); ?>">
                                <strong><?php echo esc_html( $imie_nazwisko ); ?></strong>
                                <span><?php echo esc_html( $stanowisko ); ?></span>
                            </li>
                        <?php endwhile; ?>
                    </ul>
                <?php endif; ?>
            </div>
        <?php endwhile; ?>
    </section>
<?php endif; ?>

Warto pamiętać, że get_row_index() wewnątrz wewnętrznej pętli pracownicy_dzialu zwraca pozycję pracownika w danym dziale, a nie globalny licznik zewnętrzny. Jest to zgodne z kontekstem aktualnie aktywnego kursora the_row().

#Zabezpieczenia i profilaktyka bledow w srodowiskach produkcyjnych

Wdrożenie powtarzalnych pól na serwisach o dużym natężeniu ruchu wymaga wdrożenia mechanizmów defensywnych. Niedopatrzenia mogą skutkować przerwaniem działania skryptu PHP lub podatnościami bezpieczeństwa.

  1. Walidacja typu danych przed count(): Próba wykonania funkcji count( get_field('pole') ) w sytuacji, gdy pole jest puste lub zwraca wartość logiczną false, wywoła w PHP 8 błąd krytyczny TypeError. Zawsze sprawdzaj is_array() przed zliczeniem elementów.
  2. Ucieczka atrybutów danych HTML: Jeśli przekazujesz indeks do atrybutu data w znaczniku (np. data-row-id), zawsze rzutuj wartość na liczbę całkowitą (int) $indeks lub użyj esc_attr().
  3. Zapobieganie pętlom nieskończonym: Zawsze upewnij się, że wywołanie the_row() znajduje się bezpośrednio wewnątrz nawiasów pętli while. Pominięcie tej funkcji spowoduje nieskończoną pętlę i wyczerpanie limitu pamięci serwera.
  4. Czyszczenie pamięci podręcznej zapytań: Przy bardzo dużych importach treści (np. z systemów ERP lub plików CSV), po zaktualizowaniu setek wierszy repeatera używaj clean_post_cache( $post_id ), aby odświeżyć pamięć podręczną obiektów WordPressa.

#Najczęstsze pytania o stylowanie repeater ACF

#Czym różni się get_row_index od ręcznego licznika zmiennej pomocniczej

Funkcja get_row_index() jest natywną metodą ACF, która automatycznie zwraca pozycję bieżącego wiersza w pętli, zaczynając od liczby 1. Zwalnia programistę z konieczności deklarowania i inkrementowania zmiennych pomocniczych przed i wewnątrz pętli.

#Czy lepiej używać klas PHP czy selektorów CSS nth-child

Do czysto wizualnych modyfikacji (kolor tła, zmiana kierunku flexbox, marginesy) zdecydowanie zaleca się CSS :nth-child(). Zmniejsza to objętość kodu HTML i separuje warstwę prezentacji. PHP jest konieczne, gdy parzystość determinuje odmienną strukturę tagów HTML lub ładowanie różnych szablonów.

#Jak bezpiecznie pobierać obrazy wewnątrz repeatera

W konfiguracji pola ACF ustaw zwracany format jako Image ID lub Image Array. W kodzie PHP najlepiej wykorzystać identyfikator i wywołać wp_get_attachment_image($grafika['id'], 'rozmiar'), co automatycznie wygeneruje atrybuty srcset, sizes oraz nowoczesne leniwe ładowanie.

#Dlaczego strona z dużym polem Repeater ładuje się powoli

ACF przechowuje każdą komórkę repeatera jako osobny wpis w tabeli wp_postmeta. Przy dziesiątkach wierszy baza danych musi przetworzyć setki zapytań o pojedyncze metadane. Rozwiązaniem jest buforowanie fragmentów HTML (Transients API) lub optymalizacja zapytań poprzez jednorazowe pobranie tablicy get_field().

#Praktyczny przyklad implementacji: cennik uslug z wyroznionym pakietem

Aby zilustrować połączenie wszystkich omówionych technik, przeanalizujmy kompletny, produkcyjny przykład sekcji cennika. W tym scenariuszu edytor wprowadza dowolną liczbę pakietów w repeaterze, a motyw automatycznie stosuje następujące reguły:

  • Drugi lub trzeci pakiet może zostać oznaczony jako rekomendowany za pomocą pola wyboru w panelu administracyjnym.
  • Wiersze nieparzyste i parzyste otrzymują subtelne klasy pomocnicze do animacji pojawiania się w CSS.
  • Pierwszy pakiet otrzymuje etykietę startową, a ostatni link kontaktowy do dedykowanej wyceny.
<?php if ( have_rows('pakiety_cennika') ) : 
    $wszystkie_pakiety = get_field('pakiety_cennika');
    $lacznie           = is_array( $wszystkie_pakiety ) ? count( $wszystkie_pakiety ) : 0;
?>
    <section class="cennik-sekcja" id="cennik" aria-label="<?php esc_attr_e('Tabela cenowa', 'wppoland'); ?>">
        <div class="cennik-siatka">
            <?php while ( have_rows('pakiety_cennika') ) : the_row(); 
                $indeks       = get_row_index();
                $nazwa        = get_sub_field('nazwa_pakietu');
                $cena         = get_sub_field('cena_pakietu');
                $opis         = get_sub_field('opis_pakietu');
                $czy_polecany = (bool) get_sub_field('oznacz_jako_bestseller');
                $funkcje      = get_sub_field('lista_funkcji');

                $klasy = [ 'karta-cenowa', 'pozycja-' . $indeks ];
                if ( $czy_polecany ) {
                    $klasy[] = 'karta-wyrozniona';
                }
                if ( $indeks === 1 ) {
                    $klasy[] = 'karta-startowa';
                }
                if ( $indeks === $lacznie ) {
                    $klasy[] = 'karta-enterprise';
                }
                $klasy[] = ( $indeks % 2 === 0 ) ? 'odcien-b' : 'odcien-a';
            ?>
                <div class="<?php echo esc_attr( implode( ' ', $klasy ) ); ?>">
                    <?php if ( $czy_polecany ) : ?>
                        <span class="etykieta-bestseller"><?php esc_html_e('Najpopularniejszy', 'wppoland'); ?></span>
                    <?php endif; ?>

                    <h3 class="tytul-pakietu"><?php echo esc_html( $nazwa ); ?></h3>
                    <div class="wartosc-ceny"><?php echo esc_html( $cena ); ?></div>
                    <p class="opis-pakietu"><?php echo esc_html( $opis ); ?></p>

                    <?php if ( ! empty( $funkcje ) && is_array( $funkcje ) ) : ?>
                        <ul class="cechy-pakietu">
                            <?php foreach ( $funkcje as $cecha ) : ?>
                                <li><?php echo esc_html( $cecha['cecha_tekst'] ?? '' ); ?></li>
                            <?php endforeach; ?>
                        </ul>
                    <?php endif; ?>

                    <div class="akcja-pakietu">
                        <a href="#kontakt" class="przycisk-zamow"><?php esc_html_e('Wybierz pakiet', 'wppoland'); ?></a>
                    </div>
                </div>
            <?php endwhile; ?>
        </div>
    </section>
<?php endif; ?>

Stylizację arkusza CSS projektujemy w oparciu o siatkę CSS Grid, która płynnie dopasowuje się do ekranów urządzeń mobilnych:

.cennik-siatka {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 2rem;
  align-items: stretch;
}

.karta-cenowa {
  border-radius: 12px;
  padding: 2rem;
  background: #ffffff;
  border: 1px solid #e2e8f0;
  transition: transform 0.25s ease, box-shadow 0.25s ease;
}

.karta-cenowa:hover {
  transform: translateY(-4px);
  box-shadow: 0 12px 24px -8px rgba(15, 23, 42, 0.12);
}

.karta-cenowa.karta-wyrozniona {
  border: 2px solid #2563eb;
  position: relative;
  box-shadow: 0 20px 25px -5px rgba(37, 99, 235, 0.1);
}

.karta-cenowa.odcien-b {
  background-color: #f8fafc;
}

#Diagnostyka problemow i rozwiazywanie bledow w petlach ACF

Podczas pracy z powtarzalnymi polami programiści często napotykają subtelne błędy, które utrudniają poprawne wyświetlanie stylów na stronie produkcyjnej. Oto najczęstsze scenariusze problemowe wraz ze sprawdzonymi rozwiązaniami:

  1. Brak inkrementacji wskaźnika w pętli: Jeśli w pętli while( have_rows(...) ) zapomnisz wywołać funkcję the_row(), wskaźnik pozycji pozostanie na pierwszym wierszu, a funkcja get_row_index() w nieskończoność będzie zwracać jedynkę, co doprowadzi do zawieszenia procesu PHP lub przekroczenia dopuszczalnego limitu czasu wykonania skryptu (max_execution_time).
  2. Niewłaściwy kontekst get_sub_field poza pętlą: Wywołanie funkcji get_sub_field() przed instrukcją the_row() lub po zakończeniu pętli zwróci pusty ciąg znaków lub wartość null. Zawsze upewnij się, że odczyt wartości komórek następuje po przestawieniu wskaźnika wiersza.
  3. Konflikt klas pseudoklas CSS :nth-child z dodatkowymi kontenerami: Jeśli system szablonów lub wtyczka optymalizacyjna wstrzykuje dodatkowe tagi <div> w strukturze drzewa DOM (np. wrappery slajderów Swiper lub Slick), selektor :nth-child() przestaje działać w odniesieniu do elementów logicznych. W takich przypadkach zaleca się wstrzyknięcie dedykowanych klas CSS bezpośrednio w kodzie PHP lub precyzyjne adresowanie bezpośredniego rodzica (>).
  4. Rozbieżności indeksowania w systemach wielojęzycznych: W instalacjach korzystających z WPML lub Polylang tłumaczone wersje wpisu posiadają niezależne wpisy w tabeli wp_postmeta. Przy dynamicznym pobieraniu danych z innego języka zawsze przekazuj jawny identyfikator wpisu źródłowego jako drugi argument w have_rows('pole', $id_wpisu).

#Podsumowanie i lista dobrych praktyk inżynieryjnych

Pole powtarzalne ACF daje edytorom ogromną elastyczność w zarządzaniu treścią, a deweloperom pozwala budować zaawansowane, modułowe szablony. Umiejętne łączenie funkcji indeksujących get_row_index(), logiki warunkowej w PHP oraz nowoczesnych reguł CSS :nth-child() gwarantuje czysty, czytelny kod i pełną kontrolę nad wyglądem.

Podczas wdrożenia w architekturze projektu kluczowe znaczenie mają następujące zasady techniczne:

  1. Zawsze zabezpieczaj pętlę: Sprawdzaj obecność danych funkcją have_rows() i poprawnie zamykaj pętlę while.
  2. Używaj get_sub_field: Nigdy nie sięgaj po get_field wewnątrz wiersza repeatera.
  3. Stosuj ucieczkę danych: Każdy ciąg tekstowy powinien przechodzić przez esc_html(), a treść bogata przez wp_kses_post().
  4. Dbaj o bazę wp_postmeta: Złożone struktury buforuj w pamięci podręcznej Transient lub zastępuj blokami Gutenberg z InnerBlocks.

Potrzebujesz pomocy w optymalizacji skomplikowanego motywu WordPress opartego o ACF lub chcesz zlecić stworzenie dedykowanych bloków Gutenberg? Sprawdź ofertę naszych usług programistycznych jako doświadczony WordPress developer lub skonsultuj się z nami w ramach utrzymania stron WordPress.

Następny krok

Przekuj artykuł w realne wdrożenie

Pod tym wpisem dokładam linki, które domykają intencję użytkownika i prowadzą dalej w strukturze serwisu.

Chcesz wdrożyć ten temat na swojej stronie?

Jeśli chcesz przełożyć wiedzę z artykułu na działającą stronę, sklep albo przebudowę serwisu, przygotuję konkretny zakres prac.

Powiązany klaster

Sprawdź inne usługi WordPress i bazę wiedzy

Wzmocnij swój biznes dzięki profesjonalnemu wsparciu technicznemu w kluczowych obszarach ekosystemu WordPress.

Polecane artykuły