Added database structure and components

This commit is contained in:
2025-11-13 21:27:45 +00:00
parent 3f1c69c15b
commit 98a2ca4611
182 changed files with 3847 additions and 9 deletions
+253 -1
View File
@@ -1,2 +1,254 @@
# DADProject
# DAD Project - Bisca Game Platform
## Overview
The DAD project aims to develop and deploy the **Bisca Game Platform** as a web application. The platform is a distributed system that provides users with the ability to play the traditional Portuguese card game Bisca in single-player and multiplayer modes, manage their coins, and track their gaming statistics.
The web client is implemented as a **Single-Page Application (SPA)** built with **Vue.js** and hosted on the designated internal school server. The backend system includes a database, a RESTful API server, and a WebSocket server for real-time multiplayer gameplay.
## Project Architecture
### Frontend
- **Technology**: Vue.js SPA
- **Purpose**: User interface for game play, user management, coin transactions, and statistics visualization
- **Features**: Real-time game updates via WebSockets, responsive design, player profiles, leaderboards
### Backend
- **RESTful API Server**: Handles user authentication, coin transactions, game history, leaderboards, and administrative operations
- **WebSocket Server**: Manages real-time multiplayer game communication, ensuring the server acts as the authoritative source for game state
- **Database**: Persists user data, game results, transactions, and platform statistics
## Constraints
The project must adhere to the following mandatory constraints:
1. **Single-Page Application (SPA)** - The application must be implemented exclusively as an SPA
2. **Vue.js Requirement** - The client application must be built with Vue.js
3. **Payment Gateway Integration** - All external coin purchases must be processed via the provided RESTful API (Payment Gateway Service)
4. **Deployment** - The application must be deployed on the designated internal school server and be accessible via a desktop web browser
5. **Backend Technologies** - Must be open-source or available as free external services
6. **Server Compatibility** - Backend technologies must be installable on or accessible from the designated server; if not supported, compliant alternatives must be selected
7. **Database Seeding** - The deployed application's database must be seeded with realistic data in both scale and content to simulate typical usage patterns
## Core Features
### 1. User Management
- **Registration**: Users register with email, nickname, name, password (minimum 3 characters), and optional photo/avatar
- **Authentication**: Email and password-based login
- **Profile Management**: View and update profile information, change password
- **Sign Out**: Users can end their session
- **Account Deletion**: Requires explicit confirmation; permanently forfeits all coins
- **Welcome Bonus**: New users receive 10 coins upon registration
- **Admin Accounts**: Created only by existing administrators; same profile capabilities as users but cannot delete their own accounts
### 2. Coins and Transactions
- **Coin Balance**: Users can view their current coin balance
- **Coin Purchases**: External purchases via Payment Gateway Service
- **Conversion Rate**: €1 = 10 coins (purchases in €1 increments)
- **Coin Earning**: Players earn coins through gameplay and bonuses
- **Transaction History**: All coin-related transactions (bonuses, purchases, spending, earnings) are logged
- **History Access**:
- Players view only their own transaction history
- Administrators have read-only access to all players' histories
- Administrators cannot create, modify, or delete transactions
### 3. Games and Matches
#### Game Variants
- **Bisca de 3**: Three-card hands
- **Bisca de 9**: Nine-card hands
#### Game Modes
- **Single-Player**: Against a bot (available to anonymous and registered users)
- **Multiplayer**: Against human opponents (available only to registered users)
- **Match**: Best-of series (up to 4 marks/wins) with two players
#### Game Rules
- Games and matches are limited to two players
- The bot follows game rules and attempts to win when playing second; otherwise plays its lowest-value card
- Official Bisca rules apply (as defined in the "Bisca Game" documentation)
#### Multiplayer Gameplay
- **Real-Time Communication**: WebSockets for live game updates
- **Server Authority**: The server is the source of truth for:
- Card dealing
- Turn order
- Valid moves
- Trick resolution
- Drawing from stock
- Scoring
- End-of-game/match conditions
- **Move Timer**: 20-second limit per move
- **Resignation**:
- Opponent awarded all remaining cards
- Game ends immediately
- If in a match, player forfeits all remaining games
- Scores computed accordingly
- **Persistent Results**: Game and match results with metadata are persisted for user history and administrative access
### 4. Game History and Leaderboards
#### Tracking
- **Scope**: Only multiplayer games and matches are tracked
- **Metadata**: Start/end timestamps, variant, outcome (win/loss/draw/forfeit), final points, marks (capote, bandeira), duration
#### History Access
- **Players**: View only their own multiplayer history
- **Administrators**: Read-only access to all players' histories and detailed records
- **Detailed Records**: Visible only to administrators and participating players
#### Leaderboards
- **Personal Leaderboards**: Visible only to the owner; summarize totals for game wins, match wins, capotes, and bandeiras (optionally segmented by variant)
- **Global Leaderboards**: Visible to all users including anonymous visitors; highlight top players by game and match wins, may include capotes and bandeiras
- **Tie-Breaking**: Earlier achievers rank higher in case of ties
### 5. Administration
#### Administrator Capabilities
- **Profile Management**: Same as regular users, except cannot delete their own accounts
- **Account Management**: Create new administrator accounts, remove any account except their own
- **Soft Deletion**: Player accounts with prior activity must be soft-deleted
- **User Monitoring**: View all users (players and administrators), block/unblock players
- **Read-Only Access**:
- All transactions
- All multiplayer games and matches
- Platform usage summaries and statistics
#### Administrator Access Control
- Cannot play games or hold coins
- New administrator accounts can only be created by existing administrators
### 6. Statistics and Reporting
#### Anonymous Users and Players
- Access only generic, anonymized aggregates:
- Total registered players
- Games and matches played
- Recent activity summaries
#### Administrators
- Full, non-anonymized access including:
- Purchases by period and player
- Game/match volumes
- User activity breakdowns
- Time-series data
#### Visualization
- Statistics available in both visual (charts) and textual/tabular forms
- Privacy for non-admin viewers is preserved
- Exact metrics and visualizations are team-defined
## Enhancement and Innovation Opportunities
To distinguish this project, consider implementing one or more of the following:
### Custom Features (Examples)
- Support for three or more players per game (optional extension while preserving two-player mode)
- Spectator mode for multiplayer games and matches
- Full playback of past games
- Custom card sets
- Extended bot capabilities (e.g., improved AI strategies)
- Integration of bots into multiplayer games
### Architecture and Performance Enhancements (Examples)
- Queue server or worker for asynchronous, long-running operations
- Horizontal scaling of the WebSocket server
- Session recovery after WebSocket or node failures
- Cache server implementation
- NoSQL database for specific use cases
- Search engine for game history or statistics
- Asynchronous messaging broker (e.g., Kafka, RabbitMQ)
- Distributed components for improved scalability and resilience
### Implementation Quality
- Use of technologies or techniques not covered in standard coursework
- Performance optimization with empirical benchmarks
- Improved architecture and code structure
- Enhanced implementation quality and documentation
**Note**: Credit is proportional to demonstrated impact with preference given to distributed capabilities that improve architecture, scalability, or resilience.
## Technology Stack
### Frontend
- Vue.js (mandatory)
- Vite (build tool)
- ESLint (code quality)
### Backend
- **API Server**: Open-source, student-selected technology
- **WebSocket Server**: Open-source, student-selected technology
- **Database**: Open-source, student-selected technology
- All must be compatible with the designated school server
### Supported Variants
- Laravel (PHP) for REST API
- Node.js for WebSocket server
## Project Structure
```
DADProject/
├── api/ # Laravel REST API backend
├── frontend/ # Vue.js SPA
├── websockets/ # WebSocket server
└── README.md # This file
```
## Getting Started
Refer to the individual `README.md` files in each directory for setup and deployment instructions:
- [API Documentation](./api/README.md)
- [Frontend Documentation](./frontend/README.md)
- [WebSocket Server Documentation](./websockets/README.md)
## Database Schema
The application's database includes the following primary entities:
- **Users**: Player and administrator accounts
- **Games**: Individual game records (both single-player and multiplayer)
- **Matches**: Match records (series of games)
- **Coin Transactions**: Purchase and spending history
- **Coin Transaction Types**: Transaction categorization
- **Coin Purchases**: External purchase records
- **Leaderboard Data**: Aggregated player statistics
For detailed schema information, refer to the database migrations in the `api/database/migrations/` directory.
## Key Business Rules
1. **Coin System**:
- New users receive 10 welcome coins
- Conversion rate: €1 = 10 coins
- Coin purchases are made in €1 increments
- All transactions are logged and immutable (except soft-deleted accounts)
2. **Game Rules**:
- Two-player games only (in standard mode)
- Bot cannot be played in multiplayer
- 20-second move timer in multiplayer
- Resignation forfeits remaining games in a match
3. **Account Management**:
- Administrators cannot be created via public registration
- Administrators cannot delete their own accounts
- Account deletion requires explicit confirmation
- Soft deletion preserves historical data for deleted player accounts
4. **Leaderboard Ranking**:
- Multiplayer games and matches only
- Ties resolved by earlier achievement date
## Notes for Developers
- The WebSocket server acts as the authoritative source for all game state in multiplayer games
- Ensure proper error handling and recovery for WebSocket disconnections
- All coin transactions must be atomic and immutable
- Database must be seeded with realistic, varied data
- Privacy and data protection must be maintained throughout
## License
This project is part of the EI DAD 2025/26 course.
Binary file not shown.
@@ -0,0 +1,258 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
// Administrator, Player
$table->enum('type', ['A', 'P'])->default('P');
// Nickname - must be unique
$table->string('nickname', 20)->nullable()->unique();
// User access is blocked
$table->boolean('blocked')->default(false);
// User Photo/Avatar
$table->string('photo_avatar_filename')->nullable();
// Brain Coin Balance
$table->integer('coins_balance')->default(0);
// Users can be deleted with "soft deletes"
$table->softDeletes();
// custom data
$table->json('custom')->nullable();
});
// NOTE: Only multiplayer matches are registered
Schema::create('matches', function (Blueprint $table) {
$table->id();
// Type of games in the match (Bisca dos 3; Bisca dos 9)
$table->enum('type', ['3', '9'])->default('3');
// Player 1 (usually, creates the match)
$table->unsignedBigInteger('player1_user_id');
$table->foreign('player1_user_id')->references('id')->on('users');
// Player 2 (usually, joins the match)
$table->unsignedBigInteger('player2_user_id');
$table->foreign('player2_user_id')->references('id')->on('users');
// Winner (null when not ended)
$table->unsignedBigInteger('winner_user_id')->nullable();
$table->foreign('winner_user_id')->references('id')->on('users');
// Loser (null when not ended)
$table->unsignedBigInteger('loser_user_id')->nullable();
$table->foreign('loser_user_id')->references('id')->on('users');
// Match status
// Pending - Match is waiting for players
// Playing - Match is in progress
// Ended - Match is over
// Interrupted - Match was interrupted due to technical issues (not finished; no winner)
// Note: depending of the project's implementation, some status may never be used
$table->enum('status', ['Pending', 'Playing', 'Ended', 'Interrupted']);
// The stake for this match
$table->integer('stake')->default(3);
// Moment when the match began
$table->dateTime('began_at')->nullable();
// Moment when the match ended
$table->dateTime('ended_at')->nullable();
// Match total time (in seconds) = ended_at - began_at
$table->decimal('total_time', 8, 2)->nullable();
// After the match ended, the total marks for player 1
$table->integer('player1_marks')->nullable();
// After the game ended, the total marks for player 2
$table->integer('player2_marks')->nullable();
// After the match ended, the sum of points for player 1 (all games)
$table->integer('player1_points')->nullable();
// After the match ended, the sum of points for player 2 (all games)
$table->integer('player2_points')->nullable();
// custom data
$table->json('custom')->nullable();
});
// NOTE: Only multiplayer games are registered
Schema::create('games', function (Blueprint $table) {
$table->id();
// Type of game (Bisca dos 3; Bisca dos 9)
$table->enum('type', ['3', '9'])->default('3');
// Player 1 (usually, creates the game)
$table->unsignedBigInteger('player1_user_id');
$table->foreign('player1_user_id')->references('id')->on('users');
// Player 2 (usually, joins the game)
$table->unsignedBigInteger('player2_user_id');
$table->foreign('player2_user_id')->references('id')->on('users');
// True when the game has ended with a draw
$table->boolean('is_draw')->default(false);
// Winner (null when not ended, or when a draw)
$table->unsignedBigInteger('winner_user_id')->nullable();
$table->foreign('winner_user_id')->references('id')->on('users');
// Loser (null when not ended, or when a draw)
$table->unsignedBigInteger('loser_user_id')->nullable();
$table->foreign('loser_user_id')->references('id')->on('users');
// Match (null for standalone games)
$table->unsignedBigInteger('match_id')->nullable();
$table->foreign('match_id')->references('id')->on('matches');
// Game status: 'Pending', 'Playing', 'Ended', or 'Interrupted'.
// Pending: waiting for players
// Playing: in progress
// Ended: finished
// Interrupted: stopped due to technical issues (unfinished; no winner)
// Note: depending on the projects implementation, some statuses may never be used.
$table->enum('status', ['Pending', 'Playing', 'Ended', 'Interrupted']);
// Moment when the game began
$table->dateTime('began_at')->nullable();
// Moment when the game ended
$table->dateTime('ended_at')->nullable();
// Game total time (in seconds) = ended_at - began_at
$table->decimal('total_time', 8, 2)->nullable();
// After the game ended, the total of points for player 1
$table->integer('player1_points')->nullable();
// After the game ended, the total of points for player 2
$table->integer('player2_points')->nullable();
// custom data
$table->json('custom')->nullable();
});
Schema::create('coin_transaction_types', function (Blueprint $table) {
$table->id();
$table->string('name', 40);
// Credit - increase coins
// Debit - decrease coins
$table->enum('type', ['C', 'D']);
// Transaction Types can be deleted with "soft deletes"
$table->softDeletes();
// custom data
$table->json('custom')->nullable();
});
// Add default coin_transaction_types
DB::table('coin_transaction_types')->insert([
['name' => 'Bonus', 'type' => 'C'], // id = 1
['name' => 'Coin purchase', 'type' => 'C'], // id = 2
['name' => 'Game fee', 'type' => 'D'], // id = 3
['name' => 'Match stake', 'type' => 'D'], // id = 4
['name' => 'Game payout', 'type' => 'C'], // id = 5
['name' => 'Match payout', 'type' => 'C'], // id = 6
]);
Schema::create('coin_transactions', function (Blueprint $table) {
$table->id();
// Datetime of the coin transaction
$table->dateTime('transaction_datetime');
// User associated with the transaction
$table->unsignedBigInteger('user_id');
$table->foreign('user_id')->references('id')->on('users');
// Match associated with the transaction (optional - it can be null)
$table->unsignedBigInteger('match_id')->nullable();
$table->foreign('match_id')->references('id')->on('matches');
// Game associated with the transaction (optional - it can be null)
$table->unsignedBigInteger('game_id')->nullable();
$table->foreign('game_id')->references('id')->on('games');
// Type of the transaction
$table->unsignedBigInteger('coin_transaction_type_id');
$table->foreign('coin_transaction_type_id')->references('id')->on('coin_transaction_types');
// Amount of the transaction (coins)
// Positive -> increments the total amount of brain coins (Credit)
// Negative -> decrements the total amount of brain coins (Debit)
$table->integer('coins');
// custom data
$table->json('custom')->nullable();
});
Schema::create('coin_purchases', function (Blueprint $table) {
$table->id();
// Datetime of the coin purchase
$table->dateTime('purchase_datetime');
// User associated with the purchase
$table->unsignedBigInteger('user_id');
$table->foreign('user_id')->references('id')->on('users');
// Coin Transaction associated with this purchase
// One to One relation (purchase must have a coin transaction)
$table->unsignedBigInteger('coin_transaction_id')->unique();
$table->foreign('coin_transaction_id')->references('id')->on('coin_transactions');
// Amount of the purchase (real money in euros)
$table->decimal('euros', 8, 2);
// Purchases will involve a payment with a type and a reference
// MBWAY - Phone number with 9 digits
// PAYPAL - eMail
// IBAN - bank transfer (2 letters + 23 digits)
// MB - Multibanco payment - entity number (5 digits) + Reference (9 digits))
// VISA - Visa card number (16 digits)
$table->enum('payment_type', ['MBWAY', 'PAYPAL', 'IBAN', 'MB', 'VISA']);
$table->string('payment_reference', 30);
// custom data
$table->json('custom')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::drop('coin_purchases');
Schema::drop('coin_transactions');
Schema::drop('coin_transaction_types');
Schema::drop('games');
Schema::drop('matches');
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('deleted_at');
$table->dropColumn('custom');
$table->dropColumn('coins_balance');
$table->dropColumn('photo_avatar_filename');
$table->dropColumn('blocked');
$table->dropColumn('nickname');
$table->dropColumn('type');
});
}
};
+67 -8
View File
@@ -5,21 +5,80 @@ namespace Database\Seeders;
use App\Models\User;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Carbon\Carbon;
class DatabaseSeeder extends Seeder
{
use WithoutModelEvents;
/**
* Seed the application's database.
*/
public static $startDate;
public static $dbInsertBlockSize = 500;
// public static $seedType = "small";
//public static $seedType = "full";
//public static $seedLanguage = "pt_PT";
public static $seedLanguage = "en_US";
public function run(): void
{
// User::factory(10)->create();
$this->command->info("-----------------------------------------------");
$this->command->info("START of database seeder");
$this->command->info("-----------------------------------------------");
User::factory()->create([
'name' => 'Test User',
'email' => '[email protected]',
]);
self::$startDate = Carbon::now()->subMonths(14);
self::$seedLanguage = $this->command->choice('What is the language for users\' names?', ['pt_PT', 'en_US'], 0);
if (DB::getDriverName() === 'sqlite') {
DB::statement('PRAGMA foreign_keys = OFF');
} else {
DB::statement('SET foreign_key_checks=0');
// No permissions to change global setting. Change the session setting only
//DB::statement("SET @@global.time_zone = '+00:00'");
DB::statement("SET time_zone = '+00:00'");
}
DB::table('users')->delete();
DB::table('matches')->delete();
DB::table('games')->delete();
DB::table('coin_purchases')->delete();
DB::table('coin_transactions')->delete();
DB::table('coin_transaction_types')->delete();
if (DB::getDriverName() === 'sqlite') {
DB::statement("DELETE FROM sqlite_sequence WHERE name = 'users'");
DB::statement("DELETE FROM sqlite_sequence WHERE name = 'matches'");
DB::statement("DELETE FROM sqlite_sequence WHERE name = 'games'");
DB::statement("DELETE FROM sqlite_sequence WHERE name = 'coin_purchases'");
DB::statement("DELETE FROM sqlite_sequence WHERE name = 'coin_transactions'");
DB::statement("DELETE FROM sqlite_sequence WHERE name = 'coin_transaction_types'");
} else {
DB::statement('ALTER TABLE users AUTO_INCREMENT = 0');
DB::statement('ALTER TABLE matches AUTO_INCREMENT = 0');
DB::statement('ALTER TABLE games AUTO_INCREMENT = 0');
DB::statement('ALTER TABLE coin_purchases AUTO_INCREMENT = 0');
DB::statement('ALTER TABLE coin_transactions AUTO_INCREMENT = 0');
DB::statement('ALTER TABLE coin_transaction_types AUTO_INCREMENT = 0');
}
$this->command->info("-----------------------------------------------");
$this->call(TransactionTypesSeeder::class);
$this->call(UsersSeeder::class);
$this->call(InitialTransactionsSeeder::class);
$this->call(GamesSeeder::class);
$this->call(GamesTransactionsSeeder::class);
if (DB::getDriverName() === 'sqlite') {
DB::statement('PRAGMA foreign_keys = ON');
} else {
DB::statement('SET foreign_key_checks=1');
}
$this->command->info("-----------------------------------------------");
$this->command->info("END of database seeder");
$this->command->info("-----------------------------------------------");
}
}
+214
View File
@@ -0,0 +1,214 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Carbon\Carbon;
use Illuminate\Container\Attributes\Database;
class GamesSeeder extends Seeder
{
private $ratioStandaloneToMatch = 15;
private function calculateRandomSeconds($filteredCollection)
{
$totalPlayers = $filteredCollection->count() + 1;
return (12 * 60 * 60) / $totalPlayers + rand(0, 2000);
}
private function nextGameDateTime(&$d, $filteredPlayers, $withinSameMatch = false) {
if ($withinSameMatch) {
$deltaSegundos = rand(300, 900);
} else {
$deltaSegundos = $this->calculateRandomSeconds($filteredPlayers);
}
$d->addSeconds($deltaSegundos);
}
public function run(): void
{
$this->command->info("Games seeder - Start");
$start = DB::table('users')->where('type', 'P')->min('created_at');
$allPlayers = DB::table('users')->where('type', 'P')->get();
$sortedPlayers = $allPlayers->sortBy('created_at')->values();
$d = new \Carbon\Carbon($start);
$d = $d->addDay();
$now = \Carbon\Carbon::now();
$this->command->info("Starting to create games");
$games = [];
$matches = [];
$i = 0;
$filteredPlayers = null;
$filteredPlayersIds = null;
$nextCreatedAt = null;
while ($d->lte($now)) {
$i++;
if (($filteredPlayers === null) || ($nextCreatedAt === null) ||
($d->gte($nextCreatedAt))
) {
$filteredPlayers = $allPlayers->filter(function ($value) use ($d) {
return $d->gt($value->created_at);
});
$nextCreatedAtPlayer = $sortedPlayers->first(function ($value) use ($d) {
return $d->lte($value->created_at);
});
$nextCreatedAt = $nextCreatedAtPlayer ? $nextCreatedAtPlayer->created_at : new \Carbon\Carbon('9999-12-31');
$filteredPlayersIds = $filteredPlayers->pluck('id')->toArray();
}
// Only creates games or matches when there are enough players
if ($filteredPlayersIds === null || empty($filteredPlayersIds) || count($filteredPlayersIds) <2) {
$this->nextGameDateTime($d, $filteredPlayers);
continue;
}
$userIdKeys = array_rand($filteredPlayersIds, 2);
$userIDPlayer1 = $filteredPlayersIds[$userIdKeys[0]];
$userIDPlayer2 = $filteredPlayersIds[$userIdKeys[1]];
if ($userIDPlayer1 == $userIDPlayer2) {
$this->nextGameDateTime($d, $filteredPlayers);
continue;
}
$match = null;
if (rand(1, $this->ratioStandaloneToMatch) === 1) {
$match = $this->newMatch($filteredPlayers, $userIDPlayer1, $userIDPlayer2, $d);
$playersMarks = [0, 0];
$playersPoints = [0, 0];
while($playersMarks[0] < 4 && $playersMarks[1] <4) {
$newGame = $this->newGame($filteredPlayers, $match, $userIDPlayer1, $userIDPlayer2, $d);
$games[] = $newGame;
$playersPoints[0] += $newGame['player1_points'];
$playersPoints[1] += $newGame['player2_points'];
if ($newGame['player1_points'] > $newGame['player2_points']) {
if ($newGame['player1_points'] >= 120) {
$playersMarks[0]+= 4;
} elseif ($newGame['player1_points'] >= 91) {
$playersMarks[0] += 2;
} else {
$playersMarks[0]++;
}
} elseif ($newGame['player2_points'] > $newGame['player1_points']) {
if ($newGame['player2_points'] >= 120) {
$playersMarks[1]+= 4;
} elseif ($newGame['player2_points'] >= 91) {
$playersMarks[1] += 2;
} else {
$playersMarks[1]++;
}
}
}
$this->updateMatchWinner($match, $playersMarks[0], $playersMarks[1], $playersPoints[0], $playersPoints[1], $d);
$matches[] = $match;
} else {
$newGame = $this->newGame($filteredPlayers, $match, $userIDPlayer1, $userIDPlayer2, $d);
$games[] = $newGame;
}
if ($i >= DatabaseSeeder::$dbInsertBlockSize) {
if (!empty($matches)) {
DB::table('matches')->insert($matches);
$this->command->info("Saved " . count($matches) . " matches at date " . $d->format('Y-m-d H:i:s'));
}
if (!empty($games)) {
DB::table('games')->insert($games);
$this->command->info("Saved " . count($games) . " games at date " . $d->format('Y-m-d H:i:s'));
}
$i = 0;
$games = [];
$matches = [];
}
//$this->nextGameDateTime($d, $filteredPlayers);
}
if (!empty($matches)) {
DB::table('matches')->insert($matches);
$this->command->info("Saved " . count($matches) . " matches at date " . $d->format('Y-m-d H:i:s'));
}
if (!empty($games)) {
DB::table('games')->insert($games);
$this->command->info("Saved " . count($games) . " games at date " . $d->format('Y-m-d H:i:s'));
}
$this->command->info("Games seeder - End");
}
private $matchID = 0;
private function newMatch($filteredPlayers,$user1, $user2, $d)
{
$this->matchID++;
$this->nextGameDateTime($d, $filteredPlayers);
return [
'id' => $this->matchID,
'type' => random_int(1,2) == 1 ? '3' : '9',
'player1_user_id' => $user1,
'player2_user_id' => $user2,
'winner_user_id' => null,
'loser_user_id' => null,
'status' => 'Ended',
'stake' => random_int(1,4) > 1 ? 3 : random_int(4,100),
'began_at' => $d->copy(),
'ended_at' => null,
'total_time' => null,
'player1_marks' => null,
'player2_marks' => null,
'player1_points' => null,
'player2_points' => null,
'custom' => null
];
}
private function updateMatchWinner(&$match, $player1Marks, $player2Marks, $totalPlayers1, $totalPlayers2, $d)
{
$match['player1_marks'] = $player1Marks;
$match['player2_marks'] = $player2Marks;
$match['player1_points'] = $totalPlayers1;
$match['player2_points'] = $totalPlayers2;
$match['ended_at'] = $d->copy();
$match['total_time'] = $match['began_at']->diffInSeconds($match['ended_at']);
$match['winner_user_id'] = $player1Marks > $player2Marks ? $match['player1_user_id'] : ($player2Marks > $player1Marks ? $match['player2_user_id'] : null);
$match['loser_user_id'] = $player1Marks > $player2Marks ? $match['player2_user_id'] : ($player2Marks > $player1Marks ? $match['player1_user_id'] : null);
}
private $gameID = 0;
private function newGame($filteredPlayers, $match, $user1, $user2, $d)
{
$this->gameID++;
$this->nextGameDateTime($d, $filteredPlayers, $match != null);
$begin_d = $d->copy();
$pointsUser1 = 60;
$pointsUser2 = 60;
// if random == 1 it is a draw
if (random_int(1,30) > 1) {
$pointsUser1 = rand(0, 120);
$pointsUser2 = 120 - $pointsUser1;
}
$duration = random_int(200, 900);
$d->addSeconds($duration);
return [
'id' => $this->gameID,
'type' => $match ? $match['type'] : (random_int(1, 2) == 1 ? '3' : '9'),
'match_id' => $match ? $match['id'] : null,
'player1_user_id' => $user1,
'player2_user_id' => $user2,
'is_draw' => $pointsUser1 == $pointsUser2,
'winner_user_id' => $pointsUser1 > $pointsUser2 ? $user1 : ($pointsUser2 > $pointsUser1 ? $user2 : null),
'loser_user_id' => $pointsUser1 < $pointsUser2 ? $user1 : ($pointsUser2 < $pointsUser1 ? $user2 : null),
'status' => 'Ended',
'began_at' => $begin_d,
'ended_at' => $d->copy(),
'total_time' => $duration,
'player1_points' => $pointsUser1,
'player2_points' => $pointsUser2,
'custom' => null
];
}
}
@@ -0,0 +1,265 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
class GamesTransactionsSeeder extends Seeder
{
private $faker = null;
private $users = null;
public function run(): void
{
$this->command->info("Create game related Transactions");
$this->faker = \Faker\Factory::create(DatabaseSeeder::$seedLanguage);
$this->users = DB::table('users')->orderBy('id', 'asc')->get();
$games = DB::table('games')->whereNull('match_id')->orderBy('began_at', 'asc')->get();
$matches = DB::table('matches')->orderBy('began_at', 'asc')->get();
$coin_purchases = [];
$coin_transactions = [];
$i = 0;
while($games->count() > 0 || $matches->count() > 0) {
$firstGame = $games->first();
$firstMatch = $matches->first();
$isGameFirst = false;
if ($firstGame && $firstMatch) {
$isGameFirst = $firstGame->began_at < $firstMatch->began_at;
} elseif ($firstGame && !$firstMatch) {
$isGameFirst = true;
} elseif (!$firstGame && $firstMatch) {
$isGameFirst = false;
}
if ($isGameFirst) {
$this->handleGame($coin_purchases, $coin_transactions, $firstGame);
$games->shift();
} else {
$this->handleMatch($coin_purchases, $coin_transactions, $firstMatch);
$matches->shift();
}
$i++;
if ($i >= DatabaseSeeder::$dbInsertBlockSize) {
$this->command->info("Block of coin purchases and transactions");
if (!empty($coin_transactions)) {
DB::table('coin_transactions')->insert($coin_transactions);
$this->command->info("Saved " . count($coin_transactions) . " coin transactions");
}
if (!empty($coin_purchases)) {
DB::table('coin_purchases')->insert($coin_purchases);
$this->command->info("Saved " . count($coin_purchases) . " coin purchases");
}
$i = 0;
$coin_purchases = [];
$coin_transactions = [];
}
}
$this->command->info("Last block of coin purchases and transactions");
if (!empty($coin_transactions)) {
DB::table('coin_transactions')->insert($coin_transactions);
$this->command->info("Saved " . count($coin_transactions) . " coin transactions");
}
if (!empty($coin_purchases)) {
DB::table('coin_purchases')->insert($coin_purchases);
$this->command->info("Saved " . count($coin_purchases) . " coin purchases");
}
$this->updateAllUsersBalance();
$this->command->info("Game related Transactions Created Successfully.");
}
private function getTimeStampFromDB($strDateFromDB, $minusOneSecond = false)
{
if ($minusOneSecond) {
return date("Y-m-d H:i:s", strtotime($strDateFromDB . " - 1 second"));
}
return date("Y-m-d H:i:s", strtotime($strDateFromDB));
}
private function getRandomPayment($user, &$type, &$ref): void
{
$type = $this->faker->randomElement(['MBWAY', 'PAYPAL', 'IBAN', 'MB', 'VISA']);
switch ($type) {
case 'MBWAY':
$ref = rand(910000000, 999999999);
break;
case 'PAYPAL':
$ref = $user->email;
break;
case 'IBAN':
$ref = 'PT' . rand(5000000000000, 9999999999999) . rand(1000000000, 9999999999);
break;
case 'MB':
$ref = rand(10000, 99999) . '-' . rand(100000000, 999999999);
break;
case 'VISA':
$ref = rand(40000000, 49999999) . rand(10000000, 99999999);
break;
}
}
private $purchaseID = null;
private function addPurchase(&$coin_purchases, &$coin_transactions, &$user, $time, $eurosAmount)
{
$this->purchaseID++;
$coins = $eurosAmount * 10;
$newCoinTransaction = $this->addCoinTransaction($coin_transactions, $user, $coins, 2, $time, null, null);
$this->getRandomPayment($user, $paymentType, $paymentRef);
$coin_purchases[] = [
'id' => $this->purchaseID,
'purchase_datetime' => $time,
'user_id' => $user->id,
'coin_transaction_id' => $newCoinTransaction['id'],
'euros' => $eurosAmount,
'payment_type' => $paymentType,
'payment_reference' => $paymentRef,
'custom' => null,
];
}
private $transactionID = 0;
private function addCoinTransaction(&$coin_transactions, &$user, $coins, $type, $time, $game_id = null, $match_id = null)
{
if ($this->transactionID == null) {
$this->transactionID = DB::table('coin_transactions')->max('id');
}
// Types of coin transactions:
// 1 - Bonus
// 2 - Coin purchase
// 3 - Game fee - requires game_id
// 4 - Match stake - requires match_id
// 5 - Game payout - requires game_id
// 6 - Match payout - requires match_id
if(($type == 3 || $type == 5) && $game_id === null) {
throw new \Exception("Game ID is required for coin transaction type $type");
}
if(($type == 4 || $type == 6) && $match_id === null) {
throw new \Exception("Match ID is required for coin transaction type $type");
}
$this->transactionID++;
$coins = abs($coins);
if (in_array($type, [3, 4])) {
$coins *= -1;
}
$newCoinTransaction = [
'id' => $this->transactionID,
'transaction_datetime' => $time,
'user_id' => $user->id,
'match_id' => $match_id, // nullable
'game_id' => $game_id, // nullable
'coin_transaction_type_id' => $type,
'coins' => $coins,
'custom' => null,
];
$coin_transactions[] = $newCoinTransaction;
$user->coins_balance += $coins;
return $newCoinTransaction;
}
private function calculateGamePayouts($game)
{
$payouts = [0, 0];
if ($game->player1_points == $game->player2_points) {
$payouts = [1, 1];
} elseif ($game->player1_points == 120) {
$payouts = [6, 0];
} elseif ($game->player2_points == 120) {
$payouts = [0, 6];
} elseif ($game->player1_points >= 91) {
$payouts = [4, 0];
} elseif ($game->player2_points >= 91) {
$payouts = [0, 4];
} elseif ($game->player1_points >= 61) {
$payouts = [3, 0];
} elseif ($game->player2_points >= 61) {
$payouts = [0, 3];
} else { // this condition should never be reached
$payouts = [0, 0];
}
return $payouts;
}
private function calculateMatchPayouts($match)
{
$payouts = [0, 0];
if ($match->winner_user_id == $match->player1_user_id) {
$payouts = [$match->stake * 2 - 1, 0];
} elseif ($match->winner_user_id == $match->player2_user_id) {
$payouts = [0, $match->stake * 2 - 1];
}
return $payouts;
}
private function handleGame(&$coin_purchases, &$coin_transactions, $game)
{
$player1 = $this->users->where('id', $game->player1_user_id)->first();
$player2 = $this->users->where('id', $game->player2_user_id)->first();
// not enough coins to play - must purchase before the game
$timeOneSecondBefore = $this->getTimeStampFromDB($game->began_at, true);
if ($player1->coins_balance < 2) {
$this->addPurchase($coin_purchases, $coin_transactions, $player1, $timeOneSecondBefore, random_int(1, 10));
}
// not enough coins to play - must purchase before the game
if ($player2->coins_balance < 2) {
$this->addPurchase($coin_purchases, $coin_transactions, $player2, $timeOneSecondBefore, random_int(1, 10));
}
$this->addCoinTransaction($coin_transactions, $player1, -2, 3, $this->getTimeStampFromDB($game->began_at, false), $game->id);
$this->addCoinTransaction($coin_transactions, $player2, -2, 3, $this->getTimeStampFromDB($game->began_at, false), $game->id);
$payouts = $this->calculateGamePayouts($game);
if ($payouts[0] > 0) {
$this->addCoinTransaction($coin_transactions, $player1, $payouts[0], 5, $this->getTimeStampFromDB($game->ended_at, false), $game->id);
}
if ($payouts[1] > 0) {
$this->addCoinTransaction($coin_transactions, $player2, $payouts[1], 5, $this->getTimeStampFromDB($game->ended_at, false), $game->id);
}
}
private function handleMatch(&$coin_purchases, &$coin_transactions, $match)
{
$player1 = $this->users->where('id', $match->player1_user_id)->first();
$player2 = $this->users->where('id', $match->player2_user_id)->first();
// not enough coins to play - must purchase before the match
$timeOneSecondBefore = $this->getTimeStampFromDB($match->began_at, true);
if ($player1->coins_balance < $match->stake) {
$requiredCoins = $match->stake - $player1->coins_balance;
$euros = ceil(($requiredCoins + 3) / 10);
$this->addPurchase($coin_purchases, $coin_transactions, $player1, $timeOneSecondBefore, $euros);
}
// not enough coins to play - must purchase before the match
if ($player2->coins_balance < $match->stake) {
$requiredCoins = $match->stake - $player2->coins_balance;
$euros = ceil(($requiredCoins + 3) / 10);
$this->addPurchase($coin_purchases, $coin_transactions, $player2, $timeOneSecondBefore, $euros);
}
$this->addCoinTransaction($coin_transactions, $player1, $match->stake * -1, 4, $this->getTimeStampFromDB($match->began_at, false), null, $match->id);
$this->addCoinTransaction($coin_transactions, $player2, $match->stake * -1, 4, $this->getTimeStampFromDB($match->began_at, false), null, $match->id);
$payouts = $this->calculateMatchPayouts($match);
if ($payouts[0] > 0) {
$this->addCoinTransaction($coin_transactions, $player1, $payouts[0], 6, $this->getTimeStampFromDB($match->ended_at, false), null, $match->id);
}
if ($payouts[1] > 0) {
$this->addCoinTransaction($coin_transactions, $player2, $payouts[1], 6, $this->getTimeStampFromDB($match->ended_at, false), null, $match->id);
}
}
private function updateAllUsersBalance()
{
$this->command->info("Update users balances...");
foreach ($this->users as $user) {
DB::table('users')->where('id', $user->id)->update(['coins_balance' => $user->coins_balance]);
}
$this->command->info("All users balances updated.");
}
}
@@ -0,0 +1,62 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use stdClass;
class InitialTransactionsSeeder extends Seeder
{
private $allPlayers;
private $sizeOfSaveBlock = 200;
private $valueOfInitialBonus = 10;
public function run(): void
{
$this->allPlayers = DB::table('users')->where('type', 'P')->get();
$this->command->info("Create initial Bonuses transactions");
$i = $this->sizeOfSaveBlock;
$dataBlock = [];
foreach ($this->allPlayers as $player) {
if ($i <= 0) {
$this->saveDataBlock($dataBlock);
$i = $this->sizeOfSaveBlock;
$dataBlock = [];
}
$dataBlock[] = $this->createBonusTransaction($player);
$i--;
}
if (!empty($dataBlock)) {
$this->saveDataBlock($dataBlock);
}
$this->updateAllPlayerBalances();
}
private function saveDataBlock($dataBlock): void
{
$size = count($dataBlock);
DB::table('coin_transactions')->insert($dataBlock);
$this->command->info("Saved $size initial bonuses transactions");
}
private function createBonusTransaction($player): array
{
return [
'transaction_datetime' => $player->created_at,
'user_id' => $player->id,
'match_id' => null,
'game_id' => null,
'coin_transaction_type_id' => 1,
'coins' => $this->valueOfInitialBonus,
'custom' => null,
];
}
private function updateAllPlayerBalances(): void
{
DB::table('users')->where('type', 'P')->update(['coins_balance' => $this->valueOfInitialBonus]);
$this->command->info("Updated all players initial Balance");
}
}
+149
View File
@@ -0,0 +1,149 @@
<?php
namespace Database\Seeders;
use Illuminate\Support\Facades\File;
trait SeederUtils
{
private static $used_emails = [];
public function randomString(int $length = 10): string
{
return substr(str_shuffle('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'), 0, $length);
}
public function integerAsStringWithPadding(int $num, int $length = 10): string
{
return str_pad((string) $num, $length, '0', STR_PAD_LEFT);
}
public function randomIntegerAsStringWithPadding(int $min = 0, int $max = 100, int $length = 10): string
{
return $this->integerAsStringWithPadding($this->mt_rand($min, $max), $length);
}
public function randomDecimal(float $min, float $max, int $decimals = 2): float
{
$factor = pow(10, $decimals);
return mt_rand($min * $factor, $max * $factor) / $factor;
}
public function getFileNameFromString($str, $extension)
{
return str_replace(' ', '_', strtolower($this->stripAccents($str))) . ".$extension";
}
public function stripAccents($stripAccents)
{
$from = 'àáâãäçèéêëìíîïñòóôõöùúûüýÿÀÁÂÃÄÇÈÉÊËÌÍÎÏÑÒÓÔÕÖÙÚÛÜÝ';
$to = 'aaaaaceeeeiiiinooooouuuuyyAAAAACEEEEIIIINOOOOOUUUUY';
$keys = array();
$values = array();
preg_match_all('/./u', $from, $keys);
preg_match_all('/./u', $to, $values);
$mapping = array_combine($keys[0], $values[0]);
return strtr($stripAccents, $mapping);
}
public function strtr_utf8($str, $from, $to)
{
$keys = array();
$values = array();
preg_match_all('/./u', $from, $keys);
preg_match_all('/./u', $to, $values);
$mapping = array_combine($keys[0], $values[0]);
return strtr($str, $mapping);
}
public function randomName($faker, &$gender, &$fullname, &$email, $allowRepeated = false)
{
$gender = $gender ?? $faker->randomElement(['male', 'female']);
$firstname = $faker->firstName($gender);
$lastname = $faker->lastName();
$secondname = mt_rand(1, 3) == 2 ? "" : " " . $faker->firstName($gender);
$number_middlenames = $faker->randomElement([0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 3]);
$middlenames = "";
for ($i = 0; $i < $number_middlenames; $i++) {
$middlenames .= " " . $faker->lastName();
}
$fullname = $firstname . $secondname . $middlenames . " " . $lastname;
$email = strtolower($this->stripAccents($firstname) . "." . $this->stripAccents($lastname) . "@mail.pt");
if (!$allowRepeated) {
$i = 2;
while (in_array($email, self::$used_emails)) {
$email = strtolower($this->stripAccents($firstname) . "." . $this->stripAccents($lastname) . "." . $i ."@mail.pt");
$i++;
if ($i > 10) {
$this->command->error("Repeated email - not possible to create a unique email for $fullname!");
exit(1);
}
}
}
self::$used_emails[] = $email;
$gender = $gender == 'male' ? 'M' : 'F';
}
public function ramdomPaymentMethod($email, &$paymentType, &$paymentReference)
{
$paymentType = $this->faker->randomElement(['Visa', 'PayPal', 'MB WAY']);
$paymentReference = match ($paymentType) {
'Visa' => mt_rand(4, 6) .
$this->faker->randomNumber($nbDigits = 8, $strict = true) .
$this->faker->randomNumber($nbDigits = 7, $strict = true),
'PayPal' => $email,
'MB WAY' => '9' . $this->faker->randomNumber($nbDigits = 8, $strict = true)
};
}
public function cleanStorageFolder($folder, $public = true)
{
$storagePath = $public ? storage_path("app/public/$folder") : storage_path("app/private/$folder");
if (File::exists($storagePath)) {
File::deleteDirectory($storagePath);
}
if (!File::exists(storage_path("app"))) {
File::makeDirectory(storage_path("app"));
}
if ($public) {
if (!File::exists(storage_path("app/public"))) {
File::makeDirectory(storage_path("app/public"));
}
} else {
if (!File::exists(storage_path("app/private"))) {
File::makeDirectory(storage_path("app/private"));
}
}
File::makeDirectory($storagePath);
}
public function copyFileToStorage($originalFolder, $originalFileName, $folder, $id = null, $public = true)
{
$originalFullName = database_path("seeders/$originalFolder") . '/' . $originalFileName;
$storagePath = $public ? storage_path("app/public/$folder") : storage_path("app/private/$folder");
$prefix = $id ? $this->integerAsStringWithPadding($id, 5) . '_' : '';
$ext = pathinfo($originalFullName, PATHINFO_EXTENSION);
$newFileName = $prefix . $this->randomString(10) . '.' . $ext;
if (!File::exists($storagePath)) {
File::makeDirectory($storagePath);
}
if (File::exists($originalFullName)) {
File::copy($originalFullName, "$storagePath/$newFileName");
return $newFileName;
}
return null;
}
public function directCopyFileToStorage($originalFolder, $originalFileName, $folder, $public = true)
{
$originalFullName = database_path("seeders/$originalFolder") . '/' . $originalFileName;
$storagePath = $public ? storage_path("app/public/$folder") : storage_path("app/private/$folder");
if (!File::exists($storagePath)) {
File::makeDirectory($storagePath);
}
File::copy($originalFullName, "$storagePath/$originalFileName");
return $originalFileName;
}
}
@@ -0,0 +1,30 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
class TransactionTypesSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
$this->command->line("Creating Coin Transaction Types.");
// Add default coin_transaction_types
DB::table('coin_transaction_types')->insert([
['id' => 1, 'name' => 'Bonus', 'type' => 'C'], // id = 1
['id' => 2, 'name' => 'Coin purchase', 'type' => 'C'], // id = 2
['id' => 3, 'name' => 'Game fee', 'type' => 'D'], // id = 3
['id' => 4, 'name' => 'Match stake', 'type' => 'D'], // id = 4
['id' => 5, 'name' => 'Game payout', 'type' => 'C'], // id = 5
['id' => 6, 'name' => 'Match payout', 'type' => 'C'], // id = 6
]);
$this->command->line("Coin Transaction Types Created Successfully.");
}
}
+173
View File
@@ -0,0 +1,173 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Faker\Factory;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use stdClass;
class UsersSeeder extends Seeder
{
use SeederUtils;
private $faker = null;
private $files_M = [];
private $files_F = [];
public static $hashPasword = "";
public static $dbUsers = null;
public static $fixedUsers = [
['type' => 'A', 'name' => 'First Administrator', 'email' => '[email protected]', 'gender' => 'M', 'softdelete' => false],
['type' => 'A', 'name' => 'Second Administrator', 'email' => '[email protected]', 'gender' => 'F', 'softdelete' => false],
['type' => 'A', 'name' => 'Third Administrator', 'email' => '[email protected]', 'gender' => 'M', 'softdelete' => false],
['type' => 'A', 'name' => 'Forth Administrator', 'email' => '[email protected]', 'gender' => 'M', 'softdelete' => true],
['type' => 'P', 'name' => 'Player A', 'email' => '[email protected]', 'gender' => 'M', 'softdelete' => false],
['type' => 'P', 'name' => 'Player B', 'email' => '[email protected]', 'gender' => 'M', 'softdelete' => false],
['type' => 'P', 'name' => 'Player C', 'email' => '[email protected]', 'gender' => 'F', 'softdelete' => false],
['type' => 'P', 'name' => 'Player D', 'email' => '[email protected]', 'gender' => 'M', 'softdelete' => true],
['type' => 'P', 'name' => 'Player E', 'email' => '[email protected]', 'gender' => 'F', 'softdelete' => false],
['type' => 'P', 'name' => 'Player F', 'email' => '[email protected]', 'gender' => 'M', 'softdelete' => true],
];
public static $userTypes = [
'A' => 10,
'P' => 500];
public function run(): void
{
$this->command->line("Creating Users.");
self::$hashPasword = bcrypt('123');
//$this->faker = Factory::create('pt_PT');
$this->faker = Factory::create(DatabaseSeeder::$seedLanguage);
$this->cleanStorageFolder('photos_avatars');
$this->addUsersToDatabase();
$this->addPhotoFiles();
DB::table('users')->where('type', 'A')->update(['nickname' => null]);
self::$dbUsers = DB::table('users')->orderBy('id')->get();
$this->command->line("Users Created Successfully.");
}
private function addUsersToDatabase()
{
$this->command->line("Adding users to the database");
$usersAdded = Self::$fixedUsers;
foreach (self::$userTypes as $userType => $totalUsers) {
for ($i = 0; $i < $totalUsers; $i++) {
$gender = null;
$name = null;
$email = null;
$this->randomName($this->faker, $gender, $name, $email, false);
$usersAdded[] = [
'type' => $userType,
'name' => $name,
'email' => $email,
'gender' => $gender,
'softdelete' => random_int(1, 20) == 1,
];
}
}
$createdDate = DatabaseSeeder::$startDate->copy()->addMinutes(mt_rand(20000, 100000));
foreach ($usersAdded as $key => $user) {
$usersAdded[$key]['password'] = self::$hashPasword;
$createdDate = $createdDate->copy()->addMinutes(mt_rand(10, 60));
$usersAdded[$key]['created_at'] = $createdDate;
$usersAdded[$key]['email_verified_at'] = $createdDate->copy()->addMinutes(mt_rand(1, 9));
if (random_int(1, 7) > 1) {
$usersAdded[$key]['updated_at'] = $createdDate->copy()->addMinutes(mt_rand(10, 10000));
} else {
$usersAdded[$key]['updated_at'] = $createdDate;
}
$usersAdded[$key]['nickname'] = ($user['gender'] == 'M' ? 'Mickey' : 'Minnie') . ($key + 1) ;
$usersAdded[$key]['blocked'] = false;
$usersAdded[$key]['photo_avatar_filename'] = null;
$usersAdded[$key]['coins_balance'] = 0;
$usersAdded[$key]['deleted_at'] = null;
if ($usersAdded[$key]['softdelete']) {
$usersAdded[$key]['deleted_at'] = $createdDate->copy()->addMinutes(mt_rand(10001, 100000));
}
}
$arrayToStore = [];
foreach ($usersAdded as $user) {
unset($user['gender']);
unset($user['softdelete']);
$arrayToStore[] = $user;
if (count($arrayToStore) >= DatabaseSeeder::$dbInsertBlockSize) {
DB::table('users')->insert($arrayToStore);
$this->command->line("Created " . count($arrayToStore) . " users");
$arrayToStore = [];
}
}
if (count($arrayToStore) >= 1) {
DB::table('users')->insert($arrayToStore);
$this->command->line("Created " . count($arrayToStore) . " users");
}
$this->command->line("Total users created: " . DB::table('users')->count());
self::$dbUsers = DB::table('users')->get();
print_r(self::$dbUsers);
}
private function addPhotoFiles()
{
$this->command->line("Copying users' photos");
$this->fillPhotoFilesNames();
$total = count($this->files_M) + count($this->files_F);
$sortedUsers = self::$dbUsers->sortBy(function (stdClass $user) {
if ($user->id <= 10 ) {
return $user->id;
}
return match($user->type) {
'A' => 20 + $user->id,
'P' => 1000 + $user->id,
default => ($user->id < 50) ? 2000 + $user->id : 3000 + mt_rand(0, 100000),
};
});
$i = 0;
foreach($sortedUsers as $user) {
$originalFilename = str_starts_with($user->nickname, 'Mickey') ? array_shift($this->files_M) : array_shift($this->files_F);
if (!$originalFilename) {
if ((count($this->files_M) == 0) && (count($this->files_F) == 0))
break;
}
if ($originalFilename) {
$originalFilename = basename($originalFilename);
$newFileName = $this->copyFileToStorage('photos', $originalFilename, 'photos_avatars', $user->id);
$user->photo_avatar_filename = $newFileName;
DB::table('users')->where('id', $user->id)->update(['photo_avatar_filename' => $user->photo_avatar_filename]);
$i++;
if ($i % 10 == 0) {
$this->command->line("User photo $i/$total copied");
}
}
}
$this->command->line("Total of $total user's photos were copied!");
$this->directCopyFileToStorage('photos', 'anonymous.png', 'photos_avatars');
$this->command->line("Image for user with no associated photo was copied!");
}
private function fillPhotoFilesNames()
{
$allFiles = collect(File::files(database_path('seeders/photos')));
foreach ($allFiles as $f) {
if (strpos($f->getPathname(), 'm_')) {
$this->files_M[] = $f->getPathname();
} else if (strpos($f->getPathname(), 'w_')) {
$this->files_F[] = $f->getPathname();
}
}
shuffle($this->files_M);
shuffle($this->files_F);
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 574 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 564 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 549 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 535 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 601 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 543 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 594 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 536 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 557 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 550 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 482 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 543 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 480 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 558 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 587 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 536 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 512 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 530 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 507 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 535 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 497 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 558 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 524 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 560 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 581 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 593 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 588 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 571 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 542 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 591 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 521 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 529 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 584 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 568 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 512 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 537 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 555 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 583 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 531 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 549 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 579 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 452 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 563 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 490 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 593 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 503 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 577 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 511 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 592 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 530 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 658 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 587 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 486 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 499 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 546 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 611 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 555 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 559 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 504 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 561 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 624 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 623 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 468 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 535 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 519 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 514 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 626 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 495 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 604 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 631 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 574 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 529 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 598 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 564 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 570 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 476 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 557 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 535 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 558 KiB

Some files were not shown because too many files have changed in this diff Show More