From 5831068fe117712e492745c2aac106cdc32d65f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Fran=C3=A7oys?= Date: Mon, 2 Sep 2024 13:18:54 +0200 Subject: [PATCH 01/94] added info logs for computed field --- lib/equal/orm/ObjectManager.class.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/equal/orm/ObjectManager.class.php b/lib/equal/orm/ObjectManager.class.php index 246f52d4c..4a6cfe79e 100644 --- a/lib/equal/orm/ObjectManager.class.php +++ b/lib/equal/orm/ObjectManager.class.php @@ -686,6 +686,7 @@ private function load($class, $ids, $fields, $lang) { } if(count($missing_ids)) { if(isset($schema[$field]['function'])) { + trigger_error("ORM::computing 'function' for '$field' of class '$class'", QN_REPORT_INFO); $res = $this->callonce($class, $schema[$field]['function'], $missing_ids, [], $lang); if($res > 0) { foreach($missing_ids as $oid) { @@ -701,6 +702,7 @@ private function load($class, $ids, $fields, $lang) { } elseif(isset($schema[$field]['relation']) && is_array($schema[$field]['relation'])) { try { + trigger_error("ORM::computing 'relation' for '$field' of class '$class'", QN_REPORT_INFO); $res = $class::ids($ids)->read($schema[$field]['relation'])->get(true); foreach($res as $elem) { $id = $elem['id']; From bf1878ce3519c0ccdf5ccb165ac0510b9af04474 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Fran=C3=A7oys?= Date: Mon, 2 Sep 2024 13:30:35 +0200 Subject: [PATCH 02/94] fixed 'relation' computed field with final target as non-array --- lib/equal/orm/ObjectManager.class.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/equal/orm/ObjectManager.class.php b/lib/equal/orm/ObjectManager.class.php index 4a6cfe79e..a19c3464b 100644 --- a/lib/equal/orm/ObjectManager.class.php +++ b/lib/equal/orm/ObjectManager.class.php @@ -710,12 +710,12 @@ private function load($class, $ids, $fields, $lang) { while(is_array($relation)) { $target = array_key_first($relation); if(is_numeric($target)) { - $om->cache[$table_name][$id][$lang][$field] = $elem[$relation[$target]]; + $om->cache[$table_name][$id][$lang][$field] = $elem[$relation[$target]] ?? null; break; } else { - $elem = $elem[$target]; - $relation = $relation[$target]; + $elem = $elem[$target] ?? null; + $relation = isset($relation[$target]) ? (array) $relation[$target] : null; } } } From 7fa1727adeda859101f4afacda0a9c5b40d72e75 Mon Sep 17 00:00:00 2001 From: Lucas Laurent Date: Wed, 4 Sep 2024 15:45:14 +0200 Subject: [PATCH 03/94] Add DataGenerator to create random data + Add value generation to usages --- lib/equal/data/DataGenerator.class.php | 492 +++++++++++++++++++ lib/equal/orm/usages/Usage.class.php | 4 + lib/equal/orm/usages/UsageAmount.class.php | 13 +- lib/equal/orm/usages/UsageArray.class.php | 4 + lib/equal/orm/usages/UsageCountry.class.php | 26 +- lib/equal/orm/usages/UsageCurrency.class.php | 18 +- lib/equal/orm/usages/UsageDate.class.php | 26 + lib/equal/orm/usages/UsageEmail.class.php | 6 + lib/equal/orm/usages/UsageLanguage.class.php | 14 +- lib/equal/orm/usages/UsageNumber.class.php | 20 +- lib/equal/orm/usages/UsagePassword.class.php | 11 + lib/equal/orm/usages/UsagePhone.class.php | 6 + lib/equal/orm/usages/UsageText.class.php | 6 + lib/equal/orm/usages/UsageTime.php | 7 +- lib/equal/orm/usages/UsageUri.class.php | 20 + 15 files changed, 660 insertions(+), 13 deletions(-) create mode 100644 lib/equal/data/DataGenerator.class.php diff --git a/lib/equal/data/DataGenerator.class.php b/lib/equal/data/DataGenerator.class.php new file mode 100644 index 000000000..36e1d42fe --- /dev/null +++ b/lib/equal/data/DataGenerator.class.php @@ -0,0 +1,492 @@ + + Some Rights Reserved, Cedric Francoys, 2010-2024 + Licensed under GNU LGPL 3 license +*/ + +namespace equal\data; + +class DataGenerator { + + public static function plainText($max_length = 255): string { + $words = [ + 'lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', + 'adipiscing', 'elit', 'sed', 'do', 'eiusmod', 'tempor', + 'incididunt', 'ut', 'labore', 'et', 'dolore', 'magna', + 'aliqua', 'ut', 'enim', 'ad', 'minim', 'veniam', 'quis', + 'nostrud', 'exercitation', 'ullamco', 'laboris', 'nisi', + 'ut', 'aliquip', 'ex', 'ea', 'commodo', 'consequat' + ]; + + $generate_sentence = function() use ($words) { + $sentence_length = mt_rand(6, 12); + $sentence = []; + for ($i = 0; $i < $sentence_length; $i++) { + $sentence[] = $words[array_rand($words)]; + } + return ucfirst(implode(' ', $sentence)) . '.'; + }; + + $random_length = mt_rand(0, $max_length); + $random_text = ''; + + while (strlen($random_text) < $random_length) { + $paragraph_length = mt_rand(3, 7); + $paragraph = ''; + for ($i = 0; $i < $paragraph_length; $i++) { + $paragraph .= $generate_sentence() . ' '; + } + $random_text .= trim($paragraph) . "\n\n"; + } + + return trim(substr($random_text, 0, $random_length)); + } + + public static function boolean(): bool { + return boolval(mt_rand(0, 1)); + } + + public static function integer(int $length): int { + $min = (pow(10, $length) - 1) * -1; + $max = pow(10, $length) - 1; + + return mt_rand($min, $max); + } + + public static function realNumber(int $precision, int $scale): float { + $max_int_part = pow(10, $precision) - 1; + $min_int_part = -$max_int_part; + + $int_part = mt_rand($min_int_part, $max_int_part); + + $fractional_part = mt_rand(0, pow(10, $scale) - 1) / pow(10, $scale); + + $random_float = $int_part + $fractional_part; + + return round($random_float, $scale); + } + + public static function hexadecimal(int $length): string { + $num_bytes = ceil($length / 2); + $random_bytes = random_bytes($num_bytes); + $hexadecimal_string = bin2hex($random_bytes); + + return substr($hexadecimal_string, 0, $length); + } + + public static function email(): string { + $domains = ['example.com', 'test.com', 'demo.com', 'sample.org', 'mywebsite.net']; + + $characters = '0123456789abcdefghijklmnopqrstuvwxyz'; + $username_length = mt_rand(5, 10); + $username = ''; + + for ($i = 0; $i < $username_length; $i++) { + $username .= $characters[mt_rand(0, strlen($characters) - 1)]; + } + + $domain = $domains[array_rand($domains)]; + + return $username . '@' . $domain; + } + + public static function phoneNumberE164(): string { + $country_codes = [ + '+1', '+7', '+27', '+31', '+32', '+33', '+34', '+352', '+39', '+44', '+46', + '+47', '+48', '+49', '+55', '+61', '+64', '+81', '+86', '+90', '+91', '+972' + ]; + + $country_code = $country_codes[array_rand($country_codes)]; + + $number_length = 15 - strlen($country_code); + $number = ''; + for ($i = 0; $i < $number_length; $i++) { + $number .= mt_rand(0, 9); + } + + return $country_code . $number; + } + + public static function time(): string { + $hours = str_pad(mt_rand(0, 23), 2, '0', STR_PAD_LEFT); + $minutes = str_pad(mt_rand(0, 59), 2, '0', STR_PAD_LEFT); + $seconds = str_pad(mt_rand(0, 59), 2, '0', STR_PAD_LEFT); + + return $hours . ':' . $minutes . ':' . $seconds; + } + + public static function relativeUrl(): string { + $depth = mt_rand(1, 5); + + $generateRandomString = function($length) { + $characters = 'abcdefghijklmnopqrstuvwxyz0123456789'; + $random_string = ''; + for ($i = 0; $i < $length; $i++) { + $random_string .= $characters[mt_rand(0, strlen($characters) - 1)]; + } + return $random_string; + }; + + $url_path = ''; + for ($i = 0; $i < $depth; $i++) { + $url_path .= '/' . $generateRandomString(6); + } + + return $url_path; + } + + public static function url(): string { + $protocols = ['http', 'https', 'ldap', 'dns', 'ftp']; + $protocol = $protocols[array_rand($protocols)]; + + $domain_length = mt_rand(3, 10); + $path_depth = mt_rand(0, 5); + + // Helper function to generate a random string of given length + $generateRandomString = function($length) { + $characters = 'abcdefghijklmnopqrstuvwxyz0123456789'; + $random_string = ''; + for ($i = 0; $i < $length; $i++) { + $random_string .= $characters[random_int(0, strlen($characters) - 1)]; + } + return $random_string; + }; + + // Generate domain + $domain = $generateRandomString($domain_length) . '.' . $generateRandomString(3); // e.g., 'example.com' + + // Generate path + $path = ''; + for ($i = 0; $i < $path_depth; $i++) { + $path .= '/' . $generateRandomString(6); + } + + return $protocol . '://' . $domain . $path; + } + + public static function urlTel(): string { + $generateRandomNumber = function($length) { + $number = ''; + for ($i = 0; $i < $length; $i++) { + $number .= mt_rand(0, 9); + } + return $number; + }; + + $phoneNumber = $generateRandomNumber(10); + + return 'tel:' . '+32' . $phoneNumber; + } + + public static function urlMailto(): string { + return 'mailto:' . self::email(); + } + + public static function iban(): string { + $account_number_lengths = [ + 'DE' => 10, + 'GB' => 10, + 'FR' => 11, + 'ES' => 12, + 'IT' => 12, + 'NL' => 10, + 'BE' => 9 + ]; + + $bank_code_lengths = [ + 'DE' => 8, + 'GB' => 6, + 'FR' => 5, + 'ES' => 4, + 'IT' => 5, + 'NL' => 4, + 'BE' => 3 + ]; + + $bank_codes = array_keys($bank_code_lengths); + $country_code = $bank_codes[array_rand($bank_codes)]; + $bank_code_length = $bank_code_lengths[$country_code]; + + $generateRandomNumericString = function($length) { + $number = ''; + for ($i = 0; $i < $length; $i++) { + $number .= mt_rand(0, 9); + } + return $number; + }; + + $bank_code = $generateRandomNumericString($bank_code_length); + $account_number_length = $account_number_lengths[$country_code]; + $account_number = $generateRandomNumericString($account_number_length); + + $iban_base = $country_code . '00' . $bank_code . $account_number; + + $iban_numeric = ''; + foreach (str_split($iban_base) as $char) { + if (ctype_alpha($char)) { + $iban_numeric .= ord($char) - 55; + } else { + $iban_numeric .= $char; + } + } + + $mod97 = bcmod($iban_numeric, '97'); + $check_digits = str_pad(98 - $mod97, 2, '0', STR_PAD_LEFT); + + return $country_code . $check_digits . $bank_code . $account_number; + } + + public static function ean13(): string { + $ean12 = ''; + for ($i = 0; $i < 12; $i++) { + $ean12 .= mt_rand(0, 9); + } + + $sum = 0; + for ($i = 0; $i < 12; $i++) { + $digit = (int)$ean12[$i]; + if ($i % 2 === 0) { + $sum += $digit; + } else { + $sum += $digit * 3; + } + } + + $check_digit = (10 - ($sum % 10)) % 10; + + return $ean12 . $check_digit; + } + + public static function username(): string { + $usernames = [ + 'user1234', 'coolUser88', 'jane_doe22', 'johnnyBravo', 'theRealMike', + 'superstar7', 'gameMaster1', 'techGuru42', 'quickSilver', 'happyCamper', + 'blueSky5', 'codingWizard', 'magicMikey', 'fastTrack99', 'misterX', + 'adventureSeeker', 'pixelPioneer', 'ninjaWarrior', 'starGazer', 'drSmart', + 'boldExplorer', 'zenMaster', 'risingStar', 'rocketRider', 'digitalNomad', + 'echoEcho', 'nightOwl21', 'lightSpeed', 'trueBeliever', 'cyberHawk', + 'galacticHero', 'luckyCharm', 'urbanVibes', 'silentStorm', 'wildWanderer', + 'moonWalker', 'brightStar', 'vividDreamer', 'vortexVoyager', 'infiniteLoop', + 'horizonChaser', 'quickSilverFox', 'shadowKnight', 'dataMaster', 'epicQuest', + 'cosmicDancer', 'virtualVictor', 'alphaBravo', 'gammaRay', 'quantumLeap', + 'alphaWolf', 'digitalDynamo', 'codeNinja', 'retroRider', 'futureFreak', + 'hyperLink', 'wizardKing', 'neonNinja', 'techTitan', 'starshipPilot', + 'legendaryHero', 'phantomShadow', 'urbanLegend', 'novaStar', 'daringDiva', + 'trailBlazer', 'cyberChampion', 'epicGamer', 'stellarScribe', 'stormChaser', + 'lunarExplorer', 'plasmaBolt', 'infinityEdge', 'quantumQuest', 'stellarVoyager' + ]; + + return $usernames[array_rand($usernames)]; + } + + public static function firstname(): string { + $firstnames = [ + 'John', 'Jane', 'Michael', 'Emily', 'Robert', 'Jessica', 'David', 'Sarah', + 'James', 'Laura', 'Daniel', 'Sophia', 'Matthew', 'Olivia', 'Andrew', 'Isabella', + 'William', 'Mia', 'Joseph', 'Charlotte', 'Charles', 'Amelia', 'Thomas', 'Harper', + 'Christopher', 'Evelyn', 'Benjamin', 'Abigail', 'Samuel', 'Ella', 'Henry', 'Avery', + 'Lucas', 'Sofia', 'Jack', 'Grace', 'Jackson', 'Chloe', 'Ethan', 'Zoe', 'Alexander', + 'Lily', 'Ryan', 'Hannah', 'Nathan', 'Scarlett', 'Gabriel', 'Aria', 'Carter', 'Mila', + 'Isaac', 'Ella', 'Luke', 'Madison', 'Owen', 'Penelope', 'Caleb', 'Riley', 'Aiden', + 'Samantha', 'Dylan', 'Eleanor', 'Joshua', 'Layla', 'Mason', 'Nora', 'Logan', 'Lila', + 'Eli', 'Hazel', 'Cameron', 'Audrey', 'Sebastian', 'Ellie', 'Grayson', 'Stella', + 'Julian', 'Luna', 'Hudson', 'Lila', 'Wyatt', 'Nina', 'Mateo', 'Cora', 'Isaiah', + 'Vivian', 'Jordan', 'Katherine', 'Leo', 'Mackenzie', 'Harrison', 'Paige', 'Evan', + 'Alice', 'Jaxon', 'Eliana', 'Asher', 'Lydia', 'Leo', 'Julia', 'Miles', 'Caroline', + 'Jeremiah', 'Kylie', 'Jasper', 'Adeline', 'Roman', 'Piper', 'Ezekiel', 'Claire', + 'Xavier', 'Riley', 'Sawyer', 'Serenity', 'Kinsley', 'Maya', 'Zachary', 'Madeline', + 'Ariana', 'Aiden', 'Eliza', 'Avery', 'Liam', 'Sophie', 'Jaxon', 'Evangeline', + 'Daniel', 'Anna', 'Hudson', 'Natalie', 'Eli', 'Mia', 'Sebastian', 'Quinn', + 'Jameson', 'Everly', 'Santiago', 'Aurora', 'Roman', 'Naomi', 'Jackson', 'Ivy' + ]; + + return $firstnames[array_rand($firstnames)]; + } + + public static function lastname(): string { + $lastnames = [ + 'Smith', 'Johnson', 'Williams', 'Jones', 'Brown', 'Davis', 'Miller', 'Wilson', + 'Moore', 'Taylor', 'Anderson', 'Thomas', 'Jackson', 'White', 'Harris', 'Martin', + 'Thompson', 'Garcia', 'Martinez', 'Robinson', 'Clark', 'Rodriguez', 'Lewis', + 'Lee', 'Walker', 'Hall', 'Allen', 'Young', 'King', 'Wright', 'Scott', 'Torres', + 'Nguyen', 'Hill', 'Adams', 'Baker', 'Nelson', 'Carter', 'Mitchell', 'Perez', + 'Roberts', 'Turner', 'Phillips', 'Campbell', 'Parker', 'Evans', 'Edwards', + 'Collins', 'Stewart', 'Sanchez', 'Morris', 'Rogers', 'Reed', 'Cook', 'Morgan', + 'Bell', 'Murphy', 'Bailey', 'Rivera', 'Cooper', 'Richardson', 'Cox', 'Howard', + 'Ward', 'Flores', 'Wood', 'James', 'Bennett', 'Gray', 'Mendoza', 'Cruz', + 'Hughes', 'Price', 'Myers', 'Long', 'Foster', 'Sanders', 'Ross', 'Morales', + 'Powell', 'Jenkins', 'Perry', 'Butler', 'Barnes', 'Fisher', 'Henderson', + 'Coleman', 'Simmons', 'Patterson', 'Jordan', 'Reynolds', 'Hamilton', + 'Graham', 'Kim', 'Gonzalez', 'Vasquez', 'Sullivan', 'Bryant', 'Alexander', + 'Russell', 'Griffin', 'Diaz', 'Hayes', 'Wells', 'Chavez', 'Burke', 'Wood', + 'Harrison', 'Gordon', 'Walters', 'McDonald', 'Murray', 'Ford', 'Hamilton', + 'Gibson', 'Ellis', 'Ramos', 'Fisher', 'George', 'Miller', 'Harris', 'James', + 'Stone', 'Richards', 'Hunter', 'Bennett', 'Perry', 'Matthews', 'Hughes', + 'Palmer', 'Burns', 'Floyd', 'Nguyen', 'Snyder', 'Bishop', 'Newman', 'Boone', + 'Dean', 'Carr', 'Cunningham', 'Sampson', 'Marshall', 'Barnett', 'Farrell', + 'Weaver', 'Wade', 'Bradley', 'Mason', 'Newton', 'Olson' + ]; + + return $lastnames[array_rand($lastnames)]; + } + + public static function fullname(): string { + return sprintf('%s %s', self::firstname(), self::lastname()); + } + + public static function addressStreet(): string { + $number = mt_rand(1, 1200); + + $streets = [ + 'Red Street', 'Blue Avenue', 'Green Lane', 'Yellow Road', + 'Orange Boulevard', 'Purple Drive', 'Pink Place', 'Brown Terrace', + 'Gray Court', 'White Crescent', 'Black Alley', 'Silver Way', + 'Gold Street', 'Copper Crescent', 'Bronze Drive', 'Platinum Road', + 'Emerald Lane', 'Ruby Street', 'Sapphire Avenue', 'Topaz Boulevard', + 'Diamond Drive', 'Jade Place', 'Onyx Terrace', 'Quartz Way', + 'Amethyst Avenue', 'Turquoise Road', 'Opal Lane', 'Amber Street', + 'Lime Boulevard', 'Violet Drive', 'Indigo Crescent', 'Teal Place', + 'Cyan Court', 'Magenta Terrace', 'Coral Way', 'Lavender Road', + 'Cherry Lane', 'Rose Avenue', 'Marigold Street', 'Daisy Boulevard', + 'Sunflower Drive', 'Iris Place', 'Lily Court', 'Poppy Way', + 'Hibiscus Terrace', 'Gardenia Road', 'Holly Lane', 'Tulip Avenue', + 'Azalea Boulevard', 'Dandelion Drive', 'Aster Street', 'Cosmos Place', + 'Bluebell Road ', 'Hyacinth Court', 'Buttercup Avenue', 'Foxglove Lane' + ]; + + return $streets[array_rand($streets)] . ' ' . $number; + } + + public static function addressZip(): string { + return str_pad(rand(1000, 9999), 4, '0', STR_PAD_LEFT); + } + + public static function addressCity(): string { + $cities = [ + // United States + 'New York', 'Los Angeles', 'Chicago', 'Houston', 'Phoenix', + 'Philadelphia', 'San Antonio', 'San Diego', 'Dallas', 'San Jose', + + // Canada + 'Toronto', 'Vancouver', 'Montreal', 'Calgary', 'Edmonton', + 'Ottawa', 'Winnipeg', 'Quebec City', 'Hamilton', 'Kitchener', + + // United Kingdom + 'London', 'Birmingham', 'Manchester', 'Glasgow', 'Liverpool', + 'Edinburgh', 'Leeds', 'Sheffield', 'Bristol', 'Cardiff', + + // Australia + 'Sydney', 'Melbourne', 'Brisbane', 'Perth', 'Adelaide', + 'Gold Coast', 'Canberra', 'Hobart', 'Darwin', 'Newcastle', + + // Germany + 'Berlin', 'Hamburg', 'Munich', 'Cologne', 'Frankfurt', + 'Stuttgart', 'Dusseldorf', 'Dortmund', 'Essen', 'Leipzig', + + // France + 'Paris', 'Marseille', 'Lyon', 'Toulouse', 'Nice', + 'Nantes', 'Montpellier', 'Strasbourg', 'Bordeaux', 'Lille', + + // Italy + 'Rome', 'Milan', 'Naples', 'Turin', 'Palermo', + 'Genoa', 'Bologna', 'Florence', 'Catania', 'Venice', + + // Spain + 'Madrid', 'Barcelona', 'Valencia', 'Seville', 'Zaragoza', + 'Malaga', 'Murcia', 'Palma', 'Las Palmas', 'Bilbao', + + // Belgium + 'Brussels', 'Antwerp', 'Ghent', 'Bruges', 'Liege', + 'Namur', 'Ostend', 'Leuven', 'Hasselt', 'Mechelen', + + // Netherlands + 'Amsterdam', 'Rotterdam', 'The Hague', 'Utrecht', 'Eindhoven', + 'Groningen', 'Maastricht', 'Arnhem', 'Nijmegen', 'Haarlem', + + // Switzerland + 'Zurich', 'Geneva', 'Bern', 'Basel', 'Lausanne', + 'Lucerne', 'St. Moritz', 'Zug', 'Neuchatel', 'La Chaux-de-Fonds', + + // Japan + 'Tokyo', 'Osaka', 'Kyoto', 'Nagoya', 'Hiroshima', + 'Fukuoka', 'Kobe', 'Yokohama', 'Sapporo', 'Sendai', + + // China + 'Beijing', 'Shanghai', 'Guangzhou', 'Shenzhen', 'Chengdu', + 'Hong Kong', 'Hangzhou', 'Nanjing', 'Wuhan', 'Xi\'an', + + // India + 'Mumbai', 'Delhi', 'Bangalore', 'Hyderabad', 'Ahmedabad', + 'Chennai', 'Kolkata', 'Pune', 'Jaipur', 'Surat', + + // Brazil + 'Sao Paulo', 'Rio de Janeiro', 'Salvador', 'Fortaleza', 'Belo Horizonte', + 'Brasilia', 'Curitiba', 'Manaus', 'Recife', 'Porto Alegre', + + // South Africa + 'Johannesburg', 'Cape Town', 'Durban', 'Pretoria', 'Port Elizabeth', + 'Bloemfontein', 'East London', 'Polokwane', 'Nelspruit', 'Mbombela' + ]; + + return $cities[array_rand($cities)]; + } + + public static function addressCountry(): string { + $countries = [ + 'Afghanistan', 'Albania', 'Algeria', 'Andorra', 'Angola', + 'Antigua and Barbuda', 'Argentina', 'Armenia', 'Australia', 'Austria', + 'Azerbaijan', 'Bahamas', 'Bahrain', 'Bangladesh', 'Barbados', + 'Belarus', 'Belgium', 'Belize', 'Benin', 'Bhutan', + 'Bolivia', 'Bosnia and Herzegovina', 'Botswana', 'Brazil', 'Brunei', + 'Bulgaria', 'Burkina Faso', 'Burundi', 'Cabo Verde', 'Cambodia', + 'Cameroon', 'Canada', 'Central African Republic', 'Chad', 'Chile', + 'China', 'Colombia', 'Comoros', 'Congo, Democratic Republic of the', 'Congo, Republic of the', + 'Costa Rica', 'Croatia', 'Cuba', 'Cyprus', 'Czech Republic', + 'Denmark', 'Djibouti', 'Dominica', 'Dominican Republic', + 'East Timor', 'Ecuador', 'Egypt', 'El Salvador', 'Equatorial Guinea', + 'Eritrea', 'Estonia', 'Eswatini', 'Ethiopia', 'Fiji', + 'Finland', 'France', 'Gabon', 'Gambia', 'Georgia', + 'Germany', 'Ghana', 'Greece', 'Grenada', 'Guatemala', + 'Guinea', 'Guinea-Bissau', 'Guyana', 'Haiti', 'Honduras', + 'Hungary', 'Iceland', 'India', 'Indonesia', 'Iran', + 'Iraq', 'Ireland', 'Israel', 'Italy', 'Jamaica', + 'Japan', 'Jordan', 'Kazakhstan', 'Kenya', 'Kiribati', + 'Korea, North', 'Korea, South', 'Kosovo', 'Kuwait', 'Kyrgyzstan', + 'Laos', 'Latvia', 'Lebanon', 'Lesotho', 'Liberia', + 'Libya', 'Liechtenstein', 'Lithuania', 'Luxembourg', 'Madagascar', + 'Malawi', 'Malaysia', 'Maldives', 'Mali', 'Malta', + 'Marshall Islands', 'Mauritania', 'Mauritius', 'Mexico', 'Micronesia', + 'Moldova', 'Monaco', 'Mongolia', 'Montenegro', 'Morocco', + 'Mozambique', 'Myanmar', 'Namibia', 'Nauru', 'Nepal', + 'Netherlands', 'New Zealand', 'Nicaragua', 'Niger', 'Nigeria', + 'North Macedonia', 'Norway', 'Oman', 'Pakistan', 'Palau', + 'Panama', 'Papua New Guinea', 'Paraguay', 'Peru', 'Philippines', + 'Poland', 'Portugal', 'Qatar', 'Romania', 'Russia', + 'Rwanda', 'Saint Kitts and Nevis', 'Saint Lucia', 'Saint Vincent and the Grenadines', 'Samoa', + 'San Marino', 'Sao Tome and Principe', 'Saudi Arabia', 'Senegal', 'Serbia', + 'Seychelles', 'Sierra Leone', 'Singapore', 'Slovakia', 'Slovenia', + 'Solomon Islands', 'Somalia', 'South Africa', 'South Sudan', 'Spain', + 'Sri Lanka', 'Sudan', 'Suriname', 'Sweden', 'Switzerland', + 'Syria', 'Taiwan', 'Tajikistan', 'Tanzania', 'Thailand', + 'Timor-Leste', 'Togo', 'Tonga', 'Trinidad and Tobago', 'Tunisia', + 'Turkey', 'Turkmenistan', 'Tuvalu', 'Uganda', 'Ukraine', + 'United Arab Emirates', 'United Kingdom', 'United States', 'Uruguay', 'Uzbekistan', + 'Vanuatu', 'Vatican City', 'Venezuela', 'Vietnam', 'Yemen', + 'Zambia', 'Zimbabwe' + ]; + + return $countries[array_rand($countries)]; + } + + public static function address(): string { + return sprintf( + '%s, %s %s, %s', + self::addressStreet(), + self::addressZip(), + self::addressCity(), + self::addressCountry() + ); + } + +} diff --git a/lib/equal/orm/usages/Usage.class.php b/lib/equal/orm/usages/Usage.class.php index adda6606b..7022a082b 100644 --- a/lib/equal/orm/usages/Usage.class.php +++ b/lib/equal/orm/usages/Usage.class.php @@ -83,6 +83,10 @@ public function getConstraints(): array { return []; } + public function generateRandomValue() { + return null; + } + final public function getName() : string { return $this->usage_str; } diff --git a/lib/equal/orm/usages/UsageAmount.class.php b/lib/equal/orm/usages/UsageAmount.class.php index dd27d90d3..340ee0002 100644 --- a/lib/equal/orm/usages/UsageAmount.class.php +++ b/lib/equal/orm/usages/UsageAmount.class.php @@ -6,8 +6,7 @@ */ namespace equal\orm\usages; -use equal\locale\Locale; -use core\setting\Setting; +use equal\data\DataGenerator; class UsageAmount extends Usage { @@ -61,4 +60,14 @@ public function getConstraints(): array { ]; } + public function generateRandomValue(): float { + switch($this->getSubtype()) { + case 'money': + case 'percent': + case 'rate': + return DataGenerator::realNumber($this->getPrecision(), $this->getScale()); + } + return 0; + } + } diff --git a/lib/equal/orm/usages/UsageArray.class.php b/lib/equal/orm/usages/UsageArray.class.php index 23f6200d7..e35c1f576 100644 --- a/lib/equal/orm/usages/UsageArray.class.php +++ b/lib/equal/orm/usages/UsageArray.class.php @@ -26,4 +26,8 @@ public function getConstraints(): array { ]; } + public function generateRandomValue(): array { + return []; + } + } diff --git a/lib/equal/orm/usages/UsageCountry.class.php b/lib/equal/orm/usages/UsageCountry.class.php index b177eda74..3c00c81c9 100644 --- a/lib/equal/orm/usages/UsageCountry.class.php +++ b/lib/equal/orm/usages/UsageCountry.class.php @@ -9,6 +9,12 @@ class UsageCountry extends Usage { + private $numeric_country_codes = ['004','008','010','012','016','020','024','028','031','032','036','040','044','048','050','051','052','056','060','064','068','070','072','074','076','084','086','090','092','096','100','104','108','112','116','120','124','132','136','140','144','148','152','156','158','162','166','170','174','175','178','180','184','188','191','192','196','203','204','208','212','214','218','222','226','231','232','233','234','238','239','242','246','248','250','254','258','260','262','266','268','270','275','','276','288','292','296','300','304','308','312','316','320','324','328','332','334','336','340','344','348','352','356','360','364','368','372','376','380','384','388','392','398','400','404','408','410','414','417','418','422','426','428','430','434','438','440','442','446','450','454','458','462','466','470','474','478','480','484','492','496','498','499','500','504','508','512','516','520','524','528','531','533','534','535','540','548','554','558','562','566','570','574','578','580','581','583','584','585','586','591','598','600','604','608','612','616','620','624','626','630','634','638','642','643','646','652','654','659','660','662','663','666','670','674','678','682','686','688','690','694','702','703','704','705','706','710','716','724','728','729','732','740','744','748','752','756','760','762','764','768','772','776','780','784','788','792','795','796','798','800','804','807','818','826','831','832','833','834','840','850','854','858','860','862','876','882','887','894']; + + private $three_letters_country_codes = ['ABW','AFG','AGO','AIA','ALA','ALB','AND','ARE','ARG','ARM','ASM','ATA','ATF','ATG','AUS','AUT','AZE','BDI','BEL','BEN','BES','BFA','BGD','BGR','BHR','BHS','BIH','BLM','BLR','BLZ','BMU','BOL','BRA','BRB','BRN','BTN','BVT','BWA','CAF','CAN','CCK','CHE','CHL','CHN','CIV','CMR','COD','COG','COK','COL','COM','CPV','CRI','CUB','CUW','CXR','CYM','CYP','CZE','DEU','DJI','DMA','DNK','DOM','DZA','ECU','EGY','ERI','ESH','ESP','EST','ETH','FIN','FJI','FLK','FRA','FRO','FSM','GAB','GBR','GEO','GGY','GHA','GIB','GIN','GLP','GMB','GNB','GNQ','GRC','GRD','GRL','GTM','GUF','GUM','GUY','HKG','HMD','HND','HRV','HTI','HUN','IDN','IMN','IND','IOT','IRL','IRN','IRQ','ISL','ISR','ITA','JAM','JEY','JOR','JPN','KAZ','KEN','KGZ','KHM','KIR','KNA','KOR','KWT','LAO','LBN','LBR','LBY','LCA','LIE','LKA','LSO','LTU','LUX','LVA','MAC','MAF','MAR','MCO','MDA','MDG','MDV','MEX','MHL','MKD','MLI','MLT','MMR','MNE','MNG','MNP','MOZ','MRT','MSR','MTQ','MUS','MWI','MYS','MYT','NAM','NCL','NER','NFK','NGA','NIC','NIU','NLD','NOR','NPL','NRU','NZL','OMN','PAK','PAN','PCN','PER','PHL','PLW','PNG','POL','PRI','PRK','PRT','PRY','PSE','PYF','QAT','REU','ROU','RUS','RWA','SAU','SDN','SEN','SGP','SGS','SHN','SJM','SLB','SLE','SLV','SMR','SOM','SPM','SRB','SSD','STP','SUR','SVK','SVN','SWE','SWZ','SXM','SYC','SYR','TCA','TCD','TGO','THA','TJK','TKL','TKM','TLS','TON','TTO','TUN','TUR','TUV','TWN','TZA','UGA','UKR','UMI','URY','USA','UZB','VAT','VCT','VEN','VGB','VIR','VNM','VUT','WLF','WSM','YEM','ZAF','ZMB','ZWE']; + + private $two_letters_country_codes = ['AF','AX','AL','DZ','AS','AD','AO','AI','AQ','AG','AR','AM','AW','AU','AT','AZ','BS','BH','BD','BB','BY','BE','BZ','BJ','BM','BT','BO','BA','BW','BV','BR','IO','BN','BG','BF','BI','KH','CM','CA','CV','KY','CF','TD','CL','CN','CX','CC','CO','KM','CG','CD','CK','CR','CI','HR','CU','CY','CZ','DK','DJ','DM','DO','EC','EG','SV','GQ','ER','EE','ET','FK','FO','FJ','FI','FR','GF','PF','TF','GA','GM','GE','DE','GH','GI','GR','GL','GD','GP','GU','GT','GG','GN','GW','GY','HT','HM','VA','HN','HK','HU','IS','IN','ID','IR','IQ','IE','IM','IL','IT','JM','JP','JE','JO','KZ','KE','KI','KR','KW','KG','LA','LV','LB','LS','LR','LY','LI','LT','LU','MO','MK','MG','MW','MY','MV','ML','MT','MH','MQ','MR','MU','YT','MX','FM','MD','MC','MN','ME','MS','MA','MZ','MM','NA','NR','NP','NL','AN','NC','NZ','NI','NE','NG','NU','NF','MP','NO','OM','PK','PW','PS','PA','PG','PY','PE','PH','PN','PL','PT','PR','QA','RE','RO','RU','RW','BL','SH','KN','LC','MF','PM','VC','WS','SM','ST','SA','SN','RS','SC','SL','SG','SK','SI','SB','SO','ZA','GS','ES','LK','SD','SR','SJ','SZ','SE','CH','SY','TW','TJ','TZ','TH','TL','TG','TK','TO','TT','TN','TR','TM','TC','TV','UG','UA','AE','GB','US','UM','UY','UZ','VU','VE','VN','VG','VI','WF','EH','YE','ZM','ZW']; + public function __construct(string $usage_str) { parent::__construct($usage_str); if($this->length == 0) { @@ -22,7 +28,7 @@ public function getConstraints(): array { 'invalid_country' => [ 'message' => 'Value is not a 3-digits country code (iso-3166-1 numeric).', 'function' => function($value) { - return (in_array($value, ['004','008','010','012','016','020','024','028','031','032','036','040','044','048','050','051','052','056','060','064','068','070','072','074','076','084','086','090','092','096','100','104','108','112','116','120','124','132','136','140','144','148','152','156','158','162','166','170','174','175','178','180','184','188','191','192','196','203','204','208','212','214','218','222','226','231','232','233','234','238','239','242','246','248','250','254','258','260','262','266','268','270','275','','276','288','292','296','300','304','308','312','316','320','324','328','332','334','336','340','344','348','352','356','360','364','368','372','376','380','384','388','392','398','400','404','408','410','414','417','418','422','426','428','430','434','438','440','442','446','450','454','458','462','466','470','474','478','480','484','492','496','498','499','500','504','508','512','516','520','524','528','531','533','534','535','540','548','554','558','562','566','570','574','578','580','581','583','584','585','586','591','598','600','604','608','612','616','620','624','626','630','634','638','642','643','646','652','654','659','660','662','663','666','670','674','678','682','686','688','690','694','702','703','704','705','706','710','716','724','728','729','732','740','744','748','752','756','760','762','764','768','772','776','780','784','788','792','795','796','798','800','804','807','818','826','831','832','833','834','840','850','854','858','860','862','876','882','887','894'])); + return (in_array($value, $this->numeric_country_codes)); } ] ]; @@ -34,7 +40,7 @@ public function getConstraints(): array { 'invalid_country' => [ 'message' => 'Value is not a 3-letters country code (iso-3166-1 alpha-3).', 'function' => function($value) { - return (in_array($value, ['ABW','AFG','AGO','AIA','ALA','ALB','AND','ARE','ARG','ARM','ASM','ATA','ATF','ATG','AUS','AUT','AZE','BDI','BEL','BEN','BES','BFA','BGD','BGR','BHR','BHS','BIH','BLM','BLR','BLZ','BMU','BOL','BRA','BRB','BRN','BTN','BVT','BWA','CAF','CAN','CCK','CHE','CHL','CHN','CIV','CMR','COD','COG','COK','COL','COM','CPV','CRI','CUB','CUW','CXR','CYM','CYP','CZE','DEU','DJI','DMA','DNK','DOM','DZA','ECU','EGY','ERI','ESH','ESP','EST','ETH','FIN','FJI','FLK','FRA','FRO','FSM','GAB','GBR','GEO','GGY','GHA','GIB','GIN','GLP','GMB','GNB','GNQ','GRC','GRD','GRL','GTM','GUF','GUM','GUY','HKG','HMD','HND','HRV','HTI','HUN','IDN','IMN','IND','IOT','IRL','IRN','IRQ','ISL','ISR','ITA','JAM','JEY','JOR','JPN','KAZ','KEN','KGZ','KHM','KIR','KNA','KOR','KWT','LAO','LBN','LBR','LBY','LCA','LIE','LKA','LSO','LTU','LUX','LVA','MAC','MAF','MAR','MCO','MDA','MDG','MDV','MEX','MHL','MKD','MLI','MLT','MMR','MNE','MNG','MNP','MOZ','MRT','MSR','MTQ','MUS','MWI','MYS','MYT','NAM','NCL','NER','NFK','NGA','NIC','NIU','NLD','NOR','NPL','NRU','NZL','OMN','PAK','PAN','PCN','PER','PHL','PLW','PNG','POL','PRI','PRK','PRT','PRY','PSE','PYF','QAT','REU','ROU','RUS','RWA','SAU','SDN','SEN','SGP','SGS','SHN','SJM','SLB','SLE','SLV','SMR','SOM','SPM','SRB','SSD','STP','SUR','SVK','SVN','SWE','SWZ','SXM','SYC','SYR','TCA','TCD','TGO','THA','TJK','TKL','TKM','TLS','TON','TTO','TUN','TUR','TUV','TWN','TZA','UGA','UKR','UMI','URY','USA','UZB','VAT','VCT','VEN','VGB','VIR','VNM','VUT','WLF','WSM','YEM','ZAF','ZMB','ZWE'])); + return (in_array($value, $this->three_letters_country_codes)); } ] ]; @@ -45,11 +51,25 @@ public function getConstraints(): array { 'invalid_country' => [ 'message' => 'Value is not a 2-letters country code (iso-3166-1 alpha-2).', 'function' => function($value) { - return (in_array($value, ['AF','AX','AL','DZ','AS','AD','AO','AI','AQ','AG','AR','AM','AW','AU','AT','AZ','BS','BH','BD','BB','BY','BE','BZ','BJ','BM','BT','BO','BA','BW','BV','BR','IO','BN','BG','BF','BI','KH','CM','CA','CV','KY','CF','TD','CL','CN','CX','CC','CO','KM','CG','CD','CK','CR','CI','HR','CU','CY','CZ','DK','DJ','DM','DO','EC','EG','SV','GQ','ER','EE','ET','FK','FO','FJ','FI','FR','GF','PF','TF','GA','GM','GE','DE','GH','GI','GR','GL','GD','GP','GU','GT','GG','GN','GW','GY','HT','HM','VA','HN','HK','HU','IS','IN','ID','IR','IQ','IE','IM','IL','IT','JM','JP','JE','JO','KZ','KE','KI','KR','KW','KG','LA','LV','LB','LS','LR','LY','LI','LT','LU','MO','MK','MG','MW','MY','MV','ML','MT','MH','MQ','MR','MU','YT','MX','FM','MD','MC','MN','ME','MS','MA','MZ','MM','NA','NR','NP','NL','AN','NC','NZ','NI','NE','NG','NU','NF','MP','NO','OM','PK','PW','PS','PA','PG','PY','PE','PH','PN','PL','PT','PR','QA','RE','RO','RU','RW','BL','SH','KN','LC','MF','PM','VC','WS','SM','ST','SA','SN','RS','SC','SL','SG','SK','SI','SB','SO','ZA','GS','ES','LK','SD','SR','SJ','SZ','SE','CH','SY','TW','TJ','TZ','TH','TL','TG','TK','TO','TT','TN','TR','TM','TC','TV','UG','UA','AE','GB','US','UM','UY','UZ','VU','VE','VN','VG','VI','WF','EH','YE','ZM','ZW'])); + return (in_array($value, $this->two_letters_country_codes)); } ] ]; } } + public function generateRandomValue(): string { + if($this->getSubtype() == 'country.numeric') { + $country_identifiers = $this->numeric_country_codes; + } + elseif($this->getLength() === 3) { + $country_identifiers = $this->three_letters_country_codes; + } + else { + $country_identifiers = $this->two_letters_country_codes; + } + + return $country_identifiers[array_rand($country_identifiers)]; + } + } diff --git a/lib/equal/orm/usages/UsageCurrency.class.php b/lib/equal/orm/usages/UsageCurrency.class.php index 77f88f410..151d7bdf6 100644 --- a/lib/equal/orm/usages/UsageCurrency.class.php +++ b/lib/equal/orm/usages/UsageCurrency.class.php @@ -9,6 +9,10 @@ class UsageCurrency extends Usage { + private $three_digits_country_codes = ['004','008','010','012','016','020','024','028','031','032','036','040','044','048','050','051','052','056','060','064','068','070','072','074','076','084','086','090','092','096','100','104','108','112','116','120','124','132','136','140','144','148','152','156','158','162','166','170','174','175','178','180','184','188','191','192','196','203','204','208','212','214','218','222','226','231','232','233','234','238','239','242','246','248','250','254','258','260','262','266','268','270','275','','276','288','292','296','300','304','308','312','316','320','324','328','332','334','336','340','344','348','352','356','360','364','368','372','376','380','384','388','392','398','400','404','408','410','414','417','418','422','426','428','430','434','438','440','442','446','450','454','458','462','466','470','474','478','480','484','492','496','498','499','500','504','508','512','516','520','524','528','531','533','534','535','540','548','554','558','562','566','570','574','578','580','581','583','584','585','586','591','598','600','604','608','612','616','620','624','626','630','634','638','642','643','646','652','654','659','660','662','663','666','670','674','678','682','686','688','690','694','702','703','704','705','706','710','716','724','728','729','732','740','744','748','752','756','760','762','764','768','772','776','780','784','788','792','795','796','798','800','804','807','818','826','831','832','833','834','840','850','854','858','860','862','876','882','887','894']; + + private $three_letters_currency_codes = ['ADF','ADP','AED','AFA','AFN','ALL','AMD','ANG','AOA','AOK','AON','AOR','ARP','ARS','ATS','AUD','AWG','AZM','AZN','BAM','BBD','BDT','BEF','BGL','BGN','BHD','BIF','BMD','BND','BOB','BOP','BOV','BRL','BRR','BSD','BTN','BWP','BYB','BYR','BYN','BZD','CAD','CDF','CHE','CHF','CHW','CLF','CLP','CNY','COP','COU','CRC','CSD','CSK','CUC','CUP','CVE','CYP','CZK','DEM','DJF','DKK','DOP','DZD','ECS','ECV','EEK','EGP','ERN','ESP','ETB','EUR','FIM','FJD','FKP','FRF','GBP','GEL','GHS','GIP','GMD','GNF','GRD','GTQ','GWP','GYD','HKD','HNL','HRK','HTG','HUF','IDR','IEP','ILS','INR','IQD','IRR','ISK','ITL','JMD','JOD','JPY','KES','KGS','KHR','KMF','KPW','KRW','KZT','KWD','KYD','LAK','LBP','LKR','LRD','LSL','LTL','LUF','LVL','LVR','LYD','MAD','MDL','MGA','MGF','MKD','MMK','MNT','MOP','MRO','MRU','MTL','MUR','MVR','MWK','MXN','MXV','MYR','MZE','MZM','MZN','NAD','NGN','NHF','NIC','NIO','NLG','NOK','NPR','NZD','OMR','PAB','PEN','PES','PGK','PHP','PKR','PLN','PLZ','PTE','PYG','QAR','ROL','RON','RSD','RUB','RWF','SAR','SBD','SCR','SDD','SDG','SDP','SEK','SGD','SHP','SIT','SKK','SLL','SML','SOS','SRD','SSP','STD','SUB','SUR','SVC','SYP','SZL','THB','TJS','TMM','TMT','TND','TOP','TPE','TRL','TRY','TTD','TWD','TZS','UAH','UGX','USD','USN','USS','UYU','UYW','UZS','VAL','VEB','VEF','VES','VND','VUV','WST','XAF','XAG','XAU','XBA','XBB','XBC','XBD','XCD','XDR','XEU','XFO','XFU','XOF','XPD','XPF','XPT','XSU','XUA','YER','YUD','YUM','ZAR','ZMK','ZWD','ZWL','ZWR']; + public function __construct(string $usage_str) { parent::__construct($usage_str); if($this->length == 0) { @@ -22,7 +26,7 @@ public function getConstraints(): array { 'invalid_currency' => [ 'message' => 'Value is not a 3-digits country code (as of iso-3166-1).', 'function' => function($value) { - return (in_array($value, ['004','008','010','012','016','020','024','028','031','032','036','040','044','048','050','051','052','056','060','064','068','070','072','074','076','084','086','090','092','096','100','104','108','112','116','120','124','132','136','140','144','148','152','156','158','162','166','170','174','175','178','180','184','188','191','192','196','203','204','208','212','214','218','222','226','231','232','233','234','238','239','242','246','248','250','254','258','260','262','266','268','270','275','','276','288','292','296','300','304','308','312','316','320','324','328','332','334','336','340','344','348','352','356','360','364','368','372','376','380','384','388','392','398','400','404','408','410','414','417','418','422','426','428','430','434','438','440','442','446','450','454','458','462','466','470','474','478','480','484','492','496','498','499','500','504','508','512','516','520','524','528','531','533','534','535','540','548','554','558','562','566','570','574','578','580','581','583','584','585','586','591','598','600','604','608','612','616','620','624','626','630','634','638','642','643','646','652','654','659','660','662','663','666','670','674','678','682','686','688','690','694','702','703','704','705','706','710','716','724','728','729','732','740','744','748','752','756','760','762','764','768','772','776','780','784','788','792','795','796','798','800','804','807','818','826','831','832','833','834','840','850','854','858','860','862','876','882','887','894'])); + return (in_array($value, $this->three_digits_country_codes)); } ] ]; @@ -34,13 +38,21 @@ public function getConstraints(): array { default: return [ 'invalid_currency' => [ - 'message' => 'Value is not a 2-letters language code (as of iso-4217 alpha-3).', + 'message' => 'Value is not a 3-letters currency code (as of iso-4217 alpha-3).', 'function' => function($value) { - return (in_array($value, ['ADF','ADP','AED','AFA','AFN','ALL','AMD','ANG','AOA','AOK','AON','AOR','ARP','ARS','ATS','AUD','AWG','AZM','AZN','BAM','BBD','BDT','BEF','BGL','BGN','BHD','BIF','BMD','BND','BOB','BOP','BOV','BRL','BRR','BSD','BTN','BWP','BYB','BYR','BYN','BZD','CAD','CDF','CHE','CHF','CHW','CLF','CLP','CNY','COP','COU','CRC','CSD','CSK','CUC','CUP','CVE','CYP','CZK','DEM','DJF','DKK','DOP','DZD','ECS','ECV','EEK','EGP','ERN','ESP','ETB','EUR','FIM','FJD','FKP','FRF','GBP','GEL','GHS','GIP','GMD','GNF','GRD','GTQ','GWP','GYD','HKD','HNL','HRK','HTG','HUF','IDR','IEP','ILS','INR','IQD','IRR','ISK','ITL','JMD','JOD','JPY','KES','KGS','KHR','KMF','KPW','KRW','KZT','KWD','KYD','LAK','LBP','LKR','LRD','LSL','LTL','LUF','LVL','LVR','LYD','MAD','MDL','MGA','MGF','MKD','MMK','MNT','MOP','MRO','MRU','MTL','MUR','MVR','MWK','MXN','MXV','MYR','MZE','MZM','MZN','NAD','NGN','NHF','NIC','NIO','NLG','NOK','NPR','NZD','OMR','PAB','PEN','PES','PGK','PHP','PKR','PLN','PLZ','PTE','PYG','QAR','ROL','RON','RSD','RUB','RWF','SAR','SBD','SCR','SDD','SDG','SDP','SEK','SGD','SHP','SIT','SKK','SLL','SML','SOS','SRD','SSP','STD','SUB','SUR','SVC','SYP','SZL','THB','TJS','TMM','TMT','TND','TOP','TPE','TRL','TRY','TTD','TWD','TZS','UAH','UGX','USD','USN','USS','UYU','UYW','UZS','VAL','VEB','VEF','VES','VND','VUV','WST','XAF','XAG','XAU','XBA','XBB','XBC','XBD','XCD','XDR','XEU','XFO','XFU','XOF','XPD','XPF','XPT','XSU','XUA','YER','YUD','YUM','ZAR','ZMK','ZWD','ZWL','ZWR'])); + return (in_array($value, $this->three_letters_currency_codes)); } ] ]; } } + public function generateRandomValue(): string { + if($this->getSubtype() == 'iso-4217.numeric') { + return $this->three_digits_country_codes[array_rand($this->three_digits_country_codes)]; + } + + return $this->three_letters_currency_codes[array_rand($this->three_letters_currency_codes)]; + } + } diff --git a/lib/equal/orm/usages/UsageDate.class.php b/lib/equal/orm/usages/UsageDate.class.php index afdeb5547..b53dbb244 100644 --- a/lib/equal/orm/usages/UsageDate.class.php +++ b/lib/equal/orm/usages/UsageDate.class.php @@ -79,4 +79,30 @@ public function getConstraints(): array { return $constraints; } + public function generateRandomValue(): int { + switch($this->getSubtype(0)) { + case 'day': + return mt_rand(1, 31); + case 'weekday.mon': + return mt_rand(1, 7); + case 'weekday.sun': + return mt_rand(0, 6); + case 'month': + return mt_rand(1, 12); + case 'year': + return mt_rand(1, 9999); + case 'yearweek': + return mt_rand(1, 52); + case 'yearday': + return mt_rand(1, 365); + case 'time': + case 'plain': + default: + $start_timestamp = strtotime('1970-01-01 00:00:00'); + $end_timestamp = time(); + + return mt_rand($start_timestamp, $end_timestamp); + } + } + } diff --git a/lib/equal/orm/usages/UsageEmail.class.php b/lib/equal/orm/usages/UsageEmail.class.php index 3623190d6..5e5eb9840 100644 --- a/lib/equal/orm/usages/UsageEmail.class.php +++ b/lib/equal/orm/usages/UsageEmail.class.php @@ -7,6 +7,8 @@ namespace equal\orm\usages; +use equal\data\DataGenerator; + class UsageEmail extends Usage { public function __construct(string $usage_str) { @@ -39,4 +41,8 @@ public function getConstraints(): array { ]; } + public function generateRandomValue(): string { + return DataGenerator::email(); + } + } diff --git a/lib/equal/orm/usages/UsageLanguage.class.php b/lib/equal/orm/usages/UsageLanguage.class.php index e86272c93..90ead5142 100644 --- a/lib/equal/orm/usages/UsageLanguage.class.php +++ b/lib/equal/orm/usages/UsageLanguage.class.php @@ -9,6 +9,10 @@ class UsageLanguage extends Usage { + private $three_letters_langue_codes = ['aar','abk','ace','ach','ada','ady','afa','afh','afr','ain','aka','akk','alb','sqi','ale','alg','alt','amh','ang','anp','apa','ara','arc','arg','arm','hye','arn','arp','art','arw','asm','ast','ath','aus','ava','ave','awa','aym','aze','bad','bai','bak','bal','bam','ban','baq','eus','bas','bat','bej','bel','bem','ben','ber','bho','bih','bik','bin','bis','bla','bnt','tib','bod','bos','bra','bre','btk','bua','bug','bul','bur','mya','byn','cad','cai','car','cat','cau','ceb','cel','cze','ces','cha','chb','che','chg','chi','zho','chk','chm','chn','cho','chp','chr','chu','chv','chy','cmc','cnr','cop','cor','cos','cpe','cpf','cpp','cre','crh','crp','csb','cus','wel','cym','cze','ces','dak','dan','dar','day','del','den','ger','deu','dgr','din','div','doi','dra','dsb','dua','dum','dut','nld','dyu','dzo','efi','egy','eka','gre','ell','elx','eng','enm','epo','est','baq','eus','ewe','ewo','fan','fao','per','fas','fat','fij','fil','fin','fiu','fon','fra','fre','frm','fro','frr','frs','fry','ful','fur','gaa','gay','gba','gem','geo','kat','ger','deu','gez','gil','gla','gle','glg','glv','gmh','goh','gon','gor','got','grb','grc','gre','ell','grn','gsw','guj','gwi','hai','hat','hau','haw','heb','her','hil','him','hin','hit','hmn','hmo','hrv','hsb','hun','hup','arm','hye','iba','ibo','ice','isl','ido','iii','ijo','iku','ile','ilo','ina','inc','ind','ine','inh','ipk','ira','iro','ice','isl','ita','jav','jbo','jpn','jpr','jrb','kaa','kab','kac','kal','kam','kan','kar','kas','geo','kat','kau','kaw','kaz','kbd','kha','khi','khm','kho','kik','kin','kir','kmb','kok','kom','kon','kor','kos','kpe','krc','krl','kro','kru','kua','kum','kur','kut','lad','lah','lam','lao','lat','lav','lez','lim','lin','lit','lol','loz','ltz','lua','lub','lug','lui','lun','luo','lus','mac','mkd','mad','mag','mah','mai','mak','mal','man','mao','mri','map','mar','mas','may','msa','mdf','mdr','men','mga','mic','min','mis','mac','mkd','mkh','mlg','mlt','mnc','mni','mno','moh','mon','mos','mao','mri','may','msa','mul','mun','mus','mwl','mwr','bur','mya','myn','myv','nah','nai','nap','nau','nav','nbl','nde','ndo','nds','nep','new','nia','nic','niu','dut','nld','nno','nob','nog','non','nor','nqo','nso','nub','nwc','nya','nym','nyn','nyo','nzi','oci','oji','ori','orm','osa','oss','ota','oto','paa','pag','pal','pam','pan','pap','pau','peo','per','fas','phi','phn','pli','pol','pon','por','pra','pro','pus','qaa','que','raj','rap','rar','roa','roh','rom','rum','ron','rum','ron','run','rup','rus','sad','sag','sah','sai','sal','sam','san','sas','sat','scn','sco','sel','sem','sga','sgn','shn','sid','sin','sio','sit','sla','slo','slk','slo','slk','slv','sma','sme','smi','smj','smn','smo','sms','sna','snd','snk','sog','som','son','sot','spa','alb','sqi','srd','srn','srp','srr','ssa','ssw','suk','sun','sus','sux','swa','swe','syc','syr','tah','tai','tam','tat','tel','tem','ter','tet','tgk','tgl','tha','tib','bod','tig','tir','tiv','tkl','tlh','tli','tmh','tog','ton','tpi','tsi','tsn','tso','tuk','tum','tup','tur','tut','tvl','twi','tyv','udm','uga','uig','ukr','umb','und','urd','uzb','vai','ven','vie','vol','vot','wak','wal','war','was','wel','cym','wen','wln','wol','xal','xho','yao','yap','yid','yor','ypk','zap','zbl','zen','zgh','zha','chi','zho','znd','zul','zun','zxx','zza']; + + private $two_letters_language_codes = ['aa','ab','af','ak','sq','am','ar','an','hy','as','av','ae','ay','az','ba','bm','eu','be','bn','bh','bi','bo','bs','br','bg','my','ca','cs','ch','ce','zh','cu','cv','kw','co','cr','cy','cs','da','de','dv','nl','dz','el','en','eo','et','eu','ee','fo','fa','fj','fi','fr','fy','ff','ka','de','gd','ga','gl','gv','el','gn','gu','ht','ha','he','hz','hi','ho','hr','hu','hy','ig','is','io','ii','iu','ie','ia','id','ik','is','it','jv','ja','kl','kn','ks','ka','kr','kk','km','ki','rw','ky','kv','kg','ko','kj','ku','lo','la','lv','li','ln','lt','lb','lu','lg','mk','mh','ml','mi','mr','ms','mk','mg','mt','mn','mi','ms','my','na','nv','nr','nd','ng','ne','nl','nn','nb','no','ny','oc','oj','or','om','os','pa','fa','pi','pl','pt','ps','qu','rm','ro','ro','rn','ru','sg','sa','si','sk','sk','sl','se','sm','sn','sd','so','st','es','sq','sc','sr','ss','su','sw','sv','ty','ta','tt','te','tg','tl','th','bo','ti','to','tn','ts','tk','tr','tw','ug','uk','ur','uz','ve','vi','vo','cy','wa','wo','xh','yi','yo','za','zh','zu']; + public function __construct(string $usage_str) { parent::__construct($usage_str); if($this->length == 0) { @@ -24,7 +28,7 @@ public function getConstraints(): array { 'invalid_language' => [ 'message' => 'Value is not a 3-letters language code (iso-639-2 alpha-3).', 'function' => function($value) { - return (in_array($value, ['aar','abk','ace','ach','ada','ady','afa','afh','afr','ain','aka','akk','alb','sqi','ale','alg','alt','amh','ang','anp','apa','ara','arc','arg','arm','hye','arn','arp','art','arw','asm','ast','ath','aus','ava','ave','awa','aym','aze','bad','bai','bak','bal','bam','ban','baq','eus','bas','bat','bej','bel','bem','ben','ber','bho','bih','bik','bin','bis','bla','bnt','tib','bod','bos','bra','bre','btk','bua','bug','bul','bur','mya','byn','cad','cai','car','cat','cau','ceb','cel','cze','ces','cha','chb','che','chg','chi','zho','chk','chm','chn','cho','chp','chr','chu','chv','chy','cmc','cnr','cop','cor','cos','cpe','cpf','cpp','cre','crh','crp','csb','cus','wel','cym','cze','ces','dak','dan','dar','day','del','den','ger','deu','dgr','din','div','doi','dra','dsb','dua','dum','dut','nld','dyu','dzo','efi','egy','eka','gre','ell','elx','eng','enm','epo','est','baq','eus','ewe','ewo','fan','fao','per','fas','fat','fij','fil','fin','fiu','fon','fra','fre','frm','fro','frr','frs','fry','ful','fur','gaa','gay','gba','gem','geo','kat','ger','deu','gez','gil','gla','gle','glg','glv','gmh','goh','gon','gor','got','grb','grc','gre','ell','grn','gsw','guj','gwi','hai','hat','hau','haw','heb','her','hil','him','hin','hit','hmn','hmo','hrv','hsb','hun','hup','arm','hye','iba','ibo','ice','isl','ido','iii','ijo','iku','ile','ilo','ina','inc','ind','ine','inh','ipk','ira','iro','ice','isl','ita','jav','jbo','jpn','jpr','jrb','kaa','kab','kac','kal','kam','kan','kar','kas','geo','kat','kau','kaw','kaz','kbd','kha','khi','khm','kho','kik','kin','kir','kmb','kok','kom','kon','kor','kos','kpe','krc','krl','kro','kru','kua','kum','kur','kut','lad','lah','lam','lao','lat','lav','lez','lim','lin','lit','lol','loz','ltz','lua','lub','lug','lui','lun','luo','lus','mac','mkd','mad','mag','mah','mai','mak','mal','man','mao','mri','map','mar','mas','may','msa','mdf','mdr','men','mga','mic','min','mis','mac','mkd','mkh','mlg','mlt','mnc','mni','mno','moh','mon','mos','mao','mri','may','msa','mul','mun','mus','mwl','mwr','bur','mya','myn','myv','nah','nai','nap','nau','nav','nbl','nde','ndo','nds','nep','new','nia','nic','niu','dut','nld','nno','nob','nog','non','nor','nqo','nso','nub','nwc','nya','nym','nyn','nyo','nzi','oci','oji','ori','orm','osa','oss','ota','oto','paa','pag','pal','pam','pan','pap','pau','peo','per','fas','phi','phn','pli','pol','pon','por','pra','pro','pus','qaa','que','raj','rap','rar','roa','roh','rom','rum','ron','rum','ron','run','rup','rus','sad','sag','sah','sai','sal','sam','san','sas','sat','scn','sco','sel','sem','sga','sgn','shn','sid','sin','sio','sit','sla','slo','slk','slo','slk','slv','sma','sme','smi','smj','smn','smo','sms','sna','snd','snk','sog','som','son','sot','spa','alb','sqi','srd','srn','srp','srr','ssa','ssw','suk','sun','sus','sux','swa','swe','syc','syr','tah','tai','tam','tat','tel','tem','ter','tet','tgk','tgl','tha','tib','bod','tig','tir','tiv','tkl','tlh','tli','tmh','tog','ton','tpi','tsi','tsn','tso','tuk','tum','tup','tur','tut','tvl','twi','tyv','udm','uga','uig','ukr','umb','und','urd','uzb','vai','ven','vie','vol','vot','wak','wal','war','was','wel','cym','wen','wln','wol','xal','xho','yao','yap','yid','yor','ypk','zap','zbl','zen','zgh','zha','chi','zho','znd','zul','zun','zxx','zza'])); + return (in_array($value, $this->three_letters_langue_codes)); } ] ]; @@ -35,11 +39,17 @@ public function getConstraints(): array { 'invalid_language' => [ 'message' => 'Value is not a 2-letters language code (iso-639-1 alpha-2).', 'function' => function($value) { - return (in_array($value, ['aa','ab','af','ak','sq','am','ar','an','hy','as','av','ae','ay','az','ba','bm','eu','be','bn','bh','bi','bo','bs','br','bg','my','ca','cs','ch','ce','zh','cu','cv','kw','co','cr','cy','cs','da','de','dv','nl','dz','el','en','eo','et','eu','ee','fo','fa','fj','fi','fr','fy','ff','ka','de','gd','ga','gl','gv','el','gn','gu','ht','ha','he','hz','hi','ho','hr','hu','hy','ig','is','io','ii','iu','ie','ia','id','ik','is','it','jv','ja','kl','kn','ks','ka','kr','kk','km','ki','rw','ky','kv','kg','ko','kj','ku','lo','la','lv','li','ln','lt','lb','lu','lg','mk','mh','ml','mi','mr','ms','mk','mg','mt','mn','mi','ms','my','na','nv','nr','nd','ng','ne','nl','nn','nb','no','ny','oc','oj','or','om','os','pa','fa','pi','pl','pt','ps','qu','rm','ro','ro','rn','ru','sg','sa','si','sk','sk','sl','se','sm','sn','sd','so','st','es','sq','sc','sr','ss','su','sw','sv','ty','ta','tt','te','tg','tl','th','bo','ti','to','tn','ts','tk','tr','tw','ug','uk','ur','uz','ve','vi','vo','cy','wa','wo','xh','yi','yo','za','zh','zu'])); + return (in_array($value, $this->two_letters_language_codes)); } ] ]; } } + public function generateRandomValue(): string { + $language_codes = $this->getLength() === 3 ? $this->three_letters_langue_codes : $this->two_letters_language_codes; + + return $language_codes[array_rand($language_codes)]; + } + } diff --git a/lib/equal/orm/usages/UsageNumber.class.php b/lib/equal/orm/usages/UsageNumber.class.php index 85ed4754e..99e6fcfde 100644 --- a/lib/equal/orm/usages/UsageNumber.class.php +++ b/lib/equal/orm/usages/UsageNumber.class.php @@ -6,7 +6,7 @@ */ namespace equal\orm\usages; -use equal\locale\Locale; +use equal\data\DataGenerator; class UsageNumber extends Usage { @@ -91,4 +91,22 @@ public function getConstraints(): array { } } + /** + * @return bool|float|int|string|null + */ + public function generateRandomValue() { + switch($this->getSubtype(0)) { + case 'boolean': + return DataGenerator::boolean(); + case 'integer': + return DataGenerator::integer($this->getLength()); + case 'real': + return DataGenerator::realNumber($this->getPrecision(), $this->getScale()); + case 'hexadecimal': + return DataGenerator::hexadecimal($this->getLength()); + } + + return 0; + } + } diff --git a/lib/equal/orm/usages/UsagePassword.class.php b/lib/equal/orm/usages/UsagePassword.class.php index e81575472..88862a073 100644 --- a/lib/equal/orm/usages/UsagePassword.class.php +++ b/lib/equal/orm/usages/UsagePassword.class.php @@ -47,4 +47,15 @@ public function getConstraints(): array { } } + public function generateRandomValue(): string { + $password = ''; + $dict = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'; + $len = strlen($dict) - 1; + for ($i = 0; $i < 10; $i++) { + $password .= $dict[mt_rand(0, $len)]; + } + + return $password; + } + } diff --git a/lib/equal/orm/usages/UsagePhone.class.php b/lib/equal/orm/usages/UsagePhone.class.php index 8af6dd26d..13aa598bd 100644 --- a/lib/equal/orm/usages/UsagePhone.class.php +++ b/lib/equal/orm/usages/UsagePhone.class.php @@ -7,6 +7,8 @@ namespace equal\orm\usages; +use equal\data\DataGenerator; + class UsagePhone extends Usage { public function __construct(string $usage_str) { @@ -27,4 +29,8 @@ public function getConstraints(): array { ]; } + public function generateRandomValue(): string { + return DataGenerator::phoneNumberE164(); + } + } diff --git a/lib/equal/orm/usages/UsageText.class.php b/lib/equal/orm/usages/UsageText.class.php index 1b449300a..84b37db42 100644 --- a/lib/equal/orm/usages/UsageText.class.php +++ b/lib/equal/orm/usages/UsageText.class.php @@ -6,6 +6,8 @@ */ namespace equal\orm\usages; +use equal\data\DataGenerator; + class UsageText extends Usage { /** @@ -100,4 +102,8 @@ public function getConstraints(): array { ]; } + public function generateRandomValue(): string { + return DataGenerator::plainText($this->getLength()); + } + } diff --git a/lib/equal/orm/usages/UsageTime.php b/lib/equal/orm/usages/UsageTime.php index f76a0c02d..dbc3d692b 100644 --- a/lib/equal/orm/usages/UsageTime.php +++ b/lib/equal/orm/usages/UsageTime.php @@ -6,8 +6,7 @@ */ namespace equal\orm\usages; -use equal\locale\Locale; -use core\setting\Setting; +use equal\data\DataGenerator; class UsageTime extends Usage { @@ -23,4 +22,8 @@ public function getConstraints(): array { return []; } + public function generateRandomValue(): string { + return DataGenerator::time(); + } + } diff --git a/lib/equal/orm/usages/UsageUri.class.php b/lib/equal/orm/usages/UsageUri.class.php index d8e014db8..3f0d3f738 100644 --- a/lib/equal/orm/usages/UsageUri.class.php +++ b/lib/equal/orm/usages/UsageUri.class.php @@ -7,6 +7,8 @@ namespace equal\orm\usages; +use equal\data\DataGenerator; + class UsageUri extends Usage { public function __construct(string $usage_str) { @@ -94,4 +96,22 @@ public function getConstraints(): array { return []; } + public function generateRandomValue(): string { + switch($this->getSubtype()) { + case 'url.relative': + return DataGenerator::relativeUrl(); + case 'url.tel': + return DataGenerator::urlTel(); + case 'url.mailto': + return DataGenerator::urlMailto(); + case 'urn.iban': + return DataGenerator::iban(); + case 'urn.ean': + return DataGenerator::ean13(); + case 'url': + default: + return DataGenerator::url(); + } + } + } From 62d32b4db602e370b620267395259a5ffdef2f4c Mon Sep 17 00:00:00 2001 From: Lucas Laurent Date: Wed, 4 Sep 2024 16:02:23 +0200 Subject: [PATCH 04/94] Add action to generate an entity --- packages/core/actions/model/generate.php | 279 +++++++++++++++++++++++ 1 file changed, 279 insertions(+) create mode 100644 packages/core/actions/model/generate.php diff --git a/packages/core/actions/model/generate.php b/packages/core/actions/model/generate.php new file mode 100644 index 000000000..1f03b315f --- /dev/null +++ b/packages/core/actions/model/generate.php @@ -0,0 +1,279 @@ + + Some Rights Reserved, Cedric Francoys, 2010-2021 + Licensed under GNU LGPL 3 license +*/ + +use equal\data\DataGenerator; +use equal\orm\UsageFactory; + +list($params, $providers) = eQual::announce([ + 'description' => "Generate a new object with random data and given values.", + 'params' => [ + 'entity' => [ + 'description' => 'Full name (including namespace) of the class to return (e.g. \'core\\User\').', + 'type' => 'string', + 'required' => true + ], + 'fields' => [ + 'description' => 'Associative array mapping fields to their related values.', + 'type' => 'array', + 'default' => [] + ], + 'relations' => [ + 'description' => 'How to handle the object relations (many2one, one2many and many2many)', + 'type' => 'array', + 'default' => [] + + ], + 'lang' => [ + 'description ' => 'Specific language for multilang field.', + 'type' => 'string', + 'default' => constant('DEFAULT_LANG') + ] + ], + 'response' => [ + 'content-type' => 'application/json', + 'charset' => 'UTF-8', + 'accept-origin' => '*' + ], + 'access' => [ + 'visibility' => 'protected' + ], + 'constants' => ['DEFAULT_LANG'], + 'providers' => ['context', 'orm'] +]); + +/** + * @var \equal\php\Context $context + * @var \equal\orm\ObjectManager $orm + */ +list('context' => $context, 'orm' => $orm) = $providers; + +/** + * Methods + */ + +$generateRecognizableFieldRandomValue = function($field) { + switch($field) { + case 'username': + return DataGenerator::username(); + case 'firstname': + return DataGenerator::firstname(); + case 'lastname': + return DataGenerator::lastname(); + case 'fullname': + return DataGenerator::fullname(); + case 'address_street': + return DataGenerator::addressStreet(); + case 'address_zip': + return DataGenerator::addressZip(); + case 'address_city': + return DataGenerator::addressCity(); + case 'address_country': + return DataGenerator::addressCountry(); + case 'address': + return DataGenerator::address(); + } + + return null; +}; + +$generateFieldRandomValue = function($field_conf) { + switch($field_conf['type']) { + case 'string': + if(!empty($field_conf['selection'])) { + $values = array_values($field_conf['selection']); + return $values[array_rand($values)]; + } + elseif(isset($field_conf['default'])) { + return $field_conf['default']; + } + + return DataGenerator::plainText(); + case 'boolean': + return DataGenerator::boolean(); + case 'integer': + return DataGenerator::integer(9); + case 'float': + return DataGenerator::realNumber(9, 2); + } + + return null; +}; + +/** + * Action + */ + +$new_entity = []; + +$model = $orm->getModel($params['entity']); +if(!$model) { + throw new Exception("unknown_entity", QN_ERROR_INVALID_PARAM); +} + +$root_fields = ['id', 'creator', 'created', 'modifier', 'modified', 'deleted', 'state']; +$recognizable_fields = [ + 'username', 'firstname', 'lastname', 'fullname', + 'address', 'address_street', 'address_zip', 'address_city', 'address_country' +]; + +$schema = $model->getSchema(); +foreach($schema as $field => $conf) { + if(isset($params['fields'][$field])) { + $new_entity[$field] = $params['fields'][$field]; + } + + if( + in_array($field, $root_fields) + || in_array($conf['type'], ['alias', 'computed', 'one2many']) + || (in_array($conf['type'], ['many2one', 'many2many']) && !isset($params['relations'][$field])) + ) { + continue; + } + + if($conf['type'] === 'many2one') { + $ids = $conf['foreign_object']::search([])->ids(); + $mode = $params['relations'][$field]['mode'] ?? 'use-existing-or-create'; + if($mode === 'use-existing-or-create') { + if(empty($ids)) { + $mode = 'create'; + } + else { + $mode = mt_rand(0, 1) === 1 ? 'use-existing' : 'create'; + } + } + + switch($mode) { + case 'use-existing': + if(!empty($ids)) { + if(!($conf['required'] ?? false)) { + $ids[] = null; + } + + $new_entity[$field] = $ids[array_rand($ids)]; + } + break; + case 'create': + $relation_param = [ + 'entity' => $conf['foreign_object'] + ]; + foreach(['fields', 'relations'] as $param_key) { + if(isset($params['relations'][$field][$param_key])) { + $relation_param[$param_key] = $params['relations'][$field][$param_key]; + } + } + + $result = eQual::run('do', 'core_model_generate', $relation_param); + $new_entity[$field] = $result['id']; + break; + } + } + elseif($conf['type'] === 'many2many') { + $mode = $params['relations'][$field]['mode'] ?? 'use-existing-or-create'; + + $qty_conf = $params['relations'][$field]['qty'] ?? [0, 5]; + $qty = is_array($qty_conf) ? mt_rand($qty_conf[0], $qty_conf[1]) : $qty_conf; + + switch($mode) { + case 'use-existing': + $ids = $conf['foreign_object']::search([])->ids(); + $random_ids = []; + for($i = 0; $i < $qty; $i++) { + if(empty($ids)) { + break; + } + + $random_index = array_rand($ids); + $random_ids[] = $ids[$random_index]; + array_splice($ids, $random_index, 1); + } + + if(!empty($random_ids)) { + $new_entity[$field] = $random_ids; + } + break; + case 'create': + $relation_param = [ + 'entity' => $conf['foreign_object'], + 'lang' => $params['lang'] + ]; + foreach(['fields', 'relations'] as $param_key) { + if(isset($params['relations'][$field][$param_key])) { + $relation_param[$param_key] = $params['relations'][$field][$param_key]; + } + } + + $new_relation_entities_ids = []; + for($i = 0; $i < $qty; $i++) { + $result = eQual::run('do', 'core_model_generate', $relation_param); + $new_relation_entities_ids[] = $result['id']; + } + + if(!empty($new_relation_entities_ids)) { + $new_entity[$field] = $new_relation_entities_ids; + } + break; + } + } + elseif(isset($conf['usage'])) { + $usage = UsageFactory::create($conf['usage']); + $new_entity[$field] = $usage->generateRandomValue(); + } + elseif(in_array($field, $recognizable_fields)) { + $new_entity[$field] = $generateRecognizableFieldRandomValue($field); + } + else { + $new_entity[$field] = $generateFieldRandomValue($conf); + } +} + +$instance = $params['entity']::create($new_entity, $params['lang']) + ->adapt('json') + ->first(true); + +foreach($schema as $field => $conf) { + if($conf['type'] !== 'one2many' || !isset($params['relations'][$field])) { + continue; + } + + $qty_conf = $params['relations'][$field]['qty'] ?? [0, 3]; + $qty = is_array($qty_conf) ? mt_rand($qty_conf[0], $qty_conf[1]) : $qty_conf; + + $relation_param = [ + 'entity' => $conf['foreign_object'], + 'lang' => $params['lang'] + ]; + foreach(['fields', 'relations'] as $param_key) { + if(isset($params['relations'][$field][$param_key])) { + $relation_param[$param_key] = $params['relations'][$field][$param_key]; + } + } + + if(!isset($relation_param['fields'])) { + $relation_param['fields'] = []; + } + $relation_param['fields'][$conf['foreign_field']] = $instance['id']; + + $new_relation_entities_ids = []; + for($i = 0; $i < $qty; $i++) { + $result = eQual::run('do', 'core_model_generate', $relation_param); + $new_relation_entities_ids[] = $result['id']; + } + + if(!empty($new_relation_entities_ids)) { + $new_entity[$field] = $new_relation_entities_ids; + } +} + +$result = [ + 'entity' => $params['entity'], + 'id' => $instance['id'] +]; + +$context->httpResponse() + ->status(201) + ->body($result) + ->send(); From 5cc33529d57ef1ee6dcaf50f1b153f71ab549f55 Mon Sep 17 00:00:00 2001 From: Lucas Laurent Date: Wed, 4 Sep 2024 16:38:41 +0200 Subject: [PATCH 05/94] Handle fields that can be null because not required --- lib/equal/data/DataGenerator.class.php | 6 ++++-- packages/core/actions/model/generate.php | 10 ++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/equal/data/DataGenerator.class.php b/lib/equal/data/DataGenerator.class.php index 36e1d42fe..4174ebb05 100644 --- a/lib/equal/data/DataGenerator.class.php +++ b/lib/equal/data/DataGenerator.class.php @@ -43,8 +43,10 @@ public static function plainText($max_length = 255): string { return trim(substr($random_text, 0, $random_length)); } - public static function boolean(): bool { - return boolval(mt_rand(0, 1)); + public static function boolean($probability = 0.5): bool { + $probability = max(0, min(1, $probability)); + + return mt_rand() / mt_getrandmax() < $probability; } public static function integer(int $length): int { diff --git a/packages/core/actions/model/generate.php b/packages/core/actions/model/generate.php index 1f03b315f..c8dc893ff 100644 --- a/packages/core/actions/model/generate.php +++ b/packages/core/actions/model/generate.php @@ -219,8 +219,14 @@ } } elseif(isset($conf['usage'])) { - $usage = UsageFactory::create($conf['usage']); - $new_entity[$field] = $usage->generateRandomValue(); + $required = $conf['required'] ?? false; + if(!$required && DataGenerator::boolean(0.1)) { + $new_entity[$field] = null; + } + else { + $usage = UsageFactory::create($conf['usage']); + $new_entity[$field] = $usage->generateRandomValue(); + } } elseif(in_array($field, $recognizable_fields)) { $new_entity[$field] = $generateRecognizableFieldRandomValue($field); From 4a5e7cb8e1db7ef23a659b5227bb17ac7be02df4 Mon Sep 17 00:00:00 2001 From: Lucas Laurent Date: Wed, 4 Sep 2024 16:58:55 +0200 Subject: [PATCH 06/94] Handle generator_function to specify in model how to generate random value for feed --- packages/core/actions/model/generate.php | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/core/actions/model/generate.php b/packages/core/actions/model/generate.php index c8dc893ff..3ca806926 100644 --- a/packages/core/actions/model/generate.php +++ b/packages/core/actions/model/generate.php @@ -46,8 +46,8 @@ ]); /** - * @var \equal\php\Context $context - * @var \equal\orm\ObjectManager $orm + * @var \equal\php\Context $context + * @var \equal\orm\ObjectManager $orm */ list('context' => $context, 'orm' => $orm) = $providers; @@ -124,9 +124,13 @@ foreach($schema as $field => $conf) { if(isset($params['fields'][$field])) { $new_entity[$field] = $params['fields'][$field]; + continue; } - - if( + elseif(isset($conf['generator_function']) && method_exists($params['entity'], $conf['generator_function'])) { + $new_entity[$field] = $params['entity']::{$conf['generator_function']}(); + continue; + } + elseif( in_array($field, $root_fields) || in_array($conf['type'], ['alias', 'computed', 'one2many']) || (in_array($conf['type'], ['many2one', 'many2many']) && !isset($params['relations'][$field])) @@ -138,12 +142,7 @@ $ids = $conf['foreign_object']::search([])->ids(); $mode = $params['relations'][$field]['mode'] ?? 'use-existing-or-create'; if($mode === 'use-existing-or-create') { - if(empty($ids)) { - $mode = 'create'; - } - else { - $mode = mt_rand(0, 1) === 1 ? 'use-existing' : 'create'; - } + $mode = empty($ids) || DataGenerator::boolean() ? 'create' : 'use-existing'; } switch($mode) { From f269e8cfc1368a89b7f1d40edce6c4b30c72519c Mon Sep 17 00:00:00 2001 From: Lucas Laurent Date: Thu, 5 Sep 2024 09:26:47 +0200 Subject: [PATCH 07/94] Handle generate data in action core_init_package --- packages/core/actions/init/package.php | 56 +++++++++++++++++--------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/packages/core/actions/init/package.php b/packages/core/actions/init/package.php index 947586a95..bcad482ff 100644 --- a/packages/core/actions/init/package.php +++ b/packages/core/actions/init/package.php @@ -190,32 +190,50 @@ $objects_ids = []; - foreach($class['data'] as $odata) { - foreach($odata as $field => $value) { - if(!isset($schema[$field])) { - $reporter->warning("ORM::unknown field {$field} in json file '{$json_file}'."); - continue; + if(isset($class['data'])) { + foreach($class['data'] as $odata) { + foreach($odata as $field => $value) { + if(!isset($schema[$field])) { + $reporter->warning("ORM::unknown field {$field} in json file '{$json_file}'."); + continue; + } + $f = new Field($schema[$field]); + $odata[$field] = $adapter->adaptIn($value, $f->getUsage()); } - $f = new Field($schema[$field]); - $odata[$field] = $adapter->adaptIn($value, $f->getUsage()); - } - if(isset($odata['id'])) { - $res = $orm->search($entity, ['id', '=', $odata['id']]); - if($res > 0 && count($res)) { - // object already exists (either values or language might differ) + if(isset($odata['id'])) { + $res = $orm->search($entity, ['id', '=', $odata['id']]); + if($res > 0 && count($res)) { + // object already exists (either values or language might differ) + } + else { + $orm->create($entity, ['id' => $odata['id']], $lang, false); + } + $id = $odata['id']; + unset($odata['id']); } else { - $orm->create($entity, ['id' => $odata['id']], $lang, false); + $id = $orm->create($entity, [], $lang); } - $id = $odata['id']; - unset($odata['id']); + $orm->update($entity, $id, $odata, $lang); + $objects_ids[] = $id; } - else { - $id = $orm->create($entity, [], $lang); + } + elseif(isset($class['qty'])) { + $qty = is_array($class['qty']) ? mt_rand($class['qty'][0], $class['qty'][1]) : $class['qty']; + $generate_params = [ + 'entity' => $entity, + 'lang' => $class['lang'] + ]; + foreach(['fields', 'relations'] as $param_key) { + if(isset($class[$param_key])) { + $generate_params[$param_key] = $class[$param_key]; + } + } + + for($i = 0; $i < $qty; $i++) { + eQual::run('do', 'core_model_generate', $generate_params); } - $orm->update($entity, $id, $odata, $lang); - $objects_ids[] = $id; } // force a first generation of computed fields, if any From 3bc1a26fb97f57d30450a52b0c2447e9e66956f0 Mon Sep 17 00:00:00 2001 From: Lucas Laurent Date: Thu, 5 Sep 2024 09:41:50 +0200 Subject: [PATCH 08/94] Better handling of generation of username, firstname and lastname --- lib/equal/data/DataGenerator.class.php | 187 ++++++++++++++++++------- 1 file changed, 139 insertions(+), 48 deletions(-) diff --git a/lib/equal/data/DataGenerator.class.php b/lib/equal/data/DataGenerator.class.php index 4174ebb05..39cd4edd4 100644 --- a/lib/equal/data/DataGenerator.class.php +++ b/lib/equal/data/DataGenerator.class.php @@ -262,12 +262,12 @@ public static function ean13(): string { public static function username(): string { $usernames = [ - 'user1234', 'coolUser88', 'jane_doe22', 'johnnyBravo', 'theRealMike', - 'superstar7', 'gameMaster1', 'techGuru42', 'quickSilver', 'happyCamper', - 'blueSky5', 'codingWizard', 'magicMikey', 'fastTrack99', 'misterX', + 'user', 'coolUser', 'jane_doe', 'johnnyBravo', 'theRealMike', + 'superstar', 'gameMaster', 'techGuru', 'quickSilver', 'happyCamper', + 'blueSky', 'codingWizard', 'magicMikey', 'fastTrack', 'misterX', 'adventureSeeker', 'pixelPioneer', 'ninjaWarrior', 'starGazer', 'drSmart', 'boldExplorer', 'zenMaster', 'risingStar', 'rocketRider', 'digitalNomad', - 'echoEcho', 'nightOwl21', 'lightSpeed', 'trueBeliever', 'cyberHawk', + 'echoEcho', 'nightOwl', 'lightSpeed', 'trueBeliever', 'cyberHawk', 'galacticHero', 'luckyCharm', 'urbanVibes', 'silentStorm', 'wildWanderer', 'moonWalker', 'brightStar', 'vividDreamer', 'vortexVoyager', 'infiniteLoop', 'horizonChaser', 'quickSilverFox', 'shadowKnight', 'dataMaster', 'epicQuest', @@ -279,58 +279,149 @@ public static function username(): string { 'lunarExplorer', 'plasmaBolt', 'infinityEdge', 'quantumQuest', 'stellarVoyager' ]; - return $usernames[array_rand($usernames)]; + $number = mt_rand(0, 9999); + + return $usernames[array_rand($usernames)] . $number; } - public static function firstname(): string { - $firstnames = [ - 'John', 'Jane', 'Michael', 'Emily', 'Robert', 'Jessica', 'David', 'Sarah', - 'James', 'Laura', 'Daniel', 'Sophia', 'Matthew', 'Olivia', 'Andrew', 'Isabella', - 'William', 'Mia', 'Joseph', 'Charlotte', 'Charles', 'Amelia', 'Thomas', 'Harper', - 'Christopher', 'Evelyn', 'Benjamin', 'Abigail', 'Samuel', 'Ella', 'Henry', 'Avery', - 'Lucas', 'Sofia', 'Jack', 'Grace', 'Jackson', 'Chloe', 'Ethan', 'Zoe', 'Alexander', - 'Lily', 'Ryan', 'Hannah', 'Nathan', 'Scarlett', 'Gabriel', 'Aria', 'Carter', 'Mila', - 'Isaac', 'Ella', 'Luke', 'Madison', 'Owen', 'Penelope', 'Caleb', 'Riley', 'Aiden', - 'Samantha', 'Dylan', 'Eleanor', 'Joshua', 'Layla', 'Mason', 'Nora', 'Logan', 'Lila', - 'Eli', 'Hazel', 'Cameron', 'Audrey', 'Sebastian', 'Ellie', 'Grayson', 'Stella', - 'Julian', 'Luna', 'Hudson', 'Lila', 'Wyatt', 'Nina', 'Mateo', 'Cora', 'Isaiah', - 'Vivian', 'Jordan', 'Katherine', 'Leo', 'Mackenzie', 'Harrison', 'Paige', 'Evan', - 'Alice', 'Jaxon', 'Eliana', 'Asher', 'Lydia', 'Leo', 'Julia', 'Miles', 'Caroline', - 'Jeremiah', 'Kylie', 'Jasper', 'Adeline', 'Roman', 'Piper', 'Ezekiel', 'Claire', - 'Xavier', 'Riley', 'Sawyer', 'Serenity', 'Kinsley', 'Maya', 'Zachary', 'Madeline', - 'Ariana', 'Aiden', 'Eliza', 'Avery', 'Liam', 'Sophie', 'Jaxon', 'Evangeline', - 'Daniel', 'Anna', 'Hudson', 'Natalie', 'Eli', 'Mia', 'Sebastian', 'Quinn', - 'Jameson', 'Everly', 'Santiago', 'Aurora', 'Roman', 'Naomi', 'Jackson', 'Ivy' + public static function firstname($lang = null): string { + $map_lang_firstnames = [ + 'en' => [ + 'John', 'Jane', 'Michael', 'Emily', 'Robert', 'Jessica', 'David', 'Sarah', + 'James', 'Laura', 'Daniel', 'Sophia', 'Matthew', 'Olivia', 'Andrew', 'Isabella', + 'William', 'Mia', 'Joseph', 'Charlotte', 'Charles', 'Amelia', 'Thomas', 'Harper', + 'Christopher', 'Evelyn', 'Benjamin', 'Abigail', 'Samuel', 'Ella', 'Henry', 'Avery', + 'Lucas', 'Sofia', 'Jack', 'Grace', 'Jackson', 'Chloe', 'Ethan', 'Zoe', 'Alexander', + 'Lily', 'Ryan', 'Hannah', 'Nathan', 'Scarlett', 'Gabriel', 'Aria', 'Carter', 'Mila', + 'Isaac', 'Ella', 'Luke', 'Madison', 'Owen', 'Penelope', 'Caleb', 'Riley', 'Aiden', + 'Samantha', 'Dylan', 'Eleanor', 'Joshua', 'Layla', 'Mason', 'Nora', 'Logan', 'Lila', + 'Eli', 'Hazel', 'Cameron', 'Audrey', 'Sebastian', 'Ellie', 'Grayson', 'Stella', + 'Julian', 'Luna', 'Hudson', 'Lila', 'Wyatt', 'Nina', 'Mateo', 'Cora', 'Isaiah', + 'Vivian', 'Jordan', 'Katherine', 'Leo', 'Mackenzie', 'Harrison', 'Paige', 'Evan', + 'Alice', 'Jaxon', 'Eliana', 'Asher', 'Lydia', 'Leo', 'Julia', 'Miles', 'Caroline', + 'Jeremiah', 'Kylie', 'Jasper', 'Adeline', 'Roman', 'Piper', 'Ezekiel', 'Claire', + 'Xavier', 'Riley', 'Sawyer', 'Serenity', 'Kinsley', 'Maya', 'Zachary', 'Madeline', + 'Ariana', 'Aiden', 'Eliza', 'Avery', 'Liam', 'Sophie', 'Jaxon', 'Evangeline', + 'Daniel', 'Anna', 'Hudson', 'Natalie', 'Eli', 'Mia', 'Sebastian', 'Quinn', + 'Jameson', 'Everly', 'Santiago', 'Aurora', 'Roman', 'Naomi', 'Jackson', 'Ivy', + 'Finn', 'Riley', 'Oliver', 'Jade', 'Landon', 'Brianna', 'Gavin', 'Savannah', + 'Connor', 'Lily', 'Parker', 'Aubrey', 'Nolan', 'Violet', 'Bentley', 'Clara', + 'Levi', 'Ruby', 'Carson', 'Alyssa', 'Hunter', 'Faith', 'Eli', 'Zoey', 'Adrian', + 'Sienna', 'Cooper', 'Elise', 'Brody', 'Genesis', 'Grant', 'Harley', 'Tristan', + 'Eva', 'Easton', 'Sage', 'Tanner', 'Summer', 'Dominic', 'Maddie', 'Micah', + 'Tessa', 'Elias', 'Brooke', 'Elliot', 'Mallory', 'Theo', 'Delilah', 'Ryder', + 'Lana', 'Beckett', 'Reese', 'Axel', 'Anastasia', 'Malachi', 'Gemma', 'Bennett', + 'Talia', 'Brayden', 'Nadia', 'Silas', 'Camila', 'Jonah', 'Iris', 'Maxwell', + 'Isla', 'Tyler', 'Jasmine', 'Diego', 'Nova', 'Eric', 'Maren', 'Dean', 'Bianca', + 'Lincoln', 'Paisley', 'Hayden', 'Rose', 'Declan', 'Carmen', 'Oscar', 'Willa', + 'Griffin', 'Aspen', 'Ronan', 'Freya', 'Ezra', 'Willow', 'Kaden', 'Georgia' + ], + 'fr' => [ + 'Jean', 'Marie', 'Pierre', 'Sophie', 'Louis', 'Camille', 'Paul', 'Juliette', + 'Jacques', 'Chloé', 'Léon', 'Clara', 'Henri', 'Lucie', 'Thomas', 'Élodie', + 'Philippe', 'Manon', 'Michel', 'Léa', 'Nicolas', 'Amandine', 'François', 'Anaïs', + 'Antoine', 'Aurélie', 'Guillaume', 'Margaux', 'Étienne', 'Charlotte', 'Benoît', + 'Alice', 'Maxime', 'Julie', 'Hugo', 'Emilie', 'Théo', 'Isabelle', 'Vincent', + 'Valérie', 'Laurent', 'Cécile', 'Olivier', 'Maëlys', 'Damien', 'Catherine', + 'Adrien', 'Amélie', 'Georges', 'Émilie', 'Baptiste', 'Inès', 'Rémi', 'Océane', + 'Mathieu', 'Florian', 'Yves', 'Elsa', 'René', 'Jade', 'Claude', 'Clémentine', + 'André', 'Victoria', 'Gérard', 'Laure', 'Lucas', 'Sarah', 'Alain', 'Gabrielle', + 'Patrick', 'Madeleine', 'Simon', 'Louise', 'Raphaël', 'Soline', 'Arnaud', 'Léna', + 'Sébastien', 'Victoire', 'Gaspard', 'Maëlle', 'Charles', 'Rose', 'Mathis', 'Fanny', + 'Luc', 'Noémie', 'Christophe', 'Caroline', 'David', 'Jeanne', 'Emmanuel', 'Justine', + 'Xavier', 'Adèle', 'Pascal', 'Diane', 'Romain', 'Noé', 'Marc', 'Marion', 'Gaël', + 'Coralie', 'Cédric', 'Ariane','Françoise', 'Yvonne', 'Clément', 'Solange', 'Mathilde', + 'Bérénice', 'Thierry', 'Agnès', 'Pascaline', 'Alix', 'Roland', 'Brigitte', 'Sylvain', + 'Estelle', 'Fabrice', 'Lilian', 'Josiane', 'Éric', 'Serge', 'Cyril', 'Bernadette', + 'Guilhem', 'Axelle', 'Dominique', 'Ludovic', 'Véronique', 'Raymond', 'Sandrine', + 'Patrice', 'Colette', 'Basile', 'Félix', 'Jean-Marc', 'Maurice', 'Sylvie', 'Jacqueline', + 'Augustin', 'Gaston', 'Jean-Baptiste', 'Odile', 'Arlette', 'Marius', 'Christiane', + 'Fabien', 'Louison', 'Léonie', 'Yann', 'Noémie', 'Raphaëlle', 'Sébastienne', 'Florence', + 'Lucien', 'Jean-Luc', 'Fernand', 'Antoinette', 'Gisèle', 'Solène', 'Angèle', 'Edmond', + 'Céleste', 'Hélène', 'Violette' + ] ]; - return $firstnames[array_rand($firstnames)]; + if(is_null($lang) || !isset($map_lang_firstnames[$lang])) { + $all_firstnames = array_merge( + $map_lang_firstnames['en'], + $map_lang_firstnames['fr'] + ); + + return $all_firstnames[array_rand($all_firstnames)]; + } + + return $map_lang_firstnames[$lang][array_rand($map_lang_firstnames[$lang])]; } - public static function lastname(): string { - $lastnames = [ - 'Smith', 'Johnson', 'Williams', 'Jones', 'Brown', 'Davis', 'Miller', 'Wilson', - 'Moore', 'Taylor', 'Anderson', 'Thomas', 'Jackson', 'White', 'Harris', 'Martin', - 'Thompson', 'Garcia', 'Martinez', 'Robinson', 'Clark', 'Rodriguez', 'Lewis', - 'Lee', 'Walker', 'Hall', 'Allen', 'Young', 'King', 'Wright', 'Scott', 'Torres', - 'Nguyen', 'Hill', 'Adams', 'Baker', 'Nelson', 'Carter', 'Mitchell', 'Perez', - 'Roberts', 'Turner', 'Phillips', 'Campbell', 'Parker', 'Evans', 'Edwards', - 'Collins', 'Stewart', 'Sanchez', 'Morris', 'Rogers', 'Reed', 'Cook', 'Morgan', - 'Bell', 'Murphy', 'Bailey', 'Rivera', 'Cooper', 'Richardson', 'Cox', 'Howard', - 'Ward', 'Flores', 'Wood', 'James', 'Bennett', 'Gray', 'Mendoza', 'Cruz', - 'Hughes', 'Price', 'Myers', 'Long', 'Foster', 'Sanders', 'Ross', 'Morales', - 'Powell', 'Jenkins', 'Perry', 'Butler', 'Barnes', 'Fisher', 'Henderson', - 'Coleman', 'Simmons', 'Patterson', 'Jordan', 'Reynolds', 'Hamilton', - 'Graham', 'Kim', 'Gonzalez', 'Vasquez', 'Sullivan', 'Bryant', 'Alexander', - 'Russell', 'Griffin', 'Diaz', 'Hayes', 'Wells', 'Chavez', 'Burke', 'Wood', - 'Harrison', 'Gordon', 'Walters', 'McDonald', 'Murray', 'Ford', 'Hamilton', - 'Gibson', 'Ellis', 'Ramos', 'Fisher', 'George', 'Miller', 'Harris', 'James', - 'Stone', 'Richards', 'Hunter', 'Bennett', 'Perry', 'Matthews', 'Hughes', - 'Palmer', 'Burns', 'Floyd', 'Nguyen', 'Snyder', 'Bishop', 'Newman', 'Boone', - 'Dean', 'Carr', 'Cunningham', 'Sampson', 'Marshall', 'Barnett', 'Farrell', - 'Weaver', 'Wade', 'Bradley', 'Mason', 'Newton', 'Olson' + public static function lastname($lang = null): string { + $map_lang_lastnames = [ + 'en' => [ + 'Smith', 'Johnson', 'Williams', 'Jones', 'Brown', 'Davis', 'Miller', 'Wilson', + 'Moore', 'Taylor', 'Anderson', 'Thomas', 'Jackson', 'White', 'Harris', 'Martin', + 'Thompson', 'Garcia', 'Martinez', 'Robinson', 'Clark', 'Rodriguez', 'Lewis', + 'Lee', 'Walker', 'Hall', 'Allen', 'Young', 'King', 'Wright', 'Scott', 'Torres', + 'Nguyen', 'Hill', 'Adams', 'Baker', 'Nelson', 'Carter', 'Mitchell', 'Perez', + 'Roberts', 'Turner', 'Phillips', 'Campbell', 'Parker', 'Evans', 'Edwards', + 'Collins', 'Stewart', 'Sanchez', 'Morris', 'Rogers', 'Reed', 'Cook', 'Morgan', + 'Bell', 'Murphy', 'Bailey', 'Rivera', 'Cooper', 'Richardson', 'Cox', 'Howard', + 'Ward', 'Flores', 'Wood', 'James', 'Bennett', 'Gray', 'Mendoza', 'Cruz', + 'Hughes', 'Price', 'Myers', 'Long', 'Foster', 'Sanders', 'Ross', 'Morales', + 'Powell', 'Jenkins', 'Perry', 'Butler', 'Barnes', 'Fisher', 'Henderson', + 'Coleman', 'Simmons', 'Patterson', 'Jordan', 'Reynolds', 'Hamilton', + 'Graham', 'Kim', 'Gonzalez', 'Vasquez', 'Sullivan', 'Bryant', 'Alexander', + 'Russell', 'Griffin', 'Diaz', 'Hayes', 'Wells', 'Chavez', 'Burke', 'Wood', + 'Harrison', 'Gordon', 'Walters', 'McDonald', 'Murray', 'Ford', 'Hamilton', + 'Gibson', 'Ellis', 'Ramos', 'Fisher', 'George', 'Miller', 'Harris', 'James', + 'Stone', 'Richards', 'Hunter', 'Bennett', 'Perry', 'Matthews', 'Hughes', + 'Palmer', 'Burns', 'Floyd', 'Nguyen', 'Snyder', 'Bishop', 'Newman', 'Boone', + 'Dean', 'Carr', 'Cunningham', 'Sampson', 'Marshall', 'Barnett', 'Farrell', + 'Weaver', 'Wade', 'Bradley', 'Mason', 'Newton', 'Olson', + 'Hawkins', 'Chapman', 'Bowman', 'Lawrence', 'Glover', 'Barber', 'Grant', 'Wallace', + 'Keller', 'Webb', 'Spencer', 'Harvey', 'Brooks', 'Pearson', 'Francis', 'Burgess', + 'Graves', 'Lambert', 'Cross', 'Tucker', 'Fields', 'Reeves', 'Gibbs', 'Porter', + 'Daniels', 'Brady', 'Owen', 'Horton', 'McCarthy', 'Fletcher', 'Simon', 'Norris', + 'Clayton', 'Kemp', 'Fuller', 'Tyler', 'Pearce', 'Moss', 'Rowe', 'Hodges', 'Barker', + 'Hardy', 'Jennings', 'Gilbert', 'Payne', 'Webster', 'Neal', 'Sutton', 'Davidson', + 'Carlson', 'Morton', 'Kirk', 'Holland', 'Greer', 'Wheeler', 'Peters', 'Fordham' + ], + 'fr' => [ + 'Martin', 'Bernard', 'Dubois', 'Thomas', 'Robert', 'Richard', 'Petit', 'Durand', 'Leroy', 'Moreau', + 'Simon', 'Laurent', 'Lefebvre', 'Michel', 'Garcia', 'David', 'Bertrand', 'Roux', 'Vincent', 'Fournier', + 'Morel', 'Girard', 'Andre', 'Lefevre', 'Mercier', 'Dupont', 'Lambert', 'Bonnet', 'Francois', 'Martinez', + 'Legrand', 'Garnier', 'Faure', 'Rousseau', 'Blanc', 'Guerin', 'Muller', 'Henry', 'Roussel', 'Nicolas', + 'Perrin', 'Morin', 'Mathieu', 'Clement', 'Gauthier', 'Dumont', 'Lopez', 'Fontaine', 'Chevalier', + 'Robin', 'Masson', 'Sanchez', 'Gerard', 'Nguyen', 'Boyer', 'Denis', 'Lemoine', 'Duval', 'Joly', + 'Gautier', 'Roger', 'Renaud', 'Gaillard', 'Hamond', 'Boucher', 'Carpentier', 'Menard', 'Marechal', + 'Charpentier', 'Dupuis', 'Leclerc', 'Poirier', 'Guillaume', 'Leconte', 'Benoit', 'Collet', + 'Perrot', 'Jacquet', 'Rey', 'Gilles', 'Herve', 'Charrier', 'Schmitt', 'Baron', 'Perret', + 'Leblanc', 'Verdier', 'Giraud', 'Marty', 'Lemoine', 'Poulain', 'Vallet', 'Renard', + 'Marion', 'Marchand', 'Chauvin', 'Langlois', 'Teixeira', 'Bellamy', 'Lemoigne', 'Bazin', + 'Da Silva', 'Delorme', 'Aubry', 'Ferreira', 'Chauvet', 'Delaunay', 'Joubert', 'Vidal', + 'Pires', 'Blondel', 'Noel', 'Collin', 'Lucas', 'Monnier', 'Breton', 'Lejeune', 'Prevost', + 'Allard', 'Pichon', 'Le Gall', 'Lavigne', 'Roy', 'Gros', 'Chartier', 'Briand', 'Maillet', + 'Lemois', 'Dufour', 'Boutin', 'Guichard', 'Vasseur', 'Hoarau', 'Lebrun', 'Giraudet', + 'Dubois', 'Maillard', 'Millet', 'Carre', 'Coste', 'Laborde', 'Bertin', 'Moulin', + 'Turpin', 'Deschamps', 'Barthelemy', 'Descamps', 'Riviere', 'Guilbert', 'Tanguy', + 'Duchamp', 'Pasquier', 'Gaudin', 'Vial', 'Letellier', 'Meunier', 'Bouchet', 'Hebert', + 'Gosselin', 'Le Roux', 'Renou', 'Guillon', 'Delattre', 'Lefranc', 'Peltier', 'Delacroix', + 'Labbe', 'Bellanger', 'Perrier', 'Chretien', 'Bouvet', 'Ferrand', 'Vallee', 'Boulanger', + 'Vautier', 'Morvan', 'Leclercq', 'Picard', 'Jourdain', 'Cornu', 'Bodin', 'Courtois', + 'Duhamel', 'Leveque', 'Leconte', 'Aubert', 'Delaire', 'Letourneau', 'Tessier', 'Barre', + 'Fleury', 'Mallet', 'Deniau', 'Royer', 'Rigal', 'Levy', 'Bouchard', 'Charron', 'Laroche' + ] ]; - return $lastnames[array_rand($lastnames)]; + if(is_null($lang) || !isset($map_lang_firstnames[$lang])) { + $all_lastnames = array_merge( + $map_lang_lastnames['en'], + $map_lang_lastnames['fr'] + ); + + return $all_lastnames[array_rand($all_lastnames)]; + } + + return $map_lang_lastnames[$lang][array_rand($map_lang_lastnames[$lang])]; } public static function fullname(): string { From c5cc190af08de736f07d0d304c1842ecfbec97bc Mon Sep 17 00:00:00 2001 From: Lucas Laurent Date: Thu, 5 Sep 2024 10:37:19 +0200 Subject: [PATCH 09/94] Handle unique column retry to find value --- lib/equal/data/DataGenerator.class.php | 10 +- packages/core/actions/model/generate.php | 220 +++++++++++++---------- 2 files changed, 135 insertions(+), 95 deletions(-) diff --git a/lib/equal/data/DataGenerator.class.php b/lib/equal/data/DataGenerator.class.php index 39cd4edd4..3a98adfef 100644 --- a/lib/equal/data/DataGenerator.class.php +++ b/lib/equal/data/DataGenerator.class.php @@ -262,21 +262,21 @@ public static function ean13(): string { public static function username(): string { $usernames = [ - 'user', 'coolUser', 'jane_doe', 'johnnyBravo', 'theRealMike', + 'user', 'coolUser', 'jane-doe', 'johnnyBravo', 'theRealMike', 'superstar', 'gameMaster', 'techGuru', 'quickSilver', 'happyCamper', 'blueSky', 'codingWizard', 'magicMikey', 'fastTrack', 'misterX', - 'adventureSeeker', 'pixelPioneer', 'ninjaWarrior', 'starGazer', 'drSmart', - 'boldExplorer', 'zenMaster', 'risingStar', 'rocketRider', 'digitalNomad', + 'adventureSeeker', 'pixelPioneer', 'ninja-warrior', 'starGazer', 'drSmart', + 'boldExplorer', 'zenMaster', 'risingStar', 'rocket-rider', 'digitalNomad', 'echoEcho', 'nightOwl', 'lightSpeed', 'trueBeliever', 'cyberHawk', 'galacticHero', 'luckyCharm', 'urbanVibes', 'silentStorm', 'wildWanderer', 'moonWalker', 'brightStar', 'vividDreamer', 'vortexVoyager', 'infiniteLoop', 'horizonChaser', 'quickSilverFox', 'shadowKnight', 'dataMaster', 'epicQuest', 'cosmicDancer', 'virtualVictor', 'alphaBravo', 'gammaRay', 'quantumLeap', - 'alphaWolf', 'digitalDynamo', 'codeNinja', 'retroRider', 'futureFreak', + 'alphaWolf', 'digitalDynamo', 'codeNinja', 'retro-rider', 'futureFreak', 'hyperLink', 'wizardKing', 'neonNinja', 'techTitan', 'starshipPilot', 'legendaryHero', 'phantomShadow', 'urbanLegend', 'novaStar', 'daringDiva', 'trailBlazer', 'cyberChampion', 'epicGamer', 'stellarScribe', 'stormChaser', - 'lunarExplorer', 'plasmaBolt', 'infinityEdge', 'quantumQuest', 'stellarVoyager' + 'lunarExplorer', 'plasma-bolt', 'infinityEdge', 'quantumQuest', 'stellar-voyager' ]; $number = mt_rand(0, 9999); diff --git a/packages/core/actions/model/generate.php b/packages/core/actions/model/generate.php index 3ca806926..98caeb125 100644 --- a/packages/core/actions/model/generate.php +++ b/packages/core/actions/model/generate.php @@ -120,118 +120,158 @@ 'address', 'address_street', 'address_zip', 'address_city', 'address_country' ]; +$model_unique_conf = []; +if(method_exists($params['entity'], 'getUnique')) { + $model_unique_conf = (new $params['entity'])->getUnique(); +} + $schema = $model->getSchema(); foreach($schema as $field => $conf) { - if(isset($params['fields'][$field])) { - $new_entity[$field] = $params['fields'][$field]; - continue; - } - elseif(isset($conf['generator_function']) && method_exists($params['entity'], $conf['generator_function'])) { - $new_entity[$field] = $params['entity']::{$conf['generator_function']}(); - continue; - } - elseif( - in_array($field, $root_fields) - || in_array($conf['type'], ['alias', 'computed', 'one2many']) - || (in_array($conf['type'], ['many2one', 'many2many']) && !isset($params['relations'][$field])) + $field_value_forced = isset($params['fields'][$field]); + $field_has_generate_function = isset($conf['generator_function']) && method_exists($params['entity'], $conf['generator_function']); + + if( + !$field_value_forced + && !$field_has_generate_function + && ( + in_array($field, $root_fields) + || in_array($conf['type'], ['alias', 'computed', 'one2many']) + || (in_array($conf['type'], ['many2one', 'many2many']) && !isset($params['relations'][$field])) + ) ) { continue; } - if($conf['type'] === 'many2one') { - $ids = $conf['foreign_object']::search([])->ids(); - $mode = $params['relations'][$field]['mode'] ?? 'use-existing-or-create'; - if($mode === 'use-existing-or-create') { - $mode = empty($ids) || DataGenerator::boolean() ? 'create' : 'use-existing'; + $should_be_unique = $conf['unique'] ?? false; + if(!$should_be_unique && !empty($model_unique_conf)) { + foreach($model_unique_conf as $unique_conf) { + if(count($unique_conf) === 1 && $unique_conf[0] === $field) { + $should_be_unique = true; + break; + } } + } - switch($mode) { - case 'use-existing': - if(!empty($ids)) { - if(!($conf['required'] ?? false)) { - $ids[] = null; - } + $field_value_allowed = false; + $unique_retry_count = 10; + while($unique_retry_count > 0 && !$field_value_allowed) { + $unique_retry_count--; - $new_entity[$field] = $ids[array_rand($ids)]; - } - break; - case 'create': - $relation_param = [ - 'entity' => $conf['foreign_object'] - ]; - foreach(['fields', 'relations'] as $param_key) { - if(isset($params['relations'][$field][$param_key])) { - $relation_param[$param_key] = $params['relations'][$field][$param_key]; + if($field_value_forced) { + $new_entity[$field] = $params['fields'][$field]; + } + elseif($field_has_generate_function) { + $new_entity[$field] = $params['entity']::{$conf['generator_function']}(); + } + elseif($conf['type'] === 'many2one') { + $ids = $conf['foreign_object']::search([])->ids(); + $mode = $params['relations'][$field]['mode'] ?? 'use-existing-or-create'; + if($mode === 'use-existing-or-create') { + $mode = empty($ids) || DataGenerator::boolean() ? 'create' : 'use-existing'; + } + + switch($mode) { + case 'use-existing': + if(!empty($ids)) { + if(!($conf['required'] ?? false)) { + $ids[] = null; + } + + $new_entity[$field] = $ids[array_rand($ids)]; + } + break; + case 'create': + $relation_param = [ + 'entity' => $conf['foreign_object'] + ]; + foreach(['fields', 'relations'] as $param_key) { + if(isset($params['relations'][$field][$param_key])) { + $relation_param[$param_key] = $params['relations'][$field][$param_key]; + } } - } - $result = eQual::run('do', 'core_model_generate', $relation_param); - $new_entity[$field] = $result['id']; - break; + $result = eQual::run('do', 'core_model_generate', $relation_param); + $new_entity[$field] = $result['id']; + break; + } } - } - elseif($conf['type'] === 'many2many') { - $mode = $params['relations'][$field]['mode'] ?? 'use-existing-or-create'; - - $qty_conf = $params['relations'][$field]['qty'] ?? [0, 5]; - $qty = is_array($qty_conf) ? mt_rand($qty_conf[0], $qty_conf[1]) : $qty_conf; - - switch($mode) { - case 'use-existing': - $ids = $conf['foreign_object']::search([])->ids(); - $random_ids = []; - for($i = 0; $i < $qty; $i++) { - if(empty($ids)) { - break; + elseif($conf['type'] === 'many2many') { + $mode = $params['relations'][$field]['mode'] ?? 'use-existing-or-create'; + + $qty_conf = $params['relations'][$field]['qty'] ?? [0, 5]; + $qty = is_array($qty_conf) ? mt_rand($qty_conf[0], $qty_conf[1]) : $qty_conf; + + switch($mode) { + case 'use-existing': + $ids = $conf['foreign_object']::search([])->ids(); + $random_ids = []; + for($i = 0; $i < $qty; $i++) { + if(empty($ids)) { + break; + } + + $random_index = array_rand($ids); + $random_ids[] = $ids[$random_index]; + array_splice($ids, $random_index, 1); } - $random_index = array_rand($ids); - $random_ids[] = $ids[$random_index]; - array_splice($ids, $random_index, 1); - } - - if(!empty($random_ids)) { - $new_entity[$field] = $random_ids; - } - break; - case 'create': - $relation_param = [ - 'entity' => $conf['foreign_object'], - 'lang' => $params['lang'] - ]; - foreach(['fields', 'relations'] as $param_key) { - if(isset($params['relations'][$field][$param_key])) { - $relation_param[$param_key] = $params['relations'][$field][$param_key]; + if(!empty($random_ids)) { + $new_entity[$field] = $random_ids; + } + break; + case 'create': + $relation_param = [ + 'entity' => $conf['foreign_object'], + 'lang' => $params['lang'] + ]; + foreach(['fields', 'relations'] as $param_key) { + if(isset($params['relations'][$field][$param_key])) { + $relation_param[$param_key] = $params['relations'][$field][$param_key]; + } } - } - $new_relation_entities_ids = []; - for($i = 0; $i < $qty; $i++) { - $result = eQual::run('do', 'core_model_generate', $relation_param); - $new_relation_entities_ids[] = $result['id']; - } + $new_relation_entities_ids = []; + for($i = 0; $i < $qty; $i++) { + $result = eQual::run('do', 'core_model_generate', $relation_param); + $new_relation_entities_ids[] = $result['id']; + } - if(!empty($new_relation_entities_ids)) { - $new_entity[$field] = $new_relation_entities_ids; - } - break; + if(!empty($new_relation_entities_ids)) { + $new_entity[$field] = $new_relation_entities_ids; + } + break; + } } - } - elseif(isset($conf['usage'])) { - $required = $conf['required'] ?? false; - if(!$required && DataGenerator::boolean(0.1)) { - $new_entity[$field] = null; + elseif(isset($conf['usage'])) { + $required = $conf['required'] ?? false; + if(!$required && DataGenerator::boolean(0.1)) { + $new_entity[$field] = null; + } + else { + $usage = UsageFactory::create($conf['usage']); + $new_entity[$field] = $usage->generateRandomValue(); + } + } + elseif(in_array($field, $recognizable_fields)) { + $new_entity[$field] = $generateRecognizableFieldRandomValue($field); } else { - $usage = UsageFactory::create($conf['usage']); - $new_entity[$field] = $usage->generateRandomValue(); + $new_entity[$field] = $generateFieldRandomValue($conf); + } + + if($should_be_unique) { + $ids = $params['entity']::search([$field, '=', $new_entity[$field]])->ids(); + if(empty($ids)) { + $field_value_allowed = true; + } + } + else { + $field_value_allowed = true; } } - elseif(in_array($field, $recognizable_fields)) { - $new_entity[$field] = $generateRecognizableFieldRandomValue($field); - } - else { - $new_entity[$field] = $generateFieldRandomValue($conf); + + if(!$field_value_allowed) { + unset($new_entity[$field]); } } From a2f807691c80d2a6241726093ef4a6cfc0e5c918 Mon Sep 17 00:00:00 2001 From: Lucas Laurent Date: Thu, 5 Sep 2024 11:06:02 +0200 Subject: [PATCH 10/94] Enhance data generator --- lib/equal/data/DataGenerator.class.php | 381 ++++++++++++++++--------- 1 file changed, 239 insertions(+), 142 deletions(-) diff --git a/lib/equal/data/DataGenerator.class.php b/lib/equal/data/DataGenerator.class.php index 3a98adfef..bcffd43a8 100644 --- a/lib/equal/data/DataGenerator.class.php +++ b/lib/equal/data/DataGenerator.class.php @@ -78,7 +78,13 @@ public static function hexadecimal(int $length): string { } public static function email(): string { - $domains = ['example.com', 'test.com', 'demo.com', 'sample.org', 'mywebsite.net']; + $domains = [ + 'example.com', 'test.com', 'demo.com', 'sample.org', 'mywebsite.net', + 'mail.com', 'webmail.org', 'inbox.net', 'mailservice.io', 'emailprovider.co', + 'messaging.com', 'mailbox.org', 'fastmail.com', 'corpmail.net', 'freemail.org', + 'onlinemail.io', 'postbox.com', 'securemail.net', 'cloudmail.org', 'hostmail.co', + 'netmail.com', 'simplemail.io', 'yourmail.net', 'webservice.org', 'globalmail.com' + ]; $characters = '0123456789abcdefghijklmnopqrstuvwxyz'; $username_length = mt_rand(5, 10); @@ -168,17 +174,7 @@ public static function url(): string { } public static function urlTel(): string { - $generateRandomNumber = function($length) { - $number = ''; - for ($i = 0; $i < $length; $i++) { - $number .= mt_rand(0, 9); - } - return $number; - }; - - $phoneNumber = $generateRandomNumber(10); - - return 'tel:' . '+32' . $phoneNumber; + return 'tel:' . self::phoneNumberE164(); } public static function urlMailto(): string { @@ -412,7 +408,7 @@ public static function lastname($lang = null): string { ] ]; - if(is_null($lang) || !isset($map_lang_firstnames[$lang])) { + if(is_null($lang) || !isset($map_lang_lastnames[$lang])) { $all_lastnames = array_merge( $map_lang_lastnames['en'], $map_lang_lastnames['fr'] @@ -428,157 +424,258 @@ public static function fullname(): string { return sprintf('%s %s', self::firstname(), self::lastname()); } - public static function addressStreet(): string { + public static function addressStreet($lang = null): string { $number = mt_rand(1, 1200); - $streets = [ - 'Red Street', 'Blue Avenue', 'Green Lane', 'Yellow Road', - 'Orange Boulevard', 'Purple Drive', 'Pink Place', 'Brown Terrace', - 'Gray Court', 'White Crescent', 'Black Alley', 'Silver Way', - 'Gold Street', 'Copper Crescent', 'Bronze Drive', 'Platinum Road', - 'Emerald Lane', 'Ruby Street', 'Sapphire Avenue', 'Topaz Boulevard', - 'Diamond Drive', 'Jade Place', 'Onyx Terrace', 'Quartz Way', - 'Amethyst Avenue', 'Turquoise Road', 'Opal Lane', 'Amber Street', - 'Lime Boulevard', 'Violet Drive', 'Indigo Crescent', 'Teal Place', - 'Cyan Court', 'Magenta Terrace', 'Coral Way', 'Lavender Road', - 'Cherry Lane', 'Rose Avenue', 'Marigold Street', 'Daisy Boulevard', - 'Sunflower Drive', 'Iris Place', 'Lily Court', 'Poppy Way', - 'Hibiscus Terrace', 'Gardenia Road', 'Holly Lane', 'Tulip Avenue', - 'Azalea Boulevard', 'Dandelion Drive', 'Aster Street', 'Cosmos Place', - 'Bluebell Road ', 'Hyacinth Court', 'Buttercup Avenue', 'Foxglove Lane' + $map_lang_streets = [ + 'en' => [ + 'High Street', 'Main Road', 'Church Lane', 'King Street', 'Victoria Avenue', 'Queen’s Road', + 'Park Lane', 'Elm Street', 'Bridge Road', 'Cedar Drive', 'Oxford Street', 'Market Square', + 'Mill Lane', 'Greenwich Way', 'Meadow Road', 'Kingfisher Close', 'Rosemary Avenue', + 'West End', 'Holly Road', 'Sunset Boulevard', 'Newcastle Street', 'Broadway', 'Pine Hill', + 'St. James’s Street', 'Bayview Road', 'Shakespeare Avenue', 'Cloverleaf Drive', 'Springfield Road', + 'Wellington Street', 'Harrison Close', 'Golden Grove', 'Elmwood Avenue', 'Rose Lane', + 'Abbey Road', 'Silver Street', 'Orchard Way', 'Granite Road', 'Oakfield Drive', 'Riverside Walk', + 'Broad Street', 'Main Avenue', 'River Lane', 'Hampton Road', 'Baker Street', 'Long Lane', + 'Park Avenue', 'Crescent Road', 'Hilltop Drive', 'Northgate Street', 'Cleveland Avenue', + 'St. John’s Road', 'Meadowbrook Lane', 'Silverwood Drive', 'Greenfield Road', 'Windsor Crescent', + 'Castle Street', 'Fairview Avenue', 'Ridgeway Road', 'Cumberland Street', 'Abbey Lane', + 'Kingsway', 'Dove Court', 'Hollingsworth Road', 'Starlight Avenue', 'Sunrise Lane', + 'Copperfield Road', 'Thornfield Drive', 'Holly Hill', 'Fairfax Street', 'Violet Lane', + 'Laurel Avenue', 'Tudor Street', 'Bramble Road', 'Claremont Drive', 'Woodland Avenue', + 'Glenwood Road', 'Beaumont Street', 'Ridgewood Avenue', 'Daisy Lane', 'Waverley Road', + 'King’s Crescent', 'Hawthorn Road', 'Elmwood Drive', 'Haven Street', 'Meadow Street' + ], + 'fr' => [ + 'Rue de la Paix', 'Avenue des Champs-Élysées', 'Boulevard Saint-Germain', 'Place de la Concorde', + 'Rue de Rivoli', 'Avenue Victor Hugo', 'Boulevard Haussmann', 'Rue du Faubourg Saint-Honoré', + 'Avenue de l\'Opéra', 'Rue Saint-Denis', 'Place Vendôme', 'Rue de la République', + 'Boulevard Voltaire', 'Rue Lafayette', 'Avenue de la Grande Armée', 'Rue du Bac', + 'Avenue Montaigne', 'Boulevard de Sébastopol', 'Place d’Italie', 'Rue de la Fontaine', + 'Rue des Rosiers', 'Rue de Vaugirard', 'Avenue de Paris', 'Boulevard de Strasbourg', + 'Place des Vosges', 'Rue Monge', 'Avenue Foch', 'Rue de la Garenne', 'Boulevard Saint-Michel', + 'Rue de la Chapelle', 'Place du Trocadéro', 'Rue de l’Université', 'Boulevard Diderot', + 'Rue des Écoles', 'Avenue de l\'Alma', 'Rue du Temple', 'Boulevard de la Madeleine', + 'Rue du Commerce', 'Avenue des Ternes', 'Boulevard de la Liberté', 'Place des Jardins', + 'Rue du Pont-Neuf', 'Avenue de la République', 'Boulevard de la Gare', 'Rue des Fleurs', + 'Place du Marché', 'Rue du Général Leclerc', 'Avenue des Écoles', 'Boulevard du Palais', + 'Rue des Lilas', 'Avenue de la Mairie', 'Boulevard des Capucines', 'Place des Armes', + 'Rue des Peupliers', 'Avenue du Président Wilson', 'Boulevard de l’Indépendance', 'Rue des Roses', + 'Place du Général de Gaulle', 'Rue des Pommiers', 'Avenue de la Victoire', 'Boulevard de la Mer', + 'Rue des Vergers', 'Avenue des Arts', 'Boulevard de la Côte', 'Place du Château', 'Rue de la Gare', + 'Rue de l’Église', 'Avenue de la Forêt', 'Boulevard du Parc', 'Place du Soleil', 'Rue des Vignes', + 'Rue de la Liberté', 'Avenue des Érables', 'Boulevard des Temples', 'Place de la République', + 'Rue des Champs', 'Avenue des Pyramides', 'Boulevard des Pruniers', 'Place de la Bastille' + ] ]; - return $streets[array_rand($streets)] . ' ' . $number; + if(is_null($lang) || !isset($map_lang_streets[$lang])) { + $all_streets = array_merge( + $map_lang_streets['en'], + $map_lang_streets['fr'] + ); + + return $all_streets[array_rand($all_streets)] . ' ' . $number; + } + + return $map_lang_streets[$lang][array_rand($map_lang_streets[$lang])] . ' ' . $number; } public static function addressZip(): string { - return str_pad(rand(1000, 9999), 4, '0', STR_PAD_LEFT); + return mt_rand(1000, 9999); } - public static function addressCity(): string { - $cities = [ - // United States - 'New York', 'Los Angeles', 'Chicago', 'Houston', 'Phoenix', - 'Philadelphia', 'San Antonio', 'San Diego', 'Dallas', 'San Jose', - - // Canada - 'Toronto', 'Vancouver', 'Montreal', 'Calgary', 'Edmonton', - 'Ottawa', 'Winnipeg', 'Quebec City', 'Hamilton', 'Kitchener', - - // United Kingdom - 'London', 'Birmingham', 'Manchester', 'Glasgow', 'Liverpool', - 'Edinburgh', 'Leeds', 'Sheffield', 'Bristol', 'Cardiff', - - // Australia - 'Sydney', 'Melbourne', 'Brisbane', 'Perth', 'Adelaide', - 'Gold Coast', 'Canberra', 'Hobart', 'Darwin', 'Newcastle', - - // Germany - 'Berlin', 'Hamburg', 'Munich', 'Cologne', 'Frankfurt', - 'Stuttgart', 'Dusseldorf', 'Dortmund', 'Essen', 'Leipzig', - - // France - 'Paris', 'Marseille', 'Lyon', 'Toulouse', 'Nice', - 'Nantes', 'Montpellier', 'Strasbourg', 'Bordeaux', 'Lille', - - // Italy - 'Rome', 'Milan', 'Naples', 'Turin', 'Palermo', - 'Genoa', 'Bologna', 'Florence', 'Catania', 'Venice', - - // Spain - 'Madrid', 'Barcelona', 'Valencia', 'Seville', 'Zaragoza', - 'Malaga', 'Murcia', 'Palma', 'Las Palmas', 'Bilbao', - - // Belgium - 'Brussels', 'Antwerp', 'Ghent', 'Bruges', 'Liege', - 'Namur', 'Ostend', 'Leuven', 'Hasselt', 'Mechelen', - - // Netherlands - 'Amsterdam', 'Rotterdam', 'The Hague', 'Utrecht', 'Eindhoven', - 'Groningen', 'Maastricht', 'Arnhem', 'Nijmegen', 'Haarlem', - - // Switzerland - 'Zurich', 'Geneva', 'Bern', 'Basel', 'Lausanne', - 'Lucerne', 'St. Moritz', 'Zug', 'Neuchatel', 'La Chaux-de-Fonds', - - // Japan - 'Tokyo', 'Osaka', 'Kyoto', 'Nagoya', 'Hiroshima', - 'Fukuoka', 'Kobe', 'Yokohama', 'Sapporo', 'Sendai', + public static function addressCity($lang = null): string { + $map_lang_cities = [ + 'en' => [ + 'New York', 'Los Angeles', 'Chicago', 'Houston', 'Phoenix', + 'Philadelphia', 'San Antonio', 'San Diego', 'Dallas', 'San Jose', + 'Toronto', 'Vancouver', 'Montreal', 'Calgary', 'Edmonton', + 'Ottawa', 'Winnipeg', 'Quebec City', 'Hamilton', 'Kitchener', + 'London', 'Birmingham', 'Manchester', 'Glasgow', 'Liverpool', + 'Edinburgh', 'Leeds', 'Sheffield', 'Bristol', 'Cardiff', + 'Sydney', 'Melbourne', 'Brisbane', 'Perth', 'Adelaide', + 'Gold Coast', 'Canberra', 'Hobart', 'Darwin', 'Newcastle', + 'Berlin', 'Hamburg', 'Munich', 'Cologne', 'Frankfurt', + 'Stuttgart', 'Dusseldorf', 'Dortmund', 'Essen', 'Leipzig', + 'Paris', 'Marseille', 'Lyon', 'Toulouse', 'Nice', + 'Nantes', 'Montpellier', 'Strasbourg', 'Bordeaux', 'Lille', + 'Rome', 'Milan', 'Naples', 'Turin', 'Palermo', + 'Genoa', 'Bologna', 'Florence', 'Catania', 'Venice', + 'Madrid', 'Barcelona', 'Valencia', 'Seville', 'Zaragoza', + 'Malaga', 'Murcia', 'Palma', 'Las Palmas', 'Bilbao', + 'Brussels', 'Antwerp', 'Ghent', 'Bruges', 'Liege', + 'Namur', 'Ostend', 'Leuven', 'Hasselt', 'Mechelen', + 'Amsterdam', 'Rotterdam', 'The Hague', 'Utrecht', 'Eindhoven', + 'Groningen', 'Maastricht', 'Arnhem', 'Nijmegen', 'Haarlem', + 'Zurich', 'Geneva', 'Bern', 'Basel', 'Lausanne', + 'Lucerne', 'St. Moritz', 'Zug', 'Neuchatel', 'La Chaux-de-Fonds', + 'Tokyo', 'Osaka', 'Kyoto', 'Nagoya', 'Hiroshima', + 'Fukuoka', 'Kobe', 'Yokohama', 'Sapporo', 'Sendai', + 'Beijing', 'Shanghai', 'Guangzhou', 'Shenzhen', 'Chengdu', + 'Hong Kong', 'Hangzhou', 'Nanjing', 'Wuhan', 'Xi\'an', + 'Mumbai', 'Delhi', 'Bangalore', 'Hyderabad', 'Ahmedabad', + 'Chennai', 'Kolkata', 'Pune', 'Jaipur', 'Surat', + 'Sao Paulo', 'Rio de Janeiro', 'Salvador', 'Fortaleza', 'Belo Horizonte', + 'Brasilia', 'Curitiba', 'Manaus', 'Recife', 'Porto Alegre', + 'Johannesburg', 'Cape Town', 'Durban', 'Pretoria', 'Port Elizabeth', + 'Bloemfontein', 'East London', 'Polokwane', 'Nelspruit', 'Mbombela' + ], + 'fr' => [ + 'New York', 'Los Angeles', 'Chicago', 'Houston', 'Phoenix', + 'Philadelphie', 'San Antonio', 'San Diego', 'Dallas', 'San José', + 'Toronto', 'Vancouver', 'Montréal', 'Calgary', 'Edmonton', + 'Ottawa', 'Winnipeg', 'Québec', 'Hamilton', 'Kitchener', + 'Londres', 'Birmingham', 'Manchester', 'Glasgow', 'Liverpool', + 'Édimbourg', 'Leeds', 'Sheffield', 'Bristol', 'Cardiff', + 'Sydney', 'Melbourne', 'Brisbane', 'Perth', 'Adélaïde', + 'Gold Coast', 'Canberra', 'Hobart', 'Darwin', 'Newcastle', + 'Berlin', 'Hambourg', 'Munich', 'Cologne', 'Francfort', + 'Stuttgart', 'Dusseldorf', 'Dortmund', 'Essen', 'Leipzig', + 'Paris', 'Marseille', 'Lyon', 'Toulouse', 'Nice', + 'Nantes', 'Montpellier', 'Strasbourg', 'Bordeaux', 'Lille', + 'Rome', 'Milan', 'Naples', 'Turin', 'Palermo', + 'Gênes', 'Bologne', 'Florence', 'Catane', 'Venise', + 'Madrid', 'Barcelone', 'Valence', 'Séville', 'Saragosse', + 'Malaga', 'Murcie', 'Palma', 'Las Palmas', 'Bilbao', + 'Bruxelles', 'Anvers', 'Gand', 'Bruges', 'Liège', + 'Namur', 'Ostende', 'Leuven', 'Hasselt', 'Malines', + 'Amsterdam', 'Rotterdam', 'La Haye', 'Utrecht', 'Eindhoven', + 'Groningue', 'Maastricht', 'Arnhem', 'Nijmegen', 'Haarlem', + 'Zurich', 'Genève', 'Berne', 'Bâle', 'Lausanne', + 'Lucerne', 'Saint-Moritz', 'Zoug', 'Neuchâtel', 'La Chaux-de-Fonds', + 'Tokyo', 'Osaka', 'Kyoto', 'Nagoya', 'Hiroshima', + 'Fukuoka', 'Kobe', 'Yokohama', 'Sapporo', 'Sendai', + 'Pékin', 'Shanghai', 'Guangzhou', 'Shenzhen', 'Chengdu', + 'Hong Kong', 'Hangzhou', 'Nankin', 'Wuhan', 'Xi’an', + 'Mumbai', 'Delhi', 'Bangalore', 'Hyderabad', 'Ahmedabad', + 'Chennai', 'Kolkata', 'Pune', 'Jaipur', 'Surat', + 'Sao Paulo', 'Rio de Janeiro', 'Salvador', 'Fortaleza', 'Belo Horizonte', + 'Brasilia', 'Curitiba', 'Manaus', 'Recife', 'Porto Alegre', + 'Johannesbourg', 'Le Cap', 'Durban', 'Prétoria', 'Port Elizabeth', + 'Bloemfontein', 'East London', 'Polokwane', 'Nelspruit', 'Mbombela' + ] + ]; - // China - 'Beijing', 'Shanghai', 'Guangzhou', 'Shenzhen', 'Chengdu', - 'Hong Kong', 'Hangzhou', 'Nanjing', 'Wuhan', 'Xi\'an', + if(is_null($lang) || !isset($map_lang_cities[$lang])) { + $all_cities = array_merge( + $map_lang_cities['en'], + $map_lang_cities['fr'] + ); - // India - 'Mumbai', 'Delhi', 'Bangalore', 'Hyderabad', 'Ahmedabad', - 'Chennai', 'Kolkata', 'Pune', 'Jaipur', 'Surat', + return $all_cities[array_rand($all_cities)]; + } - // Brazil - 'Sao Paulo', 'Rio de Janeiro', 'Salvador', 'Fortaleza', 'Belo Horizonte', - 'Brasilia', 'Curitiba', 'Manaus', 'Recife', 'Porto Alegre', + return $map_lang_cities[$lang][array_rand($map_lang_cities[$lang])]; + } - // South Africa - 'Johannesburg', 'Cape Town', 'Durban', 'Pretoria', 'Port Elizabeth', - 'Bloemfontein', 'East London', 'Polokwane', 'Nelspruit', 'Mbombela' + public static function addressCountry($lang = null): string { + $map_lang_countries = [ + 'en' => [ + 'Afghanistan', 'Albania', 'Algeria', 'Andorra', 'Angola', + 'Antigua and Barbuda', 'Argentina', 'Armenia', 'Australia', 'Austria', + 'Azerbaijan', 'Bahamas', 'Bahrain', 'Bangladesh', 'Barbados', + 'Belarus', 'Belgium', 'Belize', 'Benin', 'Bhutan', + 'Bolivia', 'Bosnia and Herzegovina', 'Botswana', 'Brazil', 'Brunei', + 'Bulgaria', 'Burkina Faso', 'Burundi', 'Cabo Verde', 'Cambodia', + 'Cameroon', 'Canada', 'Central African Republic', 'Chad', 'Chile', + 'China', 'Colombia', 'Comoros', 'Congo, Democratic Republic of the', 'Congo, Republic of the', + 'Costa Rica', 'Croatia', 'Cuba', 'Cyprus', 'Czech Republic', + 'Denmark', 'Djibouti', 'Dominica', 'Dominican Republic', + 'East Timor', 'Ecuador', 'Egypt', 'El Salvador', 'Equatorial Guinea', + 'Eritrea', 'Estonia', 'Eswatini', 'Ethiopia', 'Fiji', + 'Finland', 'France', 'Gabon', 'Gambia', 'Georgia', + 'Germany', 'Ghana', 'Greece', 'Grenada', 'Guatemala', + 'Guinea', 'Guinea-Bissau', 'Guyana', 'Haiti', 'Honduras', + 'Hungary', 'Iceland', 'India', 'Indonesia', 'Iran', + 'Iraq', 'Ireland', 'Israel', 'Italy', 'Jamaica', + 'Japan', 'Jordan', 'Kazakhstan', 'Kenya', 'Kiribati', + 'Korea, North', 'Korea, South', 'Kosovo', 'Kuwait', 'Kyrgyzstan', + 'Laos', 'Latvia', 'Lebanon', 'Lesotho', 'Liberia', + 'Libya', 'Liechtenstein', 'Lithuania', 'Luxembourg', 'Madagascar', + 'Malawi', 'Malaysia', 'Maldives', 'Mali', 'Malta', + 'Marshall Islands', 'Mauritania', 'Mauritius', 'Mexico', 'Micronesia', + 'Moldova', 'Monaco', 'Mongolia', 'Montenegro', 'Morocco', + 'Mozambique', 'Myanmar', 'Namibia', 'Nauru', 'Nepal', + 'Netherlands', 'New Zealand', 'Nicaragua', 'Niger', 'Nigeria', + 'North Macedonia', 'Norway', 'Oman', 'Pakistan', 'Palau', + 'Panama', 'Papua New Guinea', 'Paraguay', 'Peru', 'Philippines', + 'Poland', 'Portugal', 'Qatar', 'Romania', 'Russia', + 'Rwanda', 'Saint Kitts and Nevis', 'Saint Lucia', 'Saint Vincent and the Grenadines', 'Samoa', + 'San Marino', 'Sao Tome and Principe', 'Saudi Arabia', 'Senegal', 'Serbia', + 'Seychelles', 'Sierra Leone', 'Singapore', 'Slovakia', 'Slovenia', + 'Solomon Islands', 'Somalia', 'South Africa', 'South Sudan', 'Spain', + 'Sri Lanka', 'Sudan', 'Suriname', 'Sweden', 'Switzerland', + 'Syria', 'Taiwan', 'Tajikistan', 'Tanzania', 'Thailand', + 'Timor-Leste', 'Togo', 'Tonga', 'Trinidad and Tobago', 'Tunisia', + 'Turkey', 'Turkmenistan', 'Tuvalu', 'Uganda', 'Ukraine', + 'United Arab Emirates', 'United Kingdom', 'United States', 'Uruguay', 'Uzbekistan', + 'Vanuatu', 'Vatican City', 'Venezuela', 'Vietnam', 'Yemen', + 'Zambia', 'Zimbabwe' + ], + 'fr' => [ + 'Afghanistan', 'Albanie', 'Algérie', 'Andorre', 'Angola', + 'Antigua-et-Barbuda', 'Argentine', 'Arménie', 'Australie', 'Autriche', + 'Azerbaïdjan', 'Bahamas', 'Bahreïn', 'Bangladesh', 'Barbade', + 'Bélarus', 'Belgique', 'Belize', 'Bénin', 'Bhoutan', + 'Bolivie', 'Bosnie-Herzégovine', 'Botswana', 'Brésil', 'Brunei', + 'Bulgarie', 'Burkina Faso', 'Burundi', 'Cap-Vert', 'Cambodge', + 'Cameroun', 'Canada', 'République Centrafricaine', 'Tchad', 'Chili', + 'Chine', 'Colombie', 'Comores', 'République Démocratique du Congo', 'République du Congo', + 'Côte d\'Ivoire', 'Costa Rica', 'Croatie', 'Cuba', 'Chypre', + 'République tchèque', 'Danemark', 'Djibouti', 'Dominique', 'République Dominicaine', + 'Timor oriental', 'Équateur', 'Égypte', 'El Salvador', 'Guinée équatoriale', + 'Érythrée', 'Estonie', 'Eswatini', 'Éthiopie', 'Fidji', + 'Finlande', 'France', 'Gabon', 'Gambie', 'Géorgie', + 'Allemagne', 'Ghana', 'Grèce', 'Grenade', 'Guatemala', + 'Guinée', 'Guinée-Bissau', 'Guyane', 'Haïti', 'Honduras', + 'Hongrie', 'Islande', 'Inde', 'Indonésie', 'Iran', + 'Irak', 'Irlande', 'Israël', 'Italie', 'Jamaïque', + 'Japon', 'Jordanie', 'Kazakhstan', 'Kenya', 'Kiribati', + 'Corée du Nord', 'Corée du Sud', 'Kosovo', 'Koweït', 'Kyrgyzstan', + 'Laos', 'Lettonie', 'Liban', 'Lesotho', 'Liberia', + 'Libye', 'Liechtenstein', 'Lituanie', 'Luxembourg', 'Madagascar', + 'Malawi', 'Malaisie', 'Maldives', 'Mali', 'Malte', + 'Îles Marshall', 'Mauritanie', 'Maurice', 'Mexique', 'Micronésie', + 'Moldova', 'Monaco', 'Mongolie', 'Monténégro', 'Maroc', + 'Mozambique', 'Myanmar', 'Namibie', 'Nauru', 'Népal', + 'Pays-Bas', 'Nouvelle-Zélande', 'Nicaragua', 'Niger', 'Nigeria', + 'Macédoine du Nord', 'Norvège', 'Oman', 'Pakistan', 'Palaos', + 'Panama', 'Papouasie-Nouvelle-Guinée', 'Paraguay', 'Pérou', 'Philippines', + 'Pologne', 'Portugal', 'Qatar', 'Roumanie', 'Russie', + 'Rwanda', 'Saint-Kitts-et-Nevis', 'Sainte-Lucie', 'Saint-Vincent-et-les-Grenadines', 'Samoa', + 'Saint-Marin', 'Sao Tomé-et-Principe', 'Arabie Saoudite', 'Sénégal', 'Serbie', + 'Seychelles', 'Sierra Leone', 'Singapour', 'Slovaquie', 'Slovénie', + 'Îles Salomon', 'Somalie', 'Afrique du Sud', 'Soudan du Sud', 'Espagne', + 'Sri Lanka', 'Soudan', 'Suriname', 'Suède', 'Suisse', + 'Syrie', 'Taïwan', 'Tadjikistan', 'Tanzanie', 'Thaïlande', + 'Timor-Leste', 'Togo', 'Tonga', 'Trinité-et-Tobago', 'Tunisie', + 'Turquie', 'Turkménistan', 'Tuvalu', 'Ouganda', 'Ukraine', + 'Émirats Arabes Unis', 'Royaume-Uni', 'États-Unis', 'Uruguay', 'Ouzbékistan', + 'Vanuatu', 'Vatican', 'Venezuela', 'Vietnam', 'Yémen', + 'Zambie', 'Zimbabwe' + ] ]; - return $cities[array_rand($cities)]; - } + if(is_null($lang) || !isset($map_lang_countries[$lang])) { + $all_countries = array_merge( + $map_lang_countries['en'], + $map_lang_countries['fr'] + ); - public static function addressCountry(): string { - $countries = [ - 'Afghanistan', 'Albania', 'Algeria', 'Andorra', 'Angola', - 'Antigua and Barbuda', 'Argentina', 'Armenia', 'Australia', 'Austria', - 'Azerbaijan', 'Bahamas', 'Bahrain', 'Bangladesh', 'Barbados', - 'Belarus', 'Belgium', 'Belize', 'Benin', 'Bhutan', - 'Bolivia', 'Bosnia and Herzegovina', 'Botswana', 'Brazil', 'Brunei', - 'Bulgaria', 'Burkina Faso', 'Burundi', 'Cabo Verde', 'Cambodia', - 'Cameroon', 'Canada', 'Central African Republic', 'Chad', 'Chile', - 'China', 'Colombia', 'Comoros', 'Congo, Democratic Republic of the', 'Congo, Republic of the', - 'Costa Rica', 'Croatia', 'Cuba', 'Cyprus', 'Czech Republic', - 'Denmark', 'Djibouti', 'Dominica', 'Dominican Republic', - 'East Timor', 'Ecuador', 'Egypt', 'El Salvador', 'Equatorial Guinea', - 'Eritrea', 'Estonia', 'Eswatini', 'Ethiopia', 'Fiji', - 'Finland', 'France', 'Gabon', 'Gambia', 'Georgia', - 'Germany', 'Ghana', 'Greece', 'Grenada', 'Guatemala', - 'Guinea', 'Guinea-Bissau', 'Guyana', 'Haiti', 'Honduras', - 'Hungary', 'Iceland', 'India', 'Indonesia', 'Iran', - 'Iraq', 'Ireland', 'Israel', 'Italy', 'Jamaica', - 'Japan', 'Jordan', 'Kazakhstan', 'Kenya', 'Kiribati', - 'Korea, North', 'Korea, South', 'Kosovo', 'Kuwait', 'Kyrgyzstan', - 'Laos', 'Latvia', 'Lebanon', 'Lesotho', 'Liberia', - 'Libya', 'Liechtenstein', 'Lithuania', 'Luxembourg', 'Madagascar', - 'Malawi', 'Malaysia', 'Maldives', 'Mali', 'Malta', - 'Marshall Islands', 'Mauritania', 'Mauritius', 'Mexico', 'Micronesia', - 'Moldova', 'Monaco', 'Mongolia', 'Montenegro', 'Morocco', - 'Mozambique', 'Myanmar', 'Namibia', 'Nauru', 'Nepal', - 'Netherlands', 'New Zealand', 'Nicaragua', 'Niger', 'Nigeria', - 'North Macedonia', 'Norway', 'Oman', 'Pakistan', 'Palau', - 'Panama', 'Papua New Guinea', 'Paraguay', 'Peru', 'Philippines', - 'Poland', 'Portugal', 'Qatar', 'Romania', 'Russia', - 'Rwanda', 'Saint Kitts and Nevis', 'Saint Lucia', 'Saint Vincent and the Grenadines', 'Samoa', - 'San Marino', 'Sao Tome and Principe', 'Saudi Arabia', 'Senegal', 'Serbia', - 'Seychelles', 'Sierra Leone', 'Singapore', 'Slovakia', 'Slovenia', - 'Solomon Islands', 'Somalia', 'South Africa', 'South Sudan', 'Spain', - 'Sri Lanka', 'Sudan', 'Suriname', 'Sweden', 'Switzerland', - 'Syria', 'Taiwan', 'Tajikistan', 'Tanzania', 'Thailand', - 'Timor-Leste', 'Togo', 'Tonga', 'Trinidad and Tobago', 'Tunisia', - 'Turkey', 'Turkmenistan', 'Tuvalu', 'Uganda', 'Ukraine', - 'United Arab Emirates', 'United Kingdom', 'United States', 'Uruguay', 'Uzbekistan', - 'Vanuatu', 'Vatican City', 'Venezuela', 'Vietnam', 'Yemen', - 'Zambia', 'Zimbabwe' - ]; + return $all_countries[array_rand($all_countries)]; + } - return $countries[array_rand($countries)]; + return $map_lang_countries[$lang][array_rand($map_lang_countries[$lang])]; } - public static function address(): string { + public static function address($lang = null): string { return sprintf( '%s, %s %s, %s', - self::addressStreet(), + self::addressStreet($lang), self::addressZip(), - self::addressCity(), - self::addressCountry() + self::addressCity($lang), + self::addressCountry($lang) ); } From df437527dc0d5a976a2c009662e6f21d224c92fd Mon Sep 17 00:00:00 2001 From: alexandraYesbabylon Date: Thu, 5 Sep 2024 12:08:35 +0200 Subject: [PATCH 11/94] Add missing seconds in the time for adaptOut in DataAdapterJson --- .../data/adapt/adapters/json/DataAdapterJsonTime.class.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/equal/data/adapt/adapters/json/DataAdapterJsonTime.class.php b/lib/equal/data/adapt/adapters/json/DataAdapterJsonTime.class.php index 9ef38bb6e..03d8d69a0 100644 --- a/lib/equal/data/adapt/adapters/json/DataAdapterJsonTime.class.php +++ b/lib/equal/data/adapt/adapters/json/DataAdapterJsonTime.class.php @@ -71,7 +71,8 @@ public function adaptOut($value, $usage, $locale='en') { } $hours = floor($value / 3600); $minutes = floor(($value % 3600) / 60); - return sprintf("%02d:%02d", $hours, $minutes); + $seconds = $value % (60); + return sprintf("%02d:%02d:%02d", $hours, $minutes, $seconds); } } From e89a10b1c74ad1f10cb98e42b7106e943aeab290 Mon Sep 17 00:00:00 2001 From: Lucas Laurent Date: Thu, 5 Sep 2024 14:37:50 +0200 Subject: [PATCH 12/94] Enhance generate + Add anonymize --- lib/equal/data/DataGenerator.class.php | 63 +++++++++- packages/core/actions/model/anonymize.php | 135 ++++++++++++++++++++++ packages/core/actions/model/generate.php | 97 +++------------- 3 files changed, 213 insertions(+), 82 deletions(-) create mode 100644 packages/core/actions/model/anonymize.php diff --git a/lib/equal/data/DataGenerator.class.php b/lib/equal/data/DataGenerator.class.php index bcffd43a8..191cb0f6c 100644 --- a/lib/equal/data/DataGenerator.class.php +++ b/lib/equal/data/DataGenerator.class.php @@ -7,8 +7,67 @@ namespace equal\data; +use equal\orm\UsageFactory; + class DataGenerator { + /** + * @return array|bool|float|int|mixed|string|null + */ + public static function generateByFieldConf(string $field, array $field_conf, string $lang = null) { + if(isset($conf['usage'])) { + try { + $usage = UsageFactory::create($conf['usage']); + return $usage->generateRandomValue(); + } + catch(\Exception $e) { + // Usage problem + } + } + + switch($field) { + case 'username': + return self::username(); + case 'firstname': + return self::firstname($lang); + case 'lastname': + return self::lastname($lang); + case 'fullname': + return self::fullname($lang); + case 'address_street': + return self::addressStreet($lang); + case 'address_zip': + return self::addressZip(); + case 'address_city': + return self::addressCity($lang); + case 'address_country': + return self::addressCountry($lang); + case 'address': + return self::address($lang); + } + + switch($field_conf['type']) { + case 'string': + if(!empty($field_conf['selection'])) { + $values = array_values($field_conf['selection']); + return $values[array_rand($values)]; + } + elseif(isset($field_conf['default'])) { + return $field_conf['default']; + } + + return self::plainText(); + case 'boolean': + return self::boolean(); + case 'integer': + return self::integer(9); + case 'float': + return self::realNumber(9, 2); + } + + return null; + } + public static function plainText($max_length = 255): string { $words = [ 'lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', @@ -420,8 +479,8 @@ public static function lastname($lang = null): string { return $map_lang_lastnames[$lang][array_rand($map_lang_lastnames[$lang])]; } - public static function fullname(): string { - return sprintf('%s %s', self::firstname(), self::lastname()); + public static function fullname($lang = null): string { + return sprintf('%s %s', self::firstname($lang), self::lastname($lang)); } public static function addressStreet($lang = null): string { diff --git a/packages/core/actions/model/anonymize.php b/packages/core/actions/model/anonymize.php new file mode 100644 index 000000000..9ac655c14 --- /dev/null +++ b/packages/core/actions/model/anonymize.php @@ -0,0 +1,135 @@ + + Some Rights Reserved, Cedric Francoys, 2010-2021 + Licensed under GNU LGPL 3 license +*/ + +use equal\data\DataGenerator; + +list($params, $providers) = eQual::announce([ + 'description' => "Anonymize an existing object with random data and given values.", + 'params' => [ + 'entity' => [ + 'description' => 'Full name (including namespace) of the class to return (e.g. \'core\\User\').', + 'type' => 'string', + 'required' => true + ], + 'fields' => [ + 'description' => 'List of fields that must be anonymized.', + 'type' => 'array', + 'default' => [] + ], + 'relations' => [ + 'description' => 'How to handle the object relations (many2one and one2many)', + 'type' => 'array', + 'default' => [] + ], + 'lang' => [ + 'description ' => 'Specific language for multilang field.', + 'type' => 'string', + 'default' => constant('DEFAULT_LANG') + ], + 'domain' => [ + 'description' => 'Criteria that results have to match (series of conjunctions)', + 'type' => 'array', + 'default' => [] + ] + ], + 'response' => [ + 'content-type' => 'application/json', + 'charset' => 'UTF-8', + 'accept-origin' => '*' + ], + 'access' => [ + 'visibility' => 'protected' + ], + 'constants' => ['DEFAULT_LANG'], + 'providers' => ['context', 'orm'] +]); + +/** + * @var \equal\php\Context $context + * @var \equal\orm\ObjectManager $orm + */ +list('context' => $context, 'orm' => $orm) = $providers; + +$anonymized_values = []; + +$model = $orm->getModel($params['entity']); +if(!$model) { + throw new Exception("unknown_entity", QN_ERROR_INVALID_PARAM); +} + +$schema = $model->getSchema(); + +$ids = $params['entity']::search($params['domain'])->ids(); +foreach($ids as $id) { + foreach($schema as $field => $conf) { + if(!($conf['sensitive'] ?? false) && !in_array($field, $params['fields'])) { + continue; + } + + if(isset($conf['generate']) && method_exists($params['entity'], $conf['generate'])) { + $anonymized_values[$field] = $params['entity']::{$conf['generate']}(); + } + else { + $required = $conf['required'] ?? false; + if(!$required && DataGenerator::boolean(0.05)) { + $anonymized_values[$field] = null; + } + else { + $anonymized_values[$field] = DataGenerator::generateByFieldConf($field, $conf, $params['lang']); + } + } + } + + if(!empty($anonymized_values)) { + $instance = $params['entity']::id($id) + ->update($anonymized_values) + ->read(['id']) + ->first(); + } + + foreach($schema as $field => $conf) { + if(!in_array($conf['type'], ['one2many', 'many2one']) || !isset($params['relations'][$field])) { + continue; + } + + $relation_params = [ + 'entity' => $conf['foreign_object'], + 'lang' => $params['lang'] + ]; + + foreach(['fields', 'relations', 'domain'] as $param_key) { + if(isset($params['relations'][$field][$param_key])) { + $relation_params[$param_key] = $params['relations'][$field][$param_key]; + } + } + + if(!isset($relation_params['domain'])) { + $relation_params['domain'] = []; + } + + switch($conf['type']) { + case 'one2many': + $relation_params['domain'][] = [$conf['foreign_field'], '=', $id]; + break; + case 'many2one': + $instance = $params['entity']::id($id) + ->read([$field]) + ->first(); + + if(!is_null($instance[$field])) { + $relation_params['domain'][] = ['id', '=', $instance[$field]]; + } + break; + } + + eQual::run('do', 'core_model_anonymize', $relation_params); + } +} + +$context->httpResponse() + ->status(204) + ->send(); diff --git a/packages/core/actions/model/generate.php b/packages/core/actions/model/generate.php index 98caeb125..e50bb472b 100644 --- a/packages/core/actions/model/generate.php +++ b/packages/core/actions/model/generate.php @@ -51,58 +51,6 @@ */ list('context' => $context, 'orm' => $orm) = $providers; -/** - * Methods - */ - -$generateRecognizableFieldRandomValue = function($field) { - switch($field) { - case 'username': - return DataGenerator::username(); - case 'firstname': - return DataGenerator::firstname(); - case 'lastname': - return DataGenerator::lastname(); - case 'fullname': - return DataGenerator::fullname(); - case 'address_street': - return DataGenerator::addressStreet(); - case 'address_zip': - return DataGenerator::addressZip(); - case 'address_city': - return DataGenerator::addressCity(); - case 'address_country': - return DataGenerator::addressCountry(); - case 'address': - return DataGenerator::address(); - } - - return null; -}; - -$generateFieldRandomValue = function($field_conf) { - switch($field_conf['type']) { - case 'string': - if(!empty($field_conf['selection'])) { - $values = array_values($field_conf['selection']); - return $values[array_rand($values)]; - } - elseif(isset($field_conf['default'])) { - return $field_conf['default']; - } - - return DataGenerator::plainText(); - case 'boolean': - return DataGenerator::boolean(); - case 'integer': - return DataGenerator::integer(9); - case 'float': - return DataGenerator::realNumber(9, 2); - } - - return null; -}; - /** * Action */ @@ -115,10 +63,6 @@ } $root_fields = ['id', 'creator', 'created', 'modifier', 'modified', 'deleted', 'state']; -$recognizable_fields = [ - 'username', 'firstname', 'lastname', 'fullname', - 'address', 'address_street', 'address_zip', 'address_city', 'address_country' -]; $model_unique_conf = []; if(method_exists($params['entity'], 'getUnique')) { @@ -128,7 +72,7 @@ $schema = $model->getSchema(); foreach($schema as $field => $conf) { $field_value_forced = isset($params['fields'][$field]); - $field_has_generate_function = isset($conf['generator_function']) && method_exists($params['entity'], $conf['generator_function']); + $field_has_generate_function = isset($conf['generate']) && method_exists($params['entity'], $conf['generate']); if( !$field_value_forced @@ -161,7 +105,7 @@ $new_entity[$field] = $params['fields'][$field]; } elseif($field_has_generate_function) { - $new_entity[$field] = $params['entity']::{$conf['generator_function']}(); + $new_entity[$field] = $params['entity']::{$conf['generate']}(); } elseif($conf['type'] === 'many2one') { $ids = $conf['foreign_object']::search([])->ids(); @@ -181,16 +125,16 @@ } break; case 'create': - $relation_param = [ + $relation_params = [ 'entity' => $conf['foreign_object'] ]; foreach(['fields', 'relations'] as $param_key) { if(isset($params['relations'][$field][$param_key])) { - $relation_param[$param_key] = $params['relations'][$field][$param_key]; + $relation_params[$param_key] = $params['relations'][$field][$param_key]; } } - $result = eQual::run('do', 'core_model_generate', $relation_param); + $result = eQual::run('do', 'core_model_generate', $relation_params); $new_entity[$field] = $result['id']; break; } @@ -220,19 +164,19 @@ } break; case 'create': - $relation_param = [ + $relation_params = [ 'entity' => $conf['foreign_object'], 'lang' => $params['lang'] ]; foreach(['fields', 'relations'] as $param_key) { if(isset($params['relations'][$field][$param_key])) { - $relation_param[$param_key] = $params['relations'][$field][$param_key]; + $relation_params[$param_key] = $params['relations'][$field][$param_key]; } } $new_relation_entities_ids = []; for($i = 0; $i < $qty; $i++) { - $result = eQual::run('do', 'core_model_generate', $relation_param); + $result = eQual::run('do', 'core_model_generate', $relation_params); $new_relation_entities_ids[] = $result['id']; } @@ -242,22 +186,15 @@ break; } } - elseif(isset($conf['usage'])) { + else { $required = $conf['required'] ?? false; - if(!$required && DataGenerator::boolean(0.1)) { + if(!$required && DataGenerator::boolean(0.05)) { $new_entity[$field] = null; } else { - $usage = UsageFactory::create($conf['usage']); - $new_entity[$field] = $usage->generateRandomValue(); + $new_entity[$field] = DataGenerator::generateByFieldConf($field, $conf, $params['lang']); } } - elseif(in_array($field, $recognizable_fields)) { - $new_entity[$field] = $generateRecognizableFieldRandomValue($field); - } - else { - $new_entity[$field] = $generateFieldRandomValue($conf); - } if($should_be_unique) { $ids = $params['entity']::search([$field, '=', $new_entity[$field]])->ids(); @@ -287,24 +224,24 @@ $qty_conf = $params['relations'][$field]['qty'] ?? [0, 3]; $qty = is_array($qty_conf) ? mt_rand($qty_conf[0], $qty_conf[1]) : $qty_conf; - $relation_param = [ + $relation_params = [ 'entity' => $conf['foreign_object'], 'lang' => $params['lang'] ]; foreach(['fields', 'relations'] as $param_key) { if(isset($params['relations'][$field][$param_key])) { - $relation_param[$param_key] = $params['relations'][$field][$param_key]; + $relation_params[$param_key] = $params['relations'][$field][$param_key]; } } - if(!isset($relation_param['fields'])) { - $relation_param['fields'] = []; + if(!isset($relation_params['fields'])) { + $relation_params['fields'] = []; } - $relation_param['fields'][$conf['foreign_field']] = $instance['id']; + $relation_params['fields'][$conf['foreign_field']] = $instance['id']; $new_relation_entities_ids = []; for($i = 0; $i < $qty; $i++) { - $result = eQual::run('do', 'core_model_generate', $relation_param); + $result = eQual::run('do', 'core_model_generate', $relation_params); $new_relation_entities_ids[] = $result['id']; } From 60fd9bf84d48740770a02946e22d69db2ec6f679 Mon Sep 17 00:00:00 2001 From: Lucas Laurent Date: Thu, 5 Sep 2024 16:04:47 +0200 Subject: [PATCH 13/94] Add action to anonymize data of a package using a configuration file --- packages/core/actions/package/anonymize.php | 75 +++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 packages/core/actions/package/anonymize.php diff --git a/packages/core/actions/package/anonymize.php b/packages/core/actions/package/anonymize.php new file mode 100644 index 000000000..1cb21652b --- /dev/null +++ b/packages/core/actions/package/anonymize.php @@ -0,0 +1,75 @@ + + Some Rights Reserved, Cedric Francoys, 2010-2021 + Licensed under GNU LGPL 3 license +*/ + +list($params, $providers) = eQual::announce([ + 'description' => "Anonymize an existing object with random data and given values.", + 'params' => [ + 'package' => [ + 'description' => 'Name of the package to anonymize.', + 'type' => 'string', + 'required' => true + ], + 'config_file' => [ + 'description' => 'Name of the configuration file to use to anonymize data.', + 'help' => 'Configuration file must match the format {package}/anonymize/{config_file}.json', + 'type' => 'string', + 'required' => true + ] + ], + 'response' => [ + 'content-type' => 'application/json', + 'charset' => 'UTF-8', + 'accept-origin' => '*' + ], + 'access' => [ + 'visibility' => 'protected' + ], + 'providers' => ['context'] +]); + +/** + * @var \equal\php\Context $context + */ +list('context' => $context) = $providers; + +/** + * Methods + */ + +$getAnonymizeConfig = function($config_file_path): array { + $config_file_content = file_get_contents($config_file_path); + if(!$config_file_content) { + throw new Exception('Missing anonymization config file ' . $config_file_path, EQ_ERROR_INVALID_CONFIG); + } + + $import_config = json_decode($config_file_content, true); + if(!is_array($import_config)) { + throw new Exception('Invalid anonymization configuration file', EQ_ERROR_INVALID_CONFIG); + } + + return $import_config; +}; + +/** + * Action + */ + +$entities_config = $getAnonymizeConfig( + sprintf('%s/packages/%s/anonymize/%s.json', QN_BASEDIR, $params['package'], $params['config_file']) +); + +if(!isset($entities_config[0])) { + $entities_config = [$entities_config]; +} + +foreach($entities_config as $entity_config) { + eQual::run('do', 'core_model_anonymize', $entity_config); +} + +$context->httpResponse() + ->status(204) + ->send(); From 59221c805490379abaa27d5f6f8fa71144c6e8da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Fran=C3=A7oys?= Date: Thu, 5 Sep 2024 21:30:35 +0200 Subject: [PATCH 14/94] changed loading error scope as ORM --- lib/equal/orm/ObjectManager.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/equal/orm/ObjectManager.class.php b/lib/equal/orm/ObjectManager.class.php index a19c3464b..cce897687 100644 --- a/lib/equal/orm/ObjectManager.class.php +++ b/lib/equal/orm/ObjectManager.class.php @@ -1266,8 +1266,8 @@ public function getModel($class) { $model = $this->getStaticInstance($class); } catch(Exception $e) { - trigger_error($e->getMessage(), QN_REPORT_ERROR); - // #memo - another autoload handler might be registered, so no exception can be raised + // #memo - another autoload handler might be registered, so no exception must be raised here + trigger_error('ORM::'.$e->getMessage(), QN_REPORT_ERROR); } return $model; } From 983aa2dffda778b937a7cec15d0f1da5f9087fbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Fran=C3=A7oys?= Date: Thu, 5 Sep 2024 21:31:23 +0200 Subject: [PATCH 15/94] added static keyword in phpdoc for compliance with PHP 8.0 --- lib/equal/orm/Model.class.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/equal/orm/Model.class.php b/lib/equal/orm/Model.class.php index d122085b4..31ea98805 100644 --- a/lib/equal/orm/Model.class.php +++ b/lib/equal/orm/Model.class.php @@ -13,10 +13,10 @@ * This class holds the description of an object along with the values of the currently loaded/assigned fields. * * List of static methods for building new Collection objects (accessed through magic methods): - * @method \equal\orm\Collection id($id) - * @method \equal\orm\Collection ids(array $ids=[]) - * @method \equal\orm\Collection search(array $domain=[], array $params=[], $lang=null) - * @method \equal\orm\Collection create(array $values=null, $lang=null) + * @method static \equal\orm\Collection id($id) + * @method static \equal\orm\Collection static ids(array $ids=[]) + * @method static \equal\orm\Collection static search(array $domain=[], array $params=[], $lang=null) + * @method static \equal\orm\Collection static create(array $values=null, $lang=null) * * List of static methods with variable parameters: * (These methods are handled through __callStatic method to prevent PHP strict errors & aot warnings.) @@ -143,7 +143,7 @@ private function setDefaults($values=[]) { $this->values[$field] = $defaults[$field](); } else { - // do not call since there is an ambiguity (e.g. 'time') + // do not call since there is an ambiguity with scalar (e.g. 'time') $this->values[$field] = $defaults[$field]; } } @@ -210,7 +210,7 @@ public function offsetGet($field) { * Iterator methods */ - public function rewind() : void { + public function rewind(): void { reset($this->values); } @@ -218,15 +218,15 @@ public function current() { return current($this->values); } - public function key() : string { + public function key(): string { return key($this->values); } - public function next() : void { + public function next(): void { next($this->values); } - public function valid() : bool { + public function valid(): bool { $key = key($this->values); $res = ($key !== null && $key !== false); return $res; From 56576d56771a2b4b4299b14f546ecc83bb7970d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Fran=C3=A7oys?= Date: Thu, 5 Sep 2024 21:31:46 +0200 Subject: [PATCH 16/94] fixed test related to JSON time output --- packages/core/tests/adapters.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/tests/adapters.php b/packages/core/tests/adapters.php index 3b084213e..6ccc8c11b 100644 --- a/packages/core/tests/adapters.php +++ b/packages/core/tests/adapters.php @@ -472,7 +472,7 @@ return $result; }, 'assert' => function($result) { - return ($result === '10:00'); + return ($result === '10:00:00'); } ], '4108' => [ From 54c121015a770ef37900ba7f9839be04a8d1e307 Mon Sep 17 00:00:00 2001 From: Lucas Laurent Date: Fri, 6 Sep 2024 09:50:21 +0200 Subject: [PATCH 17/94] Add protocol to url generate --- lib/equal/data/DataGenerator.class.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/equal/data/DataGenerator.class.php b/lib/equal/data/DataGenerator.class.php index 191cb0f6c..9d2c99482 100644 --- a/lib/equal/data/DataGenerator.class.php +++ b/lib/equal/data/DataGenerator.class.php @@ -203,9 +203,12 @@ public static function relativeUrl(): string { return $url_path; } - public static function url(): string { + public static function url($protocol = null): string { $protocols = ['http', 'https', 'ldap', 'dns', 'ftp']; - $protocol = $protocols[array_rand($protocols)]; + + if(is_null($protocol)) { + $protocol = $protocols[array_rand($protocols)]; + } $domain_length = mt_rand(3, 10); $path_depth = mt_rand(0, 5); From d231728a54dde552ff099f1d3d7dbe602c216757 Mon Sep 17 00:00:00 2001 From: Lucas Laurent Date: Fri, 6 Sep 2024 10:01:55 +0200 Subject: [PATCH 18/94] Fix bug --- lib/equal/data/DataGenerator.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/equal/data/DataGenerator.class.php b/lib/equal/data/DataGenerator.class.php index 9d2c99482..3f1514949 100644 --- a/lib/equal/data/DataGenerator.class.php +++ b/lib/equal/data/DataGenerator.class.php @@ -15,9 +15,9 @@ class DataGenerator { * @return array|bool|float|int|mixed|string|null */ public static function generateByFieldConf(string $field, array $field_conf, string $lang = null) { - if(isset($conf['usage'])) { + if(isset($field_conf['usage'])) { try { - $usage = UsageFactory::create($conf['usage']); + $usage = UsageFactory::create($field_conf['usage']); return $usage->generateRandomValue(); } catch(\Exception $e) { From 92ac4c7c915ac82b400a03b18ea3a79a958d1509 Mon Sep 17 00:00:00 2001 From: Lucas Laurent Date: Fri, 6 Sep 2024 10:44:54 +0200 Subject: [PATCH 19/94] Fix string with selection generate --- lib/equal/data/DataGenerator.class.php | 7 ++++++- packages/core/actions/model/generate.php | 1 - 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/equal/data/DataGenerator.class.php b/lib/equal/data/DataGenerator.class.php index 3f1514949..5a2a646b7 100644 --- a/lib/equal/data/DataGenerator.class.php +++ b/lib/equal/data/DataGenerator.class.php @@ -49,7 +49,12 @@ public static function generateByFieldConf(string $field, array $field_conf, str switch($field_conf['type']) { case 'string': if(!empty($field_conf['selection'])) { - $values = array_values($field_conf['selection']); + if(isset($field_conf['selection'][0])) { + $values = array_values($field_conf['selection']); + } + else { + $values = array_keys($field_conf['selection']); + } return $values[array_rand($values)]; } elseif(isset($field_conf['default'])) { diff --git a/packages/core/actions/model/generate.php b/packages/core/actions/model/generate.php index e50bb472b..1108311e8 100644 --- a/packages/core/actions/model/generate.php +++ b/packages/core/actions/model/generate.php @@ -6,7 +6,6 @@ */ use equal\data\DataGenerator; -use equal\orm\UsageFactory; list($params, $providers) = eQual::announce([ 'description' => "Generate a new object with random data and given values.", From 23bf2ffe2a2997a8ace9520ebdf7354c9a69f6fd Mon Sep 17 00:00:00 2001 From: Lucas Laurent Date: Fri, 6 Sep 2024 11:22:31 +0200 Subject: [PATCH 20/94] Add legal name to data generator --- lib/equal/data/DataGenerator.class.php | 74 ++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/lib/equal/data/DataGenerator.class.php b/lib/equal/data/DataGenerator.class.php index 5a2a646b7..03d3a893a 100644 --- a/lib/equal/data/DataGenerator.class.php +++ b/lib/equal/data/DataGenerator.class.php @@ -34,6 +34,8 @@ public static function generateByFieldConf(string $field, array $field_conf, str return self::lastname($lang); case 'fullname': return self::fullname($lang); + case 'legal_name': + return self::legalName($lang); case 'address_street': return self::addressStreet($lang); case 'address_zip': @@ -491,6 +493,78 @@ public static function fullname($lang = null): string { return sprintf('%s %s', self::firstname($lang), self::lastname($lang)); } + public static function legalName($lang = null): string { + $map_lang_legalNames = [ + 'en' => [ + 'Pinnacle Innovations Ltd.', 'GlobalTech Solutions LLC', + 'Blue Horizon Enterprises Inc.', 'Silverline Consulting Group', + 'Evergreen Holdings Co.', 'Brightway Capital Partners', + 'NextGen Software Corp.', 'Summit Legal Advisors LLP', + 'Aspire Marketing Associates', 'Crestview Financial Services', + 'Vanguard Media Networks', 'Oceanic Development Group', + 'Phoenix Engineering Systems', 'Cascade Resource Management', + 'Momentum Logistics Group', 'EagleRock Ventures LLC', + 'Fusion Health & Wellness Inc.', 'Broadway Research Institute', + 'Skyline Construction Co.', 'Peak Performance Consulting', + 'Heritage Legal Solutions LLP', 'Infinity Investment Trust', + 'Starlight Communications Inc.', 'Aurora Environmental Services', + 'Velocity Tech Innovators LLC', 'Redwood Analytics Solutions', + 'Everest Property Management', 'Sapphire Insurance Group Ltd.', + 'Nova Digital Media Inc.', 'Paramount Energy Solutions', + 'UrbanCore Construction Co.', 'TerraNova Agricultural Services', + 'Quantum Financial Consulting', 'Synergy Global Partners', + 'Horizon Security Solutions LLC', 'NorthStar Investments Group', + 'Zenith Technology Ventures', 'PrimeCare Medical Solutions', + 'Aurora Legal Advisory Group', 'Lakeside Manufacturing Co.', + 'Falcon Ridge Asset Management', 'Emerald Coast Logistics LLC', + 'Atlas Legal Services LLP', 'Titanium Energy Systems', + 'Genesis Marketing Innovations', 'Renaissance Capital Advisors', + 'TrueNorth Consulting Group', 'BluePeak Industries Inc.', + 'Omega Transportation Services', 'GoldenBridge Infrastructure Co.', + 'Sunrise Financial Holdings', 'Pioneer Technology Group LLC' + ], + 'fr' => [ + 'Solutions Pinnacle SARL', 'GlobalTech Innovations SAS', + 'Horizon Bleu Entreprises SA', 'Consulting Silverline Group', + 'Evergreen Holdings SARL', 'Capital Brightway Partners', + 'NextGen Software Corp.', 'Conseil Juridique Summit SCP', + 'Aspire Marketing Associés', 'Services Financiers Crestview', + 'Vanguard Médias Réseaux SARL', 'Groupe de Développement Océanique', + 'Phoenix Ingénierie Systèmes', 'Gestion des Ressources Cascade', + 'Momentum Logistique SARL', 'EagleRock Ventures SAS', + 'Fusion Santé & Bien-être Inc.', 'Institut de Recherche Broadway', + 'Construction Skyline SARL', 'Performance Consulting Peak', + 'Solutions Juridiques Héritage SCP', 'Infinity Investissement Trust', + 'Communications Starlight Inc.', 'Services Environnementaux Aurora', + 'Innovateurs Technologiques Velocity SAS', 'Alpha Conseil et Stratégie', + 'Groupe Nexus Immobilier', 'Aquitaine Développement Durable', + 'Astéria Informatique SARL', 'Helios Technologies SAS', + 'Artisanat Moderne & Tradition', 'Biotech Avancée France SA', + 'Espace Vert Solutions', 'Union des Commerçants Réunis', + 'Nouvelle Génération Énergies', 'Réseaux Mondial Média', + 'EcoSys Environnement SAS', 'Orion Énergie Renouvelable', + 'Alliance Pharmaceutique France', 'Cœur de Ville Projets Urbains', + 'Pôle d\'Innovation Numérique', 'Ciel Bleu Aviation SARL', + 'Altura Services Financiers', 'Vision Logistique Internationale', + 'Verdi Éco-Construction SARL', 'Genèse Création et Design', + 'Solstice Groupe Juridique', 'TechnoVentures France', + 'Esprit Nature & Bien-être SARL', 'Voies Nouvelles Mobilité SAS', + 'Groupe Horizon Santé SAS' + ] + ]; + + if(is_null($lang) || !isset($map_lang_legalNames[$lang])) { + $all_legal_names = array_merge( + $map_lang_legalNames['en'], + $map_lang_legalNames['fr'] + ); + + return $all_legal_names[array_rand($all_legal_names)]; + } + + return $map_lang_legalNames[$lang][array_rand($map_lang_legalNames[$lang])]; + } + public static function addressStreet($lang = null): string { $number = mt_rand(1, 1200); From e68347291545ad3249752c54764cf203dc70beb5 Mon Sep 17 00:00:00 2001 From: Lucas Laurent Date: Fri, 6 Sep 2024 11:35:42 +0200 Subject: [PATCH 21/94] Handle min max for string generation --- lib/equal/data/DataGenerator.class.php | 4 ++-- lib/equal/orm/usages/UsageText.class.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/equal/data/DataGenerator.class.php b/lib/equal/data/DataGenerator.class.php index 03d3a893a..cf3aeeae8 100644 --- a/lib/equal/data/DataGenerator.class.php +++ b/lib/equal/data/DataGenerator.class.php @@ -75,7 +75,7 @@ public static function generateByFieldConf(string $field, array $field_conf, str return null; } - public static function plainText($max_length = 255): string { + public static function plainText($min = 0, $max = 255): string { $words = [ 'lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing', 'elit', 'sed', 'do', 'eiusmod', 'tempor', @@ -94,7 +94,7 @@ public static function plainText($max_length = 255): string { return ucfirst(implode(' ', $sentence)) . '.'; }; - $random_length = mt_rand(0, $max_length); + $random_length = mt_rand($min, $max); $random_text = ''; while (strlen($random_text) < $random_length) { diff --git a/lib/equal/orm/usages/UsageText.class.php b/lib/equal/orm/usages/UsageText.class.php index 84b37db42..33d45976a 100644 --- a/lib/equal/orm/usages/UsageText.class.php +++ b/lib/equal/orm/usages/UsageText.class.php @@ -103,7 +103,7 @@ public function getConstraints(): array { } public function generateRandomValue(): string { - return DataGenerator::plainText($this->getLength()); + return DataGenerator::plainText($this->getMin(), $this->getMax()); } } From a093a3241a958ef283943e45430a27909e755a4c Mon Sep 17 00:00:00 2001 From: Lucas Laurent Date: Fri, 6 Sep 2024 15:17:31 +0200 Subject: [PATCH 22/94] Fix text usage generate value when no min or max --- lib/equal/orm/usages/UsageText.class.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/equal/orm/usages/UsageText.class.php b/lib/equal/orm/usages/UsageText.class.php index 33d45976a..8fa3d7f01 100644 --- a/lib/equal/orm/usages/UsageText.class.php +++ b/lib/equal/orm/usages/UsageText.class.php @@ -103,7 +103,12 @@ public function getConstraints(): array { } public function generateRandomValue(): string { - return DataGenerator::plainText($this->getMin(), $this->getMax()); + $max = $this->getMax(); + if($max === 0) { + $max = $this->getLength(); + } + + return DataGenerator::plainText($this->getMin(), $max); } } From 4cf2bdf07f32851569c48c8d6848f064e23e18f6 Mon Sep 17 00:00:00 2001 From: Lucas Laurent Date: Fri, 6 Sep 2024 16:14:44 +0200 Subject: [PATCH 23/94] handle global domain data --- packages/core/actions/init/package.php | 2 +- packages/core/actions/model/generate.php | 33 ++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/packages/core/actions/init/package.php b/packages/core/actions/init/package.php index bcad482ff..c7b9196ab 100644 --- a/packages/core/actions/init/package.php +++ b/packages/core/actions/init/package.php @@ -225,7 +225,7 @@ 'entity' => $entity, 'lang' => $class['lang'] ]; - foreach(['fields', 'relations'] as $param_key) { + foreach(['fields', 'relations', 'add_to_domain_data'] as $param_key) { if(isset($class[$param_key])) { $generate_params[$param_key] = $class[$param_key]; } diff --git a/packages/core/actions/model/generate.php b/packages/core/actions/model/generate.php index 1108311e8..be1055bed 100644 --- a/packages/core/actions/model/generate.php +++ b/packages/core/actions/model/generate.php @@ -6,6 +6,7 @@ */ use equal\data\DataGenerator; +use equal\orm\Domain; list($params, $providers) = eQual::announce([ 'description' => "Generate a new object with random data and given values.", @@ -26,6 +27,14 @@ 'default' => [] ], + 'add_to_domain_data' => [ + 'description' => 'Data to add to global domain data.', + 'type' => 'array' + ], + 'domain_data' => [ + 'description' => 'Global domain data.', + 'type' => 'array' + ], 'lang' => [ 'description ' => 'Specific language for multilang field.', 'type' => 'string', @@ -107,6 +116,13 @@ $new_entity[$field] = $params['entity']::{$conf['generate']}(); } elseif($conf['type'] === 'many2one') { + $domain = []; + if($conf['domain']) { + $domain = (new Domain($conf['domain'])) + ->parse(array_merge($new_entity, $params['domain_data'] ?? [])) + ->toArray(); + } + $ids = $conf['foreign_object']::search([])->ids(); $mode = $params['relations'][$field]['mode'] ?? 'use-existing-or-create'; if($mode === 'use-existing-or-create') { @@ -211,10 +227,23 @@ } } +$field_to_read = []; +if(isset($params['add_to_domain_data'])) { + $field_to_read = array_values($params['add_to_domain_data']); +} + $instance = $params['entity']::create($new_entity, $params['lang']) + ->read($field_to_read) ->adapt('json') ->first(true); +$domain_data = $params['domain_data'] ?? []; +foreach($params['add_to_domain_data'] ?? [] as $key => $field) { + if(isset($instance[$field])) { + $domain_data[$key] = $instance[$field]; + } +} + foreach($schema as $field => $conf) { if($conf['type'] !== 'one2many' || !isset($params['relations'][$field])) { continue; @@ -238,6 +267,10 @@ } $relation_params['fields'][$conf['foreign_field']] = $instance['id']; + if(!empty($domain_data)) { + $relation_params['domain_data'] = $domain_data; + } + $new_relation_entities_ids = []; for($i = 0; $i < $qty; $i++) { $result = eQual::run('do', 'core_model_generate', $relation_params); From 986dad7a3595c663e1b2904ee837d04f7c540b5a Mon Sep 17 00:00:00 2001 From: Lucas Laurent Date: Mon, 9 Sep 2024 09:27:05 +0200 Subject: [PATCH 24/94] Fix bug global domain data --- packages/core/actions/model/generate.php | 57 ++++++++++++++---------- 1 file changed, 34 insertions(+), 23 deletions(-) diff --git a/packages/core/actions/model/generate.php b/packages/core/actions/model/generate.php index be1055bed..99514cf8c 100644 --- a/packages/core/actions/model/generate.php +++ b/packages/core/actions/model/generate.php @@ -28,7 +28,7 @@ ], 'add_to_domain_data' => [ - 'description' => 'Data to add to global domain data.', + 'description' => 'Global domain data.', 'type' => 'array' ], 'domain_data' => [ @@ -59,10 +59,6 @@ */ list('context' => $context, 'orm' => $orm) = $providers; -/** - * Action - */ - $new_entity = []; $model = $orm->getModel($params['entity']); @@ -117,13 +113,13 @@ } elseif($conf['type'] === 'many2one') { $domain = []; - if($conf['domain']) { - $domain = (new Domain($conf['domain'])) + if($params['relations'][$field]['domain']) { + $domain = (new Domain($params['relations'][$field]['domain'])) ->parse(array_merge($new_entity, $params['domain_data'] ?? [])) ->toArray(); } - $ids = $conf['foreign_object']::search([])->ids(); + $ids = $conf['foreign_object']::search($domain)->ids(); $mode = $params['relations'][$field]['mode'] ?? 'use-existing-or-create'; if($mode === 'use-existing-or-create') { $mode = empty($ids) || DataGenerator::boolean() ? 'create' : 'use-existing'; @@ -140,16 +136,20 @@ } break; case 'create': - $relation_params = [ + $model_generate_params = [ 'entity' => $conf['foreign_object'] ]; foreach(['fields', 'relations'] as $param_key) { if(isset($params['relations'][$field][$param_key])) { - $relation_params[$param_key] = $params['relations'][$field][$param_key]; + $model_generate_params[$param_key] = $params['relations'][$field][$param_key]; } } - $result = eQual::run('do', 'core_model_generate', $relation_params); + if(!empty($params['domain_data'])) { + $model_generate_params['domain_data'] = $params['domain_data']; + } + + $result = eQual::run('do', 'core_model_generate', $model_generate_params); $new_entity[$field] = $result['id']; break; } @@ -162,7 +162,14 @@ switch($mode) { case 'use-existing': - $ids = $conf['foreign_object']::search([])->ids(); + $domain = []; + if($params['relations'][$field]['domain']) { + $domain = (new Domain($params['relations'][$field]['domain'])) + ->parse(array_merge($new_entity, $params['domain_data'] ?? [])) + ->toArray(); + } + + $ids = $conf['foreign_object']::search($domain)->ids(); $random_ids = []; for($i = 0; $i < $qty; $i++) { if(empty($ids)) { @@ -179,22 +186,26 @@ } break; case 'create': - $relation_params = [ + $model_generate_params = [ 'entity' => $conf['foreign_object'], 'lang' => $params['lang'] ]; foreach(['fields', 'relations'] as $param_key) { if(isset($params['relations'][$field][$param_key])) { - $relation_params[$param_key] = $params['relations'][$field][$param_key]; + $model_generate_params[$param_key] = $params['relations'][$field][$param_key]; } } $new_relation_entities_ids = []; for($i = 0; $i < $qty; $i++) { - $result = eQual::run('do', 'core_model_generate', $relation_params); + $result = eQual::run('do', 'core_model_generate', $model_generate_params); $new_relation_entities_ids[] = $result['id']; } + if(!empty($params['domain_data'])) { + $model_generate_params['domain_data'] = $params['domain_data']; + } + if(!empty($new_relation_entities_ids)) { $new_entity[$field] = $new_relation_entities_ids; } @@ -252,28 +263,28 @@ $qty_conf = $params['relations'][$field]['qty'] ?? [0, 3]; $qty = is_array($qty_conf) ? mt_rand($qty_conf[0], $qty_conf[1]) : $qty_conf; - $relation_params = [ + $model_generate_params = [ 'entity' => $conf['foreign_object'], 'lang' => $params['lang'] ]; - foreach(['fields', 'relations'] as $param_key) { + foreach(['fields', 'relations', 'add_to_domain_data'] as $param_key) { if(isset($params['relations'][$field][$param_key])) { - $relation_params[$param_key] = $params['relations'][$field][$param_key]; + $model_generate_params[$param_key] = $params['relations'][$field][$param_key]; } } - if(!isset($relation_params['fields'])) { - $relation_params['fields'] = []; + if(!isset($model_generate_params['fields'])) { + $model_generate_params['fields'] = []; } - $relation_params['fields'][$conf['foreign_field']] = $instance['id']; + $model_generate_params['fields'][$conf['foreign_field']] = $instance['id']; if(!empty($domain_data)) { - $relation_params['domain_data'] = $domain_data; + $model_generate_params['domain_data'] = $domain_data; } $new_relation_entities_ids = []; for($i = 0; $i < $qty; $i++) { - $result = eQual::run('do', 'core_model_generate', $relation_params); + $result = eQual::run('do', 'core_model_generate', $model_generate_params); $new_relation_entities_ids[] = $result['id']; } From 65ffb9024f336a1395076417ca38fdccbf162ce1 Mon Sep 17 00:00:00 2001 From: Lucas Laurent Date: Mon, 9 Sep 2024 11:38:32 +0200 Subject: [PATCH 25/94] Simplify code --- packages/core/actions/model/generate.php | 125 +++++++++++++---------- 1 file changed, 69 insertions(+), 56 deletions(-) diff --git a/packages/core/actions/model/generate.php b/packages/core/actions/model/generate.php index 99514cf8c..5c1008617 100644 --- a/packages/core/actions/model/generate.php +++ b/packages/core/actions/model/generate.php @@ -74,23 +74,23 @@ } $schema = $model->getSchema(); -foreach($schema as $field => $conf) { +foreach($schema as $field => $field_conf) { $field_value_forced = isset($params['fields'][$field]); - $field_has_generate_function = isset($conf['generate']) && method_exists($params['entity'], $conf['generate']); + $field_has_generate_function = isset($field_conf['generate']) && method_exists($params['entity'], $field_conf['generate']); if( !$field_value_forced && !$field_has_generate_function && ( in_array($field, $root_fields) - || in_array($conf['type'], ['alias', 'computed', 'one2many']) - || (in_array($conf['type'], ['many2one', 'many2many']) && !isset($params['relations'][$field])) + || in_array($field_conf['type'], ['alias', 'computed', 'one2many']) + || in_array($field_conf['type'], ['many2one', 'many2many']) ) ) { continue; } - $should_be_unique = $conf['unique'] ?? false; + $should_be_unique = $field_conf['unique'] ?? false; if(!$should_be_unique && !empty($model_unique_conf)) { foreach($model_unique_conf as $unique_conf) { if(count($unique_conf) === 1 && $unique_conf[0] === $field) { @@ -109,18 +109,52 @@ $new_entity[$field] = $params['fields'][$field]; } elseif($field_has_generate_function) { - $new_entity[$field] = $params['entity']::{$conf['generate']}(); + $new_entity[$field] = $params['entity']::{$field_conf['generate']}(); } - elseif($conf['type'] === 'many2one') { + else { + $required = $field_conf['required'] ?? false; + if(!$required && DataGenerator::boolean(0.05)) { + $new_entity[$field] = null; + } + else { + $new_entity[$field] = DataGenerator::generateByFieldConf($field, $field_conf, $params['lang']); + } + } + + if($should_be_unique) { + $ids = $params['entity']::search([$field, '=', $new_entity[$field]])->ids(); + if(empty($ids)) { + $field_value_allowed = true; + } + } + else { + $field_value_allowed = true; + } + } + + if(!$field_value_allowed) { + unset($new_entity[$field]); + } +} + +foreach($params['relations'] as $field => $relation_conf) { + if(!isset($schema[$field]) || !in_array($schema[$field]['type'], ['many2one', 'many2many'])) { + continue; + } + + $field_conf = $schema[$field]; + + switch($field_conf['type']) { + case 'many2one': $domain = []; - if($params['relations'][$field]['domain']) { - $domain = (new Domain($params['relations'][$field]['domain'])) + if($relation_conf['domain']) { + $domain = (new Domain($relation_conf['domain'])) ->parse(array_merge($new_entity, $params['domain_data'] ?? [])) ->toArray(); } - $ids = $conf['foreign_object']::search($domain)->ids(); - $mode = $params['relations'][$field]['mode'] ?? 'use-existing-or-create'; + $ids = $field_conf['foreign_object']::search($domain)->ids(); + $mode = $relation_conf['mode'] ?? 'use-existing-or-create'; if($mode === 'use-existing-or-create') { $mode = empty($ids) || DataGenerator::boolean() ? 'create' : 'use-existing'; } @@ -128,7 +162,7 @@ switch($mode) { case 'use-existing': if(!empty($ids)) { - if(!($conf['required'] ?? false)) { + if(!($field_conf['required'] ?? false)) { $ids[] = null; } @@ -137,11 +171,11 @@ break; case 'create': $model_generate_params = [ - 'entity' => $conf['foreign_object'] + 'entity' => $field_conf['foreign_object'] ]; foreach(['fields', 'relations'] as $param_key) { - if(isset($params['relations'][$field][$param_key])) { - $model_generate_params[$param_key] = $params['relations'][$field][$param_key]; + if(isset($relation_conf[$param_key])) { + $model_generate_params[$param_key] = $relation_conf[$param_key]; } } @@ -153,23 +187,23 @@ $new_entity[$field] = $result['id']; break; } - } - elseif($conf['type'] === 'many2many') { - $mode = $params['relations'][$field]['mode'] ?? 'use-existing-or-create'; + break; + case 'many2many': + $mode = $relation_conf['mode'] ?? 'use-existing-or-create'; - $qty_conf = $params['relations'][$field]['qty'] ?? [0, 5]; + $qty_conf = $relation_conf['qty'] ?? [0, 5]; $qty = is_array($qty_conf) ? mt_rand($qty_conf[0], $qty_conf[1]) : $qty_conf; switch($mode) { case 'use-existing': $domain = []; - if($params['relations'][$field]['domain']) { - $domain = (new Domain($params['relations'][$field]['domain'])) + if($relation_conf['domain']) { + $domain = (new Domain($relation_conf['domain'])) ->parse(array_merge($new_entity, $params['domain_data'] ?? [])) ->toArray(); } - $ids = $conf['foreign_object']::search($domain)->ids(); + $ids = $field_conf['foreign_object']::search($domain)->ids(); $random_ids = []; for($i = 0; $i < $qty; $i++) { if(empty($ids)) { @@ -187,12 +221,12 @@ break; case 'create': $model_generate_params = [ - 'entity' => $conf['foreign_object'], + 'entity' => $field_conf['foreign_object'], 'lang' => $params['lang'] ]; foreach(['fields', 'relations'] as $param_key) { - if(isset($params['relations'][$field][$param_key])) { - $model_generate_params[$param_key] = $params['relations'][$field][$param_key]; + if(isset($relation_conf[$param_key])) { + $model_generate_params[$param_key] = $relation_conf[$param_key]; } } @@ -211,30 +245,7 @@ } break; } - } - else { - $required = $conf['required'] ?? false; - if(!$required && DataGenerator::boolean(0.05)) { - $new_entity[$field] = null; - } - else { - $new_entity[$field] = DataGenerator::generateByFieldConf($field, $conf, $params['lang']); - } - } - - if($should_be_unique) { - $ids = $params['entity']::search([$field, '=', $new_entity[$field]])->ids(); - if(empty($ids)) { - $field_value_allowed = true; - } - } - else { - $field_value_allowed = true; - } - } - - if(!$field_value_allowed) { - unset($new_entity[$field]); + break; } } @@ -255,28 +266,30 @@ } } -foreach($schema as $field => $conf) { - if($conf['type'] !== 'one2many' || !isset($params['relations'][$field])) { +foreach($params['relations'] as $field => $relation_conf) { + if(!isset($schema[$field]) || $schema[$field]['type'] !== 'one2many') { continue; } - $qty_conf = $params['relations'][$field]['qty'] ?? [0, 3]; + $field_conf = $schema[$field]; + + $qty_conf = $relation_conf['qty'] ?? [0, 3]; $qty = is_array($qty_conf) ? mt_rand($qty_conf[0], $qty_conf[1]) : $qty_conf; $model_generate_params = [ - 'entity' => $conf['foreign_object'], + 'entity' => $field_conf['foreign_object'], 'lang' => $params['lang'] ]; foreach(['fields', 'relations', 'add_to_domain_data'] as $param_key) { - if(isset($params['relations'][$field][$param_key])) { - $model_generate_params[$param_key] = $params['relations'][$field][$param_key]; + if(isset($relation_conf[$param_key])) { + $model_generate_params[$param_key] = $relation_conf[$param_key]; } } if(!isset($model_generate_params['fields'])) { $model_generate_params['fields'] = []; } - $model_generate_params['fields'][$conf['foreign_field']] = $instance['id']; + $model_generate_params['fields'][$field_conf['foreign_field']] = $instance['id']; if(!empty($domain_data)) { $model_generate_params['domain_data'] = $domain_data; From 5b2f649431ba60adef580c83171ba31bde88e148 Mon Sep 17 00:00:00 2001 From: Lucas Laurent Date: Mon, 9 Sep 2024 12:45:16 +0200 Subject: [PATCH 26/94] Refacto using methods to simplify --- packages/core/actions/model/generate.php | 153 +++++++++++++---------- 1 file changed, 89 insertions(+), 64 deletions(-) diff --git a/packages/core/actions/model/generate.php b/packages/core/actions/model/generate.php index 5c1008617..499eeae12 100644 --- a/packages/core/actions/model/generate.php +++ b/packages/core/actions/model/generate.php @@ -59,6 +59,90 @@ */ list('context' => $context, 'orm' => $orm) = $providers; +/** + * Methods + */ + +$generateMany2One = function($field_conf, $relation_conf, $lang, $domain_data) { + $model_generate_params = [ + 'entity' => $field_conf['foreign_object'], + 'lang' => $lang + ]; + foreach(['fields', 'relations'] as $param_key) { + if(isset($relation_conf[$param_key])) { + $model_generate_params[$param_key] = $relation_conf[$param_key]; + } + } + + if(!empty($domain_data)) { + $model_generate_params['domain_data'] = $domain_data; + } + + $result = eQual::run('do', 'core_model_generate', $model_generate_params); + + return $result['id']; +}; + +$generateMany2Many = function($qty, $field_conf, $relation_conf, $lang, $domain_data) { + $model_generate_params = [ + 'entity' => $field_conf['foreign_object'], + 'lang' => $lang + ]; + foreach(['fields', 'relations'] as $param_key) { + if(isset($relation_conf[$param_key])) { + $model_generate_params[$param_key] = $relation_conf[$param_key]; + } + } + + if(!empty($domain_data)) { + $model_generate_params['domain_data'] = $domain_data; + } + + $new_relation_entities_ids = []; + for($i = 0; $i < $qty; $i++) { + $result = eQual::run('do', 'core_model_generate', $model_generate_params); + $new_relation_entities_ids[] = $result['id']; + } + + return $new_relation_entities_ids; +}; + +$generateOne2Many = function($id, $field_conf, $relation_conf, $lang, $domain_data) { + $qty_conf = $relation_conf['qty'] ?? [0, 3]; + $qty = is_array($qty_conf) ? mt_rand($qty_conf[0], $qty_conf[1]) : $qty_conf; + + $model_generate_params = [ + 'entity' => $field_conf['foreign_object'], + 'lang' => $lang + ]; + foreach(['fields', 'relations', 'add_to_domain_data'] as $param_key) { + if(isset($relation_conf[$param_key])) { + $model_generate_params[$param_key] = $relation_conf[$param_key]; + } + } + + if(!isset($model_generate_params['fields'])) { + $model_generate_params['fields'] = []; + } + $model_generate_params['fields'][$field_conf['foreign_field']] = $id; + + if(!empty($domain_data)) { + $model_generate_params['domain_data'] = $domain_data; + } + + $new_relation_entities_ids = []; + for($i = 0; $i < $qty; $i++) { + $result = eQual::run('do', 'core_model_generate', $model_generate_params); + $new_relation_entities_ids[] = $result['id']; + } + + return $new_relation_entities_ids; +}; + +/** + * Action + */ + $new_entity = []; $model = $orm->getModel($params['entity']); @@ -83,8 +167,7 @@ && !$field_has_generate_function && ( in_array($field, $root_fields) - || in_array($field_conf['type'], ['alias', 'computed', 'one2many']) - || in_array($field_conf['type'], ['many2one', 'many2many']) + || in_array($field_conf['type'], ['alias', 'computed', 'one2many', 'many2one', 'many2many']) ) ) { continue; @@ -170,21 +253,9 @@ } break; case 'create': - $model_generate_params = [ - 'entity' => $field_conf['foreign_object'] - ]; - foreach(['fields', 'relations'] as $param_key) { - if(isset($relation_conf[$param_key])) { - $model_generate_params[$param_key] = $relation_conf[$param_key]; - } - } + $new_relation_entity_id = $generateMany2One($field_conf, $relation_conf, $params['lang'], $params['domain_data'] ?? []); - if(!empty($params['domain_data'])) { - $model_generate_params['domain_data'] = $params['domain_data']; - } - - $result = eQual::run('do', 'core_model_generate', $model_generate_params); - $new_entity[$field] = $result['id']; + $new_entity[$field] = $new_relation_entity_id; break; } break; @@ -220,25 +291,7 @@ } break; case 'create': - $model_generate_params = [ - 'entity' => $field_conf['foreign_object'], - 'lang' => $params['lang'] - ]; - foreach(['fields', 'relations'] as $param_key) { - if(isset($relation_conf[$param_key])) { - $model_generate_params[$param_key] = $relation_conf[$param_key]; - } - } - - $new_relation_entities_ids = []; - for($i = 0; $i < $qty; $i++) { - $result = eQual::run('do', 'core_model_generate', $model_generate_params); - $new_relation_entities_ids[] = $result['id']; - } - - if(!empty($params['domain_data'])) { - $model_generate_params['domain_data'] = $params['domain_data']; - } + $new_relation_entities_ids = $generateMany2Many($qty, $field_conf, $relation_conf, $params['lang'], $params['domain_data'] ?? []); if(!empty($new_relation_entities_ids)) { $new_entity[$field] = $new_relation_entities_ids; @@ -271,35 +324,7 @@ continue; } - $field_conf = $schema[$field]; - - $qty_conf = $relation_conf['qty'] ?? [0, 3]; - $qty = is_array($qty_conf) ? mt_rand($qty_conf[0], $qty_conf[1]) : $qty_conf; - - $model_generate_params = [ - 'entity' => $field_conf['foreign_object'], - 'lang' => $params['lang'] - ]; - foreach(['fields', 'relations', 'add_to_domain_data'] as $param_key) { - if(isset($relation_conf[$param_key])) { - $model_generate_params[$param_key] = $relation_conf[$param_key]; - } - } - - if(!isset($model_generate_params['fields'])) { - $model_generate_params['fields'] = []; - } - $model_generate_params['fields'][$field_conf['foreign_field']] = $instance['id']; - - if(!empty($domain_data)) { - $model_generate_params['domain_data'] = $domain_data; - } - - $new_relation_entities_ids = []; - for($i = 0; $i < $qty; $i++) { - $result = eQual::run('do', 'core_model_generate', $model_generate_params); - $new_relation_entities_ids[] = $result['id']; - } + $new_relation_entities_ids = $generateOne2Many($instance['id'], $schema[$field], $relation_conf, $params['lang'], $domain_data); if(!empty($new_relation_entities_ids)) { $new_entity[$field] = $new_relation_entities_ids; From 1ff0883063eb53c37645fae21ac5f400b2e2fe83 Mon Sep 17 00:00:00 2001 From: Lucas Laurent Date: Mon, 9 Sep 2024 14:13:11 +0200 Subject: [PATCH 27/94] Remove seeding from init package.php --- packages/core/actions/init/package.php | 56 +++++++++----------------- 1 file changed, 19 insertions(+), 37 deletions(-) diff --git a/packages/core/actions/init/package.php b/packages/core/actions/init/package.php index c7b9196ab..947586a95 100644 --- a/packages/core/actions/init/package.php +++ b/packages/core/actions/init/package.php @@ -190,50 +190,32 @@ $objects_ids = []; - if(isset($class['data'])) { - foreach($class['data'] as $odata) { - foreach($odata as $field => $value) { - if(!isset($schema[$field])) { - $reporter->warning("ORM::unknown field {$field} in json file '{$json_file}'."); - continue; - } - $f = new Field($schema[$field]); - $odata[$field] = $adapter->adaptIn($value, $f->getUsage()); + foreach($class['data'] as $odata) { + foreach($odata as $field => $value) { + if(!isset($schema[$field])) { + $reporter->warning("ORM::unknown field {$field} in json file '{$json_file}'."); + continue; } + $f = new Field($schema[$field]); + $odata[$field] = $adapter->adaptIn($value, $f->getUsage()); + } - if(isset($odata['id'])) { - $res = $orm->search($entity, ['id', '=', $odata['id']]); - if($res > 0 && count($res)) { - // object already exists (either values or language might differ) - } - else { - $orm->create($entity, ['id' => $odata['id']], $lang, false); - } - $id = $odata['id']; - unset($odata['id']); + if(isset($odata['id'])) { + $res = $orm->search($entity, ['id', '=', $odata['id']]); + if($res > 0 && count($res)) { + // object already exists (either values or language might differ) } else { - $id = $orm->create($entity, [], $lang); - } - $orm->update($entity, $id, $odata, $lang); - $objects_ids[] = $id; - } - } - elseif(isset($class['qty'])) { - $qty = is_array($class['qty']) ? mt_rand($class['qty'][0], $class['qty'][1]) : $class['qty']; - $generate_params = [ - 'entity' => $entity, - 'lang' => $class['lang'] - ]; - foreach(['fields', 'relations', 'add_to_domain_data'] as $param_key) { - if(isset($class[$param_key])) { - $generate_params[$param_key] = $class[$param_key]; + $orm->create($entity, ['id' => $odata['id']], $lang, false); } + $id = $odata['id']; + unset($odata['id']); } - - for($i = 0; $i < $qty; $i++) { - eQual::run('do', 'core_model_generate', $generate_params); + else { + $id = $orm->create($entity, [], $lang); } + $orm->update($entity, $id, $odata, $lang); + $objects_ids[] = $id; } // force a first generation of computed fields, if any From f7e5af52318c2d0abc5a68f5e6acff1eb093549e Mon Sep 17 00:00:00 2001 From: Lucas Laurent Date: Mon, 9 Sep 2024 14:54:21 +0200 Subject: [PATCH 28/94] Move seed from init/package.php to package/seed.php --- packages/core/actions/package/anonymize.php | 3 +- packages/core/actions/package/seed.php | 81 +++++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 packages/core/actions/package/seed.php diff --git a/packages/core/actions/package/anonymize.php b/packages/core/actions/package/anonymize.php index 1cb21652b..37702f03c 100644 --- a/packages/core/actions/package/anonymize.php +++ b/packages/core/actions/package/anonymize.php @@ -11,11 +11,12 @@ 'package' => [ 'description' => 'Name of the package to anonymize.', 'type' => 'string', + 'usage' => 'orm/package', 'required' => true ], 'config_file' => [ 'description' => 'Name of the configuration file to use to anonymize data.', - 'help' => 'Configuration file must match the format {package}/anonymize/{config_file}.json', + 'help' => 'Configuration file must match the format "{package}/anonymize/{config_file}.json"', 'type' => 'string', 'required' => true ] diff --git a/packages/core/actions/package/seed.php b/packages/core/actions/package/seed.php new file mode 100644 index 000000000..c6624c24f --- /dev/null +++ b/packages/core/actions/package/seed.php @@ -0,0 +1,81 @@ + + Some Rights Reserved, Cedric Francoys, 2010-2021 + Licensed under GNU LGPL 3 license +*/ + +list($params, $providers) = eQual::announce([ + 'description' => 'Seed data for package using json configuration files in "package/init/seed/".', + 'params' => [ + 'package' => [ + 'description' => 'Name of the package to anonymize.', + 'type' => 'string', + 'usage' => 'orm/package', + 'required' => true + ] + ], + 'response' => [ + 'content-type' => 'application/json', + 'charset' => 'UTF-8', + 'accept-origin' => '*' + ], + 'access' => [ + 'visibility' => 'protected' + ], + 'constants' => ['DEFAULT_LANG'], + 'providers' => ['context', 'orm'] +]); + +/** + * @var \equal\php\Context $context + * @var \equal\orm\ObjectManager $orm + */ +list('context' => $context, 'orm' => $orm) = $providers; + +$data_folder = "packages/{$params['package']}/init/seed"; + +if(file_exists($data_folder) && is_dir($data_folder)) { + // handle JSON files + foreach(glob("$data_folder/*.json") as $json_file) { + $data = file_get_contents($json_file); + $classes = json_decode($data, true); + if(!$classes) { + continue; + } + foreach($classes as $class) { + $entity = $class['name'] ?? null; + if(!$entity) { + continue; + } + $lang = $class['lang'] ?? constant('DEFAULT_LANG'); + $model = $orm->getModel($entity); + $schema = $model->getSchema(); + + $objects_ids = []; + + if(!isset($class['qty'])) { + continue; + } + + $qty = is_array($class['qty']) ? mt_rand($class['qty'][0], $class['qty'][1]) : $class['qty']; + $generate_params = [ + 'entity' => $entity, + 'lang' => $class['lang'] + ]; + foreach(['fields', 'relations', 'add_to_domain_data'] as $param_key) { + if(isset($class[$param_key])) { + $generate_params[$param_key] = $class[$param_key]; + } + } + + for($i = 0; $i < $qty; $i++) { + eQual::run('do', 'core_model_generate', $generate_params); + } + } + } +} + +$context->httpResponse() + ->status(201) + ->send(); From 37f064e702e539ba514c4e53c8597debd4f313b3 Mon Sep 17 00:00:00 2001 From: Lucas Laurent Date: Mon, 9 Sep 2024 16:44:12 +0200 Subject: [PATCH 29/94] Handle add domain for next relation --- packages/core/actions/model/generate.php | 43 ++++++++++++++---------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/packages/core/actions/model/generate.php b/packages/core/actions/model/generate.php index 499eeae12..d60f88703 100644 --- a/packages/core/actions/model/generate.php +++ b/packages/core/actions/model/generate.php @@ -78,9 +78,7 @@ $model_generate_params['domain_data'] = $domain_data; } - $result = eQual::run('do', 'core_model_generate', $model_generate_params); - - return $result['id']; + return eQual::run('do', 'core_model_generate', $model_generate_params); }; $generateMany2Many = function($qty, $field_conf, $relation_conf, $lang, $domain_data) { @@ -98,13 +96,12 @@ $model_generate_params['domain_data'] = $domain_data; } - $new_relation_entities_ids = []; + $results = []; for($i = 0; $i < $qty; $i++) { - $result = eQual::run('do', 'core_model_generate', $model_generate_params); - $new_relation_entities_ids[] = $result['id']; + $results[] = eQual::run('do', 'core_model_generate', $model_generate_params); } - return $new_relation_entities_ids; + return $results; }; $generateOne2Many = function($id, $field_conf, $relation_conf, $lang, $domain_data) { @@ -130,13 +127,12 @@ $model_generate_params['domain_data'] = $domain_data; } - $new_relation_entities_ids = []; + $results = []; for($i = 0; $i < $qty; $i++) { - $result = eQual::run('do', 'core_model_generate', $model_generate_params); - $new_relation_entities_ids[] = $result['id']; + $results[] = eQual::run('do', 'core_model_generate', $model_generate_params); } - return $new_relation_entities_ids; + return $results; }; /** @@ -253,9 +249,9 @@ } break; case 'create': - $new_relation_entity_id = $generateMany2One($field_conf, $relation_conf, $params['lang'], $params['domain_data'] ?? []); + $result = $generateMany2One($field_conf, $relation_conf, $params['lang'], $params['domain_data'] ?? []); - $new_entity[$field] = $new_relation_entity_id; + $new_entity[$field] = $result['id']; break; } break; @@ -291,8 +287,9 @@ } break; case 'create': - $new_relation_entities_ids = $generateMany2Many($qty, $field_conf, $relation_conf, $params['lang'], $params['domain_data'] ?? []); + $results = $generateMany2Many($qty, $field_conf, $relation_conf, $params['lang'], $params['domain_data'] ?? []); + $new_relation_entities_ids = array_column($results, 'id'); if(!empty($new_relation_entities_ids)) { $new_entity[$field] = $new_relation_entities_ids; } @@ -324,16 +321,28 @@ continue; } - $new_relation_entities_ids = $generateOne2Many($instance['id'], $schema[$field], $relation_conf, $params['lang'], $domain_data); + $results = $generateOne2Many($instance['id'], $schema[$field], $relation_conf, $params['lang'], $domain_data); + $new_relation_entities_ids = array_column($results, 'id'); if(!empty($new_relation_entities_ids)) { $new_entity[$field] = $new_relation_entities_ids; } + + $i = 0; + foreach($results as $result) { + foreach(array_keys($relation_conf['add_to_domain_data']) as $key) { + if(isset($result['domain_data'][$key])) { + $domain_data["$field.$i.$key"] = $result['domain_data'][$key]; + } + } + $i++; + } } $result = [ - 'entity' => $params['entity'], - 'id' => $instance['id'] + 'entity' => $params['entity'], + 'id' => $instance['id'], + 'domain_data' => $domain_data ]; $context->httpResponse() From c3fa9b5e960c7d6a7555c60085156aa5f3761396 Mon Sep 17 00:00:00 2001 From: Lucas Laurent Date: Tue, 10 Sep 2024 08:21:27 +0200 Subject: [PATCH 30/94] Handle relation with computed type --- packages/core/actions/model/generate.php | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/core/actions/model/generate.php b/packages/core/actions/model/generate.php index d60f88703..bde61b782 100644 --- a/packages/core/actions/model/generate.php +++ b/packages/core/actions/model/generate.php @@ -217,13 +217,13 @@ } foreach($params['relations'] as $field => $relation_conf) { - if(!isset($schema[$field]) || !in_array($schema[$field]['type'], ['many2one', 'many2many'])) { + $field_conf = $schema[$field] ?? null; + $field_type = $field_conf['result_type'] ?? $field_conf['type'] ?? null; + if(is_null($field_conf) || !in_array($field_type, ['many2one', 'many2many'])) { continue; } - $field_conf = $schema[$field]; - - switch($field_conf['type']) { + switch($field_type) { case 'many2one': $domain = []; if($relation_conf['domain']) { @@ -317,11 +317,13 @@ } foreach($params['relations'] as $field => $relation_conf) { - if(!isset($schema[$field]) || $schema[$field]['type'] !== 'one2many') { + $field_conf = $schema[$field] ?? null; + $field_type = $field_conf['result_type'] ?? $field_conf['type'] ?? null; + if(is_null($field_conf) || $field_type !== 'one2many') { continue; } - $results = $generateOne2Many($instance['id'], $schema[$field], $relation_conf, $params['lang'], $domain_data); + $results = $generateOne2Many($instance['id'], $field_conf, $relation_conf, $params['lang'], $domain_data); $new_relation_entities_ids = array_column($results, 'id'); if(!empty($new_relation_entities_ids)) { From ac69f9365e5284a2a936044fcfdffd97a81e665f Mon Sep 17 00:00:00 2001 From: Lucas Laurent Date: Tue, 10 Sep 2024 10:12:14 +0200 Subject: [PATCH 31/94] Modify structure to simplify + Allow to seed a specif file only --- .../actions/{package => init}/anonymize.php | 50 +++++++------------ .../core/actions/{package => init}/seed.php | 10 ++++ 2 files changed, 28 insertions(+), 32 deletions(-) rename packages/core/actions/{package => init}/anonymize.php (55%) rename packages/core/actions/{package => init}/seed.php (83%) diff --git a/packages/core/actions/package/anonymize.php b/packages/core/actions/init/anonymize.php similarity index 55% rename from packages/core/actions/package/anonymize.php rename to packages/core/actions/init/anonymize.php index 37702f03c..de92fde99 100644 --- a/packages/core/actions/package/anonymize.php +++ b/packages/core/actions/init/anonymize.php @@ -16,9 +16,9 @@ ], 'config_file' => [ 'description' => 'Name of the configuration file to use to anonymize data.', - 'help' => 'Configuration file must match the format "{package}/anonymize/{config_file}.json"', - 'type' => 'string', - 'required' => true + 'help' => 'Configuration file must match the format "{package}/init/anonymize/{config_file}.json".' + . ' If no config file specified, then all files of anonymize folder are used.', + 'type' => 'string' ] ], 'response' => [ @@ -37,38 +37,24 @@ */ list('context' => $context) = $providers; -/** - * Methods - */ - -$getAnonymizeConfig = function($config_file_path): array { - $config_file_content = file_get_contents($config_file_path); - if(!$config_file_content) { - throw new Exception('Missing anonymization config file ' . $config_file_path, EQ_ERROR_INVALID_CONFIG); - } - - $import_config = json_decode($config_file_content, true); - if(!is_array($import_config)) { - throw new Exception('Invalid anonymization configuration file', EQ_ERROR_INVALID_CONFIG); - } - - return $import_config; -}; +$data_folder = "packages/{$params['package']}/init/anonymize"; -/** - * Action - */ +if(file_exists($data_folder) && is_dir($data_folder)) { + // handle JSON files + foreach(glob("$data_folder/*.json") as $json_file) { + if(isset($params['config_file']) && $params['config_file'] !== $json_file) { + continue; + } -$entities_config = $getAnonymizeConfig( - sprintf('%s/packages/%s/anonymize/%s.json', QN_BASEDIR, $params['package'], $params['config_file']) -); + $entities_config = file_get_contents($json_file); + if(!empty($entities_config) && !isset($entities_config[0])) { + $entities_config = [$entities_config]; + } -if(!isset($entities_config[0])) { - $entities_config = [$entities_config]; -} - -foreach($entities_config as $entity_config) { - eQual::run('do', 'core_model_anonymize', $entity_config); + foreach($entities_config as $entity_config) { + eQual::run('do', 'core_model_anonymize', $entity_config); + } + } } $context->httpResponse() diff --git a/packages/core/actions/package/seed.php b/packages/core/actions/init/seed.php similarity index 83% rename from packages/core/actions/package/seed.php rename to packages/core/actions/init/seed.php index c6624c24f..65c7eae7b 100644 --- a/packages/core/actions/package/seed.php +++ b/packages/core/actions/init/seed.php @@ -13,6 +13,12 @@ 'type' => 'string', 'usage' => 'orm/package', 'required' => true + ], + 'config_file' => [ + 'description' => 'Name of the configuration file to use to anonymize data.', + 'help' => 'Configuration file must match the format "{package}/init/seed/{config_file}.json".' + . ' If no config file specified, then all files of seed folder are used.', + 'type' => 'string' ] ], 'response' => [ @@ -38,6 +44,10 @@ if(file_exists($data_folder) && is_dir($data_folder)) { // handle JSON files foreach(glob("$data_folder/*.json") as $json_file) { + if(isset($params['config_file']) && $params['config_file'] !== $json_file) { + continue; + } + $data = file_get_contents($json_file); $classes = json_decode($data, true); if(!$classes) { From 9b772796a5c9b757204cce64239b4060207cdb13 Mon Sep 17 00:00:00 2001 From: Lucas Laurent Date: Tue, 10 Sep 2024 10:40:35 +0200 Subject: [PATCH 32/94] Refacto anonymize and seed --- packages/core/actions/init/anonymize.php | 23 ++++++++++++++++----- packages/core/actions/init/seed.php | 26 +++++++----------------- 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/packages/core/actions/init/anonymize.php b/packages/core/actions/init/anonymize.php index de92fde99..b6f81039a 100644 --- a/packages/core/actions/init/anonymize.php +++ b/packages/core/actions/init/anonymize.php @@ -46,13 +46,26 @@ continue; } - $entities_config = file_get_contents($json_file); - if(!empty($entities_config) && !isset($entities_config[0])) { - $entities_config = [$entities_config]; + $data = file_get_contents($json_file); + $classes = json_decode($data, true); + if(!$classes) { + continue; } + foreach($classes as $class) { + if(!isset($class['name'])) { + continue; + } + + $anonymize_params = [ + 'entity' => $class['name'], + ]; + foreach(['lang', 'fields', 'relations', 'domain'] as $param_key) { + if(isset($class[$param_key])) { + $generate_params[$param_key] = $class[$param_key]; + } + } - foreach($entities_config as $entity_config) { - eQual::run('do', 'core_model_anonymize', $entity_config); + eQual::run('do', 'core_model_anonymize', $anonymize_params); } } } diff --git a/packages/core/actions/init/seed.php b/packages/core/actions/init/seed.php index 65c7eae7b..b8a576beb 100644 --- a/packages/core/actions/init/seed.php +++ b/packages/core/actions/init/seed.php @@ -30,14 +30,13 @@ 'visibility' => 'protected' ], 'constants' => ['DEFAULT_LANG'], - 'providers' => ['context', 'orm'] + 'providers' => ['context'] ]); /** - * @var \equal\php\Context $context - * @var \equal\orm\ObjectManager $orm + * @var \equal\php\Context $context */ -list('context' => $context, 'orm' => $orm) = $providers; +list('context' => $context) = $providers; $data_folder = "packages/{$params['package']}/init/seed"; @@ -54,31 +53,20 @@ continue; } foreach($classes as $class) { - $entity = $class['name'] ?? null; - if(!$entity) { + if(!isset($class['name'], $class['qty'])) { continue; } - $lang = $class['lang'] ?? constant('DEFAULT_LANG'); - $model = $orm->getModel($entity); - $schema = $model->getSchema(); - $objects_ids = []; - - if(!isset($class['qty'])) { - continue; - } - - $qty = is_array($class['qty']) ? mt_rand($class['qty'][0], $class['qty'][1]) : $class['qty']; $generate_params = [ - 'entity' => $entity, - 'lang' => $class['lang'] + 'entity' => $class['name'], ]; - foreach(['fields', 'relations', 'add_to_domain_data'] as $param_key) { + foreach(['lang', 'fields', 'relations', 'add_to_domain_data'] as $param_key) { if(isset($class[$param_key])) { $generate_params[$param_key] = $class[$param_key]; } } + $qty = is_array($class['qty']) ? mt_rand($class['qty'][0], $class['qty'][1]) : $class['qty']; for($i = 0; $i < $qty; $i++) { eQual::run('do', 'core_model_generate', $generate_params); } From 56a3dd4302dae4f012ab885e61ae5f4467dfb518 Mon Sep 17 00:00:00 2001 From: Lucas Laurent Date: Tue, 10 Sep 2024 10:51:16 +0200 Subject: [PATCH 33/94] Fix config file filter for anonymize and seed actions --- packages/core/actions/init/anonymize.php | 2 +- packages/core/actions/init/seed.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/actions/init/anonymize.php b/packages/core/actions/init/anonymize.php index b6f81039a..c10871258 100644 --- a/packages/core/actions/init/anonymize.php +++ b/packages/core/actions/init/anonymize.php @@ -42,7 +42,7 @@ if(file_exists($data_folder) && is_dir($data_folder)) { // handle JSON files foreach(glob("$data_folder/*.json") as $json_file) { - if(isset($params['config_file']) && $params['config_file'] !== $json_file) { + if(isset($params['config_file']) && $params['config_file'] !== basename($json_file, '.json')) { continue; } diff --git a/packages/core/actions/init/seed.php b/packages/core/actions/init/seed.php index b8a576beb..05c1c000a 100644 --- a/packages/core/actions/init/seed.php +++ b/packages/core/actions/init/seed.php @@ -43,7 +43,7 @@ if(file_exists($data_folder) && is_dir($data_folder)) { // handle JSON files foreach(glob("$data_folder/*.json") as $json_file) { - if(isset($params['config_file']) && $params['config_file'] !== $json_file) { + if(isset($params['config_file']) && $params['config_file'] !== basename($json_file, '.json')) { continue; } From 513ce335f93488942400cbd6874b8583852fea5b Mon Sep 17 00:00:00 2001 From: Lucas Laurent Date: Tue, 10 Sep 2024 11:08:20 +0200 Subject: [PATCH 34/94] Fix bug var name --- packages/core/actions/init/anonymize.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/actions/init/anonymize.php b/packages/core/actions/init/anonymize.php index c10871258..1dd8493a0 100644 --- a/packages/core/actions/init/anonymize.php +++ b/packages/core/actions/init/anonymize.php @@ -61,7 +61,7 @@ ]; foreach(['lang', 'fields', 'relations', 'domain'] as $param_key) { if(isset($class[$param_key])) { - $generate_params[$param_key] = $class[$param_key]; + $anonymize_params[$param_key] = $class[$param_key]; } } From 84e2a53cfe0b50fce43f880d469db567f24b21c7 Mon Sep 17 00:00:00 2001 From: Lucas Laurent Date: Tue, 10 Sep 2024 11:24:20 +0200 Subject: [PATCH 35/94] Update description of actions anonymize and seed --- packages/core/actions/init/anonymize.php | 4 ++-- packages/core/actions/init/seed.php | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/core/actions/init/anonymize.php b/packages/core/actions/init/anonymize.php index 1dd8493a0..1ade4a3e6 100644 --- a/packages/core/actions/init/anonymize.php +++ b/packages/core/actions/init/anonymize.php @@ -6,7 +6,7 @@ */ list($params, $providers) = eQual::announce([ - 'description' => "Anonymize an existing object with random data and given values.", + 'description' => 'Anonymize objects using json configuration files in "{package}/init/anonymize/".', 'params' => [ 'package' => [ 'description' => 'Name of the package to anonymize.', @@ -15,7 +15,7 @@ 'required' => true ], 'config_file' => [ - 'description' => 'Name of the configuration file to use to anonymize data.', + 'description' => 'Name of the configuration file to use to anonymize objects.', 'help' => 'Configuration file must match the format "{package}/init/anonymize/{config_file}.json".' . ' If no config file specified, then all files of anonymize folder are used.', 'type' => 'string' diff --git a/packages/core/actions/init/seed.php b/packages/core/actions/init/seed.php index 05c1c000a..eef32b50f 100644 --- a/packages/core/actions/init/seed.php +++ b/packages/core/actions/init/seed.php @@ -6,16 +6,16 @@ */ list($params, $providers) = eQual::announce([ - 'description' => 'Seed data for package using json configuration files in "package/init/seed/".', + 'description' => 'Seed objects for package using json configuration files in "{package}/init/seed/".', 'params' => [ 'package' => [ - 'description' => 'Name of the package to anonymize.', + 'description' => 'Name of the package to seed.', 'type' => 'string', 'usage' => 'orm/package', 'required' => true ], 'config_file' => [ - 'description' => 'Name of the configuration file to use to anonymize data.', + 'description' => 'Name of the configuration file to use to seed objects.', 'help' => 'Configuration file must match the format "{package}/init/seed/{config_file}.json".' . ' If no config file specified, then all files of seed folder are used.', 'type' => 'string' From da43c4b444c4e841428c694db68554217d3029a0 Mon Sep 17 00:00:00 2001 From: Lucas Laurent Date: Tue, 10 Sep 2024 11:48:12 +0200 Subject: [PATCH 36/94] Refacto --- packages/core/actions/model/generate.php | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/actions/model/generate.php b/packages/core/actions/model/generate.php index bde61b782..5d117337f 100644 --- a/packages/core/actions/model/generate.php +++ b/packages/core/actions/model/generate.php @@ -25,7 +25,6 @@ 'description' => 'How to handle the object relations (many2one, one2many and many2many)', 'type' => 'array', 'default' => [] - ], 'add_to_domain_data' => [ 'description' => 'Global domain data.', From 47f737b883078fd3bf8d8031ea4e9869343e4d4e Mon Sep 17 00:00:00 2001 From: Lucas Laurent Date: Tue, 10 Sep 2024 12:22:37 +0200 Subject: [PATCH 37/94] Handle relation add to domain data --- packages/core/actions/model/generate.php | 52 +++++++++++++++--------- 1 file changed, 33 insertions(+), 19 deletions(-) diff --git a/packages/core/actions/model/generate.php b/packages/core/actions/model/generate.php index 5d117337f..be52d083b 100644 --- a/packages/core/actions/model/generate.php +++ b/packages/core/actions/model/generate.php @@ -28,11 +28,13 @@ ], 'add_to_domain_data' => [ 'description' => 'Global domain data.', - 'type' => 'array' + 'type' => 'array', + 'default' => [] ], 'domain_data' => [ 'description' => 'Global domain data.', - 'type' => 'array' + 'type' => 'array', + 'default' => [] ], 'lang' => [ 'description ' => 'Specific language for multilang field.', @@ -67,7 +69,7 @@ 'entity' => $field_conf['foreign_object'], 'lang' => $lang ]; - foreach(['fields', 'relations'] as $param_key) { + foreach(['fields', 'relations', 'add_to_domain_data'] as $param_key) { if(isset($relation_conf[$param_key])) { $model_generate_params[$param_key] = $relation_conf[$param_key]; } @@ -85,7 +87,7 @@ 'entity' => $field_conf['foreign_object'], 'lang' => $lang ]; - foreach(['fields', 'relations'] as $param_key) { + foreach(['fields', 'relations', 'add_to_domain_data'] as $param_key) { if(isset($relation_conf[$param_key])) { $model_generate_params[$param_key] = $relation_conf[$param_key]; } @@ -103,10 +105,7 @@ return $results; }; -$generateOne2Many = function($id, $field_conf, $relation_conf, $lang, $domain_data) { - $qty_conf = $relation_conf['qty'] ?? [0, 3]; - $qty = is_array($qty_conf) ? mt_rand($qty_conf[0], $qty_conf[1]) : $qty_conf; - +$generateOne2Many = function($id, $qty, $field_conf, $relation_conf, $lang, $domain_data) { $model_generate_params = [ 'entity' => $field_conf['foreign_object'], 'lang' => $lang @@ -251,6 +250,12 @@ $result = $generateMany2One($field_conf, $relation_conf, $params['lang'], $params['domain_data'] ?? []); $new_entity[$field] = $result['id']; + + foreach(array_keys($relation_conf['add_to_domain_data'] ?? []) as $key) { + if(isset($result['domain_data'][$key])) { + $params['domain_data']["$field.$key"] = $result['domain_data'][$key]; + } + } break; } break; @@ -292,26 +297,32 @@ if(!empty($new_relation_entities_ids)) { $new_entity[$field] = $new_relation_entities_ids; } + + $i = 0; + foreach($results as $result) { + foreach(array_keys($relation_conf['add_to_domain_data'] ?? []) as $key) { + if(isset($result['domain_data'][$key])) { + $params['domain_data']["$field.$i.$key"] = $result['domain_data'][$key]; + } + } + $i++; + } break; } break; } } -$field_to_read = []; -if(isset($params['add_to_domain_data'])) { - $field_to_read = array_values($params['add_to_domain_data']); -} +$field_to_read = array_values($params['add_to_domain_data']); $instance = $params['entity']::create($new_entity, $params['lang']) ->read($field_to_read) ->adapt('json') ->first(true); -$domain_data = $params['domain_data'] ?? []; -foreach($params['add_to_domain_data'] ?? [] as $key => $field) { +foreach($params['add_to_domain_data'] as $key => $field) { if(isset($instance[$field])) { - $domain_data[$key] = $instance[$field]; + $params['domain_data'][$key] = $instance[$field]; } } @@ -322,7 +333,10 @@ continue; } - $results = $generateOne2Many($instance['id'], $field_conf, $relation_conf, $params['lang'], $domain_data); + $qty_conf = $relation_conf['qty'] ?? [0, 3]; + $qty = is_array($qty_conf) ? mt_rand($qty_conf[0], $qty_conf[1]) : $qty_conf; + + $results = $generateOne2Many($instance['id'], $qty, $field_conf, $relation_conf, $params['lang'], $params['domain_data']); $new_relation_entities_ids = array_column($results, 'id'); if(!empty($new_relation_entities_ids)) { @@ -331,9 +345,9 @@ $i = 0; foreach($results as $result) { - foreach(array_keys($relation_conf['add_to_domain_data']) as $key) { + foreach(array_keys($relation_conf['add_to_domain_data'] ?? []) as $key) { if(isset($result['domain_data'][$key])) { - $domain_data["$field.$i.$key"] = $result['domain_data'][$key]; + $params['domain_data']["$field.$i.$key"] = $result['domain_data'][$key]; } } $i++; @@ -343,7 +357,7 @@ $result = [ 'entity' => $params['entity'], 'id' => $instance['id'], - 'domain_data' => $domain_data + 'domain_data' => $params['domain_data'] ]; $context->httpResponse() From 2787769160bc4b0d46e54dbb29aee138c48dc79e Mon Sep 17 00:00:00 2001 From: Lucas Laurent Date: Tue, 10 Sep 2024 15:53:33 +0200 Subject: [PATCH 38/94] Fix bug domain parse "object." and ".user" --- lib/equal/orm/Domain.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/equal/orm/Domain.class.php b/lib/equal/orm/Domain.class.php index 45caf479c..a838b7b55 100644 --- a/lib/equal/orm/Domain.class.php +++ b/lib/equal/orm/Domain.class.php @@ -122,7 +122,7 @@ public function parse($object = [], $user = []) { // handle object references as `value` part if(is_string($value) && strpos($value, 'object.') == 0 ) { - $target = substr($value, 0, strlen('object.')); + $target = substr($value, strlen('object.')); if(!$object || !isset($object[$target])) { continue; } @@ -145,7 +145,7 @@ public function parse($object = [], $user = []) { } // handle user references as `value` part elseif(is_string($value) && strpos($value, 'user.') == 0) { - $target = substr($value, 0, strlen('user.')); + $target = substr($value, strlen('user.')); if(!$user || !isset($user[$target])) { continue; } From dbc291d0383f83468b65002408907bab3aed9ec0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Fran=C3=A7oys?= Date: Tue, 10 Sep 2024 16:21:38 +0200 Subject: [PATCH 39/94] updated --- public/assets/img/equal_summary.png | Bin 557180 -> 451314 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/public/assets/img/equal_summary.png b/public/assets/img/equal_summary.png index 385b06f6848a38de1aa30acf2f5c84f187ed0a33..a52e1a869c1fc7558db065576a2ec04947b4cbbe 100644 GIT binary patch literal 451314 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­tKCfl^JTzGJ>PS-EG_SAh3Jxi1?0N~&N`&DmtVs+b4 zfT&71o9tEGsJ38S3Qs;Z>??!LbqUpnyM)dG0y>9TABY%(eV1dYapnzwN z*(D_4kgg~EUIh!4FhP*RXIHAEWLe6r+=mF$N{~=AP)#X<<)-YG6)aJl#Z90dbOf(B zR~Vl_^D>Y0R}p#2=8z0z)ohW8qBzlngr}AFKJ42_%S3{m5j6k2r!7RuRL)rmBue6d z0MWUy=@sKq#NYuOvh$*IEHgL%bq>!1C(ET_h0>utN-b5 zKJv^N@0{!VzUa`64ENNDCTCLJsikC*R{3qd>>OW^O`!bFY0JV!+bEg_VmZOx6oQqj z3zK2z9AXzuTMiMsE}FsO912K8a#LDz37k`tmc~(InXwu`s=;C$Z)9kfUTyDGWfW}D zR+{Ea+D!og-*|wqYv22L0pJjE0CYqM=v}bK2Sg7U&>v#no4?zWRYPMD*HQFTVMP zlOOu@W1hfu-D;)g2)m^6$vEI6nG8>~)B4 zJU<<(Q9jPKQI`pD8nZhPQ8X9Q9^n)KK%bQU9L9*M?k;VYm}L)t)$t-`il3Smy;UYg zcB%mg7A_hF?}>PIJJgf_d<&Kw2@sNK(bpfDOS7OW>Y>-&+OMSN(V~_#u2PTWpFGy3 zENvseh)1H0)AK5xBL_&>0eT-%ewh8DH7i80VK>L5sb@x}%~&HUE5LvJ7q583x1YM_ z-UEOq0pQ_p_#3WY{p?R(^FRLD*F&UmEvB3E-yw7uvv)M*>}n+(Z1y7ZaO4-j7V_Xw z$?iJ^5RgMhOo?f4YlsRFH^SxztiDF;j6(Byp_=4Jv|BAIx;1x{Y$Nu*7f7RJ(^;ml zhNzE>wSY>S#I0qh$GP_x#YG?}(8H5!Ko}K2YLm*Ec0KNB!E?VT1 za}9K1>z-)%G%E?(+uYwMgXGs6DR|PBFefo6SwQLaDo{cj0c_ zXrz-F>fF?gY!xyn?DBl`Yp?vNcf9Zq-v4mS1>H}+RKKWaJc)D}0?{z2})#*oR z^s&Zfv-<~D-4}SOjbI%xVRzp2lkgIQ_v8mrdDEy_7yzK{=Q6@?jW$Nfh+3}^kg2k6 zPK5zaY}OBd5{}nHxnZ5z<=E~+{CM88e>Ukm^jT}ZFG z=_CN$^nw$8-`)7a{^x%BIsoW94dwHz8vxZsQ=|Q=f&?vxM1Y>H@-w^jae)j2uXP*X& zg4&Z0r=urVM+%R8-uWE%KTu)N$p)J|AqLPN^#{9ev`O~>XG=6rKG{0e@HSLjI|B`D z+6&eB>VnI!S(g7SlKoI*I>*^ruZx@M$DKgR6Eps zOIcXO>L#1_EnU-mp(zi8^&mmPB&U3oS>U~g808S85F?I=4RZR41t~$0kY+&~*;MCHoPusxRzAn7-4Yx?`OuYPR7l z&3idv1TNV@hA^H})}eC<9^)a*VK(dtpbHTQqGT86o3T*_V_xwdA|m8)S@A+|(H`HK zT0@YEi0cAcXd3OdHIz@U)jEI6%;rQ~`K6tBv8rj_ZnxX+P{q=El_cA;IgyAj%O9FD zOb?G;bT5SzhZ#W4h;1CtS&HpbC61p5Qyb7xj>08blPBKKx`WJiFFp zxxGUGU@Zluy6mynZ%XYBmh||v{u2`)67{qNHq~|mao5HyGcwY{vNh??u{w_#bn<-; z(uaru!{7(-UD&Lhr(OASn6{H_nPYZs@n+mNxAj1y9h%dY3d}Q`>dbf1|E8JndKhc_ zo#RP34@pz2Ek%Ev20MLcnGPcXhf z#PJk-EjFrDU}(lI9CFvvt^n(FQ>fk!QHg zr&LA<))1DSqRlwpkzx+~>RrG%i*JDt=bV!(@yv+dX*Ex+yAI|7*@j8Pn z6UxFls#z}~h{`mgcC!ZC($FWS!bBrEnVHA0L87<}24{7nE&7RbnA$EGqbSCah7cIQhk#g-7w5tIAq0K! z(KVa^LkPdPwRwy;iJT*T5Exe{51AnfEwA!2dq4n5M;S=Gru8+z{qNOO5E-kL}D%>OJ3~e)%Q#6^vhtO~k18wx#rf!}{JbTBfQR@|+C`Sn*-9QCbqFDt zG0GI4426gh7^T^I5*&}AsjyQ*cH69y>S>mQODrI|WZPgUBMVGiRZ2m^_ySt#=BX<) zcE6-L=x7YXnr^hSxAGf(P(QTUwCXjhu*out9u~(k+WZ&;sTJHkb4EqsQMu!99DlkL znZ*cKYO=FalHK0Q-N5l?mRSBIV|KEciY0R7+H@oRneL!l*&dcP^gNX*wM0_m3EA`5 zv$bBX=BD+!<)*X+qJJ#u)G>J#iO!^Pb+MS1667^E@f$hD90eHaJpy-s4D)!>T56cmU)9icW} zr-j6MHgnw`;mcms>``G2nmnI==tB%<0Dxh=jxLgH7YNba*u04VAS_Hj9xJ- z$wL7iG5$kJm|>Lh;)8PgAV7+6>JZ~G-+|ARAWmNCZ4`N=XRXFk;@kKb^E*XoaA}nv zdAHUNr=f6sbIw4XhE^k=DW_>iHB{*ld6A^WmK<0aW6At&%5!CjX)BE+ty)yV${@+> zBGbPtR{#JY07*naRCL_RE3XkXT2jtXNi{9AFv~B#QWBzV*rCT~V?yMmEnfe$raI-@ zY7LA~m+|qj^;Bo%lR!Lhe?VnuK#>6R?}Z}X_EV@)@ThvYxEX9=B$2wy*E~l?cOw#_ zHa&OfD0;#Z)v`Ryj;VAdb0q8*eM6g#WLB*@RKwP&qDsb9l6q1wRE zujQsaJ)$VyMN`#5Q3Z8du$#Y=C2e5@B)4f|OqMzqlwU)BZK?NfLFEyxQfiH_6>K3w z7USemDy;I`P8*w(iRLwWnwOPvc0kCkT|w=m)fy&Ns?VVZ%;N(tR6PFkVsYB{ig z&ElTu#*4>l4f#=DY@F3@Niw!A&2OTa)^UIY=&h$_0j+ly&%Dyxlj83hWXsO7@MGBn zLzO_#7>Ncn+=`+y%t0YqNSat=LkxRBfN9|>jKI7CiY1%v&uIX5-K8v8Z=|A-hqQ1#|zQ z_d@_o=$ylFexYouB*x1{=q$5{ggc!c^X-P?5M7*qqq;5P^*?=-WJ4riGDSnwDiPIEOfLP!o=3okwx*h$xp-v$U}g1ocmaIC@QHHx_`GIQRv_@V5PW}&diD( zltU+7QN_1iu0g`cr|xh;)z@p~ktQ3JkE|o5JX8yAr7TA^Z-hUkill+2zEJ_n0FnM8!bcDs!=m3sIS}Hs!);HB<%;raM2k}uuS6LkUXozQjAD1Vsz|z$e3Ofma(3^ zoBxqrUertag{z5QZ84OiR*Wc==M|t%QPw|15p1_;Ll75zPyz+L1R|;;ek7ymgecM2Qe0+`NziV@ z)NzA`8j(fX&jhSy5Coha|A{wMP+x9R;WxS+1wZdpyW~vIwU~`Tc0*H>VFq5If#j~j zgsqWE!Q1ZnTH{VK-AgvSKF~gOBKfM89j)hOHLUuHOeDlCGvAKtkz07cOj@}%Cfuw; z1=C608j=Kef$n(x$0}mNO+8 z6>k!9QDgLsIkwK_p%zqgjKyXZO~_un8-=#rC9U~FUdp~BVXI8tEPy*z4lG#~S(LR4 zB4(~(K(chEUF}hsqg~F;YGm!ypl#zEEzjR7ae|MaIxTH<0kp?5W-`tau6lePC$b!I zqiLsr9nCe%sTLcxgH4n6%P!R;oY*_L%}!|i3Wgd(sMXp507jsxaB#v~Ej|OVZ4Xc} zC8y6%D0Te+je8Z3tLv3RMb-jAT5|JvS*OVY4Za55as?hi0d6 zu4M9C(G}yoK^)dut(;iWUtP1)iheUXT%yiuLg&%9lhz{c$VOnVey(lt-4vzXMD0YAC}_2bZ+D# ztLbW_?sG{)XDgpl{?fr9P(z}%CW_HOP!)KM9oG`da$Jw)Qzxm%RNOX_LT!w68J?V5w>Kp9 zuGHJeOf{d%oow^F#%SqPE)`aSQU`4&Im%RbVcRONU{mNnn@$c*!@PmDXDD4}+6)uz z_a!qTye$Tka&X-+UG1Ztxg5c$OOJX&K(_>dY5|SFmqLUgY5q~x!SGR58OdQL(o41k zqf*I~-Y;IZi6?Xj98pU)wA5WC`;s4`(rl{Rp$%a3!}xg<*lj~~GZ0uoO1j$WTDXtu z;X8irbK%GT{fn=*_VPE*@G9$