diff --git a/app/util/ArgumentNames.php b/app/util/ArgumentNames.php new file mode 100644 index 0000000..d271b57 --- /dev/null +++ b/app/util/ArgumentNames.php @@ -0,0 +1,251 @@ + 'The user access role', + self::ADJUSTMENT_DATE_TIME => 'The adjustment date time', + self::ADJUSTMENT_TYPE => 'The adjustment type', + self::ADVERTISER_UPLOAD_DATE_TIME => 'The advertiser upload date time', + self::AD_ID => 'The ad ID', + self::AD_GROUP_ID => 'The ad group ID', + self::AD_GROUP_IDS => 'The ad group IDs', + self::AD_PERSONALIZATION_CONSENT => 'The ad personalization consent, e.g., GRANTED', + self::AD_USER_DATA_CONSENT => 'The ad user data consent, e.g., GRANTED', + self::ASSET_GROUP_ID => 'The asset group ID', + self::ATTRIBUTE_VALUE => 'The attribute value', + self::AUDIENCE_ID => 'The audience ID', + self::BASE_CAMPAIGN_ID => 'The base campaign ID', + self::BID_MODIFIER_VALUE => 'The bid modifier value', + self::BILLING_SETUP_ID => 'The billing setup ID', + self::BRIDGE_MAP_VERSION_ID + => 'The version of partner IDs to be used for store-sale uploads', + self::BUSINESS_ACCOUNT_IDENTIFIER => 'The account number of the Business Profile account', + self::BUSINESS_PROFILE_LOCATION => 'The Business Profile location resource name', + self::BUSINESS_NAME => 'The Business Profile business name', + self::CALL_START_DATE_TIME => 'The call start date time', + self::CALLER_ID => 'The caller ID', + self::CALLOUT_TEXT => 'The callout text', + self::CAMPAIGN_BUDGET_ID => 'The campaign budget ID', + self::CAMPAIGN_EXPERIMENT_ID => 'The campaign experiment ID', + self::CAMPAIGN_ID => 'The campaign ID', + self::CAMPAIGN_IDS => 'The campaign IDs', + self::CARRIER_COUNTRY_CODE => 'The carrier country code', + self::CHAIN_ID => 'The retail chain ID', + self::CHECK_IN_DAY_CRITERION_ID => 'The hotel check-in day criterion ID', + self::CONVERSION_ACTION_ID => 'The conversion action ID', + self::CONVERSION_ACTION_IDS => 'The conversion action IDs', + self::CONVERSION_CUSTOM_VARIABLE_ID => 'The conversion custom variable ID', + self::CONVERSION_CUSTOM_VARIABLE_VALUE => 'The conversion custom variable value', + self::CONVERSION_DATE_TIME => 'The conversion date time', + self::CONVERSION_RATE_MODIFIER => 'The conversion rate modifier', + self::CONVERSION_VALUE => 'The conversion value', + self::COUNTRY_CODE => 'The country code', + self::CPC_BID_CEILING_MICRO_AMOUNT => 'The CPC bid ceiling micro amount', + self::CPC_BID_MICRO_AMOUNT => 'The CPC bid micro amount', + self::CRITERION_ID => 'The criterion ID', + self::CURRENCY_CODE => 'The currency code', + self::CUSTOMER_ID => 'The customer ID without dashes', + self::CUSTOMIZER_ATTRIBUTE_NAME => 'The customizer attribute name', + self::CUSTOM_KEY => 'The custom key', + self::DRAFT_ID => 'The draft ID', + self::EMAIL_ADDRESS => 'The email address', + self::END_DATE_TIME => 'The end date time', + self::EXTERNAL_ID => 'The external ID', + self::FEED_ID => 'The feed ID', + self::FEED_ITEM_ID => 'The feed item ID', + self::FEED_ITEM_IDS => 'The feed item IDs', + self::FEED_ITEM_SET_ID => 'The feed item set ID', + self::FINAL_URL => 'The final URL', + self::FLIGHT_PLACEHOLDER_FIELD_NAME => 'The flight placeholder field name', + self::FREE_FORM_KEYWORD_TEXT => 'The free-form keyword text', + self::GBRAID => 'The GBRAID identifier for an iOS app conversion', + self::GCLID => 'The Google Click ID', + self::GEO_TARGET_CONSTANT_ID => 'The geo target constant ID', + self::BUSINESS_PROFILE_ACCESS_TOKEN => 'The access token used for uploading Business Profile ' + . 'location feed data', + self::BUSINESS_PROFILE_EMAIL => 'The email address associated with the Business Profile account', + self::HOTEL_CENTER_ACCOUNT_ID => 'The hotel center account ID', + self::IMAGE_ASSET_ID => 'The image asset ID', + self::ITEM_ID => 'The item ID', + self::KEYWORD_PLAN_ID => 'The keyword plan ID', + self::KEYWORD_TEXT => 'The keyword text', + self::KEYWORD_TEXTS => 'The list of keyword texts', + self::LABEL_ID => 'The label ID', + self::LANGUAGE_CODE => 'The language code', + self::LANGUAGE_ID => 'The language ID', + self::LANGUAGE_NAME => 'The language name', + self::LOCALE => 'The locale', + self::LOCATION_ID => 'The location ID', + self::LOCATION_IDS => 'The list of location IDs', + self::LOCATION_NAMES => 'The list of location names', + self::LOGIN_CUSTOMER_ID => 'The login customer ID', + self::MANAGER_CUSTOMER_ID => 'The manager customer ID', + self::MARKETING_IMAGE_ASSET_ID => 'The ID of marketing image asset', + self::MERCHANT_CENTER_ACCOUNT_ID => 'The Merchant center account ID', + self::NAME_PREFIX => 'The name prefix', + self::OFFLINE_USER_DATA_JOB_ID => 'The offline user data job ID', + self::OFFLINE_USER_DATA_JOB_TYPE => 'The offline user data job type', + self::OMIT_UNSELECTED_RESOURCE_NAMES => 'Whether to omit unselected resource names', + self::ORDER_ID => 'The order ID', + self::OUTPUT_FILE_PATH => 'The output file path', + self::PAGE_URL => 'The page URL', + self::PARTNER_ID => 'The partner ID', + self::PAYMENTS_ACCOUNT_ID => 'The payments account ID', + self::PAYMENTS_PROFILE_ID => 'The payments profile ID', + self::PERCENT_CPC_BID_MICRO_AMOUNT => + 'The CPC bid micro amount for the Percent CPC bidding strategy', + self::PHONE_COUNTRY => 'The phone country', + self::PHONE_NUMBER => 'The phone number', + self::PLACE_ID => 'The place ID', + self::RECOMMENDATION_ID => 'The recommendation ID', + self::RESTATEMENT_VALUE => 'The restatement value', + self::CREATE_DEFAULT_LISTING_GROUP => + 'Whether it should create a default listing group', + self::DELETE_EXISTING_FEEDS => + 'Whether it should delete the existing feeds', + self::REPLACE_EXISTING_TREE => + 'Whether it should replace the existing listing group tree on an ad group/asset group', + self::QUANTITY => 'The quantity', + self::RUN_JOB => 'Whether it should run the offline user data job', + self::SALES_COUNTRY => 'The sales country', + self::SITELINK_TEXT => 'The sitelink text', + self::SQUARE_MARKETING_IMAGE_ASSET_ID => 'The ID of square marketing image asset', + self::START_DATE_TIME => 'The start date time', + self::USER_AGENT => 'The user agent', + self::USER_LIST_ID => 'The user list ID', + self::USER_LIST_IDS => 'The user list IDs', + self::THINGS_TO_DO_CENTER_ACCOUNT_ID => 'The Things to Do Center account ID', + self::WBRAID => 'The WBRAID identifer for an iOS web conversion' + ]; +} diff --git a/app/util/ArgumentParser.php b/app/util/ArgumentParser.php new file mode 100644 index 0000000..7d3fe57 --- /dev/null +++ b/app/util/ArgumentParser.php @@ -0,0 +1,110 @@ +addOption(['h', 'help', GetOpt::NO_ARGUMENT, 'Show this help and quit']); + foreach ($argumentNames as $argumentName => $argumentType) { + $normalizedOptions[$argumentName] = null; + // Adds an option for an argument using a long option name only. + $getOpt->addOption( + [ + null, + $argumentName, + $argumentType, + ArgumentNames::$ARGUMENTS_TO_DESCRIPTIONS[$argumentName] + ] + ); + + if ($argumentType === GetOpt::REQUIRED_ARGUMENT) { + $numRequiredArguments++; + } + } + + // Parse arguments and catch exceptions. + try { + $getOpt->process(); + } catch (ArgumentException $exception) { + // When there are any errors regarding arguments, such as invalid argument names, or + // specifying required arguments but not providing values, 'ArgumentException' will + // be thrown. Show the help text in these cases. + echo PHP_EOL . $getOpt->getHelpText(); + throw $exception; + } + // Show help text when requested. + if (!is_null($getOpt->getOption('help'))) { + $this->printHelpMessageAndExit($getOpt); + // Help text is printed, so no arguments are passed. The below line is reached only + // in tests. + return []; + } + + $numPassedRequiredArguments = 0; + foreach ($getOpt->getOptions() as $optionName => $optionValue) { + if ($argumentNames[$optionName] === GetOpt::REQUIRED_ARGUMENT) { + $numPassedRequiredArguments++; + } + $normalizedOptions[$optionName] = $optionValue; + } + // Don't allow the case when optional arguments are passed, but required arguments are not. + if ( + count($getOpt->getOptions()) > 0 + && $numPassedRequiredArguments !== $numRequiredArguments + ) { + echo PHP_EOL . $getOpt->getHelpText(); + throw new InvalidArgumentException( + 'All required arguments must be specified.' . PHP_EOL + ); + } + return $normalizedOptions; + } + + /** + * Print the help message and exit the program. + * + * @param GetOpt $getOpt the GetOpt object to print its help text + */ + public function printHelpMessageAndExit(GetOpt $getOpt) + { + echo PHP_EOL . $getOpt->getHelpText(); + exit; + } +} diff --git a/app/util/Feeds.php b/app/util/Feeds.php new file mode 100644 index 0000000..500d5d6 --- /dev/null +++ b/app/util/Feeds.php @@ -0,0 +1,211 @@ +getGoogleAdsServiceClient(); + // Constructs the query to get the feed item with attribute values. + $query = "SELECT feed_item.attribute_values FROM feed_item" + . " WHERE feed_item.resource_name = '$feedItemResourceName'"; + // Issues a search request. + $response = + $googleAdsServiceClient->search(SearchGoogleAdsRequest::build($customerId, $query)); + + // Returns the feed item attribute values, which belongs to the first item. We can ensure + // it belongs to the first one because we specified the feed item resource name in the + // query. + return $response->getIterator()->current()->getFeedItem(); + } + + /** + * Gets the index of the target feed item attribute value. This is needed to specify which feed + * item attribute value will be updated in the given feed item. + * + * @param FeedItemAttributeValue $targetFeedItemAttributeValue the new feed item attribute value + * that will be updated + * @param FeedItem $feedItem the feed item that will be updated. It should be populated with + * the current attribute values + * @return int the attribute index + */ + public static function attributeIndexFor( + FeedItemAttributeValue $targetFeedItemAttributeValue, + FeedItem $feedItem + ) { + $attributeIndex = -1; + // Loops through attribute values to find the index of the feed item attribute value to + // update. + foreach ($feedItem->getAttributeValues() as $feedItemAttributeValue) { + /** @var FeedItemAttributeValue $feedItemAttributeValue */ + $attributeIndex++; + // Checks if the current feedItemAttributeValue is the one we are updating + if ( + $feedItemAttributeValue->getFeedAttributeId() + === $targetFeedItemAttributeValue->getFeedAttributeId() + ) { + break; + } + } + + if ($attributeIndex === -1) { + throw new \InvalidArgumentException( + 'No matching feed attribute for feed item attribute ID: ' + . $targetFeedItemAttributeValue->getFeedAttributeId() + ); + } + + return $attributeIndex; + } + + /** + * Retrieves the place holder fields to feed attributes map for a flights feed. + * See FlightPlaceholderField.php for all available placeholder field values. + * + * @see Feeds::placeholderFieldsMapFor() + * + * @param string $feedResourceName the feed resource name to get the attributes from + * @param int $customerId the customer ID + * @param GoogleAdsClient $googleAdsClient the Google Ads API client + * @return array the map from placeholder fields to feed attributes + */ + public static function flightPlaceholderFieldsMapFor( + string $feedResourceName, + int $customerId, + GoogleAdsClient $googleAdsClient + ) { + return self::placeholderFieldsMapFor( + $feedResourceName, + $customerId, + $googleAdsClient, + [ + 'Flight Description' => FlightPlaceholderField::FLIGHT_DESCRIPTION, + 'Destination ID' => FlightPlaceholderField::DESTINATION_ID, + 'Flight Price' => FlightPlaceholderField::FLIGHT_PRICE, + 'Flight Sale Price' => FlightPlaceholderField::FLIGHT_SALE_PRICE, + 'Final URLs' => FlightPlaceholderField::FINAL_URLS + ] + ); + } + + /** + * Retrieves the place holder fields to feed attributes map for a real estate feed. + * See RealEstatePlaceholderField.php for all available placeholder field values. + * + * @see Feeds::placeholderFieldsMapFor() + * + * @param string $feedResourceName the feed resource name to get the attributes from + * @param int $customerId the customer ID + * @param GoogleAdsClient $googleAdsClient the Google Ads API client + * @return array the map from placeholder fields to feed attributes + */ + // [START add_real_estate_feed] + public static function realEstatePlaceholderFieldsMapFor( + string $feedResourceName, + int $customerId, + GoogleAdsClient $googleAdsClient + ) { + return self::placeholderFieldsMapFor( + $feedResourceName, + $customerId, + $googleAdsClient, + [ + 'Listing ID' => RealEstatePlaceholderField::LISTING_ID, + 'Listing Name' => RealEstatePlaceholderField::LISTING_NAME, + 'Final URLs' => RealEstatePlaceholderField::FINAL_URLS, + 'Image URL' => RealEstatePlaceholderField::IMAGE_URL, + 'Contextual Keywords' => RealEstatePlaceholderField::CONTEXTUAL_KEYWORDS + ] + ); + } + // [END add_real_estate_feed] + + /** + * Retrieves the placeholder fields to feed attributes map for a feed. The initial + * query retrieves the feed attributes, or columns, of the feed. Each feed attribute will also + * include the feed attribute ID, which will be used in a subsequent step. + * + * Then a map is created for the feed attributes (columns) and returned: + * - The keys are the placeholder types that the columns will be. + * - The values are the feed attributes. + * + * @param string $feedResourceName the feed resource name to get the attributes from + * @param int $customerId the customer ID + * @param GoogleAdsClient $googleAdsClient the Google Ads API client + * @param array $feedAttributeNamesMap the associative array mapping from feed attribute names + * to placeholder fields + * @return array the map from placeholder fields to feed attributes + */ + private static function placeholderFieldsMapFor( + string $feedResourceName, + int $customerId, + GoogleAdsClient $googleAdsClient, + array $feedAttributeNamesMap + ) { + $googleAdsServiceClient = $googleAdsClient->getGoogleAdsServiceClient(); + // Constructs the query to get the feed attributes for the specified feed resource name. + $query = "SELECT feed.attributes FROM feed WHERE feed.resource_name = '$feedResourceName'"; + // Issues a search request. + $response = + $googleAdsServiceClient->search(SearchGoogleAdsRequest::build($customerId, $query)); + + // Gets the first result because we only need the single feed we created previously. + /** @var GoogleAdsRow $googleAdsRow */ + $googleAdsRow = $response->getIterator()->current(); + + // Gets the attributes list from the feed and creates a map with keys of placeholder fields + // and values of feed attributes. + $feedAttributes = + iterator_to_array($googleAdsRow->getFeed()->getAttributes()->getIterator()); + $placeholderFields = array_map( + function (FeedAttribute $feedAttribute) use ($feedAttributeNamesMap) { + if (!array_key_exists($feedAttribute->getName(), $feedAttributeNamesMap)) { + throw new \RuntimeException('Invalid feed attribute name.'); + } + return $feedAttributeNamesMap[$feedAttribute->getName()]; + }, + $feedAttributes + ); + return array_combine($placeholderFields, $feedAttributes); + } +} diff --git a/app/util/Helper.php b/app/util/Helper.php new file mode 100644 index 0000000..8b9f478 --- /dev/null +++ b/app/util/Helper.php @@ -0,0 +1,67 @@ +format("Y-m-d\TH:i:s.vP"); + } + + /** + * Generates a short printable string for the current date and time in local time zone. + * @return string the result string + */ + public static function getShortPrintableDatetime(): string + { + return (new DateTime())->format("mdHisv"); + } + + /** + * Converts an amount from the micro unit to the base unit. + * + * @param int|float|null $amount the amount in micro unit + * @return float the amount converted to the base unit if not null otherwise 0 + */ + public static function microToBase($amount): float + { + return $amount ? $amount / 1000000.0 : 0.0; + } + + /** + * Converts an amount from the base unit to the micro unit. + * + * @param float|int|null $amount the amount in base unit + * @return int the amount converted to the micro unit if not null otherwise 0 + */ + public static function baseToMicro($amount): int + { + return $amount ? (int) ($amount * 1000000) : 0; + } +} diff --git a/config/plugin/webman/auto-route/app.php b/config/plugin/webman/auto-route/app.php new file mode 100644 index 0000000..4717d7f --- /dev/null +++ b/config/plugin/webman/auto-route/app.php @@ -0,0 +1,5 @@ + false, + 'default_app' => '', //默认应用,如需开启请填写默认应用的名称 +]; \ No newline at end of file diff --git a/config/plugin/webman/auto-route/route.php b/config/plugin/webman/auto-route/route.php new file mode 100644 index 0000000..53d1a5c --- /dev/null +++ b/config/plugin/webman/auto-route/route.php @@ -0,0 +1,109 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +use Webman\Route; + +// 已经设置过路由的uri则忽略 +$routes = Route::getRoutes(); +$ignore_list = []; +foreach ($routes as $tmp_route) { + $ignore_list[$tmp_route->getPath()] = 0; +} + +$default_app = config('plugin.webman.auto-route.app.default_app'); + +$suffix = config('app.controller_suffix', ''); +$suffix_length = strlen($suffix); + +// 递归遍历目录查找控制器自动设置路由 +$dir_iterator = new \RecursiveDirectoryIterator(app_path()); +$iterator = new \RecursiveIteratorIterator($dir_iterator); +foreach ($iterator as $file) { + // 忽略目录和非php文件 + if (is_dir($file) || $file->getExtension() != 'php') { + continue; + } + + $file_path = str_replace('\\', '/',$file->getPathname()); + // 文件路径里不带controller的文件忽略 + if (strpos(strtolower($file_path), '/controller/') === false) { + continue; + } + + // 只处理带 controller_suffix 后缀的 + if ($suffix_length && substr($file->getBaseName('.php'), -$suffix_length) !== $suffix) { + continue; + } + + // 根据文件路径计算uri + $uri_path = str_replace(['/controller/', '/Controller/'], '/', substr(substr($file_path, strlen(app_path())), 0, - (4 + $suffix_length))); + + // 默认应用 + $is_default_app = false; + if (is_string($default_app) && !empty($default_app)) { + $seg = explode('/', $uri_path); + if ($seg[1] == $default_app) { + $uri_path = str_replace($default_app . '/', '', $uri_path); + $is_default_app = true; + } + } + + // 根据文件路径是被类名 + $class_name = str_replace('/', '\\',substr(substr($file_path, strlen(base_path())), 0, -4)); + + if (!class_exists($class_name)) { + echo "Class $class_name not found, skip route for it\n"; + continue; + } + + // 通过反射找到这个类的所有共有方法作为action + $class = new ReflectionClass($class_name); + $class_name = $class->name; + $methods = $class->getMethods(ReflectionMethod::IS_PUBLIC); + + $route = function ($uri, $cb) use ($ignore_list) { + if (isset($ignore_list[strtolower($uri)])) { + return; + } + Route::any($uri, $cb); + if ($uri !== '') { + Route::any($uri . '/', $cb); + } + $lower_uri = strtolower($uri); + if ($lower_uri !== $uri) { + Route::any($lower_uri, $cb); + Route::any($lower_uri . '/', $cb); + } + }; + + // 设置路由 + foreach ($methods as $item) { + $action = $item->name; + if (in_array($action, ['__construct', '__destruct'])) { + continue; + } + // action为index时uri里末尾/index可以省略 + if ($action === 'index') { + // controller也为index时uri里可以省略/index/index + if (strtolower(substr($uri_path, -6)) === '/index') { + if ($is_default_app) { + $route('/', [$class_name, $action]); + } + $route(substr($uri_path, 0, -6), [$class_name, $action]); + } + $route($uri_path, [$class_name, $action]); + } + $route($uri_path.'/'.$action, [$class_name, $action]); + } +}