Задача: создать приложение имеющую структуру данных
Абитуриенты по факультетам и специальностям | ||||
Фамилия | Экзаменационные оценки | Сумма баллов | ||
Математика | Физика | Сочинение |
Специальности | ||
Код специальности | Наименование | Факультет |
Примечание: выполнить сортировку по убыванию суммы баллов с группировкой по факультетам и специальностям, вычислить количество абитуриентов по факультетам, специальностям, по вузу.
- Используемые технологии
- Установка проекта
- Миграции
- Факультет
- Специальность
- Абитуриенты
- Создание тестовых записей
- Выполнение миграции
- Создание моделей
- Создание ресурсов
- Генерация файлов
- Ресурсы
- Связанные элементы
- Страница факультета
- Ресурс и страницы специальностей
- Ресурс Абитуриентов
- Вход в систему и просмотр результата
- Создание отчета
- Создание виджетов
- Виджет статистики
- Виджет количество абитуриентов по факультетам
- Виджет количество абитуриентов по специальностям
- Заключение
Используемые технологии
Для решения этой задачи были использованы:
- Laravel 11 — php фреймворк
- Filament 3 — отличный конструктор админок для laravel
- MySQL 8.0 — база данных
- guava/filament-nested-resources — библиотека для создания вложенных ресурсов
- eightynine/filament-reports — библиотека для создания отчетов
Установка проекта
Для начала установим проект и подключим нужные библиотеки
1 2 3 4 5 |
composer create-project laravel/laravel university-admissions --prefer-dist php artisan vendor:publish --tag=laravel-assets --ansi --force php artisan key:generate --ansi php artisan migrate --graceful --ansi cd university-admissions |
Теперь можно подключить филамент и библиотеку для вложенных ресурсов
1 2 3 |
composer require filament/filament php artisan filament:install --panels composer require guava/filament-nested-resources |
Миграции
Создадим через консоль файлы моделей и миграций
1 2 3 |
php artisan make:model Faculty -m php artisan make:model Specialty -m php artisan make:model Abiturient -m |
Факультет
1 2 3 4 5 |
Schema::create('faculties', function (Blueprint $table) { $table->id(); $table->string('name'); $table->timestamps(); }); |
Специальность
1 2 3 4 5 6 7 |
Schema::create('specialties', function (Blueprint $table) { $table->id(); $table->string('code'); $table->string('name'); $table->foreignId('faculty_id')->constrained('faculties'); $table->timestamps(); }); |
Абитуриенты
1 2 3 4 5 6 7 8 9 10 |
Schema::create('abiturients', function (Blueprint $table) { table->id(); $table->string('last_name'); $table->string('first_name'); $table->decimal('math_score', 5, 2)->nullable(); $table->decimal('physics_score', 5, 2)->nullable(); $table->decimal('essay_score', 5, 2)->nullable(); $table->foreignId('specialty_id')->constrained('specialties'); $table->timestamps(); }); |
Создание тестовых записей
Создадим файлы seed и factory для создания тестовых записей
1 2 3 |
php artisan make:factory FacultyFactory php artisan make:factory SpecialtyFactory php artisan make:factory AbiturientFactory |
Заполним созданные файлы
1 2 3 4 5 6 |
public function definition(): array { return [ 'name' => $this->faker->word . ' Faculty', ]; } |
1 2 3 4 5 6 7 8 |
public function definition(): array { return [ 'code' => $this->faker->numberBetween(1000000, 2000000), 'name' => $this->faker->word . ' Specialty', 'faculty_id' => \App\Models\Faculty::factory(), // Связь с Faculty ]; } |
1 2 3 4 5 6 7 8 9 10 11 |
public function definition(): array { return [ 'first_name' => $this->faker->firstName, 'last_name' => $this->faker->lastName, 'math_score' => $this->faker->numberBetween(2, 5), 'physics_score' => $this->faker->numberBetween(2, 5), 'essay_score' => $this->faker->numberBetween(2, 5), 'specialty_id' => \App\Models\Specialty::factory(), // Связь с Specialty ]; } |
Заменим значения в файле database\seeders\DatabaseSeeder.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<?php namespace Database\Seeders; use App\Models\Abiturient; use App\Models\Faculty; use App\Models\Specialty; use Illuminate\Database\Seeder; class DatabaseSeeder extends Seeder { public function run(): void { Faculty::factory(5) ->has(Specialty::factory(8) ->has(Abiturient::factory(15)) ) ->create(); } } |
Выполнение миграции
1 |
php artisan migrate |
Создание моделей
В рамках модели «Факультет» предусмотрено единственное поле — «имя». Модель «Специальность» связана с моделью «Факультет» по принципу «один ко многим». Кроме того, существует сквозная связь с абитуриентами через hasManyThrough:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\HasManyThrough; class Faculty extends Model { use HasFactory; protected $fillable = ['name']; public function specialties() { return $this->hasMany(Specialty::class); } public function abiturients(): HasManyThrough { return $this->hasManyThrough(Abiturient::class, Specialty::class); } } |
Вот и модель специальность
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\MorphMany; class Specialty extends Model { use HasFactory; protected $fillable = ['code','name','faculty_id']; public function faculty() { return $this->belongsTo(Faculty::class); } public function abiturients() { return $this->hasMany(Abiturient::class); } } |
Модель Абитуриенты состоит из фамилии, имени и специальности. Но по заданию нужно вывести поля средний бал и полное имя.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
<?php namespace App\Models; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Abiturient extends Model { use HasFactory; protected $fillable = ['last_name', 'first_name', 'specialty_id']; public function specialty() { return $this->belongsTo(Specialty::class); } public function faculty() { return $this->hasOneThrough(Faculty::class, Specialty::class); } public function fullName() : Attribute { return new Attribute( get: fn () => "{$this->first_name} {$this->last_name}" ); } public function totalScore(): Attribute { return Attribute::make( get: fn () => $this->math_score + $this->physics_score + $this->essay_score ); } } |
После создания моделей выполним команду для заполнения бд
1 |
php artisan db:seed |
Создание ресурсов
Тут описание, как примерно должна выглядеть структура нашего проекта
Генерация файлов
Ресурсы
1 2 3 |
php artisan make:filament-resource Faculty --generate php artisan make:filament-resource Specialty --generate php artisan make:filament-resource Abiturient --generate |
Чтобы настроить вложенные ресурсы, выполните следующие действия:
- К ресурсам (корневому и всем дочерним ресурсам), которые вы хотите встроить, добавьте признак
NestedResource
. Вам нужно будет реализовать методgetAncestor
. Для корневого ресурса вернитеnull
, для всех дочерних ресурсов реализуйте его в соответствии с приведённой ниже документацией. - На каждой странице ресурсов из 1-го шага добавьте признак
NestedPage
. - Создайте
RelationManager
(документацию по нити) илиRelationPage
(документацию по нити) для объединения ресурсов. Добавьте признакNestedRelationManager
к любому из них.
Связанные элементы
php artisan make:filament-relation-manager SpecialtyResource abiturients last_name
Страница факультета
Страница для просмотра всех специальностей факультета
1 |
php artisan make:filament-page ManageFacultySpecialties --resource=FacultyResource --type=ManageRelatedRecords |
Страница для создания специальности на факультете
1 |
php artisan make:filament-page CreateFacultySpecialty --resource=FacultyResource --type=CreateRelatedRecord |
Страница для просмотра всех студентов факультета
1 |
php artisan make:filament-page ManageFacultyAbiturients --resource=FacultyResource --type=ManageRelatedRecords |
Создание страницы для редактирования абитуриента факультета
1 |
php artisan make:filament-page CreateFacultyAbiturient --resource=FacultyResource --type=CreateRelatedRecord |
В остальных страницах ListFaculty,EditFaculty,CreateFaculty
— добавим use NestedPage;
Теперь изменим ресурс факультета .app\Filament\Resources\FacultyResource.php
Добавим в ресурс NestedResource
Теперь необходимо реализовать метод getAncestor
и getBreadcrumbRecordLabel
для изменения названия факультета в хлебных крошках
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
public static function getPages(): array { return [ 'index' => Pages\ListFaculties::route('/'), 'create' => Pages\CreateFaculty::route('/create'), 'edit' => Pages\EditFaculty::route('/{record}/edit'), 'specialties' => Pages\ManageFacultySpecialties::route('/{record}/specialties'), 'specialties.create' => Pages\CreateFacultySpecialty::route('/{record}/specialties/create'), 'abiturients' => Pages\ManageFacultyAbiturients::route('/{record}/abiturients'), 'abiturients.create' => Pages\CreateFacultyAbiturient::route('/{record}/abiturients/create'), ]; } public static function getAncestor(): ?Ancestor { return null; } public static function getBreadcrumbRecordLabel(Faculty $record) { return $record->name; } public static function getRecordSubNavigation(Page $page): array { return $page->generateNavigationItems([ Pages\EditFaculty::class, Pages\ManageFacultySpecialties::class, Pages\ManageFacultyAbiturients::class, ]); } |
Так же добавим метод getRecordSubNavigation для управления специальностями и абитуриентами факультета.
Ресурс и страницы специальностей
Прописываем NestedPage во всех страницах специальности. Нужно удалить ListSpecialties в папке Pages
Создадим страницу создания абитуриентов
1 |
php artisan make:filament-page CreateFacultyAbiturient --resource=SpecialtyResource --type=CreateRelatedRecord |
Настроим Relation Manager app\Filament\Resources\SpecialtyResource\RelationManagers\AbiturientsRelationManager.php
Настроим ресурс app\Filament\Resources\SpecialtyResource.php
Добавим use NestedResource;
И связанные ресурсы и хлебные крошки
1 2 3 4 5 6 7 8 9 10 11 |
public static function getAncestor(): ?Ancestor { return Ancestor::make( 'specialties', 'faculty', ); } public static function getBreadcrumbRecordLabel(Specialty $record) { return $record->code.' - '.$record->name; } |
Ресурс Абитуриентов
Добавим поля для таблицы и формы
app\Filament\Resources\AbiturientResource.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
<?php namespace App\Filament\Resources; use App\Filament\Resources\AbiturientResource\Pages; use App\Filament\Resources\AbiturientResource\RelationManagers; use App\Models\Abiturient; use Filament\Forms; use Filament\Forms\Form; use Filament\Resources\Resource; use Filament\Tables; use Filament\Tables\Table; use Guava\FilamentNestedResources\Ancestor; use Guava\FilamentNestedResources\Concerns\NestedResource; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\SoftDeletingScope; class AbiturientResource extends Resource { use NestedResource; protected static ?string $model = Abiturient::class; protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack'; public static function form(Form $form): Form { return $form ->schema([ Forms\Components\TextInput::make('last_name') ->required(), Forms\Components\TextInput::make('first_name') ->required(), Forms\Components\TextInput::make('math_score') ->numeric(), Forms\Components\TextInput::make('physics_score') ->numeric(), Forms\Components\TextInput::make('essay_score') ->numeric(), Forms\Components\Select::make('specialty_id') ->required() ->relationship('specialty','name'), ]); } public static function table(Table $table): Table { return $table ->columns([ Tables\Columns\TextColumn::make('last_name') ->searchable(), Tables\Columns\TextColumn::make('first_name') ->searchable(), Tables\Columns\TextColumn::make('math_score') ->numeric() ->sortable(), Tables\Columns\TextColumn::make('physics_score') ->numeric() ->sortable(), Tables\Columns\TextColumn::make('essay_score') ->numeric() ->sortable(), Tables\Columns\TextColumn::make('specialty.name') ->numeric() ->sortable(), Tables\Columns\TextColumn::make('created_at') ->dateTime() ->sortable() ->toggleable(isToggledHiddenByDefault: true), Tables\Columns\TextColumn::make('updated_at') ->dateTime() ->sortable() ->toggleable(isToggledHiddenByDefault: true), ]) ->filters([ // ]) ->actions([ Tables\Actions\EditAction::make(), ]) ->bulkActions([ Tables\Actions\BulkActionGroup::make([ Tables\Actions\DeleteBulkAction::make(), ]), ]); } public static function getRelations(): array { return [ // ]; } public static function getPages(): array { return [ //'index' => Pages\ListAbiturients::route('/'), 'create' => Pages\CreateAbiturient::route('/create'), 'edit' => Pages\EditAbiturient::route('/{record}/edit'), ]; } public static function getAncestor(): ?Ancestor { return Ancestor::make( 'abiturients', 'speciality', ); } } |
Вход в систему и просмотр результата
Запустим браузер и войдем в созданную админку
Если все было создано без ошибок то нас ждет примерно такой результат:
Создание отчета
Для генерации отчета установим нужный пакет и создадим файл с первым отчетом
1 2 3 |
composer require eightynine/filament-reports php artisan make:filament-report AbiturientsReport |
app\Filament\Reports\AbiturientsReport.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
<?php namespace App\Filament\Reports; use App\Models\Abiturient; use App\Models\Faculty; use App\Models\Specialty; use EightyNine\Reports\Report; use EightyNine\Reports\Components\Body; use EightyNine\Reports\Components\Body\TextColumn; use EightyNine\Reports\Components\Footer; use EightyNine\Reports\Components\Header; use EightyNine\Reports\Components\Text; use Filament\Forms\Form; use Filament\Forms; use Filament\Forms\Get; use Filament\Forms\Set; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Log; class AbiturientsReport extends Report { public ?string $heading = "Report"; // public ?string $subHeading = "A great report"; public function header(Header $header): Header { return $header ->schema([ Header\Layout\HeaderRow::make() ->schema([ Header\Layout\HeaderColumn::make() ->schema([ Text::make("Sent abiturient report ") ->title() ->primary(), Text::make("A report where you can see which messages have been sent") ->subtitle(), ]), ]), ]); } public function body(Body $body): Body { return $body ->schema([ Body\Table::make() ->columns([ TextColumn::make("specialty") ->groupRows(), TextColumn::make("fullName"), TextColumn::make("mathScore"), TextColumn::make("physicsScore"), TextColumn::make("essayScore"), TextColumn::make("totalScore") ]) ->data(fn(?array $filters) => empty($filters['specialty_id']) ? collect([]) : Abiturient::query() ->when(isset($filters['specialty_id']), function ($query) use ($filters) { return $query->where('specialty_id', '=', $filters['specialty_id']); }) ->select('*') // Select all columns ->selectRaw('math_score + physics_score + essay_score as total_score') ->orderBy('total_score', 'desc') ->get() ->map(function ($abiturient) { return [ 'specialty' => $abiturient->specialty->name . ' ('.$abiturient->specialty->faculty->name .')', 'fullName' => $abiturient->fullName, 'mathScore' => $abiturient->math_score, 'physicsScore' => $abiturient->physics_score, 'essayScore' => $abiturient->essay_score, 'totalScore' => $abiturient->totalScore, ]; })), ]); } public function footer(Footer $footer): Footer { return $footer ->schema([ Footer\Layout\FooterColumn::make() ->schema([ Text::make("Generated on: " . now()->format('d.m.Y H:i:s')), ]) ->alignRight(), ]); } public function filterForm(Form $form): Form { return $form ->schema([ Forms\Components\Select::make('faculty_id') ->options(fn (Get $get): Collection => Faculty::query() ->pluck('name', 'id')) ->live() ->afterStateUpdated(fn (Set $set) => $set('specialty_id', null)), Forms\Components\Select::make('specialty_id') ->options(fn (Get $get): Collection => Specialty::query() ->where('faculty_id',$get('faculty_id')) ->pluck('name', 'id')) ->live(), ]); } } |
1 |
Не забудь подключить плагин в файле app\Providers\Filament\AdminPanelProvider.php
Отчет можно найти в основном меню
Создание виджетов
В нашем случаем мы создадим виджет для просмотра количества и два виджета с таблицей, в которой будет информация по факультетам и специальностями
Виджет статистики
1 |
php artisan make:filament-widget StatsOverview --stats-overview |
Вот и код виджета:
1 2 3 4 5 6 7 8 |
protected function getStats(): array { return [ Stat::make('Всего факультетов', Models\Faculty::query()->count()), Stat::make('Всего специальностей', Models\Specialty::query()->count()), Stat::make('Всего абитуриентов', Models\Abiturient::query()->count()), ]; } |
Виджет количество абитуриентов по факультетам
Теперь мы рассмотрим создание виджета, который будет отображать количество абитуриентов по факультетам. Этот виджет будет полезен для администрации университета, чтобы отслеживать количество поступающих на каждый факультет.
Выполним в консоли команду и настроим заполнение нашего виджета
1 |
php artisan make:filament-widget GroupFaculty |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
<?php namespace App\Filament\Widgets; use App\Models\Faculty; use Filament\Tables; use Filament\Tables\Table; use Filament\Tables\Columns\TextColumn; use Filament\Widgets\TableWidget as BaseWidget; use Illuminate\Contracts\Database\Eloquent\Builder; use Filament\Tables\Filters\SelectFilter; class GroupFaculty extends BaseWidget { public function table(Table $table): Table { return $table ->query( Faculty::query() ) ->columns([ TextColumn::make('name'), TextColumn::make('specialties_count')->counts('specialties')->sortable(), TextColumn::make('abiturients_count')->counts('abiturients')->sortable(), ]); } } |
Виджет количество абитуриентов по специальностям
Рассмотрим создание виджета, который будет отображать количество абитуриентов по специальностям. Виджет имеет фильтр для выбора конкретного факультета.
Создадим файл и наполним его
1 |
php artisan make:filament-widget GroupSpecialty |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
<?php namespace App\Filament\Widgets; use App\Models\Faculty; use App\Models\Specialty; use Filament\Tables; use Filament\Widgets\TableWidget as BaseWidget; use Filament\Tables\Table; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Filters\SelectFilter; class GroupSpecialty extends BaseWidget { public function table(Table $table): Table { return $table ->query( Specialty::query() ) ->columns([ TextColumn::make('name'), TextColumn::make('abiturients_count')->counts('abiturients')->sortable(), ]) ->filters([ SelectFilter::make('faculty_id') ->options(Faculty::orderBy('name')->pluck('name', 'id')) ]); } } |
Заключение
Поздравляем, база данных успешно создана! Теперь у нас есть мощная система, позволяющая выполнять ключевые операции:
- Создание, изменение и удаление факультетов.
- Создание, изменение и удаление специальностей.
- Создание, изменение и удаление абитуриентов.
- Формирование отчетов по оценкам абитуриентов.
- Вычисление количества абитуриентов по факультетам, специальностям и вузу.
Эта база данных представляет собой фундамент для дальнейшего развития. Вот несколько идей для ее улучшения:
- Внедрение системы авторизации и управления правами доступа.
- Добавление функции отслеживания успеваемости студентов.
- Реализация возможностей для управления учебными курсами и модулями.
- Интеграция с внешними сервисами для автоматизации процессов.