Своё поле в Orchid пишется быстро: класс, наследующий базовое поле, плюс Blade-шаблон. Соблазн начать с него велик — но прежде чем создавать четвёртый файл в app/Orchid/Fields, стоит убедиться, что задача действительно не решается настройкой встроенного.
Разберём три уровня сложности — от простой обёртки до поля с внешней библиотекой — и цену, которую вы платите за каждый.
Встроенные поля Orchid
Orchid предоставляет много готовых полей:
use Orchid\Screen\Fields\*;
Input::make('name') // Текстовое поле
TextArea::make('description') // Многострочное
Select::make('status') // Выпадающий список
Relation::make('category') // Связь с моделью
DateTimer::make('date') // Дата/время
Switcher::make('active') // Переключатель
CheckBox::make('agree') // Чекбокс
Radio::make('type') // Radio-кнопки
Upload::make('files') // Загрузка файлов
Picture::make('image') // Изображение
Quill::make('content') // WYSIWYG редактор
Code::make('code') // Редактор кода
Matrix::make('data') // Таблица данныхНо иногда нужно что-то своё.
Способы расширения:
- Наследование от существующего поля
- Создание с нуля (Field + Blade)
- Интеграция JS-библиотек
Простое поле: Color Picker
Класс поля
// app/Orchid/Fields/ColorPicker.php
namespace App\Orchid\Fields;
use Orchid\Screen\Field;
class ColorPicker extends Field
{
/**
* Blade-шаблон
*/
protected $view = 'admin.fields.color-picker';
/**
* Атрибуты по умолчанию
*/
protected $attributes = [
'class' => 'form-control color-picker',
'type' => 'color',
];
/**
* Список разрешённых атрибутов
*/
protected $inlineAttributes = [
'name',
'value',
'class',
'style',
'disabled',
];
/**
* Предустановленные цвета
*/
public function colors(array $colors): self
{
$this->set('colors', $colors);
return $this;
}
/**
* Формат вывода (hex, rgb, hsl)
*/
public function format(string $format): self
{
$this->set('format', $format);
return $this;
}
}Blade-шаблон
{{-- resources/views/admin/fields/color-picker.blade.php --}}
@component($typeForm, get_defined_vars())
<div class="color-picker-wrapper">
<input
{{ $attributes }}
@if(isset($colors))
data-colors="{{ json_encode($colors) }}"
@endif
data-format="{{ $format ?? 'hex' }}"
>
@if(isset($colors) && count($colors))
<div class="color-presets mt-2">
@foreach($colors as $color)
<button
type="button"
class="color-preset-btn"
style="background-color: {{ $color }}"
data-color="{{ $color }}"
></button>
@endforeach
</div>
@endif
</div>
@endcomponent
@push('scripts')
<script>
document.querySelectorAll('.color-preset-btn').forEach(btn => {
btn.addEventListener('click', function() {
const input = this.closest('.color-picker-wrapper').querySelector('input');
input.value = this.dataset.color;
input.dispatchEvent(new Event('change'));
});
});
</script>
@endpush
@push('styles')
<style>
.color-preset-btn {
width: 24px;
height: 24px;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
margin-right: 4px;
}
.color-preset-btn:hover {
transform: scale(1.1);
}
</style>
@endpushИспользование
use App\Orchid\Fields\ColorPicker;
Layout::rows([
ColorPicker::make('settings.primary_color')
->title('Основной цвет')
->colors(['#3490dc', '#38c172', '#e3342f', '#f6993f'])
->format('hex'),
]);Поле отвечает за ввод, а не за корректность данных. Ограничения в шаблоне — формат, маска, набор допустимых значений — существуют для удобства оператора и обходятся отправкой формы напрямую. Валидация должна быть в Request или в обработчике, как для любого другого поля.
Особенно это касается полей со сложным значением (JSON, координаты, структуры): туда придёт то, что прислали, а не то, что вы нарисовали в интерфейсе.
Поле с JS-библиотекой: Map Selector
Установка библиотеки
npm install leafletКласс поля
// app/Orchid/Fields/MapSelector.php
namespace App\Orchid\Fields;
use Orchid\Screen\Field;
class MapSelector extends Field
{
protected $view = 'admin.fields.map-selector';
protected $attributes = [
'class' => 'map-selector',
];
protected $inlineAttributes = [
'name',
'value',
];
/**
* Начальные координаты
*/
public function center(float $lat, float $lng): self
{
$this->set('center', ['lat' => $lat, 'lng' => $lng]);
return $this;
}
/**
* Уровень зума
*/
public function zoom(int $zoom): self
{
$this->set('zoom', $zoom);
return $this;
}
/**
* Высота карты
*/
public function height(string $height): self
{
$this->set('height', $height);
return $this;
}
}Blade-шаблон
{{-- resources/views/admin/fields/map-selector.blade.php --}}
@component($typeForm, get_defined_vars())
<div
id="map-{{ $name }}"
class="map-container"
style="height: {{ $height ?? '400px' }}"
data-center="{{ json_encode($center ?? ['lat' => 55.7558, 'lng' => 37.6173]) }}"
data-zoom="{{ $zoom ?? 12 }}"
></div>
<input
type="hidden"
name="{{ $name }}"
id="input-{{ $name }}"
value="{{ $value }}"
>
<div class="mt-2 small text-muted coordinates-display" id="coords-{{ $name }}">
Координаты: <span>не выбраны</span>
</div>
@endcomponent
@push('head')
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
@endpush
@push('scripts')
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const mapEl = document.getElementById('map-{{ $name }}');
const input = document.getElementById('input-{{ $name }}');
const coordsDisplay = document.querySelector('#coords-{{ $name }} span');
const center = JSON.parse(mapEl.dataset.center);
const zoom = parseInt(mapEl.dataset.zoom);
const map = L.map(mapEl).setView([center.lat, center.lng], zoom);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap'
}).addTo(map);
let marker = null;
// Если есть сохранённое значение
if (input.value) {
const coords = JSON.parse(input.value);
marker = L.marker([coords.lat, coords.lng]).addTo(map);
coordsDisplay.textContent = `${coords.lat.toFixed(6)}, ${coords.lng.toFixed(6)}`;
}
// Клик по карте
map.on('click', function(e) {
const lat = e.latlng.lat;
const lng = e.latlng.lng;
if (marker) {
marker.setLatLng(e.latlng);
} else {
marker = L.marker(e.latlng).addTo(map);
}
input.value = JSON.stringify({ lat, lng });
coordsDisplay.textContent = `${lat.toFixed(6)}, ${lng.toFixed(6)}`;
});
});
</script>
@endpushИспользование
use App\Orchid\Fields\MapSelector;
Layout::rows([
MapSelector::make('location.coordinates')
->title('Местоположение')
->center(55.7558, 37.6173) // Москва
->zoom(12)
->height('300px'),
]);JSON Editor поле
// app/Orchid/Fields/JsonEditor.php
namespace App\Orchid\Fields;
use Orchid\Screen\Field;
class JsonEditor extends Field
{
protected $view = 'admin.fields.json-editor';
protected $attributes = [
'class' => 'json-editor',
];
/**
* Схема JSON (для валидации)
*/
public function schema(array $schema): self
{
$this->set('schema', $schema);
return $this;
}
/**
* Режим редактора (tree, code, text)
*/
public function mode(string $mode): self
{
$this->set('mode', $mode);
return $this;
}
}{{-- resources/views/admin/fields/json-editor.blade.php --}}
@component($typeForm, get_defined_vars())
<div
id="jsoneditor-{{ $name }}"
class="json-editor-container"
style="height: 400px; border: 1px solid #ced4da; border-radius: 4px;"
></div>
<textarea
name="{{ $name }}"
id="input-{{ $name }}"
style="display: none;"
>{{ $value }}</textarea>
@endcomponent
@push('head')
<link href="https://cdn.jsdelivr.net/npm/jsoneditor@9/dist/jsoneditor.min.css" rel="stylesheet">
@endpush
@push('scripts')
<script src="https://cdn.jsdelivr.net/npm/jsoneditor@9/dist/jsoneditor.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const container = document.getElementById('jsoneditor-{{ $name }}');
const textarea = document.getElementById('input-{{ $name }}');
const options = {
mode: '{{ $mode ?? "tree" }}',
modes: ['tree', 'code', 'text'],
onChangeText: function(jsonString) {
textarea.value = jsonString;
}
};
const editor = new JSONEditor(container, options);
// Загружаем начальные данные
try {
const json = JSON.parse(textarea.value || '{}');
editor.set(json);
} catch (e) {
editor.set({});
}
// Перед отправкой формы обновляем textarea
container.closest('form').addEventListener('submit', function() {
try {
textarea.value = JSON.stringify(editor.get());
} catch (e) {}
});
});
</script>
@endpushНаследование от существующего поля
// app/Orchid/Fields/PhoneInput.php
namespace App\Orchid\Fields;
use Orchid\Screen\Fields\Input;
class PhoneInput extends Input
{
public function __construct()
{
parent::__construct();
$this->mask('+7 (999) 999-99-99')
->title('Телефон')
->placeholder('+7 (___) ___-__-__');
}
/**
* Форматирование при получении
*/
public function value($value): self
{
if ($value) {
// Форматируем номер
$clean = preg_replace('/\D/', '', $value);
$value = '+7 (' . substr($clean, 1, 3) . ') '
. substr($clean, 4, 3) . '-'
. substr($clean, 7, 2) . '-'
. substr($clean, 9, 2);
}
return parent::value($value);
}
}Совместимость: При обновлении Orchid проверяйте, не изменились ли базовые классы полей. Кастомные поля могут потребовать адаптации.
Итоги
| Подход | Когда использовать |
|---|---|
| Наследование | Небольшие изменения существующего поля |
| Field + Blade | Полностью кастомный UI |
| JS-интеграция | Сложные компоненты (карты, редакторы) |
Структура файлов:
app/Orchid/Fields/
├── ColorPicker.php
├── MapSelector.php
├── JsonEditor.php
└── PhoneInput.php
resources/views/admin/fields/
├── color-picker.blade.php
├── map-selector.blade.php
└── json-editor.blade.phpЧто учесть до того, как писать своё поле
Каждое собственное поле — это обязательство по сопровождению. Оно опирается на внутреннее устройство Orchid: базовый класс, соглашения шаблонов, способ подключения скриптов. При обновлении пакета всё это может измениться, и обновление превратится из рутинной операции в работу. Четыре собственных поля — четыре потенциальные точки отказа при каждом мажорном обновлении.
Сначала проверьте, не решается ли задача настройкой. Встроенные поля Orchid принимают довольно много параметров, а Input с маской и правилами валидации закрывает больше случаев, чем кажется на первый взгляд. Полноценное своё поле оправдано там, где нужен принципиально другой способ ввода — карта, редактор структуры, выбор цвета, — а не другое оформление.
Наследование лучше написания с нуля. Расширяя существующее поле, вы получаете совместимость с валидацией, обработкой ошибок и внешним видом бесплатно — и меньше кода, который придётся чинить при обновлении.
Проверьте поведение в модальных окнах и в повторяющихся блоках. Поле с инициализацией на DOMContentLoaded прекрасно работает на обычной форме и не работает внутри модального окна, отрисованного позже, — а это тот сценарий, который обязательно встретится.
И практический совет по внешним библиотекам. Тяжёлая библиотека (карты, редакторы) подключается на всех страницах, где встречается поле, а иногда и на всех страницах админки — в зависимости от того, как вы её добавили. Проверьте, что она загружается только там, где нужна: лишние полмегабайта на каждом экране заметны при ежедневной работе.
Комментарии
Система комментариев скоро будет подключена. А пока вы можете написать мне в Telegram или на email.