├── README.md ├── composer.json ├── config └── geo.php └── src ├── Console ├── GeoExchange.php ├── GeoInfo.php ├── GeoList.php └── GeoMaxmind.php ├── Facades └── Geo.php ├── Geo.php ├── GeoServiceProvider.php ├── Locale.php ├── Location ├── Location.php └── MaxmindLocation.php ├── Money.php └── MoneyException.php /README.md: -------------------------------------------------------------------------------- 1 | ![](https://github.com/kslimani/laravel-geo/workflows/Integration%20tests/badge.svg) 2 | 3 | # Laravel Geo 4 | 5 | A simple service provider to work with locales, countries and currencies. 6 | 7 | It uses the following dependencies : 8 | 9 | * [florianv/laravel-swap](https://github.com/florianv/laravel-swap) 10 | * [kslimani/geo-list](https://github.com/kslimani/geo-list) (tiny version of umpirsky's projects) 11 | * [maxmind/MaxMind-DB-Reader-php](https://github.com/maxmind/MaxMind-DB-Reader-php) 12 | * [moneyphp/money](https://github.com/moneyphp/money) 13 | 14 | ## Installation 15 | 16 | use Composer to add the package to your project's dependencies : 17 | 18 | ```bash 19 | composer require kslimani/laravel-geo 20 | ``` 21 | 22 | Note: Swap uses [HTTPlug](http://httplug.io/) abstraction which may require additional dependencies. For example using Curl : 23 | 24 | ```bash 25 | composer require php-http/curl-client nyholm/psr7 php-http/message 26 | ``` 27 | 28 | Optionally, adds the Geo facade in `config/app.php` : 29 | 30 | ```php 31 | 'aliases' => [ 32 | // ... 33 | 'Geo' => Sk\Geo\Facades\Geo::class, 34 | ]; 35 | ``` 36 | 37 | Publish the [configuration file](https://github.com/kslimani/laravel-geo/blob/master/config/geo.php) `config/geo.php` : 38 | 39 | ```bash 40 | php artisan vendor:publish --provider="Sk\Geo\GeoServiceProvider" --tag="config" 41 | ``` 42 | 43 | Optionally, publish [Swap service provider configuration](https://github.com/florianv/laravel-swap/blob/master/doc/readme.md#configuration) file : 44 | 45 | ```bash 46 | php artisan vendor:publish --provider="Swap\Laravel\SwapServiceProvider" 47 | ``` 48 | 49 | ## Quick usage 50 | 51 | ```php 52 | use Sk\Geo\Facades\Geo; 53 | 54 | // Get country code from ip address (US) 55 | $countryCode = Geo::location()->ipCountry('8.8.8.8'); 56 | 57 | // Get country name (United States) 58 | $countryName = Geo::locale()->country($countryCode); 59 | 60 | // Get country language code (en) 61 | $languageCode = Geo::locale()->countryLanguage($countryCode); 62 | 63 | // Get country language name (English) 64 | $languageName = Geo::locale()->language($languageCode); 65 | 66 | // Get country currency code (USD) 67 | $currencyCode = Geo::locale()->countryCurrency($countryCode); 68 | 69 | // Get country currency name (US Dollar) 70 | $currencyName = Geo::locale()->currency($currencyCode); 71 | 72 | // Make money amount 73 | $fiveDollars = Geo::money()->make('500', $currencyCode); 74 | 75 | // Get amount converted to Euro 76 | $euroAmount = Geo::money()->convert($fiveDollars, 'EUR'); 77 | 78 | // Get formatted amount ("Intl" formatter) 79 | $intlFormattedAmount = Geo::money()->format($euroAmount); 80 | 81 | // Get formatted amount ("Decimal" formatter) 82 | $decFormattedAmount = Geo::money()->formatDec($euroAmount); 83 | 84 | // Parse "Decimal" formatted amount 85 | $newEuroAmount = Geo::money()->parse($decFormattedAmount, 'EUR'); 86 | 87 | // Decompose money amount 88 | $keyValueArray = Geo::money()->decompose($fiveDollars); 89 | // [ 90 | // "locale" => "en" 91 | // "subunit" => 2 92 | // "sign" => "+" 93 | // "unsigned_part" => "5" 94 | // "decimal_part" => "00" 95 | // "grouping_separator" => "," 96 | // "decimal_separator" => "." 97 | // "symbol" => "$" 98 | // ] 99 | 100 | // Get all countries (country code -> name associative array) 101 | $countries = Geo::locale()->countries(); 102 | 103 | // Get all languages (language code -> name associative array) 104 | $languages = Geo::locale()->languages(); 105 | 106 | // Get all currencies (currency code -> name associative array) 107 | $currencies = Geo::locale()->currencies(); 108 | 109 | // All methods returning a name accept an optional locale (default is application locale) 110 | Geo::locale()->country('US', 'es'); // 'Estados Unidos' 111 | Geo::locale()->language('en', 'de') // 'Englisch' 112 | Geo::locale()->currency('USD', 'ru') // 'Доллар США' 113 | Geo::locale()->countries('zh') // [ 'BT' => '不丹', 'TL' => '东帝汶', ... ] 114 | Geo::money()->format($fiveDollars, 'fr') // '5,00 $US' 115 | Geo::money()->decompose($amount, 'fr'); // [ 'locale' => 'fr', ... ] 116 | 117 | // Get instances using app helper 118 | $location = app('geo.location'); 119 | $locale = app('geo.locale'); 120 | $money = app('geo.money'); 121 | ``` 122 | 123 | ## Console commands 124 | 125 | This package also provides some Artisan console commands : 126 | 127 | ``` 128 | geo:exchange Simple currency converter 129 | geo:info Display geo data for ip address 130 | geo:list Display countries with default language and currency 131 | geo:maxmind Display Maxmind DB countries with matched geo country name 132 | ``` 133 | 134 | Note: `geo:list` and `geo:maxmind` are mainly used for package development. 135 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kslimani/laravel-geo", 3 | "description": "Laravel Geo.", 4 | "keywords": ["country", "currency", "geo", "laravel", "locale"], 5 | "license": "MIT", 6 | "require": { 7 | "php": ">=7.2", 8 | "florianv/laravel-swap": "^2.2", 9 | "kslimani/geo-list": "^1.0", 10 | "maxmind-db/reader": "^1.8", 11 | "moneyphp/money": "^3.3" 12 | }, 13 | "require-dev": { 14 | "ext-intl": "*", 15 | "friendsofphp/php-cs-fixer": "^2.17", 16 | "nyholm/psr7": "^1.3", 17 | "orchestra/testbench": "^6.7", 18 | "php-http/curl-client": "^2.2", 19 | "php-http/message": "^1.10", 20 | "phpunit/phpunit": "^9.5" 21 | }, 22 | "autoload": { 23 | "psr-4": { 24 | "Sk\\Geo\\": "src/" 25 | } 26 | }, 27 | "autoload-dev": { 28 | "psr-4": { 29 | "Tests\\": "tests/" 30 | } 31 | }, 32 | "extra": { 33 | "laravel": { 34 | "providers": [ 35 | "Sk\\Geo\\GeoServiceProvider" 36 | ], 37 | "aliases": { 38 | "Geo": "Sk\\Geo\\Facades\\Geo" 39 | } 40 | } 41 | }, 42 | "scripts": { 43 | "lint": [ 44 | "vendor/bin/php-cs-fixer fix --diff --diff-format=udiff --using-cache=no --dry-run src", 45 | "vendor/bin/php-cs-fixer fix --diff --diff-format=udiff --using-cache=no --dry-run tests" 46 | ], 47 | "test": "phpunit" 48 | }, 49 | "config": { 50 | "sort-packages": true 51 | }, 52 | "prefer-stable": true 53 | } 54 | -------------------------------------------------------------------------------- /config/geo.php: -------------------------------------------------------------------------------- 1 | [ 17 | 'location' => MaxmindLocation::class, 18 | ], 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Location 23 | |-------------------------------------------------------------------------- 24 | | 25 | | 'maxmind' is GeoIP2 .mmdb database files. (Set to false or empty 26 | | string to disable). 27 | */ 28 | 29 | 'location' => [ 30 | 'maxmind' => [ 31 | 'country' => env('GEOIP_COUNTRY', false), 32 | 'city' => env('GEOIP_CITY', false), 33 | 'isp' => env('GEOIP_ISP', false), 34 | ], 35 | ], 36 | 37 | ]; 38 | -------------------------------------------------------------------------------- /src/Console/GeoExchange.php: -------------------------------------------------------------------------------- 1 | money = app('geo.money'); 37 | parent::__construct(); 38 | } 39 | 40 | /** 41 | * Execute the console command. 42 | * 43 | * @return mixed 44 | */ 45 | public function handle() 46 | { 47 | $amount = $this->argument('amount'); 48 | $amountCurrency = $this->argument('currency_from'); 49 | $targetCurrency = $this->argument('currency_to'); 50 | $amount = $this->money->parse($amount, $amountCurrency); 51 | $targetAmount = $this->money->convert($amount, $targetCurrency); 52 | 53 | $this->info(sprintf( 54 | '%s %s = %s %s', 55 | $this->money->formatDec($amount), 56 | $amount->getCurrency(), 57 | $this->money->formatDec($targetAmount), 58 | $targetAmount->getCurrency() 59 | )); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Console/GeoInfo.php: -------------------------------------------------------------------------------- 1 | locale = $locale; 41 | $this->location = $location; 42 | parent::__construct(); 43 | } 44 | 45 | protected function withIsp() 46 | { 47 | return method_exists($this->location, 'asn') 48 | && method_exists($this->location, 'isp'); 49 | } 50 | 51 | /** 52 | * Execute the console command. 53 | * 54 | * @return mixed 55 | */ 56 | public function handle() 57 | { 58 | $ip = $this->argument('ip'); 59 | $countryCode = $this->location->ipCountry($ip); 60 | $languageCode = $this->locale->countryLanguage($countryCode); 61 | $currencyCode = $this->locale->countryCurrency($countryCode); 62 | $this->table([ 63 | 'Country code', 64 | 'Country name', 65 | 'Language code', 66 | 'Language name', 67 | 'Currency code', 68 | 'Currency name', 69 | 'ISP ASN', 70 | 'ISP name', 71 | ], [[ 72 | $countryCode, 73 | $this->locale->country($countryCode), 74 | $languageCode, 75 | $this->locale->language($languageCode), 76 | $currencyCode, 77 | $this->locale->currency($currencyCode), 78 | $this->withIsp() ? $this->location->asn($ip) : null, 79 | $this->withIsp() ? $this->location->isp($ip) : null, 80 | ]]); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Console/GeoList.php: -------------------------------------------------------------------------------- 1 | locale = $locale; 34 | parent::__construct(); 35 | } 36 | 37 | /** 38 | * Execute the console command. 39 | * 40 | * @return mixed 41 | */ 42 | public function handle() 43 | { 44 | $countries = $this->locale->countries(); 45 | foreach ($countries as $countryCode => $country) { 46 | $languageCode = $this->locale->countryLanguage($countryCode); 47 | $currencyCode = $this->locale->countryCurrency($countryCode); 48 | $lines[] = [ 49 | $countryCode, 50 | $country, 51 | $languageCode, 52 | $this->locale->language($languageCode), 53 | $currencyCode, 54 | $this->locale->currency($currencyCode), 55 | ]; 56 | } 57 | $this->table([ 58 | 'Country code', 59 | 'Country name', 60 | 'Language code', 61 | 'Language name', 62 | 'Currency code', 63 | 'Currency name', 64 | ], $lines); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Console/GeoMaxmind.php: -------------------------------------------------------------------------------- 1 | locale = $locale; 35 | parent::__construct(); 36 | } 37 | 38 | /** 39 | * Execute the console command. 40 | * 41 | * @return mixed 42 | */ 43 | public function handle() 44 | { 45 | $countries = Cache::remember('geo-maxmind-countries', 60, function () { 46 | $lines = explode("\n", file_get_contents( 47 | 'https://dev.maxmind.com/static/csv/codes/iso3166.csv' 48 | )); 49 | 50 | $result = []; 51 | 52 | foreach ($lines as $line) { 53 | $row = str_getcsv($line); 54 | 55 | if (count($row) !== 2) { 56 | continue; 57 | } 58 | 59 | list($code, $name) = $row; 60 | $result[$code] = $name; 61 | } 62 | 63 | return $result; 64 | }); 65 | 66 | $misses = []; 67 | $lines = []; 68 | 69 | foreach ($countries as $countryCode => $country) { 70 | $countryName = $this->locale->country($countryCode); 71 | if (! $countryName) { 72 | // Country name should be NULL for 'A1', 'A2', 'O1', 'EU' and 'AP' 73 | // See: https://dev.maxmind.com/geoip/legacy/codes/iso3166/ 74 | // Country name is NULL for 'BV' : Bouvet Island (uninhabited) 75 | // Country name is NULL for 'HM' : Heard Island and McDonald Islands (no permanent human habitation) 76 | $misses[] = $countryCode; 77 | } 78 | $lines[] = [ 79 | $countryCode, 80 | $countryName, 81 | $country, 82 | ]; 83 | } 84 | 85 | $this->table([ 86 | 'Country code', 87 | 'Country name', 88 | 'Maxmind country name', 89 | ], $lines); 90 | 91 | $this->warn('Missing country codes are [\''.implode('\', \'', $misses).'\']'); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Facades/Geo.php: -------------------------------------------------------------------------------- 1 | locale = $locale; 27 | $this->location = $location; 28 | $this->money = $money; 29 | } 30 | 31 | public function locale() 32 | { 33 | return $this->locale; 34 | } 35 | 36 | public function location() 37 | { 38 | return $this->location; 39 | } 40 | 41 | public function money() 42 | { 43 | return $this->money; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/GeoServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 30 | __DIR__.'/../config/geo.php' => $this->app->configPath().'/geo.php', 31 | ], 'config'); 32 | 33 | if ($this->app->runningInConsole()) { 34 | $this->commands([ 35 | Console\GeoExchange::class, 36 | Console\GeoInfo::class, 37 | Console\GeoList::class, 38 | Console\GeoMaxmind::class, 39 | ]); 40 | } 41 | } 42 | 43 | /** 44 | * Register the service provider. 45 | * 46 | * @return void 47 | */ 48 | public function register() 49 | { 50 | // Register Swap service provider 51 | $this->app->register(SwapServiceProvider::class); 52 | 53 | $this->app->singleton('geo.locale', function ($app) { 54 | return new Locale($app->make('config')); 55 | }); 56 | 57 | $this->app->singleton('geo.money.exchange', function ($app) { 58 | return new SwapExchange($app->make('swap')); 59 | }); 60 | 61 | $this->app->singleton('geo.money', function ($app) { 62 | return new Money( 63 | $app->make('config'), 64 | $app->make('geo.money.exchange') 65 | ); 66 | }); 67 | 68 | $this->app->singleton('geo.location', function ($app) { 69 | return $app->make( 70 | $app->make('config') 71 | ->get('geo.defaults.location', MaxmindLocation::class) 72 | ); 73 | }); 74 | 75 | $this->app->singleton('geo', function ($app) { 76 | return new Geo( 77 | $app->make('geo.locale'), 78 | $app->make('geo.location'), 79 | $app->make('geo.money') 80 | ); 81 | }); 82 | } 83 | 84 | /** 85 | * Get the services provided by the provider. 86 | * 87 | * @return array 88 | */ 89 | public function provides() 90 | { 91 | return [ 92 | 'geo.locale', 93 | 'geo.money.exchange', 94 | 'geo.money', 95 | 'geo.location', 96 | 'geo', 97 | ]; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Locale.php: -------------------------------------------------------------------------------- 1 | 'en_AC', 15 | 'AD' => 'ca_AD', 16 | 'AE' => 'ar_AE', 17 | 'AF' => 'fa_AF', 18 | 'AG' => 'en_AG', 19 | 'AI' => 'en_AI', 20 | 'AL' => 'sq_AL', 21 | 'AM' => 'hy_AM', 22 | 'AN' => 'pap_AN', 23 | 'AO' => 'pt_AO', 24 | 'AQ' => 'und_AQ', // Antartica has no official language 25 | 'AR' => 'es_AR', 26 | 'AS' => 'sm_AS', 27 | 'AT' => 'de_AT', 28 | 'AU' => 'en_AU', 29 | 'AW' => 'nl_AW', 30 | 'AX' => 'sv_AX', 31 | 'AZ' => 'az_Latn_AZ', 32 | 'BA' => 'bs_BA', 33 | 'BB' => 'en_BB', 34 | 'BD' => 'bn_BD', 35 | 'BE' => 'nl_BE', 36 | 'BF' => 'mos_BF', 37 | 'BG' => 'bg_BG', 38 | 'BH' => 'ar_BH', 39 | 'BI' => 'rn_BI', 40 | 'BJ' => 'fr_BJ', 41 | 'BL' => 'fr_BL', 42 | 'BM' => 'en_BM', 43 | 'BN' => 'ms_BN', 44 | 'BO' => 'es_BO', 45 | 'BQ' => 'de_BQ', 46 | 'BR' => 'pt_BR', 47 | 'BS' => 'en_BS', 48 | 'BT' => 'dz_BT', 49 | 'BV' => 'und_BV', // Bouvet Island is uninhabited 50 | 'BW' => 'en_BW', 51 | 'BY' => 'be_BY', 52 | 'BZ' => 'en_BZ', 53 | 'CA' => 'en_CA', 54 | 'CC' => 'ms_CC', 55 | 'CD' => 'sw_CD', 56 | 'CF' => 'fr_CF', 57 | 'CG' => 'fr_CG', 58 | 'CH' => 'de_CH', 59 | 'CI' => 'fr_CI', 60 | 'CK' => 'en_CK', 61 | 'CL' => 'es_CL', 62 | 'CM' => 'fr_CM', 63 | 'CN' => 'zh_Hans_CN', 64 | 'CO' => 'es_CO', 65 | 'CR' => 'es_CR', 66 | 'CU' => 'es_CU', 67 | 'CV' => 'kea_CV', 68 | 'CW' => 'de_CW', 69 | 'CX' => 'en_CX', 70 | 'CY' => 'el_CY', 71 | 'CZ' => 'cs_CZ', 72 | 'DE' => 'de_DE', 73 | 'DG' => 'en_DG', 74 | 'DJ' => 'aa_DJ', 75 | 'DK' => 'da_DK', 76 | 'DM' => 'en_DM', 77 | 'DO' => 'es_DO', 78 | 'DZ' => 'ar_DZ', 79 | 'EA' => 'es_EA', 80 | 'EC' => 'es_EC', 81 | 'EE' => 'et_EE', 82 | 'EG' => 'ar_EG', 83 | 'EH' => 'ar_EH', 84 | 'ER' => 'ti_ER', 85 | 'ES' => 'es_ES', 86 | 'ET' => 'en_ET', 87 | 'FI' => 'fi_FI', 88 | 'FJ' => 'hi_FJ', 89 | 'FK' => 'en_FK', 90 | 'FM' => 'chk_FM', 91 | 'FO' => 'fo_FO', 92 | 'FR' => 'fr_FR', 93 | 'GA' => 'fr_GA', 94 | 'GB' => 'en_GB', 95 | 'GD' => 'en_GD', 96 | 'GE' => 'ka_GE', 97 | 'GF' => 'fr_GF', 98 | 'GG' => 'en_GG', 99 | 'GH' => 'ak_GH', 100 | 'GI' => 'en_GI', 101 | 'GL' => 'iu_GL', 102 | 'GM' => 'en_GM', 103 | 'GN' => 'fr_GN', 104 | 'GP' => 'fr_GP', 105 | 'GQ' => 'fan_GQ', 106 | 'GR' => 'el_GR', 107 | 'GS' => 'en_GS', 108 | 'GT' => 'es_GT', 109 | 'GU' => 'en_GU', 110 | 'GW' => 'pt_GW', 111 | 'GY' => 'en_GY', 112 | 'HK' => 'zh_Hant_HK', 113 | 'HM' => 'und_HM', // Heard Island and McDonald Islands has no permanent human habitation 114 | 'HN' => 'es_HN', 115 | 'HR' => 'hr_HR', 116 | 'HT' => 'ht_HT', 117 | 'HU' => 'hu_HU', 118 | 'IC' => 'es_IC', 119 | 'ID' => 'id_ID', 120 | 'IE' => 'en_IE', 121 | 'IL' => 'he_IL', 122 | 'IM' => 'en_IM', 123 | 'IN' => 'hi_IN', 124 | 'IO' => 'en_IO', 125 | 'IQ' => 'ar_IQ', 126 | 'IR' => 'fa_IR', 127 | 'IS' => 'is_IS', 128 | 'IT' => 'it_IT', 129 | 'JE' => 'en_JE', 130 | 'JM' => 'en_JM', 131 | 'JO' => 'ar_JO', 132 | 'JP' => 'ja_JP', 133 | 'KE' => 'en_KE', 134 | 'KG' => 'ky_Cyrl_KG', 135 | 'KH' => 'km_KH', 136 | 'KI' => 'en_KI', 137 | 'KM' => 'ar_KM', 138 | 'KN' => 'en_KN', 139 | 'KP' => 'ko_KP', 140 | 'KR' => 'ko_KR', 141 | 'KW' => 'ar_KW', 142 | 'KY' => 'en_KY', 143 | 'KZ' => 'ru_KZ', 144 | 'LA' => 'lo_LA', 145 | 'LB' => 'ar_LB', 146 | 'LC' => 'en_LC', 147 | 'LI' => 'de_LI', 148 | 'LK' => 'si_LK', 149 | 'LR' => 'en_LR', 150 | 'LS' => 'st_LS', 151 | 'LT' => 'lt_LT', 152 | 'LU' => 'fr_LU', 153 | 'LV' => 'lv_LV', 154 | 'LY' => 'ar_LY', 155 | 'MA' => 'ar_MA', 156 | 'MC' => 'fr_MC', 157 | 'MD' => 'ro_MD', 158 | 'ME' => 'sr_Latn_ME', 159 | 'MF' => 'fr_MF', 160 | 'MG' => 'mg_MG', 161 | 'MH' => 'mh_MH', 162 | 'MK' => 'mk_MK', 163 | 'ML' => 'bm_ML', 164 | 'MM' => 'my_MM', 165 | 'MN' => 'mn_Cyrl_MN', 166 | 'MO' => 'zh_Hant_MO', 167 | 'MP' => 'en_MP', 168 | 'MQ' => 'fr_MQ', 169 | 'MR' => 'ar_MR', 170 | 'MS' => 'en_MS', 171 | 'MT' => 'mt_MT', 172 | 'MU' => 'mfe_MU', 173 | 'MV' => 'dv_MV', 174 | 'MW' => 'ny_MW', 175 | 'MX' => 'es_MX', 176 | 'MY' => 'ms_MY', 177 | 'MZ' => 'pt_MZ', 178 | 'NA' => 'kj_NA', 179 | 'NC' => 'fr_NC', 180 | 'NE' => 'ha_Latn_NE', 181 | 'NF' => 'en_NF', 182 | 'NG' => 'en_NG', 183 | 'NI' => 'es_NI', 184 | 'NL' => 'nl_NL', 185 | 'NO' => 'nb_NO', 186 | 'NP' => 'ne_NP', 187 | 'NR' => 'en_NR', 188 | 'NU' => 'niu_NU', 189 | 'NZ' => 'en_NZ', 190 | 'OM' => 'ar_OM', 191 | 'PA' => 'es_PA', 192 | 'PE' => 'es_PE', 193 | 'PF' => 'fr_PF', 194 | 'PG' => 'tpi_PG', 195 | 'PH' => 'fil_PH', 196 | 'PK' => 'ur_PK', 197 | 'PL' => 'pl_PL', 198 | 'PM' => 'fr_PM', 199 | 'PN' => 'en_PN', 200 | 'PR' => 'es_PR', 201 | 'PS' => 'ar_PS', 202 | 'PT' => 'pt_PT', 203 | 'PW' => 'pau_PW', 204 | 'PY' => 'gn_PY', 205 | 'QA' => 'ar_QA', 206 | 'RE' => 'fr_RE', 207 | 'RO' => 'ro_RO', 208 | 'RS' => 'sr_Cyrl_RS', 209 | 'RU' => 'ru_RU', 210 | 'RW' => 'rw_RW', 211 | 'SA' => 'ar_SA', 212 | 'SB' => 'en_SB', 213 | 'SC' => 'en_SC', 214 | 'SD' => 'ar_SD', 215 | 'SE' => 'sv_SE', 216 | 'SG' => 'en_SG', 217 | 'SH' => 'en_SH', 218 | 'SI' => 'sl_SI', 219 | 'SJ' => 'nb_SJ', 220 | 'SK' => 'sk_SK', 221 | 'SL' => 'kri_SL', 222 | 'SM' => 'it_SM', 223 | 'SN' => 'fr_SN', 224 | 'SO' => 'sw_SO', 225 | 'SR' => 'srn_SR', 226 | 'SS' => 'en_SS', 227 | 'ST' => 'pt_ST', 228 | 'SV' => 'es_SV', 229 | 'SX' => 'de_SX', 230 | 'SY' => 'ar_SY', 231 | 'SZ' => 'en_SZ', 232 | 'TA' => 'en_TA', 233 | 'TC' => 'en_TC', 234 | 'TD' => 'fr_TD', 235 | 'TF' => 'fr_TF', 236 | 'TG' => 'fr_TG', 237 | 'TH' => 'th_TH', 238 | 'TJ' => 'tg_Cyrl_TJ', 239 | 'TK' => 'tkl_TK', 240 | 'TL' => 'pt_TL', 241 | 'TM' => 'tk_TM', 242 | 'TN' => 'ar_TN', 243 | 'TO' => 'to_TO', 244 | 'TR' => 'tr_TR', 245 | 'TT' => 'en_TT', 246 | 'TV' => 'tvl_TV', 247 | 'TW' => 'zh_Hant_TW', 248 | 'TZ' => 'sw_TZ', 249 | 'UA' => 'uk_UA', 250 | 'UG' => 'sw_UG', 251 | 'UM' => 'en_UM', 252 | 'US' => 'en_US', 253 | 'UY' => 'es_UY', 254 | 'UZ' => 'uz_Cyrl_UZ', 255 | 'VA' => 'it_VA', 256 | 'VC' => 'en_VC', 257 | 'VE' => 'es_VE', 258 | 'VG' => 'en_VG', 259 | 'VI' => 'en_VI', 260 | 'VN' => 'vi_VN', 261 | 'VU' => 'bi_VU', 262 | 'WF' => 'fr_WF', 263 | 'WS' => 'sm_WS', 264 | 'XK' => 'sq_XK', 265 | 'YE' => 'ar_YE', 266 | 'YT' => 'swb_YT', 267 | 'ZA' => 'en_ZA', 268 | 'ZM' => 'en_ZM', 269 | 'ZW' => 'sn_ZW', 270 | ]; 271 | 272 | /** 273 | * @var \Illuminate\Contracts\Config\Repository 274 | */ 275 | protected $config; 276 | 277 | /** 278 | * @var string 279 | */ 280 | protected $basePath; 281 | 282 | /** 283 | * @var array 284 | */ 285 | protected $countries; 286 | 287 | /** 288 | * @var array 289 | */ 290 | protected $languages; 291 | 292 | /** 293 | * @var array 294 | */ 295 | protected $currencies; 296 | 297 | /** 298 | * @var string 299 | */ 300 | protected $appLocale; 301 | 302 | /** 303 | * Create a new instance. 304 | * 305 | * @param \Illuminate\Contracts\Config\Repository $config 306 | * @return void 307 | */ 308 | public function __construct(Repository $config) 309 | { 310 | $this->config = $config; 311 | } 312 | 313 | /** 314 | * Get countries with ISO 3166-1 codes. 315 | * 316 | * @param string $locale 317 | * @return array 318 | */ 319 | public function countries($locale = null) 320 | { 321 | if (! $locale) { 322 | $locale = $this->config->get('app.locale'); 323 | } 324 | 325 | if (! $this->countries || $this->appLocale !== $locale) { 326 | $this->countries = GeoList::countries( 327 | $locale, 328 | $this->config->get('app.fallback_locale') 329 | ); 330 | 331 | // Filters exceptionally reserved codes 332 | // https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#Exceptional_reservations 333 | unset( 334 | $this->countries['EZ'], // Eurozone 335 | $this->countries['UN'] // United Nations 336 | ); 337 | 338 | asort($this->countries); 339 | $this->appLocale = $locale; 340 | } 341 | 342 | return $this->countries; 343 | } 344 | 345 | /** 346 | * Get country name. 347 | * 348 | * @param string $code 349 | * @param string $locale 350 | * @return string|null 351 | */ 352 | public function country($code, $locale = null) 353 | { 354 | if (! $locale) { 355 | $locale = $this->config->get('app.locale'); 356 | } 357 | 358 | if (! $this->countries || $this->appLocale !== $locale) { 359 | $this->countries($locale); 360 | } 361 | 362 | return isset($this->countries[$code]) ? $this->countries[$code] : null; 363 | } 364 | 365 | /** 366 | * Get languages with ISO 639-1 codes. 367 | * 368 | * @param string $locale 369 | * @return array 370 | */ 371 | public function languages($locale = null) 372 | { 373 | if (! $locale) { 374 | $locale = $this->config->get('app.locale'); 375 | } 376 | 377 | if (! $this->languages || $this->appLocale !== $locale) { 378 | $countries = $this->countries($locale); 379 | $languages = GeoList::languages( 380 | $locale, 381 | $this->config->get('app.fallback_locale') 382 | ); 383 | 384 | // Languages are filtered to match countries 385 | foreach ($countries as $code => $country) { 386 | if (! $countryLocale = $this->countryLocale($code)) { 387 | continue; 388 | } 389 | $language = $this->localeLanguage($countryLocale); 390 | if (! isset($languages[$language])) { 391 | continue; 392 | } 393 | $this->languages[$language] = $languages[$language]; 394 | } 395 | 396 | asort($this->languages); 397 | $this->appLocale = $locale; 398 | } 399 | 400 | return $this->languages; 401 | } 402 | 403 | /** 404 | * Get language name. 405 | * 406 | * @param string $code 407 | * @param string $locale 408 | * @return string|null 409 | */ 410 | public function language($code, $locale = null) 411 | { 412 | if (! $locale) { 413 | $locale = $this->config->get('app.locale'); 414 | } 415 | 416 | if (! $this->languages || $this->appLocale !== $locale) { 417 | $this->languages($locale); 418 | } 419 | 420 | return isset($this->languages[$code]) ? $this->languages[$code] : null; 421 | } 422 | 423 | /** 424 | * Get currencies with ISO 4217 codes. 425 | * 426 | * @param string $locale 427 | * @return array 428 | */ 429 | public function currencies($locale = null) 430 | { 431 | if (! $locale) { 432 | $locale = $this->config->get('app.locale'); 433 | } 434 | 435 | if (! $this->currencies || $this->appLocale !== $locale) { 436 | $countries = $this->countries($locale); 437 | $currencies = GeoList::currencies( 438 | $locale, 439 | $this->config->get('app.fallback_locale') 440 | ); 441 | 442 | // Add new Belarusian Ruble if not available 443 | if (! isset($currencies['BYN']) && isset($currencies['BYR'])) { 444 | $currencies['BYN'] = $currencies['BYR']; 445 | } 446 | 447 | // Add new Mauritanian Ouguiya if not available 448 | if (! isset($currencies['MRU']) && isset($currencies['MRO'])) { 449 | $currencies['MRU'] = $currencies['MRO']; 450 | } 451 | 452 | // Add new São Tomé & Príncipe Dobra if not available 453 | if (! isset($currencies['STN']) && isset($currencies['STD'])) { 454 | $currencies['STN'] = $currencies['STD']; 455 | } 456 | 457 | // Add new Bolívar Soberano if not available 458 | if (! isset($currencies['VES']) && isset($currencies['VEF'])) { 459 | $currencies['VES'] = $currencies['VEF']; 460 | } 461 | 462 | // Currencies are filtered to match countries 463 | foreach ($countries as $code => $country) { 464 | if (! $countryLocale = $this->countryLocale($code)) { 465 | continue; 466 | } 467 | if (! $currency = $this->localeCurrency($countryLocale)) { 468 | continue; 469 | } 470 | if (! isset($currencies[$currency])) { 471 | continue; 472 | } 473 | 474 | $this->currencies[$currency] = $currencies[$currency]; 475 | } 476 | 477 | asort($this->currencies); 478 | $this->appLocale = $locale; 479 | } 480 | 481 | return $this->currencies; 482 | } 483 | 484 | /** 485 | * Get currency name. 486 | * 487 | * @param string $code 488 | * @param string $locale 489 | * @return string|null 490 | */ 491 | public function currency($code, $locale = null) 492 | { 493 | if (! $locale) { 494 | $locale = $this->config->get('app.locale'); 495 | } 496 | 497 | if (! $this->currencies || $this->appLocale !== $locale) { 498 | $this->currencies($locale); 499 | } 500 | 501 | return isset($this->currencies[$code]) ? $this->currencies[$code] : null; 502 | } 503 | 504 | /** 505 | * Get default country locale. 506 | * 507 | * @param string $countryCode 508 | * @return string|null 509 | */ 510 | public function countryLocale($countryCode) 511 | { 512 | return isset(self::COUNTRY_LOCALE[$countryCode]) 513 | ? self::COUNTRY_LOCALE[$countryCode] 514 | : null; 515 | } 516 | 517 | /** 518 | * Get language code from country code. 519 | * 520 | * @param string $countryCode 521 | * @return string|null 522 | */ 523 | public function countryLanguage($countryCode) 524 | { 525 | if (! $locale = $this->countryLocale($countryCode)) { 526 | return null; 527 | } 528 | 529 | $language = $this->localeLanguage($locale); 530 | if (! $this->languages) { 531 | $this->languages($this->appLocale); 532 | } 533 | 534 | return isset($this->languages[$language]) ? $language : null; 535 | } 536 | 537 | /** 538 | * Get currency code from country code. 539 | * 540 | * @param string $countryCode 541 | * @return string|null 542 | */ 543 | public function countryCurrency($countryCode) 544 | { 545 | if (! $locale = $this->countryLocale($countryCode)) { 546 | return null; 547 | } 548 | if (! $currency = $this->localeCurrency($locale)) { 549 | return null; 550 | } 551 | 552 | if (! $this->currencies) { 553 | $this->currencies($this->appLocale); 554 | } 555 | 556 | return isset($this->currencies[$currency]) ? $currency : null; 557 | } 558 | 559 | /** 560 | * Get ISO 639-1 language code. 561 | * 562 | * @param string $locale 563 | */ 564 | protected function localeLanguage($locale) 565 | { 566 | list($language, $scriptOrCountry) = explode('_', $locale); 567 | 568 | return $language === 'zh' ? $language.'_'.$scriptOrCountry : $language; 569 | } 570 | 571 | /** 572 | * Get ISO 4217 currency code. 573 | * 574 | * @param string $locale 575 | * @return string|null 576 | */ 577 | protected function localeCurrency($locale) 578 | { 579 | $currency = (new NumberFormatter($locale, NumberFormatter::CURRENCY)) 580 | ->getTextAttribute(NumberFormatter::CURRENCY_CODE); 581 | 582 | // Belarusian Ruble is no longer 'BYR' 583 | if ($currency === 'BYR') { 584 | $currency = 'BYN'; 585 | } 586 | 587 | return (! $currency || $currency === 'XXX') ? null : $currency; 588 | } 589 | } 590 | -------------------------------------------------------------------------------- /src/Location/Location.php: -------------------------------------------------------------------------------- 1 | config = $config; 36 | } 37 | 38 | /** 39 | * Get DB reader. 40 | * 41 | * @param string $name 42 | * @return \MaxMind\Db\Reader|null 43 | * @throws \MaxMind\Db\Reader\InvalidDatabaseException 44 | */ 45 | protected function reader($name) 46 | { 47 | if (empty($this->readers[$name])) { 48 | $readers = $this->config->get('geo.location.maxmind'); 49 | if (empty($readers[$name]) 50 | || ! file_exists($readers[$name])) { 51 | return null; 52 | } 53 | 54 | $this->readers[$name] = new Reader($readers[$name]); 55 | } 56 | 57 | return $this->readers[$name]; 58 | } 59 | 60 | /** 61 | * Looks up the ip address in the DB reader. 62 | * 63 | * @param string $name 64 | * @param string $ip 65 | * @return array|null 66 | * @throws \InvalidArgumentException 67 | */ 68 | public function get($name, $ip) 69 | { 70 | if (! $reader = $this->reader($name)) { 71 | return null; 72 | } 73 | 74 | return $reader->get($ip); 75 | } 76 | 77 | /** 78 | * Close DB reader. 79 | * 80 | * @param string $name 81 | * @return void 82 | */ 83 | public function close($name) 84 | { 85 | if (isset($this->readers[$name])) { 86 | $this->readers[$name]->close(); 87 | unset($this->readers[$name]); 88 | } 89 | } 90 | 91 | /** 92 | * {@inheritdoc} 93 | */ 94 | public function ipCountry($ip) 95 | { 96 | $data = $this->get('country', $ip); 97 | 98 | if (! isset($data['country']['iso_code'])) { 99 | return null; 100 | } 101 | 102 | if (in_array($data['country']['iso_code'], self::FILTER)) { 103 | return null; 104 | } 105 | 106 | return $data['country']['iso_code']; 107 | } 108 | 109 | /** 110 | * Get city name from ip address. 111 | * 112 | * @param string $ip 113 | * @param string $locale 114 | * @return string|null 115 | */ 116 | public function city($ip, $locale = 'en') 117 | { 118 | $data = $this->get('city', $ip); 119 | 120 | if (! empty($data['city']['names'][$locale])) { 121 | return $data['city']['names'][$locale]; 122 | } elseif (! empty($data['city']['names']['en'])) { 123 | return $data['city']['names']['en']; 124 | } 125 | 126 | return null; 127 | } 128 | 129 | /** 130 | * Get ISP name from ip address. 131 | * 132 | * @param string $ip 133 | * @return string|null 134 | */ 135 | public function isp($ip) 136 | { 137 | $data = $this->get('isp', $ip); 138 | 139 | if (isset($data['isp'])) { 140 | return $data['isp']; 141 | } 142 | 143 | // Fallback to AS organization 144 | return isset($data['autonomous_system_organization']) 145 | ? $data['autonomous_system_organization'] 146 | : null; 147 | } 148 | 149 | /** 150 | * Get AS number from ip address. 151 | * 152 | * @param string $ip 153 | * @return int|null 154 | */ 155 | public function asn($ip) 156 | { 157 | $data = $this->get('isp', $ip); 158 | 159 | return isset($data['autonomous_system_number']) 160 | ? $data['autonomous_system_number'] 161 | : null; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/Money.php: -------------------------------------------------------------------------------- 1 | config = $config; 48 | $this->exchange = $exchange; 49 | } 50 | 51 | /** 52 | * Get or set exchange instance. 53 | * 54 | * @param \Money\Exchange $exchange 55 | * @return \Money\Exchange 56 | */ 57 | public function exchange(Exchange $exchange = null) 58 | { 59 | if ($exchange) { 60 | $this->exchange = $exchange; 61 | } 62 | 63 | return $this->exchange; 64 | } 65 | 66 | /** 67 | * Replace Exchange instance by FixedExchange. 68 | * 69 | * @param array $list 70 | * @return \Money\Exchange 71 | */ 72 | public function fixedExchange(array $list) 73 | { 74 | $this->exchange = new FixedExchange($list); 75 | 76 | return $this->exchange; 77 | } 78 | 79 | /** 80 | * Get Money ISO currencies. 81 | * 82 | * @return \Money\Currencies\ISOCurrencies 83 | */ 84 | public function currencies() 85 | { 86 | if (! $this->currencies) { 87 | $this->currencies = new ISOCurrencies(); 88 | } 89 | 90 | return $this->currencies; 91 | } 92 | 93 | /** 94 | * Convert money value to currency. 95 | * 96 | * @param \Money\Money $money 97 | * @param string|\Money\Currency $currency 98 | * @param int $roundingMode 99 | * @return \Money\Money 100 | * @throws MoneyException 101 | */ 102 | public function convert(MoneyValue $money, $currency, $roundingMode = MoneyValue::ROUND_HALF_UP) 103 | { 104 | if (! $this->converter) { 105 | $this->converter = new Converter( 106 | $this->currencies(), 107 | $this->exchange 108 | ); 109 | } 110 | try { 111 | return $this->converter->convert( 112 | $money, 113 | $this->currency($currency), 114 | $roundingMode 115 | ); 116 | } catch (\Exception $e) { 117 | throw new MoneyException('Money convert', $e); 118 | } 119 | } 120 | 121 | /** 122 | * Format money value using "Intl" formatter. 123 | * 124 | * @param \Money\Money $money 125 | * @param string $locale 126 | * @return string 127 | * @throws MoneyException 128 | */ 129 | public function format(MoneyValue $money, $locale = null) 130 | { 131 | try { 132 | if (! $locale) { 133 | $locale = $this->config->get('app.locale'); 134 | } 135 | return (new IntlMoneyFormatter( 136 | new NumberFormatter($locale, NumberFormatter::CURRENCY), 137 | $this->currencies() 138 | ))->format($money); 139 | } catch (\Exception $e) { 140 | throw new MoneyException('Money Intl format', $e); 141 | } 142 | } 143 | 144 | /** 145 | * Format money value using "Decimal" formatter. 146 | * 147 | * @param \Money\Money $money 148 | * @return string 149 | * @throws MoneyException 150 | */ 151 | public function formatDec(MoneyValue $money) 152 | { 153 | try { 154 | return (new DecimalMoneyFormatter($this->currencies())) 155 | ->format($money); 156 | } catch (\Exception $e) { 157 | throw new MoneyException('Money decimal format', $e); 158 | } 159 | } 160 | 161 | /** 162 | * Shortcut to create currency. 163 | * 164 | * @param string|\Money\Currency $code 165 | * @return \Money\Currency 166 | */ 167 | public function currency($code) 168 | { 169 | if (is_string($code)) { 170 | return new Currency($code); 171 | } 172 | 173 | return $code; 174 | } 175 | 176 | /** 177 | * Parse a decimal money amount for currency. 178 | * 179 | * @param string $amount 180 | * @param string $currency 181 | * @return \Money\Money 182 | * @throws MoneyException 183 | */ 184 | public function parse($amount, $currency) 185 | { 186 | try { 187 | return (new \Money\Parser\DecimalMoneyParser($this->currencies())) 188 | ->parse($amount, $this->currency($currency)); 189 | } catch (\Exception $e) { 190 | throw new MoneyException('Money parse', $e); 191 | } 192 | } 193 | 194 | /** 195 | * Shortcut to make money value. 196 | * Amount MUST be expressed in the smallest units of currency. 197 | * 198 | * @param int|string $amount 199 | * @param string|\Money\Currency $currency 200 | * @return \Money\Money 201 | * @throws MoneyException 202 | */ 203 | public function make($amount, $currency) 204 | { 205 | try { 206 | return new MoneyValue($amount, $this->currency($currency)); 207 | } catch (\Exception $e) { 208 | throw new MoneyException('Money make', $e); 209 | } 210 | } 211 | 212 | /** 213 | * Decompose a money value. 214 | * 215 | * @param \Money\Money $money 216 | * @param string $locale 217 | * @return array 218 | */ 219 | public function decompose(MoneyValue $money, $locale = null) 220 | { 221 | if (! $locale) { 222 | $locale = $this->config->get('app.locale'); 223 | } 224 | 225 | $formatter = new NumberFormatter($locale, NumberFormatter::CURRENCY); 226 | $subUnit = $this->currencies()->subunitFor($money->getCurrency()); 227 | $parts = explode('.', $this->formatDec($money->absolute())); 228 | 229 | $decomposed = [ 230 | 'locale' => $locale, 231 | 'subunit' => $subUnit, 232 | 'sign' => $money->isPositive() ? '+' : '-', 233 | 'unsigned_part' => $parts[0], 234 | 'decimal_part' => isset($parts[1]) ? $parts[1] : '', 235 | 'grouping_separator' => $formatter->getSymbol(NumberFormatter::GROUPING_SEPARATOR_SYMBOL), 236 | 'decimal_separator' => $subUnit > 0 ? $formatter->getSymbol(NumberFormatter::DECIMAL_SEPARATOR_SYMBOL) : '', 237 | ]; 238 | 239 | // Symbol workaround : https://github.com/moneyphp/money/issues/330#issuecomment-267295863 240 | 241 | // Prevent any extra spaces, etc. in formatted currency 242 | $formatter->setPattern('¤'); 243 | 244 | // Prevent significant digits (e.g. cents) in formatted currency 245 | $formatter->setAttribute(NumberFormatter::MAX_SIGNIFICANT_DIGITS, 0); 246 | 247 | // Get the formatted price for '0' 248 | $formattedPrice = $formatter->formatCurrency(0, $money->getCurrency()->getCode()); 249 | 250 | // Strip out the zero digit to get the currency symbol 251 | $zero = $formatter->getSymbol(NumberFormatter::ZERO_DIGIT_SYMBOL); 252 | $decomposed['symbol'] = str_replace($zero, '', $formattedPrice); 253 | 254 | return $decomposed; 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /src/MoneyException.php: -------------------------------------------------------------------------------- 1 | getMessage(), 11 | $previous->getCode() 12 | ); 13 | } 14 | } 15 | --------------------------------------------------------------------------------